toDateString(); $yesterday = Carbon::yesterday()->toDateString(); $salesToday = $this->getDailySales($tenantId, $today); $salesYesterday = $this->getDailySales($tenantId, $yesterday); $financeToday = $this->getDailyFinance($tenantId, $today); $financeYesterday = $this->getDailyFinance($tenantId, $yesterday); $productionToday = $this->getDailyProduction($tenantId, $today); $productionYesterday = $this->getDailyProduction($tenantId, $yesterday); $inventoryToday = $this->getDailyInventory($tenantId, $today); $inventoryYesterday = $this->getDailyInventory($tenantId, $yesterday); $systemToday = $this->getDailySystem($tenantId, $today); $systemYesterday = $this->getDailySystem($tenantId, $yesterday); return [ 'orders' => [ 'count' => $salesToday['order_count'], 'amount' => $salesToday['order_amount'], 'delta_count' => $salesToday['order_count'] - $salesYesterday['order_count'], 'delta_amount' => $salesToday['order_amount'] - $salesYesterday['order_amount'], ], 'sales' => [ 'amount' => $salesToday['sales_amount'], 'delta' => $salesToday['sales_amount'] - $salesYesterday['sales_amount'], ], 'cashflow' => [ 'net' => $financeToday['net_cashflow'], 'delta' => $financeToday['net_cashflow'] - $financeYesterday['net_cashflow'], ], 'production' => [ 'efficiency' => $productionToday['efficiency_rate'], 'defect_rate' => $productionToday['defect_rate'], 'delta_efficiency' => $productionToday['efficiency_rate'] - $productionYesterday['efficiency_rate'], ], 'inventory' => [ 'below_safety' => $inventoryToday['below_safety_count'], 'delta' => $inventoryToday['below_safety_count'] - $inventoryYesterday['below_safety_count'], ], 'system' => [ 'active_users' => $systemToday['active_user_count'], 'delta' => $systemToday['active_user_count'] - $systemYesterday['active_user_count'], ], ]; } /** * 매출 추이 (7일) */ public function getSalesTrend(?int $tenantId, int $days = 7): array { $from = Carbon::today()->subDays($days - 1)->toDateString(); $to = Carbon::today()->toDateString(); $rows = StatSalesDaily::forTenant($tenantId) ->forDateRange($from, $to) ->selectRaw('stat_date, SUM(order_amount) as order_amount, SUM(sales_amount) as sales_amount') ->groupBy('stat_date') ->orderBy('stat_date') ->get(); return $this->fillDates($this->generateDateRange($from, $to), $rows, ['order_amount', 'sales_amount']); } /** * 자금 흐름 추이 (7일) */ public function getFinanceTrend(?int $tenantId, int $days = 7): array { $from = Carbon::today()->subDays($days - 1)->toDateString(); $to = Carbon::today()->toDateString(); $rows = StatFinanceDaily::forTenant($tenantId) ->forDateRange($from, $to) ->selectRaw('stat_date, SUM(deposit_amount) as deposit_amount, SUM(withdrawal_amount) as withdrawal_amount') ->groupBy('stat_date') ->orderBy('stat_date') ->get(); return $this->fillDates($this->generateDateRange($from, $to), $rows, ['deposit_amount', 'withdrawal_amount']); } /** * 생산 현황 추이 (7일) */ public function getProductionTrend(?int $tenantId, int $days = 7): array { $from = Carbon::today()->subDays($days - 1)->toDateString(); $to = Carbon::today()->toDateString(); $rows = StatProductionDaily::forTenant($tenantId) ->forDateRange($from, $to) ->selectRaw('stat_date, SUM(production_qty) as production_qty, AVG(defect_rate) as defect_rate') ->groupBy('stat_date') ->orderBy('stat_date') ->get(); return $this->fillDates($this->generateDateRange($from, $to), $rows, ['production_qty', 'defect_rate']); } /** * 시스템 활동 추이 (7일) */ public function getSystemTrend(?int $tenantId, int $days = 7): array { $from = Carbon::today()->subDays($days - 1)->toDateString(); $to = Carbon::today()->toDateString(); $rows = StatSystemDaily::forTenant($tenantId) ->forDateRange($from, $to) ->selectRaw('stat_date, SUM(active_user_count) as active_user_count, SUM(api_request_count) as api_request_count') ->groupBy('stat_date') ->orderBy('stat_date') ->get(); return $this->fillDates($this->generateDateRange($from, $to), $rows, ['active_user_count', 'api_request_count']); } /** * 최근 알림 5건 */ public function getRecentAlerts(?int $tenantId, int $limit = 5): \Illuminate\Database\Eloquent\Collection { $query = StatAlert::query() ->where('is_resolved', false) ->orderByDesc('created_at') ->limit($limit); if ($tenantId) { $query->where('tenant_id', $tenantId); } return $query->get(); } /** * 이번 달 도메인별 요약 */ public function getMonthlySummary(?int $tenantId): array { $year = (int) Carbon::now()->format('Y'); $month = (int) Carbon::now()->format('n'); $monthLabel = Carbon::now()->format('Y-m'); $sales = StatSalesMonthly::forTenant($tenantId) ->forMonth($year, $month) ->selectRaw('SUM(order_count) as order_count, SUM(order_amount) as order_amount, SUM(sales_amount) as sales_amount') ->first(); $finance = StatFinanceMonthly::forTenant($tenantId) ->forMonth($year, $month) ->selectRaw('SUM(deposit_total) as deposit_total, SUM(withdrawal_total) as withdrawal_total, SUM(net_cashflow) as net_cashflow') ->first(); return [ 'month' => $monthLabel, 'sales' => [ 'order_count' => (int) ($sales->order_count ?? 0), 'order_amount' => (float) ($sales->order_amount ?? 0), 'sales_amount' => (float) ($sales->sales_amount ?? 0), ], 'finance' => [ 'deposit_total' => (float) ($finance->deposit_total ?? 0), 'withdrawal_total' => (float) ($finance->withdrawal_total ?? 0), 'net_cashflow' => (float) ($finance->net_cashflow ?? 0), ], ]; } // --- Private Helpers --- private function getDailySales(?int $tenantId, string $date): array { $row = StatSalesDaily::forTenant($tenantId) ->forDate($date) ->selectRaw('SUM(order_count) as order_count, SUM(order_amount) as order_amount, SUM(sales_amount) as sales_amount') ->first(); return [ 'order_count' => (int) ($row->order_count ?? 0), 'order_amount' => (float) ($row->order_amount ?? 0), 'sales_amount' => (float) ($row->sales_amount ?? 0), ]; } private function getDailyFinance(?int $tenantId, string $date): array { $row = StatFinanceDaily::forTenant($tenantId) ->forDate($date) ->selectRaw('SUM(net_cashflow) as net_cashflow, SUM(deposit_amount) as deposit_amount, SUM(withdrawal_amount) as withdrawal_amount') ->first(); return [ 'net_cashflow' => (float) ($row->net_cashflow ?? 0), 'deposit_amount' => (float) ($row->deposit_amount ?? 0), 'withdrawal_amount' => (float) ($row->withdrawal_amount ?? 0), ]; } private function getDailyProduction(?int $tenantId, string $date): array { $row = StatProductionDaily::forTenant($tenantId) ->forDate($date) ->selectRaw('AVG(efficiency_rate) as efficiency_rate, AVG(defect_rate) as defect_rate') ->first(); return [ 'efficiency_rate' => (float) ($row->efficiency_rate ?? 0), 'defect_rate' => (float) ($row->defect_rate ?? 0), ]; } private function getDailyInventory(?int $tenantId, string $date): array { $row = StatInventoryDaily::forTenant($tenantId) ->forDate($date) ->selectRaw('SUM(below_safety_count) as below_safety_count') ->first(); return [ 'below_safety_count' => (int) ($row->below_safety_count ?? 0), ]; } private function getDailySystem(?int $tenantId, string $date): array { $row = StatSystemDaily::forTenant($tenantId) ->forDate($date) ->selectRaw('SUM(active_user_count) as active_user_count') ->first(); return [ 'active_user_count' => (int) ($row->active_user_count ?? 0), ]; } private function generateDateRange(string $from, string $to): array { $dates = []; $current = Carbon::parse($from); $end = Carbon::parse($to); while ($current->lte($end)) { $dates[] = $current->toDateString(); $current->addDay(); } return $dates; } private function fillDates(array $dates, $rows, array $fields): array { $indexed = $rows->keyBy(fn ($row) => Carbon::parse($row->stat_date)->toDateString()); $result = [ 'labels' => [], 'datasets' => [], ]; foreach ($fields as $field) { $result['datasets'][$field] = []; } foreach ($dates as $date) { $result['labels'][] = Carbon::parse($date)->format('m/d'); foreach ($fields as $field) { $result['datasets'][$field][] = (float) ($indexed[$date]?->$field ?? 0); } } return $result; } }