- 모델 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>
298 lines
10 KiB
PHP
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;
|
|
}
|
|
}
|