【ダッシュボード】グラフ表示実装
All checks were successful
Deploy main / deploy (push) Successful in 25s

This commit is contained in:
OU.ZAIKOU 2026-02-05 01:24:19 +09:00
parent a118709f9b
commit db681b219a
2 changed files with 236 additions and 114 deletions

View File

@ -236,7 +236,65 @@ class InformationController extends Controller
]; ];
} }
return view('admin.information.dashboard', compact('totalStats', 'cityStats')); // グラフ用データ: city_name と utilization_rate, waiting_count のみ
$cityStatsChart = [];
foreach ($cities as $city) {
$parkIds = DB::table('park')
->where('city_id', $city->city_id)
->pluck('park_id')
->toArray();
// park_standard と park_number の合計
$parkNumberStats = DB::table('park_number')
->whereIn('park_id', $parkIds)
->selectRaw('COALESCE(SUM(park_standard), 0) as total_standard, COALESCE(SUM(park_number), 0) as total_number')
->first();
$parkStandard = $parkNumberStats->total_standard ?? 0;
$parkNumber = $parkNumberStats->total_number ?? 0;
// 利用率計算floor((park_number / park_standard) * 100)
$utilizationRate = $parkStandard > 0
? (int) floor(($parkNumber / $parkStandard) * 100)
: 0;
// 予約待ち人数
$waitingCount = 0;
if (!empty($parkIds)) {
$waitingQuery = DB::table('reserve')
->whereIn('park_id', $parkIds)
->where('valid_flag', 1)
->whereNull('contract_id');
try {
DB::table('reserve')
->select(DB::raw('1'))
->whereNotNull('reserve_cancel_flag')
->limit(1)
->first();
$waitingQuery = $waitingQuery
->where(function ($q) {
$q->whereNull('reserve_cancel_flag')
->orWhere('reserve_cancel_flag', 0);
})
->whereNull('reserve_cancelday');
} catch (\Exception $e) {
// キャンセルフラグが未運用
}
$waitingCount = $waitingQuery->count();
}
$cityStatsChart[] = [
'city_id' => $city->city_id,
'city_name' => $city->city_name,
'utilization_rate' => $utilizationRate,
'waiting_count' => $waitingCount,
];
}
return view('admin.information.dashboard', compact('totalStats', 'cityStats', 'cityStatsChart'));
} }
// ステータス一括更新(着手=2 / 対応完了=3 // ステータス一括更新(着手=2 / 対応完了=3

View File

