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,124 @@
1
+ import { useState } from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import { useAuth } from '../../context/AuthContext';
4
+
5
+ const Navbar = () => {
6
+ const { user, logout } = useAuth();
7
+ const location = useLocation();
8
+ const [menuOpen, setMenuOpen] = useState(false);
9
+
10
+ const isActive = (path: string) =>
11
+ location.pathname === path
12
+ ? 'bg-yellow-400 text-gray-900 font-semibold'
13
+ : 'text-gray-100 hover:bg-yellow-400 hover:text-gray-900';
14
+
15
+ const handleLogout = async () => {
16
+ await logout();
17
+ };
18
+
19
+ return (
20
+ <nav className="bg-gray-900 shadow-lg sticky top-0 z-50">
21
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
22
+ <div className="flex items-center justify-between h-16">
23
+ {/* Logo */}
24
+ <div className="flex items-center gap-3">
25
+ <div className="w-10 h-10 bg-yellow-400 rounded-full flex items-center justify-center">
26
+ <span className="text-gray-900 font-bold text-lg">Y</span>
27
+ </div>
28
+ <div>
29
+ <p className="text-yellow-400 font-bold text-lg leading-none">Y-Bus</p>
30
+ <p className="text-gray-400 text-xs">Reservation System</p>
31
+ </div>
32
+ </div>
33
+
34
+ {/* Desktop Nav */}
35
+ <div className="hidden md:flex items-center gap-1">
36
+ <Link to="/dashboard" className={`px-4 py-2 rounded-md text-sm transition-colors ${isActive('/dashboard')}`}>
37
+ Dashboard
38
+ </Link>
39
+ <Link to="/buses" className={`px-4 py-2 rounded-md text-sm transition-colors ${isActive('/buses')}`}>
40
+ Buses
41
+ </Link>
42
+ <Link to="/schedules" className={`px-4 py-2 rounded-md text-sm transition-colors ${isActive('/schedules')}`}>
43
+ Schedules
44
+ </Link>
45
+ <Link to="/bookings" className={`px-4 py-2 rounded-md text-sm transition-colors ${isActive('/bookings')}`}>
46
+ Bookings
47
+ </Link>
48
+ <Link to="/report" className={`px-4 py-2 rounded-md text-sm transition-colors ${isActive('/report')}`}>
49
+ Report
50
+ </Link>
51
+ {user?.UserRole === 'admin' && (
52
+ <Link to="/users" className={`px-4 py-2 rounded-md text-sm transition-colors ${isActive('/users')}`}>
53
+ Users
54
+ </Link>
55
+ )}
56
+ </div>
57
+
58
+ {/* User Info */}
59
+ <div className="hidden md:flex items-center gap-3">
60
+ <div className="text-right">
61
+ <p className="text-gray-100 text-sm font-medium">{user?.UserName}</p>
62
+ <p className="text-yellow-400 text-xs capitalize">{user?.UserRole}</p>
63
+ </div>
64
+ <button
65
+ onClick={handleLogout}
66
+ className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm transition-colors"
67
+ >
68
+ Logout
69
+ </button>
70
+ </div>
71
+
72
+ {/* Mobile menu button */}
73
+ <button
74
+ className="md:hidden text-gray-100"
75
+ onClick={() => setMenuOpen(!menuOpen)}
76
+ >
77
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
78
+ {menuOpen
79
+ ? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
80
+ : <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />}
81
+ </svg>
82
+ </button>
83
+ </div>
84
+ </div>
85
+
86
+ {/* Mobile menu */}
87
+ {menuOpen && (
88
+ <div className="md:hidden bg-gray-800 px-4 pb-4 space-y-1">
89
+ {[
90
+ { to: '/dashboard', label: 'Dashboard' },
91
+ { to: '/buses', label: 'Buses' },
92
+ { to: '/schedules', label: 'Schedules' },
93
+ { to: '/bookings', label: 'Bookings' },
94
+ { to: '/report', label: 'Report' },
95
+ ...(user?.UserRole === 'admin' ? [{ to: '/users', label: 'Users' }] : []),
96
+ ].map(({ to, label }) => (
97
+ <Link
98
+ key={to}
99
+ to={to}
100
+ className={`block px-4 py-2 rounded-md text-sm transition-colors ${isActive(to)}`}
101
+ onClick={() => setMenuOpen(false)}
102
+ >
103
+ {label}
104
+ </Link>
105
+ ))}
106
+ <div className="pt-2 border-t border-gray-700 flex items-center justify-between">
107
+ <div>
108
+ <p className="text-gray-100 text-sm">{user?.UserName}</p>
109
+ <p className="text-yellow-400 text-xs capitalize">{user?.UserRole}</p>
110
+ </div>
111
+ <button
112
+ onClick={handleLogout}
113
+ className="bg-red-600 hover:bg-red-700 text-white px-3 py-1.5 rounded-md text-sm"
114
+ >
115
+ Logout
116
+ </button>
117
+ </div>
118
+ </div>
119
+ )}
120
+ </nav>
121
+ );
122
+ };
123
+
124
+ export default Navbar;
@@ -0,0 +1,30 @@
1
+ import { ReactNode } from 'react';
2
+ import { Navigate } from 'react-router-dom';
3
+ import { useAuth } from '../../context/AuthContext';
4
+
5
+ interface ProtectedRouteProps {
6
+ children: ReactNode;
7
+ adminOnly?: boolean;
8
+ }
9
+
10
+ const ProtectedRoute = ({ children, adminOnly = false }: ProtectedRouteProps) => {
11
+ const { user, loading } = useAuth();
12
+
13
+ if (loading) {
14
+ return (
15
+ <div className="min-h-screen flex items-center justify-center bg-gray-50">
16
+ <div className="text-center">
17
+ <div className="w-12 h-12 border-4 border-yellow-400 border-t-transparent rounded-full animate-spin mx-auto" />
18
+ <p className="text-gray-500 mt-3 text-sm">Loading...</p>
19
+ </div>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ if (!user) return <Navigate to="/login" replace />;
25
+ if (adminOnly && user.UserRole !== 'admin') return <Navigate to="/dashboard" replace />;
26
+
27
+ return <>{children}</>;
28
+ };
29
+
30
+ export default ProtectedRoute;
@@ -0,0 +1,24 @@
1
+ interface StatCardProps {
2
+ title: string;
3
+ value: string | number;
4
+ icon: string;
5
+ color: string;
6
+ subtitle?: string;
7
+ }
8
+
9
+ const StatCard = ({ title, value, icon, color, subtitle }: StatCardProps) => {
10
+ return (
11
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center gap-4 hover:shadow-md transition-shadow">
12
+ <div className={`w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${color}`}>
13
+ {icon}
14
+ </div>
15
+ <div>
16
+ <p className="text-gray-500 text-sm font-medium">{title}</p>
17
+ <p className="text-3xl font-bold text-gray-800">{value}</p>
18
+ {subtitle && <p className="text-xs text-gray-400 mt-1">{subtitle}</p>}
19
+ </div>
20
+ </div>
21
+ );
22
+ };
23
+
24
+ export default StatCard;
@@ -0,0 +1,67 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ interface Column<T> {
4
+ key: string;
5
+ label: string;
6
+ render?: (item: T) => ReactNode;
7
+ }
8
+
9
+ interface TableProps<T> {
10
+ columns: Column<T>[];
11
+ data: T[];
12
+ keyExtractor: (item: T) => string | number;
13
+ loading?: boolean;
14
+ emptyMessage?: string;
15
+ }
16
+
17
+ function Table<T>({ columns, data, keyExtractor, loading, emptyMessage = 'No data found' }: TableProps<T>) {
18
+ if (loading) {
19
+ return (
20
+ <div className="flex items-center justify-center py-12">
21
+ <div className="w-10 h-10 border-4 border-yellow-400 border-t-transparent rounded-full animate-spin" />
22
+ </div>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <div className="overflow-x-auto rounded-xl border border-gray-200 shadow-sm">
28
+ <table className="min-w-full divide-y divide-gray-200">
29
+ <thead className="bg-gray-900">
30
+ <tr>
31
+ {columns.map((col) => (
32
+ <th
33
+ key={col.key}
34
+ className="px-4 py-3 text-left text-xs font-semibold text-yellow-400 uppercase tracking-wider"
35
+ >
36
+ {col.label}
37
+ </th>
38
+ ))}
39
+ </tr>
40
+ </thead>
41
+ <tbody className="bg-white divide-y divide-gray-100">
42
+ {data.length === 0 ? (
43
+ <tr>
44
+ <td colSpan={columns.length} className="px-4 py-8 text-center text-gray-400 text-sm">
45
+ {emptyMessage}
46
+ </td>
47
+ </tr>
48
+ ) : (
49
+ data.map((item) => (
50
+ <tr key={keyExtractor(item)} className="hover:bg-yellow-50 transition-colors">
51
+ {columns.map((col) => (
52
+ <td key={col.key} className="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
53
+ {col.render
54
+ ? col.render(item)
55
+ : String((item as Record<string, unknown>)[col.key] ?? '')}
56
+ </td>
57
+ ))}
58
+ </tr>
59
+ ))
60
+ )}
61
+ </tbody>
62
+ </table>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export default Table;
@@ -0,0 +1,41 @@
1
+ import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
2
+ import { User, AuthContextType } from '../types';
3
+ import { login as apiLogin, logout as apiLogout, getSession } from '../api/auth';
4
+
5
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
6
+
7
+ export const AuthProvider = ({ children }: { children: ReactNode }) => {
8
+ const [user, setUser] = useState<User | null>(null);
9
+ const [loading, setLoading] = useState(true);
10
+
11
+ useEffect(() => {
12
+ const checkSession = async () => {
13
+ const sessionUser = await getSession();
14
+ setUser(sessionUser);
15
+ setLoading(false);
16
+ };
17
+ checkSession();
18
+ }, []);
19
+
20
+ const login = async (username: string, password: string) => {
21
+ const loggedUser = await apiLogin(username, password);
22
+ setUser(loggedUser);
23
+ };
24
+
25
+ const logout = async () => {
26
+ await apiLogout();
27
+ setUser(null);
28
+ };
29
+
30
+ return (
31
+ <AuthContext.Provider value={{ user, login, logout, loading }}>
32
+ {children}
33
+ </AuthContext.Provider>
34
+ );
35
+ };
36
+
37
+ export const useAuth = (): AuthContextType => {
38
+ const context = useContext(AuthContext);
39
+ if (!context) throw new Error('useAuth must be used within AuthProvider');
40
+ return context;
41
+ };
@@ -0,0 +1,43 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Custom scrollbar */
4
+ ::-webkit-scrollbar {
5
+ width: 6px;
6
+ height: 6px;
7
+ }
8
+ ::-webkit-scrollbar-track {
9
+ background: #f1f5f9;
10
+ }
11
+ ::-webkit-scrollbar-thumb {
12
+ background: #94a3b8;
13
+ border-radius: 3px;
14
+ }
15
+ ::-webkit-scrollbar-thumb:hover {
16
+ background: #64748b;
17
+ }
18
+
19
+ /* Print styles */
20
+ @media print {
21
+ nav, .print\:hidden {
22
+ display: none !important;
23
+ }
24
+ body {
25
+ background: white;
26
+ }
27
+ .print\:block {
28
+ display: block !important;
29
+ }
30
+ }
31
+
32
+ /* Smooth transitions */
33
+ * {
34
+ transition-property: color, background-color, border-color, box-shadow;
35
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
36
+ transition-duration: 150ms;
37
+ }
38
+
39
+ /* Focus ring */
40
+ *:focus-visible {
41
+ outline: 2px solid #facc15;
42
+ outline-offset: 2px;
43
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "./index.css";
4
+ import App from "./App";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
@@ -0,0 +1,295 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { getBookings, createBooking, updateBooking, deleteBooking } from '../api/bookings';
3
+ import { getSchedules } from '../api/schedules';
4
+ import { Booking, Schedule } from '../types';
5
+ import Table from '../components/common/Table';
6
+ import Modal from '../components/common/Modal';
7
+ import { useAuth } from '../context/AuthContext';
8
+
9
+ const defaultForm = {
10
+ ScheduleID: 0,
11
+ UserID: 0,
12
+ PassengerName: '',
13
+ PassengerGender: 'Male' as 'Male' | 'Female' | 'Other',
14
+ PassengerPhone: '',
15
+ SeatNumber: '',
16
+ PaymentStatus: 'Pending' as 'Pending' | 'Paid' | 'Cancelled',
17
+ BookingDate: new Date().toISOString().split('T')[0],
18
+ };
19
+
20
+ const Bookings = () => {
21
+ const { user } = useAuth();
22
+ const [bookings, setBookings] = useState<Booking[]>([]);
23
+ const [schedules, setSchedules] = useState<Schedule[]>([]);
24
+ const [loading, setLoading] = useState(true);
25
+ const [modalOpen, setModalOpen] = useState(false);
26
+ const [deleteModalOpen, setDeleteModalOpen] = useState(false);
27
+ const [viewModalOpen, setViewModalOpen] = useState(false);
28
+ const [editBooking, setEditBooking] = useState<Booking | null>(null);
29
+ const [viewBooking, setViewBooking] = useState<Booking | null>(null);
30
+ const [deleteId, setDeleteId] = useState<number | null>(null);
31
+ const [form, setForm] = useState(defaultForm);
32
+ const [saving, setSaving] = useState(false);
33
+ const [error, setError] = useState('');
34
+ const [search, setSearch] = useState('');
35
+ const [paymentFilter, setPaymentFilter] = useState('All');
36
+
37
+ const fetchData = async () => {
38
+ setLoading(true);
39
+ try {
40
+ const [b, s] = await Promise.all([getBookings(), getSchedules()]);
41
+ setBookings(b);
42
+ setSchedules(s.filter((sc) => sc.ScheduleStatus === 'Active'));
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ };
47
+
48
+ useEffect(() => { fetchData(); }, []);
49
+
50
+ const openAdd = () => {
51
+ setEditBooking(null);
52
+ setForm({ ...defaultForm, ScheduleID: schedules[0]?.ScheduleID || 0, UserID: user?.UserID || 0 });
53
+ setError('');
54
+ setModalOpen(true);
55
+ };
56
+
57
+ const openEdit = (b: Booking) => {
58
+ setEditBooking(b);
59
+ setForm({
60
+ ScheduleID: b.ScheduleID,
61
+ UserID: b.UserID,
62
+ PassengerName: b.PassengerName,
63
+ PassengerGender: b.PassengerGender,
64
+ PassengerPhone: b.PassengerPhone,
65
+ SeatNumber: b.SeatNumber,
66
+ PaymentStatus: b.PaymentStatus,
67
+ BookingDate: b.BookingDate?.split('T')[0] || new Date().toISOString().split('T')[0],
68
+ });
69
+ setError('');
70
+ setModalOpen(true);
71
+ };
72
+
73
+ const handleSubmit = async (e: React.FormEvent) => {
74
+ e.preventDefault();
75
+ setSaving(true);
76
+ setError('');
77
+ try {
78
+ if (editBooking) {
79
+ await updateBooking(editBooking.BookingID, form);
80
+ } else {
81
+ await createBooking({ ...form, UserID: user?.UserID || 0 });
82
+ }
83
+ setModalOpen(false);
84
+ fetchData();
85
+ } catch (err: unknown) {
86
+ setError(err instanceof Error ? err.message : 'An error occurred');
87
+ } finally {
88
+ setSaving(false);
89
+ }
90
+ };
91
+
92
+ const handleDelete = async () => {
93
+ if (!deleteId) return;
94
+ try {
95
+ await deleteBooking(deleteId);
96
+ setDeleteModalOpen(false);
97
+ fetchData();
98
+ } catch (err: unknown) {
99
+ alert(err instanceof Error ? err.message : 'Delete failed');
100
+ }
101
+ };
102
+
103
+ const filtered = bookings.filter((b) => {
104
+ const matchSearch =
105
+ b.PassengerName.toLowerCase().includes(search.toLowerCase()) ||
106
+ (b.RouteName || '').toLowerCase().includes(search.toLowerCase()) ||
107
+ b.SeatNumber.toLowerCase().includes(search.toLowerCase()) ||
108
+ b.PassengerPhone.includes(search);
109
+ const matchPayment = paymentFilter === 'All' || b.PaymentStatus === paymentFilter;
110
+ return matchSearch && matchPayment;
111
+ });
112
+
113
+ const columns = [
114
+ { key: 'BookingID', label: '#' },
115
+ { key: 'PassengerName', label: 'Passenger' },
116
+ { key: 'PassengerGender', label: 'Gender' },
117
+ { key: 'PassengerPhone', label: 'Phone' },
118
+ { key: 'RouteName', label: 'Route' },
119
+ { key: 'SeatNumber', label: 'Seat' },
120
+ { key: 'PaymentStatus', label: 'Payment', render: (b: Booking) => (
121
+ <span className={`px-2 py-1 rounded-full text-xs font-medium ${
122
+ b.PaymentStatus === 'Paid' ? 'bg-green-100 text-green-700'
123
+ : b.PaymentStatus === 'Cancelled' ? 'bg-red-100 text-red-700'
124
+ : 'bg-yellow-100 text-yellow-700'
125
+ }`}>{b.PaymentStatus}</span>
126
+ )},
127
+ { key: 'BookingDate', label: 'Date', render: (b: Booking) =>
128
+ b.BookingDate ? new Date(b.BookingDate).toLocaleDateString('en-RW') : 'N/A'
129
+ },
130
+ {
131
+ key: 'actions', label: 'Actions',
132
+ render: (b: Booking) => (
133
+ <div className="flex gap-2">
134
+ <button onClick={() => { setViewBooking(b); setViewModalOpen(true); }} className="bg-gray-50 hover:bg-gray-100 text-gray-700 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors">View</button>
135
+ <button onClick={() => openEdit(b)} 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>
136
+ <button onClick={() => { setDeleteId(b.BookingID); 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>
137
+ </div>
138
+ ),
139
+ },
140
+ ];
141
+
142
+ return (
143
+ <div className="space-y-5">
144
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
145
+ <div>
146
+ <h1 className="text-2xl font-bold text-gray-800">Passenger Bookings</h1>
147
+ <p className="text-gray-500 text-sm mt-1">Register and manage passenger reservations</p>
148
+ </div>
149
+ <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 transition-colors shadow-sm">
150
+ + New Booking
151
+ </button>
152
+ </div>
153
+
154
+ <div className="flex flex-col sm:flex-row gap-3">
155
+ <input
156
+ type="text"
157
+ placeholder="Search by passenger, route, seat, or phone..."
158
+ value={search}
159
+ onChange={(e) => setSearch(e.target.value)}
160
+ 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"
161
+ />
162
+ <div className="flex gap-2">
163
+ {['All', 'Pending', 'Paid', 'Cancelled'].map((s) => (
164
+ <button
165
+ key={s}
166
+ onClick={() => setPaymentFilter(s)}
167
+ className={`px-3 py-2 rounded-lg text-xs font-medium transition-colors ${
168
+ paymentFilter === s ? 'bg-gray-900 text-yellow-400' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
169
+ }`}
170
+ >
171
+ {s}
172
+ </button>
173
+ ))}
174
+ </div>
175
+ </div>
176
+
177
+ <Table columns={columns} data={filtered} keyExtractor={(b) => b.BookingID} loading={loading} emptyMessage="No bookings found." />
178
+
179
+ {/* View Modal */}
180
+ <Modal isOpen={viewModalOpen} onClose={() => setViewModalOpen(false)} title="Booking Details" size="md">
181
+ {viewBooking && (
182
+ <div className="space-y-4">
183
+ <div className="grid grid-cols-2 gap-4">
184
+ {[
185
+ { label: 'Booking ID', value: `#${viewBooking.BookingID}` },
186
+ { label: 'Booking Date', value: viewBooking.BookingDate ? new Date(viewBooking.BookingDate).toLocaleDateString() : 'N/A' },
187
+ { label: 'Passenger Name', value: viewBooking.PassengerName },
188
+ { label: 'Gender', value: viewBooking.PassengerGender },
189
+ { label: 'Phone', value: viewBooking.PassengerPhone },
190
+ { label: 'Seat Number', value: viewBooking.SeatNumber },
191
+ { label: 'Route', value: viewBooking.RouteName || 'N/A' },
192
+ { label: 'Departure', value: viewBooking.DeparturePoint || 'N/A' },
193
+ { label: 'Destination', value: viewBooking.Destination || 'N/A' },
194
+ { label: 'Bus Plate', value: viewBooking.PlateNumber || 'N/A' },
195
+ { label: 'Ticket Price', value: viewBooking.TicketPrice ? `${Number(viewBooking.TicketPrice).toLocaleString()} RWF` : 'N/A' },
196
+ { label: 'Booked By', value: viewBooking.UserName || 'N/A' },
197
+ ].map(({ label, value }) => (
198
+ <div key={label}>
199
+ <p className="text-xs text-gray-400 font-medium">{label}</p>
200
+ <p className="text-sm text-gray-800 font-semibold mt-0.5">{value}</p>
201
+ </div>
202
+ ))}
203
+ </div>
204
+ <div>
205
+ <p className="text-xs text-gray-400 font-medium">Payment Status</p>
206
+ <span className={`mt-1 inline-block px-3 py-1 rounded-full text-sm font-medium ${
207
+ viewBooking.PaymentStatus === 'Paid' ? 'bg-green-100 text-green-700'
208
+ : viewBooking.PaymentStatus === 'Cancelled' ? 'bg-red-100 text-red-700'
209
+ : 'bg-yellow-100 text-yellow-700'
210
+ }`}>{viewBooking.PaymentStatus}</span>
211
+ </div>
212
+ </div>
213
+ )}
214
+ </Modal>
215
+
216
+ {/* Add/Edit Modal */}
217
+ <Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title={editBooking ? 'Edit Booking' : 'New Passenger Booking'} size="lg">
218
+ <form onSubmit={handleSubmit} className="space-y-4">
219
+ {error && <div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-600">{error}</div>}
220
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
221
+ <div className="sm:col-span-2">
222
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Schedule (Route) *</label>
223
+ <select required value={form.ScheduleID} onChange={(e) => setForm({ ...form, ScheduleID: parseInt(e.target.value) })}
224
+ 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">
225
+ <option value={0}>Select Schedule</option>
226
+ {schedules.map((s) => (
227
+ <option key={s.ScheduleID} value={s.ScheduleID}>
228
+ {s.RouteName} — {s.DeparturePoint} → {s.Destination} ({s.PlateNumber})
229
+ </option>
230
+ ))}
231
+ </select>
232
+ </div>
233
+ <div>
234
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Passenger Name *</label>
235
+ <input required type="text" value={form.PassengerName} onChange={(e) => setForm({ ...form, PassengerName: e.target.value })}
236
+ 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"
237
+ placeholder="Full name" />
238
+ </div>
239
+ <div>
240
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Gender *</label>
241
+ <select value={form.PassengerGender} onChange={(e) => setForm({ ...form, PassengerGender: e.target.value as 'Male' | 'Female' | 'Other' })}
242
+ 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">
243
+ <option value="Male">Male</option>
244
+ <option value="Female">Female</option>
245
+ <option value="Other">Other</option>
246
+ </select>
247
+ </div>
248
+ <div>
249
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Phone Number *</label>
250
+ <input required type="tel" value={form.PassengerPhone} onChange={(e) => setForm({ ...form, PassengerPhone: e.target.value })}
251
+ 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"
252
+ placeholder="+250..." />
253
+ </div>
254
+ <div>
255
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Seat Number *</label>
256
+ <input required type="text" value={form.SeatNumber} onChange={(e) => setForm({ ...form, SeatNumber: e.target.value.toUpperCase() })}
257
+ 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"
258
+ placeholder="e.g. A1, B3" />
259
+ </div>
260
+ <div>
261
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Payment Status *</label>
262
+ <select value={form.PaymentStatus} onChange={(e) => setForm({ ...form, PaymentStatus: e.target.value as 'Pending' | 'Paid' | 'Cancelled' })}
263
+ 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">
264
+ <option value="Pending">Pending</option>
265
+ <option value="Paid">Paid</option>
266
+ <option value="Cancelled">Cancelled</option>
267
+ </select>
268
+ </div>
269
+ <div>
270
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Booking Date *</label>
271
+ <input required type="date" value={form.BookingDate} onChange={(e) => setForm({ ...form, BookingDate: e.target.value })}
272
+ 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" />
273
+ </div>
274
+ </div>
275
+ <div className="flex gap-3 pt-2">
276
+ <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>
277
+ <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">
278
+ {saving ? 'Saving...' : editBooking ? 'Update Booking' : 'Create Booking'}
279
+ </button>
280
+ </div>
281
+ </form>
282
+ </Modal>
283
+
284
+ <Modal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} title="Confirm Delete" size="sm">
285
+ <p className="text-gray-600 text-sm mb-4">Are you sure you want to delete this booking?</p>
286
+ <div className="flex gap-3">
287
+ <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>
288
+ <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>
289
+ </div>
290
+ </Modal>
291
+ </div>
292
+ );
293
+ };
294
+
295
+ export default Bookings;