npms-exam-kit 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 (69) hide show
  1. package/bin/exam-kit.js +357 -0
  2. package/package.json +25 -0
  3. package/projects/CRPMS-main/backend-project/config/db.js +12 -0
  4. package/projects/CRPMS-main/backend-project/config/initDb.js +92 -0
  5. package/projects/CRPMS-main/backend-project/middleware/auth.js +8 -0
  6. package/projects/CRPMS-main/backend-project/package-lock.json +1429 -0
  7. package/projects/CRPMS-main/backend-project/package.json +21 -0
  8. package/projects/CRPMS-main/backend-project/routes/auth.js +26 -0
  9. package/projects/CRPMS-main/backend-project/routes/cars.js +36 -0
  10. package/projects/CRPMS-main/backend-project/routes/payments.js +69 -0
  11. package/projects/CRPMS-main/backend-project/routes/reports.js +58 -0
  12. package/projects/CRPMS-main/backend-project/routes/serviceRecords.js +91 -0
  13. package/projects/CRPMS-main/backend-project/routes/services.js +36 -0
  14. package/projects/CRPMS-main/backend-project/server.js +44 -0
  15. package/projects/CRPMS-main/database.sql +59 -0
  16. package/projects/CRPMS-main/frontend-project/README.md +16 -0
  17. package/projects/CRPMS-main/frontend-project/eslint.config.js +21 -0
  18. package/projects/CRPMS-main/frontend-project/index.html +13 -0
  19. package/projects/CRPMS-main/frontend-project/package-lock.json +3356 -0
  20. package/projects/CRPMS-main/frontend-project/package.json +32 -0
  21. package/projects/CRPMS-main/frontend-project/public/favicon.svg +1 -0
  22. package/projects/CRPMS-main/frontend-project/public/icons.svg +24 -0
  23. package/projects/CRPMS-main/frontend-project/src/App.css +184 -0
  24. package/projects/CRPMS-main/frontend-project/src/App.jsx +72 -0
  25. package/projects/CRPMS-main/frontend-project/src/api/axios.js +8 -0
  26. package/projects/CRPMS-main/frontend-project/src/assets/hero.png +0 -0
  27. package/projects/CRPMS-main/frontend-project/src/assets/react.svg +1 -0
  28. package/projects/CRPMS-main/frontend-project/src/assets/vite.svg +1 -0
  29. package/projects/CRPMS-main/frontend-project/src/components/Navbar.jsx +54 -0
  30. package/projects/CRPMS-main/frontend-project/src/components/ProtectedRoute.jsx +9 -0
  31. package/projects/CRPMS-main/frontend-project/src/context/AuthContext.jsx +35 -0
  32. package/projects/CRPMS-main/frontend-project/src/index.css +14 -0
  33. package/projects/CRPMS-main/frontend-project/src/main.jsx +10 -0
  34. package/projects/CRPMS-main/frontend-project/src/pages/Bill.jsx +227 -0
  35. package/projects/CRPMS-main/frontend-project/src/pages/Cars.jsx +112 -0
  36. package/projects/CRPMS-main/frontend-project/src/pages/Login.jsx +78 -0
  37. package/projects/CRPMS-main/frontend-project/src/pages/Payments.jsx +153 -0
  38. package/projects/CRPMS-main/frontend-project/src/pages/Reports.jsx +199 -0
  39. package/projects/CRPMS-main/frontend-project/src/pages/ServiceRecords.jsx +182 -0
  40. package/projects/CRPMS-main/frontend-project/src/pages/Services.jsx +125 -0
  41. package/projects/CRPMS-main/frontend-project/vite.config.js +10 -0
  42. package/projects/SIMS-master/backend-project/.env.example +6 -0
  43. package/projects/SIMS-master/backend-project/config/db.js +12 -0
  44. package/projects/SIMS-master/backend-project/middleware/auth.js +8 -0
  45. package/projects/SIMS-master/backend-project/package-lock.json +1221 -0
  46. package/projects/SIMS-master/backend-project/package.json +23 -0
  47. package/projects/SIMS-master/backend-project/routes/auth.js +29 -0
  48. package/projects/SIMS-master/backend-project/routes/reports.js +51 -0
  49. package/projects/SIMS-master/backend-project/routes/spareParts.js +34 -0
  50. package/projects/SIMS-master/backend-project/routes/stockIn.js +53 -0
  51. package/projects/SIMS-master/backend-project/routes/stockOut.js +146 -0
  52. package/projects/SIMS-master/backend-project/seed.js +20 -0
  53. package/projects/SIMS-master/backend-project/server.js +43 -0
  54. package/projects/SIMS-master/database.sql +43 -0
  55. package/projects/SIMS-master/frontend-project/index.html +12 -0
  56. package/projects/SIMS-master/frontend-project/package-lock.json +3352 -0
  57. package/projects/SIMS-master/frontend-project/package.json +27 -0
  58. package/projects/SIMS-master/frontend-project/postcss.config.js +6 -0
  59. package/projects/SIMS-master/frontend-project/src/App.jsx +53 -0
  60. package/projects/SIMS-master/frontend-project/src/components/Navbar.jsx +103 -0
  61. package/projects/SIMS-master/frontend-project/src/index.css +3 -0
  62. package/projects/SIMS-master/frontend-project/src/main.jsx +10 -0
  63. package/projects/SIMS-master/frontend-project/src/pages/Login.jsx +92 -0
  64. package/projects/SIMS-master/frontend-project/src/pages/Reports.jsx +279 -0
  65. package/projects/SIMS-master/frontend-project/src/pages/SparePart.jsx +185 -0
  66. package/projects/SIMS-master/frontend-project/src/pages/StockIn.jsx +170 -0
  67. package/projects/SIMS-master/frontend-project/src/pages/StockOut.jsx +288 -0
  68. package/projects/SIMS-master/frontend-project/tailwind.config.js +11 -0
  69. package/projects/SIMS-master/frontend-project/vite.config.js +9 -0
