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.
Files changed (94) hide show
  1. package/apps/SFMS/.env +9 -0
  2. package/apps/SFMS/README.md +0 -0
  3. package/apps/SFMS/backend/.env +9 -0
  4. package/apps/SFMS/backend/.env.example +9 -0
  5. package/apps/SFMS/backend/package-lock.json +1580 -0
  6. package/apps/SFMS/backend/package.json +23 -0
  7. package/apps/SFMS/backend/src/config/database.js +7 -0
  8. package/apps/SFMS/backend/src/config/env.js +35 -0
  9. package/apps/SFMS/backend/src/middleware/authMiddleware.js +32 -0
  10. package/apps/SFMS/backend/src/models/Payment.js +12 -0
  11. package/apps/SFMS/backend/src/models/Student.js +12 -0
  12. package/apps/SFMS/backend/src/models/User.js +13 -0
  13. package/apps/SFMS/backend/src/routes/authRoutes.js +93 -0
  14. package/apps/SFMS/backend/src/routes/paymentRoutes.js +117 -0
  15. package/apps/SFMS/backend/src/routes/reportRoutes.js +59 -0
  16. package/apps/SFMS/backend/src/routes/studentRoutes.js +79 -0
  17. package/apps/SFMS/backend/src/server.js +34 -0
  18. package/apps/SFMS/frontend/.env.example +8 -0
  19. package/apps/SFMS/frontend/dist/assets/index-B08X8imN.css +1 -0
  20. package/apps/SFMS/frontend/dist/assets/index-DVO0_wcb.js +67 -0
  21. package/apps/SFMS/frontend/dist/favicon.svg +4 -0
  22. package/apps/SFMS/frontend/dist/index.html +20 -0
  23. package/apps/SFMS/frontend/index.html +19 -0
  24. package/apps/SFMS/frontend/package-lock.json +2667 -0
  25. package/apps/SFMS/frontend/package.json +23 -0
  26. package/apps/SFMS/frontend/postcss.config.js +6 -0
  27. package/apps/SFMS/frontend/public/favicon.svg +4 -0
  28. package/apps/SFMS/frontend/src/App.jsx +41 -0
  29. package/apps/SFMS/frontend/src/api/apiClient.js +41 -0
  30. package/apps/SFMS/frontend/src/components/AppLayout.jsx +60 -0
  31. package/apps/SFMS/frontend/src/context/AuthContext.jsx +79 -0
  32. package/apps/SFMS/frontend/src/index.css +229 -0
  33. package/apps/SFMS/frontend/src/main.jsx +16 -0
  34. package/apps/SFMS/frontend/src/pages/DashboardPage.jsx +82 -0
  35. package/apps/SFMS/frontend/src/pages/LoginPage.jsx +142 -0
  36. package/apps/SFMS/frontend/src/pages/PaymentsPage.jsx +269 -0
  37. package/apps/SFMS/frontend/src/pages/ReportsPage.jsx +114 -0
  38. package/apps/SFMS/frontend/src/pages/StudentsPage.jsx +257 -0
  39. package/apps/SFMS/frontend/tailwind.config.js +21 -0
  40. package/apps/SFMS/frontend/vite.config.js +35 -0
  41. package/apps/SIMS/.env +4 -0
  42. package/apps/SIMS/README.md +138 -0
  43. package/apps/SIMS/backend/.env +4 -0
  44. package/apps/SIMS/backend/.env.example +4 -0
  45. package/apps/SIMS/backend/package-lock.json +1600 -0
  46. package/apps/SIMS/backend/package.json +22 -0
  47. package/apps/SIMS/backend/src/config/db.js +9 -0
  48. package/apps/SIMS/backend/src/controllers/authController.js +93 -0
  49. package/apps/SIMS/backend/src/controllers/simsReportController.js +94 -0
  50. package/apps/SIMS/backend/src/controllers/sparePartController.js +41 -0
  51. package/apps/SIMS/backend/src/controllers/stockInController.js +45 -0
  52. package/apps/SIMS/backend/src/controllers/stockOutController.js +123 -0
  53. package/apps/SIMS/backend/src/middleware/auth.js +8 -0
  54. package/apps/SIMS/backend/src/models/SparePart.js +17 -0
  55. package/apps/SIMS/backend/src/models/StockIn.js +16 -0
  56. package/apps/SIMS/backend/src/models/StockOut.js +18 -0
  57. package/apps/SIMS/backend/src/models/User.js +11 -0
  58. package/apps/SIMS/backend/src/routes/authRoutes.js +12 -0
  59. package/apps/SIMS/backend/src/routes/simsReportRoutes.js +8 -0
  60. package/apps/SIMS/backend/src/routes/sparePartRoutes.js +8 -0
  61. package/apps/SIMS/backend/src/routes/stockInRoutes.js +8 -0
  62. package/apps/SIMS/backend/src/routes/stockOutRoutes.js +10 -0
  63. package/apps/SIMS/backend/src/server.js +62 -0
  64. package/apps/SIMS/backend/src/utils/passwordPolicy.js +10 -0
  65. package/apps/SIMS/backend/src/utils/sparePartHelpers.js +5 -0
  66. package/apps/SIMS/frontend/dist/assets/index-3hv-vGL2.css +2 -0
  67. package/apps/SIMS/frontend/dist/assets/index-T8XT7M6y.js +19 -0
  68. package/apps/SIMS/frontend/dist/index.html +14 -0
  69. package/apps/SIMS/frontend/index.html +13 -0
  70. package/apps/SIMS/frontend/package-lock.json +3053 -0
  71. package/apps/SIMS/frontend/package.json +31 -0
  72. package/apps/SIMS/frontend/src/App.jsx +112 -0
  73. package/apps/SIMS/frontend/src/api/authApi.js +7 -0
  74. package/apps/SIMS/frontend/src/api/client.js +8 -0
  75. package/apps/SIMS/frontend/src/api/simsReportApi.js +5 -0
  76. package/apps/SIMS/frontend/src/api/sparePartsApi.js +4 -0
  77. package/apps/SIMS/frontend/src/api/stockInApi.js +4 -0
  78. package/apps/SIMS/frontend/src/api/stockOutApi.js +6 -0
  79. package/apps/SIMS/frontend/src/api/usersApi.js +3 -0
  80. package/apps/SIMS/frontend/src/components/AppLayout.jsx +60 -0
  81. package/apps/SIMS/frontend/src/index.css +737 -0
  82. package/apps/SIMS/frontend/src/main.jsx +13 -0
  83. package/apps/SIMS/frontend/src/pages/DashboardPage.jsx +179 -0
  84. package/apps/SIMS/frontend/src/pages/LoginPage.jsx +75 -0
  85. package/apps/SIMS/frontend/src/pages/RegisterPage.jsx +78 -0
  86. package/apps/SIMS/frontend/src/pages/ReportsPage.jsx +108 -0
  87. package/apps/SIMS/frontend/src/pages/ResetPasswordPage.jsx +75 -0
  88. package/apps/SIMS/frontend/src/pages/SparePartPage.jsx +128 -0
  89. package/apps/SIMS/frontend/src/pages/StockInPage.jsx +100 -0
  90. package/apps/SIMS/frontend/src/pages/StockOutPage.jsx +206 -0
  91. package/apps/SIMS/frontend/src/utils/passwordPolicy.js +8 -0
  92. package/apps/SIMS/frontend/vite.config.js +8 -0
  93. package/apps/config.js +13 -0
  94. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { getSpareParts } from "../api/sparePartsApi";
