This commit is contained in:
parent
a118709f9b
commit
db681b219a
@ -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)
|
||||
|
||||
@ -257,125 +257,189 @@
|
||||
@push('scripts')
|
||||
<script src="{{ asset('plugins/chart.js/Chart.min.js') }}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// データテーブル初期化
|
||||
(function() {
|
||||
// window.onload で全体を囲むことで、他の onload と競合しないようにする
|
||||
window.onload = function() {
|
||||
try {
|
||||
console.log('[dashboard] start');
|
||||
|
||||
// DataTable 初期化
|
||||
try {
|
||||
$('#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 }
|
||||
],
|
||||
language: {
|
||||
lengthMenu: " _MENU_ 件表示",
|
||||
search: "検索:",
|
||||
info: "_TOTAL_ 件中 _START_ から _END_ まで表示",
|
||||
infoEmpty: "0 件中 0 から 0 まで表示",
|
||||
infoFiltered: "(全 _MAX_ 件より抽出)",
|
||||
paginate: {
|
||||
first: "最初",
|
||||
last: "最後",
|
||||
next: "次",
|
||||
previous: "前"
|
||||
},
|
||||
"pageLength": 10,
|
||||
"order": [[ 6, "desc" ]], // 利用率で降順ソート
|
||||
"columnDefs": [
|
||||
{ "orderable": false, "targets": 8 } // 操作列はソート無効
|
||||
]
|
||||
zeroRecords: "該当するデータがありません"
|
||||
}
|
||||
});
|
||||
|
||||
// 利用率チャート(円グラフ)
|
||||
var ctxUtil = document.getElementById('utilizationChart').getContext('2d');
|
||||
var utilizationChart = new Chart(ctxUtil, {
|
||||
type: 'pie',
|
||||
} catch (e) {
|
||||
console.error('[dashboard] DataTable error:', e);
|
||||
}
|
||||
|
||||
if (typeof Chart === 'undefined') {
|
||||
console.error('[dashboard] Chart.js is undefined(Chart.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: [
|
||||
@foreach($cityStats as $stat)
|
||||
'{{ $stat['city']->city_name }} ({{ $stat['utilization_rate'] }}%)',
|
||||
@endforeach
|
||||
],
|
||||
labels: utilLabels,
|
||||
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
|
||||
label: '利用率(%)',
|
||||
data: utilData,
|
||||
backgroundColor: genBarColors(utilData.length, 0.8),
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
padding: 10
|
||||
maintainAspectRatio: false,
|
||||
legend: { display: false },
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function(tooltipItem) {
|
||||
return '利用率: ' + tooltipItem.yLabel + '%';
|
||||
}
|
||||
}
|
||||
},
|
||||
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 + '%)';
|
||||
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',
|
||||
// 旧实例があれば破棄(再描画で真っ白になる事故防止)
|
||||
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: [
|
||||
@foreach($cityStats as $stat)
|
||||
'{{ $stat['city']->city_name }}',
|
||||
@endforeach
|
||||
],
|
||||
labels: waitLabels,
|
||||
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)'
|
||||
]
|
||||
data: waitData,
|
||||
backgroundColor: genBarColors(waitData.length, 0.8),
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
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>
|
||||
|
||||
<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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user