1417 lines
85 KiB
TypeScript
1417 lines
85 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Users, Bike, DollarSign, TrendingUp, Activity, ArrowUpRight, ArrowDownRight,
|
|
Clock, Shield, AlertTriangle, CheckCircle, XCircle, RefreshCw,
|
|
Search, Lock, Unlock, Volume2, ShieldAlert, Cpu, Terminal, Settings, ChevronRight,
|
|
Database, PlusCircle, Wrench, BatteryCharging, CreditCard, FileText, Check, ArrowDownUp, Ticket, UserCheck, Eye, Trash2
|
|
} from 'lucide-react';
|
|
import { kycRequests as mockKycRequests, bikes as mockBikes, rentals as mockRentals, transactions as mockTransactions } from '@/data/mockData';
|
|
import toast from 'react-hot-toast';
|
|
import Link from 'next/link';
|
|
|
|
interface AuditLog {
|
|
id: string;
|
|
timestamp: string;
|
|
source: 'FLEET' | 'KYC' | 'SWAP' | 'ACCOUNTING' | 'USER';
|
|
level: 'info' | 'warning' | 'critical' | 'success';
|
|
message: string;
|
|
}
|
|
|
|
const initialAuditLogs: AuditLog[] = [
|
|
{ id: 'log-1', timestamp: '23:25:12', source: 'FLEET', level: 'info', message: 'EV Bike Plate Dhaka Metro-1290 reported battery status at 88%' },
|
|
{ id: 'log-2', timestamp: '23:24:45', source: 'KYC', level: 'success', message: 'Front Desk submitted TIN verification for Biker USR-009' },
|
|
{ id: 'log-3', timestamp: '23:22:10', source: 'SWAP', level: 'warning', message: 'Gulshan Swap Station Cabinet #5 reported abnormal temperature cell rise (48°C)' },
|
|
{ id: 'log-4', timestamp: '23:20:05', source: 'ACCOUNTING', level: 'info', message: 'Journal Draft JV-2026-004 auto-posted for Biker rental fee payment' },
|
|
{ id: 'log-5', timestamp: '23:18:15', source: 'USER', level: 'critical', message: 'Overdue Lock Pending for Bike Plate Dhaka Metro-5621 (Farid Ahmed)' }
|
|
];
|
|
|
|
const mockLiveMessages = [
|
|
{ source: 'FLEET' as const, level: 'info' as const, message: 'GPS coordinates synced for Yadea DT3 (Dhaka Metro-004)' },
|
|
{ source: 'SWAP' as const, level: 'success' as const, message: 'Biker Rahim Ahmed completed battery swap in 18 seconds (Cabinet #2)' },
|
|
{ source: 'ACCOUNTING' as const, level: 'success' as const, message: 'Payment gateway captured ৳300 auto-inflow for single rent' },
|
|
{ source: 'FLEET' as const, level: 'warning' as const, message: 'Speed alert triggered: EV Dhaka Metro-1290 exceeded 55 km/h in urban zone' },
|
|
{ source: 'KYC' as const, level: 'info' as const, message: 'Biker Jamal Uddin completed verification of email address' },
|
|
{ source: 'SWAP' as const, level: 'critical' as const, message: 'Cabinet #8 in Banani Station reported battery lock failure' },
|
|
{ source: 'ACCOUNTING' as const, level: 'info' as const, message: 'Investor return ledger recalculated for gold plan tier' }
|
|
];
|
|
|
|
export default function AdminDashboard() {
|
|
// Simulated Roles: super_admin | hub_manager | accountant | front_desk
|
|
const [activeRole, setActiveRole] = useState<'super_admin' | 'hub_manager' | 'accountant' | 'front_desk'>('super_admin');
|
|
|
|
// Live Dynamic State
|
|
const [kycRequests, setKycRequests] = useState(mockKycRequests);
|
|
const [telematicsBikes, setTelematicsBikes] = useState([
|
|
{ id: 'tel-1', model: 'Etron ET50', plateNumber: 'Dhaka Metro Cha-1234', batteryLevel: 78, location: 'Gulshan 1', status: 'rented' },
|
|
{ id: 'tel-2', model: 'Yadea DT3', plateNumber: 'Dhaka Metro Cha-5678', batteryLevel: 95, location: 'Banani', status: 'available' },
|
|
{ id: 'tel-3', model: 'AIMA Lightning', plateNumber: 'Dhaka Metro Cha-9012', batteryLevel: 62, location: 'Uttara', status: 'rented' },
|
|
{ id: 'tel-4', model: 'TVS iQube', plateNumber: 'Dhaka Metro Cha-3456', batteryLevel: 45, location: 'Workshop', status: 'available' }
|
|
]);
|
|
const [rentalsList, setRentalsList] = useState(mockRentals);
|
|
const [auditLogs, setAuditLogs] = useState<AuditLog[]>(initialAuditLogs);
|
|
const [activeChartMetric, setActiveChartMetric] = useState<'revenue' | 'utilization' | 'swaps'>('revenue');
|
|
|
|
// Interactive Filters & Searches
|
|
const [kycSearch, setKycSearch] = useState('');
|
|
const [fleetSearch, setFleetSearch] = useState('');
|
|
const [sandboxMode, setSandboxMode] = useState(true);
|
|
const [liveStreamActive, setLiveStreamActive] = useState(true);
|
|
const [systemTime, setSystemTime] = useState('');
|
|
const [showToolsDrawer, setShowToolsDrawer] = useState(false);
|
|
|
|
// Hub Manager Interactive battery cabinet states
|
|
const [cabinetSlots, setCabinetSlots] = useState([
|
|
{ slot: 1, charge: 98, status: 'fully_charged', temp: 31 },
|
|
{ slot: 2, charge: 95, status: 'fully_charged', temp: 32 },
|
|
{ slot: 3, charge: 15, status: 'charging', temp: 38 },
|
|
{ slot: 4, charge: 88, status: 'ready', temp: 33 },
|
|
{ slot: 5, charge: 5, status: 'discharged', temp: 42 },
|
|
{ slot: 6, charge: 96, status: 'fully_charged', temp: 30 },
|
|
{ slot: 7, charge: 0, status: 'empty', temp: 25 },
|
|
{ slot: 8, charge: 91, status: 'ready', temp: 34 }
|
|
]);
|
|
|
|
// Hub Manager Service Tickets
|
|
const [serviceTickets, setServiceTickets] = useState([
|
|
{ id: 'tkt-01', model: 'Yadea DT3', issue: 'Rear Brake Pad friction replacement', urgency: 'high', hub: 'Gulshan Hub' },
|
|
{ id: 'tkt-02', model: 'TVS iQube', issue: 'Front Left LED replacement', urgency: 'medium', hub: 'Gulshan Hub' },
|
|
{ id: 'tkt-03', model: 'Etron ET50', issue: 'Throttle wire connectivity check', urgency: 'low', hub: 'Banani Hub' }
|
|
]);
|
|
|
|
// Accountant Interactive vouchers
|
|
const [journalVouchers, setJournalVouchers] = useState([
|
|
{ id: 'JV-2026-004', desc: 'Accrued Biker Rental fees single pay', debit: 45600, credit: 45600, date: '17 May', status: 'draft' },
|
|
{ id: 'JV-2026-003', desc: 'Swap cabinet electricity amortization', debit: 12000, credit: 12000, date: '16 May', status: 'posted' },
|
|
{ id: 'JV-2026-002', desc: 'Disbursed Investor returns Gold tier share', debit: 62100, credit: 62100, date: '15 May', status: 'posted' }
|
|
]);
|
|
|
|
// Accountant past due bikers
|
|
const [pastDueBikers, setPastDueBikers] = useState([
|
|
{ id: 'usr-09', name: 'Farid Ahmed', pastDueAmount: 1200, lastAttempt: '14 May', mobile: '+8801700000001' },
|
|
{ id: 'usr-11', name: 'Tariqul Islam', pastDueAmount: 850, lastAttempt: '15 May', mobile: '+8801700000002' }
|
|
]);
|
|
|
|
// Front Desk walk-in check-in bikers queue
|
|
const [walkinQueue, setWalkinQueue] = useState([
|
|
{ id: 'q-1', name: 'Zahid Hasan', purpose: 'EV Assignment check-in', regDate: '17:42' },
|
|
{ id: 'q-2', name: 'Mahbub Alam', purpose: 'Physical document KYC file check', regDate: '17:45' }
|
|
]);
|
|
|
|
// Front Desk Ready Plates
|
|
const [assignedPlateVal, setAssignedPlateVal] = useState('Dhaka Metro Cha-5678');
|
|
|
|
// Real-time ticking Clock & Live WebSocket Telemetry Simulation
|
|
useEffect(() => {
|
|
const updateTime = () => {
|
|
const now = new Date();
|
|
setSystemTime(now.toLocaleTimeString());
|
|
};
|
|
updateTime();
|
|
const clockInterval = setInterval(updateTime, 1000);
|
|
|
|
let liveLogInterval: NodeJS.Timeout;
|
|
if (liveStreamActive) {
|
|
liveLogInterval = setInterval(() => {
|
|
const randMsg = mockLiveMessages[Math.floor(Math.random() * mockLiveMessages.length)];
|
|
const now = new Date();
|
|
const timestampStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
|
|
const newLog: AuditLog = {
|
|
id: `sim-log-${Date.now()}`,
|
|
timestamp: timestampStr,
|
|
source: randMsg.source,
|
|
level: randMsg.level,
|
|
message: randMsg.message
|
|
};
|
|
|
|
setAuditLogs(prev => [newLog, ...prev.slice(0, 7)]);
|
|
}, 4500);
|
|
}
|
|
|
|
return () => {
|
|
clearInterval(clockInterval);
|
|
if (liveLogInterval) clearInterval(liveLogInterval);
|
|
};
|
|
}, [liveStreamActive]);
|
|
|
|
// Dynamic state actions
|
|
const handleApproveKYC = (id: string, name: string) => {
|
|
setKycRequests(prev => prev.map(k => k.id === id ? { ...k, status: 'approved' } : k));
|
|
toast.success(`Applicant ${name} successfully approved. Role updated to Biker!`);
|
|
|
|
const now = new Date().toLocaleTimeString();
|
|
setAuditLogs(prev => [
|
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'KYC', level: 'success', message: `Admin approved KYC document verification for Biker ${name}` },
|
|
...prev
|
|
]);
|
|
};
|
|
|
|
const handleRejectKYC = (id: string, name: string) => {
|
|
setKycRequests(prev => prev.map(k => k.id === id ? { ...k, status: 'rejected' } : k));
|
|
toast.error(`Applicant ${name} KYC documents rejected.`);
|
|
|
|
const now = new Date().toLocaleTimeString();
|
|
setAuditLogs(prev => [
|
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'KYC', level: 'warning', message: `Admin rejected KYC files for Biker ${name} (Blurry images)` },
|
|
...prev
|
|
]);
|
|
};
|
|
|
|
const handleToggleLock = (bikeId: string, model: string, currentStatus: string) => {
|
|
toast.loading(`Sending OTA secure immobilization package to EV...`, { duration: 1000 });
|
|
|
|
setTimeout(() => {
|
|
setTelematicsBikes(prev => prev.map(b => {
|
|
if (b.id === bikeId) {
|
|
const nextStatus = b.status === 'rented' ? 'available' : 'rented';
|
|
toast.success(`Vehicle ${b.plateNumber} remote status set to: ${nextStatus === 'rented' ? 'LOCKED' : 'UNLOCKED'}`);
|
|
return { ...b, status: nextStatus };
|
|
}
|
|
return b;
|
|
}));
|
|
|
|
const now = new Date().toLocaleTimeString();
|
|
setAuditLogs(prev => [
|
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'FLEET', level: 'critical', message: `OTA secure signal sent: Toggled lock state for vehicle ${model}` },
|
|
...prev
|
|
]);
|
|
}, 1000);
|
|
};
|
|
|
|
const handleTriggerSiren = (plateNumber: string) => {
|
|
toast.success(`Siren audio warning triggered remotely on ${plateNumber}!`, { icon: '🔊' });
|
|
const now = new Date().toLocaleTimeString();
|
|
setAuditLogs(prev => [
|
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'FLEET', level: 'warning', message: `Admin triggered audio warning beacon on vehicle ${plateNumber}` },
|
|
...prev
|
|
]);
|
|
};
|
|
|
|
// Hub Manager remote opening
|
|
const handleOpenCabinetSlot = (slot: number) => {
|
|
toast.loading(`Issuing remote release command for swap slot #${slot}...`, { duration: 1200 });
|
|
setTimeout(() => {
|
|
setCabinetSlots(prev => prev.map(s => s.slot === slot ? { ...s, status: 'empty', charge: 0 } : s));
|
|
toast.success(`Cabinet Slot #${slot} released successfully! Door opened.`, { icon: '🔓' });
|
|
|
|
const now = new Date().toLocaleTimeString();
|
|
setAuditLogs(prev => [
|
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'SWAP', level: 'success', message: `Hub Manager remotely opened lock for charging slot Cabinet #${slot}` },
|
|
...prev
|
|
]);
|
|
}, 1200);
|
|
};
|
|
|
|
// Dispatch mechanics
|
|
const handleDispatchTicket = (id: string, issue: string) => {
|
|
toast.success(`Mechanics team dispatched for ticket ${id}!`);
|
|
setServiceTickets(prev => prev.filter(t => t.id !== id));
|
|
|
|
const now = new Date().toLocaleTimeString();
|
|
setAuditLogs(prev => [
|
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'FLEET', level: 'info', message: `Service Ticket ${id} dispatched: ${issue}` },
|
|
...prev
|
|
]);
|
|
};
|
|
|
|
// Accountant post voucher
|
|
const handlePostVoucher = (id: string) => {
|
|
setJournalVouchers(prev => prev.map(v => v.id === id ? { ...v, status: 'posted' } : v));
|
|
toast.success(`Journal Voucher ${id} posted successfully to double-entry ledger!`, { icon: '📝' });
|
|
|
|
const now = new Date().toLocaleTimeString();
|
|
setAuditLogs(prev => [
|
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'ACCOUNTING', level: 'success', message: `Accountant posted Journal Entry ${id} to active books` },
|
|
...prev
|
|
]);
|
|
};
|
|
|
|
// Accountant trigger warning SMS
|
|
const handleSendBillWarning = (name: string, amt: number) => {
|
|
toast.success(`Past due payment reminder of ৳${amt} sent to ${name}!`, { icon: '💬' });
|
|
const now = new Date().toLocaleTimeString();
|
|
setAuditLogs(prev => [
|
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'USER', level: 'warning', message: `SMS payment collection reminder dispatched to ${name} (৳${amt})` },
|
|
...prev
|
|
]);
|
|
};
|
|
|
|
// Front Desk Check-in biker Walk-in assignment
|
|
const handleAssignBikerWalkin = (qId: string, name: string) => {
|
|
toast.success(`EV plate ${assignedPlateVal} successfully assigned to Checked-in biker ${name}!`, { icon: '🏍️' });
|
|
setWalkinQueue(prev => prev.filter(q => q.id !== qId));
|
|
|
|
// Add Yadea/TVS assignment update
|
|
const now = new Date().toLocaleTimeString();
|
|
setAuditLogs(prev => [
|
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'USER', level: 'success', message: `Front Desk checked-in walk-in Biker ${name} to vehicle ${assignedPlateVal}` },
|
|
...prev
|
|
]);
|
|
};
|
|
|
|
// Environmental load simulations
|
|
const runSimulatorTool = (type: string) => {
|
|
setShowToolsDrawer(false);
|
|
toast.loading(`Applying simulation conditions...`, { duration: 1000 });
|
|
|
|
setTimeout(() => {
|
|
const now = new Date().toLocaleTimeString();
|
|
switch (type) {
|
|
case 'rain':
|
|
toast.success('Simulation active: Monsoon load applied! Restricting speed limit.');
|
|
setAuditLogs(prev => [
|
|
{ id: `sim-${Date.now()}`, timestamp: now, source: 'FLEET', level: 'warning', message: 'Load rules: Monsoon weather limits active. Battery cell lock set to 45°C.' },
|
|
...prev
|
|
]);
|
|
break;
|
|
case 'swaps':
|
|
toast.success('Simulation: Grid peak loading limits active.');
|
|
setAuditLogs(prev => [
|
|
{ id: `sim-${Date.now()}`, timestamp: now, source: 'SWAP', level: 'info', message: 'Power stress: Grid swap balancer activated. Fast charge locked.' },
|
|
...prev
|
|
]);
|
|
break;
|
|
case 'backup':
|
|
toast.success('Ledger journals compressed and backed up.');
|
|
setAuditLogs(prev => [
|
|
{ id: `sim-${Date.now()}`, timestamp: now, source: 'ACCOUNTING', level: 'success', message: 'Secure ledger checkpoint drafted and saved to backup servers' },
|
|
...prev
|
|
]);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
// Filter calculations
|
|
const pendingKYCList = kycRequests.filter(k => k.status === 'pending');
|
|
const activeRentalsCount = rentalsList.filter(r => r.status === 'active').length;
|
|
const totalBikersCount = 156 + (kycRequests.filter(k => k.status === 'approved').length);
|
|
|
|
const filteredKYCs = pendingKYCList.filter(k => {
|
|
const q = kycSearch.toLowerCase();
|
|
return k.userName.toLowerCase().includes(q) || k.phone.includes(q);
|
|
});
|
|
|
|
const filteredFleet = telematicsBikes.filter(b => {
|
|
const q = fleetSearch.toLowerCase();
|
|
return b.model.toLowerCase().includes(q) || b.plateNumber.toLowerCase().includes(q) || b.location.toLowerCase().includes(q);
|
|
});
|
|
|
|
return (
|
|
<div className="p-4 lg:p-6 max-w-8xl mx-auto space-y-6">
|
|
|
|
{/* 👑 SUPER ADMIN SECURITY HEADER BANNER WITH DYNAMIC ROLE SWITCHER */}
|
|
<div className="bg-white text-slate-800 rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden">
|
|
<div className="absolute right-0 top-0 translate-x-12 -translate-y-12 w-64 h-64 bg-accent/5 rounded-full blur-3xl" />
|
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 relative z-10">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 rounded-xl bg-accent/5 border border-accent/20 flex items-center justify-center text-accent shrink-0 shadow-sm">
|
|
<Shield className="w-6 h-6" />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<h1 className="text-xl lg:text-2xl font-black tracking-tight text-slate-800">JAIBEN Operations Command Center</h1>
|
|
<span className="px-2.5 py-0.5 bg-amber-100 border border-amber-200 text-amber-800 rounded-full text-[10px] font-extrabold uppercase tracking-wider flex items-center gap-1">
|
|
👑 SUPER ADMIN ACCESS
|
|
</span>
|
|
{sandboxMode && (
|
|
<span className="px-2 py-0.5 bg-emerald-100 border border-emerald-250 text-emerald-800 rounded-full text-[10px] font-bold">
|
|
Sandbox Active
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-1">Real-time telemetry, OTA secure triggers, and accounting ledger supervisor node</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* DYNAMIC ROLE IMPERSONATION dropdown */}
|
|
<div className="flex items-center gap-3 bg-slate-50 p-2.5 rounded-xl border border-slate-200 self-start lg:self-center font-bold text-xs">
|
|
<span className="text-slate-400 uppercase text-[9px]">Role Node:</span>
|
|
<select
|
|
value={activeRole}
|
|
onChange={(e) => {
|
|
setActiveRole(e.target.value as any);
|
|
toast.success(`Switched active dashboard to: ${e.target.value.replace('_', ' ').toUpperCase()}`);
|
|
}}
|
|
className="bg-white border border-slate-200 text-slate-700 px-3 py-1.5 rounded-lg text-xs font-extrabold focus:outline-none cursor-pointer hover:border-slate-350"
|
|
>
|
|
<option value="super_admin">Super Admin / Manager</option>
|
|
<option value="hub_manager">Hub Manager (Gulshan/Banani)</option>
|
|
<option value="accountant">Accountant / Corporate Books</option>
|
|
<option value="front_desk">Front Desk Officer (Reception)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ========================================================================================================= */}
|
|
{/* 1. 👑 SUPER ADMIN VIEW / STATS / WORKSPACES */}
|
|
{/* ========================================================================================================= */}
|
|
{activeRole === 'super_admin' && (
|
|
<>
|
|
{/* STATS */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{[
|
|
{ label: 'Total active Bikers', value: totalBikersCount, change: '+14%', trend: 'up', icon: Users, color: 'text-blue-600 border-blue-100 bg-blue-50/30' },
|
|
{ label: 'Live Active Rentals', value: activeRentalsCount, change: '+18%', trend: 'up', icon: Bike, color: 'text-emerald-600 border-emerald-100 bg-emerald-50/30' },
|
|
{ label: 'Aggregate Cash Flow', value: '৳984.6k', change: '+28%', trend: 'up', icon: DollarSign, color: 'text-purple-600 border-purple-100 bg-purple-50/30' },
|
|
{ label: 'Cabinet Swaps (24h)', value: '254', change: '-4%', trend: 'down', icon: TrendingUp, color: 'text-amber-600 border-amber-100 bg-amber-50/30' }
|
|
].map((stat, i) => {
|
|
const Icon = stat.icon;
|
|
return (
|
|
<div key={i} className="bg-white rounded-xl p-4 shadow-sm border border-slate-100 flex flex-col justify-between hover:shadow-md transition-shadow relative overflow-hidden">
|
|
<div className="flex justify-between items-start">
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center border ${stat.color}`}>
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full flex items-center gap-0.5 ${stat.trend === 'up' ? 'bg-green-50 border border-green-200 text-green-700' : 'bg-red-50 border border-red-200 text-red-700'
|
|
}`}>
|
|
{stat.trend === 'up' ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
|
|
{stat.change}
|
|
</span>
|
|
</div>
|
|
<div className="mt-4">
|
|
<p className="text-2xl font-black text-slate-800 tracking-tight">{stat.value}</p>
|
|
<p className="text-xs text-slate-400 font-semibold uppercase mt-0.5">{stat.label}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* TELEMETRY CHARTS & LOG TERMINAL */}
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-5 space-y-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b border-slate-100 pb-4">
|
|
<div>
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><Activity className="w-4 h-4 text-accent" /> Telemetry Analytics</h2>
|
|
<p className="text-xs text-slate-400">Live operational ledger logs plotted against target performance guidelines</p>
|
|
</div>
|
|
<div className="flex gap-1.5 p-1 bg-slate-50 border border-slate-200 rounded-lg self-start">
|
|
{[
|
|
{ id: 'revenue', label: 'Revenue Curve' },
|
|
{ id: 'utilization', label: 'Utilization' },
|
|
{ id: 'swaps', label: 'Cabinet stress' }
|
|
].map(opt => (
|
|
<button
|
|
key={opt.id}
|
|
onClick={() => { setActiveChartMetric(opt.id as any); toast.success(`Loaded ${opt.label}`); }}
|
|
className={`px-3 py-1 text-xs font-bold rounded-md transition-all cursor-pointer ${activeChartMetric === opt.id ? 'bg-accent text-white shadow' : 'text-accent hover:text-slate-850'
|
|
}`}
|
|
>
|
|
{opt.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hand-Crafted Interactive Vector Graph */}
|
|
<div className="bg-slate-100 p-4 rounded-xl border border-slate-50 relative overflow-hidden shadow-inner">
|
|
<div className="absolute top-3 right-3 flex items-center gap-1.5 text-[9px] text-slate-400 font-mono">
|
|
<span className="w-2.5 h-2.5 bg-accent rounded-full inline-block" /> Active Operations
|
|
<span className="w-2.5 h-2.5 bg-slate-650 rounded-full inline-block ml-2" /> Target Projection
|
|
</div>
|
|
|
|
{activeChartMetric === 'revenue' && (
|
|
<svg viewBox="0 0 500 200" className="w-full h-auto text-accent select-none overflow-visible">
|
|
<defs>
|
|
<linearGradient id="chartGrad" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="var(--color-accent, #10b981)" stopOpacity="0.25" />
|
|
<stop offset="100%" stopColor="var(--color-accent, #10b981)" stopOpacity="0.0" />
|
|
</linearGradient>
|
|
</defs>
|
|
<line x1="0" y1="50" x2="500" y2="50" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="100" x2="500" y2="100" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="150" x2="500" y2="150" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<path d="M 0,160 Q 120,130 250,90 T 500,40" fill="none" stroke="#64748b" strokeWidth="2" strokeDasharray="4" />
|
|
<path d="M 0,170 C 80,180 150,110 250,120 C 350,130 400,60 500,50 L 500,200 L 0,200 Z" fill="url(#chartGrad)" />
|
|
<path d="M 0,170 C 80,180 150,110 250,120 C 350,130 400,60 500,50" fill="none" stroke="currentColor" strokeWidth="3" className="stroke-accent" />
|
|
<circle cx="250" cy="120" r="5" fill="#ffffff" className="stroke-accent" strokeWidth="3" />
|
|
<circle cx="500" cy="50" r="5" fill="#ffffff" className="stroke-accent" strokeWidth="3" />
|
|
<g transform="translate(265, 105)" className="opacity-95">
|
|
<rect width="90" height="35" rx="5" fill="#0f172a" stroke="#334155" strokeWidth="1" />
|
|
<text x="8" y="15" fill="#94a3b8" fontSize="8" fontWeight="bold" fontFamily="monospace">Date: 17th May</text>
|
|
<text x="8" y="27" fill="#ffffff" fontSize="9" fontWeight="bold" fontFamily="sans-serif">৳45,600 Daily</text>
|
|
</g>
|
|
<text x="5" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Mon</text>
|
|
<text x="125" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Tue</text>
|
|
<text x="245" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Wed</text>
|
|
<text x="365" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Thu</text>
|
|
<text x="475" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Fri</text>
|
|
</svg>
|
|
)}
|
|
|
|
{activeChartMetric === 'utilization' && (
|
|
<svg viewBox="0 0 500 200" className="w-full h-auto text-blue-500 select-none overflow-visible">
|
|
<defs>
|
|
<linearGradient id="chartGradBlue" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.25" />
|
|
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0.0" />
|
|
</linearGradient>
|
|
</defs>
|
|
<line x1="0" y1="50" x2="500" y2="50" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="100" x2="500" y2="100" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="150" x2="500" y2="150" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<path d="M 0,140 Q 150,140 250,70 T 500,60" fill="none" stroke="#64748b" strokeWidth="2" strokeDasharray="4" />
|
|
<path d="M 0,150 C 90,130 180,160 250,90 C 320,20 410,95 500,40 L 500,200 L 0,200 Z" fill="url(#chartGradBlue)" />
|
|
<path d="M 0,150 C 90,130 180,160 250,90 C 320,20 410,95 500,40" fill="none" stroke="#3b82f6" strokeWidth="3" />
|
|
<circle cx="250" cy="90" r="5" fill="#ffffff" stroke="#3b82f6" strokeWidth="3" />
|
|
<g transform="translate(265, 75)">
|
|
<rect width="90" height="35" rx="5" fill="#0f172a" stroke="#334155" strokeWidth="1" />
|
|
<text x="8" y="15" fill="#94a3b8" fontSize="8" fontWeight="bold" fontFamily="monospace">Utilization rate</text>
|
|
<text x="8" y="27" fill="#3b82f6" fontSize="10" fontWeight="bold" fontFamily="sans-serif">78% capacity</text>
|
|
</g>
|
|
<text x="5" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Mon</text>
|
|
<text x="125" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Tue</text>
|
|
<text x="245" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Wed</text>
|
|
<text x="365" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Thu</text>
|
|
<text x="475" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Fri</text>
|
|
</svg>
|
|
)}
|
|
|
|
{activeChartMetric === 'swaps' && (
|
|
<svg viewBox="0 0 500 200" className="w-full h-auto text-purple-500 select-none overflow-visible">
|
|
<defs>
|
|
<linearGradient id="chartGradPurple" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#a855f7" stopOpacity="0.25" />
|
|
<stop offset="100%" stopColor="#a855f7" stopOpacity="0.0" />
|
|
</linearGradient>
|
|
</defs>
|
|
<line x1="0" y1="50" x2="500" y2="50" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="100" x2="500" y2="100" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="150" x2="500" y2="150" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<path d="M 0,160 Q 150,110 250,100 T 500,80" fill="none" stroke="#64748b" strokeWidth="2" strokeDasharray="4" />
|
|
<path d="M 0,180 C 100,140 120,70 250,90 C 350,110 390,40 500,70 L 500,200 L 0,200 Z" fill="url(#chartGradPurple)" />
|
|
<path d="M 0,180 C 100,140 120,70 250,90 C 350,110 390,40 500,70" fill="none" stroke="#a855f7" strokeWidth="3" />
|
|
<circle cx="250" cy="90" r="5" fill="#ffffff" stroke="#a855f7" strokeWidth="3" />
|
|
<g transform="translate(265, 75)">
|
|
<rect width="90" height="35" rx="5" fill="#0f172a" stroke="#334155" strokeWidth="1" />
|
|
<text x="8" y="15" fill="#94a3b8" fontSize="8" fontWeight="bold" fontFamily="monospace">Swaps volume</text>
|
|
<text x="8" y="27" fill="#a855f7" fontSize="10" fontWeight="bold" fontFamily="sans-serif">254 completed</text>
|
|
</g>
|
|
<text x="5" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Mon</text>
|
|
<text x="125" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Tue</text>
|
|
<text x="245" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Wed</text>
|
|
<text x="365" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Thu</text>
|
|
<text x="475" y="195" fill="#64748b" fontSize="8" fontFamily="monospace">Fri</text>
|
|
</svg>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* LIVE TELEMETRY BLACK FEED TERMINAL */}
|
|
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-lg flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
|
|
<h2 className="font-extrabold text-sm text-slate-200 flex items-center gap-2">
|
|
<Terminal className="w-4 h-4 text-emerald-400 animate-pulse" /> Live Telemetry Feed
|
|
</h2>
|
|
<span className="w-2.5 h-2.5 bg-emerald-500 rounded-full inline-block animate-ping" />
|
|
</div>
|
|
|
|
<div className="p-4 font-mono text-[11px] text-slate-300 space-y-3 flex-1 overflow-y-auto max-h-[220px] lg:max-h-[300px]">
|
|
{auditLogs.map(log => {
|
|
const colors = {
|
|
info: 'text-blue-400',
|
|
warning: 'text-amber-400',
|
|
critical: 'text-red-400 font-bold',
|
|
success: 'text-emerald-400'
|
|
};
|
|
return (
|
|
<div key={log.id} className="space-y-0.5 border-b border-slate-800/40 pb-2">
|
|
<div className="flex justify-between items-center text-[10px] text-slate-500">
|
|
<span>[{log.timestamp}] source: <strong className="text-slate-400">{log.source}</strong></span>
|
|
<span className={`uppercase font-bold text-[8px] px-1 py-0.2 rounded bg-slate-800/50 ${colors[log.level]}`}>
|
|
{log.level}
|
|
</span>
|
|
</div>
|
|
<p className="leading-relaxed text-slate-350">{log.message}</p>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="p-3 border-t border-slate-800 bg-slate-950/60 flex justify-between items-center">
|
|
<span className="text-[10px] text-slate-500 font-mono">Telemetry synchronized</span>
|
|
<button
|
|
onClick={() => { setAuditLogs(initialAuditLogs); toast.success('Telemetry logs reset'); }}
|
|
className="text-[10px] font-bold text-accent hover:underline cursor-pointer"
|
|
>
|
|
Reset Logs
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* DUAL WORKSPACE: INTERACTIVE KYC DESK & TELEMATICS FLEET CONTROL */}
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
{/* KYC Pending Desk */}
|
|
<div className="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-100 flex items-center justify-between flex-wrap gap-2">
|
|
<div>
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><Shield className="w-4 h-4 text-amber-500" /> KYC Pending Desk</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">{pendingKYCList.length} requests await secure super-admin sign-off</p>
|
|
</div>
|
|
<div className="relative w-full sm:w-auto">
|
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Filter applicant..."
|
|
value={kycSearch}
|
|
onChange={(e) => setKycSearch(e.target.value)}
|
|
className="pl-8 pr-3 py-1 border border-slate-200 rounded-lg text-xs bg-white text-slate-700 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="divide-y divide-slate-100 flex-1 overflow-y-auto max-h-[350px]">
|
|
{filteredKYCs.map(kyc => (
|
|
<div key={kyc.id} className="p-4 hover:bg-slate-50/50 transition-colors space-y-3">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<h4 className="font-bold text-slate-800 text-sm">{kyc.userName}</h4>
|
|
<p className="text-xs text-slate-500 mt-0.5">{kyc.phone}</p>
|
|
<div className="flex gap-1.5 mt-2">
|
|
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[9px] font-semibold">TIN Verified</span>
|
|
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[9px] font-semibold">NID Match</span>
|
|
</div>
|
|
</div>
|
|
<span className="text-[10px] text-slate-400 font-semibold">{kyc.submittedAt}</span>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleApproveKYC(kyc.id, kyc.userName)}
|
|
className="flex-1 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold rounded-lg transition-colors flex items-center justify-center gap-1 cursor-pointer"
|
|
>
|
|
<CheckCircle className="w-3.5 h-3.5" /> Approve
|
|
</button>
|
|
<button
|
|
onClick={() => handleRejectKYC(kyc.id, kyc.userName)}
|
|
className="flex-1 py-1.5 border border-red-200 text-red-600 hover:bg-red-50 text-xs font-bold rounded-lg transition-colors flex items-center justify-center gap-1 cursor-pointer"
|
|
>
|
|
<XCircle className="w-3.5 h-3.5" /> Reject
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{filteredKYCs.length === 0 && (
|
|
<div className="p-12 text-center text-slate-400 space-y-2">
|
|
<CheckCircle className="w-12 h-12 text-slate-200 mx-auto" />
|
|
<p className="text-xs font-semibold">No pending KYC files.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Live Hardware EV Fleet lock controller dashboard */}
|
|
<div className="lg:col-span-2 bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-100 flex items-center justify-between flex-wrap gap-2">
|
|
<div>
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><Bike className="w-4 h-4 text-accent" /> OTA Vehicle Telematics Dashboard</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Direct OTA remote control node to override vehicle immobilizers or warn operators</p>
|
|
</div>
|
|
|
|
<div className="relative w-full sm:w-auto">
|
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Filter EV plate number..."
|
|
value={fleetSearch}
|
|
onChange={(e) => setFleetSearch(e.target.value)}
|
|
className="pl-8 pr-3 py-1 border border-slate-200 rounded-lg text-xs bg-white text-slate-700 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop Table View */}
|
|
<div className="hidden md:block overflow-x-auto flex-1">
|
|
<table className="w-full text-xs">
|
|
<thead className="bg-slate-50/50 border-b border-slate-100">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left font-bold text-slate-500 uppercase tracking-wider">EV Info</th>
|
|
<th className="px-4 py-3 text-left font-bold text-slate-500 uppercase tracking-wider">Charge status</th>
|
|
<th className="px-4 py-3 text-left font-bold text-slate-500 uppercase tracking-wider">Audit location</th>
|
|
<th className="px-4 py-3 text-left font-bold text-slate-500 uppercase tracking-wider">OTA Lock Override</th>
|
|
<th className="px-4 py-3 text-right font-bold text-slate-500 uppercase tracking-wider">Siren Warning</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-50">
|
|
{filteredFleet.map(bike => {
|
|
const isRented = bike.status === 'rented';
|
|
return (
|
|
<tr key={bike.id} className="hover:bg-slate-50/40 transition-colors">
|
|
<td className="px-4 py-3">
|
|
<p className="font-extrabold text-slate-800">{bike.model}</p>
|
|
<span className="text-[10px] text-slate-400 font-mono tracking-tight">{bike.plateNumber}</span>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className={`w-2.5 h-2.5 rounded-full inline-block ${bike.batteryLevel > 70 ? 'bg-green-500' : bike.batteryLevel > 30 ? 'bg-amber-500' : 'bg-red-500'
|
|
}`} />
|
|
<span className="font-mono font-bold text-slate-700">{bike.batteryLevel}%</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3 font-semibold text-slate-500">
|
|
{bike.location}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<button
|
|
onClick={() => handleToggleLock(bike.id, bike.model, bike.status)}
|
|
className={`py-1 px-3 rounded-lg text-[10px] font-extrabold tracking-wider uppercase transition-all flex items-center gap-1 shadow-sm cursor-pointer ${isRented
|
|
? 'bg-slate-900 border border-slate-900 text-white hover:bg-slate-800'
|
|
: 'bg-emerald-50 border border-emerald-250 text-emerald-700 hover:bg-emerald-100'
|
|
}`}
|
|
>
|
|
{isRented ? (
|
|
<>
|
|
<Lock className="w-3.5 h-3.5" /> Secure locked
|
|
</>
|
|
) : (
|
|
<>
|
|
<Unlock className="w-3.5 h-3.5" /> Active unlocked
|
|
</>
|
|
)}
|
|
</button>
|
|
</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<button
|
|
onClick={() => handleTriggerSiren(bike.plateNumber)}
|
|
className="p-1.5 hover:bg-red-50 rounded-lg text-slate-400 hover:text-red-650 transition-colors cursor-pointer"
|
|
title="Trigger Anti-theft Audio Warning Beacon"
|
|
>
|
|
<Volume2 className="w-4 h-4" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Mobile Card Layout */}
|
|
<div className="block md:hidden p-4 space-y-3 overflow-y-auto max-h-[350px] bg-slate-50/30 border-t border-slate-100">
|
|
{filteredFleet.map(bike => {
|
|
const isRented = bike.status === 'rented';
|
|
return (
|
|
<div key={bike.id} className="bg-white p-4 rounded-xl border border-slate-100 shadow-sm space-y-3">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h4 className="font-extrabold text-slate-800 text-sm">{bike.model}</h4>
|
|
<p className="text-[10px] text-slate-400 font-mono mt-0.5">{bike.plateNumber}</p>
|
|
</div>
|
|
<div className="flex items-center gap-1 bg-slate-50 border border-slate-100 rounded-lg px-2 py-0.5">
|
|
<span className={`w-2 h-2 rounded-full ${bike.batteryLevel > 70 ? 'bg-green-500' : bike.batteryLevel > 30 ? 'bg-amber-500' : 'bg-red-500'
|
|
}`} />
|
|
<span className="font-mono text-slate-700 font-bold text-[10px]">{bike.batteryLevel}%</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-between items-center text-xs font-bold text-slate-500">
|
|
<span>Audit Location:</span>
|
|
<span className="text-slate-800">{bike.location}</span>
|
|
</div>
|
|
<div className="flex gap-2 pt-1 border-t border-slate-50">
|
|
<button
|
|
onClick={() => handleToggleLock(bike.id, bike.model, bike.status)}
|
|
className={`flex-1 py-1.5 rounded-lg text-[10px] font-extrabold tracking-wider uppercase flex items-center justify-center gap-1 shadow-sm cursor-pointer ${isRented
|
|
? 'bg-slate-900 border border-slate-900 text-white hover:bg-slate-800'
|
|
: 'bg-emerald-50 border border-emerald-250 text-emerald-700 hover:bg-emerald-100'
|
|
}`}
|
|
>
|
|
{isRented ? (
|
|
<><Lock className="w-3 h-3" /> Secure locked</>
|
|
) : (
|
|
<><Unlock className="w-3 h-3" /> Active unlocked</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => handleTriggerSiren(bike.plateNumber)}
|
|
className="px-3 py-1.5 border border-slate-200 hover:bg-red-50 text-slate-400 hover:text-red-650 rounded-lg transition-colors flex items-center justify-center cursor-pointer"
|
|
>
|
|
<Volume2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* ========================================================================================================= */}
|
|
{/* 2. 🏢 HUB MANAGER VIEW / STATS / WORKSPACES */}
|
|
{/* ========================================================================================================= */}
|
|
{activeRole === 'hub_manager' && (
|
|
<>
|
|
{/* STATS */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{[
|
|
{ label: 'Total EV Hub Inventory', value: '28 Bikes', change: '100% capacity', icon: Bike, color: 'text-blue-600 border-blue-100 bg-blue-50/30' },
|
|
{ label: 'Swappable Battery Cells', value: '84 Cells', change: 'Gulshan Hub', icon: BatteryCharging, color: 'text-emerald-600 border-emerald-100 bg-emerald-50/30' },
|
|
{ label: 'Active Charging Cabinets', value: '16 Slots', change: '8 empty slots', icon: Cpu, color: 'text-purple-600 border-purple-100 bg-purple-50/30' },
|
|
{ label: 'Pending Service Tickets', value: serviceTickets.length, change: '2 critical dispatches', icon: Wrench, color: 'text-amber-600 border-amber-100 bg-amber-50/30' }
|
|
].map((stat, i) => {
|
|
const Icon = stat.icon;
|
|
return (
|
|
<div key={i} className="bg-white rounded-xl p-4 shadow-sm border border-slate-100 flex flex-col justify-between hover:shadow-md transition-shadow relative overflow-hidden">
|
|
<div className="flex justify-between items-start">
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center border ${stat.color}`}>
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
<span className="text-[9px] font-extrabold uppercase px-2 py-0.5 bg-slate-100 border rounded-full text-slate-500">
|
|
{stat.change}
|
|
</span>
|
|
</div>
|
|
<div className="mt-4">
|
|
<p className="text-2xl font-black text-slate-800 tracking-tight">{stat.value}</p>
|
|
<p className="text-xs text-slate-400 font-semibold uppercase mt-0.5">{stat.label}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* TELEMETRY CHARTS & THERMAL SPECTRUMS */}
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-5 space-y-4">
|
|
<div>
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><BatteryCharging className="w-4 h-4 text-emerald-500" /> Hub Grid Thermal Balancing Curve</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Gulshan Cabinet battery temperature levels monitored during concurrent quick swap balancing</p>
|
|
</div>
|
|
|
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 relative overflow-hidden">
|
|
<svg viewBox="0 0 500 150" className="w-full h-auto text-emerald-500 select-none overflow-visible">
|
|
<line x1="0" y1="40" x2="500" y2="40" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="80" x2="500" y2="80" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="120" x2="500" y2="120" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
|
|
{/* Temp Curve (Normal limits: 30-45 deg C) */}
|
|
<path d="M 0,130 C 80,120 150,60 250,90 C 350,110 400,50 500,45" fill="none" stroke="currentColor" strokeWidth="3" />
|
|
<circle cx="250" cy="90" r="5" fill="#ffffff" stroke="#10b981" strokeWidth="3" />
|
|
|
|
<g transform="translate(265, 75)">
|
|
<rect width="90" height="35" rx="5" fill="#0f172a" stroke="#334155" strokeWidth="1" />
|
|
<text x="8" y="15" fill="#94a3b8" fontSize="8" fontWeight="bold" fontFamily="monospace">Cell Temp</text>
|
|
<text x="8" y="27" fill="#10b981" fontSize="10" fontWeight="bold" fontFamily="sans-serif">38°C Balanced</text>
|
|
</g>
|
|
<text x="5" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">08:00</text>
|
|
<text x="125" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">12:00</text>
|
|
<text x="245" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">16:00</text>
|
|
<text x="365" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">20:00</text>
|
|
<text x="475" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">23:00</text>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
{/* LIVE TELEMETRY BLACK FEED TERMINAL */}
|
|
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-lg flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
|
|
<h2 className="font-extrabold text-sm text-slate-200 flex items-center gap-2">
|
|
<Terminal className="w-4 h-4 text-emerald-400 animate-pulse" /> Live Telemetry Feed
|
|
</h2>
|
|
<span className="w-2.5 h-2.5 bg-emerald-500 rounded-full inline-block animate-ping" />
|
|
</div>
|
|
|
|
<div className="p-4 font-mono text-[11px] text-slate-300 space-y-3 flex-1 overflow-y-auto max-h-[220px]">
|
|
{auditLogs.filter(l => l.source === 'SWAP' || l.source === 'FLEET').map(log => (
|
|
<div key={log.id} className="space-y-0.5 border-b border-slate-800/40 pb-2">
|
|
<div className="flex justify-between items-center text-[10px] text-slate-500">
|
|
<span>[{log.timestamp}] source: <strong className="text-slate-400">{log.source}</strong></span>
|
|
</div>
|
|
<p className="leading-relaxed text-slate-350">{log.message}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="p-3 border-t border-slate-800 bg-slate-950/60 text-right">
|
|
<span className="text-[10px] text-slate-500 font-mono">Hub Telemetry synced</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* DUAL WORKSPACE: INTERACTIVE CABINET DOOR RELEASER & SERVICE TICKETS */}
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
|
|
{/* Interactive Cabinet Slots Releaser */}
|
|
<div className="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-100">
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><Cpu className="w-4 h-4 text-accent" /> Gulshan Swap Cabinet #1</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Click to remotely release slot electromagnet lock during hardware issues</p>
|
|
</div>
|
|
|
|
<div className="p-4 grid grid-cols-4 gap-2 flex-1">
|
|
{cabinetSlots.map(cab => {
|
|
const isEmpty = cab.status === 'empty';
|
|
const isCharging = cab.status === 'charging';
|
|
return (
|
|
<button
|
|
key={cab.slot}
|
|
onClick={() => !isEmpty && handleOpenCabinetSlot(cab.slot)}
|
|
disabled={isEmpty}
|
|
className={`p-2 rounded-xl border flex flex-col justify-between text-left h-20 transition-all cursor-pointer ${
|
|
isEmpty
|
|
? 'bg-slate-50 border-slate-200 text-slate-400 opacity-60 cursor-not-allowed'
|
|
: isCharging
|
|
? 'bg-amber-50 border-amber-200 text-amber-700 hover:border-amber-400'
|
|
: 'bg-emerald-50 border-emerald-250 text-emerald-800 hover:border-emerald-400'
|
|
}`}
|
|
>
|
|
<span className="text-[9px] font-bold">Slot #{cab.slot}</span>
|
|
<div>
|
|
<p className="text-[10px] font-black">{isEmpty ? 'Empty' : `${cab.charge}%`}</p>
|
|
<span className="text-[8px] opacity-75 font-semibold leading-none block mt-0.5">{cab.temp}°C</span>
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Service / Maintenance Tickets Desk */}
|
|
<div className="lg:col-span-2 bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-100">
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><Wrench className="w-4 h-4 text-amber-500" /> Active Maintenance Ticket queue</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Authorize physical mechanics team dispatchments to handle bike failures</p>
|
|
</div>
|
|
|
|
<div className="divide-y divide-slate-100 flex-1">
|
|
{serviceTickets.map(tkt => (
|
|
<div key={tkt.id} className="p-4 hover:bg-slate-50/50 transition-colors flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs font-semibold">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-extrabold text-slate-800 text-sm">{tkt.model}</span>
|
|
<span className="px-2 py-0.5 bg-slate-150 border border-slate-200 text-slate-650 rounded text-[9px] font-bold font-mono">{tkt.id}</span>
|
|
<span className={`px-2 py-0.5 rounded text-[8px] font-extrabold uppercase ${
|
|
tkt.urgency === 'high' ? 'bg-red-105 text-red-700' : 'bg-amber-100 text-amber-700'
|
|
}`}>
|
|
{tkt.urgency} Urgency
|
|
</span>
|
|
</div>
|
|
<p className="text-slate-500">{tkt.issue}</p>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => handleDispatchTicket(tkt.id, tkt.issue)}
|
|
className="py-1.5 px-4 bg-slate-900 border border-slate-900 text-white rounded-lg text-xs font-bold hover:bg-slate-850 transition-all cursor-pointer"
|
|
>
|
|
Dispatch mechanics
|
|
</button>
|
|
</div>
|
|
))}
|
|
|
|
{serviceTickets.length === 0 && (
|
|
<div className="p-12 text-center text-slate-400 space-y-2">
|
|
<CheckCircle className="w-12 h-12 text-slate-200 mx-auto" />
|
|
<p className="text-xs font-semibold">All service tickets resolved! Hub operational.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* ========================================================================================================= */}
|
|
{/* 3. 💼 ACCOUNTANT VIEW / STATS / WORKSPACES */}
|
|
{/* ========================================================================================================= */}
|
|
{activeRole === 'accountant' && (
|
|
<>
|
|
{/* STATS */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{[
|
|
{ label: 'Daily Rental Inflow', value: '৳45,600', change: '+23% daily', icon: DollarSign, color: 'text-blue-600 border-blue-100 bg-blue-50/30' },
|
|
{ label: 'Total Rental Earnings', value: '৳128,400', change: 'Active ledger', icon: ArrowDownUp, color: 'text-emerald-600 border-emerald-100 bg-emerald-50/30' },
|
|
{ label: 'Disbursed gold ROI payout', value: '৳62,100', change: '84 investors', icon: CreditCard, color: 'text-purple-600 border-purple-100 bg-purple-50/30' },
|
|
{ label: 'A/R Overdue balance', value: `৳${pastDueBikers.reduce((s, b) => s + b.pastDueAmount, 0)}`, change: `${pastDueBikers.length} overdue accounts`, icon: FileText, color: 'text-amber-600 border-amber-100 bg-amber-50/30' }
|
|
].map((stat, i) => {
|
|
const Icon = stat.icon;
|
|
return (
|
|
<div key={i} className="bg-white rounded-xl p-4 shadow-sm border border-slate-100 flex flex-col justify-between hover:shadow-md transition-shadow relative overflow-hidden">
|
|
<div className="flex justify-between items-start">
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center border ${stat.color}`}>
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
<span className="text-[9px] font-extrabold uppercase px-2 py-0.5 bg-slate-100 border rounded-full text-slate-500">
|
|
{stat.change}
|
|
</span>
|
|
</div>
|
|
<div className="mt-4">
|
|
<p className="text-2xl font-black text-slate-800 tracking-tight">{stat.value}</p>
|
|
<p className="text-xs text-slate-400 font-semibold uppercase mt-0.5">{stat.label}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* TELEMETRY CHARTS & ACCOUNTING BALANCES */}
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-5 space-y-4">
|
|
<div>
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><DollarSign className="w-4 h-4 text-purple-500" /> Weekly Corporate Cash Ledger curve</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Rental receivables collection compared with investor yield payouts</p>
|
|
</div>
|
|
|
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 relative overflow-hidden">
|
|
<svg viewBox="0 0 500 150" className="w-full h-auto text-purple-500 select-none overflow-visible">
|
|
<line x1="0" y1="40" x2="500" y2="40" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="80" x2="500" y2="80" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="120" x2="500" y2="120" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
|
|
{/* Ledger Curve */}
|
|
<path d="M 0,110 Q 150,140 250,70 T 500,30" fill="none" stroke="#475569" strokeWidth="2" strokeDasharray="4" />
|
|
<path d="M 0,130 C 90,80 180,150 250,60 C 320,20 410,95 500,25" fill="none" stroke="currentColor" strokeWidth="3" />
|
|
<circle cx="250" cy="60" r="5" fill="#ffffff" stroke="#a855f7" strokeWidth="3" />
|
|
|
|
<g transform="translate(265, 45)">
|
|
<rect width="90" height="35" rx="5" fill="#0f172a" stroke="#334155" strokeWidth="1" />
|
|
<text x="8" y="15" fill="#94a3b8" fontSize="8" fontWeight="bold" fontFamily="monospace">Ledger Flow</text>
|
|
<text x="8" y="27" fill="#a855f7" fontSize="10" fontWeight="bold" fontFamily="sans-serif">৳128,400 Pay</text>
|
|
</g>
|
|
<text x="5" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">Mon</text>
|
|
<text x="125" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">Tue</text>
|
|
<text x="245" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">Wed</text>
|
|
<text x="365" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">Thu</text>
|
|
<text x="475" y="145" fill="#64748b" fontSize="8" fontFamily="monospace">Fri</text>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
{/* LIVE TELEMETRY BLACK FEED TERMINAL */}
|
|
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-lg flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
|
|
<h2 className="font-extrabold text-sm text-slate-200 flex items-center gap-2">
|
|
<Terminal className="w-4 h-4 text-emerald-400 animate-pulse" /> Live Telemetry Feed
|
|
</h2>
|
|
<span className="w-2.5 h-2.5 bg-emerald-500 rounded-full inline-block animate-ping" />
|
|
</div>
|
|
|
|
<div className="p-4 font-mono text-[11px] text-slate-300 space-y-3 flex-1 overflow-y-auto max-h-[220px]">
|
|
{auditLogs.filter(l => l.source === 'ACCOUNTING' || l.source === 'USER').map(log => (
|
|
<div key={log.id} className="space-y-0.5 border-b border-slate-800/40 pb-2">
|
|
<div className="flex justify-between items-center text-[10px] text-slate-500">
|
|
<span>[{log.timestamp}] source: <strong className="text-slate-400">{log.source}</strong></span>
|
|
</div>
|
|
<p className="leading-relaxed text-slate-350">{log.message}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="p-3 border-t border-slate-800 bg-slate-950/60 text-right">
|
|
<span className="text-[10px] text-slate-500 font-mono">Ledger Node synced</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* DUAL WORKSPACE: JOURNAL ENTRIES DRAFTS & OVERDUE COLLECTIONS */}
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
|
|
{/* Journal Entry Drafts Desk */}
|
|
<div className="lg:col-span-2 bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-100">
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><FileText className="w-4 h-4 text-accent" /> Active Journal Voucher drafts</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Post auto-accrued billing vouchers to Active Corporate books</p>
|
|
</div>
|
|
|
|
<div className="divide-y divide-slate-100 flex-1">
|
|
{journalVouchers.map(jv => (
|
|
<div key={jv.id} className="p-4 hover:bg-slate-50/50 transition-colors flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs font-semibold">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-extrabold text-slate-800 text-sm">{jv.id}</span>
|
|
<span className="px-2 py-0.5 bg-slate-100 text-slate-650 rounded text-[9px] font-bold font-mono">{jv.date}</span>
|
|
<span className={`px-2.5 py-0.5 rounded-full text-[9px] font-extrabold uppercase ${
|
|
jv.status === 'posted' ? 'bg-emerald-50 border border-emerald-200 text-emerald-800' : 'bg-amber-100 text-amber-700'
|
|
}`}>
|
|
{jv.status}
|
|
</span>
|
|
</div>
|
|
<p className="text-slate-500">{jv.desc}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-right">
|
|
<p className="text-slate-400 text-[10px]">Debit / Credit</p>
|
|
<p className="font-extrabold text-slate-800">৳{jv.debit}</p>
|
|
</div>
|
|
{jv.status === 'draft' ? (
|
|
<button
|
|
onClick={() => handlePostVoucher(jv.id)}
|
|
className="py-1.5 px-4 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition-all cursor-pointer"
|
|
>
|
|
Post Voucher
|
|
</button>
|
|
) : (
|
|
<div className="py-1.5 px-4 bg-slate-100 text-slate-400 rounded-lg text-xs font-bold flex items-center gap-1 select-none">
|
|
<Check className="w-3.5 h-3.5 text-emerald-500" /> Posted
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Overdue Biker Collections desk */}
|
|
<div className="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-100">
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><AlertTriangle className="w-4 h-4 text-red-500" /> Overdue Biker Ledgers</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Collect due balances or issue instant remote SMS payment warnings</p>
|
|
</div>
|
|
|
|
<div className="divide-y divide-slate-100 flex-1">
|
|
{pastDueBikers.map(bkr => (
|
|
<div key={bkr.id} className="p-4 hover:bg-slate-50/50 transition-colors space-y-3 text-xs font-semibold">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h4 className="font-bold text-slate-800 text-sm">{bkr.name}</h4>
|
|
<p className="text-[10px] text-slate-400 font-mono mt-0.5">{bkr.mobile}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-[9px] text-slate-450 uppercase leading-none">Past Due</p>
|
|
<p className="font-extrabold text-red-600 text-sm mt-0.5">৳{bkr.pastDueAmount}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleSendBillWarning(bkr.name, bkr.pastDueAmount)}
|
|
className="flex-1 py-1.5 border border-slate-200 hover:bg-slate-50 text-slate-750 text-[10px] font-bold rounded-lg transition-colors flex items-center justify-center gap-1 cursor-pointer"
|
|
>
|
|
<Volume2 className="w-3 h-3" /> SMS Reminder
|
|
</button>
|
|
<button
|
|
onClick={() => handleToggleLock('tel-3', bkr.name, 'rented')}
|
|
className="flex-1 py-1.5 bg-slate-900 border border-slate-900 hover:bg-slate-800 text-white text-[10px] font-bold rounded-lg transition-colors flex items-center justify-center gap-1 cursor-pointer"
|
|
>
|
|
<Lock className="w-3 h-3" /> OTA Lock
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* ========================================================================================================= */}
|
|
{/* 4. 🙋 FRONT DESK OFFICER VIEW / STATS / WORKSPACES */}
|
|
{/* ========================================================================================================= */}
|
|
{activeRole === 'front_desk' && (
|
|
<>
|
|
{/* STATS */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{[
|
|
{ label: 'Bikers Waiting in Queue', value: `${walkinQueue.length} Bikers`, change: 'Walk-in lobby', icon: Users, color: 'text-blue-600 border-blue-100 bg-blue-50/30' },
|
|
{ label: 'Ready EV Plates in parking', value: '8 Bikes', change: 'Assigned available', icon: Bike, color: 'text-emerald-600 border-emerald-100 bg-emerald-50/30' },
|
|
{ label: 'Incomplete KYC Files', value: `${kycRequests.filter(k => k.status === 'pending').length} Files`, change: 'Awaiting doc upload', icon: Shield, color: 'text-purple-600 border-purple-100 bg-purple-50/30' },
|
|
{ label: 'Swaps Processed Today', value: '42 Swaps', change: 'Manual tokens issued', icon: BatteryCharging, color: 'text-amber-600 border-amber-100 bg-amber-50/30' }
|
|
].map((stat, i) => {
|
|
const Icon = stat.icon;
|
|
return (
|
|
<div key={i} className="bg-white rounded-xl p-4 shadow-sm border border-slate-100 flex flex-col justify-between hover:shadow-md transition-shadow relative overflow-hidden">
|
|
<div className="flex justify-between items-start">
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center border ${stat.color}`}>
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
<span className="text-[9px] font-extrabold uppercase px-2 py-0.5 bg-slate-100 border rounded-full text-slate-500">
|
|
{stat.change}
|
|
</span>
|
|
</div>
|
|
<div className="mt-4">
|
|
<p className="text-2xl font-black text-slate-800 tracking-tight">{stat.value}</p>
|
|
<p className="text-xs text-slate-400 font-semibold uppercase mt-0.5">{stat.label}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* TELEMETRY CHARTS & WALK-IN LOG TELEMETRY */}
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-5 space-y-4">
|
|
<div>
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><Ticket className="w-4 h-4 text-emerald-500" /> Walk-in Swaps Queue density</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Average wait times mapped against support hours throughout today</p>
|
|
</div>
|
|
|
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 relative overflow-hidden">
|
|
<svg viewBox="0 0 500 150" className="w-full h-auto text-blue-500 select-none overflow-visible">
|
|
<line x1="0" y1="40" x2="500" y2="40" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="80" x2="500" y2="80" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
<line x1="0" y1="120" x2="500" y2="120" stroke="#e2e8f0" strokeDasharray="3" strokeWidth="0.5" />
|
|
|
|
{/* Wait times Bar Chart SVG */}
|
|
<rect x="30" y="70" width="30" height="50" rx="3" fill="#3b82f6" />
|
|
<rect x="110" y="40" width="30" height="80" rx="3" fill="#3b82f6" />
|
|
<rect x="190" y="30" width="30" height="90" rx="3" fill="#3b82f6" />
|
|
<rect x="270" y="60" width="30" height="60" rx="3" fill="#3b82f6" />
|
|
<rect x="350" y="80" width="30" height="40" rx="3" fill="#3b82f6" />
|
|
<rect x="430" y="90" width="30" height="30" rx="3" fill="#3b82f6" />
|
|
|
|
<text x="35" y="140" fill="#64748b" fontSize="8" fontFamily="monospace">09:00</text>
|
|
<text x="115" y="140" fill="#64748b" fontSize="8" fontFamily="monospace">11:00</text>
|
|
<text x="195" y="140" fill="#64748b" fontSize="8" fontFamily="monospace">13:00</text>
|
|
<text x="275" y="140" fill="#64748b" fontSize="8" fontFamily="monospace">15:00</text>
|
|
<text x="355" y="140" fill="#64748b" fontSize="8" fontFamily="monospace">17:00</text>
|
|
<text x="435" y="140" fill="#64748b" fontSize="8" fontFamily="monospace">19:00</text>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
{/* LIVE TELEMETRY BLACK FEED TERMINAL */}
|
|
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-lg flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
|
|
<h2 className="font-extrabold text-sm text-slate-200 flex items-center gap-2">
|
|
<Terminal className="w-4 h-4 text-emerald-400 animate-pulse" /> Live Telemetry Feed
|
|
</h2>
|
|
<span className="w-2.5 h-2.5 bg-emerald-500 rounded-full inline-block animate-ping" />
|
|
</div>
|
|
|
|
<div className="p-4 font-mono text-[11px] text-slate-300 space-y-3 flex-1 overflow-y-auto max-h-[220px]">
|
|
{auditLogs.filter(l => l.source === 'USER' || l.source === 'KYC').map(log => (
|
|
<div key={log.id} className="space-y-0.5 border-b border-slate-800/40 pb-2">
|
|
<div className="flex justify-between items-center text-[10px] text-slate-500">
|
|
<span>[{log.timestamp}] source: <strong className="text-slate-400">{log.source}</strong></span>
|
|
</div>
|
|
<p className="leading-relaxed text-slate-350">{log.message}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="p-3 border-t border-slate-800 bg-slate-950/60 text-right">
|
|
<span className="text-[10px] text-slate-500 font-mono">Front Desk synced</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* DUAL WORKSPACE: WALK-IN BIKE ASSIGNMENTS & KYC PHYSICAL DOCUMENTS DESK */}
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
|
|
{/* Walk-in Biker check-in desk */}
|
|
<div className="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-100">
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><UserCheck className="w-4 h-4 text-accent" /> Walk-in Biker Assignment desk</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Assign ready EV plate numbers directly to lobby checked-in bikers</p>
|
|
</div>
|
|
|
|
<div className="divide-y divide-slate-100 flex-1">
|
|
{walkinQueue.map(qBkr => (
|
|
<div key={qBkr.id} className="p-4 hover:bg-slate-50/50 transition-colors space-y-3 text-xs font-semibold">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h4 className="font-bold text-slate-800 text-sm">{qBkr.name}</h4>
|
|
<p className="text-[10px] text-slate-450 mt-0.5">{qBkr.purpose}</p>
|
|
</div>
|
|
<span className="text-[10px] text-slate-400 font-mono">{qBkr.regDate} check-in</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<select
|
|
value={assignedPlateVal}
|
|
onChange={(e) => setAssignedPlateVal(e.target.value)}
|
|
className="flex-1 bg-white border border-slate-200 text-slate-700 px-2.5 py-1.5 rounded-lg text-xs font-bold focus:outline-none"
|
|
>
|
|
<option value="Dhaka Metro Cha-5678">Yadea DT3 (Banani)</option>
|
|
<option value="Dhaka Metro Cha-3456">TVS iQube (Workshop)</option>
|
|
</select>
|
|
<button
|
|
onClick={() => handleAssignBikerWalkin(qBkr.id, qBkr.name)}
|
|
className="py-1.5 px-4 bg-accent hover:bg-accent-dark text-white rounded-lg text-xs font-bold transition-all cursor-pointer"
|
|
>
|
|
Assign EV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{walkinQueue.length === 0 && (
|
|
<div className="p-12 text-center text-slate-400 space-y-2">
|
|
<CheckCircle className="w-12 h-12 text-slate-200 mx-auto" />
|
|
<p className="text-xs font-semibold">All checked-in walkins assigned! Lobby empty.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Incomplete KYC physical document uploads */}
|
|
<div className="lg:col-span-2 bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden flex flex-col justify-between">
|
|
<div className="px-5 py-4 border-b border-slate-100 flex items-center justify-between flex-wrap gap-2">
|
|
<div>
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><Shield className="w-4 h-4 text-purple-500" /> Walk-in KYC Upload Queue</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">{pendingKYCList.length} bikers require physical document scan uploads</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="divide-y divide-slate-100 flex-1">
|
|
{pendingKYCList.slice(0, 3).map(kyc => (
|
|
<div key={kyc.id} className="p-4 hover:bg-slate-50/50 transition-colors flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs font-semibold">
|
|
<div className="space-y-0.5">
|
|
<p className="font-extrabold text-slate-800 text-sm">{kyc.userName}</p>
|
|
<p className="text-slate-450 font-mono text-[10px]">{kyc.phone} • Submitted: {kyc.submittedAt}</p>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => toast.success(`Uploading TIN PDF doc files for ${kyc.userName}`)}
|
|
className="py-1.5 px-3 bg-white border border-slate-200 text-slate-750 rounded-lg text-xs font-bold hover:bg-slate-50 transition-all cursor-pointer flex items-center gap-1"
|
|
>
|
|
<PlusCircle className="w-3.5 h-3.5" /> Upload TIN
|
|
</button>
|
|
<button
|
|
onClick={() => handleApproveKYC(kyc.id, kyc.userName)}
|
|
className="py-1.5 px-4 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition-all cursor-pointer flex items-center gap-1"
|
|
>
|
|
<CheckCircle className="w-3.5 h-3.5" /> Verify NID
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* COMMAND COMMANDS Drawer triggers */}
|
|
{activeRole === 'super_admin' && (
|
|
<div className="grid lg:grid-cols-2 gap-6">
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5 space-y-4">
|
|
<div>
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><Settings className="w-4 h-4 text-purple-600" /> Super-Admin Commands Console</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Rapid access control widgets to configure environment variables or databases</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
{[
|
|
{ label: 'Sandbox Payments', active: sandboxMode, onClick: () => { setSandboxMode(!sandboxMode); toast.success(sandboxMode ? 'Sandbox payments deactivated' : 'Sandbox active'); } },
|
|
{ label: 'Live Events Logs', active: liveStreamActive, onClick: () => setLiveStreamActive(!liveStreamActive) },
|
|
{ label: 'Run global Load', active: false, onClick: () => setShowToolsDrawer(true) },
|
|
{ label: 'Database Backup', active: false, onClick: () => runSimulatorTool('backup') }
|
|
].map((tool, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={tool.onClick}
|
|
className={`p-3 rounded-xl border flex flex-col justify-between text-left h-24 transition-all cursor-pointer ${tool.active
|
|
? 'border-accent bg-accent/5 text-accent shadow-sm'
|
|
: 'border-slate-200 text-slate-655 bg-white hover:border-slate-350 hover:bg-slate-50/50'
|
|
}`}
|
|
>
|
|
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-400">Environment Node</span>
|
|
<div>
|
|
<p className="text-xs font-black text-slate-850 leading-tight">{tool.label}</p>
|
|
<span className="text-[9px] font-bold text-slate-400 uppercase mt-0.5 block">
|
|
{tool.active ? 'active / enabled' : 'click to run'}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Global Security Alerts Panel */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5 space-y-4">
|
|
<div>
|
|
<h2 className="font-extrabold text-slate-800 flex items-center gap-1.5"><ShieldAlert className="w-4 h-4 text-red-500 animate-bounce" /> Live Security & Telemetry alerts</h2>
|
|
<p className="text-xs text-slate-400 mt-0.5">Critical operating limit warnings pushed from connected swap hardware nodes</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3 p-3 bg-red-50 rounded-xl border border-red-100">
|
|
<AlertTriangle className="w-5 h-5 text-red-500 animate-pulse" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs font-bold text-red-800 leading-tight">Monsoon Load Warning active</p>
|
|
<p className="text-[10px] text-red-500 mt-0.5">Speed limited to 30km/h for Yadea fleet elements inside high rainfall areas.</p>
|
|
</div>
|
|
<button
|
|
onClick={() => toast.success('Override monsoons restrictions accepted')}
|
|
className="text-[9px] font-extrabold uppercase text-red-700 bg-red-100 hover:bg-red-200 px-2 py-1 rounded transition-colors shrink-0 cursor-pointer"
|
|
>
|
|
Override Rules
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 p-3 bg-amber-50 rounded-xl border border-amber-100">
|
|
<Clock className="w-5 h-5 text-amber-500" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs font-bold text-amber-800 leading-tight">Battery inventory warnings at Gulshan station</p>
|
|
<p className="text-[10px] text-amber-500 mt-0.5">Station has only 1 fully charged cabinet remaining. Restock is required immediately.</p>
|
|
</div>
|
|
<Link
|
|
href="/admin/swap-stations"
|
|
className="text-[9px] font-extrabold uppercase text-amber-700 bg-amber-100 hover:bg-amber-200 px-2 py-1 rounded transition-colors shrink-0 font-bold"
|
|
>
|
|
Dispatch Hub
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Global Simulator modal */}
|
|
{showToolsDrawer && (
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden border border-slate-100 flex flex-col">
|
|
<div className="p-4 bg-slate-900 text-white flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Cpu className="w-5 h-5 text-amber-400" />
|
|
<h3 className="font-extrabold text-sm uppercase tracking-wider">Run Environment Simulation</h3>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowToolsDrawer(false)}
|
|
className="p-1 hover:bg-white/10 rounded-lg transition-colors text-white"
|
|
>
|
|
<XCircle className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-5 space-y-4">
|
|
<p className="text-xs text-slate-500 leading-relaxed">Select global environment load override conditions.</p>
|
|
|
|
<div className="space-y-2">
|
|
<button
|
|
onClick={() => runSimulatorTool('rain')}
|
|
className="w-full flex items-center justify-between p-3 border border-slate-200 rounded-xl hover:border-amber-300 hover:bg-amber-50/20 text-left transition-all group cursor-pointer"
|
|
>
|
|
<div>
|
|
<h4 className="text-xs font-bold text-slate-800">Monsoon Heavy Rain load</h4>
|
|
<p className="text-[10px] text-slate-400 mt-0.5">Speed restricted to 30km/h.</p>
|
|
</div>
|
|
<ChevronRight className="w-4 h-4 text-slate-400 group-hover:translate-x-1 transition-transform" />
|
|
</button>
|
|
<button
|
|
onClick={() => runSimulatorTool('swaps')}
|
|
className="w-full flex items-center justify-between p-3 border border-slate-200 rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left transition-all group cursor-pointer"
|
|
>
|
|
<div>
|
|
<h4 className="text-xs font-bold text-slate-800">Peak Grid load</h4>
|
|
<p className="text-[10px] text-slate-400 mt-0.5"> cabinet reserves restricted.</p>
|
|
</div>
|
|
<ChevronRight className="w-4 h-4 text-slate-400 group-hover:translate-x-1 transition-transform" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 bg-slate-50 border-t border-slate-100 flex justify-end">
|
|
<button
|
|
onClick={() => setShowToolsDrawer(false)}
|
|
className="py-1.5 px-4 bg-white border border-slate-200 text-slate-600 rounded-lg text-xs font-bold hover:bg-slate-50 transition-colors"
|
|
>
|
|
Cancel / Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |