415 lines
21 KiB
PHP
415 lines
21 KiB
PHP
<div class="space-y-6" wire:key="reports-component">
|
|
{{-- Page Header --}}
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<flux:heading size="lg">Reports & History</flux:heading>
|
|
<flux:subheading>Generate comprehensive tracking reports and view historical data</flux:subheading>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<flux:button wire:click="generateReport" variant="primary" size="sm" icon="document-arrow-down"
|
|
wire:loading.attr="disabled">
|
|
<span wire:loading.remove>Generate Report</span>
|
|
<span wire:loading>Generating...</span>
|
|
</flux:button>
|
|
<flux:button wire:click="exportData" variant="outline" size="sm" icon="arrow-down-tray">
|
|
Export Data
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Report Configuration --}}
|
|
<div class="bg-white shadow rounded-lg border border-zinc-200">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<flux:heading size="base" class="mb-4">Report Configuration</flux:heading>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Report Type</flux:label>
|
|
<flux:select wire:model.live="reportType" size="sm">
|
|
<option value="summary">Summary Report</option>
|
|
<option value="detailed">Detailed History</option>
|
|
<option value="trips">Trip Analysis</option>
|
|
<option value="stops">Stops Report</option>
|
|
<option value="geofence">Geofence Events</option>
|
|
<option value="speed">Speed Analysis</option>
|
|
<option value="fuel">Fuel Consumption</option>
|
|
</flux:select>
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Device</flux:label>
|
|
<flux:select wire:model.live="selectedDevice" size="sm">
|
|
<option value="">All Devices</option>
|
|
@foreach($devices as $device)
|
|
<option value="{{ $device->id }}">{{ $device->name }}</option>
|
|
@endforeach
|
|
</flux:select>
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Start Date</flux:label>
|
|
<flux:input wire:model.live="startDate" type="datetime-local" size="sm" />
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>End Date</flux:label>
|
|
<flux:input wire:model.live="endDate" type="datetime-local" size="sm" />
|
|
</flux:field>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Quick Date Range</flux:label>
|
|
<div class="grid grid-cols-3 gap-2">
|
|
<flux:button wire:click="setDateRange('today')" variant="ghost" size="xs">Today</flux:button>
|
|
<flux:button wire:click="setDateRange('yesterday')" variant="ghost" size="xs">Yesterday</flux:button>
|
|
<flux:button wire:click="setDateRange('week')" variant="ghost" size="xs">This Week</flux:button>
|
|
<flux:button wire:click="setDateRange('month')" variant="ghost" size="xs">This Month</flux:button>
|
|
<flux:button wire:click="setDateRange('lastMonth')" variant="ghost" size="xs">Last Month</flux:button>
|
|
<flux:button wire:click="setDateRange('custom')" variant="ghost" size="xs">Custom</flux:button>
|
|
</div>
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Output Format</flux:label>
|
|
<flux:select wire:model.live="outputFormat" size="sm">
|
|
<option value="pdf">PDF Report</option>
|
|
<option value="excel">Excel Spreadsheet</option>
|
|
<option value="csv">CSV Data</option>
|
|
<option value="html">HTML View</option>
|
|
</flux:select>
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:field>
|
|
<flux:label>Include Options</flux:label>
|
|
<div class="space-y-2">
|
|
<label class="flex items-center space-x-2">
|
|
<input type="checkbox" wire:model.live="includeMap" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
<span class="text-sm">Include Map</span>
|
|
</label>
|
|
<label class="flex items-center space-x-2">
|
|
<input type="checkbox" wire:model.live="includeCharts" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
<span class="text-sm">Include Charts</span>
|
|
</label>
|
|
<label class="flex items-center space-x-2">
|
|
<input type="checkbox" wire:model.live="includeEvents" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
<span class="text-sm">Include Events</span>
|
|
</label>
|
|
</div>
|
|
</flux:field>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Report Summary Stats --}}
|
|
@if($reportData)
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
|
<flux:icon name="map" class="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-blue-600">Total Distance</p>
|
|
<p class="text-lg font-semibold text-blue-900">{{ number_format($reportData['total_distance'] ?? 0, 1) }} km</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
|
<flux:icon name="clock" class="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-green-600">Total Time</p>
|
|
<p class="text-lg font-semibold text-green-900">{{ $reportData['total_time'] ?? '0h 0m' }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center">
|
|
<flux:icon name="bolt" class="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-yellow-600">Avg Speed</p>
|
|
<p class="text-lg font-semibold text-yellow-900">{{ number_format($reportData['avg_speed'] ?? 0, 1) }} km/h</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
|
|
<flux:icon name="pause" class="w-4 h-4 text-white" />
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-purple-600">Total Stops</p>
|
|
<p class="text-lg font-semibold text-purple-900">{{ $reportData['total_stops'] ?? 0 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Report Content Area --}}
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{{-- Report Display --}}
|
|
<div class="lg:col-span-2">
|
|
<div class="bg-white shadow rounded-lg border border-zinc-200">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<flux:heading size="base">
|
|
{{ ucfirst(str_replace('_', ' ', $reportType)) }} Report
|
|
</flux:heading>
|
|
@if($reportData)
|
|
<div class="text-sm text-gray-500">
|
|
{{ \Carbon\Carbon::parse($startDate)->format('M j, Y') }} -
|
|
{{ \Carbon\Carbon::parse($endDate)->format('M j, Y') }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if($reportData)
|
|
@if($reportType === 'summary')
|
|
@include('livewire.reports.summary-report')
|
|
@elseif($reportType === 'detailed')
|
|
@include('livewire.reports.detailed-report')
|
|
@elseif($reportType === 'trips')
|
|
@include('livewire.reports.trips-report')
|
|
@elseif($reportType === 'stops')
|
|
@include('livewire.reports.stops-report')
|
|
@elseif($reportType === 'geofence')
|
|
@include('livewire.reports.geofence-report')
|
|
@elseif($reportType === 'speed')
|
|
@include('livewire.reports.speed-report')
|
|
@elseif($reportType === 'fuel')
|
|
@include('livewire.reports.fuel-report')
|
|
@endif
|
|
@else
|
|
<div class="text-center py-12">
|
|
<flux:icon name="document-chart-bar" class="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">No Report Generated</h3>
|
|
<p class="text-gray-500 mb-4">Configure your report parameters and click "Generate Report" to view data.</p>
|
|
<flux:button wire:click="generateReport" variant="primary" size="sm">
|
|
Generate Report
|
|
</flux:button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Historical Timeline & Map --}}
|
|
<div class="lg:col-span-1">
|
|
{{-- Map Display --}}
|
|
@if($includeMap && $reportData)
|
|
<div class="bg-white shadow rounded-lg border border-zinc-200 mb-6">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<flux:heading size="base" class="mb-4">Route Map</flux:heading>
|
|
<div id="reportMap" class="w-full h-64 rounded-lg bg-gray-100"></div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Quick Statistics --}}
|
|
<div class="bg-white shadow rounded-lg border border-zinc-200">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<flux:heading size="base" class="mb-4">Quick Statistics</flux:heading>
|
|
|
|
@if($reportData)
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between">
|
|
<span class="text-sm text-gray-600">Max Speed</span>
|
|
<span class="text-sm font-medium text-gray-900">{{ number_format($reportData['max_speed'] ?? 0, 1) }} km/h</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-sm text-gray-600">Min Speed</span>
|
|
<span class="text-sm font-medium text-gray-900">{{ number_format($reportData['min_speed'] ?? 0, 1) }} km/h</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-sm text-gray-600">Total Events</span>
|
|
<span class="text-sm font-medium text-gray-900">{{ $reportData['total_events'] ?? 0 }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-sm text-gray-600">Driving Time</span>
|
|
<span class="text-sm font-medium text-gray-900">{{ $reportData['driving_time'] ?? '0h 0m' }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-sm text-gray-600">Idle Time</span>
|
|
<span class="text-sm font-medium text-gray-900">{{ $reportData['idle_time'] ?? '0h 0m' }}</span>
|
|
</div>
|
|
@if(isset($reportData['fuel_consumption']))
|
|
<div class="flex justify-between">
|
|
<span class="text-sm text-gray-600">Fuel Used</span>
|
|
<span class="text-sm font-medium text-gray-900">{{ number_format($reportData['fuel_consumption'], 2) }} L</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<div class="text-center py-6 text-gray-500">
|
|
<div class="text-sm">Generate a report to view statistics</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Recent Activity --}}
|
|
<div class="bg-white shadow rounded-lg border border-zinc-200 mt-6">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<flux:heading size="base" class="mb-4">Recent Activity</flux:heading>
|
|
|
|
@if($recentEvents && count($recentEvents) > 0)
|
|
<div class="space-y-3">
|
|
@foreach($recentEvents as $event)
|
|
<div class="flex items-start space-x-3">
|
|
<div class="flex-shrink-0">
|
|
@php
|
|
$iconClass = match($event['type']) {
|
|
'geofenceEnter', 'geofenceExit' => 'bg-blue-500',
|
|
'alarm' => 'bg-red-500',
|
|
'ignitionOn', 'ignitionOff' => 'bg-green-500',
|
|
default => 'bg-gray-500'
|
|
};
|
|
@endphp
|
|
<div class="w-2 h-2 rounded-full {{ $iconClass }}"></div>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium text-gray-900">{{ $event['message'] }}</p>
|
|
<p class="text-xs text-gray-500">{{ $event['time'] }}</p>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="text-center py-6 text-gray-500">
|
|
<div class="text-sm">No recent activity</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Load map assets if map is included --}}
|
|
@if($includeMap)
|
|
@assets
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin=""/>
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
@endassets
|
|
|
|
@script
|
|
<script>
|
|
let reportMap = null;
|
|
|
|
function initReportMap() {
|
|
if (reportMap) {
|
|
reportMap.remove();
|
|
}
|
|
|
|
const mapCenter = @json($mapCenter ?? ['lat' => 40.7128, 'lng' => -74.0060]);
|
|
const reportData = @json($reportData);
|
|
|
|
if (document.getElementById('reportMap')) {
|
|
reportMap = L.map('reportMap').setView([mapCenter.lat, mapCenter.lng], 13);
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
maxZoom: 19
|
|
}).addTo(reportMap);
|
|
|
|
// Add route path if available
|
|
if (reportData && reportData.route_coordinates) {
|
|
const coordinates = reportData.route_coordinates.map(coord => [coord.lat, coord.lng]);
|
|
|
|
if (coordinates.length > 0) {
|
|
// Add route line
|
|
L.polyline(coordinates, {
|
|
color: 'blue',
|
|
weight: 3,
|
|
opacity: 0.7
|
|
}).addTo(reportMap);
|
|
|
|
// Add start marker
|
|
L.marker(coordinates[0], {
|
|
icon: L.divIcon({
|
|
className: 'custom-div-icon',
|
|
html: '<div class="bg-green-500 w-4 h-4 rounded-full border-2 border-white"></div>',
|
|
iconSize: [16, 16],
|
|
iconAnchor: [8, 8]
|
|
})
|
|
}).bindPopup('Start').addTo(reportMap);
|
|
|
|
// Add end marker
|
|
if (coordinates.length > 1) {
|
|
L.marker(coordinates[coordinates.length - 1], {
|
|
icon: L.divIcon({
|
|
className: 'custom-div-icon',
|
|
html: '<div class="bg-red-500 w-4 h-4 rounded-full border-2 border-white"></div>',
|
|
iconSize: [16, 16],
|
|
iconAnchor: [8, 8]
|
|
})
|
|
}).bindPopup('End').addTo(reportMap);
|
|
}
|
|
|
|
// Fit map to route
|
|
reportMap.fitBounds(coordinates);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize map when report data is updated
|
|
$wire.on('reportGenerated', () => {
|
|
setTimeout(() => {
|
|
initReportMap();
|
|
}, 100);
|
|
});
|
|
|
|
// Cleanup when component is destroyed
|
|
document.addEventListener('livewire:navigate', function() {
|
|
if (reportMap) {
|
|
reportMap.remove();
|
|
reportMap = null;
|
|
}
|
|
});
|
|
</script>
|
|
@endscript
|
|
@endif
|
|
|
|
<style>
|
|
.custom-div-icon {
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
</style>
|
|
</div>
|