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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const bcrypt = require('bcryptjs');
|
|
4
|
+
const pool = require('../db');
|
|
5
|
+
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
|
6
|
+
|
|
7
|
+
// GET /api/users — Get all users (admin only)
|
|
8
|
+
router.get('/', requireAdmin, async (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const [rows] = await pool.execute('SELECT UserID, UserName, UserRole FROM yk_users ORDER BY UserID DESC');
|
|
11
|
+
res.json(rows);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// POST /api/users — Create user (admin only)
|
|
18
|
+
router.post('/', requireAdmin, async (req, res) => {
|
|
19
|
+
const { UserName, Password, UserRole } = req.body;
|
|
20
|
+
if (!UserName || !Password || !UserRole) {
|
|
21
|
+
return res.status(400).json({ message: 'UserName, Password, and UserRole are required.' });
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const hashedPassword = await bcrypt.hash(Password, 10);
|
|
25
|
+
const [result] = await pool.execute(
|
|
26
|
+
'INSERT INTO yk_users (UserName, Password, UserRole) VALUES (?, ?, ?)',
|
|
27
|
+
[UserName, hashedPassword, UserRole]
|
|
28
|
+
);
|
|
29
|
+
const [newUser] = await pool.execute(
|
|
30
|
+
'SELECT UserID, UserName, UserRole FROM yk_users WHERE UserID = ?',
|
|
31
|
+
[result.insertId]
|
|
32
|
+
);
|
|
33
|
+
res.status(201).json(newUser[0]);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err.code === 'ER_DUP_ENTRY') {
|
|
36
|
+
return res.status(409).json({ message: 'A user with this username already exists.' });
|
|
37
|
+
}
|
|
38
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// PUT /api/users/:id — Update user (admin only)
|
|
43
|
+
router.put('/:id', requireAdmin, async (req, res) => {
|
|
44
|
+
const { UserName, Password, UserRole } = req.body;
|
|
45
|
+
try {
|
|
46
|
+
if (Password) {
|
|
47
|
+
const hashedPassword = await bcrypt.hash(Password, 10);
|
|
48
|
+
await pool.execute(
|
|
49
|
+
'UPDATE yk_users SET UserName = ?, Password = ?, UserRole = ? WHERE UserID = ?',
|
|
50
|
+
[UserName, hashedPassword, UserRole, req.params.id]
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
await pool.execute(
|
|
54
|
+
'UPDATE yk_users SET UserName = ?, UserRole = ? WHERE UserID = ?',
|
|
55
|
+
[UserName, UserRole, req.params.id]
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const [updated] = await pool.execute(
|
|
59
|
+
'SELECT UserID, UserName, UserRole FROM yk_users WHERE UserID = ?',
|
|
60
|
+
[req.params.id]
|
|
61
|
+
);
|
|
62
|
+
if (updated.length === 0) return res.status(404).json({ message: 'User not found' });
|
|
63
|
+
res.json(updated[0]);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.code === 'ER_DUP_ENTRY') {
|
|
66
|
+
return res.status(409).json({ message: 'A user with this username already exists.' });
|
|
67
|
+
}
|
|
68
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// DELETE /api/users/:id — Delete user (admin only)
|
|
73
|
+
router.delete('/:id', requireAdmin, async (req, res) => {
|
|
74
|
+
if (parseInt(req.params.id) === req.session.user.UserID) {
|
|
75
|
+
return res.status(400).json({ message: 'You cannot delete your own account.' });
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const [result] = await pool.execute('DELETE FROM yk_users WHERE UserID = ?', [req.params.id]);
|
|
79
|
+
if (result.affectedRows === 0) return res.status(404).json({ message: 'User not found' });
|
|
80
|
+
res.json({ message: 'User deleted successfully' });
|
|
81
|
+
} catch (err) {
|
|
82
|
+
res.status(500).json({ message: 'Server error', error: err.message });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
module.exports = router;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const session = require('express-session');
|
|
3
|
+
const cors = require('cors');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const authRoutes = require('./routes/auth');
|
|
7
|
+
const busRoutes = require('./routes/buses');
|
|
8
|
+
const scheduleRoutes = require('./routes/schedules');
|
|
9
|
+
const bookingRoutes = require('./routes/bookings');
|
|
10
|
+
const userRoutes = require('./routes/users');
|
|
11
|
+
|
|
12
|
+
const app = express();
|
|
13
|
+
const PORT = process.env.PORT || 5000;
|
|
14
|
+
|
|
15
|
+
// ─── Middleware ────────────────────────────────────────────────
|
|
16
|
+
app.use(express.json());
|
|
17
|
+
app.use(express.urlencoded({ extended: true }));
|
|
18
|
+
|
|
19
|
+
// CORS — allow frontend origin with credentials
|
|
20
|
+
app.use(cors({
|
|
21
|
+
origin: ['http://localhost:5173', 'http://localhost:3000'],
|
|
22
|
+
credentials: true,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Session configuration (session-based auth)
|
|
26
|
+
app.use(session({
|
|
27
|
+
secret: process.env.SESSION_SECRET || 'ybus_secret_key_2026',
|
|
28
|
+
resave: false,
|
|
29
|
+
saveUninitialized: false,
|
|
30
|
+
cookie: {
|
|
31
|
+
httpOnly: true,
|
|
32
|
+
secure: false, // set to true in production with HTTPS
|
|
33
|
+
maxAge: 1000 * 60 * 60 * 8, // 8 hours
|
|
34
|
+
sameSite: 'lax',
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// ─── Routes ───────────────────────────────────────────────────
|
|
39
|
+
app.use('/api/auth', authRoutes);
|
|
40
|
+
app.use('/api/buses', busRoutes);
|
|
41
|
+
app.use('/api/schedules', scheduleRoutes);
|
|
42
|
+
app.use('/api/bookings', bookingRoutes);
|
|
43
|
+
app.use('/api/users', userRoutes);
|
|
44
|
+
|
|
45
|
+
// Health check
|
|
46
|
+
app.get('/api/health', (req, res) => {
|
|
47
|
+
res.json({ status: 'OK', message: 'Y-Bus API is running', timestamp: new Date() });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─── Start Server ─────────────────────────────────────────────
|
|
51
|
+
app.listen(PORT, () => {
|
|
52
|
+
console.log(`\n🚌 Y-Bus Reservation API running on http://localhost:${PORT}`);
|
|
53
|
+
console.log(`📋 Health check: http://localhost:${PORT}/api/health\n`);
|
|
54
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const targetArg = process.argv[2];
|
|
9
|
+
const targetDir = path.resolve(process.cwd(), targetArg ?? ".");
|
|
10
|
+
const projectName = path.basename(targetDir);
|
|
11
|
+
|
|
12
|
+
const ignoredNames = new Set([
|
|
13
|
+
"bin",
|
|
14
|
+
"node_modules",
|
|
15
|
+
"dist",
|
|
16
|
+
".git",
|
|
17
|
+
".npmignore",
|
|
18
|
+
"package-lock.json",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
async function pathExists(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(filePath);
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function copyEntry(sourcePath, destinationPath) {
|
|
31
|
+
const stats = await fs.lstat(sourcePath);
|
|
32
|
+
|
|
33
|
+
if (stats.isDirectory()) {
|
|
34
|
+
await fs.mkdir(destinationPath, { recursive: true });
|
|
35
|
+
const entries = await fs.readdir(sourcePath, { withFileTypes: true });
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (ignoredNames.has(entry.name)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await copyEntry(
|
|
43
|
+
path.join(sourcePath, entry.name),
|
|
44
|
+
path.join(destinationPath, entry.name)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await fs.copyFile(sourcePath, destinationPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
if (await pathExists(targetDir)) {
|
|
56
|
+
const existingEntries = await fs.readdir(targetDir);
|
|
57
|
+
if (existingEntries.length > 0) {
|
|
58
|
+
console.error(`Target directory "${targetDir}" is not empty.`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const destinationPackage = {
|
|
66
|
+
name: projectName,
|
|
67
|
+
private: true,
|
|
68
|
+
version: "0.0.0",
|
|
69
|
+
type: "module",
|
|
70
|
+
scripts: {
|
|
71
|
+
"dev:frontend": "npm run dev --workspace frontend-project",
|
|
72
|
+
"build:frontend": "npm run build --workspace frontend-project",
|
|
73
|
+
"dev:backend": "npm run dev --workspace backend-project",
|
|
74
|
+
"start:backend": "npm start --workspace backend-project",
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const sourceEntries = await fs.readdir(packageRoot, { withFileTypes: true });
|
|
79
|
+
|
|
80
|
+
for (const entry of sourceEntries) {
|
|
81
|
+
if (ignoredNames.has(entry.name) || entry.name === "package.json") {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await copyEntry(
|
|
86
|
+
path.join(packageRoot, entry.name),
|
|
87
|
+
path.join(targetDir, entry.name)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await fs.writeFile(
|
|
92
|
+
path.join(targetDir, "package.json"),
|
|
93
|
+
`${JSON.stringify(destinationPackage, null, 2)}\n`
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
console.log(`Created ${projectName} in ${targetDir}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main().catch((error) => {
|
|
100
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Y Bus Reservation System</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-vite-tailwind",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@types/react-router-dom": "^5.3.3",
|
|
13
|
+
"axios": "^1.16.1",
|
|
14
|
+
"clsx": "2.1.1",
|
|
15
|
+
"react": "19.2.6",
|
|
16
|
+
"react-dom": "19.2.6",
|
|
17
|
+
"react-router-dom": "^7.15.1",
|
|
18
|
+
"tailwind-merge": "3.4.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@tailwindcss/vite": "4.1.17",
|
|
22
|
+
"@types/node": "22.19.17",
|
|
23
|
+
"@types/react": "19.2.7",
|
|
24
|
+
"@types/react-dom": "19.2.3",
|
|
25
|
+
"@vitejs/plugin-react": "5.1.1",
|
|
26
|
+
"tailwindcss": "4.1.17",
|
|
27
|
+
"typescript": "5.9.3",
|
|
28
|
+
"vite": "7.3.2",
|
|
29
|
+
"vite-plugin-singlefile": "2.3.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
|
2
|
+
import { AuthProvider } from './context/AuthContext';
|
|
3
|
+
import ProtectedRoute from './components/common/ProtectedRoute';
|
|
4
|
+
import Layout from './components/common/Layout';
|
|
5
|
+
import Login from './pages/Login';
|
|
6
|
+
import Dashboard from './pages/Dashboard';
|
|
7
|
+
import Buses from './pages/Buses';
|
|
8
|
+
import Schedules from './pages/Schedules';
|
|
9
|
+
import Bookings from './pages/Bookings';
|
|
10
|
+
import Report from './pages/Report';
|
|
11
|
+
import Users from './pages/Users';
|
|
12
|
+
|
|
13
|
+
function App() {
|
|
14
|
+
return (
|
|
15
|
+
<BrowserRouter>
|
|
16
|
+
<AuthProvider>
|
|
17
|
+
<Routes>
|
|
18
|
+
<Route path="/login" element={<Login />} />
|
|
19
|
+
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
20
|
+
|
|
21
|
+
<Route
|
|
22
|
+
path="/dashboard"
|
|
23
|
+
element={
|
|
24
|
+
<ProtectedRoute>
|
|
25
|
+
<Layout><Dashboard /></Layout>
|
|
26
|
+
</ProtectedRoute>
|
|
27
|
+
}
|
|
28
|
+
/>
|
|
29
|
+
<Route
|
|
30
|
+
path="/buses"
|
|
31
|
+
element={
|
|
32
|
+
<ProtectedRoute>
|
|
33
|
+
<Layout><Buses /></Layout>
|
|
34
|
+
</ProtectedRoute>
|
|
35
|
+
}
|
|
36
|
+
/>
|
|
37
|
+
<Route
|
|
38
|
+
path="/schedules"
|
|
39
|
+
element={
|
|
40
|
+
<ProtectedRoute>
|
|
41
|
+
<Layout><Schedules /></Layout>
|
|
42
|
+
</ProtectedRoute>
|
|
43
|
+
}
|
|
44
|
+
/>
|
|
45
|
+
<Route
|
|
46
|
+
path="/bookings"
|
|
47
|
+
element={
|
|
48
|
+
<ProtectedRoute>
|
|
49
|
+
<Layout><Bookings /></Layout>
|
|
50
|
+
</ProtectedRoute>
|
|
51
|
+
}
|
|
52
|
+
/>
|
|
53
|
+
<Route
|
|
54
|
+
path="/report"
|
|
55
|
+
element={
|
|
56
|
+
<ProtectedRoute>
|
|
57
|
+
<Layout><Report /></Layout>
|
|
58
|
+
</ProtectedRoute>
|
|
59
|
+
}
|
|
60
|
+
/>
|
|
61
|
+
<Route
|
|
62
|
+
path="/users"
|
|
63
|
+
element={
|
|
64
|
+
<ProtectedRoute adminOnly>
|
|
65
|
+
<Layout><Users /></Layout>
|
|
66
|
+
</ProtectedRoute>
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
|
71
|
+
</Routes>
|
|
72
|
+
</AuthProvider>
|
|
73
|
+
</BrowserRouter>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default App;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import api from './axios';
|
|
2
|
+
import { User } from '../types';
|
|
3
|
+
|
|
4
|
+
export const login = async (UserName: string, Password: string): Promise<User> => {
|
|
5
|
+
const res = await api.post('/auth/login', { UserName, Password });
|
|
6
|
+
return res.data.user;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const logout = async (): Promise<void> => {
|
|
10
|
+
await api.post('/auth/logout');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getSession = async (): Promise<User | null> => {
|
|
14
|
+
try {
|
|
15
|
+
const res = await api.get('/auth/session');
|
|
16
|
+
return res.data.user;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import api from './axios';
|
|
2
|
+
import { Booking } from '../types';
|
|
3
|
+
|
|
4
|
+
export const getBookings = async (): Promise<Booking[]> => {
|
|
5
|
+
const res = await api.get('/bookings');
|
|
6
|
+
return res.data;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getBooking = async (id: number): Promise<Booking> => {
|
|
10
|
+
const res = await api.get(`/bookings/${id}`);
|
|
11
|
+
return res.data;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const createBooking = async (data: Omit<Booking, 'BookingID'>): Promise<Booking> => {
|
|
15
|
+
const res = await api.post('/bookings', data);
|
|
16
|
+
return res.data;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const updateBooking = async (id: number, data: Partial<Booking>): Promise<Booking> => {
|
|
20
|
+
const res = await api.put(`/bookings/${id}`, data);
|
|
21
|
+
return res.data;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const deleteBooking = async (id: number): Promise<void> => {
|
|
25
|
+
await api.delete(`/bookings/${id}`);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getPassengerReport = async () => {
|
|
29
|
+
const res = await api.get('/bookings/report/passengers');
|
|
30
|
+
return res.data;
|
|
31
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import api from './axios';
|
|
2
|
+
import { Bus } from '../types';
|
|
3
|
+
|
|
4
|
+
export const getBuses = async (): Promise<Bus[]> => {
|
|
5
|
+
const res = await api.get('/buses');
|
|
6
|
+
return res.data;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getBus = async (id: number): Promise<Bus> => {
|
|
10
|
+
const res = await api.get(`/buses/${id}`);
|
|
11
|
+
return res.data;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const createBus = async (data: Omit<Bus, 'BusID'>): Promise<Bus> => {
|
|
15
|
+
const res = await api.post('/buses', data);
|
|
16
|
+
return res.data;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const updateBus = async (id: number, data: Omit<Bus, 'BusID'>): Promise<Bus> => {
|
|
20
|
+
const res = await api.put(`/buses/${id}`, data);
|
|
21
|
+
return res.data;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const deleteBus = async (id: number): Promise<void> => {
|
|
25
|
+
await api.delete(`/buses/${id}`);
|
|
26
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import api from './axios';
|
|
2
|
+
import { Schedule } from '../types';
|
|
3
|
+
|
|
4
|
+
export const getSchedules = async (): Promise<Schedule[]> => {
|
|
5
|
+
const res = await api.get('/schedules');
|
|
6
|
+
return res.data;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getSchedule = async (id: number): Promise<Schedule> => {
|
|
10
|
+
const res = await api.get(`/schedules/${id}`);
|
|
11
|
+
return res.data;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const createSchedule = async (data: Omit<Schedule, 'ScheduleID'>): Promise<Schedule> => {
|
|
15
|
+
const res = await api.post('/schedules', data);
|
|
16
|
+
return res.data;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const updateSchedule = async (id: number, data: Omit<Schedule, 'ScheduleID'>): Promise<Schedule> => {
|
|
20
|
+
const res = await api.put(`/schedules/${id}`, data);
|
|
21
|
+
return res.data;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const deleteSchedule = async (id: number): Promise<void> => {
|
|
25
|
+
await api.delete(`/schedules/${id}`);
|
|
26
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import api from './axios';
|
|
2
|
+
import { User } from '../types';
|
|
3
|
+
|
|
4
|
+
export const getUsers = async (): Promise<User[]> => {
|
|
5
|
+
const res = await api.get('/users');
|
|
6
|
+
return res.data;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const createUser = async (data: { UserName: string; Password: string; UserRole: string }): Promise<User> => {
|
|
10
|
+
const res = await api.post('/users', data);
|
|
11
|
+
return res.data;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const updateUser = async (id: number, data: { UserName?: string; Password?: string; UserRole?: string }): Promise<User> => {
|
|
15
|
+
const res = await api.put(`/users/${id}`, data);
|
|
16
|
+
return res.data;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const deleteUser = async (id: number): Promise<void> => {
|
|
20
|
+
await api.delete(`/users/${id}`);
|
|
21
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import Navbar from './Navbar';
|
|
3
|
+
|
|
4
|
+
const Layout = ({ children }: { children: ReactNode }) => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="min-h-screen bg-gray-50">
|
|
7
|
+
<Navbar />
|
|
8
|
+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
9
|
+
{children}
|
|
10
|
+
</main>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default Layout;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ModalProps {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
title: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Modal = ({ isOpen, onClose, title, children, size = 'md' }: ModalProps) => {
|
|
12
|
+
if (!isOpen) return null;
|
|
13
|
+
|
|
14
|
+
const sizeClasses = {
|
|
15
|
+
sm: 'max-w-sm',
|
|
16
|
+
md: 'max-w-md',
|
|
17
|
+
lg: 'max-w-lg',
|
|
18
|
+
xl: 'max-w-2xl',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center px-4">
|
|
23
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
|
24
|
+
<div className={`relative bg-white rounded-xl shadow-2xl w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto`}>
|
|
25
|
+
<div className="flex items-center justify-between p-5 border-b border-gray-200 bg-gray-900 rounded-t-xl">
|
|
26
|
+
<h2 className="text-lg font-semibold text-yellow-400">{title}</h2>
|
|
27
|
+
<button
|
|
28
|
+
onClick={onClose}
|
|
29
|
+
className="text-gray-400 hover:text-white transition-colors"
|
|
30
|
+
>
|
|
31
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
32
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
33
|
+
</svg>
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="p-5">{children}</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default Modal;
|