tenantId(); $query = Client::query() ->where('tenant_id', $tenantId) // is_active=true인 악성채권이 있는 거래처만 ->whereHas('badDebts', function ($q) { $q->where('is_active', true); }) // 활성 악성채권 eager loading (추심중/법적조치 + is_active=true) ->with(['activeBadDebts' => function ($q) { $q->with('assignedUser:id,name') ->orderBy('created_at', 'desc'); }]) // 집계: 총 미수금액 (is_active=true인 건만) ->withSum(['badDebts as total_debt_amount' => function ($q) { $q->where('is_active', true); }], 'debt_amount') // 집계: 최대 연체일수 (is_active=true인 건만) ->withMax(['badDebts as max_overdue_days' => function ($q) { $q->where('is_active', true); }], 'overdue_days') // 집계: 악성채권 건수 (is_active=true인 건만) ->withCount(['badDebts as active_bad_debt_count' => function ($q) { $q->where('is_active', true); }]); // 거래처 필터 if (! empty($params['client_id'])) { $query->where('id', $params['client_id']); } // 상태 필터 (해당 상태의 악성채권이 있는 거래처만) if (! empty($params['status'])) { $query->whereHas('badDebts', function ($q) use ($params) { $q->where('is_active', true) ->where('status', $params['status']); }); } // 검색어 필터 if (! empty($params['search'])) { $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('client_code', 'like', "%{$search}%"); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'total_debt_amount'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); // 페이지네이션 $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 악성채권 요약 통계 (is_active=true인 건만) */ public function summary(array $params = []): array { $tenantId = $this->tenantId(); // is_active=true인 악성채권만 통계 $query = BadDebt::query() ->where('bad_debts.tenant_id', $tenantId) ->where('bad_debts.is_active', true); // 거래처 필터 if (! empty($params['client_id'])) { $query->where('client_id', $params['client_id']); } // 전체 합계 $totalAmount = (clone $query)->sum('debt_amount'); // 상태별 합계 $collectingAmount = (clone $query)->collecting()->sum('debt_amount'); $legalActionAmount = (clone $query)->legalAction()->sum('debt_amount'); $recoveredAmount = (clone $query)->recovered()->sum('debt_amount'); $badDebtAmount = (clone $query)->badDebt()->sum('debt_amount'); // 거래처 수 (is_active=true인 악성채권이 있는) $clientCount = BadDebt::query() ->where('tenant_id', $tenantId) ->where('is_active', true) ->distinct('client_id') ->count('client_id'); // per-card sub_label: 각 상태별 최다 금액 거래처명 + 건수 $subLabels = $this->buildPerCardSubLabels($query); return [ 'total_amount' => (float) $totalAmount, 'collecting_amount' => (float) $collectingAmount, 'legal_action_amount' => (float) $legalActionAmount, 'recovered_amount' => (float) $recoveredAmount, 'bad_debt_amount' => (float) $badDebtAmount, 'client_count' => $clientCount, 'sub_labels' => $subLabels, ]; } /** * 카드별 sub_label 생성 (최다 금액 거래처명 + 건수) */ private function buildPerCardSubLabels($baseQuery): array { $result = []; $statusScopes = [ 'dc1' => null, // 전체 (누적) 'dc2' => 'collecting', // 추심중 'dc3' => 'legalAction', // 법적조치 'dc4' => 'recovered', // 회수완료 ]; foreach ($statusScopes as $cardId => $scope) { $q = clone $baseQuery; if ($scope) { $q = $q->$scope(); } $clientCount = (clone $q)->distinct('client_id')->count('client_id'); if ($clientCount <= 0) { $result[$cardId] = null; continue; } $topClient = (clone $q) ->join('clients', 'bad_debts.client_id', '=', 'clients.id') ->selectRaw('clients.name, SUM(bad_debts.debt_amount) as total_amount') ->groupBy('clients.id', 'clients.name') ->orderByDesc('total_amount') ->first(); if ($topClient) { $result[$cardId] = $clientCount > 1 ? $topClient->name.' 외 '.($clientCount - 1).'건' : $topClient->name; } else { $result[$cardId] = null; } } return $result; } /** * 악성채권 상세 조회 */ public function show(int $id): BadDebt { $tenantId = $this->tenantId(); return BadDebt::query() ->where('tenant_id', $tenantId) ->with([ 'client', 'assignedUser:id,name', 'creator:id,name', 'documents.file', 'memos.creator:id,name', ]) ->findOrFail($id); } /** * 악성채권 등록 */ public function store(array $data): BadDebt { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { $badDebt = new BadDebt; $badDebt->tenant_id = $tenantId; $badDebt->client_id = $data['client_id']; $badDebt->debt_amount = $data['debt_amount']; $badDebt->status = $data['status'] ?? BadDebt::STATUS_COLLECTING; $badDebt->overdue_days = $data['overdue_days'] ?? 0; $badDebt->assigned_user_id = $data['assigned_user_id'] ?? null; $badDebt->occurred_at = $data['occurred_at'] ?? null; $badDebt->closed_at = $data['closed_at'] ?? null; $badDebt->is_active = $data['is_active'] ?? true; $badDebt->options = $data['options'] ?? null; $badDebt->created_by = $userId; $badDebt->updated_by = $userId; $badDebt->save(); return $badDebt->load(['client:id,name,client_code', 'assignedUser:id,name']); }); } /** * 악성채권 수정 */ public function update(int $id, array $data): BadDebt { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $badDebt = BadDebt::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (isset($data['client_id'])) { $badDebt->client_id = $data['client_id']; } if (isset($data['debt_amount'])) { $badDebt->debt_amount = $data['debt_amount']; } if (isset($data['status'])) { $badDebt->status = $data['status']; } if (isset($data['overdue_days'])) { $badDebt->overdue_days = $data['overdue_days']; } if (array_key_exists('assigned_user_id', $data)) { $badDebt->assigned_user_id = $data['assigned_user_id']; } if (array_key_exists('occurred_at', $data)) { $badDebt->occurred_at = $data['occurred_at']; } if (array_key_exists('closed_at', $data)) { $badDebt->closed_at = $data['closed_at']; } if (isset($data['is_active'])) { $badDebt->is_active = $data['is_active']; } if (array_key_exists('options', $data)) { $badDebt->options = $data['options']; } $badDebt->updated_by = $userId; $badDebt->save(); return $badDebt->fresh(['client:id,name,client_code', 'assignedUser:id,name']); }); } /** * 악성채권 삭제 */ public function destroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $badDebt = BadDebt::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $badDebt->deleted_by = $userId; $badDebt->save(); $badDebt->delete(); return true; }); } /** * 설정 토글 (is_active) */ public function toggle(int $id): BadDebt { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $badDebt = BadDebt::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $badDebt->is_active = ! $badDebt->is_active; $badDebt->updated_by = $userId; $badDebt->save(); return $badDebt->fresh(['client:id,name,client_code', 'assignedUser:id,name']); }); } /** * 서류 첨부 */ public function addDocument(int $id, array $data): BadDebtDocument { $tenantId = $this->tenantId(); $badDebt = BadDebt::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $document = new BadDebtDocument; $document->bad_debt_id = $badDebt->id; $document->document_type = $data['document_type']; $document->file_id = $data['file_id']; $document->save(); return $document->load('file'); } /** * 서류 삭제 */ public function removeDocument(int $id, int $documentId): bool { $tenantId = $this->tenantId(); $badDebt = BadDebt::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $document = BadDebtDocument::query() ->where('bad_debt_id', $badDebt->id) ->findOrFail($documentId); return $document->delete(); } /** * 메모 추가 */ public function addMemo(int $id, array $data): BadDebtMemo { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $badDebt = BadDebt::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $memo = new BadDebtMemo; $memo->bad_debt_id = $badDebt->id; $memo->content = $data['content']; $memo->created_by = $userId; $memo->save(); return $memo->load('creator:id,name'); } /** * 메모 삭제 */ public function removeMemo(int $id, int $memoId): bool { $tenantId = $this->tenantId(); $badDebt = BadDebt::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $memo = BadDebtMemo::query() ->where('bad_debt_id', $badDebt->id) ->findOrFail($memoId); return $memo->delete(); } }