krgm.so-manager-dev.com/resources/views/admin/information/dashboard.blade.php
OU.ZAIKOU db681b219a
All checks were successful
Deploy main / deploy (push) Successful in 25s
【ダッシュボード】グラフ表示実装
2026-02-05 01:24:19 +09:00

446 lines
20 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.app')
@section('title', '総合ダッシュボード')
@section('content')
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0 text-dark">総合ダッシュボード</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('information') }}">ホーム</a></li>
<li class="breadcrumb-item active">総合ダッシュボード</li>
</ol>
</div>
</div>
</div>
</div>
<section class="content">
<div class="container-fluid">
<!-- 全体統計情報 -->
<div class="row">
<div class="col-12">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">全体統計</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-lg-2 col-6">
<div class="small-box bg-info">
<div class="inner">
<h3>{{ $totalStats['total_cities'] }}</h3>
<p>自治体数</p>
</div>
<div class="icon">
<i class="fa fa-map-marker"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-success">
<div class="inner">
<h3>{{ $totalStats['total_parks'] }}</h3>
<p>駐輪場数</p>
</div>
<div class="icon">
<i class="fa fa-building"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-warning">
<div class="inner">
<h3>{{ number_format($totalStats['total_contracts']) }}</h3>
<p>総契約数</p>
</div>
<div class="icon">
<i class="fa fa-file-text"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-danger">
<div class="inner">
<h3>{{ number_format($totalStats['total_waiting']) }}</h3>
<p>総予約待ち</p>
</div>
<div class="icon">
<i class="fa fa-clock-o"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-purple">
<div class="inner">
<h3>{{ number_format($totalStats['total_capacity']) }}</h3>
<p>総収容台数</p>
</div>
<div class="icon">
<i class="fa fa-bicycle"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-teal">
<div class="inner">
<h3>{{ $totalStats['total_utilization_rate'] }}%</h3>
<p>全体利用率</p>
</div>
<div class="icon">
<i class="fa fa-pie-chart"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 自治体別統計表 -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">自治体別統計</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fa fa-minus"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover" id="cityStatsTable">
<thead class="thead-dark">
<tr>
<th>自治体名</th>
<th class="text-center">駐輪場数</th>
<th class="text-center">契約数</th>
<th class="text-center">利用者数</th>
<th class="text-center">予約待ち</th>
<th class="text-center">収容台数</th>
<th class="text-center">利用率</th>
<th class="text-center">空き台数</th>
<th class="text-center">操作</th>
</tr>
</thead>
<tbody>
@foreach($cityStats as $stat)
<tr>
<td>
<strong>{{ $stat['city']->city_name }}</strong>
</td>
<td class="text-center">
{{ $stat['parks_count'] }}
</td>
<td class="text-center">
{{ number_format($stat['contracts_count']) }}
</td>
<td class="text-center">
{{ number_format($stat['users_count']) }}
</td>
<td class="text-center">
@if($stat['waiting_count'] > 0)
<span class="badge badge-warning">{{ number_format($stat['waiting_count']) }}</span>
@else
<span class="badge badge-success">0</span>
@endif
</td>
<td class="text-center">
{{ number_format($stat['capacity']) }}
</td>
<td class="text-center">
@php
$rate = $stat['utilization_rate'];
$badgeClass = $rate >= 90 ? 'danger' : ($rate >= 70 ? 'warning' : 'success');
@endphp
<span class="badge badge-{{ $badgeClass }}">{{ $rate }}%</span>
</td>
<td class="text-center">
@if($stat['available_spaces'] > 0)
<span class="text-success">{{ number_format($stat['available_spaces']) }}</span>
@else
<span class="text-danger">満車</span>
@endif
</td>
<td class="text-center">
<a href="{{ route('city_dashboard', ['city_id' => $stat['city']->city_id]) }}"
class="btn btn-sm btn-primary">
<i class="fa fa-eye"></i> 詳細
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 利用率チャート -->
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">自治体別利用率</h3>
</div>
<div class="card-body">
<canvas id="utilizationChart" width="400" height="300"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">予約待ち状況</h3>
</div>
<div class="card-body">
<canvas id="waitingChart" width="400" height="300"></canvas>
</div>
</div>
</div>
</div>
<!-- アラート表示 -->
<div class="row">
<div class="col-12">
<div class="card card-warning">
<div class="card-header">
<h3 class="card-title">⚠️ 注意が必要な自治体</h3>
</div>
<div class="card-body">
@php
$alertCities = collect($cityStats)->filter(function($stat) {
return $stat['utilization_rate'] >= 90 || $stat['waiting_count'] > 10;
});
@endphp
@if($alertCities->count() > 0)
<div class="row">
@foreach($alertCities as $stat)
<div class="col-md-4">
<div class="alert alert-warning">
<h5><i class="icon fa fa-exclamation-triangle"></i> {{ $stat['city']->city_name }}</h5>
@if($stat['utilization_rate'] >= 90)
<p>利用率が {{ $stat['utilization_rate'] }}% と高くなっています</p>
@endif
@if($stat['waiting_count'] > 10)
<p>予約待ちが {{ $stat['waiting_count'] }} います</p>
@endif
<a href="{{ route('city_dashboard', ['city_id' => $stat['city']->city_id]) }}"
class="btn btn-sm btn-warning">詳細確認</a>
</div>
</div>
@endforeach
</div>
@else
<div class="alert alert-success">
<i class="icon fa fa-check"></i> 現在、特に注意が必要な自治体はありません。
</div>
@endif
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
@push('scripts')
<script src="{{ asset('plugins/chart.js/Chart.min.js') }}"></script>
<script>
(function() {
// window.onload で全体を囲むことで、他の onload と競合しないようにする
window.onload = function() {
try {
console.log('[dashboard] start');
// 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: ""
},
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
}]
},
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: '利用率(%)' }
}]
}
}
};
// 旧实例があれば破棄(再描画で真っ白になる事故防止)
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