`); w.document.close(); }; return (
Analytics
Revenue, bookings & fleet performance
{showReport&&(
Monthly Revenue Report
Bookings
{rptBks.length}
Revenue
{MVR(rptRev)}
Avg Booking
{rptBks.length?MVR(Math.round(rptRev/rptBks.length)):'—'}
Bikes Used
{rptBikesUsed}
{rptBks.map(b=>( ))} {!rptBks.length&&} {rptBks.length>0&&}
CustomerBikeAmount
{b.userName}
{fmtDate(b.startDate)} → {fmtDate(b.endDate)}
{b.bikeName} {MVR(b.total)}
No bookings in {MN_FULL[reportMonth]} {reportYear}
Total{MVR(rptRev)}
)} {/* KPI row */}
=0?'↑ '+revPct+'% vs last month':'↓ '+Math.abs(revPct)+'% vs last month'):null} subPos={revPct>=0}/>
{/* Second KPI row */}
70?'var(--green)':utilPct>40?'var(--amber)':'var(--text2)'} sub={rentedToday+' / '+visibleBikes.length+' bikes out today'} subPos={true}/> 80?'var(--green)':'var(--amber)'} sub={signedContracts+' of '+totalContracts+' signed'} subPos={signRate>60}/> =0?'↑ '+bkPct+'% vs last month':'↓ '+Math.abs(bkPct)+'% vs last month'):null} subPos={bkPct>=0}/>
{/* Revenue chart */}
Monthly Revenue — Last 6 Months
{months.map((m,i) => { const h = Math.max(4, Math.round((m.rev/maxRev)*116)); return (
{m.rev>0 &&
{MVR(m.rev)}
}
{m.label}
{m.count>0 &&
{m.count}×
}
); })}
{/* Upcoming returns + Booking status */}
Upcoming Returns (next 7 days)
{upcoming.length ? upcoming.map((b,i) => { const e = new Date(b.endDate||b.end_date); const diff = Math.round((e-today)/86400000); return (
{b.userName||b.user_name}
{b.bikeName||b.bike_name}
{diff===0?'Today':diff===1?'Tomorrow':fmtDate(b.endDate||b.end_date)}
); }) :
No returns in the next 7 days.
}
Booking Status Breakdown
{['confirmed','active','completed','cancelled','pending'].map(s => { const cnt = bookings.filter(b=>b.status===s).length; const pct = bookings.length ? Math.round((cnt/bookings.length)*100) : 0; const COLOR = {confirmed:'var(--blue)',active:'var(--green)',completed:'var(--text2)',cancelled:'var(--red)',pending:'var(--amber)'}; const BG = {confirmed:'var(--blue-dim)',active:'var(--green-dim)',completed:'var(--faint)',cancelled:'var(--red-dim)',pending:'var(--amber-dim)'}; return (
{s} {cnt}
); })}
{/* Bike performance table */}
Performance by Bike
{byBike.map(b => ( ))} {!byBike.length && }
BikeTripsRevenueAvg/TripYield/Day
{b.name}
{b.year}
{b.bookings} {MVR(b.revenue)} {b.bookings>0?MVR(Math.round(b.revenue/b.bookings)):'—'} {b.days>0?MVR(b.yield):'—'}
No booking data yet.
{/* Mobile bike cards */}
{byBike.map(b => (
{b.name} ({b.year})
{b.bookings} trips · {b.days} days rented
{MVR(b.revenue)}
{b.days>0?MVR(b.yield)+'/day':'—'}
))} {!byBike.length &&
No booking data yet.
}
); } const DEFAULT_PERMS = { bookings: true, contracts: true, users: false, bikes: true, nonpayers: false, invoices: true, content: false, analytics: false }; const ROLE_CFG = { superadmin: { label: 'Super Admin', color: 'var(--red)', bg: 'var(--red-dim)' }, manager: { label: 'Manager', color: 'var(--blue)', bg: 'var(--blue-dim)' }, staff: { label: 'Staff', color: 'var(--green)', bg: 'var(--green-dim)' }, }; const ALL_PERMS = ['bookings','contracts','users','bikes','nonpayers','invoices','content','analytics']; // Pages staff role is allowed to access (in display order) const STAFF_PAGES = ['dashboard','bookings','contracts','invoices','bikes']; // Returns NAV_ITEMS visible to this user, with Fleet label for bikes when staff function navItemsFor(adminUser) { if (!adminUser) return []; if (adminUser.role === 'staff') { return NAV_ITEMS .filter(n => STAFF_PAGES.includes(n.id)) .map(n => n.id === 'bikes' ? {...n, label:'Fleet', mobIcon:'\ud83c\udfcd'} : n) .sort((a,b) => STAFF_PAGES.indexOf(a.id) - STAFF_PAGES.indexOf(b.id)); } return NAV_ITEMS; } function PageAdminUsers({currentUser}) { const [admins, setAdmins] = useState([]); const [loading, setLoading] = useState(true); const [showAdd, setShowAdd] = useState(false); const [detail, setDetail] = useState(null); const [form, setForm] = useState({name:'',email:'',password:'',role:'staff',permissions:{...DEFAULT_PERMS},active:true}); const [saving, setSaving] = useState(false); const [err, setErr] = useState(''); const load = async () => { setLoading(true); try { const rows = await sbGet('admin_users','select=*&order=created_at.asc'); setAdmins(rows||[]); } catch(e) {} setLoading(false); }; useEffect(()=>{ load(); },[]); const save = async () => { if(!form.name.trim()||!form.email.trim()||!form.password.trim()) { setErr('Name, email and password are required.'); return; } setSaving(true); setErr(''); try { await sbInsert('admin_users',{name:form.name.trim(),email:form.email.trim().toLowerCase(),password:form.password,role:form.role,permissions:form.permissions,active:form.active}); await load(); setShowAdd(false); setForm({name:'',email:'',password:'',role:'staff',permissions:{...DEFAULT_PERMS},active:true}); } catch(e) { setErr(e.message||'Failed to create user.'); } setSaving(false); }; const toggleActive = async (u) => { await sbUpdate('admin_users', u.id, {active:!u.active}); await load(); }; const updateRole = async (u, role) => { await sbUpdate('admin_users', u.id, {role}); await load(); if(detail&&detail.id===u.id) setDetail(prev=>({...prev,role})); }; const updatePerms = async (u, perms) => { await sbUpdate('admin_users', u.id, {permissions:perms}); await load(); if(detail&&detail.id===u.id) setDetail(prev=>({...prev,permissions:perms})); }; const resetPw = async (u, newPw) => { if(!newPw.trim()) return; await sbUpdate('admin_users', u.id, {password:newPw.trim()}); alert('Password updated.'); }; const PermToggle = ({perms, setPerms, disabled}) => (
{ALL_PERMS.map(p=>( ))}
); return (
Access Control
Admin Users
{currentUser?.role==='superadmin'&&}
{loading ?
Loading…
: ( <>
{admins.map(u=>( ))}
NameEmailRoleLast LoginStatusActions
{u.name}{u.id===currentUser?.id&&(you)} {u.email} {u.last_login?fmtDate(u.last_login.split('T')[0]):'Never'} {u.active?'Active':'Inactive'}
{currentUser?.role==='superadmin'&&u.id!==currentUser?.id&&( )}
{}
{admins.map(u=>(
setDetail(u)}>
{u.name}{u.id===currentUser?.id&&' (you)'}
{u.email}
{u.active?'Active':'Inactive'} Last login: {u.last_login?fmtDate(u.last_login.split('T')[0]):'Never'}
))}
)} {} {showAdd&&( setShowAdd(false)}>
{[['name','Full Name'],['email','Email Address'],['password','Password']].map(([k,l])=>(
setForm(p=>({...p,[k]:ev.target.value}))}/>
))}
setForm(prev=>({...prev,permissions:p}))}/>
{err&&
{err}
}
)} {} {detail&&( setDetail(null)} wide>
{detail.name}
{detail.email}
{fmtDate(detail.created_at?.split('T')[0])}
{detail.last_login?fmtDate(detail.last_login.split('T')[0]):'Never'}
{currentUser?.role==='superadmin'&&detail.id!==currentUser?.id&&( <>
{['staff','manager','superadmin'].map(r=>( ))}
updatePerms(detail,p)}/>
)} {(currentUser?.role!=='superadmin'||detail.id===currentUser?.id)&&(
Contact your super admin to make changes to this account.
)}
)}
); } /* ════ ACCOUNTING ════ */ const ACCOUNT_TYPES = ['asset','liability','equity','income','expense']; const ACCOUNT_TYPE_COLOR = { asset: {color:'var(--blue)',bg:'#0A1628',border:'#0d1f3c'}, liability: {color:'var(--amber)',bg:'#1C1200',border:'#2a1c00'}, equity: {color:'#A855F7',bg:'#1A0A2A',border:'#2d1545'}, income: {color:'var(--green)',bg:'#052010',border:'#0a3018'}, expense: {color:'var(--red-soft)',bg:'#2A0000',border:'#440000'}, }; const TX_TYPES = ['income','expense','transfer','journal']; const PAYMENT_METHODS = ['cash','bank_transfer','card','other']; function fmtCurrency(amount,code='MVR'){return code+' '+Number(amount).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2});} async function loadAccounting(){const[accs,txs,curs]=await Promise.all([sbGet('accounts','select=*&active=eq.true&order=code.asc'),sbGet('transactions','select=*&order=date.desc,created_at.desc'),sbGet('currencies','select=*&active=eq.true&order=code.asc')]);return{accounts:accs||[],transactions:txs||[],currencies:curs||[]};} function calcPL(transactions,period='month'){const now=new Date();const start=new Date(now.getFullYear(),period==='month'?now.getMonth():period==='quarter'?Math.floor(now.getMonth()/3)*3:0,1);const txs=transactions.filter(t=>new Date(t.date)>=start);const income=txs.filter(t=>t.type==='income').reduce((a,t)=>a+Number(t.amount_mvr),0);const expense=txs.filter(t=>t.type==='expense').reduce((a,t)=>a+Number(t.amount_mvr),0);return{income,expense,net:income-expense,txs};} function AccDashboard({data}){const{transactions}=data;const month=calcPL(transactions,'month');const year=calcPL(transactions,'year');const recentTx=[...transactions].slice(0,8);const StatCard=({label,value,color})=>(
{label}
{value}
);return(
This Month
=0?'#22C55E':'#FF5555'}/>
This Year
=0?'#22C55E':'#FF5555'}/>
Recent Transactions
{recentTx.length===0&&
No transactions yet.
}{recentTx.map(t=>(
{t.description}
{fmtDate(t.date)} · {t.type} · {t.payment_method||'—'}
{t.type==='income'?'+':'-'}{fmtCurrency(t.amount_mvr)}
))}
);} function AccTransactions({data,reload,adminUser}){const{accounts,currencies,transactions}=data;const[showAdd,setShowAdd]=useState(false);const[filter,setFilter]=useState('all');const[search,setSearch]=useState('');const[form,setForm]=useState({date:nowStr(),type:'income',description:'',reference:'',debit_account:'',credit_account:'',amount:'',currency:'MVR',rate_to_mvr:'1',payment_method:'cash',notes:''});const filtered=transactions.filter(t=>{if(filter!=='all'&&t.type!==filter)return false;if(search&&!t.description.toLowerCase().includes(search.toLowerCase()))return false;return true;});const amountMVR=()=>(Number(form.amount)||0)*(Number(form.rate_to_mvr)||1);const saveTransaction=async()=>{if(!form.description||!form.amount||!form.debit_account||!form.credit_account)return;await sbInsert('transactions',{date:form.date,type:form.type,description:form.description,reference:form.reference||null,debit_account:form.debit_account,credit_account:form.credit_account,amount:Number(form.amount),currency:form.currency,rate_to_mvr:Number(form.rate_to_mvr)||1,amount_mvr:amountMVR(),payment_method:form.payment_method,notes:form.notes||null,created_by:adminUser?.id||null});await reload();setShowAdd(false);setForm({date:nowStr(),type:'income',description:'',reference:'',debit_account:'',credit_account:'',amount:'',currency:'MVR',rate_to_mvr:'1',payment_method:'cash',notes:''});};return(
{['all',...TX_TYPES].map(f=>())}
setSearch(ev.target.value)} placeholder="Search transactions…" style={{marginBottom:12}}/>
{filtered.map(t=>())}{!filtered.length&&}
DateDescriptionTypeMethodAmount (MVR)
{fmtDate(t.date)}
{t.description}
{t.reference&&
{t.reference}
}
{t.type}{t.payment_method?.replace('_',' ')||'—'}{t.type==='income'?'+':'-'}{fmtCurrency(t.amount_mvr)}
No transactions found.
{filtered.map(t=>(
{t.description}
{fmtDate(t.date)} · {t.payment_method?.replace('_',' ')||'—'}
{t.type==='income'?'+':'-'}{fmtCurrency(t.amount_mvr)}
))}
{showAdd&&(setShowAdd(false)} wide>
setForm(p=>({...p,date:ev.target.value}))}/>
setForm(p=>({...p,description:ev.target.value}))} placeholder="e.g. Bike rental payment"/>
setForm(p=>({...p,reference:ev.target.value}))} placeholder="e.g. RIDE-001"/>
setForm(p=>({...p,amount:ev.target.value}))} placeholder="0.00"/>
setForm(p=>({...p,rate_to_mvr:ev.target.value}))}/>