- 모델 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>
243 lines
8.2 KiB
PHP
243 lines
8.2 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '통계 대시보드')
|
|
@section('page-title', '통계 대시보드')
|
|
|
|
@section('content')
|
|
{{-- 요약 카드 --}}
|
|
@include('stats.dashboard.partials._summary-cards')
|
|
|
|
{{-- 추이 차트 --}}
|
|
@include('stats.dashboard.partials._charts')
|
|
|
|
{{-- 하단 섹션 --}}
|
|
@include('stats.dashboard.partials._bottom-section')
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const salesTrend = @json($salesTrend);
|
|
const financeTrend = @json($financeTrend);
|
|
const productionTrend = @json($productionTrend);
|
|
const systemTrend = @json($systemTrend);
|
|
|
|
const defaultOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: { usePointStyle: true, padding: 15, font: { size: 11 } }
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: { display: false },
|
|
ticks: { font: { size: 11 } }
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: { color: '#f3f4f6' },
|
|
ticks: {
|
|
font: { size: 11 },
|
|
callback: function(value) {
|
|
if (value >= 1000000) return (value / 1000000).toFixed(0) + 'M';
|
|
if (value >= 1000) return (value / 1000).toFixed(0) + 'K';
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 매출 추이 (Bar + Line)
|
|
new Chart(document.getElementById('salesChart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: salesTrend.labels,
|
|
datasets: [
|
|
{
|
|
label: '주문금액',
|
|
data: salesTrend.datasets.order_amount,
|
|
backgroundColor: 'rgba(59, 130, 246, 0.5)',
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
order: 2,
|
|
},
|
|
{
|
|
label: '매출액',
|
|
data: salesTrend.datasets.sales_amount,
|
|
type: 'line',
|
|
borderColor: 'rgb(16, 185, 129)',
|
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
borderWidth: 2,
|
|
pointRadius: 3,
|
|
pointBackgroundColor: 'rgb(16, 185, 129)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
order: 1,
|
|
}
|
|
]
|
|
},
|
|
options: defaultOptions,
|
|
});
|
|
|
|
// 자금 흐름 (Stacked Bar)
|
|
new Chart(document.getElementById('financeChart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: financeTrend.labels,
|
|
datasets: [
|
|
{
|
|
label: '입금',
|
|
data: financeTrend.datasets.deposit_amount,
|
|
backgroundColor: 'rgba(16, 185, 129, 0.7)',
|
|
borderRadius: 4,
|
|
},
|
|
{
|
|
label: '출금',
|
|
data: financeTrend.datasets.withdrawal_amount.map(v => -v),
|
|
backgroundColor: 'rgba(239, 68, 68, 0.7)',
|
|
borderRadius: 4,
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
...defaultOptions,
|
|
scales: {
|
|
...defaultOptions.scales,
|
|
x: { ...defaultOptions.scales.x, stacked: true },
|
|
y: {
|
|
...defaultOptions.scales.y,
|
|
stacked: true,
|
|
beginAtZero: false,
|
|
ticks: {
|
|
...defaultOptions.scales.y.ticks,
|
|
callback: function(value) {
|
|
const abs = Math.abs(value);
|
|
if (abs >= 1000000) return (value / 1000000).toFixed(0) + 'M';
|
|
if (abs >= 1000) return (value / 1000).toFixed(0) + 'K';
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
// 생산 현황 (Dual axis Line)
|
|
new Chart(document.getElementById('productionChart'), {
|
|
type: 'line',
|
|
data: {
|
|
labels: productionTrend.labels,
|
|
datasets: [
|
|
{
|
|
label: '생산수량',
|
|
data: productionTrend.datasets.production_qty,
|
|
borderColor: 'rgb(139, 92, 246)',
|
|
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
|
borderWidth: 2,
|
|
pointRadius: 3,
|
|
pointBackgroundColor: 'rgb(139, 92, 246)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
yAxisID: 'y',
|
|
},
|
|
{
|
|
label: '불량률(%)',
|
|
data: productionTrend.datasets.defect_rate,
|
|
borderColor: 'rgb(239, 68, 68)',
|
|
borderWidth: 2,
|
|
borderDash: [5, 5],
|
|
pointRadius: 3,
|
|
pointBackgroundColor: 'rgb(239, 68, 68)',
|
|
tension: 0.3,
|
|
yAxisID: 'y1',
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
...defaultOptions,
|
|
scales: {
|
|
...defaultOptions.scales,
|
|
y: {
|
|
...defaultOptions.scales.y,
|
|
position: 'left',
|
|
title: { display: true, text: '생산수량', font: { size: 11 } },
|
|
},
|
|
y1: {
|
|
position: 'right',
|
|
beginAtZero: true,
|
|
grid: { drawOnChartArea: false },
|
|
ticks: {
|
|
font: { size: 11 },
|
|
callback: function(value) { return value + '%'; }
|
|
},
|
|
title: { display: true, text: '불량률(%)', font: { size: 11 } },
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
// 시스템 활동 (Line)
|
|
new Chart(document.getElementById('systemChart'), {
|
|
type: 'line',
|
|
data: {
|
|
labels: systemTrend.labels,
|
|
datasets: [
|
|
{
|
|
label: '활성사용자',
|
|
data: systemTrend.datasets.active_user_count,
|
|
borderColor: 'rgb(99, 102, 241)',
|
|
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
|
borderWidth: 2,
|
|
pointRadius: 3,
|
|
pointBackgroundColor: 'rgb(99, 102, 241)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
yAxisID: 'y',
|
|
},
|
|
{
|
|
label: 'API 요청',
|
|
data: systemTrend.datasets.api_request_count,
|
|
borderColor: 'rgb(245, 158, 11)',
|
|
borderWidth: 2,
|
|
pointRadius: 3,
|
|
pointBackgroundColor: 'rgb(245, 158, 11)',
|
|
tension: 0.3,
|
|
yAxisID: 'y1',
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
...defaultOptions,
|
|
scales: {
|
|
...defaultOptions.scales,
|
|
y: {
|
|
...defaultOptions.scales.y,
|
|
position: 'left',
|
|
title: { display: true, text: '사용자', font: { size: 11 } },
|
|
},
|
|
y1: {
|
|
position: 'right',
|
|
beginAtZero: true,
|
|
grid: { drawOnChartArea: false },
|
|
ticks: {
|
|
font: { size: 11 },
|
|
callback: function(value) {
|
|
if (value >= 1000) return (value / 1000).toFixed(0) + 'K';
|
|
return value;
|
|
}
|
|
},
|
|
title: { display: true, text: 'API 요청', font: { size: 11 } },
|
|
}
|
|
}
|
|
},
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|