create-steve-rogers 1.0.0 → 1.0.2
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/apps/SFMS/.env +9 -0
- package/apps/SFMS/README.md +0 -0
- package/apps/SFMS/backend/.env +9 -0
- package/apps/SFMS/backend/.env.example +9 -0
- package/apps/SFMS/backend/package-lock.json +1580 -0
- package/apps/SFMS/backend/package.json +23 -0
- package/apps/SFMS/backend/src/config/database.js +7 -0
- package/apps/SFMS/backend/src/config/env.js +35 -0
- package/apps/SFMS/backend/src/middleware/authMiddleware.js +32 -0
- package/apps/SFMS/backend/src/models/Payment.js +12 -0
- package/apps/SFMS/backend/src/models/Student.js +12 -0
- package/apps/SFMS/backend/src/models/User.js +13 -0
- package/apps/SFMS/backend/src/routes/authRoutes.js +93 -0
- package/apps/SFMS/backend/src/routes/paymentRoutes.js +117 -0
- package/apps/SFMS/backend/src/routes/reportRoutes.js +59 -0
- package/apps/SFMS/backend/src/routes/studentRoutes.js +79 -0
- package/apps/SFMS/backend/src/server.js +34 -0
- package/apps/SFMS/frontend/.env.example +8 -0
- package/apps/SFMS/frontend/dist/assets/index-B08X8imN.css +1 -0
- package/apps/SFMS/frontend/dist/assets/index-DVO0_wcb.js +67 -0
- package/apps/SFMS/frontend/dist/favicon.svg +4 -0
- package/apps/SFMS/frontend/dist/index.html +20 -0
- package/apps/SFMS/frontend/index.html +19 -0
- package/apps/SFMS/frontend/package-lock.json +2667 -0
- package/apps/SFMS/frontend/package.json +23 -0
- package/apps/SFMS/frontend/postcss.config.js +6 -0
- package/apps/SFMS/frontend/public/favicon.svg +4 -0
- package/apps/SFMS/frontend/src/App.jsx +41 -0
- package/apps/SFMS/frontend/src/api/apiClient.js +41 -0
- package/apps/SFMS/frontend/src/components/AppLayout.jsx +60 -0
- package/apps/SFMS/frontend/src/context/AuthContext.jsx +79 -0
- package/apps/SFMS/frontend/src/index.css +229 -0
- package/apps/SFMS/frontend/src/main.jsx +16 -0
- package/apps/SFMS/frontend/src/pages/DashboardPage.jsx +82 -0
- package/apps/SFMS/frontend/src/pages/LoginPage.jsx +142 -0
- package/apps/SFMS/frontend/src/pages/PaymentsPage.jsx +269 -0
- package/apps/SFMS/frontend/src/pages/ReportsPage.jsx +114 -0
- package/apps/SFMS/frontend/src/pages/StudentsPage.jsx +257 -0
- package/apps/SFMS/frontend/tailwind.config.js +21 -0
- package/apps/SFMS/frontend/vite.config.js +35 -0
- package/apps/SIMS/.env +4 -0
- package/apps/SIMS/README.md +138 -0
- package/apps/SIMS/backend/.env +4 -0
- package/apps/SIMS/backend/.env.example +4 -0
- package/apps/SIMS/backend/package-lock.json +1600 -0
- package/apps/SIMS/backend/package.json +22 -0
- package/apps/SIMS/backend/src/config/db.js +9 -0
- package/apps/SIMS/backend/src/controllers/authController.js +93 -0
- package/apps/SIMS/backend/src/controllers/simsReportController.js +94 -0
- package/apps/SIMS/backend/src/controllers/sparePartController.js +41 -0
- package/apps/SIMS/backend/src/controllers/stockInController.js +45 -0
- package/apps/SIMS/backend/src/controllers/stockOutController.js +123 -0
- package/apps/SIMS/backend/src/middleware/auth.js +8 -0
- package/apps/SIMS/backend/src/models/SparePart.js +17 -0
- package/apps/SIMS/backend/src/models/StockIn.js +16 -0
- package/apps/SIMS/backend/src/models/StockOut.js +18 -0
- package/apps/SIMS/backend/src/models/User.js +11 -0
- package/apps/SIMS/backend/src/routes/authRoutes.js +12 -0
- package/apps/SIMS/backend/src/routes/simsReportRoutes.js +8 -0
- package/apps/SIMS/backend/src/routes/sparePartRoutes.js +8 -0
- package/apps/SIMS/backend/src/routes/stockInRoutes.js +8 -0
- package/apps/SIMS/backend/src/routes/stockOutRoutes.js +10 -0
- package/apps/SIMS/backend/src/server.js +62 -0
- package/apps/SIMS/backend/src/utils/passwordPolicy.js +10 -0
- package/apps/SIMS/backend/src/utils/sparePartHelpers.js +5 -0
- package/apps/SIMS/frontend/dist/assets/index-3hv-vGL2.css +2 -0
- package/apps/SIMS/frontend/dist/assets/index-T8XT7M6y.js +19 -0
- package/apps/SIMS/frontend/dist/index.html +14 -0
- package/apps/SIMS/frontend/index.html +13 -0
- package/apps/SIMS/frontend/package-lock.json +3053 -0
- package/apps/SIMS/frontend/package.json +31 -0
- package/apps/SIMS/frontend/src/App.jsx +112 -0
- package/apps/SIMS/frontend/src/api/authApi.js +7 -0
- package/apps/SIMS/frontend/src/api/client.js +8 -0
- package/apps/SIMS/frontend/src/api/simsReportApi.js +5 -0
- package/apps/SIMS/frontend/src/api/sparePartsApi.js +4 -0
- package/apps/SIMS/frontend/src/api/stockInApi.js +4 -0
- package/apps/SIMS/frontend/src/api/stockOutApi.js +6 -0
- package/apps/SIMS/frontend/src/api/usersApi.js +3 -0
- package/apps/SIMS/frontend/src/components/AppLayout.jsx +60 -0
- package/apps/SIMS/frontend/src/index.css +737 -0
- package/apps/SIMS/frontend/src/main.jsx +13 -0
- package/apps/SIMS/frontend/src/pages/DashboardPage.jsx +179 -0
- package/apps/SIMS/frontend/src/pages/LoginPage.jsx +75 -0
- package/apps/SIMS/frontend/src/pages/RegisterPage.jsx +78 -0
- package/apps/SIMS/frontend/src/pages/ReportsPage.jsx +108 -0
- package/apps/SIMS/frontend/src/pages/ResetPasswordPage.jsx +75 -0
- package/apps/SIMS/frontend/src/pages/SparePartPage.jsx +128 -0
- package/apps/SIMS/frontend/src/pages/StockInPage.jsx +100 -0
- package/apps/SIMS/frontend/src/pages/StockOutPage.jsx +206 -0
- package/apps/SIMS/frontend/src/utils/passwordPolicy.js +8 -0
- package/apps/SIMS/frontend/vite.config.js +8 -0
- package/apps/config.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { apiRequest } from '../api/apiClient.js';
|
|
3
|
+
|
|
4
|
+
const inputClass = 'input-field';
|
|
5
|
+
|
|
6
|
+
export function StudentsPage() {
|
|
7
|
+
const [list, setList] = useState([]);
|
|
8
|
+
const [submitting, setSubmitting] = useState(false);
|
|
9
|
+
const [error, setError] = useState('');
|
|
10
|
+
const [formTarget, setFormTarget] = useState(null);
|
|
11
|
+
|
|
12
|
+
const [fullName, setFullName] = useState('');
|
|
13
|
+
const [className, setClassName] = useState('');
|
|
14
|
+
const [parentPhone, setParentPhone] = useState('');
|
|
15
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
16
|
+
|
|
17
|
+
async function load() {
|
|
18
|
+
setError('');
|
|
19
|
+
try {
|
|
20
|
+
const data = await apiRequest('/api/students');
|
|
21
|
+
setList(data);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
setError(e.message);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
load();
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
function validate() {
|
|
32
|
+
const err = {};
|
|
33
|
+
if (!fullName.trim()) err.fullName = 'Required';
|
|
34
|
+
if (!className.trim()) err.className = 'Required';
|
|
35
|
+
if (!parentPhone.trim()) {
|
|
36
|
+
err.parentPhone = 'Required';
|
|
37
|
+
} else if (!/^[0-9]{1,10}$/.test(parentPhone)) {
|
|
38
|
+
err.parentPhone = 'Phone must be digits and no more than 10 characters';
|
|
39
|
+
}
|
|
40
|
+
setFieldErrors(err);
|
|
41
|
+
return Object.keys(err).length === 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function openNew() {
|
|
45
|
+
setFormTarget('new');
|
|
46
|
+
setFullName('');
|
|
47
|
+
setClassName('');
|
|
48
|
+
setParentPhone('');
|
|
49
|
+
setFieldErrors({});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function openEdit(row) {
|
|
53
|
+
setFormTarget(row);
|
|
54
|
+
setFullName(row.full_name || '');
|
|
55
|
+
setClassName(row.class || '');
|
|
56
|
+
setParentPhone(row.parent_phone || '');
|
|
57
|
+
setFieldErrors({});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function closeForm() {
|
|
61
|
+
setFormTarget(null);
|
|
62
|
+
setFieldErrors({});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function save(e) {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
if (!validate()) return;
|
|
68
|
+
setSubmitting(true);
|
|
69
|
+
setError('');
|
|
70
|
+
try {
|
|
71
|
+
const payload = {
|
|
72
|
+
full_name: fullName.trim(),
|
|
73
|
+
class: className.trim(),
|
|
74
|
+
parent_phone: parentPhone.trim(),
|
|
75
|
+
};
|
|
76
|
+
if (formTarget !== 'new' && formTarget?.id) {
|
|
77
|
+
await apiRequest(`/api/students/${formTarget.id}`, {
|
|
78
|
+
method: 'PUT',
|
|
79
|
+
body: JSON.stringify(payload),
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
await apiRequest('/api/students', { method: 'POST', body: JSON.stringify(payload) });
|
|
83
|
+
}
|
|
84
|
+
closeForm();
|
|
85
|
+
await load();
|
|
86
|
+
} catch (err) {
|
|
87
|
+
setError(err.message);
|
|
88
|
+
} finally {
|
|
89
|
+
setSubmitting(false);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function remove(id) {
|
|
94
|
+
if (!window.confirm('Delete this student?')) return;
|
|
95
|
+
setError('');
|
|
96
|
+
try {
|
|
97
|
+
await apiRequest(`/api/students/${id}`, { method: 'DELETE' });
|
|
98
|
+
await load();
|
|
99
|
+
} catch (e) {
|
|
100
|
+
setError(e.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div>
|
|
106
|
+
<div className="app-header">
|
|
107
|
+
<p className="text-sm uppercase tracking-[0.3em] text-slate-500">Students</p>
|
|
108
|
+
<h1 className="page-title">Student roster</h1>
|
|
109
|
+
<p className="page-subtitle">Add, edit, and keep track of each student record from a sleek panel.</p>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between mb-6">
|
|
113
|
+
<div className="space-y-1">
|
|
114
|
+
<p className="text-base font-semibold text-slate-900">Manage student details</p>
|
|
115
|
+
<p className="text-sm text-slate-500">Everything is organized in one clean table.</p>
|
|
116
|
+
</div>
|
|
117
|
+
<button type="button" onClick={openNew} className="btn-primary w-full sm:w-auto">
|
|
118
|
+
+ New student
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{error && (
|
|
123
|
+
<div className="mb-6 rounded-[28px] border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700">
|
|
124
|
+
{error}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{formTarget === 'new' && (
|
|
129
|
+
<form onSubmit={save} className="form-card mb-8 max-w-2xl space-y-5">
|
|
130
|
+
<div className="flex items-center justify-between gap-4">
|
|
131
|
+
<h2 className="text-xl font-semibold text-slate-900">New student</h2>
|
|
132
|
+
<button type="button" onClick={closeForm} className="btn-secondary">
|
|
133
|
+
Close
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
138
|
+
<div className="input-group">
|
|
139
|
+
<label className="text-sm font-medium text-slate-700">Full name</label>
|
|
140
|
+
<input className={inputClass} value={fullName} onChange={(e) => setFullName(e.target.value)} />
|
|
141
|
+
{fieldErrors.fullName && <p className="text-xs text-red-600">{fieldErrors.fullName}</p>}
|
|
142
|
+
</div>
|
|
143
|
+
<div className="input-group">
|
|
144
|
+
<label className="text-sm font-medium text-slate-700">Class</label>
|
|
145
|
+
<input className={inputClass} value={className} onChange={(e) => setClassName(e.target.value)} />
|
|
146
|
+
{fieldErrors.className && <p className="text-xs text-red-600">{fieldErrors.className}</p>}
|
|
147
|
+
</div>
|
|
148
|
+
<div className="input-group">
|
|
149
|
+
<label className="text-sm font-medium text-slate-700">Parent phone</label>
|
|
150
|
+
<input
|
|
151
|
+
type="tel"
|
|
152
|
+
inputMode="numeric"
|
|
153
|
+
maxLength="10"
|
|
154
|
+
className={inputClass}
|
|
155
|
+
value={parentPhone}
|
|
156
|
+
onChange={(e) => setParentPhone(e.target.value.replace(/\D/g, '').slice(0, 10))}
|
|
157
|
+
placeholder="Digits only"
|
|
158
|
+
/>
|
|
159
|
+
{fieldErrors.parentPhone && <p className="text-xs text-red-600">{fieldErrors.parentPhone}</p>}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className="form-actions">
|
|
164
|
+
<button type="submit" disabled={submitting} className="btn-primary">
|
|
165
|
+
{submitting ? 'Saving…' : 'Save'}
|
|
166
|
+
</button>
|
|
167
|
+
<button type="button" onClick={closeForm} className="btn-secondary">
|
|
168
|
+
Cancel
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</form>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
<div className="table-shell">
|
|
175
|
+
<table className="min-w-full text-sm">
|
|
176
|
+
<thead className="table-head text-left">
|
|
177
|
+
<tr>
|
|
178
|
+
<th className="table-cell font-medium">Name</th>
|
|
179
|
+
<th className="table-cell font-medium">Class</th>
|
|
180
|
+
<th className="table-cell font-medium">Parent phone</th>
|
|
181
|
+
<th className="table-cell font-medium">Actions</th>
|
|
182
|
+
</tr>
|
|
183
|
+
</thead>
|
|
184
|
+
<tbody>
|
|
185
|
+
{list.map((u) => (
|
|
186
|
+
<tr key={u.id} className="table-row">
|
|
187
|
+
<td className="table-cell font-semibold text-slate-900">{u.full_name}</td>
|
|
188
|
+
<td className="table-cell">{u.class}</td>
|
|
189
|
+
<td className="table-cell">{u.parent_phone}</td>
|
|
190
|
+
<td className="table-cell space-x-2 whitespace-nowrap">
|
|
191
|
+
<button type="button" className="btn-secondary" onClick={() => openEdit(u)}>
|
|
192
|
+
Edit
|
|
193
|
+
</button>
|
|
194
|
+
<button type="button" className="btn-destructive" onClick={() => remove(u.id)}>
|
|
195
|
+
Delete
|
|
196
|
+
</button>
|
|
197
|
+
</td>
|
|
198
|
+
</tr>
|
|
199
|
+
))}
|
|
200
|
+
</tbody>
|
|
201
|
+
</table>
|
|
202
|
+
{list.length === 0 && <p className="placeholder-card">No students yet.</p>}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{formTarget && formTarget !== 'new' && (
|
|
206
|
+
<div
|
|
207
|
+
className="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50"
|
|
208
|
+
onClick={(e) => e.target === e.currentTarget && closeForm()}
|
|
209
|
+
role="presentation"
|
|
210
|
+
>
|
|
211
|
+
<div
|
|
212
|
+
className="form-card max-w-md w-full"
|
|
213
|
+
onClick={(e) => e.stopPropagation()}
|
|
214
|
+
role="dialog"
|
|
215
|
+
aria-modal="true"
|
|
216
|
+
>
|
|
217
|
+
<div className="flex items-center justify-between gap-4 mb-4">
|
|
218
|
+
<h2 className="text-xl font-semibold text-slate-900">Edit student</h2>
|
|
219
|
+
<button type="button" onClick={closeForm} className="btn-secondary">
|
|
220
|
+
Close
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
<form onSubmit={save} className="space-y-4">
|
|
224
|
+
<div className="input-group">
|
|
225
|
+
<label className="text-sm font-medium text-slate-700">Name</label>
|
|
226
|
+
<input className={inputClass} value={fullName} onChange={(e) => setFullName(e.target.value)} />
|
|
227
|
+
{fieldErrors.fullName && <p className="text-xs text-red-600">{fieldErrors.fullName}</p>}
|
|
228
|
+
</div>
|
|
229
|
+
<div className="input-group">
|
|
230
|
+
<label className="text-sm font-medium text-slate-700">Class</label>
|
|
231
|
+
<input className={inputClass} value={className} onChange={(e) => setClassName(e.target.value)} />
|
|
232
|
+
</div>
|
|
233
|
+
<div className="input-group">
|
|
234
|
+
<label className="text-sm font-medium text-slate-700">Parent phone</label>
|
|
235
|
+
<input
|
|
236
|
+
type="tel"
|
|
237
|
+
inputMode="numeric"
|
|
238
|
+
maxLength="10"
|
|
239
|
+
className={inputClass}
|
|
240
|
+
value={parentPhone}
|
|
241
|
+
onChange={(e) => setParentPhone(e.target.value.replace(/\D/g, '').slice(0, 10))}
|
|
242
|
+
placeholder="Digits only"
|
|
243
|
+
/>
|
|
244
|
+
{fieldErrors.parentPhone && <p className="text-xs text-red-600">{fieldErrors.parentPhone}</p>}
|
|
245
|
+
</div>
|
|
246
|
+
<div className="form-actions">
|
|
247
|
+
<button type="submit" disabled={submitting} className="btn-primary w-full">
|
|
248
|
+
Save
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
</form>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: ['./index.html', './src/**/*.{js,jsx}'],
|
|
4
|
+
theme: {
|
|
5
|
+
extend: {
|
|
6
|
+
fontFamily: {
|
|
7
|
+
sans: ['DM Sans', 'system-ui', 'sans-serif'],
|
|
8
|
+
display: ['Outfit', 'system-ui', 'sans-serif'],
|
|
9
|
+
},
|
|
10
|
+
colors: {
|
|
11
|
+
sfms: {
|
|
12
|
+
ink: '#0f172a',
|
|
13
|
+
mist: '#f1f5f9',
|
|
14
|
+
accent: '#0d9488',
|
|
15
|
+
accentDark: '#0f766e',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
plugins: [],
|
|
21
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineConfig, loadEnv } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_DEV_PORT = 5173;
|
|
5
|
+
const DEFAULT_API_PROXY = 'http://localhost:5000';
|
|
6
|
+
|
|
7
|
+
export default defineConfig(({ mode }) => {
|
|
8
|
+
const rootEnv = loadEnv(mode, process.cwd(), '');
|
|
9
|
+
|
|
10
|
+
const port =
|
|
11
|
+
Number(
|
|
12
|
+
rootEnv.VITE_DEV_SERVER_PORT ||
|
|
13
|
+
rootEnv.SFMS_FRONTEND_PORT ||
|
|
14
|
+
`${DEFAULT_DEV_PORT}`
|
|
15
|
+
) || DEFAULT_DEV_PORT;
|
|
16
|
+
|
|
17
|
+
const proxyTarget =
|
|
18
|
+
rootEnv.VITE_API_PROXY_TARGET ||
|
|
19
|
+
rootEnv.VITE_BACKEND_URL ||
|
|
20
|
+
DEFAULT_API_PROXY;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
plugins: [react()],
|
|
24
|
+
server: {
|
|
25
|
+
port,
|
|
26
|
+
strictPort: true,
|
|
27
|
+
proxy: {
|
|
28
|
+
'/api': {
|
|
29
|
+
target: proxyTarget,
|
|
30
|
+
changeOrigin: true,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
package/apps/SIMS/.env
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# SIMS — Stock Inventory Management System
|
|
2
|
+
|
|
3
|
+
[How this maps to the generic EPMS marking rubric (and what differs) → `EXAM_CHECKLIST_MARKING.md`](EXAM_CHECKLIST_MARKING.md)
|
|
4
|
+
|
|
5
|
+
## ERD (Entity Relationship Diagram) — for your paper
|
|
6
|
+
|
|
7
|
+
Draw this on **plain paper** with standard symbols: **entities (rectangles)**, **relationships (diamonds or simple lines)**, **primary keys (PK)**, **foreign keys (FK)**, and **cardinalities (1, N)**.
|
|
8
|
+
|
|
9
|
+
### 1) Entity: `User` (login / session)
|
|
10
|
+
|
|
11
|
+
| Field | Type | Key |
|
|
12
|
+
|----------------|---------|-----|
|
|
13
|
+
| `userId` | (PK) | PK |
|
|
14
|
+
| `username` | String | |
|
|
15
|
+
| `passwordHash` | String | |
|
|
16
|
+
|
|
17
|
+
- **No foreign keys.** Use this only for authentication (session). You can draw it **separate** from the stock model (no line to Spare part / stock tables), unless your assessor wants a line—usually **not required**.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### 2) Entity: `Spare_Part` (catalog + current stock)
|
|
22
|
+
|
|
23
|
+
| Field | Type | Key |
|
|
24
|
+
|-------------|---------|-----|
|
|
25
|
+
| `sparePartId` / `_id` | (id) | **PK** |
|
|
26
|
+
| `name` | String | |
|
|
27
|
+
| `category` | String | |
|
|
28
|
+
| `quantity` | Number | |
|
|
29
|
+
| `unitPrice` | Number | |
|
|
30
|
+
| `totalPrice`| Number | |
|
|
31
|
+
|
|
32
|
+
- One row = one part type (e.g. oil filter) with **current** quantity in stock and price fields as in the exam.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### 3) Entity: `Stock_In` (parts received into store)
|
|
37
|
+
|
|
38
|
+
| Field | Type | Key |
|
|
39
|
+
|------------------|--------|-----|
|
|
40
|
+
| `stockInId` / `_id` | (id) | **PK** |
|
|
41
|
+
| `sparePartId` | ref | **FK → `Spare_Part.sparePartId`** |
|
|
42
|
+
| `stockInQuantity`| Number | |
|
|
43
|
+
| `stockInDate` | Date | |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### 4) Entity: `Stock_Out` (parts taken from store)
|
|
48
|
+
|
|
49
|
+
| Field | Type | Key |
|
|
50
|
+
|------------------------|--------|-----|
|
|
51
|
+
| `stockOutId` / `_id` | (id) | **PK** |
|
|
52
|
+
| `sparePartId` | ref | **FK → `Spare_Part.sparePartId`** |
|
|
53
|
+
| `stockOutQuantity` | Number | |
|
|
54
|
+
| `stockOutUnitPrice` | Number | |
|
|
55
|
+
| `stockOutTotalPrice` | Number | |
|
|
56
|
+
| `stockOutDate` | Date | |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Relationships and cardinality
|
|
61
|
+
|
|
62
|
+
| Relationship | From | To | Cardinality (typical) |
|
|
63
|
+
|--------------|------|-----|------------------------|
|
|
64
|
+
| **Supplies to stock in** | `Spare_Part` | `Stock_In` | **1 : N** (one part type, many stock-in rows over time) |
|
|
65
|
+
| **Supplies to stock out** | `Spare_Part` | `Stock_Out` | **1 : N** (one part type, many stock-out rows over time) |
|
|
66
|
+
| | `Stock_In` | `Spare_Part` | each row **N : 1** (many stock-in rows point to one part) |
|
|
67
|
+
| | `Stock_Out` | `Spare_Part` | each row **N : 1** (many stock-out rows point to one part) |
|
|
68
|
+
|
|
69
|
+
- **There is no direct relationship** between `Stock_In` and `Stock_Out` (no FK between them); they both only link to **`Spare_Part`**.
|
|
70
|
+
|
|
71
|
+
### Small diagram (ASCII — for copy on paper)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
┌──────────┐ 1 N ┌───────────┐
|
|
75
|
+
│ User │ (no FK to stock) │ Spare │
|
|
76
|
+
│ PK: id │ │ _Part │
|
|
77
|
+
└──────────┘ │ PK: id │
|
|
78
|
+
└─────┬─────┘
|
|
79
|
+
┌──────────────────┼──────────────────┐
|
|
80
|
+
1 │ N 1 │ N
|
|
81
|
+
┌─────────▼──────┐ ┌───────▼──────────┐
|
|
82
|
+
│ Stock_In │ │ Stock_Out │
|
|
83
|
+
│ PK, FK→Spare │ │ PK, FK→Spare │
|
|
84
|
+
└────────────────┘ └────────────────┘
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Summary sentence:** *One spare part can have many stock-in records and many stock-out records; each stock-in and stock-out row references exactly one spare part.*
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
Practical exam project : full-stack app with **MongoDB**, **Express**, **React**, **Tailwind**, **session login**, **Axios**.
|
|
92
|
+
|
|
93
|
+
## Exam rules implemented
|
|
94
|
+
|
|
95
|
+
| Requirement | How |
|
|
96
|
+
|-------------|-----|
|
|
97
|
+
| ERD: Spare_Part, Stock_In, Stock_Out (PK/FK) | On paper; backend uses `SparePart` with refs from `StockIn` / `StockOut` |
|
|
98
|
+
| DB name **SIMS** | `MONGO_URI` → database `sims` |
|
|
99
|
+
| Insert on Spare Part, Stock In, Stock Out | `POST` only (no update/delete) on spare parts and stock in |
|
|
100
|
+
| Update / delete / list only on **Stock Out** | `GET`, `PUT`, `DELETE` on `/api/stock-out/:id` |
|
|
101
|
+
| Menu: Spare Part, Stock In, Stock Out, **Reports**, **Logout** | `AppLayout.jsx` |
|
|
102
|
+
| Session login, **strong encrypted password** | Bcrypt; register requires 8+ chars, upper, lower, number |
|
|
103
|
+
| Tailwind, responsive, Axios | Vite + `@tailwindcss/vite` |
|
|
104
|
+
| Reports: **daily stock status** + **daily stock out** | `GET /api/reports/daily-stock-status?date=`, `GET /api/reports/daily-stockout?date=` |
|
|
105
|
+
|
|
106
|
+
## Ports (avoids clash with EPMS on same machine)
|
|
107
|
+
|
|
108
|
+
- Backend: **5001** (configurable in `.env`)
|
|
109
|
+
- Frontend: **5174** (Vite)
|
|
110
|
+
- `FRONTEND_URL` in backend `.env` should match the Vite URL
|
|
111
|
+
|
|
112
|
+
## Run
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
cd sims/backend
|
|
116
|
+
cp .env.example .env
|
|
117
|
+
npm install
|
|
118
|
+
npm run dev
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
cd sims/frontend
|
|
123
|
+
npm install
|
|
124
|
+
npm run dev
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- API: [http://localhost:5001/api/health](http://localhost:5001/api/health)
|
|
128
|
+
- App: [http://localhost:5174](http://localhost:5174)
|
|
129
|
+
|
|
130
|
+
## Project layout
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
sims/
|
|
134
|
+
backend/ Node + Express + Mongoose
|
|
135
|
+
frontend/ React + Vite + Tailwind
|
|
136
|
+
README.md
|
|
137
|
+
```
|
|
138
|
+
|