3
+ import { getStockIn } from "../api/stockInApi";
4
+ import { getStockOut } from "../api/stockOutApi";
5
+
6
+ const money = new Intl.NumberFormat("en-US", {
7
+ style: "currency",
8
+ currency: "USD",
9
+ maximumFractionDigits: 2,
10
+ });
11
+
12
+ const number = new Intl.NumberFormat("en-US");
13
+
14
+ function DashboardPage() {
15
+ const [parts, setParts] = useState([]);
16
+ const [stockInRows, setStockInRows] = useState([]);
17
+ const [stockOutRows, setStockOutRows] = useState([]);
18
+ const [message, setMessage] = useState("");
19
+
20
+ useEffect(() => {
21
+ Promise.all([getSpareParts(), getStockIn(), getStockOut()])
22
+ .then(([partsRes, inRes, outRes]) => {
23
+ setParts(partsRes.data);
24
+ setStockInRows(inRes.data);
25
+ setStockOutRows(outRes.data);
26
+ })
27
+ .catch(() => setMessage("Dashboard data failed to load"));
28
+ }, []);
29
+
30
+ const summary = useMemo(() => {
31
+ const totalQuantity = parts.reduce((sum, item) => sum + Number(item.quantity || 0), 0);
32
+ const totalValue = parts.reduce((sum, item) => sum + Number(item.totalPrice || 0), 0);
33
+ const totalIn = stockInRows.reduce((sum, item) => sum + Number(item.stockInQuantity || 0), 0);
34
+ const totalOut = stockOutRows.reduce((sum, item) => sum + Number(item.stockOutQuantity || 0), 0);
35
+
36
+ return {
37
+ totalParts: parts.length,
38
+ totalQuantity,
39
+ totalValue,
40
+ totalIn,
41
+ totalOut,
42
+ };
43
+ }, [parts, stockInRows, stockOutRows]);
44
+
45
+ const lowStock = useMemo(
46
+ () =>
47
+ [...parts]
48
+ .sort((a, b) => Number(a.quantity || 0) - Number(b.quantity || 0))
49
+ .slice(0, 5),
50
+ [parts],
51
+ );
52
+
53
+ const categoryRows = useMemo(() => {
54
+ const groups = new Map();
55
+ parts.forEach((part) => {
56
+ const category = part.category || "Uncategorised";
57
+ const current = groups.get(category) || { category, count: 0, quantity: 0, value: 0 };
58
+ current.count += 1;
59
+ current.quantity += Number(part.quantity || 0);
60
+ current.value += Number(part.totalPrice || 0);
61
+ groups.set(category, current);
62
+ });
63
+ return [...groups.values()].sort((a, b) => b.value - a.value).slice(0, 6);
64
+ }, [parts]);
65
+
66
+ const recentStockOut = useMemo(
67
+ () =>
68
+ [...stockOutRows]
69
+ .sort((a, b) => new Date(b.stockOutDate || 0) - new Date(a.stockOutDate || 0))
70
+ .slice(0, 6),
71
+ [stockOutRows],
72
+ );
73
+
74
+ return (
75
+ <div className="dashboard-page">
76
+ <div className="dashboard-hero">
77
+ <div>
78
+ <p className="dashboard-kicker">Live overview</p>
79
+ <h1>Inventory dashboard</h1>
80
+ </div>
81
+ <div className="dashboard-hero-total">
82
+ <span>Total value</span>
83
+ <strong>{money.format(summary.totalValue)}</strong>
84
+ </div>
85
+ </div>
86
+
87
+ {message && <p className="dashboard-message">{message}</p>}
88
+
89
+ <div className="dashboard-stats">
90
+ <article>
91
+ <span>Parts</span>
92
+ <strong>{number.format(summary.totalParts)}</strong>
93
+ </article>
94
+ <article>
95
+ <span>Stored units</span>
96
+ <strong>{number.format(summary.totalQuantity)}</strong>
97
+ </article>
98
+ <article>
99
+ <span>Stock in units</span>
100
+ <strong>{number.format(summary.totalIn)}</strong>
101
+ </article>
102
+ <article>
103
+ <span>Stock out units</span>
104
+ <strong>{number.format(summary.totalOut)}</strong>
105
+ </article>
106
+ </div>
107
+
108
+ <div className="dashboard-grid">
109
+ <section className="dashboard-panel dashboard-panel-wide">
110
+ <h2>Category value</h2>
111
+ <div className="dashboard-bars">
112
+ {categoryRows.map((row) => {
113
+ const percent = summary.totalValue ? Math.max((row.value / summary.totalValue) * 100, 5) : 0;
114
+ return (
115
+ <div key={row.category} className="dashboard-bar-row">
116
+ <div>
117
+ <strong>{row.category}</strong>
118
+ <span>
119
+ {row.count} part{row.count === 1 ? "" : "s"} / {number.format(row.quantity)} units
120
+ </span>
121
+ </div>
122
+ <div className="dashboard-bar-track" aria-label={`${row.category} value`}>
123
+ <span style={{ width: `${percent}%` }} />
124
+ </div>
125
+ <em>{money.format(row.value)}</em>
126
+ </div>
127
+ );
128
+ })}
129
+ </div>
130
+ </section>
131
+
132
+ <section className="dashboard-panel">
133
+ <h2>Lowest stock</h2>
134
+ <div className="dashboard-list">
135
+ {lowStock.map((part) => (
136
+ <div key={part._id} className="dashboard-list-item">
137
+ <span>
138
+ <strong>{part.name}</strong>
139
+ <small>{part.category}</small>
140
+ </span>
141
+ <em>{number.format(Number(part.quantity || 0))}</em>
142
+ </div>
143
+ ))}
144
+ </div>
145
+ </section>
146
+
147
+ <section className="dashboard-panel dashboard-panel-wide">
148
+ <h2>Recent stock out</h2>
149
+ <div className="overflow-x-auto">
150
+ <table className="w-full min-w-[560px] text-sm">
151
+ <thead>
152
+ <tr>
153
+ <th className="p-2 text-left">Part</th>
154
+ <th className="p-2 text-right">Qty</th>
155
+ <th className="p-2 text-right">Total</th>
156
+ <th className="p-2 text-left">Date</th>
157
+ </tr>
158
+ </thead>
159
+ <tbody>
160
+ {recentStockOut.map((row) => (
161
+ <tr key={row._id}>
162
+ <td className="p-2">{row.sparePart?.name || ""}</td>
163
+ <td className="p-2 text-right">{row.stockOutQuantity}</td>
164
+ <td className="p-2 text-right">{money.format(Number(row.stockOutTotalPrice || 0))}</td>
165
+ <td className="p-2">
166
+ {row.stockOutDate ? new Date(row.stockOutDate).toLocaleDateString() : ""}
167
+ </td>
168
+ </tr>
169
+ ))}
170
+ </tbody>
171
+ </table>
172
+ </div>
173
+ </section>
174
+ </div>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ export default DashboardPage;
@@ -0,0 +1,75 @@
1
+ import { useState } from "react";
2
+ import { Link, useNavigate } from "react-router-dom";
3
+ import { loginUser } from "../api/authApi";
4
+
5
+ function LoginPage({ onLoginSuccess }) {
6
+ const [username, setUsername] = useState("");
7
+ const [password, setPassword] = useState("");
8
+ const [error, setError] = useState("");
9
+ const navigate = useNavigate();
10
+
11
+ const handleLogin = async (e) => {
12
+ e.preventDefault();
13
+ setError("");
14
+ const u = username.trim();
15
+ if (u.length < 3) {
16
+ setError("Username must be at least 3 characters");
17
+ return;
18
+ }
19
+ try {
20
+ const r = await loginUser({ username: u, password });
21
+ onLoginSuccess(r.data.username);
22
+ navigate("/dashboard");
23
+ } catch (err) {
24
+ setError(err.response?.data?.message || "Login failed");
25
+ }
26
+ };
27
+
28
+ return (
29
+ <div className="auth-screen">
30
+ <div className="auth-intro">
31
+ <p>SIMS</p>
32
+ <h2>Parts, stock flow, and daily reports in one control room.</h2>
33
+ </div>
34
+ <div className="auth-panel">
35
+ <h1 className="text-2xl font-bold text-emerald-900">SIMS Login</h1>
36
+ <p className="mb-4 text-slate-600">Stock Inventory Management System</p>
37
+ {error && <div className="mb-3 rounded bg-red-50 px-3 py-2 text-sm text-red-700">{error}</div>}
38
+ <form onSubmit={handleLogin} className="space-y-3">
39
+ <input
40
+ className="w-full rounded border px-3 py-2"
41
+ placeholder="Username"
42
+ value={username}
43
+ onChange={(e) => setUsername(e.target.value)}
44
+ minLength={3}
45
+ required
46
+ />
47
+ <input
48
+ type="password"
49
+ className="w-full rounded border px-3 py-2"
50
+ placeholder="Password"
51
+ value={password}
52
+ onChange={(e) => setPassword(e.target.value)}
53
+ required
54
+ />
55
+ <button
56
+ type="submit"
57
+ className="w-full rounded bg-emerald-700 py-2 font-medium text-white hover:bg-emerald-800"
58
+ >
59
+ Login
60
+ </button>
61
+ </form>
62
+ <div className="mt-4 space-y-1 text-sm">
63
+ <Link to="/register" className="block text-emerald-800 hover:underline">
64
+ Create account
65
+ </Link>
66
+ <Link to="/reset-password" className="block text-amber-800 hover:underline">
67
+ Reset password
68
+ </Link>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ export default LoginPage;
@@ -0,0 +1,78 @@
1
+ import { useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { registerUser } from "../api/authApi";
4
+ import { strongPasswordError } from "../utils/passwordPolicy";
5
+
6
+ function RegisterPage() {
7
+ const [username, setUsername] = useState("");
8
+ const [password, setPassword] = useState("");
9
+ const [msg, setMsg] = useState("");
10
+ const [error, setError] = useState("");
11
+
12
+ const submit = async (e) => {
13
+ e.preventDefault();
14
+ setError("");
15
+ setMsg("");
16
+ const u = username.trim();
17
+ if (u.length < 3) {
18
+ setError("Username must be at least 3 characters");
19
+ return;
20
+ }
21
+ const pErr = strongPasswordError(password);
22
+ if (pErr) {
23
+ setError(pErr);
24
+ return;
25
+ }
26
+ try {
27
+ const r = await registerUser({ username: u, password });
28
+ setMsg(r.data.message);
29
+ setPassword("");
30
+ } catch (err) {
31
+ setError(err.response?.data?.message || "Registration failed");
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="auth-screen">
37
+ <div className="auth-intro">
38
+ <p>New operator</p>
39
+ <h2>Create access for the stock workspace.</h2>
40
+ </div>
41
+ <div className="auth-panel">
42
+ <h1 className="text-2xl font-bold text-emerald-900">Register</h1>
43
+ <p className="mb-2 text-sm text-slate-500">
44
+ Strong password: 8+ chars, uppercase, lowercase, number
45
+ </p>
46
+ {error && <div className="mb-2 rounded bg-red-50 px-3 py-2 text-sm text-red-700">{error}</div>}
47
+ {msg && <div className="mb-2 rounded bg-green-50 px-3 py-2 text-sm text-green-800">{msg}</div>}
48
+ <form onSubmit={submit} className="space-y-3">
49
+ <input
50
+ className="w-full rounded border px-3 py-2"
51
+ placeholder="Username"
52
+ value={username}
53
+ onChange={(e) => setUsername(e.target.value)}
54
+ minLength={3}
55
+ required
56
+ />
57
+ <input
58
+ type="password"
59
+ className="w-full rounded border px-3 py-2"
60
+ placeholder="Password"
61
+ value={password}
62
+ onChange={(e) => setPassword(e.target.value)}
63
+ minLength={8}
64
+ required
65
+ />
66
+ <button type="submit" className="w-full rounded bg-emerald-600 py-2 text-white hover:bg-emerald-700">
67
+ Register
68
+ </button>
69
+ </form>
70
+ <Link to="/login" className="mt-3 inline-block text-sm text-blue-700 hover:underline">
71
+ Back to login
72
+ </Link>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ export default RegisterPage;
@@ -0,0 +1,108 @@
1
+ import { useState } from "react";
2
+ import { getDailyStockOut, getDailyStockStatus } from "../api/simsReportApi";
3
+
4
+ function ReportsPage() {
5
+ const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10));
6
+ const [statusRows, setStatusRows] = useState([]);
7
+ const [outRows, setOutRows] = useState([]);
8
+ const [message, setMessage] = useState("");
9
+
10
+ const run = async () => {
11
+ setMessage("");
12
+ try {
13
+ const [a, b] = await Promise.all([getDailyStockStatus(date), getDailyStockOut(date)]);
14
+ setStatusRows(a.data);
15
+ setOutRows(b.data);
16
+ if (!a.data.length && !b.data.length) setMessage("No data for this day");
17
+ } catch {
18
+ setMessage("Report failed");
19
+ }
20
+ };
21
+
22
+ return (
23
+ <div className="space-y-6">
24
+ <div className="flex flex-wrap items-end gap-2">
25
+ <div>
26
+ <label className="text-sm text-slate-600">Date</label>
27
+ <input
28
+ type="date"
29
+ className="block rounded border px-2 py-2"
30
+ value={date}
31
+ onChange={(e) => setDate(e.target.value)}
32
+ />
33
+ </div>
34
+ <button
35
+ type="button"
36
+ className="rounded bg-emerald-800 px-4 py-2 text-white hover:bg-emerald-900"
37
+ onClick={run}
38
+ >
39
+ Generate
40
+ </button>
41
+ </div>
42
+ {message && <p className="text-sm text-slate-500">{message}</p>}
43
+
44
+ <section className="rounded-lg border border-emerald-200 bg-white p-4 shadow">
45
+ <h2 className="mb-3 text-lg font-semibold text-emerald-900">Daily stock status</h2>
46
+ <p className="mb-2 text-sm text-slate-500">Spare name, stored, stock in / out (day), remaining (current stock)</p>
47
+ <div className="overflow-x-auto">
48
+ <table className="w-full min-w-[720px] text-sm">
49
+ <thead>
50
+ <tr className="bg-slate-100">
51
+ <th className="p-2 text-left">Spare name</th>
52
+ <th className="p-2 text-left">Category</th>
53
+ <th className="p-2 text-right">Stored qty (current)</th>
54
+ <th className="p-2 text-right">Stock in (day)</th>
55
+ <th className="p-2 text-right">Stock out (day)</th>
56
+ <th className="p-2 text-right">Remaining (current)</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ {statusRows.map((r) => (
61
+ <tr key={r.spareName + r.category} className="border-b">
62
+ <td className="p-2">{r.spareName}</td>
63
+ <td className="p-2">{r.category}</td>
64
+ <td className="p-2 text-right">{r.storedQuantity}</td>
65
+ <td className="p-2 text-right">{r.stockInQuantity}</td>
66
+ <td className="p-2 text-right">{r.stockOutQuantity}</td>
67
+ <td className="p-2 text-right">{r.remainingQuantity}</td>
68
+ </tr>
69
+ ))}
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+ </section>
74
+
75
+ <section className="rounded-lg border border-blue-200 bg-white p-4 shadow">
76
+ <h2 className="mb-3 text-lg font-semibold text-blue-900">Daily stock out report</h2>
77
+ <div className="overflow-x-auto">
78
+ <table className="w-full min-w-[600px] text-sm">
79
+ <thead>
80
+ <tr className="bg-slate-100">
81
+ <th className="p-2 text-left">Spare</th>
82
+ <th className="p-2 text-right">Qty out</th>
83
+ <th className="p-2 text-right">Unit</th>
84
+ <th className="p-2 text-right">Total</th>
85
+ <th className="p-2 text-left">Date</th>
86
+ </tr>
87
+ </thead>
88
+ <tbody>
89
+ {outRows.map((r) => (
90
+ <tr key={r.id} className="border-b">
91
+ <td className="p-2">{r.spareName}</td>
92
+ <td className="p-2 text-right">{r.stockOutQuantity}</td>
93
+ <td className="p-2 text-right">{r.stockOutUnitPrice}</td>
94
+ <td className="p-2 text-right">{r.stockOutTotalPrice}</td>
95
+ <td className="p-2">
96
+ {r.stockOutDate ? new Date(r.stockOutDate).toLocaleString() : ""}
97
+ </td>
98
+ </tr>
99
+ ))}
100
+ </tbody>
101
+ </table>
102
+ </div>
103
+ </section>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ export default ReportsPage;
@@ -0,0 +1,75 @@
1
+ import { useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { recoverPassword } from "../api/authApi";
4
+ import { strongPasswordError } from "../utils/passwordPolicy";
5
+
6
+ function ResetPasswordPage() {
7
+ const [username, setUsername] = useState("");
8
+ const [newPassword, setNewPassword] = useState("");
9
+ const [msg, setMsg] = useState("");
10
+ const [error, setError] = useState("");
11
+
12
+ const submit = async (e) => {
13
+ e.preventDefault();
14
+ setError("");
15
+ setMsg("");
16
+ const u = username.trim();
17
+ if (u.length < 3) {
18
+ setError("Username must be at least 3 characters");
19
+ return;
20
+ }
21
+ const pErr = strongPasswordError(newPassword);
22
+ if (pErr) {
23
+ setError(pErr);
24
+ return;
25
+ }
26
+ try {
27
+ const r = await recoverPassword({ username: u, newPassword });
28
+ setMsg(r.data.message);
29
+ setNewPassword("");
30
+ } catch (err) {
31
+ setError(err.response?.data?.message || "Reset failed");
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="auth-screen">
37
+ <div className="auth-intro">
38
+ <p>Recovery</p>
39
+ <h2>Reset credentials and get back to the floor.</h2>
40
+ </div>
41
+ <div className="auth-panel">
42
+ <h1 className="text-2xl font-bold text-amber-900">Reset password</h1>
43
+ {error && <div className="mb-2 text-sm text-red-700">{error}</div>}
44
+ {msg && <div className="mb-2 text-sm text-green-800">{msg}</div>}
45
+ <form onSubmit={submit} className="space-y-3">
46
+ <input
47
+ className="w-full rounded border px-3 py-2"
48
+ placeholder="Username"
49
+ value={username}
50
+ onChange={(e) => setUsername(e.target.value)}
51
+ minLength={3}
52
+ required
53
+ />
54
+ <input
55
+ type="password"
56
+ className="w-full rounded border px-3 py-2"
57
+ placeholder="New password (strong)"
58
+ value={newPassword}
59
+ onChange={(e) => setNewPassword(e.target.value)}
60
+ minLength={8}
61
+ required
62
+ />
63
+ <button type="submit" className="w-full rounded bg-amber-600 py-2 text-white hover:bg-amber-700">
64
+ Update password
65
+ </button>
66
+ </form>
67
+ <Link to="/login" className="mt-3 inline-block text-sm text-blue-700 hover:underline">
68
+ Back to login
69
+ </Link>
70
+ </div>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ export default ResetPasswordPage;
@@ -0,0 +1,128 @@
1
+ import { useEffect, useState } from "react";
2
+ import { getSpareParts, createSparePart } from "../api/sparePartsApi";
3
+
4
+ const initial = { name: "", category: "", quantity: "", unitPrice: "" };
5
+
6
+ function SparePartPage() {
7
+ const [form, setForm] = useState(initial);
8
+ const [rows, setRows] = useState([]);
9
+ const [message, setMessage] = useState("");
10
+
11
+ const load = () =>
12
+ getSpareParts()
13
+ .then((r) => setRows(r.data))
14
+ .catch(() => setMessage("Failed to load parts"));
15
+
16
+ useEffect(() => {
17
+ load();
18
+ }, []);
19
+
20
+ const onChange = (e) => setForm((f) => ({ ...f, [e.target.name]: e.target.value }));
21
+
22
+ const onSubmit = async (e) => {
23
+ e.preventDefault();
24
+ setMessage("");
25
+ const q = Number(form.quantity);
26
+ const u = Number(form.unitPrice);
27
+ if (Number.isNaN(q) || q < 0 || Number.isNaN(u) || u < 0) {
28
+ setMessage("Invalid numbers");
29
+ return;
30
+ }
31
+ try {
32
+ await createSparePart({
33
+ name: form.name.trim(),
34
+ category: form.category.trim(),
35
+ quantity: q,
36
+ unitPrice: u,
37
+ });
38
+ setForm(initial);
39
+ setMessage("Saved");
40
+ load();
41
+ } catch (err) {
42
+ setMessage(err.response?.data?.message || "Failed to save");
43
+ }
44
+ };
45
+
46
+ return (
47
+ <div className="grid gap-4 md:grid-cols-2">
48
+ <section className="rounded-lg border border-emerald-200 bg-white p-4 shadow">
49
+ <h2 className="mb-3 text-lg font-semibold text-emerald-900">Spare Part (insert only)</h2>
50
+ <form onSubmit={onSubmit} className="space-y-2">
51
+ <input
52
+ name="name"
53
+ className="w-full rounded border px-3 py-2"
54
+ placeholder="Name"
55
+ value={form.name}
56
+ onChange={onChange}
57
+ required
58
+ />
59
+ <input
60
+ name="category"
61
+ className="w-full rounded border px-3 py-2"
62
+ placeholder="Category"
63
+ value={form.category}
64
+ onChange={onChange}
65
+ required
66
+ />
67
+ <input
68
+ type="number"
69
+ name="quantity"
70
+ className="w-full rounded border px-3 py-2"
71
+ placeholder="Quantity"
72
+ min="0"
73
+ value={form.quantity}
74
+ onChange={onChange}
75
+ required
76
+ />
77
+ <input
78
+ type="number"
79
+ name="unitPrice"
80
+ className="w-full rounded border px-3 py-2"
81
+ placeholder="Unit price"
82
+ min="0"
83
+ step="0.01"
84
+ value={form.unitPrice}
85
+ onChange={onChange}
86
+ required
87
+ />
88
+ <button
89
+ type="submit"
90
+ className="w-full rounded bg-emerald-700 py-2 font-medium text-white hover:bg-emerald-800"
91
+ >
92
+ Save
93
+ </button>
94
+ </form>
95
+ {message && <p className="mt-2 text-sm text-slate-600">{message}</p>}
96
+ </section>
97
+ <section className="rounded-lg border border-slate-200 bg-white p-4 shadow">
98
+ <h2 className="mb-3 text-lg font-semibold">Parts list (read)</h2>
99
+ <div className="max-h-96 overflow-auto">
100
+ <table className="w-full text-left text-sm">
101
+ <thead className="sticky top-0 bg-slate-100">
102
+ <tr>
103
+ <th className="p-2">Name</th>
104
+ <th className="p-2">Category</th>
105
+ <th className="p-2">Qty</th>
106
+ <th className="p-2">Unit</th>
107
+ <th className="p-2">Total</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ {rows.map((p) => (
112
+ <tr key={p._id} className="border-b">
113
+ <td className="p-2">{p.name}</td>
114
+ <td className="p-2">{p.category}</td>
115
+ <td className="p-2">{p.quantity}</td>
116
+ <td className="p-2">{p.unitPrice}</td>
117
+ <td className="p-2">{p.totalPrice}</td>
118
+ </tr>
119
+ ))}
120
+ </tbody>
121
+ </table>
122
+ </div>
123
+ </section>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ export default SparePartPage;