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.
- package/bin/exam-kit.js +357 -0
- package/package.json +25 -0
- package/projects/CRPMS-main/backend-project/config/db.js +12 -0
- package/projects/CRPMS-main/backend-project/config/initDb.js +92 -0
- package/projects/CRPMS-main/backend-project/middleware/auth.js +8 -0
- package/projects/CRPMS-main/backend-project/package-lock.json +1429 -0
- package/projects/CRPMS-main/backend-project/package.json +21 -0
- package/projects/CRPMS-main/backend-project/routes/auth.js +26 -0
- package/projects/CRPMS-main/backend-project/routes/cars.js +36 -0
- package/projects/CRPMS-main/backend-project/routes/payments.js +69 -0
- package/projects/CRPMS-main/backend-project/routes/reports.js +58 -0
- package/projects/CRPMS-main/backend-project/routes/serviceRecords.js +91 -0
- package/projects/CRPMS-main/backend-project/routes/services.js +36 -0
- package/projects/CRPMS-main/backend-project/server.js +44 -0
- package/projects/CRPMS-main/database.sql +59 -0
- package/projects/CRPMS-main/frontend-project/README.md +16 -0
- package/projects/CRPMS-main/frontend-project/eslint.config.js +21 -0
- package/projects/CRPMS-main/frontend-project/index.html +13 -0
- package/projects/CRPMS-main/frontend-project/package-lock.json +3356 -0
- package/projects/CRPMS-main/frontend-project/package.json +32 -0
- package/projects/CRPMS-main/frontend-project/public/favicon.svg +1 -0
- package/projects/CRPMS-main/frontend-project/public/icons.svg +24 -0
- package/projects/CRPMS-main/frontend-project/src/App.css +184 -0
- package/projects/CRPMS-main/frontend-project/src/App.jsx +72 -0
- package/projects/CRPMS-main/frontend-project/src/api/axios.js +8 -0
- package/projects/CRPMS-main/frontend-project/src/assets/hero.png +0 -0
- package/projects/CRPMS-main/frontend-project/src/assets/react.svg +1 -0
- package/projects/CRPMS-main/frontend-project/src/assets/vite.svg +1 -0
- package/projects/CRPMS-main/frontend-project/src/components/Navbar.jsx +54 -0
- package/projects/CRPMS-main/frontend-project/src/components/ProtectedRoute.jsx +9 -0
- package/projects/CRPMS-main/frontend-project/src/context/AuthContext.jsx +35 -0
- package/projects/CRPMS-main/frontend-project/src/index.css +14 -0
- package/projects/CRPMS-main/frontend-project/src/main.jsx +10 -0
- package/projects/CRPMS-main/frontend-project/src/pages/Bill.jsx +227 -0
- package/projects/CRPMS-main/frontend-project/src/pages/Cars.jsx +112 -0
- package/projects/CRPMS-main/frontend-project/src/pages/Login.jsx +78 -0
- package/projects/CRPMS-main/frontend-project/src/pages/Payments.jsx +153 -0
- package/projects/CRPMS-main/frontend-project/src/pages/Reports.jsx +199 -0
- package/projects/CRPMS-main/frontend-project/src/pages/ServiceRecords.jsx +182 -0
- package/projects/CRPMS-main/frontend-project/src/pages/Services.jsx +125 -0
- package/projects/CRPMS-main/frontend-project/vite.config.js +10 -0
- package/projects/SIMS-master/backend-project/.env.example +6 -0
- package/projects/SIMS-master/backend-project/config/db.js +12 -0
- package/projects/SIMS-master/backend-project/middleware/auth.js +8 -0
- package/projects/SIMS-master/backend-project/package-lock.json +1221 -0
- package/projects/SIMS-master/backend-project/package.json +23 -0
- package/projects/SIMS-master/backend-project/routes/auth.js +29 -0
- package/projects/SIMS-master/backend-project/routes/reports.js +51 -0
- package/projects/SIMS-master/backend-project/routes/spareParts.js +34 -0
- package/projects/SIMS-master/backend-project/routes/stockIn.js +53 -0
- package/projects/SIMS-master/backend-project/routes/stockOut.js +146 -0
- package/projects/SIMS-master/backend-project/seed.js +20 -0
- package/projects/SIMS-master/backend-project/server.js +43 -0
- package/projects/SIMS-master/database.sql +43 -0
- package/projects/SIMS-master/frontend-project/index.html +12 -0
- package/projects/SIMS-master/frontend-project/package-lock.json +3352 -0
- package/projects/SIMS-master/frontend-project/package.json +27 -0
- package/projects/SIMS-master/frontend-project/postcss.config.js +6 -0
- package/projects/SIMS-master/frontend-project/src/App.jsx +53 -0
- package/projects/SIMS-master/frontend-project/src/components/Navbar.jsx +103 -0
- package/projects/SIMS-master/frontend-project/src/index.css +3 -0
- package/projects/SIMS-master/frontend-project/src/main.jsx +10 -0
- package/projects/SIMS-master/frontend-project/src/pages/Login.jsx +92 -0
- package/projects/SIMS-master/frontend-project/src/pages/Reports.jsx +279 -0
- package/projects/SIMS-master/frontend-project/src/pages/SparePart.jsx +185 -0
- package/projects/SIMS-master/frontend-project/src/pages/StockIn.jsx +170 -0
- package/projects/SIMS-master/frontend-project/src/pages/StockOut.jsx +288 -0
- package/projects/SIMS-master/frontend-project/tailwind.config.js +11 -0
- 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,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;
|