diff --git a/src/app/admin/service-centers/[id]/page.tsx b/src/app/admin/service-centers/[id]/page.tsx new file mode 100644 index 0000000..90238f8 --- /dev/null +++ b/src/app/admin/service-centers/[id]/page.tsx @@ -0,0 +1,801 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { + ArrowLeft, Building2, Star, Phone, Mail, MapPin, Activity, Wrench, + Battery, Bike, DollarSign, CheckCircle2, Clock, AlertTriangle, Search, + SlidersHorizontal, ArrowUpDown, User, Calendar, Shield, Tag, Plus, Eye, + BarChart3, Percent, ChevronRight, ExternalLink +} from 'lucide-react'; +import Link from 'next/link'; +import { ServiceCenter } from '../page'; + +// Interface for Maintenance History Record +interface HistoryRecord { + id: string; + date: string; + assetId: string; + assetType: 'EV Bike' | 'Battery'; + serviceType: 'Damage' | 'Repair' | 'Service' | 'Battery Swap' | 'Inspection'; + description: string; + severity: 'critical' | 'major' | 'minor' | 'cosmetic'; + status: 'completed' | 'in_progress' | 'parts_ordered'; + estimatedCost: number; + actualCost: number; + partsUsed: { name: string; qty: number; price: number }[]; + laborCost: number; + technician: string; +} + +// Generate realistic mock history data based on Center ID/Name +const getMockHistoryData = (centerName: string): HistoryRecord[] => { + const baseHistory: HistoryRecord[] = [ + { + id: 'MNT-101', + date: '2024-03-21', + assetId: 'EV-004', + assetType: 'EV Bike', + serviceType: 'Damage', + description: 'Front fender shattered in traffic collision. Replaced brackets and front wheel.', + severity: 'major', + status: 'in_progress', + estimatedCost: 3500, + actualCost: 3200, + partsUsed: [ + { name: 'Front fender', qty: 1, price: 1500 }, + { name: 'Mounting brackets', qty: 2, price: 800 }, + { name: 'Brake pads', qty: 1, price: 600 } + ], + laborCost: 1200, + technician: 'Sabbir Ahmed' + }, + { + id: 'MNT-102', + date: '2024-03-18', + assetId: 'BAT-044', + assetType: 'Battery', + serviceType: 'Battery Swap', + description: 'Internal diagnostic showing rapid voltage degradation. Replaced cells and recalibrated BMS.', + severity: 'critical', + status: 'completed', + estimatedCost: 12000, + actualCost: 11500, + partsUsed: [ + { name: 'Battery 60V cell pack', qty: 1, price: 9500 }, + { name: 'BMS Controller Board', qty: 1, price: 2000 } + ], + laborCost: 2500, + technician: 'Kamrul Hasan' + }, + { + id: 'MNT-103', + date: '2024-03-15', + assetId: 'EV-012', + assetType: 'EV Bike', + serviceType: 'Service', + description: 'Routine 5,000km periodic maintenance. Calibrated drum brakes and greased chassis bearings.', + severity: 'minor', + status: 'completed', + estimatedCost: 1500, + actualCost: 1450, + partsUsed: [ + { name: 'Brake Cable', qty: 1, price: 250 }, + { name: 'Sprocket kit', qty: 1, price: 450 } + ], + laborCost: 750, + technician: 'Sabbir Ahmed' + }, + { + id: 'MNT-104', + date: '2024-03-10', + assetId: 'BAT-021', + assetType: 'Battery', + serviceType: 'Inspection', + description: 'Thermal warning flag during hyper-charging cycle. Terminals cleaned and thermal gel reapplied.', + severity: 'cosmetic', + status: 'completed', + estimatedCost: 500, + actualCost: 400, + partsUsed: [ + { name: 'Thermal paste', qty: 1, price: 150 } + ], + laborCost: 250, + technician: 'Kamrul Hasan' + }, + { + id: 'MNT-105', + date: '2024-03-05', + assetId: 'EV-009', + assetType: 'EV Bike', + serviceType: 'Repair', + description: 'Throttle failure reported by delivery driver. Replaced magnetic sensor assembly.', + severity: 'major', + status: 'completed', + estimatedCost: 1800, + actualCost: 2100, + partsUsed: [ + { name: 'Throttle control assembly', qty: 1, price: 800 }, + { name: 'Wiring loom adapter', qty: 1, price: 450 } + ], + laborCost: 850, + technician: 'Rafiqul Islam' + }, + { + id: 'MNT-106', + date: '2024-02-28', + assetId: 'EV-017', + assetType: 'EV Bike', + serviceType: 'Damage', + description: 'Rear tire blowout due to road debris. Replacement and alignment completed.', + severity: 'minor', + status: 'completed', + estimatedCost: 2800, + actualCost: 2750, + partsUsed: [ + { name: 'Rear Tire tubeless', qty: 1, price: 2200 }, + { name: 'Chain replacement', qty: 1, price: 400 } + ], + laborCost: 500, + technician: 'Sabbir Ahmed' + }, + { + id: 'MNT-107', + date: '2024-02-20', + assetId: 'BAT-089', + assetType: 'Battery', + serviceType: 'Battery Swap', + description: 'Dead module replacement under premium warranty. Replaced sub-assemblies.', + severity: 'critical', + status: 'parts_ordered', + estimatedCost: 15000, + actualCost: 0, + partsUsed: [ + { name: 'Battery 48V cell pack', qty: 1, price: 8000 } + ], + laborCost: 1500, + technician: 'Kamrul Hasan' + } + ]; + + // Variations in records based on Center's specialty & size to make data dynamic + if (centerName.includes('Gulshan') || centerName.includes('Center A')) { + return baseHistory; + } else if (centerName.includes('Banani') || centerName.includes('Center B')) { + return baseHistory.filter(h => h.serviceType === 'Battery Swap' || h.serviceType === 'Service' || h.serviceType === 'Inspection').map(h => ({ + ...h, + id: h.id.replace('10', '20'), + technician: 'Tanvir Rahman' + })); + } else { + // Uttara / Authorized + return baseHistory.filter(h => h.serviceType === 'Inspection' || h.serviceType === 'Repair').map(h => ({ + ...h, + id: h.id.replace('10', '30'), + technician: 'Arif Chowdhury' + })); + } +}; + +export default function ServiceCenterDetailsPage() { + const params = useParams(); + const router = useRouter(); + const id = params.id as string; + + const [isMounted, setIsMounted] = useState(false); + const [center, setCenter] = useState(null); + const [history, setHistory] = useState([]); + + // Filtering / Sorting / Search states for history + const [searchQuery, setSearchQuery] = useState(''); + const [assetFilter, setAssetFilter] = useState('all'); + const [typeFilter, setTypeFilter] = useState('all'); + const [severityFilter, setSeverityFilter] = useState('all'); + const [statusFilter, setStatusFilter] = useState('all'); + + const [sortBy, setSortBy] = useState<'date-desc' | 'date-asc' | 'cost-desc' | 'cost-asc' | 'urgency-desc' | 'urgency-asc'>('date-desc'); + + // Map Interactive detail popup state + const [mapPopup, setMapPopup] = useState(null); + + useEffect(() => { + setIsMounted(true); + + // Load Service Centers from localStorage + const stored = localStorage.getItem('jaiben_service_centers'); + let foundCenter: ServiceCenter | null = null; + + if (stored) { + try { + const centers: ServiceCenter[] = JSON.parse(stored); + foundCenter = centers.find(c => c.id === id) || null; + } catch (e) {} + } + + if (foundCenter) { + setCenter(foundCenter); + setHistory(getMockHistoryData(foundCenter.name)); + } else { + router.push('/admin/service-centers'); + } + }, [id, router]); + + if (!isMounted || !center) return null; + + // Filter History records + const filteredHistory = history.filter(h => { + const matchesSearch = h.id.toLowerCase().includes(searchQuery.toLowerCase()) || + h.assetId.toLowerCase().includes(searchQuery.toLowerCase()) || + h.description.toLowerCase().includes(searchQuery.toLowerCase()) || + h.technician.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesAsset = assetFilter === 'all' || h.assetType === assetFilter; + const matchesType = typeFilter === 'all' || h.serviceType === typeFilter; + const matchesSeverity = severityFilter === 'all' || h.severity === severityFilter; + const matchesStatus = statusFilter === 'all' || h.status === statusFilter; + + return matchesSearch && matchesAsset && matchesType && matchesSeverity && matchesStatus; + }); + + // Sort History records + const severityWeights = { cosmetic: 1, minor: 2, major: 3, critical: 4 }; + const sortedHistory = [...filteredHistory].sort((a, b) => { + switch (sortBy) { + case 'date-desc': + return new Date(b.date).getTime() - new Date(a.date).getTime(); + case 'date-asc': + return new Date(a.date).getTime() - new Date(b.date).getTime(); + case 'cost-desc': + return (b.actualCost || b.estimatedCost) - (a.actualCost || a.estimatedCost); + case 'cost-asc': + return (a.actualCost || a.estimatedCost) - (b.actualCost || b.estimatedCost); + case 'urgency-desc': + return severityWeights[b.severity] - severityWeights[a.severity]; + case 'urgency-asc': + return severityWeights[a.severity] - severityWeights[b.severity]; + default: + return 0; + } + }); + + // Financial & Aggregate calculations + const totalRepairs = history.length; + const completedRepairs = history.filter(h => h.status === 'completed'); + + const totalEstimatedCost = completedRepairs.reduce((sum, h) => sum + h.estimatedCost, 0); + const totalActualCost = completedRepairs.reduce((sum, h) => sum + h.actualCost, 0); + const costVariance = totalActualCost - totalEstimatedCost; + + const totalPartsCost = completedRepairs.reduce((sum, h) => sum + h.partsUsed.reduce((s, p) => s + (p.price * p.qty), 0), 0); + const totalLaborCost = completedRepairs.reduce((sum, h) => sum + h.laborCost, 0); + const totalSpend = totalPartsCost + totalLaborCost; + + // Aggregate Parts Utilized Log + const partsAggregated: { name: string; totalQty: number; totalCost: number }[] = []; + history.forEach(h => { + h.partsUsed.forEach(part => { + const existing = partsAggregated.find(p => p.name === part.name); + if (existing) { + existing.totalQty += part.qty; + existing.totalCost += part.price * part.qty; + } else { + partsAggregated.push({ + name: part.name, + totalQty: part.qty, + totalCost: part.price * part.qty + }); + } + }); + }); + const topParts = partsAggregated.sort((a, b) => b.totalQty - a.totalQty).slice(0, 5); + + const statusColors = { + active: 'bg-emerald-100 text-emerald-700', + busy: 'bg-amber-100 text-amber-700', + inactive: 'bg-slate-100 text-slate-700' + }; + + const severityColors = { + critical: 'bg-red-100 text-red-700', + major: 'bg-orange-100 text-orange-700', + minor: 'bg-amber-100 text-amber-700', + cosmetic: 'bg-slate-100 text-slate-700' + }; + + const statusHistoryColors = { + completed: 'bg-emerald-100 text-emerald-700', + in_progress: 'bg-blue-100 text-blue-700', + parts_ordered: 'bg-purple-100 text-purple-700' + }; + + return ( +
+ + {/* Navigation Top - standard layout of other detail profiles */} +
+ + + + Node Registry: {center.id} + +
+ + {/* Main Profile Info Header - rounded-xl alignment matching maintenance page */} +
+ + {/* Glow effect decorative */} +
+ +
+ +
+ +
+
+ +
+
+

