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,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;
|