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; } }