{center.name}

+
+ + {center.status} + +
+ + {center.rating.toFixed(1)} +
+
+
+
+ + {/* Profile items - clean, consistent spacing */} +
+ +
+ +
+ {center.address} + {center.googleMapLink && ( + + Map Link + + )} +
+
+ +
+ + {center.phone} +
+ +
+ + {center.email} +
+ +
+ + Staff: {center.staffCount} technicians +
+ +
+ + Capacity: {center.capacity} total slots +
+ +
+ + {/* Specialization List Header */} +
+ Node Specializations +
+ {center.specialization.map(spec => ( + + {spec} + + ))} +
+
+ +
+ + {/* Quick Stats Header Summary - simplified matching maintenance specs, occupancy not needed */} +
+ +
+

Repairs Logs

+

{totalRepairs}

+

{completedRepairs.length} completed

+
+ +
+

Capacity

+

{center.capacity}

+

Service slots registered

+
+ +
+ +
+ +
+ + {/* Analytics: Map Mockup & Cost Breakdown Grid */} +
+ + {/* INTERACTIVE STYLIZED MAP CONTAINER - aligned clean white rounded-xl styles */} +
+ +
+

Node Location Map

+

Dhaka city arterial coverage grid mockup

+
+ + {/* Map canvas container */} +
+ + {/* Pulsating target coordinate representing the Center */} +
setMapPopup(center.name)} + > + +
+
+
+
+ + {/* Stylized Dhaka grids & landmarks using SVGs */} + + {/* Arterial Highways */} + + + + + + + + + {/* Waterway (Gulshan Lake) */} + + + + {/* Other hubs mockup dots */} + + + + + + + {/* Scale watermark */} +
+ GPS: {center.latitude.toFixed(4)}°N, {center.longitude.toFixed(4)}°E +
+ + {/* Stylized popup when clicked */} + {mapPopup && ( +
+
+ {center.name} + +
+

{center.address}

+
+ Capacity: {center.capacity} slots + Rating: {center.rating.toFixed(1)} +
+
+ )} + + {/* Custom street labels */} +
+ Gulshan Lake Road +
+
+ Tejgaon-Gulshan Link Road +
+ +
+ +
+ 📍 Click GPS coordinate node for telemetry details +
+ +
+ + {/* FINANCIAL PERFORMANCE & EXPENSE TRACKING */} +
+ +
+