@ -257,125 +257,189 @@
@push('scripts') @push('scripts')
<script src="{{ asset('plugins/chart.js/Chart.min.js') }}"></script> <script src="{{ asset('plugins/chart.js/Chart.min.js') }}"></script>
<script> <script>
$(document).ready(function() { (function() {
// データテーブル初期化 // window.onload で全体を囲むことで、他の onload と競合しないようにする
$('#cityStatsTable').DataTable({ window.onload = function() {
"language": { try {
"url": "//cdn.datatables.net/plug-ins/1.10.21/i18n/Japanese.json" console.log('[dashboard] start');
},
"pageLength": 10,
"order": [[ 6, "desc" ]], // 利用率で降順ソート
"columnDefs": [
{ "orderable": false, "targets": 8 } // 操作列はソート無効
]
});
// 利用率チャート(円グラフ) // DataTable 初期化
var ctxUtil = document.getElementById('utilizationChart').getContext('2d'); try {
var utilizationChart = new Chart(ctxUtil, { $('#cityStatsTable').DataTable({
type: 'pie', pageLength: 10,
data: { order: [[ 6, "desc" ]],
labels: [ columnDefs: [
@foreach($cityStats as $stat) { orderable: false, targets: 8 }
'{{ $stat['city']->city_name }} ({{ $stat['utilization_rate'] }}%)', ],
@endforeach language: {
], lengthMenu: " _MENU_ 件表示",
datasets: [{ search: "検索:",
label: '利用率', info: "_TOTAL_ 件中 _START_ から _END_ まで表示",
data: [ infoEmpty: "0 件中 0 から 0 まで表示",
@foreach($cityStats as $stat) infoFiltered: "(全 _MAX_ 件より抽出)",
{{ $stat['utilization_rate'] }}, paginate: {
@endforeach first: "最初",
], last: "最後",
backgroundColor: [ next: "",
'rgba(255, 99, 132, 0.8)', previous: ""
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)',
'rgba(75, 192, 192, 0.8)',
'rgba(153, 102, 255, 0.8)',
'rgba(255, 159, 64, 0.8)',
'rgba(199, 199, 199, 0.8)',
'rgba(83, 102, 255, 0.8)',
'rgba(255, 193, 7, 0.8)',
'rgba(220, 53, 69, 0.8)',
'rgba(108, 117, 125, 0.8)',
'rgba(40, 167, 69, 0.8)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
'rgba(199, 199, 199, 1)',
'rgba(83, 102, 255, 1)',
'rgba(255, 193, 7, 1)',
'rgba(220, 53, 69, 1)',
'rgba(108, 117, 125, 1)',
'rgba(40, 167, 69, 1)'
],
borderWidth: 2
}]
}, },
options: { zeroRecords: "該当するデータがありません"
responsive: true, }
plugins: { });
legend: {
position: 'bottom', } catch (e) {
labels: { console.error('[dashboard] DataTable error:', e);
boxWidth: 12, }
padding: 10
} if (typeof Chart === 'undefined') {
console.error('[dashboard] Chart.js is undefinedChart.min.js が読めていない)');
return;
}
console.log('[dashboard] Chart.js version =', Chart.version || '(unknown)');
// ===== 数据(末尾カンマ回避)=====
var utilLabels = [
@foreach($cityStats as $stat)
'{{ $stat['city']->city_name }}'@if(!$loop->last),@endif
@endforeach
];
var utilData = [
@foreach($cityStats as $stat)
{{ (int)$stat['utilization_rate'] }}@if(!$loop->last),@endif
@endforeach
];
var waitLabels = [
@foreach($cityStats as $stat)
'{{ $stat['city']->city_name }}'@if(!$loop->last),@endif
@endforeach
];
var waitData = [
@foreach($cityStats as $stat)
{{ (int)$stat['waiting_count'] }}@if(!$loop->last),@endif
@endforeach
];
console.log('[dashboard] util labels/data len =', utilLabels.length, utilData.length);
console.log('[dashboard] wait labels/data len =', waitLabels.length, waitData.length);
function genBarColors(n, alpha) {
var colors = [];
for (var i = 0; i < n; i++) {
var hue = Math.floor((360 / Math.max(n, 1)) * i);
colors.push('hsla(' + hue + ',70%,55%,' + (alpha || 0.8) + ')');
}
return colors;
}
// ===== 利用率bar=====
var c1 = document.getElementById('utilizationChart');
if (!c1) {
console.error('[dashboard] utilizationChart not found');
return;
}
var utilConfigV2 = {
type: 'bar',
data: {
labels: utilLabels,
datasets: [{
label: '利用率(%)',
data: utilData,
backgroundColor: genBarColors(utilData.length, 0.8),
borderWidth: 1
}]
}, },
tooltip: { options: {
callbacks: { responsive: true,
label: function(context) { maintainAspectRatio: false,
var label = context.label.split(' (')[0]; // 自治体名のみ取得 legend: { display: false },
var value = context.parsed; tooltips: {
var total = context.dataset.data.reduce((a, b) => a + b, 0); callbacks: {
var percentage = ((value / total) * 100).toFixed(1); label: function(tooltipItem) {
return label + ': ' + value + '% (全体の' + percentage + '%)'; return '利用率: ' + tooltipItem.yLabel + '%';
}
} }
},
scales: {
xAxes: [{ ticks: { autoSkip: false, maxRotation: 60, minRotation: 60, fontSize: 10} }],
yAxes: [{
ticks: { beginAtZero: true, max: 100 },
scaleLabel: { display: true, labelString: '利用率(%)' }
}]
} }
} }
} };
}
});
// 予約待ちチャート // 旧实例があれば破棄(再描画で真っ白になる事故防止)
var ctxWait = document.getElementById('waitingChart').getContext('2d'); if (window._utilChart && window._utilChart.destroy) window._utilChart.destroy();
var waitingChart = new Chart(ctxWait, { window._utilChart = new Chart(c1.getContext('2d'), utilConfigV2);
type: 'doughnut', console.log('[dashboard] utilization chart rendered');
data: {
labels: [ // ===== 予約待ちbar=====
@foreach($cityStats as $stat) var c2 = document.getElementById('waitingChart');
'{{ $stat['city']->city_name }}', if (!c2) {
@endforeach console.error('[dashboard] waitingChart not found');
], return;
datasets: [{ }
label: '予約待ち人数',
data: [ var waitConfigV2 = {
@foreach($cityStats as $stat) type: 'bar',
{{ $stat['waiting_count'] }}, data: {
@endforeach labels: waitLabels,
], datasets: [{
backgroundColor: [ label: '予約待ち人数',
'rgba(255, 99, 132, 0.8)', data: waitData,
'rgba(54, 162, 235, 0.8)', backgroundColor: genBarColors(waitData.length, 0.8),
'rgba(255, 206, 86, 0.8)', borderWidth: 1
'rgba(75, 192, 192, 0.8)', }]
'rgba(153, 102, 255, 0.8)', },
'rgba(255, 159, 64, 0.8)', options: {
'rgba(199, 199, 199, 0.8)', responsive: true,
'rgba(83, 102, 255, 0.8)' maintainAspectRatio: false,
] legend: { display: false },
}] tooltips: {
}, callbacks: {
options: { label: function(tooltipItem) {
responsive: true return '予約待ち: ' + tooltipItem.yLabel + '人';
}
}
},
scales: {
xAxes: [{ ticks: { autoSkip: false, maxRotation: 60, minRotation: 60, fontSize: 10} }],
yAxes: [{
ticks: { beginAtZero: true },
scaleLabel: { display: true, labelString: '人数' }
}]
}
}
};
if (window._waitChart && window._waitChart.destroy) window._waitChart.destroy();
window._waitChart = new Chart(c2.getContext('2d'), waitConfigV2);
console.log('[dashboard] waiting chart rendered');
} catch (e) {
console.error('[dashboard] fatal error:', e);
} }
}); };
}); })();
</script> </script>
@endpush
<style>
/* Chart.js v2 で maintainAspectRatio:false を使用する場合、canvas に固定高さが必要 */
#utilizationChart, #waitingChart { height: 300px !important; }
/* ページネーションボタン同士に余白を追加し、「前123次」のように詰まるのを防止 */
.dataTables_wrapper .dataTables_paginate .paginate_button{
margin: 0 4px !important;
display: inline-block !important;
}
/* 現在ページボタンにも適度な内側余白を設定 */
.dataTables_wrapper .dataTables_paginate .paginate_button{
padding: .2rem .55rem !important;
}
</style>
@endpush