create-sales 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 (44) hide show
  1. package/.gitignore +19 -0
  2. package/README.md +40 -0
  3. package/backend-project/.env.example +11 -0
  4. package/backend-project/database/schema.sql +101 -0
  5. package/backend-project/db.js +13 -0
  6. package/backend-project/middleware/auth.js +24 -0
  7. package/backend-project/package.json +21 -0
  8. package/backend-project/routes/auth.js +55 -0
  9. package/backend-project/routes/bookings.js +151 -0
  10. package/backend-project/routes/buses.js +81 -0
  11. package/backend-project/routes/schedules.js +95 -0
  12. package/backend-project/routes/users.js +86 -0
  13. package/backend-project/server.js +54 -0
  14. package/bin/create-sales.js +102 -0
  15. package/frontend-project/index.html +12 -0
  16. package/frontend-project/package.json +31 -0
  17. package/frontend-project/src/App.tsx +77 -0
  18. package/frontend-project/src/api/auth.ts +20 -0
  19. package/frontend-project/src/api/axios.ts +11 -0
  20. package/frontend-project/src/api/bookings.ts +31 -0
  21. package/frontend-project/src/api/buses.ts +26 -0
  22. package/frontend-project/src/api/schedules.ts +26 -0
  23. package/frontend-project/src/api/users.ts +21 -0
  24. package/frontend-project/src/components/common/Layout.tsx +15 -0
  25. package/frontend-project/src/components/common/Modal.tsx +42 -0
  26. package/frontend-project/src/components/common/Navbar.tsx +124 -0
  27. package/frontend-project/src/components/common/ProtectedRoute.tsx +30 -0
  28. package/frontend-project/src/components/common/StatCard.tsx +24 -0
  29. package/frontend-project/src/components/common/Table.tsx +67 -0
  30. package/frontend-project/src/context/AuthContext.tsx +41 -0
  31. package/frontend-project/src/index.css +43 -0
  32. package/frontend-project/src/main.tsx +10 -0
  33. package/frontend-project/src/pages/Bookings.tsx +295 -0
  34. package/frontend-project/src/pages/Buses.tsx +255 -0
  35. package/frontend-project/src/pages/Dashboard.tsx +197 -0
  36. package/frontend-project/src/pages/Login.tsx +112 -0
  37. package/frontend-project/src/pages/Report.tsx +252 -0
  38. package/frontend-project/src/pages/Schedules.tsx +256 -0
  39. package/frontend-project/src/pages/Users.tsx +199 -0
  40. package/frontend-project/src/types/index.ts +63 -0
  41. package/frontend-project/src/utils/cn.ts +6 -0
  42. package/frontend-project/tsconfig.json +31 -0
  43. package/frontend-project/vite.config.ts +19 -0
  44. package/package.json +50 -0