Financial Performance & Cost Margins

+

Aggregated historical metrics from completed maintenance invoices

+
+ + {/* Financial details panel */} +
+ +
+ Estimated Spend +
+

৳{totalEstimatedCost.toLocaleString()}

+

Budgeted repairs cost

+
+
+ +
+ Actual Invoice Spend +
+

৳{totalActualCost.toLocaleString()}

+

Billed repair totals

+
+
+ +
0 ? 'bg-rose-50 border-rose-100' : 'bg-emerald-50 border-emerald-100'}`}> + Cost Variance +
+

0 ? 'text-rose-700' : 'text-emerald-700'}`}> + {costVariance > 0 ? `+৳${costVariance.toLocaleString()}` : `-৳${Math.abs(costVariance).toLocaleString()}`} +

+

+ {costVariance > 0 ? 'Over budget invoices' : 'Under budget savings!'} +

+
+
+ +
+ + {/* Parts Used Aggregates & Labor Breakdown */} +
+ + {/* Margins */} +
+

Expense Margin Distribution

+ +
+ + {/* Parts Spend Bar */} +
+
+ Spare Parts Cost + ৳{totalPartsCost.toLocaleString()} ({totalSpend > 0 ? Math.round((totalPartsCost/totalSpend)*100) : 0}%) +
+
+
0 ? (totalPartsCost/totalSpend)*100 : 0}%` }} + /> +
+
+ + {/* Labor Spend Bar */} +
+
+ Labor Costs + ৳{totalLaborCost.toLocaleString()} ({totalSpend > 0 ? Math.round((totalLaborCost/totalSpend)*100) : 0}%) +
+
+
0 ? (totalLaborCost/totalSpend)*100 : 0}%` }} + /> +
+
+ +
+ + {/* General Health Tip */} +
+ + + Cost Ratio Notice: This node maintains a healthy parts-to-labor ratio of {totalSpend > 0 ? Math.round((totalPartsCost/totalSpend)*100) : 0}:{totalSpend > 0 ? Math.round((totalLaborCost/totalSpend)*100) : 0}. Lower labor ratios reflect technician efficiency. + +
+ +
+ + {/* Parts utilized list */} +
+

