tenantId(); $query = Purchase::query() ->where('tenant_id', $tenantId) ->with(['client:id,name', 'approval:id,document_number,title,form_id', 'approval.form:id,name,category']); // 검색어 필터 if (! empty($params['search'])) { $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('purchase_number', 'like', "%{$search}%") ->orWhere('description', 'like', "%{$search}%") ->orWhereHas('client', function ($q) use ($search) { $q->where('name', 'like', "%{$search}%"); }); }); } // 날짜 범위 필터 if (! empty($params['start_date'])) { $query->where('purchase_date', '>=', $params['start_date']); } if (! empty($params['end_date'])) { $query->where('purchase_date', '<=', $params['end_date']); } // 거래처 필터 if (! empty($params['client_id'])) { $query->where('client_id', $params['client_id']); } // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 정렬 $sortBy = $params['sort_by'] ?? 'purchase_date'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); // 페이지네이션 $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 매입 상세 조회 */ public function show(int $id): Purchase { $tenantId = $this->tenantId(); return Purchase::query() ->where('tenant_id', $tenantId) ->with(['client:id,name', 'withdrawal', 'creator:id,name', 'approval:id,document_number,title,form_id,content', 'approval.form:id,name,category']) ->findOrFail($id); } /** * 매입 등록 */ public function store(array $data): Purchase { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 매입번호 자동 생성 $purchaseNumber = $this->generatePurchaseNumber($tenantId, $data['purchase_date']); $purchase = new Purchase; $purchase->tenant_id = $tenantId; $purchase->purchase_number = $purchaseNumber; $purchase->purchase_date = $data['purchase_date']; $purchase->client_id = $data['client_id']; $purchase->supply_amount = $data['supply_amount']; $purchase->tax_amount = $data['tax_amount']; $purchase->total_amount = $data['total_amount']; $purchase->purchase_type = $data['purchase_type'] ?? null; $purchase->description = $data['description'] ?? null; $purchase->status = 'draft'; $purchase->withdrawal_id = $data['withdrawal_id'] ?? null; $purchase->approval_id = $data['approval_id'] ?? null; $purchase->created_by = $userId; $purchase->updated_by = $userId; $purchase->save(); return $purchase->load(['client:id,name']); }); } /** * 매입 수정 */ public function update(int $id, array $data): Purchase { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $purchase = Purchase::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 토글 필드만 업데이트하는 경우는 canEdit 체크 건너뛰기 $toggleOnlyFields = ['tax_invoice_received']; $isToggleOnly = count(array_diff(array_keys($data), $toggleOnlyFields)) === 0; // 확정 후에는 수정 불가 (토글 필드만 업데이트하는 경우 제외) if (! $isToggleOnly && ! $purchase->canEdit()) { throw new \Exception(__('error.purchase.cannot_edit')); } if (isset($data['purchase_date'])) { $purchase->purchase_date = $data['purchase_date']; } if (isset($data['client_id'])) { $purchase->client_id = $data['client_id']; } if (isset($data['supply_amount'])) { $purchase->supply_amount = $data['supply_amount']; } if (isset($data['tax_amount'])) { $purchase->tax_amount = $data['tax_amount']; } if (isset($data['total_amount'])) { $purchase->total_amount = $data['total_amount']; } if (array_key_exists('description', $data)) { $purchase->description = $data['description']; } if (array_key_exists('withdrawal_id', $data)) { $purchase->withdrawal_id = $data['withdrawal_id']; } if (array_key_exists('purchase_type', $data)) { $purchase->purchase_type = $data['purchase_type']; } if (array_key_exists('tax_invoice_received', $data)) { $purchase->tax_invoice_received = $data['tax_invoice_received']; } if (array_key_exists('approval_id', $data)) { $purchase->approval_id = $data['approval_id']; } $purchase->updated_by = $userId; $purchase->save(); return $purchase->fresh(['client:id,name']); }); } /** * 매입 삭제 */ public function destroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $purchase = Purchase::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 확정 후에는 삭제 불가 if (! $purchase->canDelete()) { throw new \Exception(__('error.purchase.cannot_delete')); } $purchase->deleted_by = $userId; $purchase->save(); $purchase->delete(); return true; }); } /** * 매입 확정 */ public function confirm(int $id): Purchase { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $purchase = Purchase::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $purchase->canConfirm()) { throw new \Exception(__('error.purchase.cannot_confirm')); } $purchase->status = 'confirmed'; $purchase->updated_by = $userId; $purchase->save(); return $purchase->fresh(['client:id,name']); }); } /** * 매입 요약 (기간별 합계) */ public function summary(array $params): array { $tenantId = $this->tenantId(); $query = Purchase::query() ->where('tenant_id', $tenantId); // 날짜 범위 필터 if (! empty($params['start_date'])) { $query->where('purchase_date', '>=', $params['start_date']); } if (! empty($params['end_date'])) { $query->where('purchase_date', '<=', $params['end_date']); } // 거래처 필터 if (! empty($params['client_id'])) { $query->where('client_id', $params['client_id']); } // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 전체 합계 $totalSupply = (clone $query)->sum('supply_amount'); $totalTax = (clone $query)->sum('tax_amount'); $totalAmount = (clone $query)->sum('total_amount'); $count = (clone $query)->count(); // 상태별 합계 $byStatus = (clone $query) ->select('status', DB::raw('SUM(total_amount) as total'), DB::raw('COUNT(*) as count')) ->groupBy('status') ->get() ->keyBy('status') ->toArray(); return [ 'total_supply_amount' => (float) $totalSupply, 'total_tax_amount' => (float) $totalTax, 'total_amount' => (float) $totalAmount, 'total_count' => $count, 'by_status' => $byStatus, ]; } /** * 매입유형 일괄 변경 * * @param array $ids */ public function bulkUpdatePurchaseType(array $ids, string $purchaseType): int { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return Purchase::query() ->where('tenant_id', $tenantId) ->whereIn('id', $ids) ->update([ 'purchase_type' => $purchaseType, 'updated_by' => $userId, ]); } /** * 세금계산서 수취 일괄 설정 * * @param array $ids */ public function bulkUpdateTaxReceived(array $ids, bool $taxInvoiceReceived): int { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return Purchase::query() ->where('tenant_id', $tenantId) ->whereIn('id', $ids) ->update([ 'tax_invoice_received' => $taxInvoiceReceived, 'updated_by' => $userId, ]); } /** * 대시보드 상세 조회 (CEO 대시보드 모달용) * * @return array{ * summary: array{current_month_total: float, previous_month_total: float, change_rate: float, count: int}, * monthly_trend: array, * by_type: array, * items: array * } */ public function dashboardDetail(): array { $tenantId = $this->tenantId(); // 현재 월 범위 $currentMonthStart = now()->startOfMonth()->toDateString(); $currentMonthEnd = now()->endOfMonth()->toDateString(); // 전월 범위 $previousMonthStart = now()->subMonth()->startOfMonth()->toDateString(); $previousMonthEnd = now()->subMonth()->endOfMonth()->toDateString(); // 1. 요약 정보 $currentMonthTotal = Purchase::query() ->where('tenant_id', $tenantId) ->whereBetween('purchase_date', [$currentMonthStart, $currentMonthEnd]) ->sum('total_amount'); $previousMonthTotal = Purchase::query() ->where('tenant_id', $tenantId) ->whereBetween('purchase_date', [$previousMonthStart, $previousMonthEnd]) ->sum('total_amount'); $currentMonthCount = Purchase::query() ->where('tenant_id', $tenantId) ->whereBetween('purchase_date', [$currentMonthStart, $currentMonthEnd]) ->count(); $changeRate = $previousMonthTotal > 0 ? round((($currentMonthTotal - $previousMonthTotal) / $previousMonthTotal) * 100, 1) : 0; // 2. 월별 추이 (최근 7개월) $monthlyTrend = []; for ($i = 6; $i >= 0; $i--) { $monthStart = now()->subMonths($i)->startOfMonth(); $monthEnd = now()->subMonths($i)->endOfMonth(); $amount = Purchase::query() ->where('tenant_id', $tenantId) ->whereBetween('purchase_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->sum('total_amount'); $monthlyTrend[] = [ 'label' => $monthStart->format('n').'월', // "1월", "2월" 형식 'amount' => (float) $amount, ]; } // 3. 유형별 분포 (현재 월) $byTypeRaw = Purchase::query() ->where('tenant_id', $tenantId) ->whereBetween('purchase_date', [$currentMonthStart, $currentMonthEnd]) ->select('purchase_type', DB::raw('SUM(total_amount) as amount')) ->groupBy('purchase_type') ->get(); $byType = []; $totalAmount = $byTypeRaw->sum('amount'); // 유형별 색상 매핑 $typeColors = [ 'raw_material' => '#60A5FA', // 원자재 - 파랑 'sub_material' => '#34D399', // 부자재 - 초록 'packaging' => '#FBBF24', // 포장재 - 노랑 'consumable' => '#F87171', // 소모품 - 빨강 'unset' => '#9CA3AF', // 미설정 - 회색 ]; $colorIndex = 0; $defaultColors = ['#60A5FA', '#34D399', '#FBBF24', '#F87171', '#A78BFA', '#F472B6']; foreach ($byTypeRaw as $item) { $type = $item->purchase_type ?? 'unset'; $byType[] = [ 'type' => $type, 'type_label' => Purchase::PURCHASE_TYPES[$type] ?? '미설정', 'amount' => (float) $item->amount, 'ratio' => $totalAmount > 0 ? round(($item->amount / $totalAmount) * 100, 1) : 0, 'color' => $typeColors[$type] ?? $defaultColors[$colorIndex++ % count($defaultColors)], ]; } // ratio 내림차순 정렬 usort($byType, fn ($a, $b) => $b['ratio'] <=> $a['ratio']); // 4. 일별 매입 내역 (현재 월) $items = Purchase::query() ->where('tenant_id', $tenantId) ->whereBetween('purchase_date', [$currentMonthStart, $currentMonthEnd]) ->with(['client:id,name']) ->orderBy('purchase_date', 'desc') ->get() ->map(function ($purchase) { $type = $purchase->purchase_type ?? 'unset'; return [ 'id' => $purchase->id, 'purchase_date' => $purchase->purchase_date->format('Y-m-d'), 'vendor_name' => $purchase->client?->name ?? '-', 'amount' => (float) $purchase->total_amount, 'type' => $type, 'type_label' => Purchase::PURCHASE_TYPES[$type] ?? '미설정', ]; }) ->toArray(); return [ 'summary' => [ 'current_month_amount' => (float) $currentMonthTotal, 'previous_month_amount' => (float) $previousMonthTotal, 'change_rate' => $changeRate, 'count' => $currentMonthCount, ], 'monthly_trend' => $monthlyTrend, 'by_type' => $byType, 'items' => $items, ]; } /** * 매입번호 자동 생성 */ private function generatePurchaseNumber(int $tenantId, string $purchaseDate): string { $prefix = 'PU'.date('Ymd', strtotime($purchaseDate)); $lastPurchase = Purchase::query() ->where('tenant_id', $tenantId) ->where('purchase_number', 'like', $prefix.'%') ->orderBy('purchase_number', 'desc') ->first(); if ($lastPurchase) { $lastSeq = (int) substr($lastPurchase->purchase_number, -4); $newSeq = $lastSeq + 1; } else { $newSeq = 1; } return $prefix.str_pad($newSeq, 4, '0', STR_PAD_LEFT); } }