// CeyRide — Admin Portal v3 (Mobile-fixed, Image Manager, all modules) // Site image slot schema — defines the manageable image positions in the frontend. // 'src' values are fallbacks only; actual paths are loaded from get_homepage_config on login. const UI_IMAGE_SCHEMA = [ { id:1, section:'Homepage', key:'hero_bg', label:'Hero Background', src:'', used:'Hero section background' }, // Featured Vehicle slots removed — featured vehicles are managed // via CMS → Vehicles → star toggle (vehicles.is_featured_home) // NOT via static image upload. See cms_vehicles.php action=feature. { id:2, section:'Services', key:'vehicle_rental_img',label:'Vehicle Rental', src:'', used:'Services — Vehicle Rental card' }, { id:3, section:'Services', key:'hire_driver_img', label:'Hire a Driver', src:'', used:'Services — Hire Driver card' }, { id:4, section:'Services', key:'airport_img', label:'Airport Transfers', src:'', used:'Services — Airport Transfer card' }, { id:5, section:'Services', key:'tour_img', label:'Tour Packages', src:'', used:'Services — Tour Packages card' }, { id:6, section:'Services', key:'corporate_img', label:'Corporate Fleet', src:'', used:'Services — Corporate Fleet card' }, { id:7, section:'About', key:'about_main', label:'About Main Image', src:'', used:'About Us — main banner' }, { id:8, section:'Vehicles', key:'vehicles_banner', label:'Vehicles Page Banner',src:'', used:'Vehicles page header' }, { id:9, section:'Auth', key:'auth_car', label:'Login Page Car', src:'', used:'Auth page side panel' }, ]; // ── CMS Image URL resolver ──────────────────────────────────────────── // The CMS is served from /cms-dev/ but all media files live at the // staging root (/images/vehicles/, /images/ui/, etc.). // A relative path like 'images/vehicles/x.jpg' rendered inside /cms-dev/ // resolves to /cms-dev/images/vehicles/x.jpg → 404. // resolveImgUrl() prepends the staging origin so every gets the // correct absolute URL regardless of which sub-path the CMS is served from. // // CEYRIDE_API_BASE = 'https://devstaging.ceyride.com/api/' // CMS_MEDIA_BASE = 'https://devstaging.ceyride.com/' const CMS_MEDIA_BASE = (function() { const base = window.CEYRIDE_API_BASE || ''; // Strip trailing 'api/' or 'api' to get the site root return base.replace(/api\/?$/, ''); })(); function resolveImgUrl(path) { if (!path || typeof path !== 'string' || path.trim() === '') return ''; const p = path.trim(); // Already absolute — return as-is if (/^https?:\/\//.test(p) || p.startsWith('//') || p.startsWith('/')) return p; // Relative path — prefix with staging root return CMS_MEDIA_BASE + p; } // ─── SHARED HELPERS ────────────────────────────────────────── const SC = { pending: { bg:'#FEF3C7', color:'#92400E', label:'Pending' }, approved: { bg:'#D1FAE5', color:'#065F46', label:'Approved' }, rejected: { bg:'#FEE2E2', color:'#991B1B', label:'Rejected' }, confirmed: { bg:'#D1FAE5', color:'#065F46', label:'Confirmed' }, cancelled: { bg:'#FEE2E2', color:'#991B1B', label:'Cancelled' }, active: { bg:'#D1FAE5', color:'#065F46', label:'Active' }, under_review: { bg:'#EFF6FF', color:'#1D4ED8', label:'Under Review' }, completed: { bg:'#F0FDF4', color:'#166534', label:'Completed' }, blocked: { bg:'#FEE2E2', color:'#991B1B', label:'Blocked' }, }; function CmsBadge({ status }) { const s = SC[status] || { bg:'#F3F4F6', color:'#374151', label: status }; return {s.label}; } function Stat({ icon, label, value, sub, color='#7C3AED', onClick }) { return (
{icon}
{value}
{label}
{sub &&
{sub}
}
); } function Bar({ data, labels, color='#7C3AED', h=80 }) { if (!data || data.length === 0) return (
No data yet
); const mx = Math.max(...data) || 1; return (
{data.map((v,i) => (
{labels[i]}
))}
); } const INP = { padding:'10px 13px', borderRadius:10, border:'1.5px solid rgba(20,20,19,0.12)', fontSize:14, fontFamily:'inherit', outline:'none', background:'#fff', color:'#141413', width:'100%', boxSizing:'border-box' }; const BTN = { border:'none', borderRadius:10, padding:'8px 14px', fontWeight:700, fontSize:13, cursor:'pointer', fontFamily:'inherit', whiteSpace:'nowrap' }; function Modal({ title, onClose, children, wide }) { React.useEffect(() => { document.body.style.overflow = 'hidden'; const h = e => { if (e.key==='Escape') onClose(); }; window.addEventListener('keydown', h); return () => { document.body.style.overflow = ''; window.removeEventListener('keydown', h); }; }, []); return (
e.stopPropagation()} style={{ background:'#fff', borderRadius:20, width:'100%', maxWidth:wide?700:480, maxHeight:'92vh', display:'flex', flexDirection:'column', boxShadow:'0 20px 60px rgba(20,20,19,0.2)' }}>
{title}
{children}
); } // ─── ADMIN LOGIN ───────────────────────────────────────────── function AdminLogin({ onLogin }) { const [email, setEmail] = React.useState(''); const [pw, setPw] = React.useState(''); const [err, setErr] = React.useState(''); const [loading, setLoading] = React.useState(false); const [showPw, setShowPw] = React.useState(false); const go = async () => { setErr(''); if (!email || !pw) { setErr('Both fields required.'); return; } setLoading(true); try { const res = await fetch('api/admin.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'admin_login', email: email.trim().toLowerCase(), password: pw }), }); const json = await res.json(); if (!res.ok || json.error) { setLoading(false); setErr(json.error || 'Invalid credentials. Access denied.'); return; } const s = { id: json.admin.id, name: json.admin.name, email: json.admin.email, role: json.admin.role, token: json.admin.token, // server-issued JWT — required for all admin API calls loginTime: Date.now(), }; localStorage.setItem('cr_admin', JSON.stringify(s)); onLogin(s); } catch { setLoading(false); setErr('Network error. Could not reach the server — check your connection.'); } }; return (
Cey
R
ide
ADMIN PORTAL