Top Spare Parts Log

+ +
+ {topParts.length === 0 ? ( +

No spare parts recorded yet

+ ) : topParts.map(part => ( +
+ {part.name} +
+ Qty: {part.totalQty} + ৳{part.totalCost.toLocaleString()} +
+
+ ))} +
+
+ +
+ +
+ +
+ + {/* Interactive History Log - aligned standard filters and table headers */} +
+ + {/* Section title */} +
+
+

+ + Serviced Fleet History Log +

+

Integrated audit list for EV bikes and Battery Swap maintenance nodes

+
+ + {/* Quick Counter */} + + {sortedHistory.length} Matches Found + +
+ + {/* Filter Controls Panel */} +
+ + {/* Search bar */} +
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-3 py-2 border border-slate-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all bg-white" + /> +
+ + {/* Filter 1: Asset Type */} + + + {/* Filter 2: Service Type */} + + + {/* Sorting Dropdown */} + + +
+ + {/* Table / List representation */} + {sortedHistory.length === 0 ? ( +
+ +

No matching service records

+

Try clearing filters or search variables

+
+ ) : ( +
+ + + + + + + + + + + + + + + {sortedHistory.map(record => ( + + + + + + + + + + + + + + + + + + ))} + +
Record IDAsset CodeService TypeDescriptionSeverityStatusInvoice costDetails
{record.id} +
+ {record.assetType === 'EV Bike' ? ( + + ) : ( + + )} +
+ {record.assetId} + {record.assetType} +
+
+
{record.serviceType} +
+

{record.description}

+
+ Tech: {record.technician} + {record.date} +
+
+
+ + {record.severity} + + + + {record.status.replace('_', ' ')} + + + {record.actualCost > 0 ? ( + ৳{record.actualCost.toLocaleString()} + ) : ( + Pending invoice + )} + + {/* Deep link details */} + + + +
+
+ )} + +
+ +
+ ); +} diff --git a/src/app/admin/service-centers/page.tsx b/src/app/admin/service-centers/page.tsx new file mode 100644 index 0000000..2755651 --- /dev/null +++ b/src/app/admin/service-centers/page.tsx @@ -0,0 +1,1106 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { + Search, Plus, Edit, Trash2, Building2, Star, Phone, Mail, + MapPin, Grid, List, Wrench, ChevronRight, X, Save, + AlertCircle, ExternalLink, Globe +} from 'lucide-react'; +import Link from 'next/link'; + +// Interface for Service Center data +export interface ServiceCenter { + id: string; + name: string; + address: string; + googleMapLink: string; + phone: string; + email: string; + rating: number; + capacity: number; // Maximum repair slots + staffCount: number; // Number of technicians + specialization: string[]; + status: 'active' | 'inactive' | 'busy'; + activeRepairs: number; // Current repairs in progress + latitude: number; + longitude: number; +} + +const DEFAULT_CENTERS: ServiceCenter[] = [ + { + id: 'SC-001', + name: 'Service Center A', + address: 'House 45, Road 13, Gulshan 1, Dhaka', + googleMapLink: 'https://maps.google.com/?q=House+45+Road+13+Gulshan+1+Dhaka', + phone: '+8801712345670', + email: 'service.a@jaiben.com', + rating: 4.8, + capacity: 20, + staffCount: 8, + specialization: ['Battery diagnostics', 'Motor Repair', 'Heavy Mechanical', 'Brake Calibration'], + status: 'busy', + activeRepairs: 14, + latitude: 23.777176, + longitude: 90.405537 + }, + { + id: 'SC-002', + name: 'Service Center B', + address: 'Road 11, Banani, Dhaka', + googleMapLink: 'https://maps.google.com/?q=Road+11+Banani+Dhaka', + phone: '+8801712345671', + email: 'service.b@jaiben.com', + rating: 4.5, + capacity: 15, + staffCount: 5, + specialization: ['Battery Swap', 'General Tuning', 'Fast Charging', 'Wiring Fixes'], + status: 'active', + activeRepairs: 7, + latitude: 23.7937, + longitude: 90.4066 + }, + { + id: 'SC-003', + name: 'Authorized Service Center', + address: 'Sector 3, Uttara, Dhaka', + googleMapLink: 'https://maps.google.com/?q=Sector+3+Uttara+Dhaka', + phone: '+8801712345672', + email: 'auth.service@jaiben.com', + rating: 4.2, + capacity: 12, + staffCount: 4, + specialization: ['Routine Inspections', 'Tire Replacement', 'Software Updates', 'Brake Tuning'], + status: 'inactive', + activeRepairs: 0, + latitude: 23.8759, + longitude: 90.3996 + }, + { + id: 'SC-004', + name: 'JAIBEN Service Center - Gulshan', + address: 'Gulshan Link Road, Dhaka', + googleMapLink: 'https://maps.google.com/?q=Gulshan+Link+Road+Dhaka', + phone: '+8801712345673', + email: 'gulshan.hub@jaiben.com', + rating: 4.6, + capacity: 25, + staffCount: 10, + specialization: ['Heavy Mechanical', 'Chassis Alignment', 'Suspension Rebuild', 'Motor Overhaul'], + status: 'active', + activeRepairs: 18, + latitude: 23.7725, + longitude: 90.4088 + }, + { + id: 'SC-005', + name: 'JAIBEN Service Center - Banani', + address: 'Banani Block E, Dhaka', + googleMapLink: 'https://maps.google.com/?q=Banani+Block+E+Dhaka', + phone: '+8801712345674', + email: 'banani.hub@jaiben.com', + rating: 4.7, + capacity: 18, + staffCount: 7, + specialization: ['Battery Diagnostics', 'Cosmetic Repairs', 'Standard Maintenance'], + status: 'active', + activeRepairs: 9, + latitude: 23.7915, + longitude: 90.4022 + } +]; + +const ALL_SPECIALIZATIONS = [ + 'Battery Swap', + 'Battery diagnostics', + 'Motor Repair', + 'Motor Overhaul', + 'Heavy Mechanical', + 'Brake Calibration', + 'Brake Tuning', + 'General Tuning', + 'Fast Charging', + 'Wiring Fixes', + 'Routine Inspections', + 'Tire Replacement', + 'Software Updates', + 'Chassis Alignment', + 'Suspension Rebuild', + 'Cosmetic Repairs', + 'Standard Maintenance' +]; + +export default function ServiceCentersDashboard() { + const router = useRouter(); + const [isMounted, setIsMounted] = useState(false); + const [centers, setCenters] = useState([]); + + // Filtering & View states + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [specFilter, setSpecFilter] = useState('all'); + const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid'); + + // Modals state + const [showAddModal, setShowAddModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [selectedCenter, setSelectedCenter] = useState(null); + + // Form states + const [formData, setFormData] = useState({ + name: '', + address: '', + googleMapLink: '', + phone: '', + email: '', + capacity: 15, + staffCount: 5, + specializations: [] as string[], + status: 'active' as 'active' | 'inactive' | 'busy', + activeRepairs: 0, + rating: 5.0, + latitude: 23.7771, + longitude: 90.4055 + }); + + // Fetch initial data + useEffect(() => { + setIsMounted(true); + const stored = localStorage.getItem('jaiben_service_centers'); + if (stored) { + try { + setCenters(JSON.parse(stored)); + } catch (e) { + setCenters(DEFAULT_CENTERS); + } + } else { + localStorage.setItem('jaiben_service_centers', JSON.stringify(DEFAULT_CENTERS)); + setCenters(DEFAULT_CENTERS); + } + }, []); + + const saveToLocalStorage = (updatedCenters: ServiceCenter[]) => { + localStorage.setItem('jaiben_service_centers', JSON.stringify(updatedCenters)); + setCenters(updatedCenters); + }; + + if (!isMounted) return null; + + // Filter Centers + const filteredCenters = centers.filter(c => { + const matchesSearch = c.name.toLowerCase().includes(searchQuery.toLowerCase()) || + c.address.toLowerCase().includes(searchQuery.toLowerCase()) || + c.phone.includes(searchQuery) || + c.id.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesStatus = statusFilter === 'all' || c.status === statusFilter; + const matchesSpec = specFilter === 'all' || c.specialization.some(s => s.toLowerCase() === specFilter.toLowerCase()); + + return matchesSearch && matchesStatus && matchesSpec; + }); + + // Calculate Statistics (Aligned to Clean and simple parameters) + const stats = { + totalCenters: centers.length, + totalCapacity: centers.reduce((sum, c) => sum + c.capacity, 0), + totalStaff: centers.reduce((sum, c) => sum + c.staffCount, 0), + avgRating: centers.length > 0 ? (centers.reduce((sum, c) => sum + c.rating, 0) / centers.length).toFixed(1) : '0.0' + }; + + // Create Center handler + const handleCreate = (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.name.trim() || !formData.address.trim()) return; + + const newId = `SC-${String(centers.length + 1).padStart(3, '0')}`; + const newCenter: ServiceCenter = { + id: newId, + name: formData.name, + address: formData.address, + googleMapLink: formData.googleMapLink || `https://maps.google.com/?q=${encodeURIComponent(formData.name + ' ' + formData.address)}`, + phone: formData.phone || '+8801700000000', + email: formData.email || 'info@jaiben.com', + rating: formData.rating, + capacity: Number(formData.capacity) || 10, + staffCount: Number(formData.staffCount) || 3, + specialization: formData.specializations.length > 0 ? formData.specializations : ['General Tuning'], + status: formData.status, + activeRepairs: Number(formData.activeRepairs) || 0, + latitude: formData.latitude, + longitude: formData.longitude + }; + + const updated = [...centers, newCenter]; + saveToLocalStorage(updated); + + // Reset Form & Close Modal + setFormData({ + name: '', + address: '', + googleMapLink: '', + phone: '', + email: '', + capacity: 15, + staffCount: 5, + specializations: [], + status: 'active', + activeRepairs: 0, + rating: 5.0, + latitude: 23.7771, + longitude: 90.4055 + }); + setShowAddModal(false); + }; + + // Edit Click Handler + const handleEditClick = (c: ServiceCenter) => { + setSelectedCenter(c); + setFormData({ + name: c.name, + address: c.address, + googleMapLink: c.googleMapLink || '', + phone: c.phone, + email: c.email, + capacity: c.capacity, + staffCount: c.staffCount, + specializations: c.specialization, + status: c.status, + activeRepairs: c.activeRepairs, + rating: c.rating, + latitude: c.latitude, + longitude: c.longitude + }); + setShowEditModal(true); + }; + + // Update Center handler + const handleUpdate = (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedCenter) return; + + const updated = centers.map(c => { + if (c.id === selectedCenter.id) { + return { + ...c, + name: formData.name, + address: formData.address, + googleMapLink: formData.googleMapLink || `https://maps.google.com/?q=${encodeURIComponent(formData.name + ' ' + formData.address)}`, + phone: formData.phone, + email: formData.email, + capacity: Number(formData.capacity), + staffCount: Number(formData.staffCount), + specialization: formData.specializations.length > 0 ? formData.specializations : ['General Tuning'], + status: formData.status, + activeRepairs: Number(formData.activeRepairs), + rating: formData.rating, + latitude: formData.latitude, + longitude: formData.longitude + }; + } + return c; + }); + + saveToLocalStorage(updated); + setShowEditModal(false); + setSelectedCenter(null); + }; + + // Delete handler + const handleDelete = () => { + if (!selectedCenter) return; + const updated = centers.filter(c => c.id !== selectedCenter.id); + saveToLocalStorage(updated); + setShowDeleteModal(false); + setSelectedCenter(null); + }; + + // Specialization checkbox toggling helper + const handleSpecToggle = (spec: string) => { + if (formData.specializations.includes(spec)) { + setFormData({ + ...formData, + specializations: formData.specializations.filter(s => s !== spec) + }); + } else { + setFormData({ + ...formData, + specializations: [...formData.specializations, spec] + }); + } + }; + + const statusColors = { + active: 'bg-emerald-100 text-emerald-700', + busy: 'bg-amber-100 text-amber-700', + inactive: 'bg-slate-100 text-slate-700' + }; + + return ( +
+ + {/* Header aligned like other pages */} +
+
+

