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,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "frontend-project",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SmartPark SIMS Frontend",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.16.1",
|
|
13
|
+
"html2pdf.js": "^0.14.0",
|
|
14
|
+
"react": "^18.2.0",
|
|
15
|
+
"react-dom": "^18.2.0",
|
|
16
|
+
"react-router-dom": "^6.20.1",
|
|
17
|
+
"sucrase": "^3.34.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@babel/traverse": "^7.25.0",
|
|
21
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
22
|
+
"autoprefixer": "^10.4.16",
|
|
23
|
+
"postcss": "^8.4.32",
|
|
24
|
+
"tailwindcss": "^3.3.6",
|
|
25
|
+
"vite": "^5.0.8"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import Login from './pages/Login';
|
|
5
|
+
import SparePart from './pages/SparePart';
|
|
6
|
+
import StockIn from './pages/StockIn';
|
|
7
|
+
import StockOut from './pages/StockOut';
|
|
8
|
+
import Reports from './pages/Reports';
|
|
9
|
+
import Navbar from './components/Navbar';
|
|
10
|
+
|
|
11
|
+
axios.defaults.baseURL = 'http://localhost:5000';
|
|
12
|
+
axios.defaults.withCredentials = true;
|
|
13
|
+
|
|
14
|
+
function PrivateRoute({ children, isAuthenticated }) {
|
|
15
|
+
return isAuthenticated ? children : <Navigate to="/login" replace />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function App() {
|
|
19
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
axios.get('/api/auth/check')
|
|
24
|
+
.then(res => setIsAuthenticated(res.data.authenticated))
|
|
25
|
+
.catch(() => setIsAuthenticated(false))
|
|
26
|
+
.finally(() => setLoading(false));
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
if (loading) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex justify-center items-center min-h-screen bg-gray-100">
|
|
32
|
+
<div className="text-blue-800 text-xl font-semibold">Loading...</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Router>
|
|
39
|
+
{isAuthenticated && <Navbar setIsAuthenticated={setIsAuthenticated} />}
|
|
40
|
+
<Routes>
|
|
41
|
+
<Route path="/login" element={<Login setIsAuthenticated={setIsAuthenticated} />} />
|
|
42
|
+
<Route path="/" element={<PrivateRoute isAuthenticated={isAuthenticated}><SparePart /></PrivateRoute>} />
|
|
43
|
+
<Route path="/spare-parts" element={<PrivateRoute isAuthenticated={isAuthenticated}><SparePart /></PrivateRoute>} />
|
|
44
|
+
<Route path="/stock-in" element={<PrivateRoute isAuthenticated={isAuthenticated}><StockIn /></PrivateRoute>} />
|
|
45
|
+
<Route path="/stock-out" element={<PrivateRoute isAuthenticated={isAuthenticated}><StockOut /></PrivateRoute>} />
|
|
46
|
+
<Route path="/reports" element={<PrivateRoute isAuthenticated={isAuthenticated}><Reports /></PrivateRoute>} />
|
|
47
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
48
|
+
</Routes>
|
|
49
|
+
</Router>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default App;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
|
|
5
|
+
function Navbar({ setIsAuthenticated }) {
|
|
6
|
+
const location = useLocation();
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
9
|
+
|
|
10
|
+
const handleLogout = async () => {
|
|
11
|
+
try {
|
|
12
|
+
await axios.post('/api/auth/logout');
|
|
13
|
+
} catch (_) {}
|
|
14
|
+
setIsAuthenticated(false);
|
|
15
|
+
navigate('/login');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const links = [
|
|
19
|
+
{ to: '/spare-parts', label: 'Spare Parts' },
|
|
20
|
+
{ to: '/stock-in', label: 'Stock In' },
|
|
21
|
+
{ to: '/stock-out', label: 'Stock Out' },
|
|
22
|
+
{ to: '/reports', label: 'Reports' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const isActive = (path) =>
|
|
26
|
+
location.pathname === path
|
|
27
|
+
? 'bg-blue-900 text-white'
|
|
28
|
+
: 'text-blue-100 hover:bg-blue-700 hover:text-white';
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<nav className="bg-blue-800 shadow-lg">
|
|
32
|
+
<div className="max-w-7xl mx-auto px-4">
|
|
33
|
+
<div className="flex items-center justify-between h-16">
|
|
34
|
+
{/* Brand */}
|
|
35
|
+
<div className="flex items-center space-x-2">
|
|
36
|
+
<div className="bg-white rounded p-1">
|
|
37
|
+
<svg className="w-6 h-6 text-blue-800" fill="currentColor" viewBox="0 0 20 20">
|
|
38
|
+
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" />
|
|
39
|
+
</svg>
|
|
40
|
+
</div>
|
|
41
|
+
<span className="text-white font-bold text-lg">SmartPark SIMS</span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* Desktop links */}
|
|
45
|
+
<div className="hidden md:flex items-center space-x-1">
|
|
46
|
+
{links.map(link => (
|
|
47
|
+
<Link
|
|
48
|
+
key={link.to}
|
|
49
|
+
to={link.to}
|
|
50
|
+
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${isActive(link.to)}`}
|
|
51
|
+
>
|
|
52
|
+
{link.label}
|
|
53
|
+
</Link>
|
|
54
|
+
))}
|
|
55
|
+
<button
|
|
56
|
+
onClick={handleLogout}
|
|
57
|
+
className="ml-2 px-4 py-2 rounded-md text-sm font-medium bg-red-600 text-white hover:bg-red-700 transition-colors"
|
|
58
|
+
>
|
|
59
|
+
Logout
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Mobile menu button */}
|
|
64
|
+
<button
|
|
65
|
+
className="md:hidden text-white p-2 rounded"
|
|
66
|
+
onClick={() => setMenuOpen(!menuOpen)}
|
|
67
|
+
>
|
|
68
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
69
|
+
{menuOpen
|
|
70
|
+
? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
71
|
+
: <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
72
|
+
}
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Mobile menu */}
|
|
78
|
+
{menuOpen && (
|
|
79
|
+
<div className="md:hidden pb-3 space-y-1">
|
|
80
|
+
{links.map(link => (
|
|
81
|
+
<Link
|
|
82
|
+
key={link.to}
|
|
83
|
+
to={link.to}
|
|
84
|
+
onClick={() => setMenuOpen(false)}
|
|
85
|
+
className={`block px-4 py-2 rounded-md text-sm font-medium ${isActive(link.to)}`}
|
|
86
|
+
>
|
|
87
|
+
{link.label}
|
|
88
|
+
</Link>
|
|
89
|
+
))}
|
|
90
|
+
<button
|
|
91
|
+
onClick={handleLogout}
|
|
92
|
+
className="w-full text-left px-4 py-2 rounded-md text-sm font-medium bg-red-600 text-white hover:bg-red-700"
|
|
93
|
+
>
|
|
94
|
+
Logout
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</nav>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default Navbar;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
|
|
5
|
+
function Login({ setIsAuthenticated }) {
|
|
6
|
+
const [form, setForm] = useState({ username: '', password: '' });
|
|
7
|
+
const [error, setError] = useState('');
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
|
|
11
|
+
const handleSubmit = async (e) => {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
setLoading(true);
|
|
14
|
+
setError('');
|
|
15
|
+
try {
|
|
16
|
+
await axios.post('/api/auth/login', form);
|
|
17
|
+
setIsAuthenticated(true);
|
|
18
|
+
navigate('/spare-parts');
|
|
19
|
+
} catch (err) {
|
|
20
|
+
setError(err.response?.data?.message || 'Login failed. Please try again.');
|
|
21
|
+
} finally {
|
|
22
|
+
setLoading(false);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="min-h-screen bg-gradient-to-br from-blue-900 via-blue-800 to-blue-600 flex items-center justify-center px-4">
|
|
28
|
+
<div className="bg-white rounded-2xl shadow-2xl p-8 w-full max-w-md">
|
|
29
|
+
<div className="text-center mb-8">
|
|
30
|
+
<div className="inline-flex items-center justify-center w-16 h-16 bg-blue-800 rounded-full mb-4">
|
|
31
|
+
<svg className="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
32
|
+
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" />
|
|
33
|
+
</svg>
|
|
34
|
+
</div>
|
|
35
|
+
<h1 className="text-3xl font-bold text-blue-900">SmartPark</h1>
|
|
36
|
+
<p className="text-gray-500 mt-1 text-sm">Stock Inventory Management System</p>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{error && (
|
|
40
|
+
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-5 text-sm flex items-center gap-2">
|
|
41
|
+
<svg className="w-4 h-4 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
42
|
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" />
|
|
43
|
+
</svg>
|
|
44
|
+
{error}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
49
|
+
<div>
|
|
50
|
+
<label className="block text-sm font-semibold text-gray-700 mb-1">Username</label>
|
|
51
|
+
<input
|
|
52
|
+
type="text"
|
|
53
|
+
value={form.username}
|
|
54
|
+
onChange={e => setForm({ ...form, username: e.target.value })}
|
|
55
|
+
required
|
|
56
|
+
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
|
57
|
+
placeholder="Enter your username"
|
|
58
|
+
autoComplete="username"
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<label className="block text-sm font-semibold text-gray-700 mb-1">Password</label>
|
|
63
|
+
<input
|
|
64
|
+
type="password"
|
|
65
|
+
value={form.password}
|
|
66
|
+
onChange={e => setForm({ ...form, password: e.target.value })}
|
|
67
|
+
required
|
|
68
|
+
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
|
69
|
+
placeholder="Enter your password"
|
|
70
|
+
autoComplete="current-password"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
<button
|
|
74
|
+
type="submit"
|
|
75
|
+
disabled={loading}
|
|
76
|
+
className="w-full bg-blue-800 text-white py-2.5 rounded-lg font-semibold hover:bg-blue-900 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
|
77
|
+
>
|
|
78
|
+
{loading ? 'Signing in...' : 'Sign In'}
|
|
79
|
+
</button>
|
|
80
|
+
</form>
|
|
81
|
+
|
|
82
|
+
<div className="mt-6 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
|
83
|
+
<p className="text-xs text-gray-500 text-center">
|
|
84
|
+
Default credentials: <span className="font-semibold text-gray-700">admin</span> / <span className="font-semibold text-gray-700">Admin@123</span>
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default Login;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
function Reports() {
|
|
5
|
+
const today = new Date().toISOString().split('T')[0];
|
|
6
|
+
const [selectedDate, setSelectedDate] = useState(today);
|
|
7
|
+
const [stockOutReport, setStockOutReport] = useState([]);
|
|
8
|
+
const [stockStatus, setStockStatus] = useState([]);
|
|
9
|
+
const [activeTab, setActiveTab] = useState('stockout');
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const reportRef = useRef();
|
|
12
|
+
|
|
13
|
+
const fetchReports = async (date) => {
|
|
14
|
+
setLoading(true);
|
|
15
|
+
try {
|
|
16
|
+
const [soRes, ssRes] = await Promise.all([
|
|
17
|
+
axios.get(`/api/reports/daily-stockout?date=${date}`),
|
|
18
|
+
axios.get(`/api/reports/stock-status?date=${date}`),
|
|
19
|
+
]);
|
|
20
|
+
setStockOutReport(soRes.data);
|
|
21
|
+
setStockStatus(ssRes.data);
|
|
22
|
+
} catch (_) {}
|
|
23
|
+
setLoading(false);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
useEffect(() => { fetchReports(selectedDate); }, [selectedDate]);
|
|
27
|
+
|
|
28
|
+
const stockOutTotal = stockOutReport.reduce((sum, r) => sum + parseFloat(r.StockOutTotalPrice || 0), 0);
|
|
29
|
+
const stockOutQty = stockOutReport.reduce((sum, r) => sum + r.StockOutQuantity, 0);
|
|
30
|
+
|
|
31
|
+
const handlePrint = () => window.print();
|
|
32
|
+
|
|
33
|
+
const [exporting, setExporting] = useState(false);
|
|
34
|
+
|
|
35
|
+
const handleExportPDF = async () => {
|
|
36
|
+
if (exporting) return;
|
|
37
|
+
setExporting(true);
|
|
38
|
+
try {
|
|
39
|
+
const { jsPDF } = await import('jspdf');
|
|
40
|
+
const doc = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' });
|
|
41
|
+
let y = 20;
|
|
42
|
+
const c = (t, s = 14, b = 'bold') => { doc.setFontSize(s); doc.setFont('helvetica', b); doc.text(t, (210 - doc.getTextWidth(t)) / 2, y); y += s * 0.45; };
|
|
43
|
+
const l = (t, s = 10, b = 'normal', x = 15) => { doc.setFontSize(s); doc.setFont('helvetica', b); doc.text(t, x, y); y += s * 0.45; };
|
|
44
|
+
const hdr = (cols) => { doc.setFontSize(9); doc.setFont('helvetica', 'bold'); const xs = [15, 65, 105, 140, 170]; cols.forEach((c, i) => doc.text(c, xs[i], y)); y += 5; doc.line(15, y, 195, y); y += 3; };
|
|
45
|
+
const row = (cols) => { doc.setFontSize(9); doc.setFont('helvetica', 'normal'); const xs = [15, 65, 105, 140, 170]; cols.forEach((c, i) => doc.text(String(c), xs[i], y)); y += 4.5; };
|
|
46
|
+
|
|
47
|
+
c('SmartPark - SIMS', 16);
|
|
48
|
+
c(`Date: ${new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-GB', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}`, 10, 'normal');
|
|
49
|
+
y += 4;
|
|
50
|
+
|
|
51
|
+
if (activeTab === 'stockout') {
|
|
52
|
+
c('DAILY STOCK OUT REPORT', 12);
|
|
53
|
+
y += 2;
|
|
54
|
+
hdr(['#', 'Spare Part', 'Category', 'Qty Out', 'Total (RWF)']);
|
|
55
|
+
stockOutReport.forEach((r, i) => row([i + 1, r.SparepartName, r.Category, r.StockOutQuantity, Number(r.StockOutTotalPrice).toLocaleString()]));
|
|
56
|
+
if (stockOutReport.length > 0) { doc.line(15, y, 195, y); y += 3; doc.setFont('helvetica', 'bold'); row(['', 'TOTAL', '', stockOutQty, stockOutTotal.toLocaleString()]); }
|
|
57
|
+
} else {
|
|
58
|
+
c('DAILY STOCK STATUS REPORT', 12);
|
|
59
|
+
y += 2;
|
|
60
|
+
hdr(['#', 'Spare Part', 'Category', 'Stored', 'Remaining']);
|
|
61
|
+
stockStatus.forEach((r, i) => row([i + 1, r.SparepartName, r.Category, r.StoredQuantity, r.RemainingQuantity]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
doc.save(`SmartPark_SIMS_Report_${selectedDate}.pdf`);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('PDF export failed:', err);
|
|
67
|
+
} finally {
|
|
68
|
+
setExporting(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
74
|
+
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
75
|
+
<div>
|
|
76
|
+
<h2 className="text-2xl font-bold text-gray-800">Reports</h2>
|
|
77
|
+
<p className="text-gray-500 text-sm mt-1">Daily stock reports for SmartPark</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="flex items-center gap-3">
|
|
80
|
+
<div>
|
|
81
|
+
<label className="block text-xs text-gray-500 mb-1">Select Date</label>
|
|
82
|
+
<input
|
|
83
|
+
type="date"
|
|
84
|
+
value={selectedDate}
|
|
85
|
+
onChange={e => setSelectedDate(e.target.value)}
|
|
86
|
+
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<button
|
|
90
|
+
onClick={handlePrint}
|
|
91
|
+
className="mt-5 bg-blue-800 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-900 transition-colors flex items-center gap-2"
|
|
92
|
+
>
|
|
93
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
94
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
|
|
95
|
+
</svg>
|
|
96
|
+
Print
|
|
97
|
+
</button>
|
|
98
|
+
<button
|
|
99
|
+
onClick={handleExportPDF}
|
|
100
|
+
disabled={exporting}
|
|
101
|
+
className="mt-5 bg-green-700 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-800 transition-colors flex items-center gap-2 disabled:opacity-60"
|
|
102
|
+
>
|
|
103
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
104
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
105
|
+
</svg>
|
|
106
|
+
{exporting ? 'Exporting...' : 'Export PDF'}
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Tabs */}
|
|
112
|
+
<div className="flex border-b mb-6">
|
|
113
|
+
<button
|
|
114
|
+
onClick={() => setActiveTab('stockout')}
|
|
115
|
+
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${activeTab === 'stockout' ? 'border-blue-800 text-blue-800' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
|
|
116
|
+
>
|
|
117
|
+
Daily Stock Out Report
|
|
118
|
+
</button>
|
|
119
|
+
<button
|
|
120
|
+
onClick={() => setActiveTab('status')}
|
|
121
|
+
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${activeTab === 'status' ? 'border-blue-800 text-blue-800' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
|
|
122
|
+
>
|
|
123
|
+
Stock Status Report
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{loading ? (
|
|
128
|
+
<div className="text-center py-12 text-gray-400">Loading reports...</div>
|
|
129
|
+
) : (
|
|
130
|
+
<div ref={reportRef}>
|
|
131
|
+
{/* Daily Stock Out Report */}
|
|
132
|
+
{activeTab === 'stockout' && (
|
|
133
|
+
<div>
|
|
134
|
+
<div className="mb-4 p-4 bg-blue-50 rounded-lg border border-blue-100 flex flex-wrap gap-6">
|
|
135
|
+
<div>
|
|
136
|
+
<span className="text-xs text-blue-600 font-medium uppercase">Report Date</span>
|
|
137
|
+
<p className="text-base font-bold text-blue-900">
|
|
138
|
+
{new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-GB', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
<div>
|
|
142
|
+
<span className="text-xs text-blue-600 font-medium uppercase">Total Records</span>
|
|
143
|
+
<p className="text-base font-bold text-blue-900">{stockOutReport.length}</p>
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<span className="text-xs text-blue-600 font-medium uppercase">Total Qty Out</span>
|
|
147
|
+
<p className="text-base font-bold text-blue-900">{stockOutQty}</p>
|
|
148
|
+
</div>
|
|
149
|
+
<div>
|
|
150
|
+
<span className="text-xs text-blue-600 font-medium uppercase">Total Value (RWF)</span>
|
|
151
|
+
<p className="text-base font-bold text-blue-900">{stockOutTotal.toLocaleString()}</p>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="bg-white rounded-xl shadow overflow-hidden">
|
|
156
|
+
<div className="px-6 py-4 border-b bg-gray-50">
|
|
157
|
+
<h3 className="font-semibold text-gray-700">SmartPark — Daily Stock Out Report</h3>
|
|
158
|
+
<p className="text-xs text-gray-500 mt-0.5">Date: {new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-GB')}</p>
|
|
159
|
+
</div>
|
|
160
|
+
<div className="overflow-x-auto">
|
|
161
|
+
<table className="w-full text-sm">
|
|
162
|
+
<thead className="bg-blue-800 text-white">
|
|
163
|
+
<tr>
|
|
164
|
+
<th className="px-4 py-3 text-left">#</th>
|
|
165
|
+
<th className="px-4 py-3 text-left">Spare Part</th>
|
|
166
|
+
<th className="px-4 py-3 text-left">Category</th>
|
|
167
|
+
<th className="px-4 py-3 text-right">Qty Out</th>
|
|
168
|
+
<th className="px-4 py-3 text-right">Unit Price (RWF)</th>
|
|
169
|
+
<th className="px-4 py-3 text-right">Total Price (RWF)</th>
|
|
170
|
+
<th className="px-4 py-3 text-left">Date</th>
|
|
171
|
+
</tr>
|
|
172
|
+
</thead>
|
|
173
|
+
<tbody className="divide-y divide-gray-100">
|
|
174
|
+
{stockOutReport.length === 0 ? (
|
|
175
|
+
<tr>
|
|
176
|
+
<td colSpan="7" className="text-center py-10 text-gray-400">
|
|
177
|
+
No stock out records for {new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-GB')}.
|
|
178
|
+
</td>
|
|
179
|
+
</tr>
|
|
180
|
+
) : stockOutReport.map((r, i) => (
|
|
181
|
+
<tr key={r.StockOutID} className="hover:bg-gray-50">
|
|
182
|
+
<td className="px-4 py-3 text-gray-500">{i + 1}</td>
|
|
183
|
+
<td className="px-4 py-3 font-semibold">{r.SparepartName}</td>
|
|
184
|
+
<td className="px-4 py-3">
|
|
185
|
+
<span className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full text-xs">{r.Category}</span>
|
|
186
|
+
</td>
|
|
187
|
+
<td className="px-4 py-3 text-right font-semibold text-red-600">{r.StockOutQuantity}</td>
|
|
188
|
+
<td className="px-4 py-3 text-right">{Number(r.StockOutUnitPrice).toLocaleString()}</td>
|
|
189
|
+
<td className="px-4 py-3 text-right font-semibold">{Number(r.StockOutTotalPrice).toLocaleString()}</td>
|
|
190
|
+
<td className="px-4 py-3">{new Date(r.StockOutDate).toLocaleDateString('en-GB')}</td>
|
|
191
|
+
</tr>
|
|
192
|
+
))}
|
|
193
|
+
{stockOutReport.length > 0 && (
|
|
194
|
+
<tr className="bg-blue-50 font-bold">
|
|
195
|
+
<td colSpan="3" className="px-4 py-3 text-right text-gray-700">TOTALS</td>
|
|
196
|
+
<td className="px-4 py-3 text-right text-red-700">{stockOutQty}</td>
|
|
197
|
+
<td className="px-4 py-3"></td>
|
|
198
|
+
<td className="px-4 py-3 text-right text-gray-800">{stockOutTotal.toLocaleString()}</td>
|
|
199
|
+
<td className="px-4 py-3"></td>
|
|
200
|
+
</tr>
|
|
201
|
+
)}
|
|
202
|
+
</tbody>
|
|
203
|
+
</table>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{/* Stock Status Report */}
|
|
210
|
+
{activeTab === 'status' && (
|
|
211
|
+
<div>
|
|
212
|
+
<div className="mb-4 p-4 bg-blue-50 rounded-lg border border-blue-100">
|
|
213
|
+
<span className="text-xs text-blue-600 font-medium uppercase">Report Date</span>
|
|
214
|
+
<p className="text-base font-bold text-blue-900">
|
|
215
|
+
{new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-GB', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
|
|
216
|
+
</p>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div className="bg-white rounded-xl shadow overflow-hidden">
|
|
220
|
+
<div className="px-6 py-4 border-b bg-gray-50">
|
|
221
|
+
<h3 className="font-semibold text-gray-700">SmartPark Daily Stock Status Report</h3>
|
|
222
|
+
<p className="text-xs text-gray-500 mt-0.5">Date: {new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-GB')}</p>
|
|
223
|
+
</div>
|
|
224
|
+
<div className="overflow-x-auto">
|
|
225
|
+
<table className="w-full text-sm">
|
|
226
|
+
<thead className="bg-blue-800 text-white">
|
|
227
|
+
<tr>
|
|
228
|
+
<th className="px-4 py-3 text-left">#</th>
|
|
229
|
+
<th className="px-4 py-3 text-left">Spare Part</th>
|
|
230
|
+
<th className="px-4 py-3 text-left">Category</th>
|
|
231
|
+
<th className="px-4 py-3 text-right">Stored Qty</th>
|
|
232
|
+
<th className="px-4 py-3 text-right">Stock Out</th>
|
|
233
|
+
<th className="px-4 py-3 text-right">Remaining Qty</th>
|
|
234
|
+
<th className="px-4 py-3 text-center">Status</th>
|
|
235
|
+
</tr>
|
|
236
|
+
</thead>
|
|
237
|
+
<tbody className="divide-y divide-gray-100">
|
|
238
|
+
{stockStatus.length === 0 ? (
|
|
239
|
+
<tr>
|
|
240
|
+
<td colSpan="7" className="text-center py-10 text-gray-400">No spare parts found.</td>
|
|
241
|
+
</tr>
|
|
242
|
+
) : stockStatus.map((r, i) => {
|
|
243
|
+
const isLow = r.RemainingQuantity <= 5;
|
|
244
|
+
const isEmpty = r.RemainingQuantity === 0;
|
|
245
|
+
return (
|
|
246
|
+
<tr key={r.SparePartID} className="hover:bg-gray-50">
|
|
247
|
+
<td className="px-4 py-3 text-gray-500">{i + 1}</td>
|
|
248
|
+
<td className="px-4 py-3 font-semibold">{r.SparepartName}</td>
|
|
249
|
+
<td className="px-4 py-3">
|
|
250
|
+
<span className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full text-xs">{r.Category}</span>
|
|
251
|
+
</td>
|
|
252
|
+
<td className="px-4 py-3 text-right">{r.StoredQuantity}</td>
|
|
253
|
+
<td className="px-4 py-3 text-right font-semibold text-red-600">{r.DailyStockOut}</td>
|
|
254
|
+
<td className="px-4 py-3 text-right font-bold text-gray-800">{r.RemainingQuantity}</td>
|
|
255
|
+
<td className="px-4 py-3 text-center">
|
|
256
|
+
{isEmpty ? (
|
|
257
|
+
<span className="bg-red-100 text-red-700 px-2 py-0.5 rounded-full text-xs font-medium">Out of Stock</span>
|
|
258
|
+
) : isLow ? (
|
|
259
|
+
<span className="bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full text-xs font-medium">Low Stock</span>
|
|
260
|
+
) : (
|
|
261
|
+
<span className="bg-green-100 text-green-700 px-2 py-0.5 rounded-full text-xs font-medium">In Stock</span>
|
|
262
|
+
)}
|
|
263
|
+
</td>
|
|
264
|
+
</tr>
|
|
265
|
+
);
|
|
266
|
+
})}
|
|
267
|
+
</tbody>
|
|
268
|
+
</table>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export default Reports;
|