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,199 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { getUsers, createUser, updateUser, deleteUser } from '../api/users';
3
+ import { User } from '../types';
4
+ import Table from '../components/common/Table';
5
+ import Modal from '../components/common/Modal';
6
+ import { useAuth } from '../context/AuthContext';
7
+
8
+ const defaultForm = { UserName: '', Password: '', UserRole: 'agent' };
9
+
10
+ const Users = () => {
11
+ const { user: currentUser } = useAuth();
12
+ const [users, setUsers] = useState<User[]>([]);
13
+ const [loading, setLoading] = useState(true);
14
+ const [modalOpen, setModalOpen] = useState(false);
15
+ const [deleteModalOpen, setDeleteModalOpen] = useState(false);
16
+ const [editUser, setEditUser] = useState<User | null>(null);
17
+ const [deleteId, setDeleteId] = useState<number | null>(null);
18
+ const [form, setForm] = useState(defaultForm);
19
+ const [saving, setSaving] = useState(false);
20
+ const [error, setError] = useState('');
21
+ const [search, setSearch] = useState('');
22
+
23
+ const fetchUsers = async () => {
24
+ setLoading(true);
25
+ try {
26
+ const data = await getUsers();
27
+ setUsers(data);
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ };
32
+
33
+ useEffect(() => { fetchUsers(); }, []);
34
+
35
+ const openAdd = () => {
36
+ setEditUser(null);
37
+ setForm(defaultForm);
38
+ setError('');
39
+ setModalOpen(true);
40
+ };
41
+
42
+ const openEdit = (u: User) => {
43
+ setEditUser(u);
44
+ setForm({ UserName: u.UserName, Password: '', UserRole: u.UserRole });
45
+ setError('');
46
+ setModalOpen(true);
47
+ };
48
+
49
+ const handleSubmit = async (e: React.FormEvent) => {
50
+ e.preventDefault();
51
+ setSaving(true);
52
+ setError('');
53
+ try {
54
+ if (editUser) {
55
+ const payload: { UserName: string; UserRole: string; Password?: string } = {
56
+ UserName: form.UserName,
57
+ UserRole: form.UserRole,
58
+ };
59
+ if (form.Password) payload.Password = form.Password;
60
+ await updateUser(editUser.UserID, payload);
61
+ } else {
62
+ await createUser(form);
63
+ }
64
+ setModalOpen(false);
65
+ fetchUsers();
66
+ } catch (err: unknown) {
67
+ setError(err instanceof Error ? err.message : 'An error occurred');
68
+ } finally {
69
+ setSaving(false);
70
+ }
71
+ };
72
+
73
+ const handleDelete = async () => {
74
+ if (!deleteId) return;
75
+ if (deleteId === currentUser?.UserID) {
76
+ alert('You cannot delete your own account.');
77
+ return;
78
+ }
79
+ try {
80
+ await deleteUser(deleteId);
81
+ setDeleteModalOpen(false);
82
+ fetchUsers();
83
+ } catch (err: unknown) {
84
+ alert(err instanceof Error ? err.message : 'Delete failed');
85
+ }
86
+ };
87
+
88
+ const filtered = users.filter(
89
+ (u) =>
90
+ u.UserName.toLowerCase().includes(search.toLowerCase()) ||
91
+ u.UserRole.toLowerCase().includes(search.toLowerCase())
92
+ );
93
+
94
+ const columns = [
95
+ { key: 'UserID', label: '#' },
96
+ { key: 'UserName', label: 'Username' },
97
+ { key: 'UserRole', label: 'Role', render: (u: User) => (
98
+ <span className={`px-2 py-1 rounded-full text-xs font-medium ${
99
+ u.UserRole === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-blue-100 text-blue-700'
100
+ }`}>{u.UserRole}</span>
101
+ )},
102
+ {
103
+ key: 'actions', label: 'Actions',
104
+ render: (u: User) => (
105
+ <div className="flex gap-2">
106
+ <button onClick={() => openEdit(u)} 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>
107
+ {u.UserID !== currentUser?.UserID && (
108
+ <button onClick={() => { setDeleteId(u.UserID); 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>
109
+ )}
110
+ </div>
111
+ ),
112
+ },
113
+ ];
114
+
115
+ if (currentUser?.UserRole !== 'admin') {
116
+ return (
117
+ <div className="flex items-center justify-center min-h-[60vh]">
118
+ <div className="text-center">
119
+ <p className="text-4xl mb-3">🚫</p>
120
+ <h2 className="text-xl font-bold text-gray-800">Access Denied</h2>
121
+ <p className="text-gray-500 mt-2">Only administrators can manage users.</p>
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ return (
128
+ <div className="space-y-5">
129
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
130
+ <div>
131
+ <h1 className="text-2xl font-bold text-gray-800">User Management</h1>
132
+ <p className="text-gray-500 text-sm mt-1">Manage system users and their roles</p>
133
+ </div>
134
+ <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">
135
+ + Add New User
136
+ </button>
137
+ </div>
138
+
139
+ <input
140
+ type="text"
141
+ placeholder="Search by username or role..."
142
+ value={search}
143
+ onChange={(e) => setSearch(e.target.value)}
144
+ className="w-full sm:max-w-sm border border-gray-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400"
145
+ />
146
+
147
+ <Table columns={columns} data={filtered} keyExtractor={(u) => u.UserID} loading={loading} emptyMessage="No users found." />
148
+
149
+ <Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title={editUser ? 'Edit User' : 'Add New User'}>
150
+ <form onSubmit={handleSubmit} className="space-y-4">
151
+ {error && <div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-600">{error}</div>}
152
+ <div>
153
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Username *</label>
154
+ <input required type="text" value={form.UserName} onChange={(e) => setForm({ ...form, UserName: e.target.value })}
155
+ 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"
156
+ placeholder="Enter username" />
157
+ </div>
158
+ <div>
159
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">
160
+ Password {editUser ? '(leave blank to keep)' : '*'}
161
+ </label>
162
+ <input
163
+ type="password"
164
+ required={!editUser}
165
+ value={form.Password}
166
+ onChange={(e) => setForm({ ...form, Password: e.target.value })}
167
+ 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"
168
+ placeholder={editUser ? 'Leave blank to keep current password' : 'Enter password'}
169
+ />
170
+ </div>
171
+ <div>
172
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Role *</label>
173
+ <select value={form.UserRole} onChange={(e) => setForm({ ...form, UserRole: e.target.value })}
174
+ 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">
175
+ <option value="agent">Agent</option>
176
+ <option value="admin">Admin</option>
177
+ </select>
178
+ </div>
179
+ <div className="flex gap-3 pt-2">
180
+ <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>
181
+ <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">
182
+ {saving ? 'Saving...' : editUser ? 'Update User' : 'Add User'}
183
+ </button>
184
+ </div>
185
+ </form>
186
+ </Modal>
187
+
188
+ <Modal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} title="Confirm Delete" size="sm">
189
+ <p className="text-gray-600 text-sm mb-4">Are you sure you want to delete this user?</p>
190
+ <div className="flex gap-3">
191
+ <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>
192
+ <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>
193
+ </div>
194
+ </Modal>
195
+ </div>
196
+ );
197
+ };
198
+
199
+ export default Users;
@@ -0,0 +1,63 @@
1
+ export interface User {
2
+ UserID: number;
3
+ UserName: string;
4
+ UserRole: 'admin' | 'agent';
5
+ }
6
+
7
+ export interface Bus {
8
+ BusID: number;
9
+ PlateNumber: string;
10
+ TotalSeats: number;
11
+ BusType: string;
12
+ }
13
+
14
+ export interface Schedule {
15
+ ScheduleID: number;
16
+ BusID: number;
17
+ RouteName: string;
18
+ DeparturePoint: string;
19
+ Destination: string;
20
+ DepartureTime: string;
21
+ EstimatedArrivalTime: string;
22
+ TicketPrice: number;
23
+ ScheduleStatus: 'Active' | 'Inactive' | 'Cancelled';
24
+ PlateNumber?: string;
25
+ BusType?: string;
26
+ TotalSeats?: number;
27
+ }
28
+
29
+ export interface Booking {
30
+ BookingID: number;
31
+ ScheduleID: number;
32
+ UserID: number;
33
+ PassengerName: string;
34
+ PassengerGender: 'Male' | 'Female' | 'Other';
35
+ PassengerPhone: string;
36
+ SeatNumber: string;
37
+ PaymentStatus: 'Pending' | 'Paid' | 'Cancelled';
38
+ BookingDate: string;
39
+ RouteName?: string;
40
+ DeparturePoint?: string;
41
+ Destination?: string;
42
+ DepartureTime?: string;
43
+ PlateNumber?: string;
44
+ UserName?: string;
45
+ TicketPrice?: number;
46
+ }
47
+
48
+ export interface AuthContextType {
49
+ user: User | null;
50
+ login: (username: string, password: string) => Promise<void>;
51
+ logout: () => Promise<void>;
52
+ loading: boolean;
53
+ }
54
+
55
+ export interface PassengerReport {
56
+ RouteName: string;
57
+ DeparturePoint: string;
58
+ Destination: string;
59
+ DepartureTime: string;
60
+ PlateNumber: string;
61
+ TotalPassengers: number;
62
+ Passengers: Booking[];
63
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "ignoreDeprecations": "6.0",
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Path mapping */
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "@/*": ["src/*"]
22
+ },
23
+
24
+ /* Linting */
25
+ "strict": true,
26
+ "noUnusedLocals": true,
27
+ "noUnusedParameters": true,
28
+ "noFallthroughCasesInSwitch": true
29
+ },
30
+ "include": ["src", "vite.config.ts"]
31
+ }
@@ -0,0 +1,19 @@
1
+ import tailwindcss from "@tailwindcss/vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import { defineConfig } from "vite";
4
+ import { viteSingleFile } from "vite-plugin-singlefile";
5
+
6
+ const srcPath = decodeURIComponent(new URL("./src", import.meta.url).pathname).replace(
7
+ /^\/([A-Za-z]:)/,
8
+ "$1"
9
+ );
10
+
11
+ // https://vite.dev/config/
12
+ export default defineConfig({
13
+ plugins: [react(), tailwindcss(), viteSingleFile()],
14
+ resolve: {
15
+ alias: {
16
+ "@": srcPath,
17
+ },
18
+ },
19
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "create-sales",
3
+ "version": "1.0.0",
4
+ "description": "npm create sales initializer for a full-stack bus reservation template.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "create-sales": "bin/create-sales.js"
11
+ },
12
+ "workspaces": [
13
+ "frontend-project",
14
+ "backend-project"
15
+ ],
16
+ "scripts": {
17
+ "dev:frontend": "npm run dev --workspace frontend-project",
18
+ "build:frontend": "npm run build --workspace frontend-project",
19
+ "dev:backend": "npm run dev --workspace backend-project",
20
+ "start:backend": "npm start --workspace backend-project"
21
+ },
22
+ "keywords": [
23
+ "template",
24
+ "full-stack",
25
+ "react",
26
+ "vite",
27
+ "typescript",
28
+ "express",
29
+ "mysql",
30
+ "tailwindcss",
31
+ "bus-reservation"
32
+ ],
33
+ "files": [
34
+ "README.md",
35
+ ".gitignore",
36
+ "bin/create-sales.js",
37
+ "frontend-project/index.html",
38
+ "frontend-project/package.json",
39
+ "frontend-project/tsconfig.json",
40
+ "frontend-project/vite.config.ts",
41
+ "frontend-project/src",
42
+ "backend-project/package.json",
43
+ "backend-project/.env.example",
44
+ "backend-project/db.js",
45
+ "backend-project/server.js",
46
+ "backend-project/database",
47
+ "backend-project/middleware",
48
+ "backend-project/routes"
49
+ ]
50
+ }