Admin Login

Secure access for authorised personnel only.

{err &&
⚠️ {err}
}
setEmail(e.target.value)} onKeyDown={e=>e.key==='Enter'&&go()} />
setPw(e.target.value)} onKeyDown={e=>e.key==='Enter'&&go()} />
Authorised personnel only. Contact the system administrator if you have lost access.
); } // ─── SVG ICON SYSTEM — replaces all emoji ──────────────────── function SvgIcon({ children, size=18 }) { return ( {children} ); } const ICONS = { dashboard: , car: , users: , compass: , package: , calendar: , route: , wallet: , image: , fileText: , shield: , bell: , logout: , externalLink:, barChart: , }; // ─── SIDEBAR ───────────────────────────────────────────────── const NAVS = [ { key:'dashboard', label:'Dashboard', icon:ICONS.dashboard }, { key:'vehicles', label:'Vehicles', icon:ICONS.car, badge:'veh' }, { key:'drivers', label:'Drivers', icon:ICONS.users, badge:'drv' }, { key:'guides', label:'Tour Guides', icon:ICONS.compass, badge:'gde' }, { key:'packages', label:'Packages', icon:ICONS.package, badge:'pkg' }, { key:'bookings', label:'Bookings', icon:ICONS.calendar, badge:'bk' }, { key:'driver-bookings', label:'Driver Bookings', icon:ICONS.route, badge:'dbk' }, { key:'finance', label:'Finance', icon:ICONS.wallet }, { key:'users', label:'Users', icon:ICONS.users }, { key:'images', label:'UI Images', icon:ICONS.image }, { key:'content', label:'Content', icon:ICONS.fileText }, { key:'auditlog', label:'Audit Log', icon:ICONS.shield }, { key:'notifs', label:'Notifications', icon:ICONS.bell, badge:'ntf' }, ]; function Sidebar({ active, setActive, onLogout, open, setOpen, badges }) { return ( <> {open &&
setOpen(false)} style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.45)', zIndex:199 }} />} ); } function TopBar({ admin, mod, onMenu, notifCount, setMod, onSync, syncing, onSyncCheck }) { const label = NAVS.find(n=>n.key===mod); return (
{label?.icon} {label?.label || 'Admin'}
{/* ── Save & Sync button (restored) ── */} {/* ── Sync Check button ── */}
setMod('notifs')} style={{ position:'relative', cursor:'pointer', fontSize:20 }}> 🔔 {notifCount>0 && {notifCount}}
{(admin?.name||'A')[0]}
{admin?.name}
{admin?.role}
); } // ─── DASHBOARD ─────────────────────────────────────────────── function Dashboard({ data, setMod }) { const pv = (data.registrations||[]).filter(r=>r.status==='pending').length; const pd = data.drivers.filter(d=>d.status==='pending').length; const pg = data.guides.filter(g=>g.status==='pending').length; const pp = data.packages.filter(p=>p.status==='pending').length; const pb = data.bookings.filter(b=>b.status==='pending').length; const nr = data.notifications.filter(n=>!n.read).length; const totalRev = data.bookings.filter(b=>b.status==='confirmed').reduce((s,b)=>s+(Number(b.total)||0),0); return (

