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)
|
// ステータス一括更新(着手=2 / 対応完了=3)
|
||||||
|
|||||||
@ -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 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: 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
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user