Compare commits
2 Commits
fb1eff4931
...
8f445857a9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f445857a9 | ||
|
|
440a87f0b5 |
@@ -862,7 +862,7 @@ export default function BikerDetailPage() {
|
|||||||
<ArrowLeft className="w-4 h-4" /> Back to Bikers
|
<ArrowLeft className="w-4 h-4" /> Back to Bikers
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-4">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-8">
|
||||||
<div className="p-4 lg:p-6 flex flex-col lg:flex-row lg:items-center gap-4">
|
<div className="p-4 lg:p-6 flex flex-col lg:flex-row lg:items-center gap-4">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
{biker.profileImage ? (
|
{biker.profileImage ? (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key,
|
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key,
|
||||||
CheckCircle, XCircle, ChevronLeft, Save, Printer, Send, QrCode,
|
CheckCircle, XCircle, ChevronLeft, Save, Printer, Send, QrCode,
|
||||||
Wallet, Building, Edit, MessageSquare, Calendar, ArrowLeft, Trash2,
|
Wallet, Building, Edit, MessageSquare, Calendar, ArrowLeft, Trash2,
|
||||||
Package, Settings
|
Package, Settings, History, ArrowRight
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
@@ -284,7 +284,6 @@ export default function MaintenanceDetailPage() {
|
|||||||
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
|
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
|
||||||
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
|
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
|
||||||
const [showAddPartModal, setShowAddPartModal] = useState(false);
|
const [showAddPartModal, setShowAddPartModal] = useState(false);
|
||||||
const [showAddServiceCostModal, setShowAddServiceCostModal] = useState(false);
|
|
||||||
const [partSearch, setPartSearch] = useState('');
|
const [partSearch, setPartSearch] = useState('');
|
||||||
const [invoiceData, setInvoiceData] = useState({ tips: 0, discount: 0 });
|
const [invoiceData, setInvoiceData] = useState({ tips: 0, discount: 0 });
|
||||||
const [invoiceCreated, setInvoiceCreated] = useState(false);
|
const [invoiceCreated, setInvoiceCreated] = useState(false);
|
||||||
@@ -295,7 +294,6 @@ export default function MaintenanceDetailPage() {
|
|||||||
const [actualCost, setActualCost] = useState('');
|
const [actualCost, setActualCost] = useState('');
|
||||||
const [selectedPart, setSelectedPart] = useState<EVPart | null>(null);
|
const [selectedPart, setSelectedPart] = useState<EVPart | null>(null);
|
||||||
const [partQuantity, setPartQuantity] = useState(1);
|
const [partQuantity, setPartQuantity] = useState(1);
|
||||||
const [serviceCostInput, setServiceCostInput] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const found = mockMaintenance.find(r => r.id === id);
|
const found = mockMaintenance.find(r => r.id === id);
|
||||||
@@ -457,12 +455,12 @@ export default function MaintenanceDetailPage() {
|
|||||||
<ArrowLeft className="w-4 h-4" /> Back to Maintenance
|
<ArrowLeft className="w-4 h-4" /> Back to Maintenance
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-12 lg:mb-0">
|
||||||
<div className="p-6 border-b border-slate-100">
|
<div className="p-6 border-b border-slate-100">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<h1 className="text-2xl font-extrabold text-slate-800">{record.id}</h1>
|
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">{record.id}</h1>
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${severityColors[record.severity]}`}>
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${severityColors[record.severity]}`}>
|
||||||
{record.severity}
|
{record.severity}
|
||||||
</span>
|
</span>
|
||||||
@@ -475,7 +473,7 @@ export default function MaintenanceDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-slate-500 mt-1">{typeLabels[record.type]} • {record.date}</p>
|
<p className="text-slate-500 mt-1">{typeLabels[record.type]} • {record.date}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<>
|
<>
|
||||||
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
|
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
|
||||||
@@ -619,6 +617,48 @@ export default function MaintenanceDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100">
|
||||||
|
<h3 className="font-semibold text-orange-800 mb-3 flex items-center gap-2">
|
||||||
|
<History className="w-5 h-5" /> Issue History
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{record.batteryId && (
|
||||||
|
<Link
|
||||||
|
href={`/admin/batteries/${record.batteryId}`}
|
||||||
|
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">
|
||||||
|
<Battery className="w-4 h-4 text-green-600" />
|
||||||
|
<span className="text-sm text-slate-700">Battery History</span>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="w-4 h-4 text-orange-400" />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{record.bikeId && (
|
||||||
|
<Link
|
||||||
|
href={`/admin/fleet/${record.bikeId}`}
|
||||||
|
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">
|
||||||
|
<Bike className="w-4 h-4 text-purple-600" />
|
||||||
|
<span className="text-sm text-slate-700">Fleet History</span>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="w-4 h-4 text-orange-400" />
|
||||||
|
</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 className="bg-green-50 p-4 rounded-xl border border-green-100">
|
<div className="bg-green-50 p-4 rounded-xl border border-green-100">
|
||||||
<h3 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
|
<h3 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
|
||||||
<User className="w-5 h-5" /> Reporter
|
<User className="w-5 h-5" /> Reporter
|
||||||
@@ -790,8 +830,10 @@ export default function MaintenanceDetailPage() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
{(record.partsUsed || []).length > 0 ? (
|
{record.partsUsed && record.partsUsed.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="hidden lg:block overflow-x-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="bg-orange-100">
|
<thead className="bg-orange-100">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -843,19 +885,67 @@ export default function MaintenanceDetailPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot className="bg-orange-50">
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="px-3 py-2 text-right font-semibold text-orange-800">Parts Total:</td>
|
|
||||||
<td className="px-3 py-2 text-right font-bold text-orange-700">
|
|
||||||
৳{record.partsUsed?.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString()}
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="lg:hidden grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{record.partsUsed.map((part) => (
|
||||||
|
<div key={part.id} className="bg-white rounded-lg border border-orange-200 p-3">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<span className="font-medium text-slate-800">{part.partName}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setRecord(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
partsUsed: prev.partsUsed?.filter(p => p.id !== part.id)
|
||||||
|
} : null);
|
||||||
|
}}
|
||||||
|
className="text-red-400 hover:text-red-600 p-1"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-slate-500">Qty:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={part.quantity}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newQty = Math.max(1, parseInt(e.target.value) || 1);
|
||||||
|
setRecord(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
partsUsed: prev.partsUsed?.map(p =>
|
||||||
|
p.id === part.id
|
||||||
|
? { ...p, quantity: newQty, totalPrice: newQty * p.unitPrice }
|
||||||
|
: p
|
||||||
|
)
|
||||||
|
} : null);
|
||||||
|
}}
|
||||||
|
className="w-16 px-2 py-1 border border-orange-200 rounded text-center text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-slate-600">৳{part.unitPrice.toLocaleString()}/unit</span>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 border-t border-orange-100 flex justify-between">
|
||||||
|
<span className="text-xs text-slate-500">Total</span>
|
||||||
|
<span className="font-bold text-orange-700">৳{part.totalPrice.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-orange-400">No parts added</p>
|
<p className="text-sm text-orange-400">No parts added</p>
|
||||||
)}
|
)}
|
||||||
|
{record.partsUsed && record.partsUsed.length > 0 && (
|
||||||
|
<div className="bg-orange-50 rounded-lg p-3 flex justify-between items-center">
|
||||||
|
<span className="font-semibold text-orange-800">Parts Total:</span>
|
||||||
|
<span className="text-lg font-bold text-orange-700">
|
||||||
|
৳{record.partsUsed.reduce((sum, p) => sum + p.totalPrice, 0).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -922,17 +1012,22 @@ export default function MaintenanceDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div className="bg-slate-50 p-3 rounded-lg">
|
<div className="bg-slate-50 p-3 rounded-lg">
|
||||||
<div className="flex justify-between text-sm mb-2">
|
|
||||||
<span className="text-slate-500">Estimated Cost:</span>
|
|
||||||
<span className="font-medium text-slate-600">৳{record.estimatedCost.toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm mb-2">
|
<div className="flex justify-between text-sm mb-2">
|
||||||
<span className="text-slate-500">Parts Total:</span>
|
<span className="text-slate-500">Parts Total:</span>
|
||||||
<span className="font-medium text-orange-600">৳{record.partsUsed?.reduce((s, p) => s + p.totalPrice, 0).toLocaleString()}</span>
|
<span className="font-medium text-orange-600">৳{record.partsUsed?.reduce((s, p) => s + p.totalPrice, 0).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm mb-2">
|
<div>
|
||||||
<span className="text-slate-500">Service Cost (Labor):</span>
|
<label className="text-xs font-medium text-blue-600 mb-1 block">Service Cost (Labor)</label>
|
||||||
<span className="font-medium text-blue-600">৳{(record.serviceCost || 0).toLocaleString()}</span>
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={record.serviceCost || ''}
|
||||||
|
onChange={(e) => setRecord(prev => prev ? { ...prev, serviceCost: parseInt(e.target.value) || 0 } : null)}
|
||||||
|
className="flex-1 px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
||||||
|
placeholder="Enter labor cost"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1256,59 +1351,6 @@ export default function MaintenanceDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="fixed bottom-4 right-4 z-40">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAddServiceCostModal(true)}
|
|
||||||
className="px-4 py-3 bg-blue-600 text-white rounded-full shadow-lg hover:bg-blue-700 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<DollarSign className="w-5 h-5" /> Add Service Cost
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showAddServiceCostModal && (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm">
|
|
||||||
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
|
||||||
<h3 className="font-semibold text-slate-800 flex items-center gap-2">
|
|
||||||
<Wrench className="w-5 h-5 text-blue-600" /> Add Service Cost
|
|
||||||
</h3>
|
|
||||||
<button onClick={() => setShowAddServiceCostModal(false)} className="text-slate-400 hover:text-slate-600">
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-600 mb-1 block">Service Cost (Labor charge)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={serviceCostInput}
|
|
||||||
onChange={(e) => setServiceCostInput(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-lg"
|
|
||||||
placeholder="Enter service cost"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-50 p-3 rounded-lg">
|
|
||||||
<p className="text-sm text-slate-600">Current Service Cost: <span className="font-bold text-blue-600">৳{record?.serviceCost || 0}</span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
|
||||||
<button onClick={() => setShowAddServiceCostModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const cost = parseFloat(serviceCostInput) || 0;
|
|
||||||
setRecord(prev => prev ? { ...prev, serviceCost: cost } : null);
|
|
||||||
setShowAddServiceCostModal(false);
|
|
||||||
setServiceCostInput('');
|
|
||||||
}}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" /> Add Cost
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showPaymentSuccess && (
|
{showPaymentSuccess && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone,
|
AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone,
|
||||||
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key,
|
MapPin, FileText, Image as ImageIcon, DollarSign, Wrench, Battery, Key,
|
||||||
CheckCircle, XCircle, ChevronDown, ChevronUp, Download, Eye, Edit,
|
CheckCircle, XCircle, ChevronDown, ChevronUp, Download, Eye, Edit,
|
||||||
MessageSquare, Filter, Calendar, Save, Printer, Send
|
MessageSquare, Filter, Calendar, Save, Printer, Send, Activity
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
type TransactionType = 'deposit' | 'rent_income' | 'investor_funding' | 'investor_withdraw' | 'salary' | 'rent_expense' | 'utility' | 'maintenance' | 'bike_purchase' | 'bike_sale' | 'other_income' | 'other_expense';
|
type TransactionType = 'deposit' | 'rent_income' | 'investor_funding' | 'investor_withdraw' | 'salary' | 'rent_expense' | 'utility' | 'maintenance' | 'bike_purchase' | 'bike_sale' | 'other_income' | 'other_expense';
|
||||||
@@ -245,7 +246,9 @@ const typeIcons: Record<string, any> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function MaintenancePage() {
|
export default function MaintenancePage() {
|
||||||
const [activeTab, setActiveTab] = useState<'all' | MaintenanceType>('all');
|
const router = useRouter();
|
||||||
|
const [mainCategory, setMainCategory] = useState<'damage' | 'maintenance'>('damage');
|
||||||
|
const [targetType, setTargetType] = useState<'all' | 'battery' | 'fleet'>('all');
|
||||||
const [records, setRecords] = useState<MaintenanceRecord[]>(mockMaintenance);
|
const [records, setRecords] = useState<MaintenanceRecord[]>(mockMaintenance);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
@@ -259,20 +262,45 @@ export default function MaintenancePage() {
|
|||||||
const [expandedNotes, setExpandedNotes] = useState<string[]>([]);
|
const [expandedNotes, setExpandedNotes] = useState<string[]>([]);
|
||||||
const [newNoteText, setNewNoteText] = useState('');
|
const [newNoteText, setNewNoteText] = useState('');
|
||||||
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
|
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
|
||||||
|
const [reportType, setReportType] = useState<'damage' | 'maintenance'>('damage');
|
||||||
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
|
|
||||||
const filteredRecords = records.filter(r => {
|
const filteredRecords = records.filter(r => {
|
||||||
const matchesTab = activeTab === 'all' || r.type === activeTab;
|
const isDamage = r.type === 'damage';
|
||||||
|
const matchesCategory = mainCategory === 'damage' ? isDamage : !isDamage;
|
||||||
|
const matchesTarget = targetType === 'all' ||
|
||||||
|
(targetType === 'battery' && r.batteryId) ||
|
||||||
|
(targetType === 'fleet' && r.bikeId && !r.batteryId);
|
||||||
const matchesSearch = !searchQuery ||
|
const matchesSearch = !searchQuery ||
|
||||||
r.bikeId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
r.bikeId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
r.bikePlate.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
r.bikePlate.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
r.reporterName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
r.reporterName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
r.id.toLowerCase().includes(searchQuery.toLowerCase());
|
r.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
(r.batteryId && r.batteryId.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||||
const matchesStatus = statusFilter === 'all' || r.status === statusFilter;
|
const matchesStatus = statusFilter === 'all' || r.status === statusFilter;
|
||||||
return matchesTab && matchesSearch && matchesStatus;
|
return matchesCategory && matchesTarget && matchesSearch && matchesStatus;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const damageRecords = records.filter(r => r.type === 'damage');
|
||||||
|
const maintenanceRecords = records.filter(r => r.type !== 'damage');
|
||||||
|
const currentMonth = new Date().toISOString().slice(0, 7);
|
||||||
const stats = {
|
const stats = {
|
||||||
|
damageCount: damageRecords.length,
|
||||||
|
maintenanceCount: maintenanceRecords.length,
|
||||||
|
damageThisMonth: damageRecords.filter(r => r.date?.slice(0, 7) === currentMonth).length,
|
||||||
|
maintenanceThisMonth: maintenanceRecords.filter(r => r.date?.slice(0, 7) === currentMonth).length,
|
||||||
|
completedThisMonth: records.filter(r => r.status === 'completed' && r.resolvedAt?.slice(0, 7) === currentMonth).length,
|
||||||
|
batteryDamage: damageRecords.filter(r => r.batteryId).length,
|
||||||
|
fleetDamage: damageRecords.filter(r => r.bikeId && !r.batteryId).length,
|
||||||
|
batteryMaintenance: maintenanceRecords.filter(r => r.batteryId).length,
|
||||||
|
fleetMaintenance: maintenanceRecords.filter(r => r.bikeId && !r.batteryId).length,
|
||||||
|
upcomingBattery: maintenanceRecords.filter(r => r.batteryId && r.status === 'reported').length,
|
||||||
|
upcomingFleet: maintenanceRecords.filter(r => r.bikeId && !r.batteryId && r.status === 'reported').length,
|
||||||
|
ongoingBattery: maintenanceRecords.filter(r => r.batteryId && r.status === 'in_progress').length,
|
||||||
|
ongoingFleet: maintenanceRecords.filter(r => r.bikeId && !r.batteryId && r.status === 'in_progress').length,
|
||||||
|
pendingMaintenance: maintenanceRecords.filter(r => r.status === 'reported' || r.status === 'in_progress').length,
|
||||||
|
completedMaintenance: maintenanceRecords.filter(r => r.status === 'completed').length,
|
||||||
critical: records.filter(r => r.severity === 'critical' && r.status !== 'completed').length,
|
critical: records.filter(r => r.severity === 'critical' && r.status !== 'completed').length,
|
||||||
inProgress: records.filter(r => r.status === 'in_progress' || r.status === 'parts_ordered').length,
|
inProgress: records.filter(r => r.status === 'in_progress' || r.status === 'parts_ordered').length,
|
||||||
completed: records.filter(r => r.status === 'completed').length,
|
completed: records.filter(r => r.status === 'completed').length,
|
||||||
@@ -397,7 +425,7 @@ export default function MaintenancePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 lg:p-6">
|
<div className="p-4 lg:p-6 mb-6 lg:mb-0">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Damage & Maintenance</h1>
|
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Damage & Maintenance</h1>
|
||||||
@@ -413,36 +441,45 @@ export default function MaintenancePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<div className="bg-white rounded-xl p-5 shadow-sm border border-red-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center">
|
||||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
<AlertTriangle className="w-6 h-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<p className="text-2xl font-extrabold text-slate-800">{stats.critical}</p>
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
<p className="text-sm text-slate-500">Critical</p>
|
<p className="text-2xl font-extrabold text-slate-800">{stats.damageCount}</p>
|
||||||
|
<span className="text-xs text-red-600 font-medium bg-red-50 px-2 py-0.5 rounded">{stats.damageThisMonth} this month</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-500">Total Damage</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<div className="bg-white rounded-xl p-5 shadow-sm border border-blue-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
|
||||||
<Wrench className="w-6 h-6 text-blue-600" />
|
<Wrench className="w-6 h-6 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<p className="text-2xl font-extrabold text-slate-800">{stats.inProgress}</p>
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
<p className="text-sm text-slate-500">In Progress</p>
|
<p className="text-2xl font-extrabold text-slate-800">{stats.maintenanceCount}</p>
|
||||||
|
<span className="text-xs text-blue-600 font-medium bg-blue-50 px-2 py-0.5 rounded">{stats.maintenanceThisMonth} this month</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-500">Total Maintenance</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<div className="bg-white rounded-xl p-5 shadow-sm border border-green-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center">
|
||||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1">
|
||||||
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
<p className="text-2xl font-extrabold text-slate-800">{stats.completed}</p>
|
<p className="text-2xl font-extrabold text-slate-800">{stats.completed}</p>
|
||||||
|
<span className="text-xs text-green-600 font-medium bg-green-50 px-2 py-0.5 rounded">{stats.completedThisMonth} this month</span>
|
||||||
|
</div>
|
||||||
<p className="text-sm text-slate-500">Completed</p>
|
<p className="text-sm text-slate-500">Completed</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -458,7 +495,7 @@ export default function MaintenancePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
{/* <div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 rounded-xl bg-purple-50 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-xl bg-purple-50 flex items-center justify-center">
|
||||||
<DollarSign className="w-6 h-6 text-purple-600" />
|
<DollarSign className="w-6 h-6 text-purple-600" />
|
||||||
@@ -468,43 +505,145 @@ export default function MaintenancePage() {
|
|||||||
<p className="text-sm text-slate-500">Total Cost</p>
|
<p className="text-sm text-slate-500">Total Cost</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mainCategory === 'damage' && (
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="bg-red-50 rounded-xl p-4 border border-red-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{stats.damageCount}</p>
|
||||||
|
<p className="text-sm text-slate-500">Total Damage</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 rounded-xl p-4 border border-green-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
|
||||||
|
<Battery className="w-5 h-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{stats.batteryDamage}</p>
|
||||||
|
<p className="text-sm text-slate-500">Battery Damage</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-purple-50 rounded-xl p-4 border border-purple-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
|
||||||
|
<Bike className="w-5 h-5 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{stats.fleetDamage}</p>
|
||||||
|
<p className="text-sm text-slate-500">Fleet Damage</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-orange-50 rounded-xl p-4 border border-orange-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
|
||||||
|
<Clock className="w-5 h-5 text-orange-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{stats.critical}</p>
|
||||||
|
<p className="text-sm text-slate-500">Critical Damage</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mainCategory === 'maintenance' && (
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="bg-blue-50 rounded-xl p-4 border border-blue-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||||
|
<Wrench className="w-5 h-5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{stats.maintenanceCount}</p>
|
||||||
|
<p className="text-sm text-slate-500">Total Maintenance</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-purple-50 rounded-xl p-4 border border-purple-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
|
||||||
|
<Bike className="w-5 h-5 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{stats.upcomingFleet}</p>
|
||||||
|
<p className="text-sm text-slate-500">Upcoming Fleet</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 rounded-xl p-4 border border-green-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
|
||||||
|
<Battery className="w-5 h-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{stats.upcomingBattery}</p>
|
||||||
|
<p className="text-sm text-slate-500">Upcoming Battery</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-orange-50 rounded-xl p-4 border border-orange-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
|
||||||
|
<Activity className="w-5 h-5 text-orange-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-slate-800">{stats.ongoingBattery + stats.ongoingFleet}</p>
|
||||||
|
<p className="text-sm text-slate-500">Ongoing Maintenance</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-6">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-6">
|
||||||
<div className="p-4 border-b border-slate-100">
|
<div className="p-4 border-b border-slate-100">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
|
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
||||||
|
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('all')}
|
onClick={() => setMainCategory('damage')}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium ${activeTab === 'all' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 ${mainCategory === 'damage' ? 'bg-red-600 text-white shadow-sm' : 'text-slate-600 hover:bg-white hover:shadow-sm'}`}
|
||||||
>
|
|
||||||
All
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('damage')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'damage' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
|
||||||
>
|
>
|
||||||
<AlertTriangle className="w-4 h-4" /> Damage
|
<AlertTriangle className="w-4 h-4" /> Damage
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('repair')}
|
onClick={() => setMainCategory('maintenance')}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'repair' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 ${mainCategory === 'maintenance' ? 'bg-blue-600 text-white shadow-sm' : 'text-slate-600 hover:bg-white hover:shadow-sm'}`}
|
||||||
>
|
>
|
||||||
<Wrench className="w-4 h-4" /> Repair
|
<Wrench className="w-4 h-4" /> Maintenance
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setTargetType('all')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium ${targetType === 'all' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
||||||
|
>
|
||||||
|
All
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('service')}
|
onClick={() => setTargetType('battery')}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'service' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${targetType === 'battery' ? 'bg-green-600 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
||||||
>
|
|
||||||
<Wrench className="w-4 h-4" /> Service
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('battery_swap')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'battery_swap' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
|
||||||
>
|
>
|
||||||
<Battery className="w-4 h-4" /> Battery
|
<Battery className="w-4 h-4" /> Battery
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setTargetType('fleet')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${targetType === 'fleet' ? 'bg-purple-600 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
|
||||||
|
>
|
||||||
|
<Bike className="w-4 h-4" /> Fleet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -537,47 +676,59 @@ export default function MaintenancePage() {
|
|||||||
{filteredRecords.map(record => {
|
{filteredRecords.map(record => {
|
||||||
const TypeIcon = typeIcons[record.type];
|
const TypeIcon = typeIcons[record.type];
|
||||||
return (
|
return (
|
||||||
<Link key={record.id} href={`/admin/maintenance/${record.id}`} className="block p-5 hover:bg-slate-50 transition-colors">
|
<Link key={record.id} href={`/admin/maintenance/${record.id}`} className="block p-4 lg:p-5 hover:bg-slate-50 transition-colors">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-start gap-4">
|
<div className="flex flex-col lg:flex-row lg:items-start gap-3 lg:gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-start gap-3">
|
||||||
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center">
|
<div className="w-10 h-10 lg:w-12 lg:h-12 rounded-lg lg:rounded-xl bg-slate-100 flex items-center justify-center flex-shrink-0">
|
||||||
<TypeIcon className="w-6 h-6 text-slate-600" />
|
<TypeIcon className="w-5 h-5 lg:w-6 lg:h-6 text-slate-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-1.5 lg:gap-2">
|
||||||
<p className="font-semibold text-slate-800">{record.id}</p>
|
<p className="font-semibold text-slate-800 text-sm lg:text-base">{record.id}</p>
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${severityColors[record.severity]}`}>
|
{record.batteryId && (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-full bg-green-100 text-green-700">
|
||||||
|
<Battery className="w-3 h-3" /> Battery
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!record.batteryId && record.bikeId && (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-700">
|
||||||
|
<Bike className="w-3 h-3" /> Fleet
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-full ${severityColors[record.severity]}`}>
|
||||||
{record.severity}
|
{record.severity}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-500 flex items-center gap-2">
|
<p className="text-xs lg:text-sm text-slate-500 flex flex-wrap items-center gap-x-1 lg:gap-x-2">
|
||||||
<Bike className="w-3 h-3" /> {record.bikeModel} ({record.bikePlate})
|
<span className="flex items-center gap-1"><Bike className="w-3 h-3" /> {record.bikeModel}</span>
|
||||||
<span className="text-slate-300">|</span>
|
<span className="hidden sm:inline text-slate-300">|</span>
|
||||||
<User className="w-3 h-3" /> {record.reporterName}
|
<span className="text-xs">{record.bikePlate}</span>
|
||||||
|
<span className="hidden lg:inline text-slate-300">|</span>
|
||||||
|
<span className="flex items-center gap-1"><User className="w-3 h-3" /> {record.reporterName}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm text-slate-700">{record.description}</p>
|
<p className="text-sm text-slate-700 line-clamp-2">{record.description}</p>
|
||||||
<div className="flex flex-wrap gap-4 text-sm text-slate-500 mt-1">
|
<div className="flex flex-wrap gap-2 lg:gap-4 text-xs lg:text-sm text-slate-500 mt-1.5">
|
||||||
<p className="flex items-center gap-1">
|
<p className="flex items-center gap-1">
|
||||||
<Clock className="w-3 h-3" /> {record.date}
|
<Clock className="w-3 h-3" /> {record.date}
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center gap-1">
|
<p className="flex items-center gap-1 truncate max-w-[100px] lg:max-w-none">
|
||||||
<MapPin className="w-3 h-3" /> {record.location}
|
<MapPin className="w-3 h-3 flex-shrink-0" /> <span className="truncate">{record.location}</span>
|
||||||
</p>
|
</p>
|
||||||
{record.images.length > 0 && (
|
{record.images.length > 0 && (
|
||||||
<p className="flex items-center gap-1 text-blue-600">
|
<p className="flex items-center gap-1 text-blue-600">
|
||||||
<Image className="w-3 h-3" /> {record.images.length} photos
|
<ImageIcon className="w-3 h-3" /> {record.images.length}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{record.notes.length > 0 && (
|
{record.notes.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleNotes(record.id)}
|
onClick={(e) => { e.preventDefault(); toggleNotes(record.id); }}
|
||||||
className="flex items-center gap-1 text-purple-600"
|
className="flex items-center gap-1 text-purple-600"
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-3 h-3" /> {record.notes.length} notes
|
<MessageSquare className="w-3 h-3" /> {record.notes.length}
|
||||||
{expandedNotes.includes(record.id) ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
{expandedNotes.includes(record.id) ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -592,24 +743,24 @@ export default function MaintenancePage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between lg:justify-end gap-2 lg:gap-3 mt-2 lg:mt-0">
|
||||||
<div className="text-right">
|
<div className="text-left lg:text-right">
|
||||||
<p className="text-sm font-medium text-slate-700">৳{record.actualCost || record.estimatedCost}</p>
|
<p className="text-sm font-medium text-slate-700">৳{record.actualCost || record.estimatedCost}</p>
|
||||||
<p className="text-xs text-slate-500">{record.paymentStatus === 'paid' ? 'Paid' : record.paymentStatus === 'approved' ? 'Approved' : 'Payment ' + record.paymentStatus}</p>
|
<p className="text-xs text-slate-500">{record.paymentStatus === 'paid' ? 'Paid' : record.paymentStatus === 'approved' ? 'Approved' : 'Payment ' + record.paymentStatus}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[record.status]}`}>
|
<div className="flex items-center gap-1.5 lg:gap-2">
|
||||||
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full ${statusColors[record.status]}`}>
|
||||||
{record.status === 'reported' && <Clock className="w-3 h-3" />}
|
{record.status === 'reported' && <Clock className="w-3 h-3" />}
|
||||||
{record.status === 'in_progress' && <Wrench className="w-3 h-3" />}
|
{record.status === 'in_progress' && <Wrench className="w-3 h-3" />}
|
||||||
{record.status === 'parts_ordered' && <AlertTriangle className="w-3 h-3" />}
|
{record.status === 'parts_ordered' && <AlertTriangle className="w-3 h-3" />}
|
||||||
{record.status === 'completed' && <CheckCircle className="w-3 h-3" />}
|
{record.status === 'completed' && <CheckCircle className="w-3 h-3" />}
|
||||||
{record.status === 'cancelled' && <XCircle className="w-3 h-3" />}
|
{record.status === 'cancelled' && <XCircle className="w-3 h-3" />}
|
||||||
{record.status.replace('_', ' ')}
|
<span className="hidden sm:inline">{record.status.replace('_', ' ')}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.preventDefault(); setSelectedRecord(record); setShowDetailsModal(true); }}
|
onClick={(e) => { e.preventDefault(); router.push(`/admin/maintenance/${record.id}`); }}
|
||||||
className="p-2 hover:bg-slate-100 rounded-lg text-slate-500"
|
className="p-1.5 lg:p-2 hover:bg-slate-100 rounded-lg text-slate-500"
|
||||||
title="View Details"
|
title="View Details"
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
@@ -714,7 +865,7 @@ export default function MaintenancePage() {
|
|||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
{selectedRecord.images.map((img) => (
|
{selectedRecord.images.map((img) => (
|
||||||
<div key={img.id} className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center">
|
<div key={img.id} className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center">
|
||||||
<Image className="w-8 h-8 text-slate-400" />
|
<ImageIcon className="w-8 h-8 text-slate-400" />
|
||||||
<span className="text-xs text-slate-500 mt-1">{img.name}</span>
|
<span className="text-xs text-slate-500 mt-1">{img.name}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -794,18 +945,71 @@ export default function MaintenancePage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 overflow-y-auto max-h-[70vh] space-y-4">
|
<div className="p-4 overflow-y-auto max-h-[70vh] space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-semibold text-slate-700 mb-2 block">Report Type *</label>
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setReportType('damage')}
|
||||||
|
className={`flex-1 py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-medium transition-colors ${reportType === 'damage' ? 'border-red-500 bg-red-50 text-red-700' : 'border-slate-200 text-slate-600 hover:border-red-300'}`}
|
||||||
|
>
|
||||||
|
<AlertTriangle className="w-5 h-5" /> Damage
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setReportType('maintenance')}
|
||||||
|
className={`flex-1 py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-medium transition-colors ${reportType === 'maintenance' ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-600 hover:border-blue-300'}`}
|
||||||
|
>
|
||||||
|
<Wrench className="w-5 h-5" /> Maintenance
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-semibold text-slate-700 mb-2 block">Select Target *</label>
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-4 border-2 rounded-xl flex flex-col items-center gap-2 hover:border-green-500 hover:bg-green-50 transition-colors"
|
||||||
|
>
|
||||||
|
<Battery className="w-8 h-8 text-green-600" />
|
||||||
|
<span className="font-medium text-slate-700">Battery</span>
|
||||||
|
<span className="text-xs text-slate-500">For battery issues</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-4 border-2 rounded-xl flex flex-col items-center gap-2 hover:border-purple-500 hover:bg-purple-50 transition-colors"
|
||||||
|
>
|
||||||
|
<Bike className="w-8 h-8 text-purple-600" />
|
||||||
|
<span className="font-medium text-slate-700">Fleet (Bike)</span>
|
||||||
|
<span className="text-xs text-slate-500">For bike issues</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Issue Type *</label>
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Category *</label>
|
||||||
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
||||||
|
<option value="">Select Category</option>
|
||||||
|
{reportType === 'damage' ? (
|
||||||
|
<>
|
||||||
<option value="damage">Damage</option>
|
<option value="damage">Damage</option>
|
||||||
<option value="repair">Repair</option>
|
<option value="repair">Repair</option>
|
||||||
|
<option value="accident">Accident</option>
|
||||||
|
<option value="theft">Theft</option>
|
||||||
|
<option value="vandalism">Vandalism</option>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<option value="service">Service</option>
|
<option value="service">Service</option>
|
||||||
|
<option value="routine_service">Routine Service</option>
|
||||||
|
<option value="repair">Repair</option>
|
||||||
<option value="battery_swap">Battery Swap</option>
|
<option value="battery_swap">Battery Swap</option>
|
||||||
<option value="inspection">Inspection</option>
|
<option value="inspection">Inspection</option>
|
||||||
<option value="other">Other</option>
|
</>
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{reportType === 'damage' ? (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Severity *</label>
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Severity *</label>
|
||||||
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
||||||
@@ -815,32 +1019,72 @@ export default function MaintenancePage() {
|
|||||||
<option value="cosmetic">Cosmetic</option>
|
<option value="cosmetic">Cosmetic</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Bike ID *</label>
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Status</label>
|
||||||
<input type="text" placeholder="EV-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="in_progress">In Progress</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Battery ID</label>
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Battery ID</label>
|
||||||
<input type="text" placeholder="BAT-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
<input type="text" placeholder="BAT-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Bike ID</label>
|
||||||
|
<input type="text" placeholder="EV-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{reportType === 'damage' ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Reporter Name *</label>
|
||||||
|
<input type="text" placeholder="Enter name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Reporter Phone *</label>
|
||||||
|
<input type="tel" placeholder="01XXXXXXXXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Performed By</label>
|
||||||
|
<input type="text" placeholder="Technician name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Next Due Date</label>
|
||||||
|
<input type="date" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Description *</label>
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Description *</label>
|
||||||
<textarea rows={3} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Describe the issue..." />
|
<textarea rows={3} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Describe the issue in detail..." />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Location / Hub *</label>
|
||||||
|
<input type="text" placeholder="e.g., Gulshan Hub" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Location *</label>
|
<label className="text-xs font-medium text-slate-600 mb-1 block">
|
||||||
<input type="text" placeholder="Where did the issue occur?" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
{reportType === 'damage' ? 'Estimated Cost (৳)' : 'Service Cost (৳)'}
|
||||||
</div>
|
</label>
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Estimated Cost</label>
|
|
||||||
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-slate-600 mb-1 block">Upload Images</label>
|
<label className="text-xs font-medium text-slate-600 mb-1 block">Upload Images</label>
|
||||||
<div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center">
|
<div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center hover:border-accent cursor-pointer">
|
||||||
<Image className="w-8 h-8 text-slate-300 mx-auto mb-2" />
|
<ImageIcon className="w-8 h-8 text-slate-300 mx-auto mb-2" />
|
||||||
<p className="text-sm text-slate-500">Drag and drop or click to upload</p>
|
<p className="text-sm text-slate-500">Click to upload images</p>
|
||||||
|
<p className="text-xs text-slate-400">JPG, PNG up to 5MB</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -848,7 +1092,7 @@ export default function MaintenancePage() {
|
|||||||
<button onClick={() => setShowNewModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
<button onClick={() => setShowNewModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => { setShowNewModal(false); alert('Issue reported successfully!'); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark">
|
<button onClick={() => { setShowNewModal(false); setShowSuccessModal(true); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark">
|
||||||
Submit Report
|
Submit Report
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -953,6 +1197,24 @@ export default function MaintenancePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showSuccessModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6 text-center">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<CheckCircle className="w-8 h-8 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-slate-800 mb-2">Issue Reported Successfully!</h3>
|
||||||
|
<p className="text-slate-500 mb-6">Your issue has been submitted and will be reviewed shortly.</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSuccessModal(false)}
|
||||||
|
className="px-6 py-2 bg-accent text-white rounded-lg hover:bg-accent-dark"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user