create-vrrsystem-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/bin/index.js +30 -0
  2. package/package.json +29 -0
  3. package/templates/VehicleRentalReservationSystem/Documentation/API.md +57 -0
  4. package/templates/VehicleRentalReservationSystem/Documentation/DFD.md +62 -0
  5. package/templates/VehicleRentalReservationSystem/Documentation/ERD.md +109 -0
  6. package/templates/VehicleRentalReservationSystem/Documentation/Installation.md +40 -0
  7. package/templates/VehicleRentalReservationSystem/README.md +165 -0
  8. package/templates/VehicleRentalReservationSystem/backend-project/.env.example +8 -0
  9. package/templates/VehicleRentalReservationSystem/backend-project/config/db.js +10 -0
  10. package/templates/VehicleRentalReservationSystem/backend-project/controllers/auth.controller.js +39 -0
  11. package/templates/VehicleRentalReservationSystem/backend-project/controllers/customer.controller.js +19 -0
  12. package/templates/VehicleRentalReservationSystem/backend-project/controllers/dashboard.controller.js +48 -0
  13. package/templates/VehicleRentalReservationSystem/backend-project/controllers/report.controller.js +45 -0
  14. package/templates/VehicleRentalReservationSystem/backend-project/controllers/reservation.controller.js +94 -0
  15. package/templates/VehicleRentalReservationSystem/backend-project/controllers/user.controller.js +16 -0
  16. package/templates/VehicleRentalReservationSystem/backend-project/controllers/vehicle.controller.js +20 -0
  17. package/templates/VehicleRentalReservationSystem/backend-project/middleware/auth.js +22 -0
  18. package/templates/VehicleRentalReservationSystem/backend-project/middleware/errorHandler.js +10 -0
  19. package/templates/VehicleRentalReservationSystem/backend-project/middleware/validate.js +6 -0
  20. package/templates/VehicleRentalReservationSystem/backend-project/models/Customer.js +11 -0
  21. package/templates/VehicleRentalReservationSystem/backend-project/models/ReservationRental.js +17 -0
  22. package/templates/VehicleRentalReservationSystem/backend-project/models/User.js +20 -0
  23. package/templates/VehicleRentalReservationSystem/backend-project/models/Vehicle.js +14 -0
  24. package/templates/VehicleRentalReservationSystem/backend-project/package-lock.json +1695 -0
  25. package/templates/VehicleRentalReservationSystem/backend-project/package.json +26 -0
  26. package/templates/VehicleRentalReservationSystem/backend-project/routes/auth.routes.js +21 -0
  27. package/templates/VehicleRentalReservationSystem/backend-project/routes/customer.routes.js +20 -0
  28. package/templates/VehicleRentalReservationSystem/backend-project/routes/dashboard.routes.js +6 -0
  29. package/templates/VehicleRentalReservationSystem/backend-project/routes/report.routes.js +6 -0
  30. package/templates/VehicleRentalReservationSystem/backend-project/routes/reservation.routes.js +14 -0
  31. package/templates/VehicleRentalReservationSystem/backend-project/routes/user.routes.js +10 -0
  32. package/templates/VehicleRentalReservationSystem/backend-project/routes/vehicle.routes.js +21 -0
  33. package/templates/VehicleRentalReservationSystem/backend-project/seed/seed.js +80 -0
  34. package/templates/VehicleRentalReservationSystem/backend-project/server.js +40 -0
  35. package/templates/VehicleRentalReservationSystem/backend-project/utils/token.js +3 -0
  36. package/templates/VehicleRentalReservationSystem/frontend-project/index.html +14 -0
  37. package/templates/VehicleRentalReservationSystem/frontend-project/package-lock.json +3759 -0
  38. package/templates/VehicleRentalReservationSystem/frontend-project/package.json +32 -0
  39. package/templates/VehicleRentalReservationSystem/frontend-project/postcss.config.js +1 -0
  40. package/templates/VehicleRentalReservationSystem/frontend-project/src/App.jsx +33 -0
  41. package/templates/VehicleRentalReservationSystem/frontend-project/src/components/ConfirmDelete.jsx +12 -0
  42. package/templates/VehicleRentalReservationSystem/frontend-project/src/components/Modal.jsx +22 -0
  43. package/templates/VehicleRentalReservationSystem/frontend-project/src/components/ProtectedRoute.jsx +9 -0
  44. package/templates/VehicleRentalReservationSystem/frontend-project/src/components/Sidebar.jsx +42 -0
  45. package/templates/VehicleRentalReservationSystem/frontend-project/src/components/Topbar.jsx +24 -0
  46. package/templates/VehicleRentalReservationSystem/frontend-project/src/context/AuthContext.jsx +30 -0
  47. package/templates/VehicleRentalReservationSystem/frontend-project/src/context/ThemeContext.jsx +13 -0
  48. package/templates/VehicleRentalReservationSystem/frontend-project/src/index.css +37 -0
  49. package/templates/VehicleRentalReservationSystem/frontend-project/src/layouts/AppLayout.jsx +18 -0
  50. package/templates/VehicleRentalReservationSystem/frontend-project/src/main.jsx +20 -0
  51. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Customers.jsx +95 -0
  52. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Dashboard.jsx +84 -0
  53. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Login.jsx +54 -0
  54. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Register.jsx +36 -0
  55. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Rentals.jsx +58 -0
  56. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Reports.jsx +108 -0
  57. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Reservations.jsx +125 -0
  58. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Settings.jsx +32 -0
  59. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Users.jsx +78 -0
  60. package/templates/VehicleRentalReservationSystem/frontend-project/src/pages/Vehicles.jsx +110 -0
  61. package/templates/VehicleRentalReservationSystem/frontend-project/src/services/api.js +16 -0
  62. package/templates/VehicleRentalReservationSystem/frontend-project/src/utils/format.js +20 -0
  63. package/templates/VehicleRentalReservationSystem/frontend-project/tailwind.config.js +18 -0
  64. package/templates/VehicleRentalReservationSystem/frontend-project/vite.config.js +9 -0