Service Centers

+

Configure and manage EV bikes & battery repair nodes across Dhaka city

+
+
+ +
+
+ + {/* Stats Cards - aligned to style of maintenance page */} +
+ +
+
+ +
+
+

{stats.totalCenters}

+

Total Centers

+
+
+ +
+
+ +
+
+

{stats.totalCapacity}

+

Total Capacity

+
+
+ +
+
+ +
+
+

{stats.totalStaff}

+

Technicians

+
+
+ +
+
+ +
+
+

{stats.avgRating}

+

Average Rating

+
+
+ +
+ + {/* Filter and View Toggle Panel - clean, identical spacing and roundings */} +
+ + {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ + {/* Filters */} +
+ + + + + + {/* View mode toggle */} +
+ + +
+ +
+ +
+ + {/* Main Listing Section */} + {filteredCenters.length === 0 ? ( +
+ +

No service centers found

+

Try adjusting your filters or search options

+
+ ) : viewMode === 'grid' ? ( + + // GRID VIEW - Standard rounded-xl styles, matches maintenance records +
+ {filteredCenters.map(center => { + return ( +
+ +
+ +
+
+ + {center.id} + +

+ {center.name} +

+
+ + + {center.status} + +
+ + {/* Rating / Technicians / Address */} +
+
+ + {center.rating.toFixed(1)} + | + {center.staffCount} Technicians +
+ +

+ + {center.address} +

+ + {/* Google Map Link display in cards */} + {center.googleMapLink && ( + e.stopPropagation()} + > + Google Maps Link + + )} +
+ + {/* Specializations Tag List */} +
+ {center.specialization.slice(0, 3).map((spec) => ( + + {spec} + + ))} + {center.specialization.length > 3 && ( + + +{center.specialization.length - 3} more + + )} +
+ +
+ + {/* Footer buttons */} +
+
+ + +
+ + + View Profile + + +
+ +
+ ); + })} +
+ ) : ( + + // TABLE VIEW +
+
+ + + + + + + + + + + + + + {filteredCenters.map(center => { + return ( + + + + + + + + + + ); + })} + +
IDName & LocationSpecializationsStatusTechniciansRatingActions
{center.id} +
+ + {center.name} + +
+ + {center.address} + + {center.googleMapLink && ( + <> + | + + Map Link + + + )} +
+
+
+
+ {center.specialization.slice(0, 2).map(spec => ( + + {spec} + + ))} + {center.specialization.length > 2 && ( + + +{center.specialization.length - 2} more + + )} +
+
+ + {center.status} + + + {center.staffCount} + +
+ + {center.rating.toFixed(1)} +
+
+
+ + + + + +
+
+
+
+ + )} + + {/* REGISTER/CREATE MODAL - aligned design like maintenance form modals */} + {showAddModal && ( +
+
+ +
+
+ +

Register Service Center

+
+ +
+ +
+ +
+ +
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, address: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, googleMapLink: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, phone: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, email: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, capacity: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, staffCount: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + +
+ +
+ + setFormData({ ...formData, rating: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, latitude: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, longitude: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + {/* Specializations list */} +
+ +
+ {ALL_SPECIALIZATIONS.map(spec => ( + + ))} +
+
+ + {/* Action buttons */} +
+ + +
+ +
+
+
+ )} + + {/* EDIT SERVICE CENTER MODAL - aligned design like maintenance detail forms */} + {showEditModal && selectedCenter && ( +
+
+ +
+
+ +

Modify Center: {selectedCenter.id}

+
+ +
+ +
+ +
+ +
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, address: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, googleMapLink: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, phone: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, email: e.target.value })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, capacity: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, staffCount: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + +
+ +
+ + setFormData({ ...formData, rating: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, latitude: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + setFormData({ ...formData, longitude: Number(e.target.value) })} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all" + /> +
+ +
+ + {/* Specializations list */} +
+ +
+ {ALL_SPECIALIZATIONS.map(spec => ( + + ))} +
+
+ + {/* Action buttons */} +
+ + +
+ +
+
+
+ )} + + {/* DELETE CONFIRMATION DIALOG - standard modal sizes */} + {showDeleteModal && selectedCenter && ( +
+
+
+
+ +
+
+

Deregister Service Center?

+

+ Are you sure you want to delete {selectedCenter.name}? + This action is permanent and will remove all local registration data. +

+
+
+ + +
+
+
+
+ )} + +
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 344042d..1eb8769 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -23,7 +23,8 @@ import { LogOut, Calculator, Wrench, - Target, User, History, Bell + Target, User, History, Bell, + Building2 } from 'lucide-react'; import { getUserName, getUserRole, logout } from '@/lib/auth'; @@ -53,6 +54,7 @@ const adminNavItems: NavItem[] = [ { label: 'Swap Stations (P3)', href: '/admin/swap-stations', icon: Zap }, { label: 'Damage & Maintenance', href: '/admin/maintenance', icon: Wrench }, + { label: 'Service Centers', href: '/admin/service-centers', icon: Building2 }, { label: 'Accounting', href: '/admin/accounting', icon: Calculator }, { label: 'Hubs', href: '/admin/hub', icon: MapPin }, { label: 'Reports', href: '/admin/reports', icon: BarChart3 },