ostroner 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 (112) hide show
  1. package/bin/create-project.js +57 -0
  2. package/package.json +23 -0
  3. package/templates/stock-chain/backend/.env +9 -0
  4. package/templates/stock-chain/backend/.eslintrc.js +6 -0
  5. package/templates/stock-chain/backend/.prettierrc +14 -0
  6. package/templates/stock-chain/backend/README.md +1 -0
  7. package/templates/stock-chain/backend/package-lock.json +1666 -0
  8. package/templates/stock-chain/backend/package.json +21 -0
  9. package/templates/stock-chain/backend/src/app.js +59 -0
  10. package/templates/stock-chain/backend/src/config/db.js +29 -0
  11. package/templates/stock-chain/backend/src/controllers/auth.js +31 -0
  12. package/templates/stock-chain/backend/src/controllers/bookingcontroller.js +65 -0
  13. package/templates/stock-chain/backend/src/controllers/buscontroller.js +59 -0
  14. package/templates/stock-chain/backend/src/controllers/deliverycontroller.js +35 -0
  15. package/templates/stock-chain/backend/src/controllers/schedulecontroller.js +85 -0
  16. package/templates/stock-chain/backend/src/controllers/shipmentcontroller.js +35 -0
  17. package/templates/stock-chain/backend/src/controllers/suppliercontroller.js +35 -0
  18. package/templates/stock-chain/backend/src/controllers/usercontroller.js +59 -0
  19. package/templates/stock-chain/backend/src/middleware/auth.js +11 -0
  20. package/templates/stock-chain/backend/src/middleware/errorHandler.js +7 -0
  21. package/templates/stock-chain/backend/src/models/delivery.js +20 -0
  22. package/templates/stock-chain/backend/src/models/index.js +6 -0
  23. package/templates/stock-chain/backend/src/models/shipment.js +20 -0
  24. package/templates/stock-chain/backend/src/models/supplier.js +17 -0
  25. package/templates/stock-chain/backend/src/models/user.js +15 -0
  26. package/templates/stock-chain/backend/src/routes/authroute.js +11 -0
  27. package/templates/stock-chain/backend/src/routes/bookingroute.js +18 -0
  28. package/templates/stock-chain/backend/src/routes/busroute.js +18 -0
  29. package/templates/stock-chain/backend/src/routes/deliveriesroute.js +18 -0
  30. package/templates/stock-chain/backend/src/routes/scheduleroute.js +18 -0
  31. package/templates/stock-chain/backend/src/routes/shipmentsroute.js +18 -0
  32. package/templates/stock-chain/backend/src/routes/suppliersroute.js +18 -0
  33. package/templates/stock-chain/backend/src/routes/userroute.js +18 -0
  34. package/templates/stock-chain/frontend/.env +1 -0
  35. package/templates/stock-chain/frontend/README.md +16 -0
  36. package/templates/stock-chain/frontend/eslint.config.js +21 -0
  37. package/templates/stock-chain/frontend/index.html +13 -0
  38. package/templates/stock-chain/frontend/package-lock.json +3131 -0
  39. package/templates/stock-chain/frontend/package.json +33 -0
  40. package/templates/stock-chain/frontend/public/favicon.svg +1 -0
  41. package/templates/stock-chain/frontend/public/icons.svg +24 -0
  42. package/templates/stock-chain/frontend/src/App.jsx +55 -0
  43. package/templates/stock-chain/frontend/src/assets/hero.png +0 -0
  44. package/templates/stock-chain/frontend/src/assets/react.svg +1 -0
  45. package/templates/stock-chain/frontend/src/assets/vite.svg +1 -0
  46. package/templates/stock-chain/frontend/src/components/Button.jsx +15 -0
  47. package/templates/stock-chain/frontend/src/components/Input.jsx +25 -0
  48. package/templates/stock-chain/frontend/src/context/AuthContext.jsx +59 -0
  49. package/templates/stock-chain/frontend/src/index.css +7 -0
  50. package/templates/stock-chain/frontend/src/main.jsx +18 -0
  51. package/templates/stock-chain/frontend/src/pages/AppLayout.jsx +125 -0
  52. package/templates/stock-chain/frontend/src/pages/Login.jsx +78 -0
  53. package/templates/stock-chain/frontend/src/pages/ManagerDeliveries.jsx +113 -0
  54. package/templates/stock-chain/frontend/src/pages/ManagerShipments.jsx +113 -0
  55. package/templates/stock-chain/frontend/src/pages/ManagerSuppliers.jsx +122 -0
  56. package/templates/stock-chain/frontend/src/pages/Register.jsx +60 -0
  57. package/templates/stock-chain/frontend/src/services/api.js +8 -0
  58. package/templates/stock-chain/frontend/src/services/authService.js +11 -0
  59. package/templates/stock-chain/frontend/vite.config.js +8 -0
  60. package/templates/y-bus/backend/.env +9 -0
  61. package/templates/y-bus/backend/.eslintrc.js +6 -0
  62. package/templates/y-bus/backend/.prettierrc +14 -0
  63. package/templates/y-bus/backend/README.md +1 -0
  64. package/templates/y-bus/backend/package-lock.json +1666 -0
  65. package/templates/y-bus/backend/package.json +21 -0
  66. package/templates/y-bus/backend/src/app.js +44 -0
  67. package/templates/y-bus/backend/src/config/db.js +29 -0
  68. package/templates/y-bus/backend/src/controllers/auth.js +23 -0
  69. package/templates/y-bus/backend/src/controllers/bookingcontroller.js +65 -0
  70. package/templates/y-bus/backend/src/controllers/buscontroller.js +59 -0
  71. package/templates/y-bus/backend/src/controllers/schedulecontroller.js +85 -0
  72. package/templates/y-bus/backend/src/controllers/usercontroller.js +59 -0
  73. package/templates/y-bus/backend/src/middleware/auth.js +11 -0
  74. package/templates/y-bus/backend/src/middleware/errorHandler.js +7 -0
  75. package/templates/y-bus/backend/src/models/booking.js +27 -0
  76. package/templates/y-bus/backend/src/models/bus.js +16 -0
  77. package/templates/y-bus/backend/src/models/index.js +15 -0
  78. package/templates/y-bus/backend/src/models/schedule.js +25 -0
  79. package/templates/y-bus/backend/src/models/user.js +20 -0
  80. package/templates/y-bus/backend/src/routes/bookingroute.js +18 -0
  81. package/templates/y-bus/backend/src/routes/busroute.js +18 -0
  82. package/templates/y-bus/backend/src/routes/scheduleroute.js +18 -0
  83. package/templates/y-bus/backend/src/routes/userroute.js +18 -0
  84. package/templates/y-bus/frontend/.env +1 -0
  85. package/templates/y-bus/frontend/README.md +16 -0
  86. package/templates/y-bus/frontend/eslint.config.js +21 -0
  87. package/templates/y-bus/frontend/index.html +13 -0
  88. package/templates/y-bus/frontend/package-lock.json +3131 -0
  89. package/templates/y-bus/frontend/package.json +33 -0
  90. package/templates/y-bus/frontend/public/favicon.svg +1 -0
  91. package/templates/y-bus/frontend/public/icons.svg +24 -0
  92. package/templates/y-bus/frontend/src/App.jsx +108 -0
  93. package/templates/y-bus/frontend/src/assets/hero.png +0 -0
  94. package/templates/y-bus/frontend/src/assets/react.svg +1 -0
  95. package/templates/y-bus/frontend/src/assets/vite.svg +1 -0
  96. package/templates/y-bus/frontend/src/components/Button.jsx +15 -0
  97. package/templates/y-bus/frontend/src/components/Input.jsx +25 -0
  98. package/templates/y-bus/frontend/src/context/AuthContext.jsx +59 -0
  99. package/templates/y-bus/frontend/src/index.css +7 -0
  100. package/templates/y-bus/frontend/src/main.jsx +18 -0
  101. package/templates/y-bus/frontend/src/pages/AppLayout.jsx +135 -0
  102. package/templates/y-bus/frontend/src/pages/CustomerTrips.jsx +101 -0
  103. package/templates/y-bus/frontend/src/pages/Login.jsx +81 -0
  104. package/templates/y-bus/frontend/src/pages/ManagerBuses.jsx +140 -0
  105. package/templates/y-bus/frontend/src/pages/ManagerDashboard.jsx +108 -0
  106. package/templates/y-bus/frontend/src/pages/ManagerReport.jsx +89 -0
  107. package/templates/y-bus/frontend/src/pages/ManagerSchedules.jsx +233 -0
  108. package/templates/y-bus/frontend/src/pages/MyBookings.jsx +78 -0
  109. package/templates/y-bus/frontend/src/pages/Register.jsx +67 -0
  110. package/templates/y-bus/frontend/src/services/api.js +8 -0
  111. package/templates/y-bus/frontend/src/services/authService.js +33 -0
  112. package/templates/y-bus/frontend/vite.config.js +8 -0
