tenantId(); $page = (int) ($params['page'] ?? 1); $size = (int) ($params['size'] ?? 20); $q = trim((string) ($params['q'] ?? '')); $status = $params['status'] ?? null; $processType = $params['process_type'] ?? null; $assigneeId = $params['assignee_id'] ?? null; $teamId = $params['team_id'] ?? null; $scheduledFrom = $params['scheduled_from'] ?? null; $scheduledTo = $params['scheduled_to'] ?? null; $query = WorkOrder::query() ->where('tenant_id', $tenantId) ->with(['assignee:id,name', 'team:id,name', 'salesOrder:id,order_no']); // 검색어 if ($q !== '') { $query->where(function ($qq) use ($q) { $qq->where('work_order_no', 'like', "%{$q}%") ->orWhere('project_name', 'like', "%{$q}%"); }); } // 상태 필터 if ($status !== null) { $query->where('status', $status); } // 공정유형 필터 if ($processType !== null) { $query->where('process_type', $processType); } // 담당자 필터 if ($assigneeId !== null) { $query->where('assignee_id', $assigneeId); } // 팀 필터 if ($teamId !== null) { $query->where('team_id', $teamId); } // 예정일 범위 if ($scheduledFrom !== null) { $query->where('scheduled_date', '>=', $scheduledFrom); } if ($scheduledTo !== null) { $query->where('scheduled_date', '<=', $scheduledTo); } $query->orderByDesc('created_at'); return $query->paginate($size, ['*'], 'page', $page); } /** * 통계 조회 */ public function stats(): array { $tenantId = $this->tenantId(); $counts = WorkOrder::where('tenant_id', $tenantId) ->select('status', DB::raw('count(*) as count')) ->groupBy('status') ->pluck('count', 'status') ->toArray(); return [ 'total' => array_sum($counts), 'unassigned' => $counts[WorkOrder::STATUS_UNASSIGNED] ?? 0, 'pending' => $counts[WorkOrder::STATUS_PENDING] ?? 0, 'waiting' => $counts[WorkOrder::STATUS_WAITING] ?? 0, 'in_progress' => $counts[WorkOrder::STATUS_IN_PROGRESS] ?? 0, 'completed' => $counts[WorkOrder::STATUS_COMPLETED] ?? 0, 'shipped' => $counts[WorkOrder::STATUS_SHIPPED] ?? 0, ]; } /** * 단건 조회 */ public function show(int $id) { $tenantId = $this->tenantId(); $workOrder = WorkOrder::where('tenant_id', $tenantId) ->with([ 'assignee:id,name', 'team:id,name', 'salesOrder:id,order_no,project_name', 'items', 'bendingDetail', 'issues' => fn ($q) => $q->orderByDesc('created_at'), ]) ->find($id); if (! $workOrder) { throw new NotFoundHttpException(__('error.not_found')); } return $workOrder; } /** * 생성 */ public function store(array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 작업지시번호 자동 생성 $data['work_order_no'] = $this->generateWorkOrderNo($tenantId); $data['tenant_id'] = $tenantId; $data['created_by'] = $userId; $data['updated_by'] = $userId; // 담당자가 있으면 상태를 pending으로 if (! empty($data['assignee_id'])) { $data['status'] = $data['status'] ?? WorkOrder::STATUS_PENDING; } $items = $data['items'] ?? []; $bendingDetail = $data['bending_detail'] ?? null; unset($data['items'], $data['bending_detail']); $workOrder = WorkOrder::create($data); // 품목 저장 foreach ($items as $index => $item) { $item['sort_order'] = $index; $workOrder->items()->create($item); } // 벤딩 상세 저장 (벤딩 공정인 경우) if ($data['process_type'] === WorkOrder::PROCESS_BENDING && $bendingDetail) { $workOrder->bendingDetail()->create($bendingDetail); } return $workOrder->load(['assignee:id,name', 'team:id,name', 'items', 'bendingDetail']); }); } /** * 수정 */ public function update(int $id, array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $workOrder = WorkOrder::where('tenant_id', $tenantId)->find($id); if (! $workOrder) { throw new NotFoundHttpException(__('error.not_found')); } return DB::transaction(function () use ($workOrder, $data, $userId) { $data['updated_by'] = $userId; $items = $data['items'] ?? null; $bendingDetail = $data['bending_detail'] ?? null; unset($data['items'], $data['bending_detail'], $data['work_order_no']); // 번호 변경 불가 $workOrder->update($data); // 품목 교체 (있는 경우) if ($items !== null) { $workOrder->items()->delete(); foreach ($items as $index => $item) { $item['sort_order'] = $index; $workOrder->items()->create($item); } } // 벤딩 상세 업데이트 if ($bendingDetail !== null && $workOrder->process_type === WorkOrder::PROCESS_BENDING) { $workOrder->bendingDetail()->updateOrCreate( ['work_order_id' => $workOrder->id], $bendingDetail ); } return $workOrder->load(['assignee:id,name', 'team:id,name', 'items', 'bendingDetail']); }); } /** * 삭제 */ public function destroy(int $id) { $tenantId = $this->tenantId(); $workOrder = WorkOrder::where('tenant_id', $tenantId)->find($id); if (! $workOrder) { throw new NotFoundHttpException(__('error.not_found')); } // 진행 중이거나 완료된 작업은 삭제 불가 if (in_array($workOrder->status, [ WorkOrder::STATUS_IN_PROGRESS, WorkOrder::STATUS_COMPLETED, WorkOrder::STATUS_SHIPPED, ])) { throw new BadRequestHttpException(__('error.work_order.cannot_delete_in_progress')); } $workOrder->delete(); return 'success'; } /** * 상태 변경 */ public function updateStatus(int $id, string $status) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $workOrder = WorkOrder::where('tenant_id', $tenantId)->find($id); if (! $workOrder) { throw new NotFoundHttpException(__('error.not_found')); } // 상태 유효성 검증 if (! in_array($status, WorkOrder::STATUSES)) { throw new BadRequestHttpException(__('error.invalid_status')); } $workOrder->status = $status; $workOrder->updated_by = $userId; // 상태에 따른 타임스탬프 업데이트 switch ($status) { case WorkOrder::STATUS_IN_PROGRESS: $workOrder->started_at = $workOrder->started_at ?? now(); break; case WorkOrder::STATUS_COMPLETED: $workOrder->completed_at = now(); break; case WorkOrder::STATUS_SHIPPED: $workOrder->shipped_at = now(); break; } $workOrder->save(); return $workOrder->load(['assignee:id,name', 'team:id,name']); } /** * 담당자 배정 */ public function assign(int $id, array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $workOrder = WorkOrder::where('tenant_id', $tenantId)->find($id); if (! $workOrder) { throw new NotFoundHttpException(__('error.not_found')); } $workOrder->assignee_id = $data['assignee_id']; $workOrder->team_id = $data['team_id'] ?? $workOrder->team_id; $workOrder->updated_by = $userId; // 미배정이었으면 대기로 변경 if ($workOrder->status === WorkOrder::STATUS_UNASSIGNED) { $workOrder->status = WorkOrder::STATUS_PENDING; } $workOrder->save(); return $workOrder->load(['assignee:id,name', 'team:id,name']); } /** * 벤딩 항목 토글 */ public function toggleBendingField(int $id, string $field) { $tenantId = $this->tenantId(); $workOrder = WorkOrder::where('tenant_id', $tenantId)->find($id); if (! $workOrder) { throw new NotFoundHttpException(__('error.not_found')); } if ($workOrder->process_type !== WorkOrder::PROCESS_BENDING) { throw new BadRequestHttpException(__('error.work_order.not_bending_process')); } $detail = $workOrder->bendingDetail; if (! $detail) { $detail = $workOrder->bendingDetail()->create([]); } if (! in_array($field, WorkOrderBendingDetail::PROCESS_FIELDS)) { throw new BadRequestHttpException(__('error.invalid_field')); } $detail->toggleField($field); return $detail; } /** * 이슈 추가 */ public function addIssue(int $workOrderId, array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $workOrder = WorkOrder::where('tenant_id', $tenantId)->find($workOrderId); if (! $workOrder) { throw new NotFoundHttpException(__('error.not_found')); } $data['reported_by'] = $userId; return $workOrder->issues()->create($data); } /** * 이슈 해결 */ public function resolveIssue(int $workOrderId, int $issueId) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $workOrder = WorkOrder::where('tenant_id', $tenantId)->find($workOrderId); if (! $workOrder) { throw new NotFoundHttpException(__('error.not_found')); } $issue = $workOrder->issues()->find($issueId); if (! $issue) { throw new NotFoundHttpException(__('error.not_found')); } $issue->resolve($userId); return $issue; } /** * 작업지시번호 자동 생성 */ private function generateWorkOrderNo(int $tenantId): string { $prefix = 'WO'; $date = now()->format('Ymd'); // 오늘 날짜 기준 마지막 번호 조회 $lastNo = WorkOrder::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('work_order_no', 'like', "{$prefix}{$date}%") ->orderByDesc('work_order_no') ->value('work_order_no'); if ($lastNo) { $seq = (int) substr($lastNo, -4) + 1; } else { $seq = 1; } return sprintf('%s%s%04d', $prefix, $date, $seq); } }