feat: add battery and bike maintenance history pages and update navigation links to include source tracking
This commit is contained in:
@@ -624,7 +624,7 @@ export default function MaintenanceDetailPage() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{record.batteryId && (
|
{record.batteryId && (
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/batteries/${record.batteryId}`}
|
href={`/admin/maintenance/history/battery/${record.batteryId}?from=${record.id}`}
|
||||||
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
|
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -636,7 +636,7 @@ export default function MaintenanceDetailPage() {
|
|||||||
)}
|
)}
|
||||||
{record.bikeId && (
|
{record.bikeId && (
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/fleet/${record.bikeId}`}
|
href={`/admin/maintenance/history/bike/${record.bikeId}?from=${record.id}`}
|
||||||
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
|
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -646,16 +646,7 @@ export default function MaintenanceDetailPage() {
|
|||||||
<ArrowRight className="w-4 h-4 text-orange-400" />
|
<ArrowRight className="w-4 h-4 text-orange-400" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link
|
|
||||||
href="/admin/maintenance"
|
|
||||||
className="flex items-center justify-between p-2 bg-white rounded-lg hover:bg-orange-50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Wrench className="w-4 h-4 text-blue-600" />
|
|
||||||
<span className="text-sm text-slate-700">All Maintenance</span>
|
|
||||||
</div>
|
|
||||||
<ArrowRight className="w-4 h-4 text-orange-400" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
541
src/app/admin/maintenance/history/battery/[id]/page.tsx
Normal file
541
src/app/admin/maintenance/history/battery/[id]/page.tsx
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, use } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
Wrench, ArrowLeft, Battery, AlertTriangle, Calendar, DollarSign, Clock,
|
||||||
|
CheckCircle, XCircle, Search, RefreshCw, ChevronLeft, ChevronRight,
|
||||||
|
Eye, User, FileText, ArrowRight, ShieldAlert, Sparkles
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface HistoryRecord {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
type: 'damage' | 'maintenance' | 'service' | 'repair' | 'inspection' | 'battery_swap';
|
||||||
|
severity: 'critical' | 'major' | 'minor' | 'cosmetic';
|
||||||
|
status: 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
|
||||||
|
description: string;
|
||||||
|
cost: number;
|
||||||
|
reporter: string;
|
||||||
|
resolvedAt?: string;
|
||||||
|
partsUsed?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BatteryMaintenanceHistoryPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const batteryId = use(params).id;
|
||||||
|
const fromRecord = searchParams.get('from');
|
||||||
|
|
||||||
|
// Realistic mock data for a specific battery's maintenance and damage history
|
||||||
|
const [historyList] = useState<HistoryRecord[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'MNT-003',
|
||||||
|
date: '2024-03-19',
|
||||||
|
type: 'battery_swap',
|
||||||
|
severity: 'minor',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Battery not holding charge properly - swapped under warranty.',
|
||||||
|
cost: 0,
|
||||||
|
reporter: 'Jamal (Biker)',
|
||||||
|
resolvedAt: '2024-03-19',
|
||||||
|
partsUsed: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MNT-009',
|
||||||
|
date: '2024-03-05',
|
||||||
|
type: 'repair',
|
||||||
|
severity: 'major',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Battery port connector pin replacement & calibration.',
|
||||||
|
cost: 1200,
|
||||||
|
reporter: 'Uttara Hub Staff',
|
||||||
|
resolvedAt: '2024-03-06',
|
||||||
|
partsUsed: ['Connector Pins', 'Silicone Seals']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MNT-015',
|
||||||
|
date: '2024-02-15',
|
||||||
|
type: 'service',
|
||||||
|
severity: 'minor',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Cell rebalancing and firmware upgrade for BMS.',
|
||||||
|
cost: 800,
|
||||||
|
reporter: 'Authorized Service Center',
|
||||||
|
resolvedAt: '2024-02-15',
|
||||||
|
partsUsed: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MNT-020',
|
||||||
|
date: '2024-01-22',
|
||||||
|
type: 'damage',
|
||||||
|
severity: 'critical',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Cell thermal runaway inspection due to temperature alert.',
|
||||||
|
cost: 1500,
|
||||||
|
reporter: 'System Alert',
|
||||||
|
resolvedAt: '2024-01-24',
|
||||||
|
partsUsed: ['BMS Module']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MNT-025',
|
||||||
|
date: '2023-12-01',
|
||||||
|
type: 'inspection',
|
||||||
|
severity: 'cosmetic',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Outer plastic protective case scratch audit and hub cleanup.',
|
||||||
|
cost: 0,
|
||||||
|
reporter: 'Kamal Ahmed (Biker)',
|
||||||
|
resolvedAt: '2023-12-01',
|
||||||
|
partsUsed: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client Side Filter & Sorting States
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [typeFilter, setTypeFilter] = useState('all');
|
||||||
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
|
const [dateFrom, setDateFrom] = useState('');
|
||||||
|
const [dateTo, setDateTo] = useState('');
|
||||||
|
const [sortBy, setSortBy] = useState<'date' | 'cost' | 'severity'>('date');
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const pageSize = 5;
|
||||||
|
|
||||||
|
const handleSort = (field: 'date' | 'cost' | 'severity') => {
|
||||||
|
if (sortBy === field) {
|
||||||
|
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
|
||||||
|
} else {
|
||||||
|
setSortBy(field);
|
||||||
|
setSortOrder('desc');
|
||||||
|
}
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logic
|
||||||
|
const filteredList = historyList.filter(item => {
|
||||||
|
if (typeFilter !== 'all') {
|
||||||
|
if (typeFilter === 'damage_all') {
|
||||||
|
if (item.type !== 'damage') return false;
|
||||||
|
} else if (typeFilter === 'damage_cosmetic') {
|
||||||
|
if (item.type !== 'damage' || item.severity !== 'cosmetic') return false;
|
||||||
|
} else if (typeFilter === 'damage_minor') {
|
||||||
|
if (item.type !== 'damage' || item.severity !== 'minor') return false;
|
||||||
|
} else if (typeFilter === 'damage_major') {
|
||||||
|
if (item.type !== 'damage' || item.severity !== 'major') return false;
|
||||||
|
} else if (typeFilter === 'damage_critical') {
|
||||||
|
if (item.type !== 'damage' || item.severity !== 'critical') return false;
|
||||||
|
} else if (typeFilter === 'maintenance_all') {
|
||||||
|
if (item.type === 'damage') return false;
|
||||||
|
} else if (typeFilter === 'maintenance_service') {
|
||||||
|
if (item.type !== 'service') return false;
|
||||||
|
} else if (typeFilter === 'maintenance_repair') {
|
||||||
|
if (item.type !== 'repair') return false;
|
||||||
|
} else if (typeFilter === 'maintenance_inspection') {
|
||||||
|
if (item.type !== 'inspection') return false;
|
||||||
|
} else if (typeFilter === 'maintenance_battery_swap') {
|
||||||
|
if (item.type !== 'battery_swap') return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusFilter !== 'all' && item.status !== statusFilter) return false;
|
||||||
|
if (searchQuery && !item.description.toLowerCase().includes(searchQuery.toLowerCase()) && !item.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||||
|
if (dateFrom && new Date(item.date) < new Date(dateFrom)) return false;
|
||||||
|
if (dateTo && new Date(item.date) > new Date(dateTo)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedList = [...filteredList].sort((a, b) => {
|
||||||
|
let comp = 0;
|
||||||
|
if (sortBy === 'date') {
|
||||||
|
comp = new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||||
|
} else if (sortBy === 'cost') {
|
||||||
|
comp = a.cost - b.cost;
|
||||||
|
} else if (sortBy === 'severity') {
|
||||||
|
const ranks = { critical: 4, major: 3, minor: 2, cosmetic: 1 };
|
||||||
|
comp = ranks[a.severity] - ranks[b.severity];
|
||||||
|
}
|
||||||
|
return sortOrder === 'desc' ? -comp : comp;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
const totalPages = Math.ceil(sortedList.length / pageSize);
|
||||||
|
const paginatedList = sortedList.slice((page - 1) * pageSize, page * pageSize);
|
||||||
|
|
||||||
|
// Status/Severity Badge Colors
|
||||||
|
const severityColors = {
|
||||||
|
critical: 'bg-red-100 text-red-700',
|
||||||
|
major: 'bg-orange-100 text-orange-700',
|
||||||
|
minor: 'bg-amber-100 text-amber-700',
|
||||||
|
cosmetic: 'bg-slate-100 text-slate-700',
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusColors = {
|
||||||
|
reported: 'bg-amber-50 text-amber-700 border border-amber-200',
|
||||||
|
in_progress: 'bg-blue-50 text-blue-700 border border-blue-200',
|
||||||
|
parts_ordered: 'bg-purple-50 text-purple-700 border border-purple-200',
|
||||||
|
completed: 'bg-green-50 text-green-700 border border-green-200',
|
||||||
|
cancelled: 'bg-red-50 text-red-700 border border-red-200',
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalCost = filteredList.reduce((sum, item) => sum + item.cost, 0);
|
||||||
|
const criticalCount = filteredList.filter(item => item.severity === 'critical' || item.severity === 'major').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
|
||||||
|
{/* Back navigation links */}
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (fromRecord) {
|
||||||
|
router.push(`/admin/maintenance/${fromRecord}`);
|
||||||
|
} else {
|
||||||
|
router.push('/admin/maintenance');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4 text-slate-600" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-extrabold text-slate-800 flex items-center gap-2">
|
||||||
|
<Battery className="w-6 h-6 text-green-600" /> Battery History Ledger
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
Viewing comprehensive damage & maintenance history for Battery <span className="font-semibold text-green-600">{batteryId}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Metrics Row */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
|
||||||
|
<Wrench className="w-5 h-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Total Events</p>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{filteredList.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
|
||||||
|
<ShieldAlert className="w-5 h-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Major / Critical</p>
|
||||||
|
<p className="text-lg font-bold text-red-600">{criticalCount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center shrink-0">
|
||||||
|
<DollarSign className="w-5 h-5 text-emerald-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Accumulated Cost</p>
|
||||||
|
<p className="text-lg font-bold text-emerald-600">৳{totalCost.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm bg-gradient-to-br from-teal-50 to-green-50">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-teal-100 rounded-lg flex items-center justify-center shrink-0">
|
||||||
|
<Sparkles className="w-5 h-5 text-teal-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-teal-500">Health Status</p>
|
||||||
|
<p className="text-lg font-bold text-emerald-700">Excellent</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Ledger Content */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
|
||||||
|
|
||||||
|
{/* Advanced Filters */}
|
||||||
|
<div className="p-4 border-b border-slate-100 bg-slate-50/30">
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search description or reference..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
|
||||||
|
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={typeFilter}
|
||||||
|
onChange={(e) => { setTypeFilter(e.target.value); setPage(1); }}
|
||||||
|
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
<option value="all">All Types</option>
|
||||||
|
<optgroup label="Damage Levels">
|
||||||
|
<option value="damage_all">All Damages</option>
|
||||||
|
<option value="damage_cosmetic">Damage - Cosmetic</option>
|
||||||
|
<option value="damage_minor">Damage - Minor</option>
|
||||||
|
<option value="damage_major">Damage - Major</option>
|
||||||
|
<option value="damage_critical">Damage - Critical</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Maintenance Types">
|
||||||
|
<option value="maintenance_all">All Maintenance</option>
|
||||||
|
<option value="maintenance_service">Service</option>
|
||||||
|
<option value="maintenance_repair">Repair</option>
|
||||||
|
<option value="maintenance_inspection">Inspection</option>
|
||||||
|
<option value="maintenance_battery_swap">Battery Swap</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
|
||||||
|
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="all">All Status</option>
|
||||||
|
<option value="reported">Reported</option>
|
||||||
|
<option value="in_progress">In Progress</option>
|
||||||
|
<option value="parts_ordered">Parts Ordered</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
<option value="cancelled">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={dateFrom}
|
||||||
|
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
|
||||||
|
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
|
||||||
|
/>
|
||||||
|
<span className="text-slate-400">to</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={dateTo}
|
||||||
|
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
|
||||||
|
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(dateFrom || dateTo || searchQuery || typeFilter !== 'all' || statusFilter !== 'all') && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery('');
|
||||||
|
setTypeFilter('all');
|
||||||
|
setStatusFilter('all');
|
||||||
|
setDateFrom('');
|
||||||
|
setDateTo('');
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
|
||||||
|
title="Reset Filters"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 text-slate-500" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop View */}
|
||||||
|
<div className="hidden lg:block overflow-x-auto">
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead className="bg-slate-50 border-b border-slate-100">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
onClick={() => handleSort('date')}
|
||||||
|
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
|
||||||
|
>
|
||||||
|
Date {sortBy === 'date' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
Reference ID
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
onClick={() => handleSort('severity')}
|
||||||
|
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
|
||||||
|
>
|
||||||
|
Severity {sortBy === 'severity' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
onClick={() => handleSort('cost')}
|
||||||
|
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right"
|
||||||
|
>
|
||||||
|
Cost {sortBy === 'cost' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider text-center">
|
||||||
|
Action
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody className="divide-y divide-slate-100">
|
||||||
|
{paginatedList.length > 0 ? (
|
||||||
|
paginatedList.map(item => (
|
||||||
|
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
|
||||||
|
<td className="px-4 py-3.5 text-sm text-slate-600 font-medium">
|
||||||
|
{item.date}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-xs font-mono font-semibold text-slate-400">
|
||||||
|
{item.id}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-sm text-slate-700 capitalize font-medium">
|
||||||
|
{item.type.replace('_', ' ')}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5">
|
||||||
|
<span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-semibold ${severityColors[item.severity]}`}>
|
||||||
|
{item.severity}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-sm text-slate-600 max-w-xs">
|
||||||
|
<p className="truncate" title={item.description}>{item.description}</p>
|
||||||
|
{item.partsUsed && item.partsUsed.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{item.partsUsed.map(p => (
|
||||||
|
<span key={p} className="text-[10px] bg-slate-100 text-slate-600 px-1.5 py-0.2 rounded">
|
||||||
|
{p}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-sm font-bold text-slate-800 text-right">
|
||||||
|
৳{item.cost.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold capitalize ${statusColors[item.status]}`}>
|
||||||
|
{item.status.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-center">
|
||||||
|
<Link
|
||||||
|
href={`/admin/maintenance/${item.id}`}
|
||||||
|
className="inline-flex p-1.5 hover:bg-slate-100 rounded-lg text-slate-500 hover:text-slate-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8} className="px-4 py-12 text-center text-slate-400">
|
||||||
|
<AlertTriangle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
|
||||||
|
<p className="text-sm font-semibold">No maintenance logs found</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile View */}
|
||||||
|
<div className="lg:hidden divide-y divide-slate-100">
|
||||||
|
{paginatedList.length > 0 ? (
|
||||||
|
paginatedList.map(item => (
|
||||||
|
<div key={item.id} className="p-4 hover:bg-slate-50 transition-colors">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-slate-400 font-mono font-semibold">{item.id}</span>
|
||||||
|
<h4 className="text-sm font-bold text-slate-800 capitalize mt-0.5">
|
||||||
|
{item.type.replace('_', ' ')}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-sm font-extrabold text-slate-900">৳{item.cost.toLocaleString()}</span>
|
||||||
|
<span className={`block text-[10px] font-semibold mt-1 px-1.5 py-0.5 rounded capitalize ${statusColors[item.status]}`}>
|
||||||
|
{item.status.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-slate-500 mb-2">{item.description}</p>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center text-[11px] text-slate-400">
|
||||||
|
<span>{item.date}</span>
|
||||||
|
<Link
|
||||||
|
href={`/admin/maintenance/${item.id}`}
|
||||||
|
className="text-accent font-semibold hover:underline flex items-center gap-0.5"
|
||||||
|
>
|
||||||
|
Details <ArrowRight className="w-3 h-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="p-8 text-center text-slate-500">
|
||||||
|
<AlertTriangle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
|
||||||
|
<p>No maintenance logs found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Pagination */}
|
||||||
|
{sortedList.length > pageSize && (
|
||||||
|
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3 bg-slate-50/10">
|
||||||
|
<p className="text-xs sm:text-sm text-slate-500">
|
||||||
|
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedList.length)} of {sortedList.length} records
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||||
|
disabled={page === 1}
|
||||||
|
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4 text-slate-600" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
|
||||||
|
<button
|
||||||
|
key={pageNum}
|
||||||
|
onClick={() => setPage(pageNum)}
|
||||||
|
className={`w-8 h-8 rounded-lg text-sm font-semibold transition-colors ${page === pageNum
|
||||||
|
? 'bg-accent text-white'
|
||||||
|
: 'border border-slate-200 hover:bg-slate-50 bg-white text-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||||
|
disabled={page === totalPages}
|
||||||
|
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-4 h-4 text-slate-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
537
src/app/admin/maintenance/history/bike/[id]/page.tsx
Normal file
537
src/app/admin/maintenance/history/bike/[id]/page.tsx
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, use } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
Wrench, ArrowLeft, Bike, AlertTriangle, Calendar, DollarSign, Clock,
|
||||||
|
CheckCircle, XCircle, Search, RefreshCw, ChevronLeft, ChevronRight,
|
||||||
|
Eye, User, FileText, ArrowRight, ShieldAlert, Sparkles
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface HistoryRecord {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
type: 'damage' | 'maintenance' | 'service' | 'repair' | 'inspection';
|
||||||
|
severity: 'critical' | 'major' | 'minor' | 'cosmetic';
|
||||||
|
status: 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
|
||||||
|
description: string;
|
||||||
|
cost: number;
|
||||||
|
reporter: string;
|
||||||
|
resolvedAt?: string;
|
||||||
|
partsUsed?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BikeMaintenanceHistoryPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const bikeId = use(params).id;
|
||||||
|
const fromRecord = searchParams.get('from');
|
||||||
|
|
||||||
|
// Realistic mock data for a specific bike's maintenance and damage history
|
||||||
|
const [historyList] = useState<HistoryRecord[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'MNT-001',
|
||||||
|
date: '2024-03-21',
|
||||||
|
type: 'damage',
|
||||||
|
severity: 'major',
|
||||||
|
status: 'in_progress',
|
||||||
|
description: 'Front fender damaged in minor collision at Gulshan signal.',
|
||||||
|
cost: 3200,
|
||||||
|
reporter: 'Sofiq Rahman (Biker)',
|
||||||
|
partsUsed: ['Front fender', 'Mounting brackets']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MNT-005',
|
||||||
|
date: '2024-03-17',
|
||||||
|
type: 'inspection',
|
||||||
|
severity: 'minor',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Monthly scheduled routine vehicle inspection.',
|
||||||
|
cost: 250,
|
||||||
|
reporter: 'Gulshan Hub Staff',
|
||||||
|
resolvedAt: '2024-03-17',
|
||||||
|
partsUsed: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MNT-012',
|
||||||
|
date: '2024-02-10',
|
||||||
|
type: 'repair',
|
||||||
|
severity: 'critical',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Motor controller overheating check & throttle replacement.',
|
||||||
|
cost: 7500,
|
||||||
|
reporter: 'System Alert',
|
||||||
|
resolvedAt: '2024-02-12',
|
||||||
|
partsUsed: ['Throttle Assembly', 'Controller Fan']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MNT-018',
|
||||||
|
date: '2024-01-15',
|
||||||
|
type: 'service',
|
||||||
|
severity: 'minor',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Chain lubrication, brake shoe calibration, and mirror tightening.',
|
||||||
|
cost: 600,
|
||||||
|
reporter: 'Kamal Ahmed (Biker)',
|
||||||
|
resolvedAt: '2024-01-15',
|
||||||
|
partsUsed: ['Brake Shoe Set']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'MNT-022',
|
||||||
|
date: '2023-12-05',
|
||||||
|
type: 'damage',
|
||||||
|
severity: 'cosmetic',
|
||||||
|
status: 'completed',
|
||||||
|
description: 'Side mirror cracked due to parking slip.',
|
||||||
|
cost: 800,
|
||||||
|
reporter: 'Kamal Ahmed (Biker)',
|
||||||
|
resolvedAt: '2023-12-06',
|
||||||
|
partsUsed: ['Left Side Mirror']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client Side Filter & Sorting States
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [typeFilter, setTypeFilter] = useState('all');
|
||||||
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
|
const [dateFrom, setDateFrom] = useState('');
|
||||||
|
const [dateTo, setDateTo] = useState('');
|
||||||
|
const [sortBy, setSortBy] = useState<'date' | 'cost' | 'severity'>('date');
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const pageSize = 5;
|
||||||
|
|
||||||
|
const handleSort = (field: 'date' | 'cost' | 'severity') => {
|
||||||
|
if (sortBy === field) {
|
||||||
|
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
|
||||||
|
} else {
|
||||||
|
setSortBy(field);
|
||||||
|
setSortOrder('desc');
|
||||||
|
}
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logic
|
||||||
|
const filteredList = historyList.filter(item => {
|
||||||
|
if (typeFilter !== 'all') {
|
||||||
|
if (typeFilter === 'damage_all') {
|
||||||
|
if (item.type !== 'damage') return false;
|
||||||
|
} else if (typeFilter === 'damage_cosmetic') {
|
||||||
|
if (item.type !== 'damage' || item.severity !== 'cosmetic') return false;
|
||||||
|
} else if (typeFilter === 'damage_minor') {
|
||||||
|
if (item.type !== 'damage' || item.severity !== 'minor') return false;
|
||||||
|
} else if (typeFilter === 'damage_major') {
|
||||||
|
if (item.type !== 'damage' || item.severity !== 'major') return false;
|
||||||
|
} else if (typeFilter === 'damage_critical') {
|
||||||
|
if (item.type !== 'damage' || item.severity !== 'critical') return false;
|
||||||
|
} else if (typeFilter === 'maintenance_all') {
|
||||||
|
if (item.type === 'damage') return false;
|
||||||
|
} else if (typeFilter === 'maintenance_service') {
|
||||||
|
if (item.type !== 'service') return false;
|
||||||
|
} else if (typeFilter === 'maintenance_repair') {
|
||||||
|
if (item.type !== 'repair') return false;
|
||||||
|
} else if (typeFilter === 'maintenance_inspection') {
|
||||||
|
if (item.type !== 'inspection') return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusFilter !== 'all' && item.status !== statusFilter) return false;
|
||||||
|
if (searchQuery && !item.description.toLowerCase().includes(searchQuery.toLowerCase()) && !item.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||||
|
if (dateFrom && new Date(item.date) < new Date(dateFrom)) return false;
|
||||||
|
if (dateTo && new Date(item.date) > new Date(dateTo)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedList = [...filteredList].sort((a, b) => {
|
||||||
|
let comp = 0;
|
||||||
|
if (sortBy === 'date') {
|
||||||
|
comp = new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||||
|
} else if (sortBy === 'cost') {
|
||||||
|
comp = a.cost - b.cost;
|
||||||
|
} else if (sortBy === 'severity') {
|
||||||
|
const ranks = { critical: 4, major: 3, minor: 2, cosmetic: 1 };
|
||||||
|
comp = ranks[a.severity] - ranks[b.severity];
|
||||||
|
}
|
||||||
|
return sortOrder === 'desc' ? -comp : comp;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
const totalPages = Math.ceil(sortedList.length / pageSize);
|
||||||
|
const paginatedList = sortedList.slice((page - 1) * pageSize, page * pageSize);
|
||||||
|
|
||||||
|
// Status/Severity Badge Colors
|
||||||
|
const severityColors = {
|
||||||
|
critical: 'bg-red-100 text-red-700',
|
||||||
|
major: 'bg-orange-100 text-orange-700',
|
||||||
|
minor: 'bg-amber-100 text-amber-700',
|
||||||
|
cosmetic: 'bg-slate-100 text-slate-700',
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusColors = {
|
||||||
|
reported: 'bg-amber-50 text-amber-700 border border-amber-200',
|
||||||
|
in_progress: 'bg-blue-50 text-blue-700 border border-blue-200',
|
||||||
|
parts_ordered: 'bg-purple-50 text-purple-700 border border-purple-200',
|
||||||
|
completed: 'bg-green-50 text-green-700 border border-green-200',
|
||||||
|
cancelled: 'bg-red-50 text-red-700 border border-red-200',
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalCost = filteredList.reduce((sum, item) => sum + item.cost, 0);
|
||||||
|
const criticalCount = filteredList.filter(item => item.severity === 'critical' || item.severity === 'major').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
|
||||||
|
{/* Back navigation links */}
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (fromRecord) {
|
||||||
|
router.push(`/admin/maintenance/${fromRecord}`);
|
||||||
|
} else {
|
||||||
|
router.push('/admin/maintenance');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4 text-slate-600" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-extrabold text-slate-800 flex items-center gap-2">
|
||||||
|
<Bike className="w-6 h-6 text-purple-600" /> History Ledger
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
Viewing comprehensive damage & maintenance history for Bike <span className="font-semibold text-purple-600">{bikeId}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Metrics Row */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center shrink-0">
|
||||||
|
<Wrench className="w-5 h-5 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Total Events</p>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{filteredList.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
|
||||||
|
<ShieldAlert className="w-5 h-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Major / Critical</p>
|
||||||
|
<p className="text-lg font-bold text-red-600">{criticalCount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
|
||||||
|
<DollarSign className="w-5 h-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500">Accumulated Cost</p>
|
||||||
|
<p className="text-lg font-bold text-green-600">৳{totalCost.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm bg-gradient-to-br from-indigo-50 to-purple-50">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center shrink-0">
|
||||||
|
<Sparkles className="w-5 h-5 text-indigo-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-indigo-500">Service Status</p>
|
||||||
|
<p className="text-lg font-bold text-indigo-700">Healthy</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Ledger Content */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
|
||||||
|
|
||||||
|
{/* Advanced Filters */}
|
||||||
|
<div className="p-4 border-b border-slate-100 bg-slate-50/30">
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search description or reference..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
|
||||||
|
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={typeFilter}
|
||||||
|
onChange={(e) => { setTypeFilter(e.target.value); setPage(1); }}
|
||||||
|
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
<option value="all">All Types</option>
|
||||||
|
<optgroup label="Damage Levels">
|
||||||
|
<option value="damage_all">All Damages</option>
|
||||||
|
<option value="damage_cosmetic">Damage - Cosmetic</option>
|
||||||
|
<option value="damage_minor">Damage - Minor</option>
|
||||||
|
<option value="damage_major">Damage - Major</option>
|
||||||
|
<option value="damage_critical">Damage - Critical</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Maintenance Types">
|
||||||
|
<option value="maintenance_all">All Maintenance</option>
|
||||||
|
<option value="maintenance_service">Service</option>
|
||||||
|
<option value="maintenance_repair">Repair</option>
|
||||||
|
<option value="maintenance_inspection">Inspection</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
|
||||||
|
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="all">All Status</option>
|
||||||
|
<option value="reported">Reported</option>
|
||||||
|
<option value="in_progress">In Progress</option>
|
||||||
|
<option value="parts_ordered">Parts Ordered</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
<option value="cancelled">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={dateFrom}
|
||||||
|
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
|
||||||
|
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
|
||||||
|
/>
|
||||||
|
<span className="text-slate-400">to</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={dateTo}
|
||||||
|
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
|
||||||
|
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(dateFrom || dateTo || searchQuery || typeFilter !== 'all' || statusFilter !== 'all') && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery('');
|
||||||
|
setTypeFilter('all');
|
||||||
|
setStatusFilter('all');
|
||||||
|
setDateFrom('');
|
||||||
|
setDateTo('');
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
|
||||||
|
title="Reset Filters"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 text-slate-500" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop View */}
|
||||||
|
<div className="hidden lg:block overflow-x-auto">
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead className="bg-slate-50 border-b border-slate-100">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
onClick={() => handleSort('date')}
|
||||||
|
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
|
||||||
|
>
|
||||||
|
Date {sortBy === 'date' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
Reference ID
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
onClick={() => handleSort('severity')}
|
||||||
|
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
|
||||||
|
>
|
||||||
|
Severity {sortBy === 'severity' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
onClick={() => handleSort('cost')}
|
||||||
|
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right"
|
||||||
|
>
|
||||||
|
Cost {sortBy === 'cost' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider text-center">
|
||||||
|
Action
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody className="divide-y divide-slate-100">
|
||||||
|
{paginatedList.length > 0 ? (
|
||||||
|
paginatedList.map(item => (
|
||||||
|
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
|
||||||
|
<td className="px-4 py-3.5 text-sm text-slate-600 font-medium">
|
||||||
|
{item.date}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-xs font-mono font-semibold text-slate-400">
|
||||||
|
{item.id}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-sm text-slate-700 capitalize font-medium">
|
||||||
|
{item.type.replace('_', ' ')}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5">
|
||||||
|
<span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-semibold ${severityColors[item.severity]}`}>
|
||||||
|
{item.severity}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-sm text-slate-600 max-w-xs">
|
||||||
|
<p className="truncate" title={item.description}>{item.description}</p>
|
||||||
|
{item.partsUsed && item.partsUsed.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{item.partsUsed.map(p => (
|
||||||
|
<span key={p} className="text-[10px] bg-slate-100 text-slate-600 px-1.5 py-0.2 rounded">
|
||||||
|
{p}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-sm font-bold text-slate-800 text-right">
|
||||||
|
৳{item.cost.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold capitalize ${statusColors[item.status]}`}>
|
||||||
|
{item.status.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3.5 text-center">
|
||||||
|
<Link
|
||||||
|
href={`/admin/maintenance/${item.id}`}
|
||||||
|
className="inline-flex p-1.5 hover:bg-slate-100 rounded-lg text-slate-500 hover:text-slate-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8} className="px-4 py-12 text-center text-slate-400">
|
||||||
|
<AlertTriangle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
|
||||||
|
<p className="text-sm font-semibold">No maintenance logs found</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile View */}
|
||||||
|
<div className="lg:hidden divide-y divide-slate-100">
|
||||||
|
{paginatedList.length > 0 ? (
|
||||||
|
paginatedList.map(item => (
|
||||||
|
<div key={item.id} className="p-4 hover:bg-slate-50 transition-colors">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-slate-400 font-mono font-semibold">{item.id}</span>
|
||||||
|
<h4 className="text-sm font-bold text-slate-800 capitalize mt-0.5">
|
||||||
|
{item.type.replace('_', ' ')}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-sm font-extrabold text-slate-900">৳{item.cost.toLocaleString()}</span>
|
||||||
|
<span className={`block text-[10px] font-semibold mt-1 px-1.5 py-0.5 rounded capitalize ${statusColors[item.status]}`}>
|
||||||
|
{item.status.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-slate-500 mb-2">{item.description}</p>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center text-[11px] text-slate-400">
|
||||||
|
<span>{item.date}</span>
|
||||||
|
<Link
|
||||||
|
href={`/admin/maintenance/${item.id}`}
|
||||||
|
className="text-accent font-semibold hover:underline flex items-center gap-0.5"
|
||||||
|
>
|
||||||
|
Details <ArrowRight className="w-3 h-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="p-8 text-center text-slate-500">
|
||||||
|
<AlertTriangle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
|
||||||
|
<p>No maintenance logs found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Pagination */}
|
||||||
|
{sortedList.length > pageSize && (
|
||||||
|
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3 bg-slate-50/10">
|
||||||
|
<p className="text-xs sm:text-sm text-slate-500">
|
||||||
|
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedList.length)} of {sortedList.length} records
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||||
|
disabled={page === 1}
|
||||||
|
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4 text-slate-600" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
|
||||||
|
<button
|
||||||
|
key={pageNum}
|
||||||
|
onClick={() => setPage(pageNum)}
|
||||||
|
className={`w-8 h-8 rounded-lg text-sm font-semibold transition-colors ${page === pageNum
|
||||||
|
? 'bg-accent text-white'
|
||||||
|
: 'border border-slate-200 hover:bg-slate-50 bg-white text-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||||
|
disabled={page === totalPages}
|
||||||
|
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-4 h-4 text-slate-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user