@@ -0,0 +1,81 @@
1
+ import { useState } from "react";
2
+ import { Link, useNavigate } from "react-router-dom";
3
+ import toast from "react-hot-toast";
4
+ import { Eye, EyeOff, Ticket } from "lucide-react";
5
+ import { useAuth } from "../context/AuthContext";
6
+ import { Button } from "../components/Button";
7
+ import { Input } from "../components/Input";
8
+
9
+ function getHomePathByRole(role) {
10
+ return role === "manager" ? "/manager" : "/";
11
+ }
12
+
13
+ export function Login() {
14
+ const [form, setForm] = useState({ username: "", password: "" });
15
+ const [showPassword, setShowPassword] = useState(false);
16
+ const { login } = useAuth();
17
+ const navigate = useNavigate();
18
+
19
+ async function handleSubmit(event) {
20
+ event.preventDefault();
21
+ try {
22
+ const user = await login(form);
23
+ toast.success("Login successful");
24
+ navigate(getHomePathByRole(user.userrole));
25
+ } catch (error) {
26
+ toast.error(error.response?.data?.message || "Login failed");
27
+ }
28
+ }
29
+
30
+ return (
31
+ <main className="grid min-h-screen place-items-center bg-gray-100 p-4">
32
+ <form
33
+ onSubmit={handleSubmit}
34
+ className="w-full max-w-md rounded-2xl border border-gray-200 bg-white p-6 shadow-sm"
35
+ >
36
+ <div className="mb-8 flex items-center gap-3">
37
+ <div className="grid h-12 w-12 place-items-center rounded-xl border border-gray-300">
38
+ <Ticket size={22} />
39
+ </div>
40
+ <div>
41
+ <h1 className="text-xl font-bold text-gray-950">Y-BUS</h1>
42
+ <p className="text-sm text-gray-500">Sign in to continue</p>
43
+ </div>
44
+ </div>
45
+
46
+ <div className="grid gap-4">
47
+ <Input
48
+ label="Username"
49
+ value={form.username}
50
+ onChange={(event) => setForm({ ...form, username: event.target.value })}
51
+ required
52
+ />
53
+ <div className="relative">
54
+ <Input
55
+ label="Password"
56
+ type={showPassword ? "text" : "password"}
57
+ value={form.password}
58
+ onChange={(event) => setForm({ ...form, password: event.target.value })}
59
+ required
60
+ />
61
+ <button
62
+ type="button"
63
+ className="absolute bottom-3 right-3 text-gray-500 hover:text-gray-950"
64
+ onClick={() => setShowPassword((value) => !value)}
65
+ >
66
+ {showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
67
+ </button>
68
+ </div>
69
+ <Button type="submit" className="w-full">Login</Button>
70
+ </div>
71
+
72
+ <p className="mt-6 text-center text-sm text-gray-500">
73
+ No account?{" "}
74
+ <Link className="font-semibold text-gray-950 underline" to="/register">
75
+ Create one
76
+ </Link>
77
+ </p>
78
+ </form>
79
+ </main>
80
+ );
81
+ }
@@ -0,0 +1,140 @@
1
+ import { useEffect, useState } from "react";
2
+ import toast from "react-hot-toast";
3
+ import { Button } from "../components/Button";
4
+ import { Input } from "../components/Input";
5
+ import api from "../services/api";
6
+
7
+ const emptyForm = { platenumber: "", totalseats: "", bustype: "" };
8
+
9
+ export function ManagerBuses() {
10
+ const [buses, setBuses] = useState([]);
11
+ const [form, setForm] = useState(emptyForm);
12
+ const [editingId, setEditingId] = useState(null);
13
+
14
+ async function loadBuses() {
15
+ const response = await api.get("/buses");
16
+ setBuses(response.data.data || []);
17
+ }
18
+
19
+ useEffect(() => {
20
+ loadBuses().catch(() => toast.error("Failed to load buses"));
21
+ }, []);
22
+
23
+ async function handleSubmit(event) {
24
+ event.preventDefault();
25
+ try {
26
+ const payload = { ...form, totalseats: Number(form.totalseats) };
27
+ if (editingId) await api.put(`/buses/${editingId}`, payload);
28
+ else await api.post("/buses", payload);
29
+
30
+ toast.success(editingId ? "Bus updated" : "Bus created");
31
+ setForm(emptyForm);
32
+ setEditingId(null);
33
+ await loadBuses();
34
+ } catch (error) {
35
+ toast.error(error.response?.data?.message || "Saving bus failed");
36
+ }
37
+ }
38
+
39
+ async function deleteBus(id) {
40
+ if (!confirm("Delete this bus?")) return;
41
+ try {
42
+ await api.delete(`/buses/${id}`);
43
+ toast.success("Bus deleted");
44
+ await loadBuses();
45
+ } catch (error) {
46
+ toast.error(error.response?.data?.message || "Delete failed");
47
+ }
48
+ }
49
+
50
+ function editBus(bus) {
51
+ setEditingId(bus.busid);
52
+ setForm({
53
+ platenumber: bus.platenumber,
54
+ totalseats: bus.totalseats,
55
+ bustype: bus.bustype,
56
+ });
57
+ }
58
+
59
+ return (
60
+ <div className="grid gap-6 xl:grid-cols-[360px_1fr]">
61
+ <form onSubmit={handleSubmit} className="h-fit rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
62
+ <h1 className="text-2xl font-bold text-gray-950">{editingId ? "Edit bus" : "Create bus"}</h1>
63
+ <div className="mt-5 grid gap-4">
64
+ <Input
65
+ label="Plate number"
66
+ value={form.platenumber}
67
+ onChange={(event) => setForm({ ...form, platenumber: event.target.value })}
68
+ required
69
+ />
70
+ <Input
71
+ label="Total seats"
72
+ type="number"
73
+ min="1"
74
+ value={form.totalseats}
75
+ onChange={(event) => setForm({ ...form, totalseats: event.target.value })}
76
+ required
77
+ />
78
+ <Input
79
+ label="Bus type"
80
+ value={form.bustype}
81
+ onChange={(event) => setForm({ ...form, bustype: event.target.value })}
82
+ required
83
+ />
84
+ <Button type="submit">{editingId ? "Update bus" : "Save bus"}</Button>
85
+ {editingId ? (
86
+ <Button
87
+ type="button"
88
+ variant="light"
89
+ onClick={() => {
90
+ setEditingId(null);
91
+ setForm(emptyForm);
92
+ }}
93
+ >
94
+ Cancel edit
95
+ </Button>
96
+ ) : null}
97
+ </div>
98
+ </form>
99
+
100
+ <section className="space-y-4">
101
+ <div>
102
+ <h1 className="text-2xl font-bold text-gray-950">Buses</h1>
103
+ <p className="mt-1 text-sm text-gray-500">Manage fleet records used when creating schedules.</p>
104
+ </div>
105
+ <div className="overflow-x-auto rounded-2xl border border-gray-200 bg-white">
106
+ <table className="min-w-full text-left text-sm">
107
+ <thead className="bg-gray-50 text-xs uppercase tracking-wide text-gray-600">
108
+ <tr>
109
+ <th className="px-4 py-3 font-semibold">Plate</th>
110
+ <th className="px-4 py-3 font-semibold">Seats</th>
111
+ <th className="px-4 py-3 font-semibold">Type</th>
112
+ <th className="px-4 py-3 font-semibold">Actions</th>
113
+ </tr>
114
+ </thead>
115
+ <tbody>
116
+ {buses.map((bus) => (
117
+ <tr key={bus.busid}>
118
+ <td className="border-b border-gray-100 px-4 py-3 align-top font-semibold text-gray-950">{bus.platenumber}</td>
119
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{bus.totalseats}</td>
120
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{bus.bustype}</td>
121
+ <td className="border-b border-gray-100 px-4 py-3 align-top">
122
+ <div className="flex flex-wrap gap-2">
123
+ <Button variant="light" onClick={() => editBus(bus)}>Edit</Button>
124
+ <Button variant="light" onClick={() => deleteBus(bus.busid)}>Delete</Button>
125
+ </div>
126
+ </td>
127
+ </tr>
128
+ ))}
129
+ {buses.length === 0 ? (
130
+ <tr>
131
+ <td colSpan="4" className="border-b border-gray-100 px-4 py-3 text-center text-gray-500">No buses found.</td>
132
+ </tr>
133
+ ) : null}
134
+ </tbody>
135
+ </table>
136
+ </div>
137
+ </section>
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,108 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import toast from "react-hot-toast";
3
+ import api from "../services/api";
4
+
5
+ export function ManagerDashboard() {
6
+ const [buses, setBuses] = useState([]);
7
+ const [schedules, setSchedules] = useState([]);
8
+ const [bookings, setBookings] = useState([]);
9
+
10
+ useEffect(() => {
11
+ async function load() {
12
+ try {
13
+ const [busResponse, scheduleResponse, bookingResponse] = await Promise.all([
14
+ api.get("/buses"),
15
+ api.get("/schedules"),
16
+ api.get("/bookings"),
17
+ ]);
18
+ setBuses(busResponse.data.data || []);
19
+ setSchedules(scheduleResponse.data.data || []);
20
+ setBookings(bookingResponse.data.data || []);
21
+ } catch (error) {
22
+ toast.error(error.response?.data?.message || "Failed to load dashboard");
23
+ }
24
+ }
25
+
26
+ load();
27
+ }, []);
28
+
29
+ const stats = useMemo(() => {
30
+ const active = schedules.filter((schedule) => schedule.schedulestatus === "active").length;
31
+ const revenue = bookings.reduce((sum, booking) => {
32
+ const price = Number(booking.Schedule?.ticketprice || 0);
33
+ return sum + price;
34
+ }, 0);
35
+ return { active, bookings: bookings.length, revenue };
36
+ }, [schedules, bookings]);
37
+
38
+ const frw = (amount) => `FRW ${Number(amount || 0).toLocaleString()}`;
39
+
40
+ const cards = [
41
+ { label: "Buses", value: buses.length },
42
+ { label: "Active schedules", value: stats.active },
43
+ { label: "Bookings", value: stats.bookings },
44
+ { label: "Revenue", value: frw(stats.revenue) },
45
+ ];
46
+
47
+ return (
48
+ <div className="space-y-6">
49
+ <section className="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
50
+ <h1 className="text-2xl font-bold text-gray-950">Manager overview</h1>
51
+ <p className="mt-2 text-sm text-gray-500">
52
+ Quick view of fleet, schedules, bookings, and revenue.
53
+ </p>
54
+ </section>
55
+
56
+ <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
57
+ {cards.map((card) => (
58
+ <div key={card.label} className="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
59
+ <p className="text-sm font-medium text-gray-500">{card.label}</p>
60
+ <p className="mt-3 text-2xl font-bold text-gray-950">{card.value}</p>
61
+ </div>
62
+ ))}
63
+ </div>
64
+
65
+ <section className="space-y-4">
66
+ <div>
67
+ <h2 className="text-lg font-bold text-gray-950">Upcoming active schedules</h2>
68
+ <p className="mt-1 text-sm text-gray-500">Newest operational trips customers can book.</p>
69
+ </div>
70
+ <div className="overflow-x-auto rounded-2xl border border-gray-200 bg-white">
71
+ <table className="min-w-full text-left text-sm">
72
+ <thead className="bg-gray-50 text-xs uppercase tracking-wide text-gray-600">
73
+ <tr>
74
+ <th className="px-4 py-3 font-semibold">Route</th>
75
+ <th className="px-4 py-3 font-semibold">Bus</th>
76
+ <th className="px-4 py-3 font-semibold">Departure</th>
77
+ <th className="px-4 py-3 font-semibold">Price</th>
78
+ </tr>
79
+ </thead>
80
+ <tbody>
81
+ {schedules
82
+ .filter((schedule) => schedule.schedulestatus === "active")
83
+ .slice(0, 8)
84
+ .map((schedule) => (
85
+ <tr key={schedule.scheduleid}>
86
+ <td className="border-b border-gray-100 px-4 py-3 align-top">
87
+ <p className="font-semibold text-gray-950">{schedule.routename}</p>
88
+ <p className="text-xs text-gray-500">
89
+ {schedule.departurepoint} - {schedule.destination}
90
+ </p>
91
+ </td>
92
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{schedule.Bus?.platenumber || "N/A"}</td>
93
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{new Date(schedule.departuretime).toLocaleString()}</td>
94
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{frw(schedule.ticketprice)}</td>
95
+ </tr>
96
+ ))}
97
+ {schedules.filter((schedule) => schedule.schedulestatus === "active").length === 0 ? (
98
+ <tr>
99
+ <td colSpan="4" className="border-b border-gray-100 px-4 py-3 text-center text-gray-500">No active schedules.</td>
100
+ </tr>
101
+ ) : null}
102
+ </tbody>
103
+ </table>
104
+ </div>
105
+ </section>
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,89 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import toast from "react-hot-toast";
3
+ import api from "../services/api";
4
+
5
+ export function ManagerReport() {
6
+ const [bookings, setBookings] = useState([]);
7
+ const [filter, setFilter] = useState("");
8
+
9
+ useEffect(() => {
10
+ async function load() {
11
+ try {
12
+ const response = await api.get("/bookings");
13
+ setBookings(response.data.data || []);
14
+ } catch (error) {
15
+ toast.error(error.response?.data?.message || "Failed to load report");
16
+ }
17
+ }
18
+ load();
19
+ }, []);
20
+
21
+ const filteredBookings = useMemo(() => {
22
+ const keyword = filter.trim().toLowerCase();
23
+ if (!keyword) return bookings;
24
+
25
+ return bookings.filter((booking) => {
26
+ const text = [
27
+ booking.Schedule?.routename,
28
+ booking.Schedule?.departurepoint,
29
+ booking.Schedule?.destination,
30
+ booking.User?.username,
31
+ ]
32
+ .join(" ")
33
+ .toLowerCase();
34
+ return text.includes(keyword);
35
+ });
36
+ }, [bookings, filter]);
37
+
38
+ return (
39
+ <div className="space-y-6">
40
+ <section className="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
41
+ <h1 className="text-2xl font-bold text-gray-950">Booking report</h1>
42
+ <p className="mt-2 text-sm text-gray-500">View passenger bookings.</p>
43
+ <input
44
+ className="mt-5 h-11 w-full max-w-md rounded-xl border border-gray-300 bg-white px-3 outline-none focus:border-gray-950 focus:ring-2 focus:ring-gray-200"
45
+ placeholder="Search by route, location, or user"
46
+ value={filter}
47
+ onChange={(event) => setFilter(event.target.value)}
48
+ />
49
+ </section>
50
+
51
+ <div className="overflow-x-auto rounded-2xl border border-gray-200 bg-white">
52
+ <table className="min-w-full text-left text-sm">
53
+ <thead className="bg-gray-50 text-xs uppercase tracking-wide text-gray-600">
54
+ <tr>
55
+ <th className="px-4 py-3 font-semibold">Route</th>
56
+ <th className="px-4 py-3 font-semibold">Passenger</th>
57
+ <th className="px-4 py-3 font-semibold">Seat</th>
58
+ <th className="px-4 py-3 font-semibold">Payment</th>
59
+ <th className="px-4 py-3 font-semibold">User</th>
60
+ <th className="px-4 py-3 font-semibold">Date</th>
61
+ </tr>
62
+ </thead>
63
+ <tbody>
64
+ {filteredBookings.map((booking) => (
65
+ <tr key={booking.bookingid}>
66
+ <td className="border-b border-gray-100 px-4 py-3 align-top">
67
+ <p className="font-semibold text-gray-950">{booking.Schedule?.routename || "N/A"}</p>
68
+ <p className="text-xs text-gray-500">
69
+ {booking.Schedule?.departurepoint || "-"} - {booking.Schedule?.destination || "-"}
70
+ </p>
71
+ </td>
72
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{booking.passengername}</td>
73
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{booking.seatnumber}</td>
74
+ <td className="border-b border-gray-100 px-4 py-3 align-top capitalize">{booking.paymentstatus}</td>
75
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{booking.User?.username || "N/A"}</td>
76
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{new Date(booking.bookingdate).toLocaleString()}</td>
77
+ </tr>
78
+ ))}
79
+ {filteredBookings.length === 0 ? (
80
+ <tr>
81
+ <td colSpan="6" className="border-b border-gray-100 px-4 py-3 text-center text-gray-500">No report data found.</td>
82
+ </tr>
83
+ ) : null}
84
+ </tbody>
85
+ </table>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,233 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import toast from "react-hot-toast";
3
+ import { Button } from "../components/Button";
4
+ import { Input, Select } from "../components/Input";
5
+ import api from "../services/api";
6
+
7
+ const emptyForm = {
8
+ busid: "",
9
+ routename: "",
10
+ departurepoint: "",
11
+ destination: "",
12
+ departuretime: "",
13
+ estimatedarrivaltime: "",
14
+ ticketprice: "",
15
+ schedulestatus: "active",
16
+ };
17
+
18
+ function toDateTimeLocal(value) {
19
+ if (!value) return "";
20
+ return new Date(value).toISOString().slice(0, 16);
21
+ }
22
+
23
+ export function ManagerSchedules() {
24
+ const [schedules, setSchedules] = useState([]);
25
+ const [buses, setBuses] = useState([]);
26
+ const [form, setForm] = useState(emptyForm);
27
+ const [editingId, setEditingId] = useState(null);
28
+
29
+ const busNameById = useMemo(
30
+ () => Object.fromEntries(buses.map((bus) => [String(bus.busid), bus.platenumber])),
31
+ [buses]
32
+ );
33
+
34
+ const frw = (amount) => `FRW ${Number(amount || 0).toLocaleString()}`;
35
+
36
+ async function load() {
37
+ const [scheduleResponse, busResponse] = await Promise.all([
38
+ api.get("/schedules"),
39
+ api.get("/buses"),
40
+ ]);
41
+ setSchedules(scheduleResponse.data.data || []);
42
+ setBuses(busResponse.data.data || []);
43
+ }
44
+
45
+ useEffect(() => {
46
+ load().catch(() => toast.error("Failed to load schedules"));
47
+ }, []);
48
+
49
+ async function handleSubmit(event) {
50
+ event.preventDefault();
51
+ try {
52
+ const payload = {
53
+ ...form,
54
+ busid: Number(form.busid),
55
+ ticketprice: Number(form.ticketprice),
56
+ };
57
+
58
+ if (editingId) await api.put(`/schedules/${editingId}`, payload);
59
+ else await api.post("/schedules", payload);
60
+
61
+ toast.success(editingId ? "Schedule updated" : "Schedule created");
62
+ setForm(emptyForm);
63
+ setEditingId(null);
64
+ await load();
65
+ } catch (error) {
66
+ toast.error(error.response?.data?.message || "Saving schedule failed");
67
+ }
68
+ }
69
+
70
+ async function deleteSchedule(id) {
71
+ if (!confirm("Delete this schedule?")) return;
72
+ try {
73
+ await api.delete(`/schedules/${id}`);
74
+ toast.success("Schedule deleted");
75
+ await load();
76
+ } catch (error) {
77
+ toast.error(error.response?.data?.message || "Delete failed");
78
+ }
79
+ }
80
+
81
+ function editSchedule(schedule) {
82
+ setEditingId(schedule.scheduleid);
83
+ setForm({
84
+ busid: String(schedule.busid),
85
+ routename: schedule.routename,
86
+ departurepoint: schedule.departurepoint,
87
+ destination: schedule.destination,
88
+ departuretime: toDateTimeLocal(schedule.departuretime),
89
+ estimatedarrivaltime: toDateTimeLocal(schedule.estimatedarrivaltime),
90
+ ticketprice: schedule.ticketprice,
91
+ schedulestatus: schedule.schedulestatus || "active",
92
+ });
93
+ }
94
+
95
+ return (
96
+ <div className="grid gap-6 2xl:grid-cols-[420px_1fr]">
97
+ <form onSubmit={handleSubmit} className="h-fit rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
98
+ <h1 className="text-2xl font-bold text-gray-950">{editingId ? "Edit schedule" : "Create schedule"}</h1>
99
+ <div className="mt-5 grid gap-4">
100
+ <Select
101
+ label="Bus"
102
+ value={form.busid}
103
+ onChange={(event) => setForm({ ...form, busid: event.target.value })}
104
+ required
105
+ >
106
+ <option value="">Select bus</option>
107
+ {buses.map((bus) => (
108
+ <option key={bus.busid} value={bus.busid}>
109
+ {bus.platenumber} - {bus.bustype}
110
+ </option>
111
+ ))}
112
+ </Select>
113
+ <Input
114
+ label="Route name"
115
+ value={form.routename}
116
+ onChange={(event) => setForm({ ...form, routename: event.target.value })}
117
+ required
118
+ />
119
+ <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-1">
120
+ <Input
121
+ label="Departure point"
122
+ value={form.departurepoint}
123
+ onChange={(event) => setForm({ ...form, departurepoint: event.target.value })}
124
+ required
125
+ />
126
+ <Input
127
+ label="Destination"
128
+ value={form.destination}
129
+ onChange={(event) => setForm({ ...form, destination: event.target.value })}
130
+ required
131
+ />
132
+ </div>
133
+ <Input
134
+ label="Departure time"
135
+ type="datetime-local"
136
+ value={form.departuretime}
137
+ onChange={(event) => setForm({ ...form, departuretime: event.target.value })}
138
+ required
139
+ />
140
+ <Input
141
+ label="Estimated arrival time"
142
+ type="datetime-local"
143
+ value={form.estimatedarrivaltime}
144
+ onChange={(event) => setForm({ ...form, estimatedarrivaltime: event.target.value })}
145
+ required
146
+ />
147
+ <Input
148
+ label="Ticket price"
149
+ type="number"
150
+ min="0"
151
+ value={form.ticketprice}
152
+ onChange={(event) => setForm({ ...form, ticketprice: event.target.value })}
153
+ required
154
+ />
155
+ <Select
156
+ label="Status"
157
+ value={form.schedulestatus}
158
+ onChange={(event) => setForm({ ...form, schedulestatus: event.target.value })}
159
+ >
160
+ <option value="active">Active</option>
161
+ <option value="cancelled">Cancelled</option>
162
+ <option value="completed">Completed</option>
163
+ </Select>
164
+ <Button type="submit">{editingId ? "Update schedule" : "Save schedule"}</Button>
165
+ {editingId ? (
166
+ <Button
167
+ type="button"
168
+ variant="light"
169
+ onClick={() => {
170
+ setEditingId(null);
171
+ setForm(emptyForm);
172
+ }}
173
+ >
174
+ Cancel edit
175
+ </Button>
176
+ ) : null}
177
+ </div>
178
+ </form>
179
+
180
+ <section className="space-y-4">
181
+ <div>
182
+ <h1 className="text-2xl font-bold text-gray-950">Schedules</h1>
183
+ <p className="mt-1 text-sm text-gray-500">Create and update bus routes and timings.</p>
184
+ </div>
185
+ <div className="overflow-x-auto rounded-2xl border border-gray-200 bg-white">
186
+ <table className="min-w-full text-left text-sm">
187
+ <thead className="bg-gray-50 text-xs uppercase tracking-wide text-gray-600">
188
+ <tr>
189
+ <th className="px-4 py-3 font-semibold">Route</th>
190
+ <th className="px-4 py-3 font-semibold">Bus</th>
191
+ <th className="px-4 py-3 font-semibold">Departure</th>
192
+ <th className="px-4 py-3 font-semibold">Price</th>
193
+ <th className="px-4 py-3 font-semibold">Status</th>
194
+ <th className="px-4 py-3 font-semibold">Actions</th>
195
+ </tr>
196
+ </thead>
197
+ <tbody>
198
+ {schedules.map((schedule) => (
199
+ <tr key={schedule.scheduleid}>
200
+ <td className="border-b border-gray-100 px-4 py-3 align-top">
201
+ <p className="font-semibold text-gray-950">{schedule.routename}</p>
202
+ <p className="text-xs text-gray-500">
203
+ {schedule.departurepoint} - {schedule.destination}
204
+ </p>
205
+ </td>
206
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{schedule.Bus?.platenumber || busNameById[String(schedule.busid)] || "N/A"}</td>
207
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{new Date(schedule.departuretime).toLocaleString()}</td>
208
+ <td className="border-b border-gray-100 px-4 py-3 align-top">{frw(schedule.ticketprice)}</td>
209
+ <td className="border-b border-gray-100 px-4 py-3 align-top">
210
+ <span className="rounded-full border border-gray-300 px-2 py-1 text-xs capitalize">
211
+ {schedule.schedulestatus}
212
+ </span>
213
+ </td>
214
+ <td className="border-b border-gray-100 px-4 py-3 align-top">
215
+ <div className="flex flex-wrap gap-2">
216
+ <Button variant="light" onClick={() => editSchedule(schedule)}>Edit</Button>
217
+ <Button variant="light" onClick={() => deleteSchedule(schedule.scheduleid)}>Delete</Button>
218
+ </div>
219
+ </td>
220
+ </tr>
221
+ ))}
222
+ {schedules.length === 0 ? (
223
+ <tr>
224
+ <td colSpan="6" className="border-b border-gray-100 px-4 py-3 text-center text-gray-500">No schedules found.</td>
225
+ </tr>
226
+ ) : null}
227
+ </tbody>
228
+ </table>
229
+ </div>
230
+ </section>
231
+ </div>
232
+ );
233
+ }