header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.index')); } $dashboard = $this->quotationService->getDashboardStats(); $statuses = AiQuotation::getStatuses(); return view('rd.index', compact('dashboard', 'statuses')); } /** * 조직도 관리 */ public function orgChart(Request $request): View|\Illuminate\Http\Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.org-chart')); } $tenantId = session('selected_tenant_id'); // 부서 트리 (parent_id=null이 최상위) $departments = Department::where('tenant_id', $tenantId) ->where('is_active', true) ->orderBy('sort_order') ->orderBy('name') ->get(); // 전체 직원 (활성 상태) $rawEmployees = Employee::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('employee_status', 'active') ->with(['user', 'department']) ->orderBy('display_name') ->get(); // Blade @json 호환을 위해 미리 배열로 변환 $employees = $rawEmployees->map(function ($e) { return [ '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 = Tenant::find($tenantId); $companyName = $tenant->company_name ?? 'SAM'; $ceoName = $tenant->ceo_name ?? ''; return view('rd.org-chart', compact('departments', 'employees', 'companyName', 'ceoName')); } /** * 조직도 - 직원 부서 배치 */ public function orgChartAssign(Request $request): JsonResponse { $request->validate([ 'employee_id' => 'required|integer', 'department_id' => 'required|integer', ]); $tenantId = session('selected_tenant_id'); $employee = Employee::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('id', $request->employee_id) ->first(); if (! $employee) { return response()->json(['success' => false, 'message' => '직원을 찾을 수 없습니다.'], 404); } $employee->department_id = $request->department_id; $employee->save(); return response()->json(['success' => true]); } /** * 조직도 - 직원 부서 해제 (미배치로 이동) */ public function orgChartUnassign(Request $request): JsonResponse { $request->validate([ 'employee_id' => 'required|integer', ]); $tenantId = session('selected_tenant_id'); $employee = Employee::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('id', $request->employee_id) ->first(); if (! $employee) { return response()->json(['success' => false, 'message' => '직원을 찾을 수 없습니다.'], 404); } $employee->department_id = null; $employee->save(); return response()->json(['success' => true]); } /** * 조직도 - 부서 내 직원 순서/이동 일괄 처리 */ public function orgChartReorder(Request $request): JsonResponse { $request->validate([ 'moves' => 'required|array', 'moves.*.employee_id' => 'required|integer', 'moves.*.department_id' => 'nullable|integer', ]); $tenantId = session('selected_tenant_id'); foreach ($request->moves as $move) { Employee::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('id', $move['employee_id']) ->update(['department_id' => $move['department_id']]); } return response()->json(['success' => true]); } /** * 조직도 - 부서 순서 변경 (드래그 앤 드롭) */ public function orgChartReorderDepts(Request $request): JsonResponse { $request->validate([ 'orders' => 'required|array', 'orders.*.id' => 'required|integer', 'orders.*.parent_id' => 'nullable|integer', 'orders.*.sort_order' => 'required|integer', ]); $tenantId = session('selected_tenant_id'); foreach ($request->orders as $order) { Department::where('tenant_id', $tenantId) ->where('id', $order['id']) ->update([ 'parent_id' => $order['parent_id'], 'sort_order' => $order['sort_order'], ]); } return response()->json(['success' => true]); } /** * 조직도 - 부서 숨기기/표시 토글 */ public function orgChartToggleHide(Request $request): JsonResponse { $request->validate([ 'department_id' => 'required|integer', 'hidden' => 'required|boolean', ]); $tenantId = session('selected_tenant_id'); $dept = Department::where('tenant_id', $tenantId) ->where('id', $request->department_id) ->first(); if (! $dept) { return response()->json(['success' => false, 'message' => '부서를 찾을 수 없습니다.'], 404); } $options = $dept->options ?? []; $options['orgchart_hidden'] = $request->hidden; $dept->options = $options; $dept->save(); return response()->json(['success' => true]); } /** * 중대재해처벌법 실무 점검 */ public function safetyAudit(Request $request): View|\Illuminate\Http\Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.safety-audit')); } return view('rd.safety-audit'); } /** * AI 견적 목록 */ public function quotations(Request $request): View|\Illuminate\Http\Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.index')); } $statuses = AiQuotation::getStatuses(); return view('rd.ai-quotation.index', compact('statuses')); } /** * AI 견적 생성 폼 */ public function createQuotation(Request $request): View|\Illuminate\Http\Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.create')); } return view('rd.ai-quotation.create'); } /** * AI 견적 문서 (인쇄용 견적서) */ public function documentQuotation(Request $request, int $id): View { $quotation = $this->quotationService->getById($id); if (! $quotation || ! $quotation->isCompleted()) { abort(404, '완료된 견적만 문서로 조회할 수 있습니다.'); } $template = $request->query('template', 'classic'); $allowed = ['classic', 'modern', 'blue', 'dark', 'colorful']; if (! in_array($template, $allowed)) { $template = 'classic'; } return view('rd.ai-quotation.document', compact('quotation', 'template')); } /** * AI 견적 상세 */ public function showQuotation(Request $request, int $id): View|\Illuminate\Http\Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.show', $id)); } $quotation = $this->quotationService->getById($id); if (! $quotation) { abort(404, 'AI 견적을 찾을 수 없습니다.'); } return view('rd.ai-quotation.show', compact('quotation')); } /** * AI 견적 편집 (제조 모드) */ public function editQuotation(Request $request, int $id): View|\Illuminate\Http\Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.edit', $id)); } $quotation = $this->quotationService->getById($id); if (! $quotation) { abort(404, 'AI 견적을 찾을 수 없습니다.'); } if (! $quotation->isCompleted()) { abort(403, '완료된 견적만 편집할 수 있습니다.'); } return view('rd.ai-quotation.edit', compact('quotation')); } /** * 기획디자인 - 플래닝 캔버스 */ public function planningDesign(Request $request): View|\Illuminate\Http\Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.planning-design')); } return view('rd.planning-design.index'); } /** * 디자인 인사이트 - UI/UX 연구 도구 */ public function designInsight(Request $request): View|\Illuminate\Http\Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.design-insight')); } return view('rd.design-insight.index'); } /** * 사운드 로고 생성기 */ public function soundLogo(Request $request): View|\Illuminate\Http\Response { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('rd.sound-logo')); } return view('rd.sound-logo.index'); } }