Files
sam-manage/app/Services/DashboardStatService.php
권혁성 5dd580623e feat:통계 대시보드 페이지 신규 구현 (/stats/dashboard)
- 모델 7개: StatSalesDaily, StatFinanceDaily, StatProductionDaily,
  StatInventoryDaily, StatSystemDaily, StatSalesMonthly, StatFinanceMonthly
- DashboardStatService: 요약카드, 7일 추이차트, 알림, 월간요약 데이터
- StatDashboardController: HX-Redirect 패턴 적용
- 뷰: 요약카드 6개 + Chart.js 4개 차트 + 알림/월간요약 하단섹션
- 기존 대시보드 "통계 및 리포트" 바로가기 링크 연결
- 헤더 테넌트 선택 기준 전체/개별 테넌트 필터링 지원

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 14:03:58 +09:00

298 lines
10 KiB
PHP

<?php
namespace App\Services;
use App\Models\Stats\StatAlert;
use App\Models\Stats\StatFinanceDaily;
use App\Models\Stats\StatFinanceMonthly;
use App\Models\Stats\StatInventoryDaily;
use App\Models\Stats\StatProductionDaily;
use App\Models\Stats\StatSalesDaily;
use App\Models\Stats\StatSalesMonthly;
use App\Models\Stats\StatSystemDaily;
use Illuminate\Support\Carbon;
class DashboardStatService
{
/**
* 요약 카드 데이터 (오늘 + 어제)
*/
public function getSummaryCards(?int $tenantId): array
{
$today = Carbon::today()->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;
}
}