Platform Overview

{new Date().toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'})}

setMod('vehicles')} /> setMod('drivers')} /> setMod('guides')} /> setMod('bookings')} /> setMod('finance')} /> setMod('notifs')} />
Weekly Revenue (LKR)
This week: Rs. {data.revenue.daily.reduce((a,b)=>a+b,0).toLocaleString()} setMod('finance')} style={{ color:'#E84800', cursor:'pointer', fontWeight:600 }}>View →
Recent Activity
{data.notifications.slice(0,5).map((n,i) => (
{n.icon}
{n.title}
{n.time}
{!n.read &&
}
))}
Pending Reviews
{[{label:'Vehicles', count:pv, color:'#E84800'},{label:'Drivers', count:pd, color:'#0066CC'},{label:'Guides', count:pg, color:'#7C3AED'},{label:'Packages', count:pp, color:'#D97706'}].map((r,i) => (
{r.count}
{r.label} Pending
))}
); } // ─── CMS SHARED HELPERS ────────────────────────────────────── const FL = { width:'100%', padding:'10px 13px', borderRadius:10, border:'1.5px solid rgba(20,20,19,0.12)', fontSize:14, fontFamily:'inherit', outline:'none', background:'#fff', color:'#141413', boxSizing:'border-box' }; const LB = { fontSize:11, fontWeight:700, color:'#696969', textTransform:'uppercase', letterSpacing:'0.06em', display:'block', marginBottom:6 }; function FieldRow({ label, children }) { return (
{children}
); } function ImageUploadBtn({ onUpload, current, label='Upload Image' }) { const [prev, setPrev] = React.useState(current||null); const [uploading, setUploading] = React.useState(false); const pick = () => { const el = document.createElement('input'); el.type='file'; el.accept='image/jpeg,image/png,image/webp'; el.onchange = async e => { const file = e.target.files[0]; if(!file) return; if(file.size > 5*1024*1024) { alert('Max 5MB'); return; } setUploading(true); const url = URL.createObjectURL(file); setPrev(url); // Try PHP upload try { const fd = new FormData(); fd.append('image',file); fd.append('image_key','cms_'+Date.now()); const res = await fetch('api/upload_image.php',{method:'POST',body:fd}); const json = await res.json(); if(json.success) { onUpload(json.cache_url||json.path); } else { onUpload(`images/vehicles/${file.name}?t=${Date.now()}`); } } catch { onUpload(`images/vehicles/${file.name}?t=${Date.now()}`); } setUploading(false); }; el.click(); }; return (
{prev ? previewe.target.style.display='none'} /> :
🖼️
}
{uploading?'Uploading…':label}
JPG / PNG / WebP · Max 5MB
); } function CmsSearchBar({ value, onChange, placeholder='Search…' }) { return onChange(e.target.value)} />; } function StatusToggle({ active, onChange }) { return (
onChange(!active)} style={{ display:'inline-flex', alignItems:'center', gap:8, cursor:'pointer', padding:'4px 10px', borderRadius:20, background:active?'#D1FAE5':'#FEE2E2', border:`1px solid ${active?'#6EE7B7':'#FECACA'}` }}>
{active?'Active':'Inactive'}
); } function logAction(logs, setLogs, action, module, detail) { const entry = { id:Date.now(), time:new Date().toLocaleTimeString('en-GB'), action, module, detail, by:'Admin' }; setLogs(prev => [entry, ...prev.slice(0,49)]); } // ── Global admin auth header helper (used by all CMS components) ── function getAdminAuthHdr() { try { const s = JSON.parse(localStorage.getItem('cr_admin') || '{}'); return s.token ? { 'Authorization': `Bearer ${s.token}` } : {}; } catch { return {}; } } // ─── CMS: VEHICLES MANAGEMENT ──────────────────────────────── function VehiclesCMS({ data, setData, auditLog }) { const [search, setSearch] = React.useState(''); const [filter, setFilter] = React.useState('active'); const [modal, setModal] = React.useState(null); // null | 'add' | 'edit' const [delConf, setDelConf] = React.useState(null); const [form, setForm] = React.useState({}); const setF = (k,v) => setForm(f=>({...f,[k]:v})); const emptyForm = { type:'Car', brand:'', model:'', year:new Date().getFullYear(), seats:4, price_per_day:'', fuel_policy:'Full-to-Full', location:'Colombo', description:'', image_url:'', is_active:1, is_featured_home:0, featured_order:0, // Legacy flags — kept in sync with_driver:0, self_drive:1, // ── Clean driver system ─────────────────────────────── has_self_drive:1, self_drive_price:'', has_owner_driver:0, owner_driver_price:'', owner_driver_name:'', owner_driver_experience:0, owner_driver_photo:'', owner_driver_contact:'', owner_driver_languages:'', // Insurance insurance_type:'none', insurance_provider:'', insurance_policy_number:'', insurance_expiry:'', insurance_doc_path:'', }; const list = data.vehicles.filter(v => { const q = search.toLowerCase(); const sOk = filter==='all' || (filter==='active' && Number(v.is_active) === 1) || (filter==='inactive' && Number(v.is_active) === 0) || (filter==='pending' && v.status==='pending') || (filter==='featured' && Number(v.is_featured_home)===1) || (filter==='insured' && v.insurance_type && v.insurance_type !== 'none') || (filter==='unverified' && v.insurance_type && v.insurance_type !== 'none' && !v.insurance_verified); const tOk = !q || `${v.brand} ${v.model} ${v.type}`.toLowerCase().includes(q); return sOk && tOk; }); const openAdd = () => { setForm({...emptyForm}); setModal('add'); }; const openEdit = (v) => { setForm({...v}); setModal('edit'); }; const save = async () => { if(!form.brand||!form.model||!form.price_per_day) { alert('Brand, Model and Price are required.'); return; } try { const method = modal==='add' ? 'POST' : 'PUT'; const res = await fetch(`api/cms_vehicles.php${modal==='edit'?`?id=${form.id}`:''}`, { method, headers:{'Content-Type':'application/json',...getAdminAuthHdr()}, body:JSON.stringify(form) }); const json = await res.json(); if(json.id||json.success) { if(modal==='add') { setData(d=>({...d, vehicles:[{...form, id:json.id||Date.now(), status:'approved', rating:0, review_count:0}, ...d.vehicles]})); logAction(auditLog.logs, auditLog.setLogs, 'Created', 'Vehicle', `${form.brand} ${form.model}`); } else { setData(d=>({...d, vehicles:d.vehicles.map(v=>v.id===form.id?{...v,...form}:v)})); logAction(auditLog.logs, auditLog.setLogs, 'Updated', 'Vehicle', `${form.brand} ${form.model}`); } setModal(null); } } catch { // Offline/dev: update state directly if(modal==='add') { setData(d=>({...d, vehicles:[{...form, id:Date.now(), status:'approved', rating:0, review_count:0}, ...d.vehicles]})); } else { setData(d=>({...d, vehicles:d.vehicles.map(v=>v.id===form.id?{...v,...form}:v)})); } logAction(auditLog.logs, auditLog.setLogs, modal==='add'?'Created':'Updated', 'Vehicle', `${form.brand} ${form.model}`); setModal(null); } }; const del = async (id) => { try { const res = await fetch(`api/cms_vehicles.php?id=${id}`, { method: 'DELETE', headers: getAdminAuthHdr() }); const json = await res.json().catch(() => ({})); if (!res.ok || !json.success) { alert('Delete failed: ' + (json.error || 'Server error — vehicle was NOT removed. Please try again.')); setDelConf(null); return; } } catch { alert('Network error — vehicle was NOT deleted. Please check your connection and try again.'); setDelConf(null); return; } const v = data.vehicles.find(vv => vv.id === id); setData(d => ({ ...d, vehicles: d.vehicles.filter(vv => vv.id !== id) })); logAction(auditLog.logs, auditLog.setLogs, 'Deleted', 'Vehicle', v ? `${v.brand} ${v.model}` : `ID ${id}`); setDelConf(null); }; const toggleActive = async (id) => { const v = data.vehicles.find(vv => vv.id === id); if (!v) return; const newActive = v.is_active ? 0 : 1; // Optimistic update setData(d => ({ ...d, vehicles: d.vehicles.map(vv => vv.id === id ? { ...vv, is_active: newActive } : vv) })); try { const res = await fetch(`api/cms_vehicles.php?id=${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...getAdminAuthHdr() }, body: JSON.stringify({ ...v, is_active: newActive }) }); if (!res.ok) throw new Error('Server error'); } catch { // Revert optimistic update on failure setData(d => ({ ...d, vehicles: d.vehicles.map(vv => vv.id === id ? { ...vv, is_active: v.is_active } : vv) })); alert('Failed to update vehicle status. Please try again.'); return; } logAction(auditLog.logs, auditLog.setLogs, newActive ? 'Activated' : 'Deactivated', 'Vehicle', `${v.brand} ${v.model}`); }; // ── Part 15 Restore — Featured toggle ───────────────────────── const [featSaving, setFeatSaving] = React.useState(null); // vehicle id being toggled const toggleFeatured = async (id) => { const v = data.vehicles.find(vv=>vv.id===id); if (!v || featSaving === id) return; const newFeatured = Number(v.is_featured_home)===1 ? 0 : 1; // Auto-assign next order slot when featuring; 999 = unfeatured sentinel const nextOrder = newFeatured ? data.vehicles.filter(vv=>Number(vv.is_featured_home)===1).length + 1 : 999; // Optimistic update — immediately reflects intent in UI setData(d=>({...d, vehicles:d.vehicles.map(vv=>vv.id===id ? {...vv, is_featured_home:newFeatured, featured_order:nextOrder} : vv)})); setFeatSaving(id); try { const res = await fetch(`api/cms_vehicles.php?id=${id}&action=feature`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...getAdminAuthHdr() }, body: JSON.stringify({ is_featured_home: newFeatured }), }); const json = await res.json(); if (!res.ok || json.error) { // API failed — revert optimistic update so local state matches DB setData(d=>({...d, vehicles:d.vehicles.map(vv=>vv.id===id ? {...vv, is_featured_home:v.is_featured_home, featured_order:v.featured_order||0} : vv)})); console.error('[CeyRide CMS] toggleFeatured failed:', json.error || res.status); } else if (json.vehicle) { // Force integer cast — MySQLi can return "0" string which is TRUTHY in JS const synced = { ...json.vehicle, is_featured_home: Number(json.vehicle.is_featured_home), featured_order: Number(json.vehicle.featured_order), }; setData(d=>({...d, vehicles:d.vehicles.map(vv=>vv.id===id ? {...vv, ...synced} : vv)})); } } catch(e) { // Network error — revert optimistic update setData(d=>({...d, vehicles:d.vehicles.map(vv=>vv.id===id ? {...vv, is_featured_home:v.is_featured_home, featured_order:v.featured_order||0} : vv)})); console.error('[CeyRide CMS] toggleFeatured network error:', e.message); } setFeatSaving(null); logAction(auditLog.logs, auditLog.setLogs, newFeatured?'Featured':'Unfeatured', 'Vehicle', `${v.brand} ${v.model}`); }; const TYPES = ['Car','Van','SUV','Luxury','Mini Bus','Three-Wheeler','Jeep','Bus','Wedding Hire','Lorry / Truck']; const FUELS = ['Full-to-Full','Owner Covers','Customer Covers']; // ── Insurance verification toggle ────────────────────────── const [insSaving, setInsSaving] = React.useState(null); // vehicle id being verified const verifyInsurance = async (id, currentlyVerified) => { if (insSaving === id) return; const newVerified = currentlyVerified ? 0 : 1; // Optimistic update setData(d=>({...d, vehicles:d.vehicles.map(vv=>vv.id===id ? {...vv, insurance_verified:newVerified} : vv)})); setInsSaving(id); try { const res = await fetch(`api/cms_vehicles.php?id=${id}&action=verify_insurance`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...getAdminAuthHdr() }, body: JSON.stringify({ verified: newVerified }), }); const json = await res.json(); if (json.vehicle) { setData(d=>({...d, vehicles:d.vehicles.map(vv=>vv.id===id ? {...vv, ...json.vehicle} : vv)})); } } catch { // Revert on failure setData(d=>({...d, vehicles:d.vehicles.map(vv=>vv.id===id ? {...vv, insurance_verified:currentlyVerified} : vv)})); } setInsSaving(null); logAction(auditLog.logs, auditLog.setLogs, newVerified?'Verified Insurance':'Un-verified Insurance', 'Vehicle', `ID ${id}`); }; // Insurance type labels for display const INS_LABELS = { 'none': 'None', 'comprehensive': 'Comprehensive', 'third_party': 'Third-Party', 'hire_reward': 'Hire & Reward', 'self_drive_rental':'Self-Drive Rental', 'driver_included': 'Driver-Included', }; // ── Pending Registration: Approve ────────────────────────── const [approvingReg, setApprovingReg] = React.useState(null); const [regMsg, setRegMsg] = React.useState(''); const approveReg = async (reg) => { if (approvingReg) return; setApprovingReg(reg.id); setRegMsg(''); try { const res = await fetch(`api/owner_registrations.php?action=approve&id=${reg.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...getAdminAuthHdr() }, }); const json = await res.json(); if (json.success) { // Mark approved in local state setData(d => ({ ...d, registrations: (d.registrations||[]).map(r => r.id===reg.id ? {...r, status:'approved'} : r), })); // Reload vehicles table to pick up the newly inserted row try { const vRes = await fetch('api/cms_vehicles.php', { headers: getAdminAuthHdr() }); const vJson = await vRes.json(); if (vJson.vehicles) setData(d => ({...d, vehicles: vJson.vehicles})); } catch {} setRegMsg(`✅ Approved! ${reg.brand} ${reg.model} is now live in Vehicle Management.`); logAction(auditLog.logs, auditLog.setLogs, 'Approved', 'Registration', `${reg.brand} ${reg.model} (${reg.owner_name})`); } else { setRegMsg(`❌ Approval failed: ${json.error || 'Unknown error'}`); } } catch { setRegMsg('❌ Network error — please try again.'); } setApprovingReg(null); }; const rejectReg = async (reg) => { if (!confirm(`Reject vehicle registration for ${reg.owner_name} (${reg.brand} ${reg.model} · ${reg.reg_number})?`)) return; try { await fetch(`api/owner_registrations.php?action=reject&id=${reg.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...getAdminAuthHdr() }, }); setData(d => ({ ...d, registrations: (d.registrations||[]).map(r => r.id===reg.id ? {...r, status:'rejected'} : r), })); setRegMsg(`✅ Registration from ${reg.owner_name} has been rejected.`); logAction(auditLog.logs, auditLog.setLogs, 'Rejected', 'Registration', `${reg.brand} ${reg.model} (${reg.owner_name})`); } catch { setRegMsg('❌ Network error — please try again.'); } }; return (

