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,32 @@
1
+ {
2
+ "name": "vrs-frontend",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "axios": "^1.6.5",
13
+ "framer-motion": "^11.0.3",
14
+ "jspdf": "^2.5.1",
15
+ "jspdf-autotable": "^3.8.1",
16
+ "react": "^18.2.0",
17
+ "react-dom": "^18.2.0",
18
+ "react-hook-form": "^7.49.3",
19
+ "react-icons": "^5.0.1",
20
+ "react-router-dom": "^6.21.3",
21
+ "react-toastify": "^10.0.4",
22
+ "recharts": "^2.10.4",
23
+ "xlsx": "^0.18.5"
24
+ },
25
+ "devDependencies": {
26
+ "@vitejs/plugin-react": "^4.2.1",
27
+ "autoprefixer": "^10.4.17",
28
+ "postcss": "^8.4.33",
29
+ "tailwindcss": "^3.4.1",
30
+ "vite": "^5.0.12"
31
+ }
32
+ }
@@ -0,0 +1 @@
1
+ export default { plugins: { tailwindcss: {}, autoprefixer: {} } };
@@ -0,0 +1,33 @@
1
+ import { Routes, Route, Navigate } from 'react-router-dom';
2
+ import Login from './pages/Login';
3
+ import Register from './pages/Register';
4
+ import Dashboard from './pages/Dashboard';
5
+ import Customers from './pages/Customers';
6
+ import Vehicles from './pages/Vehicles';
7
+ import Reservations from './pages/Reservations';
8
+ import Rentals from './pages/Rentals';
9
+ import Reports from './pages/Reports';
10
+ import Users from './pages/Users';
11
+ import Settings from './pages/Settings';
12
+ import AppLayout from './layouts/AppLayout';
13
+ import ProtectedRoute from './components/ProtectedRoute';
14
+
15
+ export default function App() {
16
+ return (
17
+ <Routes>
18
+ <Route path="/login" element={<Login />} />
19
+ <Route path="/register" element={<Register />} />
20
+ <Route element={<ProtectedRoute><AppLayout /></ProtectedRoute>}>
21
+ <Route path="/dashboard" element={<Dashboard />} />
22
+ <Route path="/customers" element={<Customers />} />
23
+ <Route path="/vehicles" element={<Vehicles />} />
24
+ <Route path="/reservations" element={<Reservations />} />
25
+ <Route path="/rentals" element={<Rentals />} />
26
+ <Route path="/reports" element={<Reports />} />
27
+ <Route path="/users" element={<ProtectedRoute roles={['ADMIN']}><Users /></ProtectedRoute>} />
28
+ <Route path="/settings" element={<Settings />} />
29
+ </Route>
30
+ <Route path="*" element={<Navigate to="/dashboard" replace />} />
31
+ </Routes>
32
+ );
33
+ }
@@ -0,0 +1,12 @@
1
+ import Modal from './Modal';
2
+ export default function ConfirmDelete({ open, onClose, onConfirm, label }) {
3
+ return (
4
+ <Modal open={open} onClose={onClose} title="Confirm Delete">
5
+ <p className="mb-4">Are you sure you want to delete <b>{label}</b>?</p>
6
+ <div className="flex justify-end gap-2">
7
+ <button className="btn-ghost" onClick={onClose}>Cancel</button>
8
+ <button className="btn-danger" onClick={onConfirm}>Delete</button>
9
+ </div>
10
+ </Modal>
11
+ );
12
+ }
@@ -0,0 +1,22 @@
1
+ import { FiX } from 'react-icons/fi';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+
4
+ export default function Modal({ open, onClose, title, children, width='max-w-lg' }) {
5
+ return (
6
+ <AnimatePresence>
7
+ {open && (
8
+ <motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
9
+ className="fixed inset-0 z-50 bg-black/50 grid place-items-center p-4" onClick={onClose}>
10
+ <motion.div initial={{scale:0.95, y:10}} animate={{scale:1, y:0}} exit={{scale:0.95}}
11
+ className={`bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full ${width}`} onClick={e=>e.stopPropagation()}>
12
+ <div className="flex items-center justify-between border-b border-slate-200 dark:border-slate-700 p-4">
13
+ <h3 className="font-semibold">{title}</h3>
14
+ <button onClick={onClose} className="btn-ghost"><FiX/></button>
15
+ </div>
16
+ <div className="p-5">{children}</div>
17
+ </motion.div>
18
+ </motion.div>
19
+ )}
20
+ </AnimatePresence>
21
+ );
22
+ }
@@ -0,0 +1,9 @@
1
+ import { Navigate } from 'react-router-dom';
2
+ import { useAuth } from '../context/AuthContext';
3
+
4
+ export default function ProtectedRoute({ children, roles }) {
5
+ const { user } = useAuth();
6
+ if (!user) return <Navigate to="/login" replace />;
7
+ if (roles && !roles.includes(user.role)) return <Navigate to="/dashboard" replace />;
8
+ return children;
9
+ }
@@ -0,0 +1,42 @@
1
+ import { NavLink } from 'react-router-dom';
2
+ import { FiHome, FiUsers, FiTruck, FiCalendar, FiKey, FiBarChart2, FiUser, FiSettings, FiLogOut } from 'react-icons/fi';
3
+ import { useAuth } from '../context/AuthContext';
4
+
5
+ const items = [
6
+ { to: '/dashboard', label: 'Dashboard', icon: FiHome },
7
+ { to: '/customers', label: 'Customers', icon: FiUsers },
8
+ { to: '/vehicles', label: 'Vehicles', icon: FiTruck },
9
+ { to: '/reservations', label: 'Reservations', icon: FiCalendar },
10
+ { to: '/rentals', label: 'Rentals', icon: FiKey },
11
+ { to: '/reports', label: 'Reports', icon: FiBarChart2 },
12
+ { to: '/users', label: 'Users', icon: FiUser, roles: ['ADMIN'] },
13
+ { to: '/settings', label: 'Settings', icon: FiSettings }
14
+ ];
15
+
16
+ export default function Sidebar({ open }) {
17
+ const { user, logout } = useAuth();
18
+ return (
19
+ <aside className={`${open ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0 fixed lg:static z-30 top-0 left-0 h-full w-64 transition-transform`}>
20
+ <div className="h-full glass m-0 lg:m-3 lg:rounded-2xl p-5 flex flex-col">
21
+ <div className="flex items-center gap-3 mb-8 px-2">
22
+ <div className="w-10 h-10 rounded-xl bg-accent text-white grid place-items-center font-bold">SW</div>
23
+ <div>
24
+ <div className="font-bold text-primary dark:text-white">SwiftWheels</div>
25
+ <div className="text-xs text-slate-500">VRS · Huye</div>
26
+ </div>
27
+ </div>
28
+ <nav className="flex-1 space-y-1">
29
+ {items.filter(i => !i.roles || i.roles.includes(user?.role)).map(it => (
30
+ <NavLink key={it.to} to={it.to}
31
+ className={({isActive}) => `flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition ${isActive ? 'bg-accent text-white shadow' : 'text-slate-600 dark:text-slate-300 hover:bg-slate-200/50 dark:hover:bg-slate-700/50'}`}>
32
+ <it.icon /> {it.label}
33
+ </NavLink>
34
+ ))}
35
+ </nav>
36
+ <button onClick={logout} className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20">
37
+ <FiLogOut /> Logout
38
+ </button>
39
+ </div>
40
+ </aside>
41
+ );
42
+ }
@@ -0,0 +1,24 @@
1
+ import { FiMenu, FiMoon, FiSun, FiBell } from 'react-icons/fi';
2
+ import { useTheme } from '../context/ThemeContext';
3
+ import { useAuth } from '../context/AuthContext';
4
+
5
+ export default function Topbar({ onMenu }) {
6
+ const { theme, toggle } = useTheme();
7
+ const { user } = useAuth();
8
+ return (
9
+ <header className="glass m-3 mb-0 rounded-2xl px-5 py-3 flex items-center justify-between">
10
+ <div className="flex items-center gap-3">
11
+ <button className="lg:hidden btn-ghost" onClick={onMenu}><FiMenu /></button>
12
+ <div>
13
+ <div className="text-xs text-slate-500">Welcome back,</div>
14
+ <div className="font-semibold">{user?.fullName || user?.username}</div>
15
+ </div>
16
+ </div>
17
+ <div className="flex items-center gap-2">
18
+ <button className="btn-ghost" title="Notifications"><FiBell /></button>
19
+ <button className="btn-ghost" onClick={toggle} title="Toggle theme">{theme === 'dark' ? <FiSun/> : <FiMoon/>}</button>
20
+ <div className="w-9 h-9 rounded-full bg-gradient-to-br from-accent to-primary text-white grid place-items-center font-bold">{(user?.username||'U')[0].toUpperCase()}</div>
21
+ </div>
22
+ </header>
23
+ );
24
+ }
@@ -0,0 +1,30 @@
1
+ import { createContext, useContext, useEffect, useState } from 'react';
2
+ import api from '../services/api';
3
+
4
+ const AuthContext = createContext();
5
+ export const useAuth = () => useContext(AuthContext);
6
+
7
+ export function AuthProvider({ children }) {
8
+ const [user, setUser] = useState(() => {
9
+ try { return JSON.parse(localStorage.getItem('user')); } catch { return null; }
10
+ });
11
+ const [loading, setLoading] = useState(false);
12
+
13
+ const login = async (username, password) => {
14
+ const { data } = await api.post('/auth/login', { username, password });
15
+ localStorage.setItem('accessToken', data.accessToken);
16
+ localStorage.setItem('refreshToken', data.refreshToken);
17
+ localStorage.setItem('user', JSON.stringify(data.user));
18
+ setUser(data.user);
19
+ return data.user;
20
+ };
21
+ const logout = () => {
22
+ localStorage.removeItem('accessToken');
23
+ localStorage.removeItem('refreshToken');
24
+ localStorage.removeItem('user');
25
+ setUser(null);
26
+ };
27
+ const has = (...roles) => user && roles.includes(user.role);
28
+
29
+ return <AuthContext.Provider value={{ user, login, logout, loading, has }}>{children}</AuthContext.Provider>;
30
+ }
@@ -0,0 +1,13 @@
1
+ import { createContext, useContext, useEffect, useState } from 'react';
2
+ const ThemeContext = createContext();
3
+ export const useTheme = () => useContext(ThemeContext);
4
+
5
+ export function ThemeProvider({ children }) {
6
+ const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light');
7
+ useEffect(() => {
8
+ document.documentElement.classList.toggle('dark', theme === 'dark');
9
+ localStorage.setItem('theme', theme);
10
+ }, [theme]);
11
+ const toggle = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
12
+ return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>;
13
+ }
@@ -0,0 +1,37 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ html, body, #root { height: 100%; }
6
+ body {
7
+ @apply font-sans bg-bglight text-slate-800 transition-colors;
8
+ }
9
+ .dark body { @apply bg-bgdark text-slate-100; }
10
+
11
+ .glass {
12
+ @apply bg-white/70 backdrop-blur-xl border border-white/40 shadow-lg;
13
+ }
14
+ .dark .glass { @apply bg-slate-800/60 border-slate-700/50; }
15
+
16
+ .card { @apply glass rounded-2xl p-5; }
17
+ .input {
18
+ @apply w-full px-4 py-2.5 rounded-lg border border-slate-300 bg-white/80 text-sm
19
+ focus:outline-none focus:ring-2 focus:ring-accent transition;
20
+ }
21
+ .dark .input { @apply bg-slate-800 border-slate-700 text-slate-100; }
22
+ .btn { @apply inline-flex items-center gap-2 px-4 py-2 rounded-lg font-medium text-sm transition; }
23
+ .btn-primary { @apply btn bg-accent text-white hover:bg-blue-700 shadow; }
24
+ .btn-ghost { @apply btn text-slate-600 hover:bg-slate-200/60; }
25
+ .dark .btn-ghost { @apply text-slate-300 hover:bg-slate-700/60; }
26
+ .btn-danger { @apply btn bg-red-600 text-white hover:bg-red-700; }
27
+ .btn-success { @apply btn bg-emerald-600 text-white hover:bg-emerald-700; }
28
+
29
+ table { @apply w-full text-sm; }
30
+ thead th { @apply text-left font-semibold py-3 px-3 text-slate-500 uppercase tracking-wider text-xs border-b border-slate-200; }
31
+ .dark thead th { @apply text-slate-400 border-slate-700; }
32
+ tbody td { @apply py-3 px-3 border-b border-slate-100; }
33
+ .dark tbody td { @apply border-slate-700/50; }
34
+ tbody tr:hover { @apply bg-slate-50; }
35
+ .dark tbody tr:hover { @apply bg-slate-700/30; }
36
+
37
+ .badge { @apply inline-block px-2.5 py-1 rounded-full text-xs font-semibold; }
@@ -0,0 +1,18 @@
1
+ import { useState } from 'react';
2
+ import { Outlet } from 'react-router-dom';
3
+ import Sidebar from '../components/Sidebar';
4
+ import Topbar from '../components/Topbar';
5
+
6
+ export default function AppLayout() {
7
+ const [open, setOpen] = useState(false);
8
+ return (
9
+ <div className="min-h-screen flex">
10
+ <Sidebar open={open} />
11
+ {open && <div onClick={() => setOpen(false)} className="fixed inset-0 bg-black/40 z-20 lg:hidden" />}
12
+ <div className="flex-1 flex flex-col min-w-0">
13
+ <Topbar onMenu={() => setOpen(true)} />
14
+ <main className="flex-1 p-3 overflow-auto"><div className="glass rounded-2xl p-5 min-h-full"><Outlet /></div></main>
15
+ </div>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { BrowserRouter } from 'react-router-dom';
4
+ import { ToastContainer } from 'react-toastify';
5
+ import 'react-toastify/dist/ReactToastify.css';
6
+ import App from './App';
7
+ import { AuthProvider } from './context/AuthContext';
8
+ import { ThemeProvider } from './context/ThemeContext';
9
+ import './index.css';
10
+
11
+ ReactDOM.createRoot(document.getElementById('root')).render(
12
+ <ThemeProvider>
13
+ <AuthProvider>
14
+ <BrowserRouter>
15
+ <App />
16
+ <ToastContainer position="top-right" autoClose={2500} theme="colored" />
17
+ </BrowserRouter>
18
+ </AuthProvider>
19
+ </ThemeProvider>
20
+ );
@@ -0,0 +1,95 @@
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 { useAuth } from '../context/AuthContext';
8
+
9
+ const empty = { fullName:'', nationalId:'', phone:'', email:'', address:'' };
10
+
11
+ export default function Customers() {
12
+ const { has } = useAuth();
13
+ const [data, setData] = useState({ items: [], total: 0, pages: 1 });
14
+ const [search, setSearch] = useState('');
15
+ const [page, setPage] = useState(1);
16
+ const [modal, setModal] = useState(false);
17
+ const [form, setForm] = useState(empty);
18
+ const [editing, setEditing] = useState(null);
19
+ const [del, setDel] = useState(null);
20
+
21
+ const load = async () => {
22
+ const { data } = await api.get('/customers', { params: { search, page, limit: 10 } });
23
+ setData(data);
24
+ };
25
+ useEffect(() => { load(); }, [search, page]);
26
+
27
+ const open = (c) => { setEditing(c); setForm(c || empty); setModal(true); };
28
+ const save = async (e) => {
29
+ e.preventDefault();
30
+ try {
31
+ if (editing) await api.put(`/customers/${editing._id}`, form);
32
+ else await api.post('/customers', form);
33
+ toast.success('Saved'); setModal(false); load();
34
+ } catch (err) { toast.error(err.response?.data?.message || 'Error'); }
35
+ };
36
+ const remove = async () => {
37
+ try { await api.delete(`/customers/${del._id}`); toast.success('Deleted'); setDel(null); load(); }
38
+ catch (err) { toast.error('Cannot delete'); }
39
+ };
40
+
41
+ return (
42
+ <div>
43
+ <div className="flex flex-wrap items-center justify-between gap-3 mb-4">
44
+ <h1 className="text-2xl font-bold">Customers</h1>
45
+ <div className="flex gap-2">
46
+ <div className="relative"><FiSearch className="absolute left-3 top-3 text-slate-400"/>
47
+ <input className="input pl-10 w-64" placeholder="Search..." value={search} onChange={e=>{setPage(1);setSearch(e.target.value);}}/>
48
+ </div>
49
+ {has('ADMIN','STAFF') && <button className="btn-primary" onClick={()=>open(null)}><FiPlus/> Add Customer</button>}
50
+ </div>
51
+ </div>
52
+
53
+ <div className="overflow-x-auto">
54
+ <table>
55
+ <thead><tr><th>Full Name</th><th>National ID</th><th>Phone</th><th>Email</th><th>Address</th><th className="text-right">Actions</th></tr></thead>
56
+ <tbody>{data.items.map(c => (
57
+ <tr key={c._id}>
58
+ <td className="font-medium">{c.fullName}</td>
59
+ <td>{c.nationalId}</td><td>{c.phone}</td><td>{c.email}</td><td>{c.address}</td>
60
+ <td className="text-right">
61
+ {has('ADMIN','STAFF') && <button className="btn-ghost" onClick={()=>open(c)}><FiEdit2/></button>}
62
+ {has('ADMIN') && <button className="btn-ghost text-red-600" onClick={()=>setDel(c)}><FiTrash2/></button>}
63
+ </td>
64
+ </tr>
65
+ ))}{!data.items.length && <tr><td colSpan="6" className="text-center text-slate-400 py-8">No customers</td></tr>}</tbody>
66
+ </table>
67
+ </div>
68
+
69
+ <div className="flex justify-between items-center mt-4 text-sm">
70
+ <div>Total: {data.total}</div>
71
+ <div className="flex gap-2">
72
+ <button disabled={page<=1} onClick={()=>setPage(p=>p-1)} className="btn-ghost disabled:opacity-30">Prev</button>
73
+ <div className="px-3 py-1.5">{page} / {data.pages}</div>
74
+ <button disabled={page>=data.pages} onClick={()=>setPage(p=>p+1)} className="btn-ghost disabled:opacity-30">Next</button>
75
+ </div>
76
+ </div>
77
+
78
+ <Modal open={modal} onClose={()=>setModal(false)} title={editing ? 'Edit Customer' : 'New Customer'}>
79
+ <form onSubmit={save} className="grid grid-cols-2 gap-3">
80
+ {[['fullName','Full Name'],['nationalId','National ID'],['phone','Phone'],['email','Email'],['address','Address']].map(([k,l]) => (
81
+ <div key={k} className={k==='address' ? 'col-span-2' : ''}>
82
+ <label className="text-sm">{l}</label>
83
+ <input className="input" value={form[k]} onChange={e=>setForm({...form,[k]:e.target.value})} required/>
84
+ </div>
85
+ ))}
86
+ <div className="col-span-2 flex justify-end gap-2 mt-2">
87
+ <button type="button" className="btn-ghost" onClick={()=>setModal(false)}>Cancel</button>
88
+ <button className="btn-primary">Save</button>
89
+ </div>
90
+ </form>
91
+ </Modal>
92
+ <ConfirmDelete open={!!del} onClose={()=>setDel(null)} onConfirm={remove} label={del?.fullName}/>
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,84 @@
1
+ import { useEffect, useState } from 'react';
2
+ import api from '../services/api';
3
+ import { FiTruck, FiUsers, FiCalendar, FiDollarSign } from 'react-icons/fi';
4
+ import { LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend } from 'recharts';
5
+ import { money, date, badge } from '../utils/format';
6
+
7
+ const COLORS = ['#2563EB','#1E293B','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899'];
8
+
9
+ function Stat({ icon: Icon, label, value, color }) {
10
+ return (
11
+ <div className="card flex items-center gap-4">
12
+ <div className={`w-12 h-12 rounded-xl grid place-items-center ${color} text-white`}><Icon size={22}/></div>
13
+ <div><div className="text-xs text-slate-500 uppercase">{label}</div><div className="text-2xl font-bold">{value}</div></div>
14
+ </div>
15
+ );
16
+ }
17
+
18
+ export default function Dashboard() {
19
+ const [s, setS] = useState(null);
20
+ useEffect(() => { api.get('/dashboard').then(r => setS(r.data)); }, []);
21
+ if (!s) return <div className="text-center text-slate-500">Loading dashboard...</div>;
22
+
23
+ return (
24
+ <div className="space-y-5">
25
+ <h1 className="text-2xl font-bold">Dashboard</h1>
26
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
27
+ <Stat icon={FiTruck} label="Total Vehicles" value={s.totalVehicles} color="bg-accent"/>
28
+ <Stat icon={FiTruck} label="Available" value={s.available} color="bg-emerald-600"/>
29
+ <Stat icon={FiCalendar} label="Reserved" value={s.reserved} color="bg-amber-500"/>
30
+ <Stat icon={FiTruck} label="Rented" value={s.rented} color="bg-blue-600"/>
31
+ <Stat icon={FiUsers} label="Customers" value={s.customers} color="bg-primary"/>
32
+ <Stat icon={FiCalendar} label="Reservations" value={s.reservations} color="bg-secondary"/>
33
+ <Stat icon={FiDollarSign} label="Total Revenue" value={money(s.totalRevenue)} color="bg-emerald-700"/>
34
+ <Stat icon={FiDollarSign} label="Monthly Revenue" value={money(s.monthlyRevenue)} color="bg-blue-700"/>
35
+ </div>
36
+
37
+ <div className="grid lg:grid-cols-3 gap-4">
38
+ <div className="card lg:col-span-2">
39
+ <h3 className="font-semibold mb-3">Revenue — Last 6 Months</h3>
40
+ <ResponsiveContainer width="100%" height={280}>
41
+ <LineChart data={s.monthlyChart}>
42
+ <CartesianGrid strokeDasharray="3 3" opacity={0.3}/>
43
+ <XAxis dataKey="month"/><YAxis/><Tooltip/><Legend/>
44
+ <Line type="monotone" dataKey="revenue" stroke="#2563EB" strokeWidth={3}/>
45
+ <Line type="monotone" dataKey="count" stroke="#1E293B" strokeWidth={2}/>
46
+ </LineChart>
47
+ </ResponsiveContainer>
48
+ </div>
49
+ <div className="card">
50
+ <h3 className="font-semibold mb-3">Vehicle Types</h3>
51
+ <ResponsiveContainer width="100%" height={280}>
52
+ <PieChart>
53
+ <Pie data={s.vehicleTypes} dataKey="count" nameKey="type" outerRadius={90} label>
54
+ {s.vehicleTypes.map((_, i) => <Cell key={i} fill={COLORS[i % COLORS.length]}/>)}
55
+ </Pie>
56
+ <Tooltip/>
57
+ </PieChart>
58
+ </ResponsiveContainer>
59
+ </div>
60
+ </div>
61
+
62
+ <div className="grid lg:grid-cols-2 gap-4">
63
+ <div className="card">
64
+ <h3 className="font-semibold mb-3">Recent Reservations</h3>
65
+ <table><thead><tr><th>Customer</th><th>Vehicle</th><th>Status</th></tr></thead>
66
+ <tbody>{s.recentReservations.map(r => (
67
+ <tr key={r._id}><td>{r.customerId?.fullName}</td><td>{r.vehicleId?.plateNumber}</td>
68
+ <td><span className={`badge ${badge(r.reservationStatus)}`}>{r.reservationStatus}</span></td></tr>
69
+ ))}</tbody>
70
+ </table>
71
+ </div>
72
+ <div className="card">
73
+ <h3 className="font-semibold mb-3">Recent Rentals</h3>
74
+ <table><thead><tr><th>Customer</th><th>Vehicle</th><th>Date</th><th>Fee</th></tr></thead>
75
+ <tbody>{s.recentRentals.map(r => (
76
+ <tr key={r._id}><td>{r.customerId?.fullName}</td><td>{r.vehicleId?.plateNumber}</td>
77
+ <td>{date(r.rentalDate)}</td><td>{money(r.rentalFee)}</td></tr>
78
+ ))}</tbody>
79
+ </table>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,54 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate, Link } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+ import { toast } from 'react-toastify';
5
+ import { motion } from 'framer-motion';
6
+ import { FiLock, FiUser } from 'react-icons/fi';
7
+
8
+ export default function Login() {
9
+ const { login } = useAuth();
10
+ const nav = useNavigate();
11
+ const [form, setForm] = useState({ username: '', password: '' });
12
+ const [loading, setLoading] = useState(false);
13
+
14
+ const submit = async (e) => {
15
+ e.preventDefault();
16
+ setLoading(true);
17
+ try {
18
+ await login(form.username, form.password);
19
+ toast.success('Welcome back!');
20
+ nav('/dashboard');
21
+ } catch (err) {
22
+ toast.error(err.response?.data?.message || 'Login failed');
23
+ } finally { setLoading(false); }
24
+ };
25
+
26
+ return (
27
+ <div className="min-h-screen grid lg:grid-cols-2 bg-gradient-to-br from-slate-100 to-slate-200 dark:from-bgdark dark:to-slate-900">
28
+ <div className="hidden lg:flex items-center justify-center bg-gradient-to-br from-primary to-accent text-white p-12">
29
+ <div className="max-w-md">
30
+ <div className="text-5xl font-extrabold mb-4">SwiftWheels</div>
31
+ <div className="text-xl opacity-90 mb-6">Vehicle Rental & Reservation System</div>
32
+ <p className="opacity-80">Manage rentals, reservations, customers, and vehicles — securely and efficiently. Huye City, Rwanda.</p>
33
+ </div>
34
+ </div>
35
+ <div className="flex items-center justify-center p-6">
36
+ <motion.form initial={{ opacity:0, y:20 }} animate={{ opacity:1, y:0 }} onSubmit={submit} className="card w-full max-w-md">
37
+ <h1 className="text-2xl font-bold mb-1">Sign in</h1>
38
+ <p className="text-slate-500 text-sm mb-6">Access your VRS account</p>
39
+ <label className="text-sm font-medium">Username</label>
40
+ <div className="relative my-2"><FiUser className="absolute left-3 top-3 text-slate-400"/>
41
+ <input className="input pl-10" value={form.username} onChange={e=>setForm({...form, username:e.target.value})} required/>
42
+ </div>
43
+ <label className="text-sm font-medium">Password</label>
44
+ <div className="relative my-2"><FiLock className="absolute left-3 top-3 text-slate-400"/>
45
+ <input type="password" className="input pl-10" value={form.password} onChange={e=>setForm({...form, password:e.target.value})} required/>
46
+ </div>
47
+ <button disabled={loading} className="btn-primary w-full justify-center mt-4">{loading ? 'Signing in...' : 'Sign in'}</button>
48
+ <div className="mt-4 text-xs text-slate-500">Demo: admin/admin123 · manager/manager123 · staff/staff123</div>
49
+ <div className="mt-2 text-sm">No account? <Link to="/register" className="text-accent">Register</Link></div>
50
+ </motion.form>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,36 @@
1
+ import { useState } from 'react';
2
+ import api from '../services/api';
3
+ import { useNavigate, Link } from 'react-router-dom';
4
+ import { toast } from 'react-toastify';
5
+
6
+ export default function Register() {
7
+ const nav = useNavigate();
8
+ const [form, setForm] = useState({ username: '', password: '', fullName: '', email: '', role: 'STAFF' });
9
+ const submit = async (e) => {
10
+ e.preventDefault();
11
+ try {
12
+ await api.post('/auth/register', form);
13
+ toast.success('Account created. Please login.');
14
+ nav('/login');
15
+ } catch (err) { toast.error(err.response?.data?.message || 'Register failed'); }
16
+ };
17
+ return (
18
+ <div className="min-h-screen grid place-items-center p-6 bg-gradient-to-br from-slate-100 to-slate-200 dark:from-bgdark dark:to-slate-900">
19
+ <form onSubmit={submit} className="card w-full max-w-md">
20
+ <h1 className="text-2xl font-bold mb-4">Create account</h1>
21
+ {['fullName','username','email'].map(k => (
22
+ <div key={k} className="mb-3"><label className="text-sm capitalize">{k}</label>
23
+ <input className="input" value={form[k]} onChange={e=>setForm({...form,[k]:e.target.value})} required/></div>
24
+ ))}
25
+ <div className="mb-3"><label className="text-sm">Password</label>
26
+ <input type="password" className="input" value={form.password} onChange={e=>setForm({...form,password:e.target.value})} required/></div>
27
+ <div className="mb-3"><label className="text-sm">Role</label>
28
+ <select className="input" value={form.role} onChange={e=>setForm({...form,role:e.target.value})}>
29
+ <option>STAFF</option><option>MANAGER</option><option>ADMIN</option>
30
+ </select></div>
31
+ <button className="btn-primary w-full justify-center">Register</button>
32
+ <div className="mt-3 text-sm">Have an account? <Link to="/login" className="text-accent">Sign in</Link></div>
33
+ </form>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,58 @@
1
+ import { useEffect, useState } from 'react';
2
+ import api from '../services/api';
3
+ import { toast } from 'react-toastify';
4
+ import { FiRotateCcw, FiSearch } from 'react-icons/fi';
5
+ import { date, money, badge } from '../utils/format';
6
+ import { useAuth } from '../context/AuthContext';
7
+
8
+ export default function Rentals() {
9
+ const { has } = useAuth();
10
+ const [items, setItems] = useState([]);
11
+ const [search, setSearch] = useState('');
12
+
13
+ const load = async () => {
14
+ const { data } = await api.get('/reservations', { params: { limit: 200 } });
15
+ setItems(data.items.filter(r => r.rentalDate));
16
+ };
17
+ useEffect(() => { load(); }, []);
18
+
19
+ const ret = async (id) => {
20
+ try { await api.patch(`/reservations/${id}/return`); toast.success('Vehicle returned'); load(); }
21
+ catch (err) { toast.error(err.response?.data?.message || 'Error'); }
22
+ };
23
+
24
+ const filtered = items.filter(r => {
25
+ const s = search.toLowerCase();
26
+ return !s || (r.customerId?.fullName||'').toLowerCase().includes(s) || (r.vehicleId?.plateNumber||'').toLowerCase().includes(s);
27
+ });
28
+
29
+ return (
30
+ <div>
31
+ <div className="flex flex-wrap items-center justify-between gap-3 mb-4">
32
+ <h1 className="text-2xl font-bold">Rentals</h1>
33
+ <div className="relative"><FiSearch className="absolute left-3 top-3 text-slate-400"/>
34
+ <input className="input pl-10 w-64" placeholder="Search..." value={search} onChange={e=>setSearch(e.target.value)}/>
35
+ </div>
36
+ </div>
37
+ <div className="overflow-x-auto">
38
+ <table>
39
+ <thead><tr><th>Customer</th><th>Vehicle</th><th>Pickup</th><th>Expected Return</th><th>Returned</th><th>Status</th><th>Fee</th><th className="text-right">Actions</th></tr></thead>
40
+ <tbody>{filtered.map(r => (
41
+ <tr key={r._id}>
42
+ <td className="font-medium">{r.customerId?.fullName}</td>
43
+ <td className="font-mono">{r.vehicleId?.plateNumber}</td>
44
+ <td>{date(r.rentalDate)}</td><td>{date(r.endDate)}</td><td>{date(r.returnDate)}</td>
45
+ <td><span className={`badge ${badge(r.rentalStatus)}`}>{r.rentalStatus}</span></td>
46
+ <td>{money(r.rentalFee)}</td>
47
+ <td className="text-right">
48
+ {r.rentalStatus==='ONGOING' && has('ADMIN','STAFF') && (
49
+ <button className="btn-success" onClick={()=>ret(r._id)}><FiRotateCcw/> Return</button>
50
+ )}
51
+ </td>
52
+ </tr>
53
+ ))}{!filtered.length && <tr><td colSpan="8" className="text-center text-slate-400 py-8">No rentals</td></tr>}</tbody>
54
+ </table>
55
+ </div>
56
+ </div>
57
+ );
58
+ }