tenantId(); $userId = $this->apiUserId(); $today = Carbon::today(); // 각 카테고리별 이슈 수집 $issues = collect(); // 1. 수주 성공 (최근 7일) $issues = $issues->merge($this->getOrderSuccessIssues($tenantId, $today)); // 2. 미수금 이슈 (주식 이슈 - 연체 미수금) $issues = $issues->merge($this->getReceivableIssues($tenantId)); // 3. 재고 이슈 (직정 제고 - 안전재고 미달) $issues = $issues->merge($this->getStockIssues($tenantId)); // 4. 지출예상내역서 (승인 대기 건) $issues = $issues->merge($this->getExpectedExpenseIssues($tenantId)); // 5. 세금 신고 (부가세 D-day) $issues = $issues->merge($this->getTaxIssues($tenantId, $today)); // 6. 결재 요청 (내 결재 대기 건) $issues = $issues->merge($this->getApprovalIssues($tenantId, $userId)); // 7. 기타 (신규 거래처 등록) $issues = $issues->merge($this->getOtherIssues($tenantId, $today)); // 날짜 기준 내림차순 정렬 후 limit 적용 $sortedIssues = $issues ->sortByDesc('created_at') ->take($limit) ->values() ->map(function ($item) { // created_at 필드 제거 (정렬용으로만 사용) unset($item['created_at']); return $item; }) ->toArray(); return [ 'items' => $sortedIssues, 'total_count' => $issues->count(), ]; } /** * 수주 성공 이슈 (최근 7일 확정 수주) */ private function getOrderSuccessIssues(int $tenantId, Carbon $today): array { $orders = Order::query() ->where('tenant_id', $tenantId) ->where('status_code', 'confirmed') ->where('created_at', '>=', $today->copy()->subDays(7)) ->with('client:id,name') ->orderByDesc('created_at') ->limit(10) ->get(); return $orders->map(function ($order) { $clientName = $order->client?->name ?? __('message.today_issue.unknown_client'); $amount = number_format($order->total_amount ?? 0); return [ 'id' => 'order_'.$order->id, 'badge' => '수주 성공', 'content' => __('message.today_issue.order_success', [ 'client' => $clientName, 'amount' => $amount, ]), 'time' => $this->formatRelativeTime($order->created_at), 'date' => $order->created_at?->toDateString(), 'needsApproval' => false, 'path' => '/sales/order-management-sales', 'created_at' => $order->created_at, ]; })->toArray(); } /** * 미수금 이슈 (주식 이슈 - 연체 미수금) */ private function getReceivableIssues(int $tenantId): array { // BadDebt 모델에서 추심 진행 중인 건 조회 $badDebts = BadDebt::query() ->where('tenant_id', $tenantId) ->whereIn('status', ['in_progress', 'legal_action']) ->with('client:id,name') ->orderByDesc('created_at') ->limit(10) ->get(); return $badDebts->map(function ($debt) { $clientName = $debt->client?->name ?? __('message.today_issue.unknown_client'); $amount = number_format($debt->total_amount ?? 0); $days = $debt->overdue_days ?? 0; return [ 'id' => 'receivable_'.$debt->id, 'badge' => '주식 이슈', 'content' => __('message.today_issue.receivable_overdue', [ 'client' => $clientName, 'amount' => $amount, 'days' => $days, ]), 'time' => $this->formatRelativeTime($debt->created_at), 'date' => $debt->created_at?->toDateString(), 'needsApproval' => false, 'path' => '/accounting/receivables-status', 'created_at' => $debt->created_at, ]; })->toArray(); } /** * 재고 이슈 (직정 제고 - 안전재고 미달) */ private function getStockIssues(int $tenantId): array { $stocks = Stock::query() ->where('tenant_id', $tenantId) ->where('safety_stock', '>', 0) ->whereColumn('stock_qty', '<', 'safety_stock') ->with('item:id,name,code') ->orderByDesc('updated_at') ->limit(10) ->get(); return $stocks->map(function ($stock) { $itemName = $stock->item?->name ?? $stock->item?->code ?? __('message.today_issue.unknown_item'); return [ 'id' => 'stock_'.$stock->id, 'badge' => '직정 제고', 'content' => __('message.today_issue.stock_below_safety', [ 'item' => $itemName, ]), 'time' => $this->formatRelativeTime($stock->updated_at), 'date' => $stock->updated_at?->toDateString(), 'needsApproval' => false, 'path' => '/material/stock-status', 'created_at' => $stock->updated_at, ]; })->toArray(); } /** * 지출예상내역서 이슈 (승인 대기) */ private function getExpectedExpenseIssues(int $tenantId): array { $expenses = ExpectedExpense::query() ->where('tenant_id', $tenantId) ->where('payment_status', 'pending') ->orderByDesc('created_at') ->limit(10) ->get(); // 그룹화: 같은 날짜의 품의서들을 묶어서 표시 if ($expenses->isEmpty()) { return []; } $totalCount = $expenses->count(); $totalAmount = $expenses->sum('amount'); $firstExpense = $expenses->first(); $title = $firstExpense->description ?? __('message.today_issue.expense_item'); $content = $totalCount > 1 ? __('message.today_issue.expense_pending_multiple', [ 'title' => $title, 'count' => $totalCount - 1, 'amount' => number_format($totalAmount), ]) : __('message.today_issue.expense_pending_single', [ 'title' => $title, 'amount' => number_format($totalAmount), ]); return [ [ 'id' => 'expense_summary', 'badge' => '지출예상내역서', 'content' => $content, 'time' => $this->formatRelativeTime($firstExpense->created_at), 'date' => $firstExpense->created_at?->toDateString(), 'needsApproval' => true, 'path' => '/approval/inbox', 'created_at' => $firstExpense->created_at, ], ]; } /** * 세금 신고 이슈 (부가세 D-day) */ private function getTaxIssues(int $tenantId, Carbon $today): array { // 부가세 신고 마감일 계산 (분기별: 1/25, 4/25, 7/25, 10/25) $quarter = $today->quarter; $deadlineMonth = match ($quarter) { 1 => 1, 2 => 4, 3 => 7, 4 => 10, }; $deadlineYear = $today->year; if ($today->month > $deadlineMonth || ($today->month == $deadlineMonth && $today->day > 25)) { $deadlineMonth = match ($quarter) { 1 => 4, 2 => 7, 3 => 10, 4 => 1, }; if ($deadlineMonth == 1) { $deadlineYear++; } } $deadline = Carbon::create($deadlineYear, $deadlineMonth, 25); $daysUntil = $today->diffInDays($deadline, false); // D-30 이내인 경우에만 표시 if ($daysUntil > 30 || $daysUntil < 0) { return []; } $quarterName = match ($deadlineMonth) { 1 => '4', 4 => '1', 7 => '2', 10 => '3', }; return [ [ 'id' => 'tax_vat_'.$deadlineYear.'_'.$deadlineMonth, 'badge' => '세금 신고', 'content' => __('message.today_issue.tax_vat_deadline', [ 'quarter' => $quarterName, 'days' => $daysUntil, ]), 'time' => $this->formatRelativeTime($today), 'date' => $today->toDateString(), 'needsApproval' => false, 'path' => '/accounting/tax', 'created_at' => $today, ], ]; } /** * 결재 요청 이슈 (내 결재 대기 건) */ private function getApprovalIssues(int $tenantId, int $userId): array { $steps = ApprovalStep::query() ->whereHas('approval', function ($query) use ($tenantId) { $query->where('tenant_id', $tenantId) ->where('status', 'pending'); }) ->where('approver_id', $userId) ->where('status', 'pending') ->with(['approval' => function ($query) { $query->with('drafter:id,name'); }]) ->orderByDesc('created_at') ->limit(10) ->get(); return $steps->map(function ($step) { $drafterName = $step->approval->drafter?->name ?? __('message.today_issue.unknown_user'); $title = $step->approval->title ?? __('message.today_issue.approval_request'); return [ 'id' => 'approval_'.$step->approval->id, 'badge' => '결재 요청', 'content' => __('message.today_issue.approval_pending', [ 'title' => $title, 'drafter' => $drafterName, ]), 'time' => $this->formatRelativeTime($step->approval->created_at), 'date' => $step->approval->created_at?->toDateString(), 'needsApproval' => true, 'path' => '/approval/inbox', 'created_at' => $step->approval->created_at, ]; })->toArray(); } /** * 기타 이슈 (신규 거래처 등록 등) */ private function getOtherIssues(int $tenantId, Carbon $today): array { // 최근 7일 신규 거래처 $clients = Client::query() ->where('tenant_id', $tenantId) ->where('created_at', '>=', $today->copy()->subDays(7)) ->orderByDesc('created_at') ->limit(5) ->get(); return $clients->map(function ($client) { return [ 'id' => 'client_'.$client->id, 'badge' => '기타', 'content' => __('message.today_issue.new_client', [ 'name' => $client->name, ]), 'time' => $this->formatRelativeTime($client->created_at), 'date' => $client->created_at?->toDateString(), 'needsApproval' => false, 'path' => '/accounting/vendors', 'created_at' => $client->created_at, ]; })->toArray(); } /** * 상대 시간 포맷팅 */ private function formatRelativeTime(?Carbon $datetime): string { if (! $datetime) { return ''; } $now = Carbon::now(); $diffInMinutes = $now->diffInMinutes($datetime); $diffInHours = $now->diffInHours($datetime); $diffInDays = $now->diffInDays($datetime); if ($diffInMinutes < 60) { return __('message.today_issue.time_minutes_ago', ['minutes' => max(1, $diffInMinutes)]); } if ($diffInHours < 24) { return __('message.today_issue.time_hours_ago', ['hours' => $diffInHours]); } if ($diffInDays == 1) { return __('message.today_issue.time_yesterday'); } if ($diffInDays < 7) { return __('message.today_issue.time_days_ago', ['days' => $diffInDays]); } return $datetime->format('Y-m-d'); } }