@@ -0,0 +1,199 @@
1
+ import { useState, useEffect } from 'react';
2
+ import api from '../api/axios';
3
+
4
+ export default function Reports() {
5
+ const [tab, setTab] = useState('daily');
6
+ const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
7
+ const [dailyData, setDailyData] = useState(null);
8
+ const [summaryData, setSummaryData] = useState(null);
9
+ const [loading, setLoading] = useState(false);
10
+
11
+ const fetchDaily = async () => {
12
+ setLoading(true);
13
+ try {
14
+ const r = await api.get(`/reports/daily?date=${date}`);
15
+ setDailyData(r.data);
16
+ } catch {}
17
+ setLoading(false);
18
+ };
19
+
20
+ const fetchSummary = async () => {
21
+ setLoading(true);
22
+ try {
23
+ const r = await api.get('/reports/summary');
24
+ setSummaryData(r.data);
25
+ } catch {}
26
+ setLoading(false);
27
+ };
28
+
29
+ useEffect(() => {
30
+ if (tab === 'daily') fetchDaily();
31
+ else fetchSummary();
32
+ }, [tab]);
33
+
34
+ const fmt = n => Number(n).toLocaleString() + ' Rwf';
35
+
36
+ return (
37
+ <div className="max-w-7xl mx-auto px-4 py-8">
38
+ <h2 className="text-2xl font-bold text-blue-800 mb-6">Reports</h2>
39
+
40
+ <div className="flex gap-2 mb-6">
41
+ <button
42
+ onClick={() => setTab('daily')}
43
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${tab === 'daily' ? 'bg-blue-700 text-white' : 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50'}`}
44
+ >
45
+ Daily Report
46
+ </button>
47
+ <button
48
+ onClick={() => setTab('summary')}
49
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${tab === 'summary' ? 'bg-blue-700 text-white' : 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50'}`}
50
+ >
51
+ Summary Report
52
+ </button>
53
+ </div>
54
+
55
+ {tab === 'daily' && (
56
+ <div>
57
+ <div className="bg-white rounded-xl shadow p-6 mb-6">
58
+ <div className="flex flex-wrap items-end gap-4">
59
+ <div>
60
+ <label className="block text-sm font-medium text-gray-700 mb-1">Select Date</label>
61
+ <input
62
+ type="date"
63
+ value={date}
64
+ onChange={e => setDate(e.target.value)}
65
+ className="border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
66
+ />
67
+ </div>
68
+ <button
69
+ onClick={fetchDaily}
70
+ className="bg-blue-700 hover:bg-blue-800 text-white font-semibold px-5 py-2 rounded-lg text-sm transition-colors"
71
+ >
72
+ Generate Report
73
+ </button>
74
+ <button
75
+ onClick={() => window.print()}
76
+ className="border border-gray-300 text-gray-700 hover:bg-gray-50 font-medium px-5 py-2 rounded-lg text-sm transition-colors no-print"
77
+ >
78
+ Print
79
+ </button>
80
+ </div>
81
+ </div>
82
+
83
+ {loading && <p className="text-gray-400 text-center py-8">Loading...</p>}
84
+
85
+ {dailyData && !loading && (
86
+ <div className="bg-white rounded-xl shadow overflow-hidden">
87
+ <div className="px-6 py-4 bg-blue-50 border-b flex justify-between items-center">
88
+ <div>
89
+ <h3 className="font-bold text-blue-800">Daily Service Report</h3>
90
+ <p className="text-sm text-gray-500">Date: {dailyData.date}</p>
91
+ </div>
92
+ <div className="text-right">
93
+ <p className="text-sm text-gray-500">Total Collected</p>
94
+ <p className="text-xl font-bold text-green-700">{fmt(dailyData.totalCollected)}</p>
95
+ </div>
96
+ </div>
97
+
98
+ {dailyData.records.length === 0 ? (
99
+ <p className="text-center py-12 text-gray-400">No services recorded on {dailyData.date}.</p>
100
+ ) : (
101
+ <div className="overflow-x-auto">
102
+ <table className="w-full text-sm">
103
+ <thead className="bg-gray-50">
104
+ <tr>
105
+ {['Rec#', 'Plate', 'Model', 'Service', 'Service Price', 'Amount Paid', 'Received By'].map(h => (
106
+ <th key={h} className="text-left px-4 py-3 font-medium text-gray-600">{h}</th>
107
+ ))}
108
+ </tr>
109
+ </thead>
110
+ <tbody className="divide-y divide-gray-100">
111
+ {dailyData.records.map((r, i) => (
112
+ <tr key={i} className="hover:bg-gray-50">
113
+ <td className="px-4 py-3">{r.RecordNumber}</td>
114
+ <td className="px-4 py-3 font-mono text-blue-700">{r.PlateNumber}</td>
115
+ <td className="px-4 py-3">{r.Model}</td>
116
+ <td className="px-4 py-3">{r.ServiceName}</td>
117
+ <td className="px-4 py-3">{fmt(r.ServicePrice)}</td>
118
+ <td className="px-4 py-3 font-semibold text-green-700">{r.AmountPaid ? fmt(r.AmountPaid) : <span className="text-gray-400">Unpaid</span>}</td>
119
+ <td className="px-4 py-3">{r.ReceivedBy || '-'}</td>
120
+ </tr>
121
+ ))}
122
+ </tbody>
123
+ <tfoot>
124
+ <tr className="bg-blue-50 font-bold">
125
+ <td colSpan={5} className="px-4 py-3 text-right text-gray-700">TOTAL COLLECTED:</td>
126
+ <td className="px-4 py-3 text-green-700 text-base">{fmt(dailyData.totalCollected)}</td>
127
+ <td></td>
128
+ </tr>
129
+ </tfoot>
130
+ </table>
131
+ </div>
132
+ )}
133
+ </div>
134
+ )}
135
+ </div>
136
+ )}
137
+
138
+ {tab === 'summary' && (
139
+ <div>
140
+ <div className="flex justify-end mb-4 no-print">
141
+ <button onClick={() => window.print()} className="border border-gray-300 text-gray-700 hover:bg-gray-50 font-medium px-5 py-2 rounded-lg text-sm">
142
+ Print
143
+ </button>
144
+ </div>
145
+
146
+ {loading && <p className="text-gray-400 text-center py-8">Loading...</p>}
147
+
148
+ {summaryData && !loading && (
149
+ <div className="bg-white rounded-xl shadow overflow-hidden">
150
+ <div className="px-6 py-4 bg-blue-50 border-b flex justify-between items-center">
151
+ <h3 className="font-bold text-blue-800">Revenue Summary Report</h3>
152
+ <div className="text-right">
153
+ <p className="text-sm text-gray-500">Total Revenue</p>
154
+ <p className="text-xl font-bold text-green-700">{fmt(summaryData.totalRevenue)}</p>
155
+ </div>
156
+ </div>
157
+
158
+ {summaryData.records.length === 0 ? (
159
+ <p className="text-center py-12 text-gray-400">No payment records found.</p>
160
+ ) : (
161
+ <div className="overflow-x-auto">
162
+ <table className="w-full text-sm">
163
+ <thead className="bg-gray-50">
164
+ <tr>
165
+ {['Pay#', 'Pay Date', 'Plate', 'Service Date', 'Service', 'Amount Paid', 'Received By'].map(h => (
166
+ <th key={h} className="text-left px-4 py-3 font-medium text-gray-600">{h}</th>
167
+ ))}
168
+ </tr>
169
+ </thead>
170
+ <tbody className="divide-y divide-gray-100">
171
+ {summaryData.records.map(r => (
172
+ <tr key={r.PaymentNumber} className="hover:bg-gray-50">
173
+ <td className="px-4 py-3">{r.PaymentNumber}</td>
174
+ <td className="px-4 py-3">{r.PaymentDate?.split('T')[0]}</td>
175
+ <td className="px-4 py-3 font-mono text-blue-700">{r.PlateNumber}</td>
176
+ <td className="px-4 py-3">{r.ServiceDate?.split('T')[0]}</td>
177
+ <td className="px-4 py-3">{r.ServiceName}</td>
178
+ <td className="px-4 py-3 font-semibold text-green-700">{fmt(r.AmountPaid)}</td>
179
+ <td className="px-4 py-3">{r.ReceivedBy}</td>
180
+ </tr>
181
+ ))}
182
+ </tbody>
183
+ <tfoot>
184
+ <tr className="bg-blue-50 font-bold">
185
+ <td colSpan={5} className="px-4 py-3 text-right text-gray-700">TOTAL REVENUE:</td>
186
+ <td className="px-4 py-3 text-green-700 text-base">{fmt(summaryData.totalRevenue)}</td>
187
+ <td></td>
188
+ </tr>
189
+ </tfoot>
190
+ </table>
191
+ </div>
192
+ )}
193
+ </div>
194
+ )}
195
+ </div>
196
+ )}
197
+ </div>
198
+ );
199
+ }
@@ -0,0 +1,182 @@
1
+ import { useState, useEffect } from 'react';
2
+ import api from '../api/axios';
3
+
4
+ const EMPTY = { ServiceDate: '', PlateNumber: '', ServiceCode: '' };
5
+
6
+ export default function ServiceRecords() {
7
+ const [form, setForm] = useState(EMPTY);
8
+ const [records, setRecords] = useState([]);
9
+ const [cars, setCars] = useState([]);
10
+ const [services, setServices] = useState([]);
11
+ const [msg, setMsg] = useState({ text: '', type: '' });
12
+ const [loading, setLoading] = useState(false);
13
+ const [editId, setEditId] = useState(null);
14
+
15
+ const fetchAll = () => {
16
+ api.get('/service-records').then(r => setRecords(r.data)).catch(() => {});
17
+ api.get('/cars').then(r => setCars(r.data)).catch(() => {});
18
+ api.get('/services').then(r => setServices(r.data)).catch(() => {});
19
+ };
20
+
21
+ useEffect(() => { fetchAll(); }, []);
22
+
23
+ const flash = (text, type = 'success') => {
24
+ setMsg({ text, type });
25
+ setTimeout(() => setMsg({ text: '', type: '' }), 4000);
26
+ };
27
+
28
+ const handleSubmit = async (e) => {
29
+ e.preventDefault();
30
+ setLoading(true);
31
+ try {
32
+ if (editId) {
33
+ await api.put(`/service-records/${editId}`, form);
34
+ flash('Service record updated.');
35
+ setEditId(null);
36
+ } else {
37
+ await api.post('/service-records', form);
38
+ flash('Service record created.');
39
+ }
40
+ setForm(EMPTY);
41
+ fetchAll();
42
+ } catch (err) {
43
+ flash(err.response?.data?.error || 'Operation failed.', 'error');
44
+ } finally {
45
+ setLoading(false);
46
+ }
47
+ };
48
+
49
+ const handleEdit = (rec) => {
50
+ setForm({ ServiceDate: rec.ServiceDate?.split('T')[0] || '', PlateNumber: rec.PlateNumber, ServiceCode: rec.ServiceCode });
51
+ setEditId(rec.RecordNumber);
52
+ window.scrollTo({ top: 0, behavior: 'smooth' });
53
+ };
54
+
55
+ const handleDelete = async (id) => {
56
+ if (!window.confirm('Delete this service record?')) return;
57
+ try {
58
+ await api.delete(`/service-records/${id}`);
59
+ flash('Service record deleted.');
60
+ fetchAll();
61
+ } catch (err) {
62
+ flash(err.response?.data?.error || 'Delete failed.', 'error');
63
+ }
64
+ };
65
+
66
+ const cancelEdit = () => { setForm(EMPTY); setEditId(null); };
67
+
68
+ const fmt = n => Number(n).toLocaleString() + ' Rwf';
69
+
70
+ return (
71
+ <div className="max-w-7xl mx-auto px-4 py-8">
72
+ <h2 className="text-2xl font-bold text-blue-800 mb-6">
73
+ {editId ? `Edit Record #${editId}` : 'Create Service Record'}
74
+ </h2>
75
+
76
+ {msg.text && (
77
+ <div className={`mb-4 px-4 py-3 rounded-lg text-sm ${msg.type === 'error' ? 'bg-red-50 border border-red-300 text-red-700' : 'bg-green-50 border border-green-300 text-green-700'}`}>
78
+ {msg.text}
79
+ </div>
80
+ )}
81
+
82
+ <div className="bg-white rounded-xl shadow p-6 mb-8">
83
+ <form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-3 gap-4">
84
+ <div>
85
+ <label className="block text-sm font-medium text-gray-700 mb-1">Service Date</label>
86
+ <input
87
+ type="date"
88
+ value={form.ServiceDate}
89
+ onChange={e => setForm({ ...form, ServiceDate: e.target.value })}
90
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
91
+ required
92
+ />
93
+ </div>
94
+ <div>
95
+ <label className="block text-sm font-medium text-gray-700 mb-1">Car (Plate Number)</label>
96
+ <select
97
+ value={form.PlateNumber}
98
+ onChange={e => setForm({ ...form, PlateNumber: e.target.value })}
99
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
100
+ required
101
+ >
102
+ <option value="">-- Select Car --</option>
103
+ {cars.map(c => (
104
+ <option key={c.PlateNumber} value={c.PlateNumber}>
105
+ {c.PlateNumber} - {c.Model}
106
+ </option>
107
+ ))}
108
+ </select>
109
+ </div>
110
+ <div>
111
+ <label className="block text-sm font-medium text-gray-700 mb-1">Service</label>
112
+ <select
113
+ value={form.ServiceCode}
114
+ onChange={e => setForm({ ...form, ServiceCode: e.target.value })}
115
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
116
+ required
117
+ >
118
+ <option value="">-- Select Service --</option>
119
+ {services.map(s => (
120
+ <option key={s.ServiceCode} value={s.ServiceCode}>
121
+ {s.ServiceName} ({Number(s.ServicePrice).toLocaleString()} Rwf)
122
+ </option>
123
+ ))}
124
+ </select>
125
+ </div>
126
+ <div className="md:col-span-3 flex justify-end gap-2">
127
+ {editId && (
128
+ <button type="button" onClick={cancelEdit} className="px-6 py-2 rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-50">
129
+ Cancel
130
+ </button>
131
+ )}
132
+ <button
133
+ type="submit"
134
+ disabled={loading}
135
+ className="bg-blue-700 hover:bg-blue-800 disabled:bg-blue-400 text-white font-semibold px-6 py-2 rounded-lg transition-colors"
136
+ >
137
+ {loading ? 'Saving...' : editId ? 'Update Record' : 'Create Record'}
138
+ </button>
139
+ </div>
140
+ </form>
141
+ </div>
142
+
143
+ <div className="bg-white rounded-xl shadow overflow-hidden">
144
+ <div className="px-6 py-4 border-b border-gray-100">
145
+ <h3 className="font-semibold text-gray-800">Service Records ({records.length})</h3>
146
+ </div>
147
+ <div className="overflow-x-auto">
148
+ <table className="w-full text-sm">
149
+ <thead className="bg-gray-50">
150
+ <tr>
151
+ {['#', 'Date', 'Plate', 'Model', 'Service', 'Price', 'Mechanic', 'Actions'].map(h => (
152
+ <th key={h} className="text-left px-4 py-3 font-medium text-gray-600">{h}</th>
153
+ ))}
154
+ </tr>
155
+ </thead>
156
+ <tbody className="divide-y divide-gray-100">
157
+ {records.length === 0 ? (
158
+ <tr><td colSpan={8} className="text-center py-8 text-gray-400">No records found.</td></tr>
159
+ ) : records.map(r => (
160
+ <tr key={r.RecordNumber} className="hover:bg-gray-50">
161
+ <td className="px-4 py-3 font-medium">{r.RecordNumber}</td>
162
+ <td className="px-4 py-3">{r.ServiceDate?.split('T')[0]}</td>
163
+ <td className="px-4 py-3 font-mono text-blue-700">{r.PlateNumber}</td>
164
+ <td className="px-4 py-3">{r.Model}</td>
165
+ <td className="px-4 py-3">{r.ServiceName}</td>
166
+ <td className="px-4 py-3 text-green-700 font-semibold">{fmt(r.ServicePrice)}</td>
167
+ <td className="px-4 py-3">{r.MechanicName}</td>
168
+ <td className="px-4 py-3">
169
+ <div className="flex gap-2">
170
+ <button onClick={() => handleEdit(r)} className="text-blue-600 hover:text-blue-800 text-xs font-medium">Edit</button>
171
+ <button onClick={() => handleDelete(r.RecordNumber)} className="text-red-600 hover:text-red-800 text-xs font-medium">Delete</button>
172
+ </div>
173
+ </td>
174
+ </tr>
175
+ ))}
176
+ </tbody>
177
+ </table>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ );
182
+ }
@@ -0,0 +1,125 @@
1
+ import { useState, useEffect } from 'react';
2
+ import api from '../api/axios';
3
+
4
+ const EMPTY = { ServiceCode: '', ServiceName: '', ServicePrice: '' };
5
+
6
+ export default function Services() {
7
+ const [form, setForm] = useState(EMPTY);
8
+ const [services, setServices] = useState([]);
9
+ const [msg, setMsg] = useState({ text: '', type: '' });
10
+ const [loading, setLoading] = useState(false);
11
+
12
+ const fetchServices = () => api.get('/services').then(r => setServices(r.data)).catch(() => {});
13
+
14
+ useEffect(() => { fetchServices(); }, []);
15
+
16
+ const flash = (text, type = 'success') => {
17
+ setMsg({ text, type });
18
+ setTimeout(() => setMsg({ text: '', type: '' }), 4000);
19
+ };
20
+
21
+ const handleSubmit = async (e) => {
22
+ e.preventDefault();
23
+ setLoading(true);
24
+ try {
25
+ await api.post('/services', form);
26
+ flash('Service added successfully.');
27
+ setForm(EMPTY);
28
+ fetchServices();
29
+ } catch (err) {
30
+ flash(err.response?.data?.error || 'Failed to add service.', 'error');
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ const fmt = n => Number(n).toLocaleString() + ' Rwf';
37
+
38
+ return (
39
+ <div className="max-w-7xl mx-auto px-4 py-8">
40
+ <h2 className="text-2xl font-bold text-blue-800 mb-6">Manage Services</h2>
41
+
42
+ {msg.text && (
43
+ <div className={`mb-4 px-4 py-3 rounded-lg text-sm ${msg.type === 'error' ? 'bg-red-50 border border-red-300 text-red-700' : 'bg-green-50 border border-green-300 text-green-700'}`}>
44
+ {msg.text}
45
+ </div>
46
+ )}
47
+
48
+ <div className="bg-white rounded-xl shadow p-6 mb-8">
49
+ <form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-3 gap-4">
50
+ <div>
51
+ <label className="block text-sm font-medium text-gray-700 mb-1">Service Code</label>
52
+ <input
53
+ type="text"
54
+ value={form.ServiceCode}
55
+ onChange={e => setForm({ ...form, ServiceCode: e.target.value })}
56
+ placeholder="e.g. SRV007"
57
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
58
+ required
59
+ />
60
+ </div>
61
+ <div>
62
+ <label className="block text-sm font-medium text-gray-700 mb-1">Service Name</label>
63
+ <input
64
+ type="text"
65
+ value={form.ServiceName}
66
+ onChange={e => setForm({ ...form, ServiceName: e.target.value })}
67
+ placeholder="e.g. Brake Repair"
68
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
69
+ required
70
+ />
71
+ </div>
72
+ <div>
73
+ <label className="block text-sm font-medium text-gray-700 mb-1">Service Price (Rwf)</label>
74
+ <input
75
+ type="number"
76
+ value={form.ServicePrice}
77
+ onChange={e => setForm({ ...form, ServicePrice: e.target.value })}
78
+ placeholder="e.g. 30000"
79
+ min="0"
80
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
81
+ required
82
+ />
83
+ </div>
84
+ <div className="md:col-span-3 flex justify-end">
85
+ <button
86
+ type="submit"
87
+ disabled={loading}
88
+ className="bg-blue-700 hover:bg-blue-800 disabled:bg-blue-400 text-white font-semibold px-6 py-2 rounded-lg transition-colors"
89
+ >
90
+ {loading ? 'Saving...' : 'Add Service'}
91
+ </button>
92
+ </div>
93
+ </form>
94
+ </div>
95
+
96
+ <div className="bg-white rounded-xl shadow overflow-hidden">
97
+ <div className="px-6 py-4 border-b border-gray-100">
98
+ <h3 className="font-semibold text-gray-800">Available Services ({services.length})</h3>
99
+ </div>
100
+ <div className="overflow-x-auto">
101
+ <table className="w-full text-sm">
102
+ <thead className="bg-gray-50">
103
+ <tr>
104
+ {['Service Code', 'Service Name', 'Price (Rwf)'].map(h => (
105
+ <th key={h} className="text-left px-4 py-3 font-medium text-gray-600">{h}</th>
106
+ ))}
107
+ </tr>
108
+ </thead>
109
+ <tbody className="divide-y divide-gray-100">
110
+ {services.length === 0 ? (
111
+ <tr><td colSpan={3} className="text-center py-8 text-gray-400">No services found.</td></tr>
112
+ ) : services.map(s => (
113
+ <tr key={s.ServiceCode} className="hover:bg-gray-50">
114
+ <td className="px-4 py-3 font-mono font-medium text-blue-700">{s.ServiceCode}</td>
115
+ <td className="px-4 py-3">{s.ServiceName}</td>
116
+ <td className="px-4 py-3 font-semibold text-green-700">{fmt(s.ServicePrice)}</td>
117
+ </tr>
118
+ ))}
119
+ </tbody>
120
+ </table>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ );
125
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ export default defineConfig({
6
+ plugins: [
7
+ react(),
8
+ tailwindcss(),
9
+ ],
10
+ })
@@ -0,0 +1,6 @@
1
+ DB_HOST=localhost
2
+ DB_USER=root
3
+ DB_PASSWORD=
4
+ DB_NAME=SIMS
5
+ SESSION_SECRET=sims_secret_key
6
+ PORT=5000
@@ -0,0 +1,12 @@
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_PASSWORD || '',
7
+ database: process.env.DB_NAME || 'SIMS',
8
+ waitForConnections: true,
9
+ connectionLimit: 10,
10
+ });
11
+
12
+ module.exports = pool;
@@ -0,0 +1,8 @@
1
+ function requireAuth(req, res, next) {
2
+ if (!req.session || !req.session.user) {
3
+ return res.status(401).json({ error: 'Unauthorized. Please login.' });
4
+ }
5
+ next();
6
+ }
7
+
8
+ module.exports = requireAuth;