feat: enhance admin dashboard with real-time telematics, audit logs, and interactive management tools
This commit is contained in:
@@ -1,114 +1,623 @@
|
|||||||
import {
|
'use client';
|
||||||
Users,
|
|
||||||
Bike,
|
|
||||||
DollarSign,
|
|
||||||
TrendingUp,
|
|
||||||
Activity,
|
|
||||||
ArrowUpRight,
|
|
||||||
ArrowDownRight,
|
|
||||||
Clock,
|
|
||||||
Shield,
|
|
||||||
AlertTriangle,
|
|
||||||
CheckCircle,
|
|
||||||
XCircle
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { kycRequests, bikes, rentals, transactions } from '@/data/mockData';
|
|
||||||
|
|
||||||
const stats = [
|
import { useState, useEffect, useRef } from 'react';
|
||||||
{ label: 'Total Bikers', value: '156', change: '+12%', trend: 'up', icon: Users, color: 'text-blue-600', bg: 'bg-blue-50' },
|
import {
|
||||||
{ label: 'Active Rentals', value: '89', change: '+8%', trend: 'up', icon: Bike, color: 'text-green-600', bg: 'bg-green-50' },
|
Users, Bike, DollarSign, TrendingUp, Activity, ArrowUpRight, ArrowDownRight,
|
||||||
{ label: 'Daily Revenue', value: '৳45.6k', change: '+23%', trend: 'up', icon: DollarSign, color: 'text-purple-600', bg: 'bg-purple-50' },
|
Clock, Shield, AlertTriangle, CheckCircle, XCircle, Play, Sparkles, RefreshCw,
|
||||||
{ label: 'Fleet Utilization', value: '78%', change: '-2%', trend: 'down', icon: TrendingUp, color: 'text-amber-600', bg: 'bg-amber-50' },
|
Search, Lock, Unlock, Volume2, ShieldAlert, Cpu, Terminal, Database, Settings, HelpCircle, ChevronRight
|
||||||
|
} 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 pendingKYCs = kycRequests.filter(k => k.status === 'pending');
|
const mockLiveMessages = [
|
||||||
const activeRentals = rentals.filter(r => r.status === 'active');
|
{ 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() {
|
export default function AdminDashboard() {
|
||||||
|
// Live State
|
||||||
|
const [kycRequests, setKycRequests] = useState(mockKycRequests);
|
||||||
|
const [bikesList, setBikesList] = useState(mockBikes);
|
||||||
|
const [telematicsBikes, setTelematicsBikes] = useState([
|
||||||
|
{ id: 'tel-1', model: 'Etron ET50', plateNumber: 'Dhaka Metro Cha-1234', batteryLevel: 78, location: 'Gulshan 1', status: 'rented' }, // secure locked
|
||||||
|
{ id: 'tel-2', model: 'Yadea DT3', plateNumber: 'Dhaka Metro Cha-5678', batteryLevel: 95, location: 'Banani', status: 'available' }, // active unlocked
|
||||||
|
{ id: 'tel-3', model: 'AIMA Lightning', plateNumber: 'Dhaka Metro Cha-9012', batteryLevel: 62, location: 'Uttara', status: 'rented' }, // secure locked
|
||||||
|
{ id: 'tel-4', model: 'TVS iQube', plateNumber: 'Dhaka Metro Cha-3456', batteryLevel: 45, location: 'Workshop', status: 'available' } // active unlocked
|
||||||
|
]);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Real-time ticking time counter
|
||||||
|
const [systemTime, setSystemTime] = useState('');
|
||||||
|
|
||||||
|
// Controls drawer
|
||||||
|
const [showToolsDrawer, setShowToolsDrawer] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Clock update
|
||||||
|
const updateTime = () => {
|
||||||
|
const now = new Date();
|
||||||
|
setSystemTime(now.toLocaleTimeString());
|
||||||
|
};
|
||||||
|
updateTime();
|
||||||
|
const clockInterval = setInterval(updateTime, 1000);
|
||||||
|
|
||||||
|
// Live WebSocket simulator for Activity Log
|
||||||
|
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)]);
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(clockInterval);
|
||||||
|
if (liveLogInterval) clearInterval(liveLogInterval);
|
||||||
|
};
|
||||||
|
}, [liveStreamActive]);
|
||||||
|
|
||||||
|
// KYC Quick 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 set to Biker!`);
|
||||||
|
|
||||||
|
// Add to audit logs
|
||||||
|
const now = new Date().toLocaleTimeString();
|
||||||
|
setAuditLogs(prev => [
|
||||||
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'KYC', level: 'success', message: `Super Admin approved KYC document verification for ${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: `Super Admin rejected KYC files for ${name} (模糊不清 note)` },
|
||||||
|
...prev
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fleet Actions
|
||||||
|
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 beacon triggered remotely on ${plateNumber}!`, { icon: '🔊' });
|
||||||
|
const now = new Date().toLocaleTimeString();
|
||||||
|
setAuditLogs(prev => [
|
||||||
|
{ id: `audit-${Date.now()}`, timestamp: now, source: 'FLEET', level: 'warning', message: `Super Admin triggered anti-theft audio warning on vehicle ${plateNumber}` },
|
||||||
|
...prev
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulator Utilities
|
||||||
|
const runSimulatorTool = (type: string) => {
|
||||||
|
setShowToolsDrawer(false);
|
||||||
|
toast.loading(`Applying global load simulation...`, { duration: 1200 });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const now = new Date().toLocaleTimeString();
|
||||||
|
switch (type) {
|
||||||
|
case 'rain':
|
||||||
|
toast.success('Simulation active: heavy monsoon load rules applied! Fleet limits restricted to 30km/h.');
|
||||||
|
setAuditLogs(prev => [
|
||||||
|
{ id: `sim-${Date.now()}`, timestamp: now, source: 'FLEET', level: 'warning', message: 'Global Load Rules Override: Monsoon active. Battery thermal limit set to 45°C.' },
|
||||||
|
...prev
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
case 'swaps':
|
||||||
|
toast.success('Simulation: Toggled peak hour swap station fast loads. Grid stress active.');
|
||||||
|
setAuditLogs(prev => [
|
||||||
|
{ id: `sim-${Date.now()}`, timestamp: now, source: 'SWAP', level: 'info', message: 'Swap station rules override: High stress swap active. Buffer reserve locked at 15%.' },
|
||||||
|
...prev
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
case 'backup':
|
||||||
|
toast.success('System audit databases successfully compressed and pushed to backup secure drive.');
|
||||||
|
setAuditLogs(prev => [
|
||||||
|
{ id: `sim-${Date.now()}`, timestamp: now, source: 'ACCOUNTING', level: 'success', message: 'Secure system database checkpoint recorded. Uptime: 99.98%' },
|
||||||
|
...prev
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculations for dynamic stats
|
||||||
|
const pendingKYCList = kycRequests.filter(k => k.status === 'pending');
|
||||||
|
const activeRentalsCount = rentalsList.filter(r => r.status === 'active').length;
|
||||||
|
const totalBikersCount = mockUsersListCount();
|
||||||
|
|
||||||
|
function mockUsersListCount() {
|
||||||
|
return 156 + (kycRequests.filter(k => k.status === 'approved').length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtered lists for view
|
||||||
|
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 (
|
return (
|
||||||
<div className="p-4 lg:p-6">
|
<div className="p-4 lg:p-6 max-w-8xl mx-auto space-y-6">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
|
||||||
<div>
|
{/* 👑 SUPER ADMIN SECURITY HEADER BANNER */}
|
||||||
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Admin Dashboard</h1>
|
<div className="bg-white text-slate-800 rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden">
|
||||||
<p className="text-sm text-slate-500 mt-1">Manage fleet, users, and operations</p>
|
<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>
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 relative z-10">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm text-slate-500">Last updated:</span>
|
<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">
|
||||||
<span className="text-sm font-medium text-slate-700">Just now</span>
|
<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>
|
||||||
|
|
||||||
|
{/* System Telemetry Metadata */}
|
||||||
|
<div className="flex items-center gap-4 text-xs font-semibold self-start lg:self-center bg-slate-50 p-3 rounded-xl border border-slate-200">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className="text-slate-400 block uppercase text-[9px]">Telemetry Time</span>
|
||||||
|
<span className="text-accent font-mono tracking-wider flex items-center gap-1.5 font-bold">
|
||||||
|
<Clock className="w-3.5 h-3.5" /> {systemTime || 'Loading...'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="border-l border-slate-200 pl-4 space-y-1">
|
||||||
|
<span className="text-slate-400 block uppercase text-[9px]">Server status</span>
|
||||||
|
<span className="text-accent flex items-center gap-1 font-bold">
|
||||||
|
<Cpu className="w-3.5 h-3.5" /> ONLINE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { setLiveStreamActive(!liveStreamActive); toast.success(liveStreamActive ? 'Live Stream paused' : 'Live stream active'); }}
|
||||||
|
className="border-l border-slate-200 pl-4 text-slate-450 hover:text-slate-750 transition-colors cursor-pointer"
|
||||||
|
title="Toggle Live Stream Feed"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${liveStreamActive ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
{/* COMMAND CENTER OPERATIONS STATISTICS */}
|
||||||
{stats.map((stat, index) => {
|
<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;
|
const Icon = stat.icon;
|
||||||
return (
|
return (
|
||||||
<div key={index} className="bg-white rounded-xl p-5 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
<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 items-start justify-between mb-3">
|
<div className="flex justify-between items-start">
|
||||||
<div className={`w-12 h-12 rounded-xl ${stat.bg} flex items-center justify-center`}>
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center border ${stat.color}`}>
|
||||||
<Icon className={`w-6 h-6 ${stat.color}`} />
|
<Icon className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-xs font-semibold px-2 py-1 rounded-full flex items-center gap-1 ${
|
<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' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
}`}>
|
||||||
}`}>
|
|
||||||
{stat.trend === 'up' ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
|
{stat.trend === 'up' ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
|
||||||
{stat.change}
|
{stat.change}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-extrabold text-slate-800">{stat.value}</p>
|
<div className="mt-4">
|
||||||
<p className="text-sm text-slate-500 mt-1">{stat.label}</p>
|
<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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-3 gap-6 mb-8">
|
{/* CORE TELEMETRY ANALYTICS: CUSTOM INTERACTIVE VECTOR SVG CHARTS */}
|
||||||
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
<div className="px-5 py-4 border-b border-slate-100 flex items-center justify-between">
|
|
||||||
<h2 className="font-bold text-slate-800">Recent Rentals</h2>
|
{/* SVG High-Fidelity Chart */}
|
||||||
<button className="text-sm text-blue-600 font-medium hover:underline">View All</button>
|
<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-500 hover:text-slate-850'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
{/* Hand-Crafted Interactive Vector Graph */}
|
||||||
<thead className="bg-slate-50">
|
<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>
|
||||||
|
{/* Grid Lines */}
|
||||||
|
<line x1="0" y1="50" x2="500" y2="50" stroke="#1e293b" strokeDasharray="3" strokeWidth="0.5" />
|
||||||
|
<line x1="0" y1="100" x2="500" y2="100" stroke="#1e293b" strokeDasharray="3" strokeWidth="0.5" />
|
||||||
|
<line x1="0" y1="150" x2="500" y2="150" stroke="#1e293b" strokeDasharray="3" strokeWidth="0.5" />
|
||||||
|
|
||||||
|
{/* Projections dashed */}
|
||||||
|
<path d="M 0,160 Q 120,130 250,90 T 500,40" fill="none" stroke="#475569" strokeWidth="2" strokeDasharray="4" />
|
||||||
|
|
||||||
|
{/* Active curve gradient */}
|
||||||
|
<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)" />
|
||||||
|
|
||||||
|
{/* Active curve line */}
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
{/* Interactive Points */}
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
{/* Custom Tooltip Box */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* X Axis Labels */}
|
||||||
|
<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="#1e293b" strokeDasharray="3" strokeWidth="0.5" />
|
||||||
|
<line x1="0" y1="100" x2="500" y2="100" stroke="#1e293b" strokeDasharray="3" strokeWidth="0.5" />
|
||||||
|
<line x1="0" y1="150" x2="500" y2="150" stroke="#1e293b" strokeDasharray="3" strokeWidth="0.5" />
|
||||||
|
|
||||||
|
<path d="M 0,140 Q 150,140 250,70 T 500,60" fill="none" stroke="#475569" 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="#1e293b" strokeDasharray="3" strokeWidth="0.5" />
|
||||||
|
<line x1="0" y1="100" x2="500" y2="100" stroke="#1e293b" strokeDasharray="3" strokeWidth="0.5" />
|
||||||
|
<line x1="0" y1="150" x2="500" y2="150" stroke="#1e293b" strokeDasharray="3" strokeWidth="0.5" />
|
||||||
|
|
||||||
|
<path d="M 0,160 Q 150,110 250,100 T 500,80" fill="none" stroke="#475569" 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 Audit Audit & Log terminal Stream */}
|
||||||
|
<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 stream synchronized</span>
|
||||||
|
<button
|
||||||
|
onClick={() => { setAuditLogs(initialAuditLogs); toast.success('Telemetry console logs reset'); }}
|
||||||
|
className="text-[10px] font-bold text-accent hover:underline cursor-pointer"
|
||||||
|
>
|
||||||
|
Reset Logs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* DUAL WORKSPACE: INTERACTIVE KYC DESK & HARDWARE FLEET TELEMETRY */}
|
||||||
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
{/* Interactive KYC Pending Table / List */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Search inputs */}
|
||||||
|
<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-650 rounded text-[9px] font-semibold">TIN Files Verified</span>
|
||||||
|
<span className="px-2 py-0.5 bg-slate-100 text-slate-650 rounded text-[9px] font-semibold">License Valid</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 match your filter.</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>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rental ID</th>
|
<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 text-xs font-semibold text-slate-500 uppercase tracking-wider">Bike</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 text-xs font-semibold text-slate-500 uppercase tracking-wider">User</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 text-xs font-semibold text-slate-500 uppercase tracking-wider">Type</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-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
|
<th className="px-4 py-3 text-right font-bold text-slate-500 uppercase tracking-wider">Siren Warning</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Daily Rate</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-50">
|
<tbody className="divide-y divide-slate-50">
|
||||||
{activeRentals.map(rental => {
|
{filteredFleet.map(bike => {
|
||||||
const bike = bikes.find(b => b.id === rental.bikeId);
|
const isRented = bike.status === 'rented';
|
||||||
return (
|
return (
|
||||||
<tr key={rental.id} className="hover:bg-slate-50 transition-colors">
|
<tr key={bike.id} className="hover:bg-slate-50/40 transition-colors">
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className="text-sm font-medium text-slate-700">{rental.id}</span>
|
<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>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className="text-sm text-slate-600">{bike?.model}</span>
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-xs text-slate-400 block">{bike?.plateNumber}</span>
|
<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>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className="text-sm text-slate-600">{rental.userId}</span>
|
<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>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3 text-right">
|
||||||
<span className="text-sm text-slate-600 capitalize">{rental.type}</span>
|
<button
|
||||||
</td>
|
onClick={() => handleTriggerSiren(bike.plateNumber)}
|
||||||
<td className="px-4 py-3">
|
className="p-1.5 hover:bg-red-50 rounded-lg text-slate-400 hover:text-red-600 transition-colors cursor-pointer"
|
||||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${
|
title="Trigger Anti-theft Audio Warning Beacon"
|
||||||
rental.status === 'active' ? 'bg-green-100 text-green-700' :
|
>
|
||||||
rental.status === 'pending' ? 'bg-amber-100 text-amber-700' :
|
<Volume2 className="w-4 h-4" />
|
||||||
'bg-slate-100 text-slate-500'
|
</button>
|
||||||
}`}>
|
|
||||||
<Activity className="w-3 h-3" />
|
|
||||||
{rental.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<span className="text-sm font-semibold text-slate-700">৳{rental.dailyRate}</span>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -116,96 +625,187 @@ export default function AdminDashboard() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
|
{/* Mobile Card Layout */}
|
||||||
<div className="px-5 py-4 border-b border-slate-100 flex items-center justify-between">
|
<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">
|
||||||
<h2 className="font-bold text-slate-800">KYC Requests</h2>
|
{filteredFleet.map(bike => {
|
||||||
<span className="text-xs font-semibold px-2 py-1 bg-amber-100 text-amber-700 rounded-full">
|
const isRented = bike.status === 'rented';
|
||||||
{pendingKYCs.length} pending
|
return (
|
||||||
</span>
|
<div key={bike.id} className="bg-white p-4 rounded-xl border border-slate-100 shadow-sm space-y-3">
|
||||||
</div>
|
<div className="flex justify-between items-start">
|
||||||
<div className="divide-y divide-slate-50">
|
<div>
|
||||||
{pendingKYCs.map(kyc => (
|
<h4 className="font-extrabold text-slate-800 text-sm">{bike.model}</h4>
|
||||||
<div key={kyc.id} className="p-4 hover:bg-slate-50 transition-colors">
|
<p className="text-[10px] text-slate-400 font-mono mt-0.5">{bike.plateNumber}</p>
|
||||||
<div className="flex items-start justify-between mb-3">
|
</div>
|
||||||
<div>
|
<div className="flex items-center gap-1 bg-slate-50 border border-slate-100 rounded-lg px-2 py-0.5">
|
||||||
<p className="font-medium text-slate-700">{kyc.userName}</p>
|
<span className={`w-2 h-2 rounded-full ${bike.batteryLevel > 70 ? 'bg-green-500' : bike.batteryLevel > 30 ? 'bg-amber-500' : 'bg-red-500'
|
||||||
<p className="text-sm text-slate-400">{kyc.phone}</p>
|
}`} />
|
||||||
<p className="text-xs text-slate-400 mt-1 flex items-center gap-1">
|
<span className="font-mono text-slate-700 font-bold text-[10px]">{bike.batteryLevel}%</span>
|
||||||
<Clock className="w-3 h-3" /> {kyc.submittedAt}
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="p-1.5 bg-amber-100 rounded-lg">
|
<div className="flex justify-between items-center text-xs font-bold text-slate-500">
|
||||||
<Shield className="w-4 h-4 text-amber-600" />
|
<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>
|
||||||
|
|
||||||
|
{/* QUICK OPERATIONS CONSOLE & CENTRAL UTILITIES */}
|
||||||
|
<div className="grid lg:grid-cols-2 gap-6">
|
||||||
|
|
||||||
|
{/* Super Admin Quick tools */}
|
||||||
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
</button>
|
||||||
<button className="flex-1 py-2 bg-green-600 text-white text-sm font-semibold rounded-lg hover:bg-green-700 flex items-center justify-center gap-1">
|
|
||||||
<CheckCircle className="w-4 h-4" /> Approve
|
|
||||||
</button>
|
|
||||||
<button className="flex-1 py-2 border border-red-200 text-red-600 text-sm font-semibold rounded-lg hover:bg-red-50 flex items-center justify-center gap-1">
|
|
||||||
<XCircle className="w-4 h-4" /> Reject
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
{pendingKYCs.length === 0 && (
|
|
||||||
<div className="p-8 text-center">
|
|
||||||
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-2" />
|
|
||||||
<p className="text-sm text-slate-500">No pending KYC requests</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 gap-6">
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
|
|
||||||
<h2 className="font-bold text-slate-800 mb-4">Quick Actions</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<button className="py-3 px-4 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark transition-colors">
|
|
||||||
+ Add New Bike
|
|
||||||
</button>
|
|
||||||
<button className="py-3 px-4 bg-blue-600 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 transition-colors">
|
|
||||||
Manage Users
|
|
||||||
</button>
|
|
||||||
<button className="py-3 px-4 bg-purple-600 text-white rounded-lg font-semibold text-sm hover:bg-purple-700 transition-colors">
|
|
||||||
View Reports
|
|
||||||
</button>
|
|
||||||
<button className="py-3 px-4 border border-slate-200 text-slate-600 rounded-lg font-semibold text-sm hover:bg-slate-50 transition-colors">
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
|
{/* Global Security Alerts Panel */}
|
||||||
<h2 className="font-bold text-slate-800 mb-4">System Alerts</h2>
|
<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 & Maintenance Telemetry</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="space-y-3">
|
||||||
<div className="flex items-center gap-3 p-3 bg-red-50 rounded-lg border border-red-100">
|
<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" />
|
<AlertTriangle className="w-5 h-5 text-red-500 animate-pulse" />
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm font-medium text-red-700">3 bikes overdue maintenance</p>
|
<p className="text-xs font-bold text-red-800 leading-tight">Monsoon Load Warning active</p>
|
||||||
<p className="text-xs text-red-500">Action required</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>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => toast.success('Override monsoons restrictions: Secure sign-off 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>
|
||||||
<div className="flex items-center gap-3 p-3 bg-amber-50 rounded-lg border border-amber-100">
|
|
||||||
|
<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" />
|
<Clock className="w-5 h-5 text-amber-500" />
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm font-medium text-amber-700">5 pending KYC verifications</p>
|
<p className="text-xs font-bold text-amber-800 leading-tight">Battery inventory warnings at Gulshan station</p>
|
||||||
<p className="text-xs text-amber-500">Review needed</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>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg border border-green-100">
|
|
||||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-green-700">System running smoothly</p>
|
|
||||||
<p className="text-xs text-green-500">All services operational</p>
|
|
||||||
</div>
|
</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"
|
||||||
|
>
|
||||||
|
Dispatch Hub
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ========================================= MONSOON / GLOBAL OVERRIDE SIMULATOR PANEL DRAWER ========================================= */}
|
||||||
|
{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 animate-scaleIn">
|
||||||
|
<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. These parameters immediately scale system-wide biker limitations, accounting calculations, and thermal thresholds.</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, thermal thresholds forced.</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 Hour Swaps load</h4>
|
||||||
|
<p className="text-[10px] text-slate-400 mt-0.5"> cabinet reserves restricted, grid loading warnings active.</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user