@@ -0,0 +1,108 @@
1
+ import { useState } from 'react';
2
+ import api from '../services/api';
3
+ import { toast } from 'react-toastify';
4
+ import { FiDownload, FiPrinter, FiFile, FiFileText } from 'react-icons/fi';
5
+ import { date, money, badge } from '../utils/format';
6
+ import jsPDF from 'jspdf';
7
+ import 'jspdf-autotable';
8
+ import * as XLSX from 'xlsx';
9
+
10
+ export default function Reports() {
11
+ const [period, setPeriod] = useState('monthly');
12
+ const [report, setReport] = useState(null);
13
+ const [loading, setLoading] = useState(false);
14
+
15
+ const gen = async () => {
16
+ setLoading(true);
17
+ try { const { data } = await api.get('/reports', { params: { period } }); setReport(data); }
18
+ catch (err) { toast.error('Error generating report'); }
19
+ finally { setLoading(false); }
20
+ };
21
+
22
+ const cols = [
23
+ { k:'customerName', l:'Customer' }, { k:'nationalId', l:'NID' }, { k:'phone', l:'Phone' },
24
+ { k:'plateNumber', l:'Plate' }, { k:'brand', l:'Brand' }, { k:'model', l:'Model' },
25
+ { k:'year', l:'Year' }, { k:'vehicleType', l:'Type' },
26
+ { k:'reservationDate', l:'Reserved', fmt:date }, { k:'rentalDate', l:'Rented', fmt:date }, { k:'returnDate', l:'Returned', fmt:date },
27
+ { k:'reservationStatus', l:'Res. Status' }, { k:'rentalStatus', l:'Rental Status' }, { k:'rentalFee', l:'Fee', fmt:money }
28
+ ];
29
+
30
+ const exportPDF = () => {
31
+ const doc = new jsPDF('l', 'pt', 'a4');
32
+ doc.setFontSize(16); doc.text('SwiftWheels - VRS Report', 40, 40);
33
+ doc.setFontSize(10); doc.text(`Period: ${report.period} | ${new Date(report.from).toLocaleDateString()} → ${new Date(report.to).toLocaleDateString()}`, 40, 58);
34
+ doc.text(`Total Revenue: ${money(report.totalRevenue)} | Records: ${report.count}`, 40, 72);
35
+ doc.autoTable({
36
+ startY: 90,
37
+ head: [cols.map(c=>c.l)],
38
+ body: report.rows.map(r => cols.map(c => c.fmt ? c.fmt(r[c.k]) : (r[c.k] ?? '-'))),
39
+ styles: { fontSize: 7 }, headStyles: { fillColor: [30, 41, 59] }
40
+ });
41
+ doc.save(`VRS-Report-${report.period}-${Date.now()}.pdf`);
42
+ };
43
+ const exportExcel = () => {
44
+ const ws = XLSX.utils.json_to_sheet(report.rows);
45
+ const wb = XLSX.utils.book_new();
46
+ XLSX.utils.book_append_sheet(wb, ws, 'Report');
47
+ XLSX.writeFile(wb, `VRS-Report-${report.period}-${Date.now()}.xlsx`);
48
+ };
49
+ const exportCSV = () => {
50
+ const head = cols.map(c=>c.l).join(',');
51
+ const body = report.rows.map(r => cols.map(c => JSON.stringify(c.fmt ? c.fmt(r[c.k]) : (r[c.k] ?? ''))).join(',')).join('\n');
52
+ const blob = new Blob([head+'\n'+body], { type:'text/csv' });
53
+ const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `VRS-Report-${period}.csv`; a.click();
54
+ };
55
+ const print = () => window.print();
56
+
57
+ return (
58
+ <div>
59
+ <h1 className="text-2xl font-bold mb-4">Reports</h1>
60
+ <div className="card flex flex-wrap items-end gap-3 mb-4">
61
+ <div><label className="text-sm block">Period</label>
62
+ <select className="input w-48" value={period} onChange={e=>setPeriod(e.target.value)}>
63
+ <option value="daily">Daily</option><option value="weekly">Weekly</option>
64
+ <option value="monthly">Monthly</option><option value="yearly">Yearly</option>
65
+ <option value="all">All Time</option>
66
+ </select>
67
+ </div>
68
+ <button className="btn-primary" onClick={gen} disabled={loading}>{loading?'Generating...':'Generate Report'}</button>
69
+ {report && (
70
+ <div className="flex gap-2 ml-auto">
71
+ <button className="btn-ghost" onClick={exportPDF}><FiFile/> PDF</button>
72
+ <button className="btn-ghost" onClick={exportExcel}><FiDownload/> Excel</button>
73
+ <button className="btn-ghost" onClick={exportCSV}><FiFileText/> CSV</button>
74
+ <button className="btn-ghost" onClick={print}><FiPrinter/> Print</button>
75
+ </div>
76
+ )}
77
+ </div>
78
+
79
+ {report && (
80
+ <div id="print-area">
81
+ <div className="card mb-4 print:shadow-none">
82
+ <div className="flex justify-between flex-wrap gap-3">
83
+ <div><h2 className="font-bold text-lg">SwiftWheels Enterprises</h2><p className="text-sm text-slate-500">Vehicle Rental & Reservation Report</p></div>
84
+ <div className="text-right text-sm">
85
+ <div>Period: <b>{report.period.toUpperCase()}</b></div>
86
+ <div>{new Date(report.from).toLocaleDateString()} → {new Date(report.to).toLocaleDateString()}</div>
87
+ </div>
88
+ </div>
89
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
90
+ <div className="p-3 rounded-lg bg-slate-100 dark:bg-slate-700"><div className="text-xs">Records</div><div className="text-xl font-bold">{report.count}</div></div>
91
+ <div className="p-3 rounded-lg bg-emerald-100 text-emerald-800"><div className="text-xs">Total Revenue</div><div className="text-xl font-bold">{money(report.totalRevenue)}</div></div>
92
+ </div>
93
+ </div>
94
+ <div className="overflow-x-auto card">
95
+ <table>
96
+ <thead><tr>{cols.map(c=><th key={c.k}>{c.l}</th>)}</tr></thead>
97
+ <tbody>{report.rows.map((r,i) => (
98
+ <tr key={i}>{cols.map(c => (
99
+ <td key={c.k}>{c.k.includes('Status') ? <span className={`badge ${badge(r[c.k])}`}>{r[c.k]}</span> : (c.fmt ? c.fmt(r[c.k]) : (r[c.k] ?? '-'))}</td>
100
+ ))}</tr>
101
+ ))}{!report.rows.length && <tr><td colSpan={cols.length} className="text-center text-slate-400 py-8">No records</td></tr>}</tbody>
102
+ </table>
103
+ </div>
104
+ </div>
105
+ )}
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,125 @@
1
+ import { useEffect, useState } from 'react';
2
+ import api from '../services/api';
3
+ import { toast } from 'react-toastify';
4
+ import { FiPlus, FiCheck, FiX, FiTrash2, FiSearch, FiTruck } from 'react-icons/fi';
5
+ import Modal from '../components/Modal';
6
+ import ConfirmDelete from '../components/ConfirmDelete';
7
+ import { date, money, badge } from '../utils/format';
8
+ import { useAuth } from '../context/AuthContext';
9
+
10
+ const STATUS = ['PENDING','APPROVED','REJECTED','CANCELLED','COMPLETED'];
11
+
12
+ export default function Reservations() {
13
+ const { has } = useAuth();
14
+ const [data, setData] = useState({ items: [], total: 0, pages: 1 });
15
+ const [search, setSearch] = useState('');
16
+ const [status, setStatus] = useState('');
17
+ const [page, setPage] = useState(1);
18
+ const [modal, setModal] = useState(false);
19
+ const [customers, setCustomers] = useState([]);
20
+ const [vehicles, setVehicles] = useState([]);
21
+ const [form, setForm] = useState({ customerId:'', vehicleId:'', startDate:'', endDate:'' });
22
+ const [del, setDel] = useState(null);
23
+
24
+ const load = async () => {
25
+ const { data } = await api.get('/reservations', { params: { search, status, page, limit: 10 } });
26
+ setData(data);
27
+ };
28
+ useEffect(() => { load(); }, [search, status, page]);
29
+
30
+ const openModal = async () => {
31
+ const [c, v] = await Promise.all([api.get('/customers', { params: { limit: 200 } }), api.get('/vehicles', { params: { status: 'AVAILABLE', limit: 200 } })]);
32
+ setCustomers(c.data.items); setVehicles(v.data.items);
33
+ setForm({ customerId: c.data.items[0]?._id || '', vehicleId: v.data.items[0]?._id || '', startDate: new Date().toISOString().slice(0,10), endDate: new Date(Date.now()+86400000*3).toISOString().slice(0,10) });
34
+ setModal(true);
35
+ };
36
+ const create = async (e) => {
37
+ e.preventDefault();
38
+ try { await api.post('/reservations', form); toast.success('Reservation created'); setModal(false); load(); }
39
+ catch (err) { toast.error(err.response?.data?.message || 'Error'); }
40
+ };
41
+ const action = async (id, type) => {
42
+ try { await api.patch(`/reservations/${id}/${type}`); toast.success(type.toUpperCase()); load(); }
43
+ catch (err) { toast.error(err.response?.data?.message || 'Error'); }
44
+ };
45
+ const remove = async () => {
46
+ try { await api.delete(`/reservations/${del._id}`); toast.success('Deleted'); setDel(null); load(); }
47
+ catch (err) { toast.error('Error'); }
48
+ };
49
+
50
+ return (
51
+ <div>
52
+ <div className="flex flex-wrap items-center justify-between gap-3 mb-4">
53
+ <h1 className="text-2xl font-bold">Reservations</h1>
54
+ <div className="flex gap-2">
55
+ <select className="input w-40" value={status} onChange={e=>{setPage(1);setStatus(e.target.value);}}>
56
+ <option value="">All Status</option>{STATUS.map(s=><option key={s}>{s}</option>)}
57
+ </select>
58
+ <div className="relative"><FiSearch className="absolute left-3 top-3 text-slate-400"/>
59
+ <input className="input pl-10 w-64" placeholder="Search..." value={search} onChange={e=>{setPage(1);setSearch(e.target.value);}}/>
60
+ </div>
61
+ {has('ADMIN','STAFF') && <button className="btn-primary" onClick={openModal}><FiPlus/> New Reservation</button>}
62
+ </div>
63
+ </div>
64
+
65
+ <div className="overflow-x-auto">
66
+ <table>
67
+ <thead><tr><th>Customer</th><th>Vehicle</th><th>Start</th><th>End</th><th>Fee</th><th>Status</th><th>Rental</th><th className="text-right">Actions</th></tr></thead>
68
+ <tbody>{data.items.map(r => (
69
+ <tr key={r._id}>
70
+ <td className="font-medium">{r.customerId?.fullName}</td>
71
+ <td><div className="font-mono">{r.vehicleId?.plateNumber}</div><div className="text-xs text-slate-500">{r.vehicleId?.brand} {r.vehicleId?.model}</div></td>
72
+ <td>{date(r.startDate)}</td><td>{date(r.endDate)}</td>
73
+ <td>{money(r.rentalFee)}</td>
74
+ <td><span className={`badge ${badge(r.reservationStatus)}`}>{r.reservationStatus}</span></td>
75
+ <td><span className={`badge ${badge(r.rentalStatus)}`}>{r.rentalStatus}</span></td>
76
+ <td className="text-right whitespace-nowrap">
77
+ {r.reservationStatus==='PENDING' && has('ADMIN','MANAGER') && (
78
+ <>
79
+ <button title="Approve" className="btn-ghost text-emerald-600" onClick={()=>action(r._id,'approve')}><FiCheck/></button>
80
+ <button title="Reject" className="btn-ghost text-red-600" onClick={()=>action(r._id,'reject')}><FiX/></button>
81
+ </>
82
+ )}
83
+ {r.reservationStatus==='APPROVED' && r.rentalStatus==='NOT_STARTED' && has('ADMIN','STAFF') && (
84
+ <button title="Pickup" className="btn-ghost text-blue-600" onClick={()=>action(r._id,'pickup')}><FiTruck/></button>
85
+ )}
86
+ {has('ADMIN') && <button className="btn-ghost text-red-600" onClick={()=>setDel(r)}><FiTrash2/></button>}
87
+ </td>
88
+ </tr>
89
+ ))}{!data.items.length && <tr><td colSpan="8" className="text-center text-slate-400 py-8">No reservations</td></tr>}</tbody>
90
+ </table>
91
+ </div>
92
+
93
+ <div className="flex justify-between items-center mt-4 text-sm">
94
+ <div>Total: {data.total}</div>
95
+ <div className="flex gap-2">
96
+ <button disabled={page<=1} onClick={()=>setPage(p=>p-1)} className="btn-ghost disabled:opacity-30">Prev</button>
97
+ <div className="px-3 py-1.5">{page} / {data.pages}</div>
98
+ <button disabled={page>=data.pages} onClick={()=>setPage(p=>p+1)} className="btn-ghost disabled:opacity-30">Next</button>
99
+ </div>
100
+ </div>
101
+
102
+ <Modal open={modal} onClose={()=>setModal(false)} title="New Reservation">
103
+ <form onSubmit={create} className="grid grid-cols-2 gap-3">
104
+ <div className="col-span-2"><label className="text-sm">Customer</label>
105
+ <select className="input" value={form.customerId} onChange={e=>setForm({...form,customerId:e.target.value})} required>
106
+ {customers.map(c=><option key={c._id} value={c._id}>{c.fullName} — {c.nationalId}</option>)}
107
+ </select>
108
+ </div>
109
+ <div className="col-span-2"><label className="text-sm">Vehicle (available)</label>
110
+ <select className="input" value={form.vehicleId} onChange={e=>setForm({...form,vehicleId:e.target.value})} required>
111
+ {vehicles.map(v=><option key={v._id} value={v._id}>{v.plateNumber} — {v.brand} {v.model} (${v.dailyRate}/day)</option>)}
112
+ </select>
113
+ </div>
114
+ <div><label className="text-sm">Start Date</label><input type="date" className="input" value={form.startDate} onChange={e=>setForm({...form,startDate:e.target.value})} required/></div>
115
+ <div><label className="text-sm">End Date</label><input type="date" className="input" value={form.endDate} onChange={e=>setForm({...form,endDate:e.target.value})} required/></div>
116
+ <div className="col-span-2 flex justify-end gap-2 mt-2">
117
+ <button type="button" className="btn-ghost" onClick={()=>setModal(false)}>Cancel</button>
118
+ <button className="btn-primary">Create</button>
119
+ </div>
120
+ </form>
121
+ </Modal>
122
+ <ConfirmDelete open={!!del} onClose={()=>setDel(null)} onConfirm={remove} label={`reservation #${del?._id?.slice(-6)}`}/>
123
+ </div>
124
+ );
125
+ }
@@ -0,0 +1,32 @@
1
+ import { useTheme } from '../context/ThemeContext';
2
+ import { useAuth } from '../context/AuthContext';
3
+
4
+ export default function Settings() {
5
+ const { theme, toggle } = useTheme();
6
+ const { user } = useAuth();
7
+ return (
8
+ <div className="space-y-4">
9
+ <h1 className="text-2xl font-bold">Settings</h1>
10
+ <div className="card">
11
+ <h3 className="font-semibold mb-3">Profile</h3>
12
+ <div className="grid grid-cols-2 gap-3 text-sm">
13
+ <div><div className="text-slate-500">Username</div><div className="font-medium">{user?.username}</div></div>
14
+ <div><div className="text-slate-500">Full Name</div><div className="font-medium">{user?.fullName || '-'}</div></div>
15
+ <div><div className="text-slate-500">Email</div><div className="font-medium">{user?.email || '-'}</div></div>
16
+ <div><div className="text-slate-500">Role</div><div className="font-medium">{user?.role}</div></div>
17
+ </div>
18
+ </div>
19
+ <div className="card">
20
+ <h3 className="font-semibold mb-3">Appearance</h3>
21
+ <div className="flex items-center justify-between">
22
+ <div><div className="font-medium">Dark Mode</div><div className="text-sm text-slate-500">Toggle dark / light theme</div></div>
23
+ <button className="btn-primary" onClick={toggle}>{theme === 'dark' ? 'Switch to Light' : 'Switch to Dark'}</button>
24
+ </div>
25
+ </div>
26
+ <div className="card">
27
+ <h3 className="font-semibold mb-2">About</h3>
28
+ <p className="text-sm text-slate-500">SwiftWheels VRS v1.0.0 · Huye City, Rwanda</p>
29
+ </div>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,78 @@
1
+ import { useEffect, useState } from 'react';
2
+ import api from '../services/api';
3
+ import { toast } from 'react-toastify';
4
+ import { FiPlus, FiEdit2, FiTrash2 } from 'react-icons/fi';
5
+ import Modal from '../components/Modal';
6
+ import ConfirmDelete from '../components/ConfirmDelete';
7
+
8
+ const empty = { username:'', password:'', fullName:'', email:'', role:'STAFF', active:true };
9
+
10
+ export default function Users() {
11
+ const [items, setItems] = useState([]);
12
+ const [modal, setModal] = useState(false);
13
+ const [form, setForm] = useState(empty);
14
+ const [editing, setEditing] = useState(null);
15
+ const [del, setDel] = useState(null);
16
+
17
+ const load = async () => { const { data } = await api.get('/users'); setItems(data); };
18
+ useEffect(() => { load(); }, []);
19
+
20
+ const open = (u) => { setEditing(u); setForm(u ? { ...u, password:'' } : empty); setModal(true); };
21
+ const save = async (e) => {
22
+ e.preventDefault();
23
+ try {
24
+ if (editing) await api.put(`/users/${editing._id}`, form);
25
+ else await api.post('/users', form);
26
+ toast.success('Saved'); setModal(false); load();
27
+ } catch (err) { toast.error(err.response?.data?.message || 'Error'); }
28
+ };
29
+ const remove = async () => {
30
+ try { await api.delete(`/users/${del._id}`); toast.success('Deleted'); setDel(null); load(); }
31
+ catch (err) { toast.error('Error'); }
32
+ };
33
+
34
+ return (
35
+ <div>
36
+ <div className="flex items-center justify-between mb-4">
37
+ <h1 className="text-2xl font-bold">System Users</h1>
38
+ <button className="btn-primary" onClick={()=>open(null)}><FiPlus/> Add User</button>
39
+ </div>
40
+ <div className="overflow-x-auto">
41
+ <table>
42
+ <thead><tr><th>Username</th><th>Full Name</th><th>Email</th><th>Role</th><th>Active</th><th className="text-right">Actions</th></tr></thead>
43
+ <tbody>{items.map(u => (
44
+ <tr key={u._id}>
45
+ <td className="font-medium">{u.username}</td><td>{u.fullName}</td><td>{u.email}</td>
46
+ <td><span className="badge bg-slate-200 text-slate-700">{u.role}</span></td>
47
+ <td>{u.active ? '✅' : '❌'}</td>
48
+ <td className="text-right">
49
+ <button className="btn-ghost" onClick={()=>open(u)}><FiEdit2/></button>
50
+ <button className="btn-ghost text-red-600" onClick={()=>setDel(u)}><FiTrash2/></button>
51
+ </td>
52
+ </tr>
53
+ ))}</tbody>
54
+ </table>
55
+ </div>
56
+
57
+ <Modal open={modal} onClose={()=>setModal(false)} title={editing ? 'Edit User' : 'New User'}>
58
+ <form onSubmit={save} className="grid grid-cols-2 gap-3">
59
+ <div><label className="text-sm">Username</label><input className="input" value={form.username} onChange={e=>setForm({...form,username:e.target.value})} required/></div>
60
+ <div><label className="text-sm">Password {editing && <span className="text-xs text-slate-400">(leave empty to keep)</span>}</label>
61
+ <input type="password" className="input" value={form.password} onChange={e=>setForm({...form,password:e.target.value})} {...(!editing?{required:true}:{})}/></div>
62
+ <div><label className="text-sm">Full Name</label><input className="input" value={form.fullName} onChange={e=>setForm({...form,fullName:e.target.value})}/></div>
63
+ <div><label className="text-sm">Email</label><input type="email" className="input" value={form.email} onChange={e=>setForm({...form,email:e.target.value})}/></div>
64
+ <div><label className="text-sm">Role</label>
65
+ <select className="input" value={form.role} onChange={e=>setForm({...form,role:e.target.value})}>
66
+ <option>ADMIN</option><option>MANAGER</option><option>STAFF</option>
67
+ </select></div>
68
+ <div className="flex items-end gap-2"><input type="checkbox" id="active" checked={form.active} onChange={e=>setForm({...form,active:e.target.checked})}/><label htmlFor="active">Active</label></div>
69
+ <div className="col-span-2 flex justify-end gap-2 mt-2">
70
+ <button type="button" className="btn-ghost" onClick={()=>setModal(false)}>Cancel</button>
71
+ <button className="btn-primary">Save</button>
72
+ </div>
73
+ </form>
74
+ </Modal>
75
+ <ConfirmDelete open={!!del} onClose={()=>setDel(null)} onConfirm={remove} label={del?.username}/>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,110 @@
1
+ import { useEffect, useState } from 'react';
2
+ import api from '../services/api';
3
+ import { toast } from 'react-toastify';
4
+ import { FiPlus, FiEdit2, FiTrash2, FiSearch } from 'react-icons/fi';
5
+ import Modal from '../components/Modal';
6
+ import ConfirmDelete from '../components/ConfirmDelete';
7
+ import { badge, money } from '../utils/format';
8
+ import { useAuth } from '../context/AuthContext';
9
+
10
+ const empty = { plateNumber:'', brand:'', model:'', year: new Date().getFullYear(), vehicleType:'Sedan', purchasePrice:0, dailyRate:50, status:'AVAILABLE' };
11
+ const TYPES = ['Sedan','SUV','Hatchback','Pickup','Van','Bus','Truck','Coupe'];
12
+ const STATUS = ['AVAILABLE','RESERVED','RENTED','MAINTENANCE'];
13
+
14
+ export default function Vehicles() {
15
+ const { has } = useAuth();
16
+ const [data, setData] = useState({ items: [], total: 0, pages: 1 });
17
+ const [search, setSearch] = useState('');
18
+ const [status, setStatus] = useState('');
19
+ const [page, setPage] = useState(1);
20
+ const [modal, setModal] = useState(false);
21
+ const [form, setForm] = useState(empty);
22
+ const [editing, setEditing] = useState(null);
23
+ const [del, setDel] = useState(null);
24
+
25
+ const load = async () => {
26
+ const { data } = await api.get('/vehicles', { params: { search, status, page, limit: 10 } });
27
+ setData(data);
28
+ };
29
+ useEffect(() => { load(); }, [search, status, page]);
30
+
31
+ const open = (v) => { setEditing(v); setForm(v || empty); setModal(true); };
32
+ const save = async (e) => {
33
+ e.preventDefault();
34
+ try {
35
+ const payload = { ...form, year: +form.year, purchasePrice: +form.purchasePrice, dailyRate: +form.dailyRate };
36
+ if (editing) await api.put(`/vehicles/${editing._id}`, payload);
37
+ else await api.post('/vehicles', payload);
38
+ toast.success('Saved'); setModal(false); load();
39
+ } catch (err) { toast.error(err.response?.data?.message || 'Error'); }
40
+ };
41
+ const remove = async () => {
42
+ try { await api.delete(`/vehicles/${del._id}`); toast.success('Deleted'); setDel(null); load(); }
43
+ catch (err) { toast.error('Cannot delete'); }
44
+ };
45
+
46
+ return (
47
+ <div>
48
+ <div className="flex flex-wrap items-center justify-between gap-3 mb-4">
49
+ <h1 className="text-2xl font-bold">Vehicles</h1>
50
+ <div className="flex gap-2">
51
+ <select className="input w-40" value={status} onChange={e=>{setPage(1);setStatus(e.target.value);}}>
52
+ <option value="">All Status</option>{STATUS.map(s=><option key={s}>{s}</option>)}
53
+ </select>
54
+ <div className="relative"><FiSearch className="absolute left-3 top-3 text-slate-400"/>
55
+ <input className="input pl-10 w-64" placeholder="Search..." value={search} onChange={e=>{setPage(1);setSearch(e.target.value);}}/>
56
+ </div>
57
+ {has('ADMIN') && <button className="btn-primary" onClick={()=>open(null)}><FiPlus/> Add Vehicle</button>}
58
+ </div>
59
+ </div>
60
+
61
+ <div className="overflow-x-auto">
62
+ <table>
63
+ <thead><tr><th>Plate</th><th>Brand</th><th>Model</th><th>Year</th><th>Type</th><th>Rate/Day</th><th>Status</th><th className="text-right">Actions</th></tr></thead>
64
+ <tbody>{data.items.map(v => (
65
+ <tr key={v._id}>
66
+ <td className="font-mono font-semibold">{v.plateNumber}</td>
67
+ <td>{v.brand}</td><td>{v.model}</td><td>{v.year}</td><td>{v.vehicleType}</td><td>{money(v.dailyRate)}</td>
68
+ <td><span className={`badge ${badge(v.status)}`}>{v.status}</span></td>
69
+ <td className="text-right">
70
+ {has('ADMIN') && <button className="btn-ghost" onClick={()=>open(v)}><FiEdit2/></button>}
71
+ {has('ADMIN') && <button className="btn-ghost text-red-600" onClick={()=>setDel(v)}><FiTrash2/></button>}
72
+ </td>
73
+ </tr>
74
+ ))}{!data.items.length && <tr><td colSpan="8" className="text-center text-slate-400 py-8">No vehicles</td></tr>}</tbody>
75
+ </table>
76
+ </div>
77
+
78
+ <div className="flex justify-between items-center mt-4 text-sm">
79
+ <div>Total: {data.total}</div>
80
+ <div className="flex gap-2">
81
+ <button disabled={page<=1} onClick={()=>setPage(p=>p-1)} className="btn-ghost disabled:opacity-30">Prev</button>
82
+ <div className="px-3 py-1.5">{page} / {data.pages}</div>
83
+ <button disabled={page>=data.pages} onClick={()=>setPage(p=>p+1)} className="btn-ghost disabled:opacity-30">Next</button>
84
+ </div>
85
+ </div>
86
+
87
+ <Modal open={modal} onClose={()=>setModal(false)} title={editing ? 'Edit Vehicle' : 'New Vehicle'}>
88
+ <form onSubmit={save} className="grid grid-cols-2 gap-3">
89
+ <div><label className="text-sm">Plate Number</label><input className="input" value={form.plateNumber} onChange={e=>setForm({...form,plateNumber:e.target.value.toUpperCase()})} required/></div>
90
+ <div><label className="text-sm">Brand</label><input className="input" value={form.brand} onChange={e=>setForm({...form,brand:e.target.value})} required/></div>
91
+ <div><label className="text-sm">Model</label><input className="input" value={form.model} onChange={e=>setForm({...form,model:e.target.value})} required/></div>
92
+ <div><label className="text-sm">Year</label><input type="number" className="input" value={form.year} onChange={e=>setForm({...form,year:e.target.value})} required/></div>
93
+ <div><label className="text-sm">Type</label>
94
+ <select className="input" value={form.vehicleType} onChange={e=>setForm({...form,vehicleType:e.target.value})}>{TYPES.map(t=><option key={t}>{t}</option>)}</select>
95
+ </div>
96
+ <div><label className="text-sm">Status</label>
97
+ <select className="input" value={form.status} onChange={e=>setForm({...form,status:e.target.value})}>{STATUS.map(s=><option key={s}>{s}</option>)}</select>
98
+ </div>
99
+ <div><label className="text-sm">Purchase Price ($)</label><input type="number" className="input" value={form.purchasePrice} onChange={e=>setForm({...form,purchasePrice:e.target.value})} required/></div>
100
+ <div><label className="text-sm">Daily Rate ($)</label><input type="number" className="input" value={form.dailyRate} onChange={e=>setForm({...form,dailyRate:e.target.value})} required/></div>
101
+ <div className="col-span-2 flex justify-end gap-2 mt-2">
102
+ <button type="button" className="btn-ghost" onClick={()=>setModal(false)}>Cancel</button>
103
+ <button className="btn-primary">Save</button>
104
+ </div>
105
+ </form>
106
+ </Modal>
107
+ <ConfirmDelete open={!!del} onClose={()=>setDel(null)} onConfirm={remove} label={del?.plateNumber}/>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,16 @@
1
+ import axios from 'axios';
2
+ const api = axios.create({ baseURL: '/api' });
3
+ api.interceptors.request.use((c) => {
4
+ const t = localStorage.getItem('accessToken');
5
+ if (t) c.headers.Authorization = `Bearer ${t}`;
6
+ return c;
7
+ });
8
+ api.interceptors.response.use(r => r, (err) => {
9
+ if (err.response?.status === 401) {
10
+ localStorage.removeItem('accessToken');
11
+ localStorage.removeItem('user');
12
+ if (!location.pathname.includes('/login')) location.href = '/login';
13
+ }
14
+ return Promise.reject(err);
15
+ });
16
+ export default api;
@@ -0,0 +1,20 @@
1
+ export const money = (n) => `$${(n||0).toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
2
+ export const date = (d) => d ? new Date(d).toLocaleDateString() : '-';
3
+ export const badge = (s) => {
4
+ const m = {
5
+ AVAILABLE: 'bg-emerald-100 text-emerald-700',
6
+ RESERVED: 'bg-amber-100 text-amber-700',
7
+ RENTED: 'bg-blue-100 text-blue-700',
8
+ MAINTENANCE: 'bg-slate-200 text-slate-700',
9
+ PENDING: 'bg-amber-100 text-amber-700',
10
+ APPROVED: 'bg-blue-100 text-blue-700',
11
+ REJECTED: 'bg-red-100 text-red-700',
12
+ CANCELLED: 'bg-slate-200 text-slate-700',
13
+ COMPLETED: 'bg-emerald-100 text-emerald-700',
14
+ ONGOING: 'bg-blue-100 text-blue-700',
15
+ RETURNED: 'bg-emerald-100 text-emerald-700',
16
+ LATE: 'bg-red-100 text-red-700',
17
+ NOT_STARTED: 'bg-slate-100 text-slate-700'
18
+ };
19
+ return m[s] || 'bg-slate-100 text-slate-700';
20
+ };
@@ -0,0 +1,18 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ['./index.html','./src/**/*.{js,jsx}'],
4
+ darkMode: 'class',
5
+ theme: {
6
+ extend: {
7
+ colors: {
8
+ primary: '#1E293B',
9
+ secondary: '#334155',
10
+ accent: '#2563EB',
11
+ bglight: '#F8FAFC',
12
+ bgdark: '#0F172A'
13
+ },
14
+ fontFamily: { sans: ['Inter','system-ui','sans-serif'] }
15
+ }
16
+ },
17
+ plugins: []
18
+ };
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ export default defineConfig({
4
+ plugins: [react()],
5
+ server: {
6
+ port: 5173,
7
+ proxy: { '/api': 'http://localhost:5000' }
8
+ }
9
+ });