【ダッシュボード】グラフ表示実装
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,
order: [[ 6, "desc" ]],
columnDefs: [
{ orderable: false, targets: 8 }
],
language: {
lengthMenu: " _MENU_ 件表示",
search: "検索:",
info: "_TOTAL_ 件中 _START_ から _END_ まで表示",
infoEmpty: "0 件中 0 から 0 まで表示",
infoFiltered: "(全 _MAX_ 件より抽出)",
paginate: {
first: "最初",
last: "最後",
next: "",
previous: ""
},
zeroRecords: "該当するデータがありません"
}
});
} catch (e) {
console.error('[dashboard] DataTable error:', e);
}
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: { data: {
labels: [ labels: utilLabels,
@foreach($cityStats as $stat)
'{{ $stat['city']->city_name }} ({{ $stat['utilization_rate'] }}%)',
@endforeach
],
datasets: [{ datasets: [{
label: '利用率', label: '利用率(%)',
data: [ data: utilData,
@foreach($cityStats as $stat) backgroundColor: genBarColors(utilData.length, 0.8),
{{ $stat['utilization_rate'] }}, borderWidth: 1
@endforeach
],
backgroundColor: [
'rgba(255, 99, 132, 0.8)',
'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: { options: {
responsive: true, responsive: true,
plugins: { maintainAspectRatio: false,
legend: { legend: { display: false },
position: 'bottom', tooltips: {
labels: { callbacks: {
boxWidth: 12, label: function(tooltipItem) {
padding: 10 return '利用率: ' + tooltipItem.yLabel + '%';
}
} }
}, },
tooltip: { scales: {
callbacks: { xAxes: [{ ticks: { autoSkip: false, maxRotation: 60, minRotation: 60, fontSize: 10} }],
label: function(context) { yAxes: [{
var label = context.label.split(' (')[0]; // 自治体名のみ取得 ticks: { beginAtZero: true, max: 100 },
var value = context.parsed; scaleLabel: { display: true, labelString: '利用率(%)' }
var total = context.dataset.data.reduce((a, b) => a + b, 0); }]
var percentage = ((value / total) * 100).toFixed(1);
return label + ': ' + value + '% (全体の' + percentage + '%)';
} }
} }
} };
}
}
});
// 予約待ちチャート // 旧实例があれば破棄(再描画で真っ白になる事故防止)
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');
// ===== 予約待ちbar=====
var c2 = document.getElementById('waitingChart');
if (!c2) {
console.error('[dashboard] waitingChart not found');
return;
}
var waitConfigV2 = {
type: 'bar',
data: { data: {
labels: [ labels: waitLabels,
@foreach($cityStats as $stat)
'{{ $stat['city']->city_name }}',
@endforeach
],
datasets: [{ datasets: [{
label: '予約待ち人数', label: '予約待ち人数',
data: [ data: waitData,
@foreach($cityStats as $stat) backgroundColor: genBarColors(waitData.length, 0.8),
{{ $stat['waiting_count'] }}, borderWidth: 1
@endforeach
],
backgroundColor: [
'rgba(255, 99, 132, 0.8)',
'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)'
]
}] }]
}, },
options: { options: {
responsive: true responsive: true,
maintainAspectRatio: false,
legend: { display: false },
tooltips: {
callbacks: {
label: function(tooltipItem) {
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>
<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 @endpush