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.
- package/.gitignore +19 -0
- package/README.md +40 -0
- package/backend-project/.env.example +11 -0
- package/backend-project/database/schema.sql +101 -0
- package/backend-project/db.js +13 -0
- package/backend-project/middleware/auth.js +24 -0
- package/backend-project/package.json +21 -0
- package/backend-project/routes/auth.js +55 -0
- package/backend-project/routes/bookings.js +151 -0
- package/backend-project/routes/buses.js +81 -0
- package/backend-project/routes/schedules.js +95 -0
- package/backend-project/routes/users.js +86 -0
- package/backend-project/server.js +54 -0
- package/bin/create-sales.js +102 -0
- package/frontend-project/index.html +12 -0
- package/frontend-project/package.json +31 -0
- package/frontend-project/src/App.tsx +77 -0
- package/frontend-project/src/api/auth.ts +20 -0
- package/frontend-project/src/api/axios.ts +11 -0
- package/frontend-project/src/api/bookings.ts +31 -0
- package/frontend-project/src/api/buses.ts +26 -0
- package/frontend-project/src/api/schedules.ts +26 -0
- package/frontend-project/src/api/users.ts +21 -0
- package/frontend-project/src/components/common/Layout.tsx +15 -0
- package/frontend-project/src/components/common/Modal.tsx +42 -0
- package/frontend-project/src/components/common/Navbar.tsx +124 -0
- package/frontend-project/src/components/common/ProtectedRoute.tsx +30 -0
- package/frontend-project/src/components/common/StatCard.tsx +24 -0
- package/frontend-project/src/components/common/Table.tsx +67 -0
- package/frontend-project/src/context/AuthContext.tsx +41 -0
- package/frontend-project/src/index.css +43 -0
- package/frontend-project/src/main.tsx +10 -0
- package/frontend-project/src/pages/Bookings.tsx +295 -0
- package/frontend-project/src/pages/Buses.tsx +255 -0
- package/frontend-project/src/pages/Dashboard.tsx +197 -0
- package/frontend-project/src/pages/Login.tsx +112 -0
- package/frontend-project/src/pages/Report.tsx +252 -0
- package/frontend-project/src/pages/Schedules.tsx +256 -0
- package/frontend-project/src/pages/Users.tsx +199 -0
- package/frontend-project/src/types/index.ts +63 -0
- package/frontend-project/src/utils/cn.ts +6 -0
- package/frontend-project/tsconfig.json +31 -0
- package/frontend-project/vite.config.ts +19 -0
- package/package.json +50 -0
package/.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
.env
|
|
5
|
+
.env.local
|
|
6
|
+
.env.*.local
|
|
7
|
+
coverage/
|
|
8
|
+
.DS_Store
|
|
9
|
+
Thumbs.db
|
|
10
|
+
npm-debug.log*
|
|
11
|
+
yarn-debug.log*
|
|
12
|
+
yarn-error.log*
|
|
13
|
+
pnpm-debug.log*
|
|
14
|
+
|
|
15
|
+
frontend-project/node_modules/
|
|
16
|
+
frontend-project/dist/
|
|
17
|
+
frontend-project/.env
|
|
18
|
+
backend-project/node_modules/
|
|
19
|
+
backend-project/.env
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# create-sales
|
|
2
|
+
|
|
3
|
+
This repository is prepared as an npm create initializer for a full-stack bus reservation system.
|
|
4
|
+
|
|
5
|
+
Use it with:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create sales my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What is included
|
|
12
|
+
|
|
13
|
+
- `frontend-project/` - React + Vite + TypeScript + Tailwind CSS client
|
|
14
|
+
- `backend-project/` - Express + MySQL + session-auth API
|
|
15
|
+
- Shared template documentation and workspace scripts at the repository root
|
|
16
|
+
|
|
17
|
+
## Publishing notes
|
|
18
|
+
|
|
19
|
+
The repository root is the npm package entry point. It is configured with workspaces so the frontend and backend can be installed and run from one place.
|
|
20
|
+
|
|
21
|
+
The published package name must be `create-sales` so npm can resolve `npm create sales` correctly.
|
|
22
|
+
|
|
23
|
+
## Getting started after unpacking the template
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
npm run dev:backend
|
|
28
|
+
npm run dev:frontend
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Backend environment
|
|
32
|
+
|
|
33
|
+
Copy `backend-project/.env.example` to `backend-project/.env` and adjust the values for your local MySQL server.
|
|
34
|
+
|
|
35
|
+
## Available root scripts
|
|
36
|
+
|
|
37
|
+
- `npm run dev:frontend`
|
|
38
|
+
- `npm run build:frontend`
|
|
39
|
+
- `npm run dev:backend`
|
|
40
|
+
- `npm run start:backend`
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
DB_HOST=localhost
|
|
2
|
+
DB_USER=root
|
|
3
|
+
DB_PASS=
|
|
4
|
+
DB_NAME=y_bus_booking
|
|
5
|
+
SESSION_SECRET=ybus_secret_key_2026
|
|
6
|
+
PORT=5000DB_HOST=localhost
|
|
7
|
+
DB_USER=root
|
|
8
|
+
DB_PASS=your_mysql_password
|
|
9
|
+
DB_NAME=y_bus_booking
|
|
10
|
+
SESSION_SECRET=ybus_super_secret_key_2026
|
|
11
|
+
PORT=5000
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- Y Bus Reservation System - Database Schema
|
|
3
|
+
-- Database: y_bus_booking
|
|
4
|
+
-- ============================================================
|
|
5
|
+
|
|
6
|
+
CREATE DATABASE IF NOT EXISTS y_bus_booking
|
|
7
|
+
CHARACTER SET utf8mb4
|
|
8
|
+
COLLATE utf8mb4_unicode_ci;
|
|
9
|
+
|
|
10
|
+
USE y_bus_booking;
|
|
11
|
+
|
|
12
|
+
-- ─── Table: yk_buses ────────────────────────────────────────
|
|
13
|
+
CREATE TABLE IF NOT EXISTS yk_buses (
|
|
14
|
+
BusID INT AUTO_INCREMENT PRIMARY KEY,
|
|
15
|
+
PlateNumber VARCHAR(20) NOT NULL UNIQUE,
|
|
16
|
+
TotalSeats INT NOT NULL DEFAULT 30,
|
|
17
|
+
BusType ENUM('Standard', 'Express', 'Luxury', 'Mini') NOT NULL DEFAULT 'Standard',
|
|
18
|
+
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
19
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
20
|
+
|
|
21
|
+
-- ─── Table: yk_users ────────────────────────────────────────
|
|
22
|
+
CREATE TABLE IF NOT EXISTS yk_users (
|
|
23
|
+
UserID INT AUTO_INCREMENT PRIMARY KEY,
|
|
24
|
+
UserName VARCHAR(100) NOT NULL UNIQUE,
|
|
25
|
+
Password VARCHAR(255) NOT NULL,
|
|
26
|
+
UserRole ENUM('admin', 'agent') NOT NULL DEFAULT 'agent',
|
|
27
|
+
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
28
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
29
|
+
|
|
30
|
+
-- ─── Table: yk_schedules ─────────────────────────────────────
|
|
31
|
+
CREATE TABLE IF NOT EXISTS yk_schedules (
|
|
32
|
+
ScheduleID INT AUTO_INCREMENT PRIMARY KEY,
|
|
33
|
+
BusID INT NOT NULL,
|
|
34
|
+
RouteName VARCHAR(150) NOT NULL,
|
|
35
|
+
DeparturePoint VARCHAR(150) NOT NULL,
|
|
36
|
+
Destination VARCHAR(150) NOT NULL,
|
|
37
|
+
DepartureTime DATETIME NOT NULL,
|
|
38
|
+
EstimatedArrivalTime DATETIME NOT NULL,
|
|
39
|
+
TicketPrice DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
|
40
|
+
ScheduleStatus ENUM('Active', 'Inactive', 'Cancelled') NOT NULL DEFAULT 'Active',
|
|
41
|
+
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
42
|
+
CONSTRAINT fk_schedule_bus FOREIGN KEY (BusID) REFERENCES yk_buses(BusID)
|
|
43
|
+
ON UPDATE CASCADE ON DELETE RESTRICT
|
|
44
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
45
|
+
|
|
46
|
+
-- ─── Table: yk_bookings ──────────────────────────────────────
|
|
47
|
+
CREATE TABLE IF NOT EXISTS yk_bookings (
|
|
48
|
+
BookingID INT AUTO_INCREMENT PRIMARY KEY,
|
|
49
|
+
ScheduleID INT NOT NULL,
|
|
50
|
+
UserID INT NOT NULL,
|
|
51
|
+
PassengerName VARCHAR(150) NOT NULL,
|
|
52
|
+
PassengerGender ENUM('Male', 'Female', 'Other') NOT NULL,
|
|
53
|
+
PassengerPhone VARCHAR(20) NOT NULL,
|
|
54
|
+
SeatNumber VARCHAR(10) NOT NULL,
|
|
55
|
+
PaymentStatus ENUM('Pending', 'Paid', 'Cancelled') NOT NULL DEFAULT 'Pending',
|
|
56
|
+
BookingDate DATE NOT NULL,
|
|
57
|
+
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
58
|
+
CONSTRAINT fk_booking_schedule FOREIGN KEY (ScheduleID) REFERENCES yk_schedules(ScheduleID)
|
|
59
|
+
ON UPDATE CASCADE ON DELETE RESTRICT,
|
|
60
|
+
CONSTRAINT fk_booking_user FOREIGN KEY (UserID) REFERENCES yk_users(UserID)
|
|
61
|
+
ON UPDATE CASCADE ON DELETE RESTRICT
|
|
62
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
63
|
+
|
|
64
|
+
-- ─── Indexes for performance ─────────────────────────────────
|
|
65
|
+
CREATE INDEX idx_bookings_schedule ON yk_bookings(ScheduleID);
|
|
66
|
+
CREATE INDEX idx_bookings_user ON yk_bookings(UserID);
|
|
67
|
+
CREATE INDEX idx_schedules_bus ON yk_schedules(BusID);
|
|
68
|
+
CREATE INDEX idx_schedules_status ON yk_schedules(ScheduleStatus);
|
|
69
|
+
|
|
70
|
+
-- ─── Seed Data ───────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
-- Default admin user (password: admin123)
|
|
73
|
+
INSERT INTO yk_users (UserName, Password, UserRole) VALUES
|
|
74
|
+
('admin', '$2a$10$rBnT2rqbZTMcFKRnVpeTG.qvdV.LKQEm/5DYuCNiUEFbCGVgD/Yme', 'admin'),
|
|
75
|
+
('agent1', '$2a$10$rBnT2rqbZTMcFKRnVpeTG.qvdV.LKQEm/5DYuCNiUEFbCGVgD/Yme', 'agent');
|
|
76
|
+
-- Both passwords above are hashed 'admin123'
|
|
77
|
+
|
|
78
|
+
-- Sample Buses
|
|
79
|
+
INSERT INTO yk_buses (PlateNumber, TotalSeats, BusType) VALUES
|
|
80
|
+
('RAD 123 A', 45, 'Standard'),
|
|
81
|
+
('RAB 456 B', 30, 'Express'),
|
|
82
|
+
('RAC 789 C', 55, 'Luxury'),
|
|
83
|
+
('RAE 012 D', 20, 'Mini');
|
|
84
|
+
|
|
85
|
+
-- Sample Schedules
|
|
86
|
+
INSERT INTO yk_schedules (BusID, RouteName, DeparturePoint, Destination, DepartureTime, EstimatedArrivalTime, TicketPrice, ScheduleStatus) VALUES
|
|
87
|
+
(1, 'KGL-BTR Route 1', 'Nyabugogo', 'Butare', '2026-02-01 06:00:00', '2026-02-01 09:00:00', 5000.00, 'Active'),
|
|
88
|
+
(2, 'KGL-MUS Route 1', 'Nyabugogo', 'Musanze', '2026-02-01 07:30:00', '2026-02-01 10:30:00', 4500.00, 'Active'),
|
|
89
|
+
(3, 'KGL-GHY Route 1', 'Downtown Kigali', 'Gisenyi', '2026-02-01 08:00:00', '2026-02-01 12:00:00', 7000.00, 'Active'),
|
|
90
|
+
(4, 'KGL-RWM Route 1', 'Nyabugogo', 'Rwamagana', '2026-02-01 09:00:00', '2026-02-01 10:30:00', 2500.00, 'Active');
|
|
91
|
+
|
|
92
|
+
-- Sample Bookings
|
|
93
|
+
INSERT INTO yk_bookings (ScheduleID, UserID, PassengerName, PassengerGender, PassengerPhone, SeatNumber, PaymentStatus, BookingDate) VALUES
|
|
94
|
+
(1, 1, 'Jean Pierre Habimana', 'Male', '+250788123456', 'A1', 'Paid', '2026-01-28'),
|
|
95
|
+
(1, 1, 'Marie Claire Uwimana', 'Female', '+250788654321', 'A2', 'Paid', '2026-01-28'),
|
|
96
|
+
(1, 2, 'Alexis Nkurunziza', 'Male', '+250789123456', 'B1', 'Pending', '2026-01-29'),
|
|
97
|
+
(2, 1, 'Diane Mukamana', 'Female', '+250788999111', 'A1', 'Paid', '2026-01-29'),
|
|
98
|
+
(2, 2, 'Patrick Mugisha', 'Male', '+250788444555', 'A2', 'Cancelled', '2026-01-30'),
|
|
99
|
+
(3, 1, 'Sandrine Uwase', 'Female', '+250789777888', 'C1', 'Paid', '2026-01-30'),
|
|
100
|
+
(3, 2, 'Eric Nshimiyimana', 'Male', '+250788333222', 'C2', 'Pending', '2026-01-31'),
|
|
101
|
+
(4, 1, 'Claudine Ingabire', 'Female', '+250789555666', 'A1', 'Paid', '2026-01-31');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const mysql = require('mysql2/promise');
|
|
2
|
+
|
|
3
|
+
const pool = mysql.createPool({
|
|
4
|
+
host: process.env.DB_HOST || 'localhost',
|
|
5
|
+
user: process.env.DB_USER || 'root',
|
|
6
|
+
password: process.env.DB_PASS || '',
|
|
7
|
+
database: process.env.DB_NAME || 'y_bus_booking',
|
|
8
|
+
waitForConnections: true,
|
|
9
|
+
connectionLimit: 10,
|
|
10
|
+
queueLimit: 0,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
module.exports = pool;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware: Require authenticated session
|
|
3
|
+
*/
|
|
4
|
+
const requireAuth = (req, res, next) => {
|
|
5
|
+
if (!req.session || !req.session.user) {
|
|
6
|
+
return res.status(401).json({ message: 'Unauthorized. Please log in.' });
|
|
7
|
+
}
|
|
8
|
+
next();
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Middleware: Require admin role
|
|
13
|
+
*/
|
|
14
|
+
const requireAdmin = (req, res, next) => {
|
|
15
|
+
if (!req.session || !req.session.user) {
|
|
16
|
+
return res.status(401).json({ message: 'Unauthorized. Please log in.' });
|
|
17
|
+
}
|
|
18
|
+
if (req.session.user.UserRole !== 'admin') {
|
|
19
|
+
return res.status(403).json({ message: 'Forbidden. Admin access required.' });
|
|
20
|
+
}
|
|
21
|
+
next();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
module.exports = { requireAuth, requireAdmin };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend-project",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Y Bus Reservation System - Backend API",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node server.js",
|
|
9
|
+
"dev": "nodemon server.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"bcryptjs": "^2.4.3",
|
|
13
|
+
"cors": "^2.8.5",
|
|
14
|
+
"express": "^4.18.2",
|
|
15
|
+
"express-session": "^1.17.3",
|
|
16
|
+
"mysql2": "^3.6.5"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"nodemon": "^3.0.2"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const bcrypt = require('bcryptjs');
|
|
4
|
+
const pool = require('../db');
|
|
5
|
+
|
|
6
|
+
// POST /api/auth/login
|
|
7
|
+
router.post('/login', async (req, res) => {
|
|
8
|
+
const { UserName, Password } = req.body;
|
|
9
|
+
if (!UserName || !Password) {
|
|
10
|
+
return res.status(400).json({ message: 'Username and password are required.' });
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const [rows] = await pool.execute(
|
|
14
|
+
'SELECT * FROM yk_users WHERE UserName = ?',
|
|
15
|
+
[UserName]
|
|
16
|
+
);
|
|
17
|
+
if (rows.length === 0) {
|
|
18
|
+
return res.status(401).json({ message: 'Invalid username or password.' });
|
|
19
|
+
}
|
|
20
|
+
const user = rows[0];
|
|
21
|
+
const isMatch = await bcrypt.compare(Password, user.Password);
|
|
22
|
+
if (!isMatch) {
|
|
23
|
+
return res.status(401).json({ message: 'Invalid username or password.' });
|
|
24
|
+
}
|
|
25
|
+
// Create session
|
|
26
|
+
req.session.user = {
|
|
27
|
+
UserID: user.UserID,
|
|
28
|
+
UserName: user.UserName,
|
|
29
|
+
UserRole: user.UserRole,
|
|
30
|
+
};
|
|
31
|
+
res.json({ message: 'Login successful', user: req.session.user });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(err);
|
|
34
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// POST /api/auth/logout
|
|
39
|
+
router.post('/logout', (req, res) => {
|
|
40
|
+
req.session.destroy((err) => {
|
|
41
|
+
if (err) return res.status(500).json({ message: 'Logout failed' });
|
|
42
|
+
res.clearCookie('connect.sid');
|
|
43
|
+
res.json({ message: 'Logged out successfully' });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// GET /api/auth/session
|
|
48
|
+
router.get('/session', (req, res) => {
|
|
49
|
+
if (req.session && req.session.user) {
|
|
50
|
+
return res.json({ user: req.session.user });
|
|
51
|
+
}
|
|
52
|
+
res.status(401).json({ message: 'No active session' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
module.exports = router;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const pool = require('../db');
|
|
4
|
+
const { requireAuth } = require('../middleware/auth');
|
|
5
|
+
|
|
6
|
+
// GET /api/bookings/report/passengers — Report grouped by schedule
|
|
7
|
+
router.get('/report/passengers', requireAuth, async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const [rows] = await pool.execute(`
|
|
10
|
+
SELECT
|
|
11
|
+
b.*,
|
|
12
|
+
s.RouteName, s.DeparturePoint, s.Destination, s.DepartureTime,
|
|
13
|
+
s.TicketPrice, s.ScheduleStatus,
|
|
14
|
+
bs.PlateNumber, bs.BusType,
|
|
15
|
+
u.UserName
|
|
16
|
+
FROM yk_bookings b
|
|
17
|
+
LEFT JOIN yk_schedules s ON b.ScheduleID = s.ScheduleID
|
|
18
|
+
LEFT JOIN yk_buses bs ON s.BusID = bs.BusID
|
|
19
|
+
LEFT JOIN yk_users u ON b.UserID = u.UserID
|
|
20
|
+
ORDER BY s.ScheduleID, b.BookingDate
|
|
21
|
+
`);
|
|
22
|
+
res.json(rows);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// GET /api/bookings — Get all bookings
|
|
29
|
+
router.get('/', requireAuth, async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const [rows] = await pool.execute(`
|
|
32
|
+
SELECT
|
|
33
|
+
b.*,
|
|
34
|
+
s.RouteName, s.DeparturePoint, s.Destination, s.DepartureTime,
|
|
35
|
+
s.TicketPrice,
|
|
36
|
+
bs.PlateNumber,
|
|
37
|
+
u.UserName
|
|
38
|
+
FROM yk_bookings b
|
|
39
|
+
LEFT JOIN yk_schedules s ON b.ScheduleID = s.ScheduleID
|
|
40
|
+
LEFT JOIN yk_buses bs ON s.BusID = bs.BusID
|
|
41
|
+
LEFT JOIN yk_users u ON b.UserID = u.UserID
|
|
42
|
+
ORDER BY b.BookingID DESC
|
|
43
|
+
`);
|
|
44
|
+
res.json(rows);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// GET /api/bookings/:id — Get single booking
|
|
51
|
+
router.get('/:id', requireAuth, async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const [rows] = await pool.execute(`
|
|
54
|
+
SELECT
|
|
55
|
+
b.*,
|
|
56
|
+
s.RouteName, s.DeparturePoint, s.Destination, s.DepartureTime,
|
|
57
|
+
s.TicketPrice,
|
|
58
|
+
bs.PlateNumber,
|
|
59
|
+
u.UserName
|
|
60
|
+
FROM yk_bookings b
|
|
61
|
+
LEFT JOIN yk_schedules s ON b.ScheduleID = s.ScheduleID
|
|
62
|
+
LEFT JOIN yk_buses bs ON s.BusID = bs.BusID
|
|
63
|
+
LEFT JOIN yk_users u ON b.UserID = u.UserID
|
|
64
|
+
WHERE b.BookingID = ?
|
|
65
|
+
`, [req.params.id]);
|
|
66
|
+
if (rows.length === 0) return res.status(404).json({ message: 'Booking not found' });
|
|
67
|
+
res.json(rows[0]);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// POST /api/bookings — Create booking
|
|
74
|
+
router.post('/', requireAuth, async (req, res) => {
|
|
75
|
+
const { ScheduleID, PassengerName, PassengerGender, PassengerPhone, SeatNumber, PaymentStatus, BookingDate } = req.body;
|
|
76
|
+
const UserID = req.session.user.UserID;
|
|
77
|
+
|
|
78
|
+
if (!ScheduleID || !PassengerName || !PassengerGender || !PassengerPhone || !SeatNumber) {
|
|
79
|
+
return res.status(400).json({ message: 'All required fields must be provided.' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Check if seat is already taken for this schedule
|
|
84
|
+
const [existing] = await pool.execute(
|
|
85
|
+
'SELECT BookingID FROM yk_bookings WHERE ScheduleID = ? AND SeatNumber = ? AND PaymentStatus != ?',
|
|
86
|
+
[ScheduleID, SeatNumber, 'Cancelled']
|
|
87
|
+
);
|
|
88
|
+
if (existing.length > 0) {
|
|
89
|
+
return res.status(409).json({ message: `Seat ${SeatNumber} is already booked for this schedule.` });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [result] = await pool.execute(
|
|
93
|
+
`INSERT INTO yk_bookings (ScheduleID, UserID, PassengerName, PassengerGender, PassengerPhone, SeatNumber, PaymentStatus, BookingDate)
|
|
94
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
95
|
+
[ScheduleID, UserID, PassengerName, PassengerGender, PassengerPhone, SeatNumber, PaymentStatus || 'Pending', BookingDate || new Date()]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const [newBooking] = await pool.execute(`
|
|
99
|
+
SELECT b.*, s.RouteName, s.DeparturePoint, s.Destination, s.DepartureTime, s.TicketPrice,
|
|
100
|
+
bs.PlateNumber, u.UserName
|
|
101
|
+
FROM yk_bookings b
|
|
102
|
+
LEFT JOIN yk_schedules s ON b.ScheduleID = s.ScheduleID
|
|
103
|
+
LEFT JOIN yk_buses bs ON s.BusID = bs.BusID
|
|
104
|
+
LEFT JOIN yk_users u ON b.UserID = u.UserID
|
|
105
|
+
WHERE b.BookingID = ?
|
|
106
|
+
`, [result.insertId]);
|
|
107
|
+
|
|
108
|
+
res.status(201).json(newBooking[0]);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// PUT /api/bookings/:id — Update booking
|
|
115
|
+
router.put('/:id', requireAuth, async (req, res) => {
|
|
116
|
+
const { ScheduleID, PassengerName, PassengerGender, PassengerPhone, SeatNumber, PaymentStatus, BookingDate } = req.body;
|
|
117
|
+
try {
|
|
118
|
+
const [result] = await pool.execute(
|
|
119
|
+
`UPDATE yk_bookings SET ScheduleID=?, PassengerName=?, PassengerGender=?, PassengerPhone=?,
|
|
120
|
+
SeatNumber=?, PaymentStatus=?, BookingDate=? WHERE BookingID=?`,
|
|
121
|
+
[ScheduleID, PassengerName, PassengerGender, PassengerPhone, SeatNumber, PaymentStatus, BookingDate, req.params.id]
|
|
122
|
+
);
|
|
123
|
+
if (result.affectedRows === 0) return res.status(404).json({ message: 'Booking not found' });
|
|
124
|
+
|
|
125
|
+
const [updated] = await pool.execute(`
|
|
126
|
+
SELECT b.*, s.RouteName, s.DeparturePoint, s.Destination, s.DepartureTime, s.TicketPrice,
|
|
127
|
+
bs.PlateNumber, u.UserName
|
|
128
|
+
FROM yk_bookings b
|
|
129
|
+
LEFT JOIN yk_schedules s ON b.ScheduleID = s.ScheduleID
|
|
130
|
+
LEFT JOIN yk_buses bs ON s.BusID = bs.BusID
|
|
131
|
+
LEFT JOIN yk_users u ON b.UserID = u.UserID
|
|
132
|
+
WHERE b.BookingID = ?
|
|
133
|
+
`, [req.params.id]);
|
|
134
|
+
res.json(updated[0]);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// DELETE /api/bookings/:id — Delete booking
|
|
141
|
+
router.delete('/:id', requireAuth, async (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const [result] = await pool.execute('DELETE FROM yk_bookings WHERE BookingID = ?', [req.params.id]);
|
|
144
|
+
if (result.affectedRows === 0) return res.status(404).json({ message: 'Booking not found' });
|
|
145
|
+
res.json({ message: 'Booking deleted successfully' });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
module.exports = router;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const pool = require('../db');
|
|
4
|
+
const { requireAuth } = require('../middleware/auth');
|
|
5
|
+
|
|
6
|
+
// GET /api/buses — Get all buses
|
|
7
|
+
router.get('/', requireAuth, async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const [rows] = await pool.execute('SELECT * FROM yk_buses ORDER BY BusID DESC');
|
|
10
|
+
res.json(rows);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// GET /api/buses/:id — Get single bus
|
|
17
|
+
router.get('/:id', requireAuth, async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const [rows] = await pool.execute('SELECT * FROM yk_buses WHERE BusID = ?', [req.params.id]);
|
|
20
|
+
if (rows.length === 0) return res.status(404).json({ message: 'Bus not found' });
|
|
21
|
+
res.json(rows[0]);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// POST /api/buses — Create bus
|
|
28
|
+
router.post('/', requireAuth, async (req, res) => {
|
|
29
|
+
const { PlateNumber, TotalSeats, BusType } = req.body;
|
|
30
|
+
if (!PlateNumber || !TotalSeats || !BusType) {
|
|
31
|
+
return res.status(400).json({ message: 'PlateNumber, TotalSeats, and BusType are required.' });
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const [result] = await pool.execute(
|
|
35
|
+
'INSERT INTO yk_buses (PlateNumber, TotalSeats, BusType) VALUES (?, ?, ?)',
|
|
36
|
+
[PlateNumber, TotalSeats, BusType]
|
|
37
|
+
);
|
|
38
|
+
const [newBus] = await pool.execute('SELECT * FROM yk_buses WHERE BusID = ?', [result.insertId]);
|
|
39
|
+
res.status(201).json(newBus[0]);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err.code === 'ER_DUP_ENTRY') {
|
|
42
|
+
return res.status(409).json({ message: 'A bus with this plate number already exists.' });
|
|
43
|
+
}
|
|
44
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// PUT /api/buses/:id — Update bus
|
|
49
|
+
router.put('/:id', requireAuth, async (req, res) => {
|
|
50
|
+
const { PlateNumber, TotalSeats, BusType } = req.body;
|
|
51
|
+
try {
|
|
52
|
+
const [result] = await pool.execute(
|
|
53
|
+
'UPDATE yk_buses SET PlateNumber = ?, TotalSeats = ?, BusType = ? WHERE BusID = ?',
|
|
54
|
+
[PlateNumber, TotalSeats, BusType, req.params.id]
|
|
55
|
+
);
|
|
56
|
+
if (result.affectedRows === 0) return res.status(404).json({ message: 'Bus not found' });
|
|
57
|
+
const [updated] = await pool.execute('SELECT * FROM yk_buses WHERE BusID = ?', [req.params.id]);
|
|
58
|
+
res.json(updated[0]);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.code === 'ER_DUP_ENTRY') {
|
|
61
|
+
return res.status(409).json({ message: 'A bus with this plate number already exists.' });
|
|
62
|
+
}
|
|
63
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// DELETE /api/buses/:id — Delete bus
|
|
68
|
+
router.delete('/:id', requireAuth, async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const [result] = await pool.execute('DELETE FROM yk_buses WHERE BusID = ?', [req.params.id]);
|
|
71
|
+
if (result.affectedRows === 0) return res.status(404).json({ message: 'Bus not found' });
|
|
72
|
+
res.json({ message: 'Bus deleted successfully' });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err.code === 'ER_ROW_IS_REFERENCED_2') {
|
|
75
|
+
return res.status(409).json({ message: 'Cannot delete bus. It is used in one or more schedules.' });
|
|
76
|
+
}
|
|
77
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
module.exports = router;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const pool = require('../db');
|
|
4
|
+
const { requireAuth } = require('../middleware/auth');
|
|
5
|
+
|
|
6
|
+
// GET /api/schedules — Get all schedules with bus info
|
|
7
|
+
router.get('/', requireAuth, async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const [rows] = await pool.execute(`
|
|
10
|
+
SELECT s.*, b.PlateNumber, b.BusType, b.TotalSeats
|
|
11
|
+
FROM yk_schedules s
|
|
12
|
+
LEFT JOIN yk_buses b ON s.BusID = b.BusID
|
|
13
|
+
ORDER BY s.ScheduleID DESC
|
|
14
|
+
`);
|
|
15
|
+
res.json(rows);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// GET /api/schedules/:id — Get single schedule
|
|
22
|
+
router.get('/:id', requireAuth, async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const [rows] = await pool.execute(`
|
|
25
|
+
SELECT s.*, b.PlateNumber, b.BusType, b.TotalSeats
|
|
26
|
+
FROM yk_schedules s
|
|
27
|
+
LEFT JOIN yk_buses b ON s.BusID = b.BusID
|
|
28
|
+
WHERE s.ScheduleID = ?
|
|
29
|
+
`, [req.params.id]);
|
|
30
|
+
if (rows.length === 0) return res.status(404).json({ message: 'Schedule not found' });
|
|
31
|
+
res.json(rows[0]);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// POST /api/schedules — Create schedule
|
|
38
|
+
router.post('/', requireAuth, async (req, res) => {
|
|
39
|
+
const { BusID, RouteName, DeparturePoint, Destination, DepartureTime, EstimatedArrivalTime, TicketPrice, ScheduleStatus } = req.body;
|
|
40
|
+
if (!BusID || !RouteName || !DeparturePoint || !Destination || !DepartureTime || !EstimatedArrivalTime || TicketPrice == null) {
|
|
41
|
+
return res.status(400).json({ message: 'All fields are required.' });
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const [result] = await pool.execute(
|
|
45
|
+
`INSERT INTO yk_schedules (BusID, RouteName, DeparturePoint, Destination, DepartureTime, EstimatedArrivalTime, TicketPrice, ScheduleStatus)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
47
|
+
[BusID, RouteName, DeparturePoint, Destination, DepartureTime, EstimatedArrivalTime, TicketPrice, ScheduleStatus || 'Active']
|
|
48
|
+
);
|
|
49
|
+
const [newRow] = await pool.execute(`
|
|
50
|
+
SELECT s.*, b.PlateNumber, b.BusType, b.TotalSeats
|
|
51
|
+
FROM yk_schedules s LEFT JOIN yk_buses b ON s.BusID = b.BusID
|
|
52
|
+
WHERE s.ScheduleID = ?
|
|
53
|
+
`, [result.insertId]);
|
|
54
|
+
res.status(201).json(newRow[0]);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// PUT /api/schedules/:id — Update schedule
|
|
61
|
+
router.put('/:id', requireAuth, async (req, res) => {
|
|
62
|
+
const { BusID, RouteName, DeparturePoint, Destination, DepartureTime, EstimatedArrivalTime, TicketPrice, ScheduleStatus } = req.body;
|
|
63
|
+
try {
|
|
64
|
+
const [result] = await pool.execute(
|
|
65
|
+
`UPDATE yk_schedules SET BusID=?, RouteName=?, DeparturePoint=?, Destination=?,
|
|
66
|
+
DepartureTime=?, EstimatedArrivalTime=?, TicketPrice=?, ScheduleStatus=? WHERE ScheduleID=?`,
|
|
67
|
+
[BusID, RouteName, DeparturePoint, Destination, DepartureTime, EstimatedArrivalTime, TicketPrice, ScheduleStatus, req.params.id]
|
|
68
|
+
);
|
|
69
|
+
if (result.affectedRows === 0) return res.status(404).json({ message: 'Schedule not found' });
|
|
70
|
+
const [updated] = await pool.execute(`
|
|
71
|
+
SELECT s.*, b.PlateNumber, b.BusType, b.TotalSeats
|
|
72
|
+
FROM yk_schedules s LEFT JOIN yk_buses b ON s.BusID = b.BusID
|
|
73
|
+
WHERE s.ScheduleID = ?
|
|
74
|
+
`, [req.params.id]);
|
|
75
|
+
res.json(updated[0]);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// DELETE /api/schedules/:id — Delete schedule
|
|
82
|
+
router.delete('/:id', requireAuth, async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const [result] = await pool.execute('DELETE FROM yk_schedules WHERE ScheduleID = ?', [req.params.id]);
|
|
85
|
+
if (result.affectedRows === 0) return res.status(404).json({ message: 'Schedule not found' });
|
|
86
|
+
res.json({ message: 'Schedule deleted successfully' });
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err.code === 'ER_ROW_IS_REFERENCED_2') {
|
|
89
|
+
return res.status(409).json({ message: 'Cannot delete schedule. It has existing bookings.' });
|
|
90
|
+
}
|
|
91
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
module.exports = router;
|