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,185 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
function Alert({ message, type, onClose }) {
|
|
5
|
+
if (!message) return null;
|
|
6
|
+
return (
|
|
7
|
+
<div className={`px-4 py-3 rounded-lg mb-4 text-sm flex justify-between items-center ${type === 'success' ? 'bg-green-50 border border-green-200 text-green-700' : 'bg-red-50 border border-red-200 text-red-700'}`}>
|
|
8
|
+
<span>{message}</span>
|
|
9
|
+
<button onClick={onClose} className="ml-4 font-bold text-lg leading-none">×</button>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function SparePart() {
|
|
15
|
+
const [spareParts, setSpareParts] = useState([]);
|
|
16
|
+
const [form, setForm] = useState({ Name: '', Category: '', Quantity: '', UnitPrice: '' });
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [alert, setAlert] = useState({ message: '', type: '' });
|
|
19
|
+
|
|
20
|
+
const fetchSpareParts = async () => {
|
|
21
|
+
try {
|
|
22
|
+
const res = await axios.get('/api/spare-parts');
|
|
23
|
+
setSpareParts(res.data);
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
useEffect(() => { fetchSpareParts(); }, []);
|
|
28
|
+
|
|
29
|
+
const showAlert = (message, type) => {
|
|
30
|
+
setAlert({ message, type });
|
|
31
|
+
setTimeout(() => setAlert({ message: '', type: '' }), 4000);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleSubmit = async (e) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setLoading(true);
|
|
37
|
+
try {
|
|
38
|
+
await axios.post('/api/spare-parts', form);
|
|
39
|
+
showAlert('Spare part added successfully!', 'success');
|
|
40
|
+
setForm({ Name: '', Category: '', Quantity: '', UnitPrice: '' });
|
|
41
|
+
fetchSpareParts();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
showAlert(err.response?.data?.message || 'Failed to add spare part.', 'error');
|
|
44
|
+
} finally {
|
|
45
|
+
setLoading(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const totalValue = spareParts.reduce((sum, sp) => sum + parseFloat(sp.TotalPrice || 0), 0);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
53
|
+
<div className="mb-6">
|
|
54
|
+
<h2 className="text-2xl font-bold text-gray-800">Spare Parts</h2>
|
|
55
|
+
<p className="text-gray-500 text-sm mt-1">Manage your spare parts inventory</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Summary cards */}
|
|
59
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
|
60
|
+
<div className="bg-blue-800 text-white rounded-xl p-5 shadow">
|
|
61
|
+
<p className="text-blue-200 text-sm">Total Parts</p>
|
|
62
|
+
<p className="text-3xl font-bold mt-1">{spareParts.length}</p>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="bg-green-700 text-white rounded-xl p-5 shadow">
|
|
65
|
+
<p className="text-green-200 text-sm">Total Stock Value (RWF)</p>
|
|
66
|
+
<p className="text-3xl font-bold mt-1">{totalValue.toLocaleString()}</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Add form */}
|
|
71
|
+
<div className="bg-white rounded-xl shadow p-6 mb-8">
|
|
72
|
+
<h3 className="text-lg font-semibold text-gray-700 mb-4 border-b pb-2">Add New Spare Part</h3>
|
|
73
|
+
<Alert message={alert.message} type={alert.type} onClose={() => setAlert({ message: '', type: '' })} />
|
|
74
|
+
<form onSubmit={handleSubmit} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
75
|
+
<div>
|
|
76
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Name *</label>
|
|
77
|
+
<input
|
|
78
|
+
type="text"
|
|
79
|
+
value={form.Name}
|
|
80
|
+
onChange={e => setForm({ ...form, Name: e.target.value })}
|
|
81
|
+
required
|
|
82
|
+
placeholder="e.g. Brake Pad"
|
|
83
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
<div>
|
|
87
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Category *</label>
|
|
88
|
+
<input
|
|
89
|
+
type="text"
|
|
90
|
+
value={form.Category}
|
|
91
|
+
onChange={e => setForm({ ...form, Category: e.target.value })}
|
|
92
|
+
required
|
|
93
|
+
placeholder="e.g. Braking System"
|
|
94
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
<div>
|
|
98
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Quantity *</label>
|
|
99
|
+
<input
|
|
100
|
+
type="number"
|
|
101
|
+
min="1"
|
|
102
|
+
value={form.Quantity}
|
|
103
|
+
onChange={e => setForm({ ...form, Quantity: e.target.value })}
|
|
104
|
+
required
|
|
105
|
+
placeholder="0"
|
|
106
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
<div>
|
|
110
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Unit Price (RWF) *</label>
|
|
111
|
+
<input
|
|
112
|
+
type="number"
|
|
113
|
+
min="0"
|
|
114
|
+
step="0.01"
|
|
115
|
+
value={form.UnitPrice}
|
|
116
|
+
onChange={e => setForm({ ...form, UnitPrice: e.target.value })}
|
|
117
|
+
required
|
|
118
|
+
placeholder="0.00"
|
|
119
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
{form.Quantity && form.UnitPrice && (
|
|
123
|
+
<div className="sm:col-span-2 lg:col-span-4">
|
|
124
|
+
<p className="text-sm text-blue-700 font-medium">
|
|
125
|
+
Total Price: RWF {(parseFloat(form.Quantity) * parseFloat(form.UnitPrice)).toLocaleString()}
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
<div className="sm:col-span-2 lg:col-span-4">
|
|
130
|
+
<button
|
|
131
|
+
type="submit"
|
|
132
|
+
disabled={loading}
|
|
133
|
+
className="bg-blue-800 text-white px-6 py-2 rounded-lg hover:bg-blue-900 font-medium transition-colors disabled:opacity-60 text-sm"
|
|
134
|
+
>
|
|
135
|
+
{loading ? 'Adding...' : 'Add Spare Part'}
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
</form>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Table */}
|
|
142
|
+
<div className="bg-white rounded-xl shadow overflow-hidden">
|
|
143
|
+
<div className="px-6 py-4 border-b">
|
|
144
|
+
<h3 className="text-lg font-semibold text-gray-700">Spare Parts List</h3>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="overflow-x-auto">
|
|
147
|
+
<table className="w-full text-sm">
|
|
148
|
+
<thead className="bg-blue-800 text-white">
|
|
149
|
+
<tr>
|
|
150
|
+
<th className="px-4 py-3 text-left">#</th>
|
|
151
|
+
<th className="px-4 py-3 text-left">Name</th>
|
|
152
|
+
<th className="px-4 py-3 text-left">Category</th>
|
|
153
|
+
<th className="px-4 py-3 text-right">Quantity</th>
|
|
154
|
+
<th className="px-4 py-3 text-right">Unit Price (RWF)</th>
|
|
155
|
+
<th className="px-4 py-3 text-right">Total Price (RWF)</th>
|
|
156
|
+
</tr>
|
|
157
|
+
</thead>
|
|
158
|
+
<tbody className="divide-y divide-gray-100">
|
|
159
|
+
{spareParts.length === 0 ? (
|
|
160
|
+
<tr>
|
|
161
|
+
<td colSpan="6" className="text-center py-10 text-gray-400">
|
|
162
|
+
No spare parts recorded yet.
|
|
163
|
+
</td>
|
|
164
|
+
</tr>
|
|
165
|
+
) : spareParts.map((sp, i) => (
|
|
166
|
+
<tr key={sp.SparePartID} className="hover:bg-gray-50 transition-colors">
|
|
167
|
+
<td className="px-4 py-3 text-gray-500">{i + 1}</td>
|
|
168
|
+
<td className="px-4 py-3 font-semibold text-gray-800">{sp.Name}</td>
|
|
169
|
+
<td className="px-4 py-3">
|
|
170
|
+
<span className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full text-xs font-medium">{sp.Category}</span>
|
|
171
|
+
</td>
|
|
172
|
+
<td className="px-4 py-3 text-right font-medium">{sp.Quantity}</td>
|
|
173
|
+
<td className="px-4 py-3 text-right">{Number(sp.UnitPrice).toLocaleString()}</td>
|
|
174
|
+
<td className="px-4 py-3 text-right font-semibold text-green-700">{Number(sp.TotalPrice).toLocaleString()}</td>
|
|
175
|
+
</tr>
|
|
176
|
+
))}
|
|
177
|
+
</tbody>
|
|
178
|
+
</table>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export default SparePart;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
function Alert({ message, type, onClose }) {
|
|
5
|
+
if (!message) return null;
|
|
6
|
+
return (
|
|
7
|
+
<div className={`px-4 py-3 rounded-lg mb-4 text-sm flex justify-between items-center ${type === 'success' ? 'bg-green-50 border border-green-200 text-green-700' : 'bg-red-50 border border-red-200 text-red-700'}`}>
|
|
8
|
+
<span>{message}</span>
|
|
9
|
+
<button onClick={onClose} className="ml-4 font-bold text-lg leading-none">×</button>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function StockIn() {
|
|
15
|
+
const [stockIns, setStockIns] = useState([]);
|
|
16
|
+
const [spareParts, setSpareParts] = useState([]);
|
|
17
|
+
const [form, setForm] = useState({ SparePartID: '', StockInQuantity: '', StockInDate: '' });
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [alert, setAlert] = useState({ message: '', type: '' });
|
|
20
|
+
|
|
21
|
+
const fetchData = async () => {
|
|
22
|
+
try {
|
|
23
|
+
const [siRes, spRes] = await Promise.all([
|
|
24
|
+
axios.get('/api/stock-in'),
|
|
25
|
+
axios.get('/api/spare-parts'),
|
|
26
|
+
]);
|
|
27
|
+
setStockIns(siRes.data);
|
|
28
|
+
setSpareParts(spRes.data);
|
|
29
|
+
} catch (_) {}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
useEffect(() => { fetchData(); }, []);
|
|
33
|
+
|
|
34
|
+
const showAlert = (message, type) => {
|
|
35
|
+
setAlert({ message, type });
|
|
36
|
+
setTimeout(() => setAlert({ message: '', type: '' }), 4000);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleSubmit = async (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
setLoading(true);
|
|
42
|
+
try {
|
|
43
|
+
await axios.post('/api/stock-in', form);
|
|
44
|
+
showAlert('Stock in recorded successfully!', 'success');
|
|
45
|
+
setForm({ SparePartID: '', StockInQuantity: '', StockInDate: '' });
|
|
46
|
+
fetchData();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
showAlert(err.response?.data?.message || 'Failed to record stock in.', 'error');
|
|
49
|
+
} finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const totalIn = stockIns.reduce((sum, si) => sum + si.StockInQuantity, 0);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
58
|
+
<div className="mb-6">
|
|
59
|
+
<h2 className="text-2xl font-bold text-gray-800">Stock In</h2>
|
|
60
|
+
<p className="text-gray-500 text-sm mt-1">Record spare parts received into stock</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Summary */}
|
|
64
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
|
65
|
+
<div className="bg-blue-800 text-white rounded-xl p-5 shadow">
|
|
66
|
+
<p className="text-blue-200 text-sm">Total Records</p>
|
|
67
|
+
<p className="text-3xl font-bold mt-1">{stockIns.length}</p>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="bg-teal-600 text-white rounded-xl p-5 shadow">
|
|
70
|
+
<p className="text-teal-100 text-sm">Total Units Received</p>
|
|
71
|
+
<p className="text-3xl font-bold mt-1">{totalIn.toLocaleString()}</p>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Form */}
|
|
76
|
+
<div className="bg-white rounded-xl shadow p-6 mb-8">
|
|
77
|
+
<h3 className="text-lg font-semibold text-gray-700 mb-4 border-b pb-2">Record Stock In</h3>
|
|
78
|
+
<Alert message={alert.message} type={alert.type} onClose={() => setAlert({ message: '', type: '' })} />
|
|
79
|
+
<form onSubmit={handleSubmit} className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
80
|
+
<div>
|
|
81
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Spare Part </label>
|
|
82
|
+
<select
|
|
83
|
+
value={form.SparePartID}
|
|
84
|
+
onChange={e => setForm({ ...form, SparePartID: e.target.value })}
|
|
85
|
+
required
|
|
86
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm bg-white"
|
|
87
|
+
>
|
|
88
|
+
<option value=""> Select Spare Part </option>
|
|
89
|
+
{spareParts.map(sp => (
|
|
90
|
+
<option key={sp.SparePartID} value={sp.SparePartID}>
|
|
91
|
+
{sp.Name} ({sp.Category})
|
|
92
|
+
</option>
|
|
93
|
+
))}
|
|
94
|
+
</select>
|
|
95
|
+
</div>
|
|
96
|
+
<div>
|
|
97
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Quantity </label>
|
|
98
|
+
<input
|
|
99
|
+
type="number"
|
|
100
|
+
min="1"
|
|
101
|
+
value={form.StockInQuantity}
|
|
102
|
+
onChange={e => setForm({ ...form, StockInQuantity: e.target.value })}
|
|
103
|
+
required
|
|
104
|
+
placeholder="Enter quantity"
|
|
105
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
<div>
|
|
109
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Date </label>
|
|
110
|
+
<input
|
|
111
|
+
type="date"
|
|
112
|
+
value={form.StockInDate}
|
|
113
|
+
onChange={e => setForm({ ...form, StockInDate: e.target.value })}
|
|
114
|
+
required
|
|
115
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="sm:col-span-3">
|
|
119
|
+
<button
|
|
120
|
+
type="submit"
|
|
121
|
+
disabled={loading}
|
|
122
|
+
className="bg-blue-800 text-white px-6 py-2 rounded-lg hover:bg-blue-900 font-medium transition-colors disabled:opacity-60 text-sm"
|
|
123
|
+
>
|
|
124
|
+
{loading ? 'Recording...' : 'Record Stock In'}
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</form>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Table */}
|
|
131
|
+
<div className="bg-white rounded-xl shadow overflow-hidden">
|
|
132
|
+
<div className="px-6 py-4 border-b">
|
|
133
|
+
<h3 className="text-lg font-semibold text-gray-700">Stock In History</h3>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="overflow-x-auto">
|
|
136
|
+
<table className="w-full text-sm">
|
|
137
|
+
<thead className="bg-blue-800 text-white">
|
|
138
|
+
<tr>
|
|
139
|
+
<th className="px-4 py-3 text-left">#</th>
|
|
140
|
+
<th className="px-4 py-3 text-left">Spare Part</th>
|
|
141
|
+
<th className="px-4 py-3 text-left">Category</th>
|
|
142
|
+
<th className="px-4 py-3 text-right">Quantity In</th>
|
|
143
|
+
<th className="px-4 py-3 text-left">Date</th>
|
|
144
|
+
</tr>
|
|
145
|
+
</thead>
|
|
146
|
+
<tbody className="divide-y divide-gray-100">
|
|
147
|
+
{stockIns.length === 0 ? (
|
|
148
|
+
<tr>
|
|
149
|
+
<td colSpan="5" className="text-center py-10 text-gray-400">No stock in records yet.</td>
|
|
150
|
+
</tr>
|
|
151
|
+
) : stockIns.map((si, i) => (
|
|
152
|
+
<tr key={si.StockInID} className="hover:bg-gray-50 transition-colors">
|
|
153
|
+
<td className="px-4 py-3 text-gray-500">{i + 1}</td>
|
|
154
|
+
<td className="px-4 py-3 font-semibold text-gray-800">{si.SparepartName}</td>
|
|
155
|
+
<td className="px-4 py-3">
|
|
156
|
+
<span className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full text-xs font-medium">{si.Category}</span>
|
|
157
|
+
</td>
|
|
158
|
+
<td className="px-4 py-3 text-right font-semibold text-teal-700">+{si.StockInQuantity}</td>
|
|
159
|
+
<td className="px-4 py-3 text-gray-600">{new Date(si.StockInDate).toLocaleDateString('en-GB')}</td>
|
|
160
|
+
</tr>
|
|
161
|
+
))}
|
|
162
|
+
</tbody>
|
|
163
|
+
</table>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default StockIn;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
function Alert({ message, type, onClose }) {
|
|
5
|
+
if (!message) return null;
|
|
6
|
+
return (
|
|
7
|
+
<div className={`px-4 py-3 rounded-lg mb-4 text-sm flex justify-between items-center ${type === 'success' ? 'bg-green-50 border border-green-200 text-green-700' : 'bg-red-50 border border-red-200 text-red-700'}`}>
|
|
8
|
+
<span>{message}</span>
|
|
9
|
+
<button onClick={onClose} className="ml-4 font-bold text-lg leading-none">×</button>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const emptyForm = { SparePartID: '', StockOutQuantity: '', StockOutUnitPrice: '', StockOutDate: '' };
|
|
15
|
+
|
|
16
|
+
function StockOut() {
|
|
17
|
+
const [stockOuts, setStockOuts] = useState([]);
|
|
18
|
+
const [spareParts, setSpareParts] = useState([]);
|
|
19
|
+
const [form, setForm] = useState(emptyForm);
|
|
20
|
+
const [editId, setEditId] = useState(null);
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
const [deleteId, setDeleteId] = useState(null);
|
|
23
|
+
const [alert, setAlert] = useState({ message: '', type: '' });
|
|
24
|
+
|
|
25
|
+
const fetchData = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const [soRes, spRes] = await Promise.all([
|
|
28
|
+
axios.get('/api/stock-out'),
|
|
29
|
+
axios.get('/api/spare-parts'),
|
|
30
|
+
]);
|
|
31
|
+
setStockOuts(soRes.data);
|
|
32
|
+
setSpareParts(spRes.data);
|
|
33
|
+
} catch (_) {}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
useEffect(() => { fetchData(); }, []);
|
|
37
|
+
|
|
38
|
+
const showAlert = (message, type) => {
|
|
39
|
+
setAlert({ message, type });
|
|
40
|
+
setTimeout(() => setAlert({ message: '', type: '' }), 4000);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleSubmit = async (e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
setLoading(true);
|
|
46
|
+
try {
|
|
47
|
+
if (editId) {
|
|
48
|
+
await axios.put(`/api/stock-out/${editId}`, form);
|
|
49
|
+
showAlert('Stock out updated successfully!', 'success');
|
|
50
|
+
setEditId(null);
|
|
51
|
+
} else {
|
|
52
|
+
await axios.post('/api/stock-out', form);
|
|
53
|
+
showAlert('Stock out recorded successfully!', 'success');
|
|
54
|
+
}
|
|
55
|
+
setForm(emptyForm);
|
|
56
|
+
fetchData();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
showAlert(err.response?.data?.message || 'Operation failed.', 'error');
|
|
59
|
+
} finally {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleEdit = (record) => {
|
|
65
|
+
setEditId(record.StockOutID);
|
|
66
|
+
setForm({
|
|
67
|
+
SparePartID: record.SparePartID,
|
|
68
|
+
StockOutQuantity: record.StockOutQuantity,
|
|
69
|
+
StockOutUnitPrice: record.StockOutUnitPrice,
|
|
70
|
+
StockOutDate: record.StockOutDate?.split('T')[0] || record.StockOutDate,
|
|
71
|
+
});
|
|
72
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleCancelEdit = () => {
|
|
76
|
+
setEditId(null);
|
|
77
|
+
setForm(emptyForm);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleDelete = async (id) => {
|
|
81
|
+
try {
|
|
82
|
+
await axios.delete(`/api/stock-out/${id}`);
|
|
83
|
+
showAlert('Stock out deleted successfully!', 'success');
|
|
84
|
+
setDeleteId(null);
|
|
85
|
+
fetchData();
|
|
86
|
+
} catch (err) {
|
|
87
|
+
showAlert(err.response?.data?.message || 'Delete failed.', 'error');
|
|
88
|
+
setDeleteId(null);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const computedTotal = form.StockOutQuantity && form.StockOutUnitPrice
|
|
93
|
+
? parseFloat(form.StockOutQuantity) * parseFloat(form.StockOutUnitPrice)
|
|
94
|
+
: 0;
|
|
95
|
+
|
|
96
|
+
const totalOut = stockOuts.reduce((sum, so) => sum + parseFloat(so.StockOutTotalPrice || 0), 0);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
100
|
+
<div className="mb-6">
|
|
101
|
+
<h2 className="text-2xl font-bold text-gray-800">Stock Out</h2>
|
|
102
|
+
<p className="text-gray-500 text-sm mt-1">Record and manage spare parts taken out of stock</p>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Summary */}
|
|
106
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
|
107
|
+
<div className="bg-blue-800 text-white rounded-xl p-5 shadow">
|
|
108
|
+
<p className="text-blue-200 text-sm">Total Records</p>
|
|
109
|
+
<p className="text-3xl font-bold mt-1">{stockOuts.length}</p>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="bg-red-600 text-white rounded-xl p-5 shadow">
|
|
112
|
+
<p className="text-red-100 text-sm">Total Value Out (RWF)</p>
|
|
113
|
+
<p className="text-3xl font-bold mt-1">{totalOut.toLocaleString()}</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Form */}
|
|
118
|
+
<div className="bg-white rounded-xl shadow p-6 mb-8">
|
|
119
|
+
<h3 className="text-lg font-semibold text-gray-700 mb-4 border-b pb-2">
|
|
120
|
+
{editId ? 'Edit Stock Out Record' : 'Record Stock Out'}
|
|
121
|
+
</h3>
|
|
122
|
+
<Alert message={alert.message} type={alert.type} onClose={() => setAlert({ message: '', type: '' })} />
|
|
123
|
+
<form onSubmit={handleSubmit} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
124
|
+
<div>
|
|
125
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Spare Part *</label>
|
|
126
|
+
<select
|
|
127
|
+
value={form.SparePartID}
|
|
128
|
+
onChange={e => setForm({ ...form, SparePartID: e.target.value })}
|
|
129
|
+
required
|
|
130
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm bg-white"
|
|
131
|
+
>
|
|
132
|
+
<option value=""> Select Spare Part </option>
|
|
133
|
+
{spareParts.map(sp => (
|
|
134
|
+
<option key={sp.SparePartID} value={sp.SparePartID}>
|
|
135
|
+
{sp.Name} (Stock: {sp.Quantity})
|
|
136
|
+
</option>
|
|
137
|
+
))}
|
|
138
|
+
</select>
|
|
139
|
+
</div>
|
|
140
|
+
<div>
|
|
141
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Quantity </label>
|
|
142
|
+
<input
|
|
143
|
+
type="number"
|
|
144
|
+
min="1"
|
|
145
|
+
value={form.StockOutQuantity}
|
|
146
|
+
onChange={e => setForm({ ...form, StockOutQuantity: e.target.value })}
|
|
147
|
+
required
|
|
148
|
+
placeholder="Enter quantity"
|
|
149
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
<div>
|
|
153
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Unit Price (RWF) </label>
|
|
154
|
+
<input
|
|
155
|
+
type="number"
|
|
156
|
+
min="0"
|
|
157
|
+
step="0.01"
|
|
158
|
+
value={form.StockOutUnitPrice}
|
|
159
|
+
onChange={e => setForm({ ...form, StockOutUnitPrice: e.target.value })}
|
|
160
|
+
required
|
|
161
|
+
placeholder="0.00"
|
|
162
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
<div>
|
|
166
|
+
<label className="block text-sm font-medium text-gray-600 mb-1">Date </label>
|
|
167
|
+
<input
|
|
168
|
+
type="date"
|
|
169
|
+
value={form.StockOutDate}
|
|
170
|
+
onChange={e => setForm({ ...form, StockOutDate: e.target.value })}
|
|
171
|
+
required
|
|
172
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
{computedTotal > 0 && (
|
|
176
|
+
<div className="sm:col-span-2 lg:col-span-4">
|
|
177
|
+
<p className="text-sm text-red-700 font-medium">
|
|
178
|
+
Total Price: RWF {computedTotal.toLocaleString()}
|
|
179
|
+
</p>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
<div className="sm:col-span-2 lg:col-span-4 flex gap-3">
|
|
183
|
+
<button
|
|
184
|
+
type="submit"
|
|
185
|
+
disabled={loading}
|
|
186
|
+
className="bg-blue-800 text-white px-6 py-2 rounded-lg hover:bg-blue-900 font-medium transition-colors disabled:opacity-60 text-sm"
|
|
187
|
+
>
|
|
188
|
+
{loading ? 'Saving...' : editId ? 'Update Record' : 'Record Stock Out'}
|
|
189
|
+
</button>
|
|
190
|
+
{editId && (
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={handleCancelEdit}
|
|
194
|
+
className="bg-gray-500 text-white px-6 py-2 rounded-lg hover:bg-gray-600 font-medium transition-colors text-sm"
|
|
195
|
+
>
|
|
196
|
+
Cancel
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</form>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Table */}
|
|
204
|
+
<div className="bg-white rounded-xl shadow overflow-hidden">
|
|
205
|
+
<div className="px-6 py-4 border-b">
|
|
206
|
+
<h3 className="text-lg font-semibold text-gray-700">Stock Out Records</h3>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="overflow-x-auto">
|
|
209
|
+
<table className="w-full text-sm">
|
|
210
|
+
<thead className="bg-blue-800 text-white">
|
|
211
|
+
<tr>
|
|
212
|
+
<th className="px-4 py-3 text-left">#</th>
|
|
213
|
+
<th className="px-4 py-3 text-left">Spare Part</th>
|
|
214
|
+
<th className="px-4 py-3 text-left">Category</th>
|
|
215
|
+
<th className="px-4 py-3 text-right">Qty Out</th>
|
|
216
|
+
<th className="px-4 py-3 text-right">Unit Price (RWF)</th>
|
|
217
|
+
<th className="px-4 py-3 text-right">Total Price (RWF)</th>
|
|
218
|
+
<th className="px-4 py-3 text-left">Date</th>
|
|
219
|
+
<th className="px-4 py-3 text-center">Actions</th>
|
|
220
|
+
</tr>
|
|
221
|
+
</thead>
|
|
222
|
+
<tbody className="divide-y divide-gray-100">
|
|
223
|
+
{stockOuts.length === 0 ? (
|
|
224
|
+
<tr>
|
|
225
|
+
<td colSpan="8" className="text-center py-10 text-gray-400">No stock out records yet.</td>
|
|
226
|
+
</tr>
|
|
227
|
+
) : stockOuts.map((so, i) => (
|
|
228
|
+
<tr key={so.StockOutID} className="hover:bg-gray-50 transition-colors">
|
|
229
|
+
<td className="px-4 py-3 text-gray-500">{i + 1}</td>
|
|
230
|
+
<td className="px-4 py-3 font-semibold text-gray-800">{so.SparepartName}</td>
|
|
231
|
+
<td className="px-4 py-3">
|
|
232
|
+
<span className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full text-xs font-medium">{so.Category}</span>
|
|
233
|
+
</td>
|
|
234
|
+
<td className="px-4 py-3 text-right font-semibold text-red-600">-{so.StockOutQuantity}</td>
|
|
235
|
+
<td className="px-4 py-3 text-right">{Number(so.StockOutUnitPrice).toLocaleString()}</td>
|
|
236
|
+
<td className="px-4 py-3 text-right font-semibold">{Number(so.StockOutTotalPrice).toLocaleString()}</td>
|
|
237
|
+
<td className="px-4 py-3 text-gray-600">{new Date(so.StockOutDate).toLocaleDateString('en-GB')}</td>
|
|
238
|
+
<td className="px-4 py-3 text-center">
|
|
239
|
+
<div className="flex justify-center gap-2">
|
|
240
|
+
<button
|
|
241
|
+
onClick={() => handleEdit(so)}
|
|
242
|
+
className="bg-yellow-500 text-white px-3 py-1 rounded text-xs font-medium hover:bg-yellow-600 transition-colors"
|
|
243
|
+
>
|
|
244
|
+
Edit
|
|
245
|
+
</button>
|
|
246
|
+
<button
|
|
247
|
+
onClick={() => setDeleteId(so.StockOutID)}
|
|
248
|
+
className="bg-red-500 text-white px-3 py-1 rounded text-xs font-medium hover:bg-red-600 transition-colors"
|
|
249
|
+
>
|
|
250
|
+
Delete
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
</td>
|
|
254
|
+
</tr>
|
|
255
|
+
))}
|
|
256
|
+
</tbody>
|
|
257
|
+
</table>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Delete Confirmation Modal */}
|
|
262
|
+
{deleteId && (
|
|
263
|
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 px-4">
|
|
264
|
+
<div className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-sm">
|
|
265
|
+
<h4 className="text-lg font-semibold text-gray-800 mb-2">Confirm Delete</h4>
|
|
266
|
+
<p className="text-gray-600 text-sm mb-6">Are you sure you want to delete this stock out record? The quantity will be restored to the spare part.</p>
|
|
267
|
+
<div className="flex gap-3 justify-end">
|
|
268
|
+
<button
|
|
269
|
+
onClick={() => setDeleteId(null)}
|
|
270
|
+
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 text-sm hover:bg-gray-50"
|
|
271
|
+
>
|
|
272
|
+
Cancel
|
|
273
|
+
</button>
|
|
274
|
+
<button
|
|
275
|
+
onClick={() => handleDelete(deleteId)}
|
|
276
|
+
className="px-4 py-2 rounded-lg bg-red-600 text-white text-sm font-medium hover:bg-red-700"
|
|
277
|
+
>
|
|
278
|
+
Delete
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export default StockOut;
|