941 lines
40 KiB
TypeScript
941 lines
40 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { useState, useEffect } from 'react';
|
|||
|
|
import { useParams, useRouter } from 'next/navigation';
|
|||
|
|
import {
|
|||
|
|
AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone,
|
|||
|
|
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key,
|
|||
|
|
CheckCircle, XCircle, ChevronLeft, Save, Printer, Send, QrCode,
|
|||
|
|
Wallet, Building, Edit, MessageSquare, Calendar, ArrowLeft
|
|||
|
|
} 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 MaintenanceType = 'damage' | 'repair' | 'service' | 'battery_swap' | 'inspection' | 'other';
|
|||
|
|
type DamageSeverity = 'critical' | 'major' | 'minor' | 'cosmetic';
|
|||
|
|
type MaintenanceStatus = 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
|
|||
|
|
type PaymentStatus = 'pending' | 'approved' | 'paid' | 'rejected';
|
|||
|
|
|
|||
|
|
interface MaintenanceRecord {
|
|||
|
|
id: string;
|
|||
|
|
date: string;
|
|||
|
|
type: MaintenanceType;
|
|||
|
|
severity: DamageSeverity;
|
|||
|
|
status: MaintenanceStatus;
|
|||
|
|
paymentStatus: PaymentStatus;
|
|||
|
|
bikeId: string;
|
|||
|
|
bikeModel: string;
|
|||
|
|
bikePlate: string;
|
|||
|
|
batteryId?: string;
|
|||
|
|
reporterId: string;
|
|||
|
|
reporterName: string;
|
|||
|
|
reporterPhone: string;
|
|||
|
|
reporterRole: 'biker' | 'staff' | 'hub';
|
|||
|
|
description: string;
|
|||
|
|
location: string;
|
|||
|
|
estimatedCost: number;
|
|||
|
|
actualCost?: number;
|
|||
|
|
partsUsed?: string[];
|
|||
|
|
images: { id: string; name: string; url: string; uploadedAt: string }[];
|
|||
|
|
assignedTo?: string;
|
|||
|
|
notes: string[];
|
|||
|
|
resolvedAt?: string;
|
|||
|
|
createdAt: string;
|
|||
|
|
createdBy: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mockMaintenance: MaintenanceRecord[] = [
|
|||
|
|
{
|
|||
|
|
id: 'MNT-001',
|
|||
|
|
date: '2024-03-21',
|
|||
|
|
type: 'damage',
|
|||
|
|
severity: 'major',
|
|||
|
|
status: 'in_progress',
|
|||
|
|
paymentStatus: 'approved',
|
|||
|
|
bikeId: 'EV-004',
|
|||
|
|
bikeModel: 'AIMA Lightning',
|
|||
|
|
bikePlate: 'Dhaka Metro Cha-5679',
|
|||
|
|
batteryId: 'BAT-044',
|
|||
|
|
reporterId: 'BIKER-004',
|
|||
|
|
reporterName: 'Sofiq Rahman',
|
|||
|
|
reporterPhone: '01712345681',
|
|||
|
|
reporterRole: 'biker',
|
|||
|
|
description: 'Front fender damaged in accident at Gulshan signal',
|
|||
|
|
location: 'Gulshan, Dhaka',
|
|||
|
|
estimatedCost: 3500,
|
|||
|
|
actualCost: 3200,
|
|||
|
|
partsUsed: ['Front fender', 'Mounting brackets'],
|
|||
|
|
images: [
|
|||
|
|
{ id: 'img1', name: 'Damage Front', url: '', uploadedAt: '2024-03-21' },
|
|||
|
|
{ id: 'img2', name: 'Damage Side', url: '', uploadedAt: '2024-03-21' },
|
|||
|
|
],
|
|||
|
|
assignedTo: 'Service Center A',
|
|||
|
|
notes: ['Parts ordered from supplier'],
|
|||
|
|
createdAt: '2024-03-21T10:00:00',
|
|||
|
|
createdBy: 'Admin',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'MNT-002',
|
|||
|
|
date: '2024-03-20',
|
|||
|
|
type: 'service',
|
|||
|
|
severity: 'minor',
|
|||
|
|
status: 'completed',
|
|||
|
|
paymentStatus: 'paid',
|
|||
|
|
bikeId: 'EV-002',
|
|||
|
|
bikeModel: 'Yadea DT3',
|
|||
|
|
bikePlate: 'Dhaka Metro Ba-1234',
|
|||
|
|
batteryId: 'BAT-021',
|
|||
|
|
reporterId: 'BIKER-002',
|
|||
|
|
reporterName: 'Karim Hasan',
|
|||
|
|
reporterPhone: '01712345679',
|
|||
|
|
reporterRole: 'biker',
|
|||
|
|
description: 'Routine service - brake adjustment and chain lubrication',
|
|||
|
|
location: 'Banani Hub',
|
|||
|
|
estimatedCost: 500,
|
|||
|
|
actualCost: 450,
|
|||
|
|
images: [],
|
|||
|
|
notes: ['Service completed'],
|
|||
|
|
resolvedAt: '2024-03-20T14:00:00',
|
|||
|
|
createdAt: '2024-03-20T08:00:00',
|
|||
|
|
createdBy: 'Hub Staff',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'MNT-003',
|
|||
|
|
date: '2024-03-19',
|
|||
|
|
type: 'battery_swap',
|
|||
|
|
severity: 'minor',
|
|||
|
|
status: 'completed',
|
|||
|
|
paymentStatus: 'pending',
|
|||
|
|
bikeId: 'EV-007',
|
|||
|
|
bikeModel: 'Etron ET50',
|
|||
|
|
bikePlate: 'Dhaka Metro Ca-8901',
|
|||
|
|
reporterId: 'BIKER-007',
|
|||
|
|
reporterName: 'Jamal',
|
|||
|
|
reporterPhone: '01712345687',
|
|||
|
|
reporterRole: 'biker',
|
|||
|
|
description: 'Battery not holding charge properly - need replacement',
|
|||
|
|
location: 'Dhanmondi, Dhaka',
|
|||
|
|
estimatedCost: 0,
|
|||
|
|
images: [],
|
|||
|
|
notes: ['Battery replaced under warranty'],
|
|||
|
|
resolvedAt: '2024-03-19T16:00:00',
|
|||
|
|
createdAt: '2024-03-19T12:00:00',
|
|||
|
|
createdBy: 'Admin',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'MNT-004',
|
|||
|
|
date: '2024-03-18',
|
|||
|
|
type: 'repair',
|
|||
|
|
severity: 'critical',
|
|||
|
|
status: 'in_progress',
|
|||
|
|
paymentStatus: 'pending',
|
|||
|
|
bikeId: 'EV-010',
|
|||
|
|
bikeModel: 'TVS iQube',
|
|||
|
|
bikePlate: 'Dhaka Metro Da-4567',
|
|||
|
|
reporterId: 'BIKER-010',
|
|||
|
|
reporterName: 'Ripon',
|
|||
|
|
reporterPhone: '01712345690',
|
|||
|
|
reporterRole: 'biker',
|
|||
|
|
description: 'Motor issue - bike not moving properly',
|
|||
|
|
location: 'Mirpur, Dhaka',
|
|||
|
|
estimatedCost: 8000,
|
|||
|
|
images: [
|
|||
|
|
{ id: 'img3', name: 'Motor Damage', url: '', uploadedAt: '2024-03-18' },
|
|||
|
|
],
|
|||
|
|
assignedTo: 'Authorized Service Center',
|
|||
|
|
notes: ['Motor needs replacement - ordered', 'Waiting for parts'],
|
|||
|
|
createdAt: '2024-03-18T09:00:00',
|
|||
|
|
createdBy: 'Admin',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'MNT-005',
|
|||
|
|
date: '2024-03-17',
|
|||
|
|
type: 'inspection',
|
|||
|
|
severity: 'minor',
|
|||
|
|
status: 'completed',
|
|||
|
|
paymentStatus: 'paid',
|
|||
|
|
bikeId: 'EV-001',
|
|||
|
|
bikeModel: 'AIMA Lightning',
|
|||
|
|
bikePlate: 'Dhaka Metro Aa-1111',
|
|||
|
|
reporterId: 'Hub-01',
|
|||
|
|
reporterName: 'Gulshan Hub',
|
|||
|
|
reporterPhone: '02-1234567',
|
|||
|
|
reporterRole: 'hub',
|
|||
|
|
description: 'Monthly inspection completed',
|
|||
|
|
location: 'Gulshan Hub',
|
|||
|
|
estimatedCost: 300,
|
|||
|
|
actualCost: 250,
|
|||
|
|
images: [],
|
|||
|
|
notes: ['All checks passed'],
|
|||
|
|
resolvedAt: '2024-03-17T15:00:00',
|
|||
|
|
createdAt: '2024-03-17T10:00:00',
|
|||
|
|
createdBy: 'Hub Staff',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'MNT-006',
|
|||
|
|
date: '2024-03-15',
|
|||
|
|
type: 'damage',
|
|||
|
|
severity: 'cosmetic',
|
|||
|
|
status: 'completed',
|
|||
|
|
paymentStatus: 'rejected',
|
|||
|
|
bikeId: 'EV-005',
|
|||
|
|
bikeModel: 'Yadea DT3',
|
|||
|
|
bikePlate: 'Dhaka Metro Ba-5678',
|
|||
|
|
reporterId: 'BIKER-005',
|
|||
|
|
reporterName: 'Rahim',
|
|||
|
|
reporterPhone: '01712345685',
|
|||
|
|
reporterRole: 'biker',
|
|||
|
|
description: 'Minor scratch on mirror - customer dropped bike slowly',
|
|||
|
|
location: 'Uttara, Dhaka',
|
|||
|
|
estimatedCost: 500,
|
|||
|
|
images: [],
|
|||
|
|
notes: ['Denied - user responsibility'],
|
|||
|
|
createdAt: '2024-03-15T14:00:00',
|
|||
|
|
createdBy: 'Admin',
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const statusColors: Record<string, string> = {
|
|||
|
|
reported: 'bg-amber-100 text-amber-700',
|
|||
|
|
in_progress: 'bg-blue-100 text-blue-700',
|
|||
|
|
parts_ordered: 'bg-purple-100 text-purple-700',
|
|||
|
|
completed: 'bg-green-100 text-green-700',
|
|||
|
|
cancelled: 'bg-red-100 text-red-700',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const severityColors: Record<string, string> = {
|
|||
|
|
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 paymentColors: Record<string, string> = {
|
|||
|
|
pending: 'bg-amber-100 text-amber-700',
|
|||
|
|
approved: 'bg-blue-100 text-blue-700',
|
|||
|
|
paid: 'bg-green-100 text-green-700',
|
|||
|
|
rejected: 'bg-red-100 text-red-700',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const typeLabels: Record<string, string> = {
|
|||
|
|
damage: 'Damage',
|
|||
|
|
repair: 'Repair',
|
|||
|
|
service: 'Service',
|
|||
|
|
battery_swap: 'Battery Swap',
|
|||
|
|
inspection: 'Inspection',
|
|||
|
|
other: 'Other',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default function MaintenanceDetailPage() {
|
|||
|
|
const params = useParams();
|
|||
|
|
const router = useRouter();
|
|||
|
|
const id = params.id as string;
|
|||
|
|
|
|||
|
|
const [record, setRecord] = useState<MaintenanceRecord | null>(null);
|
|||
|
|
const [editMode, setEditMode] = useState(false);
|
|||
|
|
const [showCompleteModal, setShowCompleteModal] = useState(false);
|
|||
|
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
|||
|
|
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
|
|||
|
|
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
|
|||
|
|
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
|
|||
|
|
const [newNoteText, setNewNoteText] = useState('');
|
|||
|
|
const [actualCost, setActualCost] = useState('');
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const found = mockMaintenance.find(r => r.id === id);
|
|||
|
|
if (found) {
|
|||
|
|
setRecord(found);
|
|||
|
|
setEditForm(found);
|
|||
|
|
setActualCost(found.actualCost?.toString() || found.estimatedCost.toString());
|
|||
|
|
}
|
|||
|
|
}, [id]);
|
|||
|
|
|
|||
|
|
if (!record) {
|
|||
|
|
return (
|
|||
|
|
<div className="p-6 flex items-center justify-center min-h-[50vh]">
|
|||
|
|
<div className="text-center">
|
|||
|
|
<Wrench className="w-16 h-16 text-slate-300 mx-auto mb-4" />
|
|||
|
|
<p className="text-slate-500">Record not found</p>
|
|||
|
|
<button
|
|||
|
|
onClick={() => router.push('/admin/maintenance')}
|
|||
|
|
className="mt-4 px-4 py-2 bg-accent text-white rounded-lg text-sm"
|
|||
|
|
>
|
|||
|
|
Back to Maintenance
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleSaveEdit = () => {
|
|||
|
|
setRecord(prev => prev ? { ...prev, ...editForm } : null);
|
|||
|
|
setEditMode(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleComplete = () => {
|
|||
|
|
if (!record) return;
|
|||
|
|
const cost = parseInt(actualCost) || record.estimatedCost;
|
|||
|
|
setRecord(prev => prev ? {
|
|||
|
|
...prev,
|
|||
|
|
status: 'completed',
|
|||
|
|
resolvedAt: new Date().toISOString().split('T')[0],
|
|||
|
|
actualCost: cost
|
|||
|
|
} : null);
|
|||
|
|
setShowCompleteModal(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handlePayment = (source: 'bank' | 'cash' | 'biker') => {
|
|||
|
|
if (!record) return;
|
|||
|
|
const cost = record.actualCost || record.estimatedCost;
|
|||
|
|
|
|||
|
|
setRecord(prev => prev ? { ...prev, paymentStatus: 'paid' } : null);
|
|||
|
|
setShowPaymentModal(false);
|
|||
|
|
setShowInvoiceModal(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleGenerateInvoice = () => {
|
|||
|
|
if (!record) return;
|
|||
|
|
import('jspdf').then(jsPDF => {
|
|||
|
|
const doc = new jsPDF.default();
|
|||
|
|
const cost = record.actualCost || record.estimatedCost;
|
|||
|
|
const qrData = `INV-${record.id}|${record.bikePlate}|${record.type}|${cost}|${new Date().toISOString().split('T')[0]}`;
|
|||
|
|
|
|||
|
|
doc.setFontSize(20);
|
|||
|
|
doc.setTextColor(6, 95, 70);
|
|||
|
|
doc.text('JAIBEN Mobility Ltd', 20, 20);
|
|||
|
|
|
|||
|
|
doc.setFontSize(14);
|
|||
|
|
doc.setTextColor(0);
|
|||
|
|
doc.text('Maintenance Invoice', 20, 32);
|
|||
|
|
|
|||
|
|
doc.setFontSize(10);
|
|||
|
|
doc.setTextColor(100);
|
|||
|
|
doc.text(`Invoice No: INV-${record.id}`, 20, 42);
|
|||
|
|
doc.text(`Date: ${record.date}`, 20, 48);
|
|||
|
|
doc.text(`Issue Type: ${typeLabels[record.type]}`, 20, 54);
|
|||
|
|
doc.text(`Severity: ${record.severity}`, 20, 60);
|
|||
|
|
doc.text(`Status: ${record.status}`, 20, 66);
|
|||
|
|
|
|||
|
|
doc.setFontSize(11);
|
|||
|
|
doc.setTextColor(0);
|
|||
|
|
doc.text('Bike Details', 20, 80);
|
|||
|
|
doc.setFontSize(10);
|
|||
|
|
doc.text(`Bike ID: ${record.bikeId}`, 20, 86);
|
|||
|
|
doc.text(`Model: ${record.bikeModel}`, 20, 92);
|
|||
|
|
doc.text(`License Plate: ${record.bikePlate}`, 20, 98);
|
|||
|
|
if (record.batteryId) doc.text(`Battery ID: ${record.batteryId}`, 20, 104);
|
|||
|
|
|
|||
|
|
doc.setFontSize(11);
|
|||
|
|
doc.text('Reporter', 20, 118);
|
|||
|
|
doc.setFontSize(10);
|
|||
|
|
doc.text(`Name: ${record.reporterName}`, 20, 124);
|
|||
|
|
doc.text(`Phone: ${record.reporterPhone}`, 20, 130);
|
|||
|
|
doc.text(`Role: ${record.reporterRole}`, 20, 136);
|
|||
|
|
|
|||
|
|
doc.setFontSize(11);
|
|||
|
|
doc.text('Description', 20, 150);
|
|||
|
|
doc.setFontSize(10);
|
|||
|
|
const descLines = doc.splitTextToSize(record.description, 170);
|
|||
|
|
doc.text(descLines, 20, 156);
|
|||
|
|
|
|||
|
|
doc.setFontSize(11);
|
|||
|
|
doc.text('Service Details', 120, 80);
|
|||
|
|
doc.setFontSize(10);
|
|||
|
|
doc.text(`Location: ${record.location}`, 120, 86);
|
|||
|
|
if (record.assignedTo) doc.text(`Assigned: ${record.assignedTo}`, 120, 92);
|
|||
|
|
if (record.resolvedAt) doc.text(`Resolved: ${record.resolvedAt}`, 120, 98);
|
|||
|
|
|
|||
|
|
doc.setFontSize(11);
|
|||
|
|
doc.text('Cost Breakdown', 20, 175);
|
|||
|
|
doc.setFontSize(10);
|
|||
|
|
doc.text(`Estimated Cost: ৳${record.estimatedCost}`, 20, 181);
|
|||
|
|
if (record.actualCost) doc.text(`Actual Cost: ৳${record.actualCost}`, 20, 187);
|
|||
|
|
if (record.partsUsed && record.partsUsed.length > 0) {
|
|||
|
|
doc.text(`Parts: ${record.partsUsed.join(', ')}`, 20, 193);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
doc.setFontSize(14);
|
|||
|
|
doc.setTextColor(6, 95, 70);
|
|||
|
|
doc.text(`Total: ৳${cost}`, 20, 205);
|
|||
|
|
|
|||
|
|
doc.setFontSize(9);
|
|||
|
|
doc.setTextColor(150);
|
|||
|
|
doc.text('Generated from JAIBEN Maintenance System', 20, 280);
|
|||
|
|
doc.text(`QR: ${qrData}`, 20, 286);
|
|||
|
|
|
|||
|
|
doc.save(`maintenance-invoice-${record.id}.pdf`);
|
|||
|
|
});
|
|||
|
|
setShowInvoiceModal(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleAddNote = () => {
|
|||
|
|
if (!record || !newNoteText.trim()) return;
|
|||
|
|
setRecord(prev => prev ? { ...prev, notes: [...prev.notes, newNoteText] } : null);
|
|||
|
|
setNewNoteText('');
|
|||
|
|
setShowAddNoteModal(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
|
|||
|
|
<button
|
|||
|
|
onClick={() => router.push('/admin/maintenance')}
|
|||
|
|
className="flex items-center gap-2 text-slate-600 hover:text-slate-800 mb-4"
|
|||
|
|
>
|
|||
|
|
<ArrowLeft className="w-4 h-4" /> Back to Maintenance
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
|||
|
|
<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>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<h1 className="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]}`}>
|
|||
|
|
{record.severity}
|
|||
|
|
</span>
|
|||
|
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[record.status]}`}>
|
|||
|
|
{record.status.replace('_', ' ')}
|
|||
|
|
</span>
|
|||
|
|
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${paymentColors[record.paymentStatus]}`}>
|
|||
|
|
{record.paymentStatus}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-slate-500 mt-1">{typeLabels[record.type]} • {record.date}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
{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">
|
|||
|
|
<Save className="w-4 h-4" /> Save
|
|||
|
|
</button>
|
|||
|
|
<button onClick={() => setEditMode(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
|
|||
|
|
Cancel
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<button onClick={() => setEditMode(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
|||
|
|
<Edit className="w-4 h-4" /> Edit
|
|||
|
|
</button>
|
|||
|
|
<button onClick={() => setShowAddNoteModal(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
|
|||
|
|
<MessageSquare className="w-4 h-4" /> Note
|
|||
|
|
</button>
|
|||
|
|
{record.status !== 'completed' && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => setShowCompleteModal(true)}
|
|||
|
|
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
<Check className="w-4 h-4" /> Complete
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{record.status === 'completed' && record.paymentStatus !== 'paid' && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => setShowPaymentModal(true)}
|
|||
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
<DollarSign className="w-4 h-4" /> Payment
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{record.paymentStatus === 'paid' && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => setShowInvoiceModal(true)}
|
|||
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
<Printer className="w-4 h-4" /> Invoice
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
|
|||
|
|
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
|
|||
|
|
<Bike className="w-5 h-5" /> Bike Information
|
|||
|
|
</h3>
|
|||
|
|
{editMode ? (
|
|||
|
|
<div className="grid grid-cols-2 gap-3">
|
|||
|
|
<div>
|
|||
|
|
<label className="text-xs text-blue-700 block mb-1">Bike ID</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.bikeId || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, bikeId: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-xs text-blue-700 block mb-1">License Plate</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.bikePlate || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, bikePlate: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-xs text-blue-700 block mb-1">Model</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.bikeModel || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, bikeModel: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-xs text-blue-700 block mb-1">Battery ID</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.batteryId || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, batteryId: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-blue-600">Bike ID</span>
|
|||
|
|
<span className="text-sm font-medium text-blue-800">{record.bikeId}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-blue-600">Model</span>
|
|||
|
|
<span className="text-sm font-medium text-blue-800">{record.bikeModel}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-blue-600">License Plate</span>
|
|||
|
|
<span className="text-sm font-medium text-blue-800">{record.bikePlate}</span>
|
|||
|
|
</div>
|
|||
|
|
{record.batteryId && (
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-blue-600">Battery ID</span>
|
|||
|
|
<span className="text-sm font-medium text-blue-800">{record.batteryId}</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<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">
|
|||
|
|
<User className="w-5 h-5" /> Reporter
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-green-600">Name</span>
|
|||
|
|
<span className="text-sm font-medium text-green-800">{record.reporterName}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-green-600">Phone</span>
|
|||
|
|
<span className="text-sm font-medium text-green-800">{record.reporterPhone}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-green-600">Role</span>
|
|||
|
|
<span className="text-sm font-medium text-green-800 capitalize">{record.reporterRole}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="bg-amber-50 p-4 rounded-xl border border-amber-100">
|
|||
|
|
<h3 className="font-semibold text-amber-800 mb-3 flex items-center gap-2">
|
|||
|
|
<FileText className="w-5 h-5" /> Description
|
|||
|
|
</h3>
|
|||
|
|
{editMode ? (
|
|||
|
|
<textarea
|
|||
|
|
value={editForm.description || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm"
|
|||
|
|
rows={3}
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<p className="text-sm text-amber-700 mb-3">{record.description}</p>
|
|||
|
|
<div className="flex items-center gap-2 text-xs text-amber-600">
|
|||
|
|
<MapPin className="w-3 h-3" /> {record.location}
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
|||
|
|
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
|||
|
|
<DollarSign className="w-5 h-5" /> Cost Details
|
|||
|
|
</h3>
|
|||
|
|
<div className="grid grid-cols-2 gap-3">
|
|||
|
|
<div className="bg-white p-3 rounded-lg">
|
|||
|
|
<p className="text-xs text-purple-600">Estimated</p>
|
|||
|
|
{editMode ? (
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={editForm.estimatedCost || 0}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, estimatedCost: parseInt(e.target.value) })}
|
|||
|
|
className="w-full px-2 py-1 border border-purple-200 rounded text-sm"
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-lg font-bold text-purple-800">৳{record.estimatedCost}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="bg-white p-3 rounded-lg">
|
|||
|
|
<p className="text-xs text-purple-600">Actual</p>
|
|||
|
|
<p className="text-lg font-bold text-purple-800">৳{record.actualCost || record.estimatedCost}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="bg-cyan-50 p-4 rounded-xl border border-cyan-100">
|
|||
|
|
<h3 className="font-semibold text-cyan-800 mb-3 flex items-center gap-2">
|
|||
|
|
<User className="w-5 h-5" /> Assigned To
|
|||
|
|
</h3>
|
|||
|
|
{editMode ? (
|
|||
|
|
<select
|
|||
|
|
value={editForm.assignedTo || ''}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, assignedTo: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-cyan-200 rounded-lg text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="">Select Service Center</option>
|
|||
|
|
<option value="Service Center A">Service Center A</option>
|
|||
|
|
<option value="Service Center B">Service Center B</option>
|
|||
|
|
<option value="Authorized Service Center">Authorized Service Center</option>
|
|||
|
|
<option value="Gulshan Hub">Gulshan Hub</option>
|
|||
|
|
<option value="Banani Hub">Banani Hub</option>
|
|||
|
|
<option value="Dhanmondi Hub">Dhanmondi Hub</option>
|
|||
|
|
</select>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-sm text-cyan-700">{record.assignedTo || 'Not assigned'}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="bg-orange-50 p-4 rounded-xl border border-orange-100">
|
|||
|
|
<div className="flex items-center justify-between mb-3">
|
|||
|
|
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
|
|||
|
|
<Wrench className="w-5 h-5" /> Parts Used
|
|||
|
|
</h3>
|
|||
|
|
{editMode && (
|
|||
|
|
<select
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (e.target.value) {
|
|||
|
|
const currentParts = editForm.partsUsed || [];
|
|||
|
|
if (!currentParts.includes(e.target.value)) {
|
|||
|
|
setEditForm({ ...editForm, partsUsed: [...currentParts, e.target.value] });
|
|||
|
|
}
|
|||
|
|
e.target.value = '';
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="px-2 py-1 text-xs border border-orange-200 rounded"
|
|||
|
|
>
|
|||
|
|
<option value="">+ Add Part</option>
|
|||
|
|
<option value="Front fender">Front fender</option>
|
|||
|
|
<option value="Rear fender">Rear fender</option>
|
|||
|
|
<option value="Mirror">Mirror</option>
|
|||
|
|
<option value="Headlight">Headlight</option>
|
|||
|
|
<option value="Tail light">Tail light</option>
|
|||
|
|
<option value="Brake pad">Brake pad</option>
|
|||
|
|
<option value="Brake shoe">Brake shoe</option>
|
|||
|
|
<option value="Chain">Chain</option>
|
|||
|
|
<option value="Battery">Battery</option>
|
|||
|
|
<option value="Motor">Motor</option>
|
|||
|
|
<option value="Controller">Controller</option>
|
|||
|
|
<option value="Throttle">Throttle</option>
|
|||
|
|
<option value="Lever">Lever</option>
|
|||
|
|
<option value="Stand">Stand</option>
|
|||
|
|
<option value="Seat">Seat</option>
|
|||
|
|
<option value="Tyre">Tyre</option>
|
|||
|
|
<option value="Tube">Tube</option>
|
|||
|
|
<option value="Mounting brackets">Mounting brackets</option>
|
|||
|
|
<option value="Bolt set">Bolt set</option>
|
|||
|
|
</select>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="flex flex-wrap gap-2">
|
|||
|
|
{(editMode ? editForm.partsUsed : record.partsUsed)?.map((part, idx) => (
|
|||
|
|
<span key={idx} className="px-3 py-1 bg-white rounded-full text-sm text-orange-700 border border-orange-200 flex items-center gap-1">
|
|||
|
|
{part}
|
|||
|
|
{editMode && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
const updated = [...(editForm.partsUsed || [])];
|
|||
|
|
updated.splice(idx, 1);
|
|||
|
|
setEditForm({ ...editForm, partsUsed: updated });
|
|||
|
|
}}
|
|||
|
|
className="ml-1 text-orange-400 hover:text-red-500"
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
{(editMode ? editForm.partsUsed : record.partsUsed)?.length === 0 && (
|
|||
|
|
<p className="text-sm text-orange-400">No parts added</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="bg-indigo-50 p-4 rounded-xl border border-indigo-100">
|
|||
|
|
<h3 className="font-semibold text-indigo-800 mb-3 flex items-center gap-2">
|
|||
|
|
<Image className="w-5 h-5" /> Images ({(editMode ? editForm.images : record.images)?.length})
|
|||
|
|
</h3>
|
|||
|
|
<div className="grid grid-cols-4 gap-2">
|
|||
|
|
{(editMode ? editForm.images : record.images)?.map((img) => (
|
|||
|
|
<div key={img.id} className="relative aspect-square bg-white rounded-lg flex flex-col items-center justify-center border border-indigo-100">
|
|||
|
|
<Image className="w-8 h-8 text-indigo-400" />
|
|||
|
|
<span className="text-xs text-indigo-500 mt-1 text-center">{img.name}</span>
|
|||
|
|
{editMode && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
const updated = (editForm.images || []).filter((i: any) => i.id !== img.id);
|
|||
|
|
setEditForm({ ...editForm, images: updated });
|
|||
|
|
}}
|
|||
|
|
className="absolute top-1 right-1 w-5 h-5 bg-red-100 rounded-full text-red-500 text-xs flex items-center justify-center"
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
{editMode && (
|
|||
|
|
<label className="aspect-square bg-white rounded-lg flex flex-col items-center justify-center border border-dashed border-indigo-200 cursor-pointer hover:bg-indigo-50">
|
|||
|
|
<Image className="w-8 h-8 text-indigo-400" />
|
|||
|
|
<span className="text-xs text-indigo-500 mt-1">+ Add</span>
|
|||
|
|
<input type="file" className="hidden" accept="image/*" />
|
|||
|
|
</label>
|
|||
|
|
)}
|
|||
|
|
{(editMode ? editForm.images : record.images)?.length === 0 && !editMode && (
|
|||
|
|
<p className="text-sm text-indigo-400 col-span-4 text-center py-4">No images</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
|||
|
|
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
|
|||
|
|
<MessageSquare className="w-5 h-5" /> Notes ({(editMode ? editForm.notes : record.notes)?.length})
|
|||
|
|
</h3>
|
|||
|
|
{editMode && (
|
|||
|
|
<div className="flex gap-2 mb-3">
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
id="newNoteInput"
|
|||
|
|
placeholder="Add a note..."
|
|||
|
|
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
|||
|
|
onKeyPress={(e) => {
|
|||
|
|
if (e.key === 'Enter') {
|
|||
|
|
const input = e.currentTarget as HTMLInputElement;
|
|||
|
|
if (input.value.trim()) {
|
|||
|
|
setEditForm({ ...editForm, notes: [...(editForm.notes || []), input.value.trim()] });
|
|||
|
|
input.value = '';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
const input = document.getElementById('newNoteInput') as HTMLInputElement;
|
|||
|
|
if (input?.value.trim()) {
|
|||
|
|
setEditForm({ ...editForm, notes: [...(editForm.notes || []), input.value.trim()] });
|
|||
|
|
input.value = '';
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="px-4 py-2 bg-accent text-white rounded-lg text-sm"
|
|||
|
|
>
|
|||
|
|
Add
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{(editMode ? editForm.notes : record.notes)?.map((note, idx) => (
|
|||
|
|
<div key={idx} className="text-sm text-slate-600 p-2 bg-white rounded-lg">
|
|||
|
|
• {note}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
{(editMode ? editForm.notes : record.notes)?.length === 0 && (
|
|||
|
|
<p className="text-sm text-slate-400">No notes yet</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{showCompleteModal && (
|
|||
|
|
<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="p-4 border-b border-slate-100">
|
|||
|
|
<h3 className="font-semibold text-slate-800">Complete Maintenance</h3>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 space-y-4">
|
|||
|
|
<div className="bg-green-50 p-4 rounded-lg">
|
|||
|
|
<p className="text-sm text-green-700">Enter actual cost to complete this record</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-sm font-medium text-slate-600 mb-2 block">Actual Cost (৳)</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={actualCost}
|
|||
|
|
onChange={(e) => setActualCost(e.target.value)}
|
|||
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-lg font-bold"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-slate-500">
|
|||
|
|
Estimated: ৳{record.estimatedCost}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
|||
|
|
<button onClick={() => setShowCompleteModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleComplete}
|
|||
|
|
className="px-4 py-2 bg-green-600 text-white rounded-lg flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
<Check className="w-4 h-4" /> Mark Complete
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{showPaymentModal && record && (
|
|||
|
|
<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-lg">
|
|||
|
|
<div className="p-4 border-b border-slate-100">
|
|||
|
|
<h3 className="font-semibold text-slate-800">Process Payment - {record.id}</h3>
|
|||
|
|
<p className="text-sm text-slate-500">Amount: ৳{record.actualCost || record.estimatedCost}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 space-y-4">
|
|||
|
|
<p className="text-sm text-slate-600 mb-2">Select payment method:</p>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handlePayment('bank')}
|
|||
|
|
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors"
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
|||
|
|
<Building className="w-5 h-5 text-blue-600" />
|
|||
|
|
</div>
|
|||
|
|
<div className="text-left">
|
|||
|
|
<p className="font-medium text-slate-800">Bank Transfer</p>
|
|||
|
|
<p className="text-xs text-slate-500">Debit Bank (1200) → Credit Maintenance (5400)</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handlePayment('cash')}
|
|||
|
|
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-green-500 hover:bg-green-50 transition-colors"
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
|
|||
|
|
<Wallet className="w-5 h-5 text-green-600" />
|
|||
|
|
</div>
|
|||
|
|
<div className="text-left">
|
|||
|
|
<p className="font-medium text-slate-800">Cash</p>
|
|||
|
|
<p className="text-xs text-slate-500">Debit Cash (1100) → Credit Maintenance (5400)</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handlePayment('biker')}
|
|||
|
|
className="w-full p-4 border-2 border-slate-200 rounded-lg hover:border-purple-500 hover:bg-purple-50 transition-colors"
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
|
|||
|
|
<User className="w-5 h-5 text-purple-600" />
|
|||
|
|
</div>
|
|||
|
|
<div className="text-left">
|
|||
|
|
<p className="font-medium text-slate-800">Biker Wallet</p>
|
|||
|
|
<p className="text-xs text-slate-500">Deduct from rider wallet</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 border-t border-slate-100 flex justify-end">
|
|||
|
|
<button onClick={() => setShowPaymentModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{showInvoiceModal && record && (
|
|||
|
|
<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-lg">
|
|||
|
|
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
|||
|
|
<h3 className="font-semibold text-slate-800">Invoice Generated!</h3>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-6 text-center">
|
|||
|
|
<div className="w-20 h-20 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center">
|
|||
|
|
<CheckCircle className="w-10 h-10 text-green-600" />
|
|||
|
|
</div>
|
|||
|
|
<p className="text-lg font-semibold text-slate-800 mb-2">Payment Complete</p>
|
|||
|
|
<p className="text-slate-600 mb-4">Invoice INV-{record.id} is ready</p>
|
|||
|
|
|
|||
|
|
<div className="bg-slate-50 p-4 rounded-lg text-left mb-4">
|
|||
|
|
<div className="flex justify-between mb-2">
|
|||
|
|
<span className="text-sm text-slate-500">Maintenance ID</span>
|
|||
|
|
<span className="text-sm font-medium">{record.id}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between mb-2">
|
|||
|
|
<span className="text-sm text-slate-500">Bike</span>
|
|||
|
|
<span className="text-sm font-medium">{record.bikePlate}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between mb-2">
|
|||
|
|
<span className="text-sm text-slate-500">Amount Paid</span>
|
|||
|
|
<span className="text-sm font-bold text-green-600">৳{record.actualCost || record.estimatedCost}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-center mb-4">
|
|||
|
|
<div className="w-24 h-24 bg-white border-2 border-slate-200 rounded-lg flex items-center justify-center">
|
|||
|
|
<QrCode className="w-16 h-16 text-slate-400" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-xs text-slate-400">Scan QR for verification</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 border-t border-slate-100 flex justify-between">
|
|||
|
|
<button
|
|||
|
|
onClick={() => setShowInvoiceModal(false)}
|
|||
|
|
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg"
|
|||
|
|
>
|
|||
|
|
Close
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleGenerateInvoice}
|
|||
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
<Printer className="w-4 h-4" /> Download PDF
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{showAddNoteModal && (
|
|||
|
|
<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="p-4 border-b border-slate-100">
|
|||
|
|
<h3 className="font-semibold text-slate-800">Add Note</h3>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4">
|
|||
|
|
<textarea
|
|||
|
|
value={newNoteText}
|
|||
|
|
onChange={(e) => setNewNoteText(e.target.value)}
|
|||
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
|||
|
|
rows={4}
|
|||
|
|
placeholder="Enter note..."
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
|||
|
|
<button onClick={() => setShowAddNoteModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleAddNote}
|
|||
|
|
disabled={!newNoteText.trim()}
|
|||
|
|
className="px-4 py-2 bg-accent text-white rounded-lg disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
Save Note
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|