Vehicle Management

{data.vehicles.length} vehicles · {data.vehicles.filter(v=>v.is_active).length} active ·{' '} ⭐ {data.vehicles.filter(v=>Number(v.is_featured_home)===1).length} featured ·{' '} 🛡️ {data.vehicles.filter(v=>v.insurance_type && v.insurance_type!=='none').length} insured {data.vehicles.filter(v=>v.insurance_type&&v.insurance_type!=='none'&&!v.insurance_verified).length > 0 && ( <> · ⚠️ {data.vehicles.filter(v=>v.insurance_type&&v.insurance_type!=='none'&&!v.insurance_verified).length} unverified )}

{[ { key:'all', label:'All' }, { key:'active', label:'Active' }, { key:'inactive', label:'Inactive' }, { key:'pending', label:'Pending' }, { key:'featured', label:'⭐ Featured' }, { key:'insured', label:'🛡️ Insured Only' }, { key:'unverified', label:'⚠️ Needs Verify' }, ].map(f=>( ))}
{/* ── Pending Vehicle Registrations from User Portal ─────── */} {(() => { const pendingRegs = (data.registrations||[]).filter(r => r.status === 'pending'); const rejectedRegs = (data.registrations||[]).filter(r => r.status === 'rejected'); const allRegs = (data.registrations||[]); if (allRegs.length === 0) return null; return (
0 ? 14 : 0, flexWrap:'wrap', gap:8 }}>
📋 Vehicle Registrations from User Portal {pendingRegs.length > 0 && ( {pendingRegs.length} Pending Approval )} {pendingRegs.length === 0 && ( All Reviewed )}
{allRegs.length} total · {pendingRegs.length} pending · {(data.registrations||[]).filter(r=>r.status==='approved').length} approved · {rejectedRegs.length} rejected
{regMsg && (
{regMsg}
)} {pendingRegs.length > 0 && (
{pendingRegs.map(reg => (
{/* ── Top row: image + core info + actions ── */}
{/* Vehicle image */}
{reg.image_1 ? {e.target.style.display='none';}} /> :
🚗
}
{/* Core vehicle info */}
{reg.vehicle_type} · {reg.brand} {reg.model} {reg.year ? `(${reg.year})` : ''}
Reg: {reg.reg_number} {reg.seats ? ` · ${reg.seats} seats` : ''} {reg.fuel_policy ? ` · ${reg.fuel_policy}` : ''}
Owner: {reg.owner_name} · {reg.phone} · {reg.email}
Submitted: {reg.created_at ? new Date(reg.created_at).toLocaleString('en-GB',{day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}) : '—'}
{/* Actions */}
{/* ── Detail grid: Pricing / Availability / Insurance / Bond / Driver ── */}
{/* Availability & Pricing */}
Availability & Pricing
{(reg.has_self_drive||reg.self_drive) ? (
🔑 Self Drive: Rs. {Number(reg.self_drive_price||0).toLocaleString()}/day
) : null} {(reg.has_owner_driver||reg.with_driver) ? (
🧑‍✈️ With Driver: Rs. {Number(reg.owner_driver_price||0).toLocaleString()}/day
) : null} {!(reg.has_self_drive||reg.self_drive) && !(reg.has_owner_driver||reg.with_driver) && (
Rs. {Number(reg.price_per_day||0).toLocaleString()}/day
)}
{/* Insurance */}
🛡️ Insurance
{reg.insurance_type && reg.insurance_type !== 'none' ? ( <>
{{none:'—',comprehensive:'Comprehensive',third_party:'Third Party',hire_reward:'Hire & Reward',self_drive_rental:'Self-Drive Rental',driver_included:'Driver Included'}[reg.insurance_type] || reg.insurance_type}
{reg.insurance_provider &&
Provider: {reg.insurance_provider}
} {reg.insurance_expiry &&
Expires: {new Date(reg.insurance_expiry).toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'})} {new Date(reg.insurance_expiry)} {reg.insurance_doc_path && } ) : (
⚠️ No insurance submitted
)}
{/* Security Bond */}
0 ? '#F0FDF4' : '#F9FAFB', border:`1px solid ${Number(reg.deposit)>0 ? '#86EFAC':'#E5E7EB'}`, borderRadius:8, padding:'8px 10px' }}>
0 ? '#059669':'#696969', textTransform:'uppercase', letterSpacing:0.5, marginBottom:5 }}> 🔒 Security Bond
{Number(reg.deposit) > 0 ? (
Rs. {Number(reg.deposit).toLocaleString()}
) : (
No bond set
)} {Number(reg.deposit) > 0 &&
Paid directly to owner · Refundable
}
{/* Driver Details (only when has driver) */} {(reg.has_owner_driver||reg.with_driver) && (reg.owner_driver_name||reg.driver_name) ? (
🧑‍✈️ Driver Info
{reg.owner_driver_name||reg.driver_name}
{reg.owner_driver_experience > 0 &&
{reg.owner_driver_experience} yrs experience
} {reg.owner_driver_languages &&
Langs: {reg.owner_driver_languages}
} {reg.driver_license &&
Lic: {reg.driver_license}
}
) : null}
))}
)} {pendingRegs.length === 0 && (
✅ No pending vehicle registrations — all submissions have been reviewed.
)}
); })()} {/* ── Featured Vehicles Summary Panel ─────────────────────── */} {(() => { const featuredList = [...data.vehicles] .filter(v=>Number(v.is_featured_home)===1 && v.is_active) .sort((a,b)=>(a.featured_order||99)-(b.featured_order||99)); const MAX_SLOTS = 12; return (
0 ? 12 : 0, flexWrap:'wrap', gap:8 }}>
Featured on Homepage =MAX_SLOTS?'#DC2626':'#EAB308', color:'#fff', borderRadius:999, fontSize:11, fontWeight:700, padding:'2px 8px' }}> {featuredList.length} / {MAX_SLOTS} slots
These vehicles appear in the Top Vehicles section & hero circles {featuredList.length > 0 && ( )}
{featuredList.length === 0 ? (
No featured vehicles yet — click ⭐ on any vehicle card to add it.
) : (
{featuredList.map((v,i) => (
#{v.featured_order||i+1} {v.image_url || v.img ? {e.target.style.display='none';}} /> : null} {v.brand} {v.model}
))}
)}
); })()}
{list.map(v => (
{v.model}{e.target.style.display='none';}} />
toggleActive(v.id)} />
{Number(v.is_featured_home)===1 && (
⭐ Featured #{v.featured_order || '—'}
)}
{v.brand} {v.model}
{v.type} · {v.year} · {v.seats} seats · Rs. {Number(v.price_per_day||0).toLocaleString()}/day
{/* Insurance status chip */} {v.insurance_type && v.insurance_type !== 'none' ? (
{v.insurance_verified ? '✅' : '⚠️'} {INS_LABELS[v.insurance_type] || v.insurance_type} {v.insurance_expiry && (() => { const daysLeft = Math.floor((new Date(v.insurance_expiry) - new Date()) / 86400000); if (daysLeft < 0) return 🚫 Expired; if (daysLeft <= 30) return ⚠️ {daysLeft}d; return null; })()}
) : (
— No Insurance
)}
{/* Insurance verify button — shown only when vehicle has insurance */} {v.insurance_type && v.insurance_type !== 'none' && ( )}
))} {list.length===0 &&
No vehicles found
}
{/* Add/Edit Modal */} {modal && ( setModal(null)} wide>
{[['Brand *','brand','text'],['Model *','model','text'],['Year','year','number'],['Seats','seats','number'],['Price Per Day (LKR) *','price_per_day','number'],['Location','location','text']].map(([l,k,t])=>( setF(k,t==='number'?+e.target.value:e.target.value)} /> ))}