Files
JML/src/app/admin/maintenance/history/battery/[id]/page.tsx

542 lines
23 KiB
TypeScript
Raw Normal View History

'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>
);
}