309 lines
9.4 KiB
PHP
309 lines
9.4 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\Tenants\Department;
|
||
|
|
use App\Models\Tenants\TenantUserProfile;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
class OrgChartService extends Service
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* 조직도 전체 조회 (부서 트리 + 직원 + 통계)
|
||
|
|
*/
|
||
|
|
public function getOrgChart(array $params): array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
$includeHidden = filter_var($params['include_hidden'] ?? true, FILTER_VALIDATE_BOOLEAN);
|
||
|
|
|
||
|
|
// 부서 전체 조회 (트리 구성을 위해 flat으로 가져옴)
|
||
|
|
$deptQuery = Department::where('tenant_id', $tenantId)
|
||
|
|
->where('is_active', true);
|
||
|
|
|
||
|
|
if (! $includeHidden) {
|
||
|
|
$deptQuery->where(function ($q) {
|
||
|
|
$q->whereNull('options->orgchart_hidden')
|
||
|
|
->orWhere('options->orgchart_hidden', false);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
$departments = $deptQuery
|
||
|
|
->orderBy('sort_order')
|
||
|
|
->orderBy('name')
|
||
|
|
->get();
|
||
|
|
|
||
|
|
// 전체 활성 직원
|
||
|
|
$employees = TenantUserProfile::where('tenant_id', $tenantId)
|
||
|
|
->active()
|
||
|
|
->with(['user:id,name,email'])
|
||
|
|
->orderBy('display_name')
|
||
|
|
->get()
|
||
|
|
->map(fn ($e) => [
|
||
|
|
'id' => $e->id,
|
||
|
|
'user_id' => $e->user_id,
|
||
|
|
'department_id' => $e->department_id,
|
||
|
|
'display_name' => $e->display_name ?? $e->user?->name ?? '(이름없음)',
|
||
|
|
'position_label' => $e->position_label,
|
||
|
|
])
|
||
|
|
->values();
|
||
|
|
|
||
|
|
// 회사 정보
|
||
|
|
$tenant = \App\Models\Tenants\Tenant::find($tenantId);
|
||
|
|
|
||
|
|
// 통계
|
||
|
|
$total = $employees->count();
|
||
|
|
$assigned = $employees->whereNotNull('department_id')->count();
|
||
|
|
|
||
|
|
// 부서 트리 구성
|
||
|
|
$deptTree = $this->buildTree($departments, $employees);
|
||
|
|
|
||
|
|
// 숨겨진 부서 목록 (include_hidden=true일 때만 의미)
|
||
|
|
$hiddenDepts = $departments->filter(fn ($d) => ($d->options['orgchart_hidden'] ?? false) === true)
|
||
|
|
->map(fn ($d) => ['id' => $d->id, 'name' => $d->name, 'code' => $d->code])
|
||
|
|
->values();
|
||
|
|
|
||
|
|
return [
|
||
|
|
'company' => [
|
||
|
|
'name' => $tenant->company_name ?? 'SAM',
|
||
|
|
'ceo_name' => $tenant->ceo_name ?? '',
|
||
|
|
],
|
||
|
|
'departments' => $deptTree,
|
||
|
|
'hidden_departments' => $hiddenDepts,
|
||
|
|
'unassigned' => $employees->whereNull('department_id')->values(),
|
||
|
|
'stats' => [
|
||
|
|
'total' => $total,
|
||
|
|
'assigned' => $assigned,
|
||
|
|
'unassigned' => $total - $assigned,
|
||
|
|
],
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 조직도 통계
|
||
|
|
*/
|
||
|
|
public function getStats(): array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
$total = TenantUserProfile::where('tenant_id', $tenantId)->active()->count();
|
||
|
|
$assigned = TenantUserProfile::where('tenant_id', $tenantId)->active()->whereNotNull('department_id')->count();
|
||
|
|
|
||
|
|
return [
|
||
|
|
'total' => $total,
|
||
|
|
'assigned' => $assigned,
|
||
|
|
'unassigned' => $total - $assigned,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 미배치 직원 목록
|
||
|
|
*/
|
||
|
|
public function getUnassigned(array $params): array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
$query = TenantUserProfile::where('tenant_id', $tenantId)
|
||
|
|
->active()
|
||
|
|
->whereNull('department_id')
|
||
|
|
->with(['user:id,name,email'])
|
||
|
|
->orderBy('display_name');
|
||
|
|
|
||
|
|
if (! empty($params['q'])) {
|
||
|
|
$q = $params['q'];
|
||
|
|
$query->where(function ($w) use ($q) {
|
||
|
|
$w->where('display_name', 'like', "%{$q}%")
|
||
|
|
->orWhereHas('user', function ($u) use ($q) {
|
||
|
|
$u->where('name', 'like', "%{$q}%");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return $query->get()
|
||
|
|
->map(fn ($e) => [
|
||
|
|
'id' => $e->id,
|
||
|
|
'user_id' => $e->user_id,
|
||
|
|
'display_name' => $e->display_name ?? $e->user?->name ?? '(이름없음)',
|
||
|
|
'position_label' => $e->position_label,
|
||
|
|
])
|
||
|
|
->values()
|
||
|
|
->toArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 직원 부서 배치
|
||
|
|
*/
|
||
|
|
public function assign(array $params): array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
$employee = TenantUserProfile::where('tenant_id', $tenantId)
|
||
|
|
->where('id', $params['employee_id'])
|
||
|
|
->first();
|
||
|
|
|
||
|
|
if (! $employee) {
|
||
|
|
return ['error' => __('error.not_found'), 'code' => 404];
|
||
|
|
}
|
||
|
|
|
||
|
|
$dept = Department::where('tenant_id', $tenantId)
|
||
|
|
->where('id', $params['department_id'])
|
||
|
|
->first();
|
||
|
|
|
||
|
|
if (! $dept) {
|
||
|
|
return ['error' => __('error.not_found'), 'code' => 404];
|
||
|
|
}
|
||
|
|
|
||
|
|
$employee->department_id = $params['department_id'];
|
||
|
|
$employee->save();
|
||
|
|
|
||
|
|
return [
|
||
|
|
'employee_id' => $employee->id,
|
||
|
|
'department_id' => $params['department_id'],
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 직원 미배치 처리
|
||
|
|
*/
|
||
|
|
public function unassign(array $params): array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
$employee = TenantUserProfile::where('tenant_id', $tenantId)
|
||
|
|
->where('id', $params['employee_id'])
|
||
|
|
->first();
|
||
|
|
|
||
|
|
if (! $employee) {
|
||
|
|
return ['error' => __('error.not_found'), 'code' => 404];
|
||
|
|
}
|
||
|
|
|
||
|
|
$employee->department_id = null;
|
||
|
|
$employee->save();
|
||
|
|
|
||
|
|
return ['employee_id' => $employee->id, 'department_id' => null];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 직원 일괄 배치/이동
|
||
|
|
*/
|
||
|
|
public function reorderEmployees(array $params): array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
DB::transaction(function () use ($params, $tenantId) {
|
||
|
|
foreach ($params['moves'] as $move) {
|
||
|
|
TenantUserProfile::where('tenant_id', $tenantId)
|
||
|
|
->where('id', $move['employee_id'])
|
||
|
|
->update(['department_id' => $move['department_id']]);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return ['processed' => count($params['moves'])];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 부서 순서/계층 일괄 변경
|
||
|
|
*/
|
||
|
|
public function reorderDepartments(array $params): array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
// 순환 참조 검증
|
||
|
|
$orders = collect($params['orders']);
|
||
|
|
foreach ($orders as $order) {
|
||
|
|
if ($order['parent_id'] !== null && $order['parent_id'] === $order['id']) {
|
||
|
|
return ['error' => '자기 자신을 상위 부서로 설정할 수 없습니다.', 'code' => 422];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 순환 참조 심층 검증 (A→B→C→A 같은 케이스)
|
||
|
|
$parentMap = $orders->pluck('parent_id', 'id')->toArray();
|
||
|
|
foreach ($parentMap as $id => $parentId) {
|
||
|
|
if ($parentId === null) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$visited = [$id];
|
||
|
|
$current = $parentId;
|
||
|
|
while ($current !== null && isset($parentMap[$current])) {
|
||
|
|
if (in_array($current, $visited)) {
|
||
|
|
return ['error' => '순환 참조가 감지되었습니다.', 'code' => 422];
|
||
|
|
}
|
||
|
|
$visited[] = $current;
|
||
|
|
$current = $parentMap[$current];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
DB::transaction(function () use ($params, $tenantId) {
|
||
|
|
foreach ($params['orders'] as $order) {
|
||
|
|
Department::where('tenant_id', $tenantId)
|
||
|
|
->where('id', $order['id'])
|
||
|
|
->update([
|
||
|
|
'parent_id' => $order['parent_id'],
|
||
|
|
'sort_order' => $order['sort_order'],
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return ['processed' => count($params['orders'])];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 부서 숨기기/표시 토글
|
||
|
|
*/
|
||
|
|
public function toggleHide(int $departmentId, array $params): array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
$dept = Department::where('tenant_id', $tenantId)
|
||
|
|
->where('id', $departmentId)
|
||
|
|
->first();
|
||
|
|
|
||
|
|
if (! $dept) {
|
||
|
|
return ['error' => __('error.not_found'), 'code' => 404];
|
||
|
|
}
|
||
|
|
|
||
|
|
$options = $dept->options ?? [];
|
||
|
|
$options['orgchart_hidden'] = $params['hidden'];
|
||
|
|
$dept->options = $options;
|
||
|
|
$dept->save();
|
||
|
|
|
||
|
|
return [
|
||
|
|
'id' => $dept->id,
|
||
|
|
'orgchart_hidden' => $params['hidden'],
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* flat 부서 목록 → 트리 구조 변환
|
||
|
|
*/
|
||
|
|
private function buildTree($departments, $employees): array
|
||
|
|
{
|
||
|
|
$deptMap = [];
|
||
|
|
foreach ($departments as $dept) {
|
||
|
|
$deptMap[$dept->id] = [
|
||
|
|
'id' => $dept->id,
|
||
|
|
'name' => $dept->name,
|
||
|
|
'code' => $dept->code,
|
||
|
|
'parent_id' => $dept->parent_id,
|
||
|
|
'sort_order' => $dept->sort_order,
|
||
|
|
'is_active' => $dept->is_active,
|
||
|
|
'orgchart_hidden' => $dept->options['orgchart_hidden'] ?? false,
|
||
|
|
'children' => [],
|
||
|
|
'employees' => $employees->where('department_id', $dept->id)->values()->toArray(),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$tree = [];
|
||
|
|
foreach ($deptMap as $id => &$node) {
|
||
|
|
if ($node['parent_id'] === null || ! isset($deptMap[$node['parent_id']])) {
|
||
|
|
$tree[] = &$node;
|
||
|
|
} else {
|
||
|
|
$deptMap[$node['parent_id']]['children'][] = &$node;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
unset($node);
|
||
|
|
|
||
|
|
return $tree;
|
||
|
|
}
|
||
|
|
}
|