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,227 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import api from '../api/axios';
3
+
4
+ export default function Bill({ paymentId, onClose }) {
5
+ const [bill, setBill] = useState(null);
6
+ const [error, setError] = useState('');
7
+ const [exporting, setExporting] = useState(false);
8
+ const printRef = useRef();
9
+
10
+ useEffect(() => {
11
+ api.get(`/payments/${paymentId}/bill`)
12
+ .then(r => setBill(r.data))
13
+ .catch(() => setError('Failed to load bill.'));
14
+ }, [paymentId]);
15
+
16
+ const handlePrint = () => window.print();
17
+
18
+ const handleExportPDF = async () => {
19
+ if (!bill || exporting) return;
20
+ setExporting(true);
21
+ try {
22
+ const { jsPDF } = await import('jspdf');
23
+ const doc = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' });
24
+ const pageW = 190;
25
+ let y = 20;
26
+
27
+ const center = (text, size = 14, style = 'bold') => {
28
+ doc.setFontSize(size);
29
+ doc.setFont('helvetica', style);
30
+ const tw = doc.getTextWidth(text);
31
+ doc.text(text, (210 - tw) / 2, y);
32
+ y += size * 0.4;
33
+ };
34
+ const line = (text, size = 10, style = 'normal', x = 15) => {
35
+ doc.setFontSize(size);
36
+ doc.setFont('helvetica', style);
37
+ doc.text(text, x, y);
38
+ y += size * 0.45;
39
+ };
40
+ const row = (l, r, size = 10) => {
41
+ doc.setFontSize(size);
42
+ doc.setFont('helvetica', 'normal');
43
+ doc.text(l, 15, y);
44
+ doc.text(r, 200 - doc.getTextWidth(r), y);
45
+ y += size * 0.45;
46
+ };
47
+
48
+ center('SmartPark Garage', 18, 'bold');
49
+ center('Rubavu District, Western Province, Rwanda', 9, 'normal');
50
+ center('PAYMENT RECEIPT', 14, 'bold');
51
+ center(`Bill #${bill.PaymentNumber}`, 9, 'normal');
52
+ y += 4;
53
+ doc.line(15, y, 195, y);
54
+ y += 6;
55
+
56
+ doc.setFontSize(9);
57
+ doc.setFont('helvetica', 'bold');
58
+ doc.text('BILLED TO:', 15, y);
59
+ doc.setFont('helvetica', 'normal');
60
+ y += 5;
61
+ line(`Driver Phone: ${bill.DriverPhone}`, 10, 'normal', 15);
62
+ line(`Plate: ${bill.PlateNumber}`, 10, 'bold', 15);
63
+ line(`${bill.type} - ${bill.Model} (${bill.ManufacturingYear})`, 10, 'normal', 15);
64
+ y += 2;
65
+
66
+ doc.setFont('helvetica', 'bold');
67
+ doc.text('PAYMENT DETAILS:', 200 - doc.getTextWidth('PAYMENT DETAILS:'), y - 5);
68
+ doc.setFont('helvetica', 'normal');
69
+ row('Payment #:', `${bill.PaymentNumber}`, 10);
70
+ row('Service Date:', bill.ServiceDate?.split('T')[0] || '', 10);
71
+ row('Payment Date:', bill.PaymentDate?.split('T')[0] || '', 10);
72
+ y += 2;
73
+ doc.line(15, y, 195, y);
74
+ y += 6;
75
+
76
+ doc.setFontSize(10);
77
+ doc.setFont('helvetica', 'bold');
78
+ doc.text('Service Description', 15, y);
79
+ doc.text('Price (Rwf)', 200 - doc.getTextWidth('Price (Rwf)'), y);
80
+ y += 5;
81
+ doc.line(15, y, 195, y);
82
+ y += 3;
83
+ doc.setFont('helvetica', 'normal');
84
+ line(bill.ServiceName, 10, 'normal', 15);
85
+ doc.text(Number(bill.ServicePrice).toLocaleString(), 200 - doc.getTextWidth(Number(bill.ServicePrice).toLocaleString()), y - 10 + 10 * 0.45);
86
+ y += 2;
87
+ doc.line(15, y, 195, y);
88
+ y += 4;
89
+ doc.setFont('helvetica', 'bold');
90
+ doc.setFontSize(12);
91
+ doc.text('TOTAL AMOUNT PAID', 15, y);
92
+ const totalStr = `${Number(bill.AmountPaid).toLocaleString()} Rwf`;
93
+ doc.setTextColor(22, 163, 74);
94
+ doc.text(totalStr, 200 - doc.getTextWidth(totalStr), y);
95
+ doc.setTextColor(0, 0, 0);
96
+ y += 10;
97
+ doc.line(15, y, 195, y);
98
+ y += 6;
99
+
100
+ doc.setFontSize(10);
101
+ doc.setFont('helvetica', 'bold');
102
+ doc.text('Mechanic:', 15, y);
103
+ doc.setFont('helvetica', 'normal');
104
+ line(bill.MechanicName, 10, 'normal', 15);
105
+ y += 2;
106
+ doc.setFont('helvetica', 'bold');
107
+ doc.text('Received By:', 200 - doc.getTextWidth('Received By:'), y - 10);
108
+ doc.setTextColor(30, 64, 175);
109
+ doc.setFont('helvetica', 'bold');
110
+ doc.text(bill.ReceivedBy, 200 - doc.getTextWidth(bill.ReceivedBy), y - 5);
111
+ doc.setTextColor(0, 0, 0);
112
+ y += 10;
113
+ doc.line(140, y, 195, y);
114
+ y += 3;
115
+ doc.setFontSize(8);
116
+ doc.setTextColor(156, 163, 175);
117
+ doc.text('Authorized Signature', 200 - doc.getTextWidth('Authorized Signature'), y);
118
+
119
+ y = 275;
120
+ doc.setFontSize(8);
121
+ doc.setTextColor(156, 163, 175);
122
+ center('Thank you for choosing SmartPark Garage!', 9, 'normal');
123
+ center(`Record #${bill.RecordNumber}`, 8, 'normal');
124
+
125
+ doc.save(`SmartPark_Bill_${bill.PaymentNumber}.pdf`);
126
+ } catch (err) {
127
+ console.error('PDF export failed:', err);
128
+ } finally {
129
+ setExporting(false);
130
+ }
131
+ };
132
+
133
+ if (error) return (
134
+ <div className="max-w-2xl mx-auto px-4 py-16 text-center">
135
+ <p className="text-red-600 mb-4">{error}</p>
136
+ <button onClick={onClose} className="text-blue-600 underline">Go back</button>
137
+ </div>
138
+ );
139
+
140
+ if (!bill) return (
141
+ <div className="flex items-center justify-center min-h-64">
142
+ <p className="text-gray-400">Loading bill...</p>
143
+ </div>
144
+ );
145
+
146
+ const fmt = n => Number(n).toLocaleString();
147
+
148
+ return (
149
+ <div className="max-w-2xl mx-auto px-4 py-8">
150
+ <div className="no-print flex gap-3 mb-6">
151
+ <button onClick={onClose} className="text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg px-4 py-2 text-sm cursor-pointer">
152
+ ← Back
153
+ </button>
154
+ <button onClick={handlePrint} className="bg-blue-700 hover:bg-blue-800 text-white rounded-lg px-4 py-2 text-sm font-medium cursor-pointer">
155
+ Print Bill
156
+ </button>
157
+ <button onClick={handleExportPDF} disabled={exporting} className="bg-green-700 hover:bg-green-800 disabled:opacity-60 text-white rounded-lg px-4 py-2 text-sm font-medium cursor-pointer">
158
+ {exporting ? 'Generating PDF...' : 'Export as PDF'}
159
+ </button>
160
+ </div>
161
+
162
+ <div ref={printRef} className="bg-white rounded-xl shadow-lg p-8 border border-gray-200">
163
+ <div className="text-center border-b pb-6 mb-6">
164
+ <h1 className="text-3xl font-bold text-blue-800">SmartPark Garage</h1>
165
+ <p className="text-gray-500 text-sm mt-1">Rubavu District, Western Province, Rwanda</p>
166
+ <h2 className="text-xl font-semibold text-gray-700 mt-4">PAYMENT RECEIPT</h2>
167
+ <p className="text-gray-400 text-sm">Bill #{bill.PaymentNumber}</p>
168
+ </div>
169
+
170
+ <div className="grid grid-cols-2 gap-6 mb-6 text-sm">
171
+ <div>
172
+ <p className="text-gray-500 font-medium mb-2">BILLED TO:</p>
173
+ <p className="font-medium">{bill.DriverPhone}</p>
174
+ <p className="text-gray-600">Plate: <span className="font-mono font-bold">{bill.PlateNumber}</span></p>
175
+ <p className="text-gray-600">{bill.type} - {bill.Model} ({bill.ManufacturingYear})</p>
176
+ </div>
177
+ <div className="text-right">
178
+ <p className="text-gray-500 font-medium mb-2">PAYMENT DETAILS:</p>
179
+ <p className="text-gray-600">Payment #: <span className="font-medium">{bill.PaymentNumber}</span></p>
180
+ <p className="text-gray-600">Service Date: <span className="font-medium">{bill.ServiceDate?.split('T')[0]}</span></p>
181
+ <p className="text-gray-600">Payment Date: <span className="font-medium">{bill.PaymentDate?.split('T')[0]}</span></p>
182
+ </div>
183
+ </div>
184
+
185
+ <table className="w-full text-sm mb-6">
186
+ <thead>
187
+ <tr className="bg-blue-50">
188
+ <th className="text-left px-4 py-3 font-semibold text-blue-800">Service Description</th>
189
+ <th className="text-right px-4 py-3 font-semibold text-blue-800">Price (Rwf)</th>
190
+ </tr>
191
+ </thead>
192
+ <tbody>
193
+ <tr className="border-b">
194
+ <td className="px-4 py-4">{bill.ServiceName}</td>
195
+ <td className="px-4 py-4 text-right">{fmt(bill.ServicePrice)}</td>
196
+ </tr>
197
+ </tbody>
198
+ <tfoot>
199
+ <tr className="bg-gray-50">
200
+ <td className="px-4 py-3 font-bold text-gray-800">TOTAL AMOUNT PAID</td>
201
+ <td className="px-4 py-3 text-right font-bold text-green-700 text-lg">{fmt(bill.AmountPaid)} Rwf</td>
202
+ </tr>
203
+ </tfoot>
204
+ </table>
205
+
206
+ <div className="border-t pt-6 grid grid-cols-2 gap-6 text-sm">
207
+ <div>
208
+ <p className="text-gray-500 font-medium">Mechanic:</p>
209
+ <p className="font-medium mt-1">{bill.MechanicName}</p>
210
+ </div>
211
+ <div className="text-right">
212
+ <p className="text-gray-500 font-medium">Received By:</p>
213
+ <p className="font-bold mt-1 text-blue-700">{bill.ReceivedBy}</p>
214
+ <div className="mt-4 border-t border-gray-300 pt-2">
215
+ <p className="text-gray-400 text-xs">Authorized Signature</p>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <div className="text-center mt-8 text-xs text-gray-400">
221
+ <p>Thank you for choosing SmartPark Garage!</p>
222
+ <p>Record #{bill.RecordNumber}</p>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ );
227
+ }
@@ -0,0 +1,112 @@
1
+ import { useState, useEffect } from 'react';
2
+ import api from '../api/axios';
3
+
4
+ const EMPTY = { PlateNumber: '', type: '', Model: '', ManufacturingYear: '', DriverPhone: '', MechanicName: '' };
5
+
6
+ export default function Cars() {
7
+ const [form, setForm] = useState(EMPTY);
8
+ const [cars, setCars] = useState([]);
9
+ const [msg, setMsg] = useState({ text: '', type: '' });
10
+ const [loading, setLoading] = useState(false);
11
+
12
+ const fetchCars = () => api.get('/cars').then(r => setCars(r.data)).catch(() => {});
13
+
14
+ useEffect(() => { fetchCars(); }, []);
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('/cars', form);
26
+ flash('Car recorded successfully.');
27
+ setForm(EMPTY);
28
+ fetchCars();
29
+ } catch (err) {
30
+ flash(err.response?.data?.error || 'Failed to add car.', 'error');
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
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">Register Car</h2>
39
+
40
+ {msg.text && (
41
+ <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'}`}>
42
+ {msg.text}
43
+ </div>
44
+ )}
45
+
46
+ <div className="bg-white rounded-xl shadow p-6 mb-8">
47
+ <form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
48
+ {[
49
+ { key: 'PlateNumber', label: 'Plate Number', placeholder: 'e.g. RAB 123 A' },
50
+ { key: 'type', label: 'Type', placeholder: 'e.g. Sedan, SUV' },
51
+ { key: 'Model', label: 'Model', placeholder: 'e.g. Toyota Corolla' },
52
+ { key: 'ManufacturingYear', label: 'Manufacturing Year', placeholder: 'e.g. 2020', type: 'number' },
53
+ { key: 'DriverPhone', label: 'Driver Phone', placeholder: 'e.g. 0788000000' },
54
+ { key: 'MechanicName', label: 'Mechanic Name', placeholder: 'e.g. Jean Pierre' },
55
+ ].map(f => (
56
+ <div key={f.key}>
57
+ <label className="block text-sm font-medium text-gray-700 mb-1">{f.label}</label>
58
+ <input
59
+ type={f.type || 'text'}
60
+ value={form[f.key]}
61
+ onChange={e => setForm({ ...form, [f.key]: e.target.value })}
62
+ placeholder={f.placeholder}
63
+ 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"
64
+ required
65
+ />
66
+ </div>
67
+ ))}
68
+ <div className="md:col-span-2 lg:col-span-3 flex justify-end">
69
+ <button
70
+ type="submit"
71
+ disabled={loading}
72
+ className="bg-blue-700 hover:bg-blue-800 disabled:bg-blue-400 text-white font-semibold px-6 py-2 rounded-lg transition-colors"
73
+ >
74
+ {loading ? 'Saving...' : 'Register Car'}
75
+ </button>
76
+ </div>
77
+ </form>
78
+ </div>
79
+
80
+ <div className="bg-white rounded-xl shadow overflow-hidden">
81
+ <div className="px-6 py-4 border-b border-gray-100">
82
+ <h3 className="font-semibold text-gray-800">Registered Cars ({cars.length})</h3>
83
+ </div>
84
+ <div className="overflow-x-auto">
85
+ <table className="w-full text-sm">
86
+ <thead className="bg-gray-50">
87
+ <tr>
88
+ {['Plate Number', 'Type', 'Model', 'Year', 'Driver Phone', 'Mechanic'].map(h => (
89
+ <th key={h} className="text-left px-4 py-3 font-medium text-gray-600">{h}</th>
90
+ ))}
91
+ </tr>
92
+ </thead>
93
+ <tbody className="divide-y divide-gray-100">
94
+ {cars.length === 0 ? (
95
+ <tr><td colSpan={6} className="text-center py-8 text-gray-400">No cars registered yet.</td></tr>
96
+ ) : cars.map(c => (
97
+ <tr key={c.PlateNumber} className="hover:bg-gray-50">
98
+ <td className="px-4 py-3 font-mono font-medium text-blue-700">{c.PlateNumber}</td>
99
+ <td className="px-4 py-3">{c.type}</td>
100
+ <td className="px-4 py-3">{c.Model}</td>
101
+ <td className="px-4 py-3">{c.ManufacturingYear}</td>
102
+ <td className="px-4 py-3">{c.DriverPhone}</td>
103
+ <td className="px-4 py-3">{c.MechanicName}</td>
104
+ </tr>
105
+ ))}
106
+ </tbody>
107
+ </table>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
@@ -0,0 +1,78 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+
5
+ export default function Login() {
6
+ const [form, setForm] = useState({ username: '', password: '' });
7
+ const [error, setError] = useState('');
8
+ const [loading, setLoading] = useState(false);
9
+ const { login } = useAuth();
10
+ const navigate = useNavigate();
11
+
12
+ const handleSubmit = async (e) => {
13
+ e.preventDefault();
14
+ setError('');
15
+ setLoading(true);
16
+ try {
17
+ await login(form.username, form.password);
18
+ navigate('/cars');
19
+ } catch (err) {
20
+ setError(err.response?.data?.error || 'Login failed. Please try again.');
21
+ } finally {
22
+ setLoading(false);
23
+ }
24
+ };
25
+
26
+ return (
27
+ <div className="min-h-screen bg-gradient-to-br from-blue-900 to-blue-700 flex items-center justify-center p-4">
28
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-8">
29
+ <div className="text-center mb-8">
30
+ <h1 className="text-3xl font-bold text-blue-800">SmartPark</h1>
31
+ <p className="text-gray-500 mt-1">Car Repair Payment Management System</p>
32
+ </div>
33
+
34
+ {error && (
35
+ <div className="bg-red-50 border border-red-300 text-red-700 rounded-lg px-4 py-3 mb-4 text-sm">
36
+ {error}
37
+ </div>
38
+ )}
39
+
40
+ <form onSubmit={handleSubmit} className="space-y-5">
41
+ <div>
42
+ <label className="block text-sm font-medium text-gray-700 mb-1">Username</label>
43
+ <input
44
+ type="text"
45
+ value={form.username}
46
+ onChange={e => setForm({ ...form, username: e.target.value })}
47
+ className="w-full border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500"
48
+ placeholder="Enter username"
49
+ required
50
+ />
51
+ </div>
52
+ <div>
53
+ <label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
54
+ <input
55
+ type="password"
56
+ value={form.password}
57
+ onChange={e => setForm({ ...form, password: e.target.value })}
58
+ className="w-full border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500"
59
+ placeholder="Enter password"
60
+ required
61
+ />
62
+ </div>
63
+ <button
64
+ type="submit"
65
+ disabled={loading}
66
+ className="w-full bg-blue-700 hover:bg-blue-800 disabled:bg-blue-400 text-white font-semibold py-2.5 rounded-lg transition-colors"
67
+ >
68
+ {loading ? 'Signing in...' : 'Sign In'}
69
+ </button>
70
+ </form>
71
+
72
+ <p className="text-center text-xs text-gray-400 mt-6">
73
+ Default credentials: admin / Admin@1234
74
+ </p>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,153 @@
1
+ import { useState, useEffect } from 'react';
2
+ import api from '../api/axios';
3
+ import Bill from './Bill';
4
+
5
+ const EMPTY = { AmountPaid: '', PaymentDate: '', RecordNumber: '' };
6
+
7
+ export default function Payments() {
8
+ const [form, setForm] = useState(EMPTY);
9
+ const [payments, setPayments] = useState([]);
10
+ const [records, setRecords] = useState([]);
11
+ const [msg, setMsg] = useState({ text: '', type: '' });
12
+ const [loading, setLoading] = useState(false);
13
+ const [billId, setBillId] = useState(null);
14
+
15
+ const fetchAll = () => {
16
+ api.get('/payments').then(r => setPayments(r.data)).catch(() => {});
17
+ api.get('/service-records').then(r => setRecords(r.data)).catch(() => {});
18
+ };
19
+
20
+ useEffect(() => { fetchAll(); }, []);
21
+
22
+ const flash = (text, type = 'success') => {
23
+ setMsg({ text, type });
24
+ setTimeout(() => setMsg({ text: '', type: '' }), 4000);
25
+ };
26
+
27
+ const handleRecordChange = (e) => {
28
+ const rec = records.find(r => r.RecordNumber == e.target.value);
29
+ setForm({ ...form, RecordNumber: e.target.value, AmountPaid: rec ? rec.ServicePrice : '' });
30
+ };
31
+
32
+ const handleSubmit = async (e) => {
33
+ e.preventDefault();
34
+ setLoading(true);
35
+ try {
36
+ const res = await api.post('/payments', form);
37
+ flash('Payment recorded successfully.');
38
+ setBillId(res.data.PaymentNumber);
39
+ setForm(EMPTY);
40
+ fetchAll();
41
+ } catch (err) {
42
+ flash(err.response?.data?.error || 'Failed to record payment.', 'error');
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ };
47
+
48
+ const fmt = n => Number(n).toLocaleString() + ' Rwf';
49
+
50
+ if (billId) {
51
+ return <Bill paymentId={billId} onClose={() => setBillId(null)} />;
52
+ }
53
+
54
+ return (
55
+ <div className="max-w-7xl mx-auto px-4 py-8">
56
+ <h2 className="text-2xl font-bold text-blue-800 mb-6">Record Payment</h2>
57
+
58
+ {msg.text && (
59
+ <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'}`}>
60
+ {msg.text}
61
+ </div>
62
+ )}
63
+
64
+ <div className="bg-white rounded-xl shadow p-6 mb-8">
65
+ <form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-3 gap-4">
66
+ <div>
67
+ <label className="block text-sm font-medium text-gray-700 mb-1">Service Record</label>
68
+ <select
69
+ value={form.RecordNumber}
70
+ onChange={handleRecordChange}
71
+ 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"
72
+ required
73
+ >
74
+ <option value="">-- Select Record --</option>
75
+ {records.map(r => (
76
+ <option key={r.RecordNumber} value={r.RecordNumber}>
77
+ #{r.RecordNumber} - {r.PlateNumber} ({r.ServiceName})
78
+ </option>
79
+ ))}
80
+ </select>
81
+ </div>
82
+ <div>
83
+ <label className="block text-sm font-medium text-gray-700 mb-1">Amount Paid (Rwf)</label>
84
+ <input
85
+ type="number"
86
+ value={form.AmountPaid}
87
+ onChange={e => setForm({ ...form, AmountPaid: e.target.value })}
88
+ placeholder="Amount in Rwf"
89
+ min="0"
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">Payment Date</label>
96
+ <input
97
+ type="date"
98
+ value={form.PaymentDate}
99
+ onChange={e => setForm({ ...form, PaymentDate: e.target.value })}
100
+ 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"
101
+ required
102
+ />
103
+ </div>
104
+ <div className="md:col-span-3 flex justify-end">
105
+ <button
106
+ type="submit"
107
+ disabled={loading}
108
+ className="bg-blue-700 hover:bg-blue-800 disabled:bg-blue-400 text-white font-semibold px-6 py-2 rounded-lg transition-colors"
109
+ >
110
+ {loading ? 'Saving...' : 'Record Payment'}
111
+ </button>
112
+ </div>
113
+ </form>
114
+ </div>
115
+
116
+ <div className="bg-white rounded-xl shadow overflow-hidden">
117
+ <div className="px-6 py-4 border-b border-gray-100">
118
+ <h3 className="font-semibold text-gray-800">Payment History ({payments.length})</h3>
119
+ </div>
120
+ <div className="overflow-x-auto">
121
+ <table className="w-full text-sm">
122
+ <thead className="bg-gray-50">
123
+ <tr>
124
+ {['Pay#', 'Date', 'Plate', 'Service', 'Amount', 'Received By', 'Bill'].map(h => (
125
+ <th key={h} className="text-left px-4 py-3 font-medium text-gray-600">{h}</th>
126
+ ))}
127
+ </tr>
128
+ </thead>
129
+ <tbody className="divide-y divide-gray-100">
130
+ {payments.length === 0 ? (
131
+ <tr><td colSpan={7} className="text-center py-8 text-gray-400">No payments recorded yet.</td></tr>
132
+ ) : payments.map(p => (
133
+ <tr key={p.PaymentNumber} className="hover:bg-gray-50">
134
+ <td className="px-4 py-3 font-medium">{p.PaymentNumber}</td>
135
+ <td className="px-4 py-3">{p.PaymentDate?.split('T')[0]}</td>
136
+ <td className="px-4 py-3 font-mono text-blue-700">{p.PlateNumber}</td>
137
+ <td className="px-4 py-3">{p.ServiceName}</td>
138
+ <td className="px-4 py-3 font-semibold text-green-700">{fmt(p.AmountPaid)}</td>
139
+ <td className="px-4 py-3">{p.ReceivedBy}</td>
140
+ <td className="px-4 py-3">
141
+ <button onClick={() => setBillId(p.PaymentNumber)} className="text-blue-600 hover:text-blue-800 text-xs font-medium underline">
142
+ View Bill
143
+ </button>
144
+ </td>
145
+ </tr>
146
+ ))}
147
+ </tbody>
148
+ </table>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ );
153
+ }