@@ -0,0 +1,252 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { getBookings } from '../api/bookings';
3
+ import { getSchedules } from '../api/schedules';
4
+ import { Booking, Schedule } from '../types';
5
+
6
+ interface RouteGroup {
7
+ ScheduleID: number;
8
+ RouteName: string;
9
+ DeparturePoint: string;
10
+ Destination: string;
11
+ DepartureTime: string;
12
+ PlateNumber: string;
13
+ BusType: string;
14
+ TicketPrice: number;
15
+ passengers: Booking[];
16
+ }
17
+
18
+ const Report = () => {
19
+ const [bookings, setBookings] = useState<Booking[]>([]);
20
+ const [schedules, setSchedules] = useState<Schedule[]>([]);
21
+ const [loading, setLoading] = useState(true);
22
+ const [selectedRoute, setSelectedRoute] = useState<number | 'all'>('all');
23
+ const [paymentFilter, setPaymentFilter] = useState('All');
24
+ const [expandedGroup, setExpandedGroup] = useState<number | null>(null);
25
+
26
+ useEffect(() => {
27
+ const fetchData = async () => {
28
+ setLoading(true);
29
+ try {
30
+ const [b, s] = await Promise.all([getBookings(), getSchedules()]);
31
+ setBookings(b);
32
+ setSchedules(s);
33
+ } finally {
34
+ setLoading(false);
35
+ }
36
+ };
37
+ fetchData();
38
+ }, []);
39
+
40
+ const filteredBookings = bookings.filter((b) =>
41
+ paymentFilter === 'All' || b.PaymentStatus === paymentFilter
42
+ );
43
+
44
+ const groupedBySchedule: RouteGroup[] = schedules.map((s) => {
45
+ const passengers = filteredBookings.filter((b) => b.ScheduleID === s.ScheduleID);
46
+ return {
47
+ ScheduleID: s.ScheduleID,
48
+ RouteName: s.RouteName,
49
+ DeparturePoint: s.DeparturePoint,
50
+ Destination: s.Destination,
51
+ DepartureTime: s.DepartureTime,
52
+ PlateNumber: s.PlateNumber || 'N/A',
53
+ BusType: s.BusType || 'N/A',
54
+ TicketPrice: s.TicketPrice,
55
+ passengers,
56
+ };
57
+ }).filter((g) => selectedRoute === 'all' || g.ScheduleID === selectedRoute);
58
+
59
+ const totalPassengers = groupedBySchedule.reduce((acc, g) => acc + g.passengers.length, 0);
60
+ const totalRevenue = groupedBySchedule.reduce((acc, g) =>
61
+ acc + g.passengers.filter(p => p.PaymentStatus === 'Paid').length * g.TicketPrice, 0
62
+ );
63
+
64
+ const handlePrint = () => window.print();
65
+
66
+ if (loading) {
67
+ return (
68
+ <div className="flex items-center justify-center min-h-[60vh]">
69
+ <div className="w-12 h-12 border-4 border-yellow-400 border-t-transparent rounded-full animate-spin" />
70
+ </div>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <div className="space-y-5">
76
+ {/* Header */}
77
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
78
+ <div>
79
+ <h1 className="text-2xl font-bold text-gray-800">Passenger Report</h1>
80
+ <p className="text-gray-500 text-sm mt-1">Booked passengers grouped by travel route schedule</p>
81
+ </div>
82
+ <button
83
+ onClick={handlePrint}
84
+ className="bg-gray-900 hover:bg-gray-700 text-yellow-400 px-5 py-2.5 rounded-xl text-sm font-semibold flex items-center gap-2 transition-colors print:hidden"
85
+ >
86
+ 🖨️ Print Report
87
+ </button>
88
+ </div>
89
+
90
+ {/* Filters */}
91
+ <div className="flex flex-col sm:flex-row gap-3 print:hidden">
92
+ <select
93
+ value={selectedRoute}
94
+ onChange={(e) => setSelectedRoute(e.target.value === 'all' ? 'all' : parseInt(e.target.value))}
95
+ className="flex-1 border border-gray-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400"
96
+ >
97
+ <option value="all">All Routes</option>
98
+ {schedules.map((s) => (
99
+ <option key={s.ScheduleID} value={s.ScheduleID}>
100
+ {s.RouteName} — {s.DeparturePoint} → {s.Destination}
101
+ </option>
102
+ ))}
103
+ </select>
104
+ <div className="flex gap-2">
105
+ {['All', 'Pending', 'Paid', 'Cancelled'].map((s) => (
106
+ <button
107
+ key={s}
108
+ onClick={() => setPaymentFilter(s)}
109
+ className={`px-3 py-2 rounded-lg text-xs font-medium transition-colors ${
110
+ paymentFilter === s ? 'bg-gray-900 text-yellow-400' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
111
+ }`}
112
+ >
113
+ {s}
114
+ </button>
115
+ ))}
116
+ </div>
117
+ </div>
118
+
119
+ {/* Summary Cards */}
120
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
121
+ <div className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
122
+ <p className="text-gray-500 text-sm">Total Routes Shown</p>
123
+ <p className="text-3xl font-bold text-gray-800 mt-1">{groupedBySchedule.length}</p>
124
+ </div>
125
+ <div className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
126
+ <p className="text-gray-500 text-sm">Total Passengers</p>
127
+ <p className="text-3xl font-bold text-gray-800 mt-1">{totalPassengers}</p>
128
+ </div>
129
+ <div className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
130
+ <p className="text-gray-500 text-sm">Estimated Revenue (RWF)</p>
131
+ <p className="text-3xl font-bold text-green-600 mt-1">{totalRevenue.toLocaleString()}</p>
132
+ <p className="text-xs text-gray-400 mt-1">Paid bookings only</p>
133
+ </div>
134
+ </div>
135
+
136
+ {/* Report — Print Header (hidden on screen) */}
137
+ <div className="hidden print:block text-center mb-6">
138
+ <h1 className="text-2xl font-bold">Y Bus Reservation System</h1>
139
+ <h2 className="text-lg">Passenger Report — By Route Schedule</h2>
140
+ <p className="text-sm text-gray-500">Generated: {new Date().toLocaleString()}</p>
141
+ </div>
142
+
143
+ {/* Groups */}
144
+ <div className="space-y-4">
145
+ {groupedBySchedule.length === 0 ? (
146
+ <div className="bg-white rounded-xl border border-gray-100 p-12 text-center">
147
+ <p className="text-4xl mb-3">📊</p>
148
+ <p className="text-gray-500">No data available for the selected filters.</p>
149
+ </div>
150
+ ) : (
151
+ groupedBySchedule.map((group) => (
152
+ <div key={group.ScheduleID} className="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden print:break-inside-avoid">
153
+ {/* Group Header */}
154
+ <div
155
+ className="p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-gray-50 cursor-pointer hover:bg-gray-100 transition-colors print:cursor-default"
156
+ onClick={() => setExpandedGroup(expandedGroup === group.ScheduleID ? null : group.ScheduleID)}
157
+ >
158
+ <div className="flex items-start gap-4">
159
+ <div className="w-10 h-10 bg-yellow-400 rounded-xl flex items-center justify-center text-gray-900 font-bold text-sm flex-shrink-0">
160
+ 🚌
161
+ </div>
162
+ <div>
163
+ <h3 className="font-semibold text-gray-800">{group.RouteName}</h3>
164
+ <p className="text-sm text-gray-500">
165
+ {group.DeparturePoint} → {group.Destination}
166
+ </p>
167
+ <div className="flex flex-wrap gap-2 mt-1">
168
+ <span className="text-xs bg-gray-200 text-gray-600 px-2 py-0.5 rounded-full">
169
+ 🚍 {group.PlateNumber} ({group.BusType})
170
+ </span>
171
+ <span className="text-xs bg-gray-200 text-gray-600 px-2 py-0.5 rounded-full">
172
+ 🕐 {group.DepartureTime ? new Date(group.DepartureTime).toLocaleString('en-RW', { dateStyle: 'short', timeStyle: 'short' }) : 'N/A'}
173
+ </span>
174
+ <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">
175
+ 💰 {Number(group.TicketPrice).toLocaleString()} RWF
176
+ </span>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ <div className="flex items-center gap-4">
181
+ <div className="text-center">
182
+ <p className="text-2xl font-bold text-gray-800">{group.passengers.length}</p>
183
+ <p className="text-xs text-gray-400">Passengers</p>
184
+ </div>
185
+ <div className="text-center">
186
+ <p className="text-2xl font-bold text-green-600">{group.passengers.filter(p => p.PaymentStatus === 'Paid').length}</p>
187
+ <p className="text-xs text-gray-400">Paid</p>
188
+ </div>
189
+ <span className="text-gray-400 print:hidden">
190
+ {expandedGroup === group.ScheduleID ? '▲' : '▼'}
191
+ </span>
192
+ </div>
193
+ </div>
194
+
195
+ {/* Passenger Table */}
196
+ <div className={`${expandedGroup === group.ScheduleID ? 'block' : 'hidden'} print:block`}>
197
+ {group.passengers.length === 0 ? (
198
+ <p className="text-center text-gray-400 text-sm py-6">No passengers for this route.</p>
199
+ ) : (
200
+ <div className="overflow-x-auto">
201
+ <table className="min-w-full divide-y divide-gray-100">
202
+ <thead className="bg-gray-900">
203
+ <tr>
204
+ {['#', 'Passenger Name', 'Gender', 'Phone', 'Seat', 'Booking Date', 'Payment'].map((h) => (
205
+ <th key={h} className="px-4 py-2.5 text-left text-xs font-semibold text-yellow-400 uppercase">{h}</th>
206
+ ))}
207
+ </tr>
208
+ </thead>
209
+ <tbody className="divide-y divide-gray-50">
210
+ {group.passengers.map((p, idx) => (
211
+ <tr key={p.BookingID} className="hover:bg-yellow-50">
212
+ <td className="px-4 py-3 text-sm text-gray-500">{idx + 1}</td>
213
+ <td className="px-4 py-3 text-sm font-medium text-gray-800">{p.PassengerName}</td>
214
+ <td className="px-4 py-3 text-sm text-gray-600">{p.PassengerGender}</td>
215
+ <td className="px-4 py-3 text-sm text-gray-600">{p.PassengerPhone}</td>
216
+ <td className="px-4 py-3 text-sm text-gray-600">{p.SeatNumber}</td>
217
+ <td className="px-4 py-3 text-sm text-gray-600">
218
+ {p.BookingDate ? new Date(p.BookingDate).toLocaleDateString('en-RW') : 'N/A'}
219
+ </td>
220
+ <td className="px-4 py-3">
221
+ <span className={`text-xs px-2 py-1 rounded-full font-medium ${
222
+ p.PaymentStatus === 'Paid' ? 'bg-green-100 text-green-700'
223
+ : p.PaymentStatus === 'Cancelled' ? 'bg-red-100 text-red-700'
224
+ : 'bg-yellow-100 text-yellow-700'
225
+ }`}>{p.PaymentStatus}</span>
226
+ </td>
227
+ </tr>
228
+ ))}
229
+ </tbody>
230
+ <tfoot className="bg-gray-50">
231
+ <tr>
232
+ <td colSpan={6} className="px-4 py-2.5 text-sm font-semibold text-gray-700 text-right">
233
+ Revenue from Paid ({group.passengers.filter(p => p.PaymentStatus === 'Paid').length} pax):
234
+ </td>
235
+ <td className="px-4 py-2.5 text-sm font-bold text-green-600">
236
+ {(group.passengers.filter(p => p.PaymentStatus === 'Paid').length * group.TicketPrice).toLocaleString()} RWF
237
+ </td>
238
+ </tr>
239
+ </tfoot>
240
+ </table>
241
+ </div>
242
+ )}
243
+ </div>
244
+ </div>
245
+ ))
246
+ )}
247
+ </div>
248
+ </div>
249
+ );
250
+ };
251
+
252
+ export default Report;
@@ -0,0 +1,256 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { getSchedules, createSchedule, updateSchedule, deleteSchedule } from '../api/schedules';
3
+ import { getBuses } from '../api/buses';
4
+ import { Schedule, Bus } from '../types';
5
+ import Table from '../components/common/Table';
6
+ import Modal from '../components/common/Modal';
7
+
8
+ const defaultForm = {
9
+ BusID: 0,
10
+ RouteName: '',
11
+ DeparturePoint: '',
12
+ Destination: '',
13
+ DepartureTime: '',
14
+ EstimatedArrivalTime: '',
15
+ TicketPrice: 0,
16
+ ScheduleStatus: 'Active' as 'Active' | 'Inactive' | 'Cancelled',
17
+ };
18
+
19
+ const Schedules = () => {
20
+ const [schedules, setSchedules] = useState<Schedule[]>([]);
21
+ const [buses, setBuses] = useState<Bus[]>([]);
22
+ const [loading, setLoading] = useState(true);
23
+ const [modalOpen, setModalOpen] = useState(false);
24
+ const [deleteModalOpen, setDeleteModalOpen] = useState(false);
25
+ const [editSchedule, setEditSchedule] = useState<Schedule | null>(null);
26
+ const [deleteId, setDeleteId] = useState<number | null>(null);
27
+ const [form, setForm] = useState(defaultForm);
28
+ const [saving, setSaving] = useState(false);
29
+ const [error, setError] = useState('');
30
+ const [search, setSearch] = useState('');
31
+ const [statusFilter, setStatusFilter] = useState('All');
32
+
33
+ const fetchData = async () => {
34
+ setLoading(true);
35
+ try {
36
+ const [s, b] = await Promise.all([getSchedules(), getBuses()]);
37
+ setSchedules(s);
38
+ setBuses(b);
39
+ if (b.length > 0 && defaultForm.BusID === 0) {
40
+ setForm((f) => ({ ...f, BusID: b[0].BusID }));
41
+ }
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ };
46
+
47
+ useEffect(() => { fetchData(); }, []);
48
+
49
+ const openAdd = () => {
50
+ setEditSchedule(null);
51
+ setForm({ ...defaultForm, BusID: buses[0]?.BusID || 0 });
52
+ setError('');
53
+ setModalOpen(true);
54
+ };
55
+
56
+ const openEdit = (s: Schedule) => {
57
+ setEditSchedule(s);
58
+ setForm({
59
+ BusID: s.BusID,
60
+ RouteName: s.RouteName,
61
+ DeparturePoint: s.DeparturePoint,
62
+ Destination: s.Destination,
63
+ DepartureTime: s.DepartureTime?.slice(0, 16) || '',
64
+ EstimatedArrivalTime: s.EstimatedArrivalTime?.slice(0, 16) || '',
65
+ TicketPrice: s.TicketPrice,
66
+ ScheduleStatus: s.ScheduleStatus,
67
+ });
68
+ setError('');
69
+ setModalOpen(true);
70
+ };
71
+
72
+ const handleSubmit = async (e: React.FormEvent) => {
73
+ e.preventDefault();
74
+ setSaving(true);
75
+ setError('');
76
+ try {
77
+ if (editSchedule) {
78
+ await updateSchedule(editSchedule.ScheduleID, form);
79
+ } else {
80
+ await createSchedule(form);
81
+ }
82
+ setModalOpen(false);
83
+ fetchData();
84
+ } catch (err: unknown) {
85
+ setError(err instanceof Error ? err.message : 'An error occurred');
86
+ } finally {
87
+ setSaving(false);
88
+ }
89
+ };
90
+
91
+ const handleDelete = async () => {
92
+ if (!deleteId) return;
93
+ try {
94
+ await deleteSchedule(deleteId);
95
+ setDeleteModalOpen(false);
96
+ fetchData();
97
+ } catch (err: unknown) {
98
+ alert(err instanceof Error ? err.message : 'Delete failed');
99
+ }
100
+ };
101
+
102
+ const filtered = schedules.filter((s) => {
103
+ const matchSearch =
104
+ s.RouteName.toLowerCase().includes(search.toLowerCase()) ||
105
+ s.DeparturePoint.toLowerCase().includes(search.toLowerCase()) ||
106
+ s.Destination.toLowerCase().includes(search.toLowerCase());
107
+ const matchStatus = statusFilter === 'All' || s.ScheduleStatus === statusFilter;
108
+ return matchSearch && matchStatus;
109
+ });
110
+
111
+ const columns = [
112
+ { key: 'ScheduleID', label: '#' },
113
+ { key: 'RouteName', label: 'Route Name' },
114
+ { key: 'DeparturePoint', label: 'From' },
115
+ { key: 'Destination', label: 'To' },
116
+ { key: 'PlateNumber', label: 'Bus' },
117
+ { key: 'DepartureTime', label: 'Departure', render: (s: Schedule) =>
118
+ s.DepartureTime ? new Date(s.DepartureTime).toLocaleString('en-RW', { dateStyle: 'short', timeStyle: 'short' }) : 'N/A'
119
+ },
120
+ { key: 'TicketPrice', label: 'Price (RWF)', render: (s: Schedule) =>
121
+ <span className="font-semibold text-gray-800">{Number(s.TicketPrice).toLocaleString()}</span>
122
+ },
123
+ { key: 'ScheduleStatus', label: 'Status', render: (s: Schedule) => (
124
+ <span className={`px-2 py-1 rounded-full text-xs font-medium ${
125
+ s.ScheduleStatus === 'Active' ? 'bg-green-100 text-green-700'
126
+ : s.ScheduleStatus === 'Cancelled' ? 'bg-red-100 text-red-700'
127
+ : 'bg-gray-100 text-gray-600'
128
+ }`}>{s.ScheduleStatus}</span>
129
+ )},
130
+ {
131
+ key: 'actions', label: 'Actions',
132
+ render: (s: Schedule) => (
133
+ <div className="flex gap-2">
134
+ <button onClick={() => openEdit(s)} className="bg-blue-50 hover:bg-blue-100 text-blue-700 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors">Edit</button>
135
+ <button onClick={() => { setDeleteId(s.ScheduleID); setDeleteModalOpen(true); }} className="bg-red-50 hover:bg-red-100 text-red-700 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors">Delete</button>
136
+ </div>
137
+ ),
138
+ },
139
+ ];
140
+
141
+ return (
142
+ <div className="space-y-5">
143
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
144
+ <div>
145
+ <h1 className="text-2xl font-bold text-gray-800">Route Schedules</h1>
146
+ <p className="text-gray-500 text-sm mt-1">Manage bus routes and departure schedules</p>
147
+ </div>
148
+ <button onClick={openAdd} className="bg-gray-900 hover:bg-gray-700 text-yellow-400 px-5 py-2.5 rounded-xl text-sm font-semibold flex items-center gap-2 transition-colors shadow-sm">
149
+ + Add New Schedule
150
+ </button>
151
+ </div>
152
+
153
+ <div className="flex flex-col sm:flex-row gap-3">
154
+ <input
155
+ type="text"
156
+ placeholder="Search by route, departure or destination..."
157
+ value={search}
158
+ onChange={(e) => setSearch(e.target.value)}
159
+ className="flex-1 border border-gray-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400"
160
+ />
161
+ <div className="flex gap-2">
162
+ {['All', 'Active', 'Inactive', 'Cancelled'].map((s) => (
163
+ <button
164
+ key={s}
165
+ onClick={() => setStatusFilter(s)}
166
+ className={`px-3 py-2 rounded-lg text-xs font-medium transition-colors ${
167
+ statusFilter === s ? 'bg-gray-900 text-yellow-400' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
168
+ }`}
169
+ >
170
+ {s}
171
+ </button>
172
+ ))}
173
+ </div>
174
+ </div>
175
+
176
+ <Table columns={columns} data={filtered} keyExtractor={(s) => s.ScheduleID} loading={loading} emptyMessage="No schedules found." />
177
+
178
+ <Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title={editSchedule ? 'Edit Schedule' : 'Add New Schedule'} size="lg">
179
+ <form onSubmit={handleSubmit} className="space-y-4">
180
+ {error && <div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-600">{error}</div>}
181
+
182
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
183
+ <div>
184
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Assigned Bus *</label>
185
+ <select required value={form.BusID} onChange={(e) => setForm({ ...form, BusID: parseInt(e.target.value) })}
186
+ className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400">
187
+ <option value={0}>Select Bus</option>
188
+ {buses.map((b) => <option key={b.BusID} value={b.BusID}>{b.PlateNumber} ({b.BusType})</option>)}
189
+ </select>
190
+ </div>
191
+ <div>
192
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Route Name *</label>
193
+ <input required type="text" value={form.RouteName} onChange={(e) => setForm({ ...form, RouteName: e.target.value })}
194
+ className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400"
195
+ placeholder="e.g. KGL-BTR Route 1" />
196
+ </div>
197
+ <div>
198
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Departure Point *</label>
199
+ <input required type="text" value={form.DeparturePoint} onChange={(e) => setForm({ ...form, DeparturePoint: e.target.value })}
200
+ className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400"
201
+ placeholder="e.g. Nyabugogo" />
202
+ </div>
203
+ <div>
204
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Destination *</label>
205
+ <input required type="text" value={form.Destination} onChange={(e) => setForm({ ...form, Destination: e.target.value })}
206
+ className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400"
207
+ placeholder="e.g. Butare" />
208
+ </div>
209
+ <div>
210
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Departure Time *</label>
211
+ <input required type="datetime-local" value={form.DepartureTime} onChange={(e) => setForm({ ...form, DepartureTime: e.target.value })}
212
+ className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400" />
213
+ </div>
214
+ <div>
215
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Est. Arrival Time *</label>
216
+ <input required type="datetime-local" value={form.EstimatedArrivalTime} onChange={(e) => setForm({ ...form, EstimatedArrivalTime: e.target.value })}
217
+ className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400" />
218
+ </div>
219
+ <div>
220
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Ticket Price (RWF) *</label>
221
+ <input required type="number" min={0} value={form.TicketPrice} onChange={(e) => setForm({ ...form, TicketPrice: parseFloat(e.target.value) })}
222
+ className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400"
223
+ placeholder="e.g. 5000" />
224
+ </div>
225
+ <div>
226
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Status *</label>
227
+ <select value={form.ScheduleStatus} onChange={(e) => setForm({ ...form, ScheduleStatus: e.target.value as 'Active' | 'Inactive' | 'Cancelled' })}
228
+ className="w-full border border-gray-200 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400">
229
+ <option value="Active">Active</option>
230
+ <option value="Inactive">Inactive</option>
231
+ <option value="Cancelled">Cancelled</option>
232
+ </select>
233
+ </div>
234
+ </div>
235
+
236
+ <div className="flex gap-3 pt-2">
237
+ <button type="button" onClick={() => setModalOpen(false)} className="flex-1 border border-gray-200 text-gray-700 py-2.5 rounded-lg text-sm hover:bg-gray-50">Cancel</button>
238
+ <button type="submit" disabled={saving} className="flex-1 bg-gray-900 text-yellow-400 py-2.5 rounded-lg text-sm font-semibold hover:bg-gray-700 disabled:opacity-50">
239
+ {saving ? 'Saving...' : editSchedule ? 'Update Schedule' : 'Add Schedule'}
240
+ </button>
241
+ </div>
242
+ </form>
243
+ </Modal>
244
+
245
+ <Modal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} title="Confirm Delete" size="sm">
246
+ <p className="text-gray-600 text-sm mb-4">Are you sure you want to delete this schedule?</p>
247
+ <div className="flex gap-3">
248
+ <button onClick={() => setDeleteModalOpen(false)} className="flex-1 border border-gray-200 text-gray-700 py-2.5 rounded-lg text-sm hover:bg-gray-50">Cancel</button>
249
+ <button onClick={handleDelete} className="flex-1 bg-red-600 text-white py-2.5 rounded-lg text-sm font-semibold hover:bg-red-700">Delete</button>
250
+ </div>
251
+ </Modal>
252
+ </div>
253
+ );
254
+ };
255
+
256
+ export default Schedules;