【ダッシュボード】グラフ表示実装
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

View File

@ -257,125 +257,189 @@
@push('scripts')
<script src="{{ asset('plugins/chart.js/Chart.min.js') }}"></script>
<script>
$(document).ready(function() {
// データテーブル初期化
$('#cityStatsTable').DataTable({
"language": {
"url": "//cdn.datatables.net/plug-ins/1.10.21/i18n/Japanese.json"
},
"pageLength": 10,
"order": [[ 6, "desc" ]], // 利用率で降順ソート
"columnDefs": [
{ "orderable": false, "targets": 8 } // 操作列はソート無効
]
});
(function() {
// window.onload で全体を囲むことで、他の onload と競合しないようにする
window.onload = function() {
try {
console.log('[dashboard] start');
// 利用率チャート(円グラフ)
var ctxUtil = document.getElementById('utilizationChart').getContext('2d');
var utilizationChart = new Chart(ctxUtil, {
type: 'pie',
data: {
labels: [
@foreach($cityStats as $stat)
'{{ $stat['city']->city_name }} ({{ $stat['utilization_rate'] }}%)',
@endforeach
],
datasets: [{
label: '利用率',
data: [
@foreach($cityStats as $stat)
{{ $stat['utilization_rate'] }},
@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
}]
// DataTable 初期化
try {
$('#cityStatsTable').DataTable({
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: ""
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 12,
padding: 10
}
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: {
labels: utilLabels,
datasets: [{
label: '利用率(%)',
data: utilData,
backgroundColor: genBarColors(utilData.length, 0.8),
borderWidth: 1
}]
},
tooltip: {
callbacks: {
label: function(context) {
var label = context.label.split(' (')[0]; // 自治体名のみ取得
var value = context.parsed;
var total = context.dataset.data.reduce((a, b) => a + b, 0);
var percentage = ((value / total) * 100).toFixed(1);
return label + ': ' + value + '% (全体の' + percentage + '%)';
options: {
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, max: 100 },
scaleLabel: { display: true, labelString: '利用率(%)' }
}]
}
}
}
}
});
};
// 予約待ちチャート
var ctxWait = document.getElementById('waitingChart').getContext('2d');
var waitingChart = new Chart(ctxWait, {
type: 'doughnut',
data: {
labels: [
@foreach($cityStats as $stat)
'{{ $stat['city']->city_name }}',
@endforeach
],
datasets: [{
label: '予約待ち人数',
data: [
@foreach($cityStats as $stat)
{{ $stat['waiting_count'] }},
@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: {
responsive: true
// 旧实例があれば破棄(再描画で真っ白になる事故防止)
if (window._utilChart && window._utilChart.destroy) window._utilChart.destroy();
window._utilChart = new Chart(c1.getContext('2d'), utilConfigV2);
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: {
labels: waitLabels,
datasets: [{
label: '予約待ち人数',
data: waitData,
backgroundColor: genBarColors(waitData.length, 0.8),
borderWidth: 1
}]
},
options: {
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>
<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