react-node-app 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.
Files changed (51) hide show
  1. package/.gitignore +6 -0
  2. package/README.md +0 -0
  3. package/backend-project/config/db.js +13 -0
  4. package/backend-project/controllers/authController.js +63 -0
  5. package/backend-project/controllers/productController.js +113 -0
  6. package/backend-project/controllers/reportController.js +93 -0
  7. package/backend-project/controllers/saleController.js +91 -0
  8. package/backend-project/controllers/stockController.js +126 -0
  9. package/backend-project/middleware/auth.js +8 -0
  10. package/backend-project/models/Product.js +40 -0
  11. package/backend-project/models/Sale.js +38 -0
  12. package/backend-project/models/StockStatus.js +35 -0
  13. package/backend-project/models/User.js +24 -0
  14. package/backend-project/package.json +24 -0
  15. package/backend-project/routes/authRoutes.js +9 -0
  16. package/backend-project/routes/productRoutes.js +9 -0
  17. package/backend-project/routes/reportRoutes.js +14 -0
  18. package/backend-project/routes/saleRoutes.js +8 -0
  19. package/backend-project/routes/stockRoutes.js +18 -0
  20. package/backend-project/server.js +55 -0
  21. package/backend-project/utils/seed.js +44 -0
  22. package/frontend-project/index.html +13 -0
  23. package/frontend-project/package.json +32 -0
  24. package/frontend-project/postcss.config.js +6 -0
  25. package/frontend-project/src/App.jsx +7 -0
  26. package/frontend-project/src/components/LoadingSpinner.jsx +8 -0
  27. package/frontend-project/src/components/Pagination.jsx +74 -0
  28. package/frontend-project/src/components/ProtectedRoute.jsx +22 -0
  29. package/frontend-project/src/components/SearchBar.jsx +16 -0
  30. package/frontend-project/src/components/StatCard.jsx +20 -0
  31. package/frontend-project/src/context/AuthContext.jsx +60 -0
  32. package/frontend-project/src/index.css +3 -0
  33. package/frontend-project/src/layouts/MainLayout.jsx +12 -0
  34. package/frontend-project/src/layouts/Sidebar.jsx +96 -0
  35. package/frontend-project/src/main.jsx +16 -0
  36. package/frontend-project/src/pages/Dashboard.jsx +184 -0
  37. package/frontend-project/src/pages/Login.jsx +111 -0
  38. package/frontend-project/src/pages/Products.jsx +192 -0
  39. package/frontend-project/src/pages/Reports.jsx +320 -0
  40. package/frontend-project/src/pages/Sales.jsx +150 -0
  41. package/frontend-project/src/pages/StockStatus.jsx +306 -0
  42. package/frontend-project/src/routes/AppRoutes.jsx +66 -0
  43. package/frontend-project/src/services/api.js +52 -0
  44. package/frontend-project/src/services/authService.js +5 -0
  45. package/frontend-project/src/services/productService.js +4 -0
  46. package/frontend-project/src/services/reportService.js +5 -0
  47. package/frontend-project/src/services/saleService.js +3 -0
  48. package/frontend-project/src/services/stockService.js +7 -0
  49. package/frontend-project/tailwind.config.js +8 -0
  50. package/frontend-project/vite.config.js +9 -0
  51. package/package.json +59 -0
@@ -0,0 +1,306 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useForm } from 'react-hook-form';
3
+ import Swal from 'sweetalert2';
4
+ import {
5
+ HiOutlinePlus,
6
+ HiOutlinePencil,
7
+ HiOutlineTrash,
8
+ } from 'react-icons/hi2';
9
+ import { getProducts } from '../services/productService';
10
+ import {
11
+ createStockStatus,
12
+ getStockStatuses,
13
+ updateStockStatus,
14
+ deleteStockStatus,
15
+ } from '../services/stockService';
16
+ import SearchBar from '../components/SearchBar';
17
+ import Pagination from '../components/Pagination';
18
+ import LoadingSpinner from '../components/LoadingSpinner';
19
+
20
+ export default function StockStatus() {
21
+ const [stockStatuses, setStockStatuses] = useState([]);
22
+ const [products, setProducts] = useState([]);
23
+ const [loading, setLoading] = useState(true);
24
+ const [search, setSearch] = useState('');
25
+ const [page, setPage] = useState(1);
26
+ const [pages, setPages] = useState(1);
27
+ const [showForm, setShowForm] = useState(false);
28
+ const [editing, setEditing] = useState(null);
29
+ const [submitting, setSubmitting] = useState(false);
30
+
31
+ const {
32
+ register,
33
+ handleSubmit,
34
+ reset,
35
+ setValue,
36
+ watch,
37
+ formState: { errors },
38
+ } = useForm();
39
+
40
+ const availableQuantity = watch('availableQuantity');
41
+ const soldQuantity = watch('soldQuantity');
42
+ const remainingQuantity =
43
+ parseInt(availableQuantity || 0) - parseInt(soldQuantity || 0);
44
+
45
+ const fetchData = async () => {
46
+ setLoading(true);
47
+ try {
48
+ const [stockRes, prodRes] = await Promise.all([
49
+ getStockStatuses({ search, page, limit: 10 }),
50
+ getProducts({ limit: 100 }),
51
+ ]);
52
+ setStockStatuses(stockRes.data.stockStatuses);
53
+ setPages(stockRes.data.pages);
54
+ setProducts(prodRes.data.products);
55
+ } catch {
56
+ // handled by interceptor
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ };
61
+
62
+ useEffect(() => {
63
+ fetchData();
64
+ }, [page, search]);
65
+
66
+ const onSubmit = async (data) => {
67
+ setSubmitting(true);
68
+ try {
69
+ if (editing) {
70
+ await updateStockStatus(editing._id, data);
71
+ Swal.fire({ icon: 'success', title: 'Updated!', timer: 1500, showConfirmButton: false, toast: true, position: 'top-end' });
72
+ } else {
73
+ await createStockStatus(data);
74
+ Swal.fire({ icon: 'success', title: 'Created!', timer: 1500, showConfirmButton: false, toast: true, position: 'top-end' });
75
+ }
76
+ reset();
77
+ setShowForm(false);
78
+ setEditing(null);
79
+ fetchData();
80
+ } catch {
81
+ // handled by interceptor
82
+ } finally {
83
+ setSubmitting(false);
84
+ }
85
+ };
86
+
87
+ const handleEdit = (item) => {
88
+ setEditing(item);
89
+ setValue('productId', item.productId?._id || item.productId);
90
+ setValue('availableQuantity', item.availableQuantity);
91
+ setValue('soldQuantity', item.soldQuantity);
92
+ setValue('generatedAt', item.generatedAt ? item.generatedAt.split('T')[0] : '');
93
+ setShowForm(true);
94
+ };
95
+
96
+ const handleDelete = async (id) => {
97
+ const result = await Swal.fire({
98
+ title: 'Are you sure?',
99
+ text: "You won't be able to revert this!",
100
+ icon: 'warning',
101
+ showCancelButton: true,
102
+ confirmButtonColor: '#1e293b',
103
+ cancelButtonColor: '#dc2626',
104
+ confirmButtonText: 'Yes, delete it!',
105
+ });
106
+
107
+ if (result.isConfirmed) {
108
+ try {
109
+ await deleteStockStatus(id);
110
+ Swal.fire({ icon: 'success', title: 'Deleted!', timer: 1500, showConfirmButton: false, toast: true, position: 'top-end' });
111
+ fetchData();
112
+ } catch {
113
+ // handled by interceptor
114
+ }
115
+ }
116
+ };
117
+
118
+ const cancelForm = () => {
119
+ reset();
120
+ setShowForm(false);
121
+ setEditing(null);
122
+ };
123
+
124
+ return (
125
+ <div className="space-y-6">
126
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
127
+ <div>
128
+ <h1 className="text-2xl font-bold text-slate-800">Stock Status</h1>
129
+ <p className="text-slate-500 text-sm mt-1">Monitor stock levels and inventory</p>
130
+ </div>
131
+ <button
132
+ onClick={() => setShowForm(true)}
133
+ className="flex items-center gap-2 px-4 py-2.5 bg-slate-800 text-white rounded-lg text-sm font-medium hover:bg-slate-700 transition-colors"
134
+ >
135
+ <HiOutlinePlus className="w-5 h-5" />
136
+ Add Stock Status
137
+ </button>
138
+ </div>
139
+
140
+ {showForm && (
141
+ <div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
142
+ <h3 className="text-sm font-semibold text-slate-700 mb-4">
143
+ {editing ? 'Edit Stock Status' : 'New Stock Status'}
144
+ </h3>
145
+ <form onSubmit={handleSubmit(onSubmit)} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
146
+ <div>
147
+ <label className="block text-sm font-medium text-slate-700 mb-1">Product</label>
148
+ <select
149
+ {...register('productId', { required: 'Product is required' })}
150
+ className="w-full px-4 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-800 focus:border-transparent bg-white"
151
+ disabled={!!editing}
152
+ >
153
+ <option value="">Select product</option>
154
+ {products.map((p) => (
155
+ <option key={p._id} value={p._id}>
156
+ {p.productName}
157
+ </option>
158
+ ))}
159
+ </select>
160
+ {errors.productId && <p className="text-red-500 text-xs mt-1">{errors.productId.message}</p>}
161
+ </div>
162
+ <div>
163
+ <label className="block text-sm font-medium text-slate-700 mb-1">Available Quantity</label>
164
+ <input
165
+ type="number"
166
+ step="1"
167
+ {...register('availableQuantity', {
168
+ required: 'Required',
169
+ min: { value: 0, message: 'Cannot be negative' },
170
+ })}
171
+ className="w-full px-4 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-800 focus:border-transparent"
172
+ />
173
+ {errors.availableQuantity && <p className="text-red-500 text-xs mt-1">{errors.availableQuantity.message}</p>}
174
+ </div>
175
+ <div>
176
+ <label className="block text-sm font-medium text-slate-700 mb-1">Sold Quantity</label>
177
+ <input
178
+ type="number"
179
+ step="1"
180
+ {...register('soldQuantity', {
181
+ required: 'Required',
182
+ min: { value: 0, message: 'Cannot be negative' },
183
+ })}
184
+ className="w-full px-4 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-800 focus:border-transparent"
185
+ />
186
+ {errors.soldQuantity && <p className="text-red-500 text-xs mt-1">{errors.soldQuantity.message}</p>}
187
+ </div>
188
+ <div>
189
+ <label className="block text-sm font-medium text-slate-700 mb-1">Remaining Quantity</label>
190
+ <input
191
+ value={remainingQuantity}
192
+ readOnly
193
+ className="w-full px-4 py-2.5 border border-gray-200 rounded-lg text-sm bg-slate-50 text-slate-500"
194
+ />
195
+ </div>
196
+ <div>
197
+ <label className="block text-sm font-medium text-slate-700 mb-1">Date</label>
198
+ <input
199
+ type="date"
200
+ {...register('generatedAt')}
201
+ className="w-full px-4 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-800 focus:border-transparent"
202
+ />
203
+ </div>
204
+ <div className="flex items-end gap-2">
205
+ <button
206
+ type="submit"
207
+ disabled={submitting}
208
+ className="px-6 py-2.5 bg-slate-800 text-white rounded-lg text-sm font-medium hover:bg-slate-700 transition-colors disabled:opacity-50"
209
+ >
210
+ {submitting ? 'Saving...' : editing ? 'Update' : 'Save'}
211
+ </button>
212
+ <button
213
+ type="button"
214
+ onClick={cancelForm}
215
+ className="px-6 py-2.5 border border-gray-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-50 transition-colors"
216
+ >
217
+ Cancel
218
+ </button>
219
+ </div>
220
+ </form>
221
+ </div>
222
+ )}
223
+
224
+ <div className="bg-white rounded-xl border border-gray-200 shadow-sm">
225
+ <div className="p-4 border-b border-gray-200">
226
+ <SearchBar value={search} onChange={(v) => { setSearch(v); setPage(1); }} placeholder="Search by product name..." />
227
+ </div>
228
+
229
+ {loading ? (
230
+ <LoadingSpinner message="Loading stock status..." />
231
+ ) : (
232
+ <>
233
+ <div className="overflow-x-auto">
234
+ <table className="w-full text-sm">
235
+ <thead>
236
+ <tr className="border-b border-gray-200 bg-slate-50">
237
+ <th className="text-left p-4 text-slate-500 font-medium">Product</th>
238
+ <th className="text-left p-4 text-slate-500 font-medium">Stored Qty</th>
239
+ <th className="text-left p-4 text-slate-500 font-medium">Sold Qty</th>
240
+ <th className="text-left p-4 text-slate-500 font-medium">Remaining</th>
241
+ <th className="text-left p-4 text-slate-500 font-medium">Date</th>
242
+ <th className="text-left p-4 text-slate-500 font-medium">Actions</th>
243
+ </tr>
244
+ </thead>
245
+ <tbody>
246
+ {stockStatuses.length > 0 ? (
247
+ stockStatuses.map((item) => (
248
+ <tr key={item._id} className="border-b border-gray-100 hover:bg-slate-50">
249
+ <td className="p-4 font-medium text-slate-700">
250
+ {item.productId?.productName || 'N/A'}
251
+ </td>
252
+ <td className="p-4 text-slate-600">{item.availableQuantity}</td>
253
+ <td className="p-4 text-slate-600">{item.soldQuantity}</td>
254
+ <td className="p-4">
255
+ <span
256
+ className={`font-medium ${
257
+ item.remainingQuantity <= 0
258
+ ? 'text-red-600'
259
+ : item.remainingQuantity < 10
260
+ ? 'text-amber-600'
261
+ : 'text-green-600'
262
+ }`}
263
+ >
264
+ {item.remainingQuantity}
265
+ </span>
266
+ </td>
267
+ <td className="p-4 text-slate-500">
268
+ {new Date(item.generatedAt).toLocaleDateString()}
269
+ </td>
270
+ <td className="p-4">
271
+ <div className="flex items-center gap-2">
272
+ <button
273
+ onClick={() => handleEdit(item)}
274
+ className="p-1.5 text-slate-500 hover:text-slate-800 hover:bg-slate-100 rounded-lg transition-colors"
275
+ >
276
+ <HiOutlinePencil className="w-4 h-4" />
277
+ </button>
278
+ <button
279
+ onClick={() => handleDelete(item._id)}
280
+ className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
281
+ >
282
+ <HiOutlineTrash className="w-4 h-4" />
283
+ </button>
284
+ </div>
285
+ </td>
286
+ </tr>
287
+ ))
288
+ ) : (
289
+ <tr>
290
+ <td colSpan={6} className="p-8 text-center text-slate-400">
291
+ No stock status records found
292
+ </td>
293
+ </tr>
294
+ )}
295
+ </tbody>
296
+ </table>
297
+ </div>
298
+ <div className="p-4">
299
+ <Pagination page={page} pages={pages} onPageChange={setPage} />
300
+ </div>
301
+ </>
302
+ )}
303
+ </div>
304
+ </div>
305
+ );
306
+ }
@@ -0,0 +1,66 @@
1
+ import { Routes, Route, Navigate } from 'react-router-dom';
2
+ import { useAuth } from '../context/AuthContext';
3
+ import ProtectedRoute from '../components/ProtectedRoute';
4
+ import Login from '../pages/Login';
5
+ import Dashboard from '../pages/Dashboard';
6
+ import Products from '../pages/Products';
7
+ import Sales from '../pages/Sales';
8
+ import StockStatus from '../pages/StockStatus';
9
+ import Reports from '../pages/Reports';
10
+
11
+ export default function AppRoutes() {
12
+ const { user, loading } = useAuth();
13
+
14
+ if (loading) return null;
15
+
16
+ return (
17
+ <Routes>
18
+ <Route
19
+ path="/login"
20
+ element={user ? <Navigate to="/dashboard" replace /> : <Login />}
21
+ />
22
+ <Route
23
+ path="/dashboard"
24
+ element={
25
+ <ProtectedRoute>
26
+ <Dashboard />
27
+ </ProtectedRoute>
28
+ }
29
+ />
30
+ <Route
31
+ path="/products"
32
+ element={
33
+ <ProtectedRoute>
34
+ <Products />
35
+ </ProtectedRoute>
36
+ }
37
+ />
38
+ <Route
39
+ path="/sales"
40
+ element={
41
+ <ProtectedRoute>
42
+ <Sales />
43
+ </ProtectedRoute>
44
+ }
45
+ />
46
+ <Route
47
+ path="/stock-status"
48
+ element={
49
+ <ProtectedRoute>
50
+ <StockStatus />
51
+ </ProtectedRoute>
52
+ }
53
+ />
54
+ <Route
55
+ path="/reports"
56
+ element={
57
+ <ProtectedRoute>
58
+ <Reports />
59
+ </ProtectedRoute>
60
+ }
61
+ />
62
+ <Route path="/" element={<Navigate to="/dashboard" replace />} />
63
+ <Route path="*" element={<Navigate to="/dashboard" replace />} />
64
+ </Routes>
65
+ );
66
+ }
@@ -0,0 +1,52 @@
1
+ import axios from 'axios';
2
+ import Swal from 'sweetalert2';
3
+
4
+ const api = axios.create({
5
+ baseURL: 'http://localhost:5000/api',
6
+ withCredentials: true,
7
+ headers: {
8
+ 'Content-Type': 'application/json',
9
+ },
10
+ });
11
+
12
+ api.interceptors.response.use(
13
+ (response) => response,
14
+ (error) => {
15
+ if (error.response) {
16
+ if (error.response.status === 401) {
17
+ const isAuthPage = window.location.pathname === '/login';
18
+ if (!isAuthPage) {
19
+ window.location.href = '/login';
20
+ }
21
+ }
22
+
23
+ const message =
24
+ error.response.data?.message || 'An unexpected error occurred';
25
+
26
+ if (error.response.status !== 401) {
27
+ Swal.fire({
28
+ icon: 'error',
29
+ title: 'Error',
30
+ text: message,
31
+ toast: true,
32
+ position: 'top-end',
33
+ timer: 3000,
34
+ showConfirmButton: false,
35
+ });
36
+ }
37
+ } else if (error.request) {
38
+ Swal.fire({
39
+ icon: 'error',
40
+ title: 'Connection Error',
41
+ text: 'Unable to connect to the server. Please check your connection.',
42
+ toast: true,
43
+ position: 'top-end',
44
+ timer: 3000,
45
+ showConfirmButton: false,
46
+ });
47
+ }
48
+ return Promise.reject(error);
49
+ }
50
+ );
51
+
52
+ export default api;
@@ -0,0 +1,5 @@
1
+ import api from './api';
2
+
3
+ export const loginUser = (data) => api.post('/auth/login', data);
4
+ export const logoutUser = () => api.post('/auth/logout');
5
+ export const checkSession = () => api.get('/auth/session');
@@ -0,0 +1,4 @@
1
+ import api from './api';
2
+
3
+ export const createProduct = (data) => api.post('/products', data);
4
+ export const getProducts = (params) => api.get('/products', { params });
@@ -0,0 +1,5 @@
1
+ import api from './api';
2
+
3
+ export const getDashboardData = () => api.get('/reports/dashboard');
4
+ export const getDailySalesReport = (params) => api.get('/reports/daily-sales', { params });
5
+ export const getStockStatusReport = (params) => api.get('/reports/stock-status', { params });
@@ -0,0 +1,3 @@
1
+ import api from './api';
2
+
3
+ export const createSale = (data) => api.post('/sales', data);
@@ -0,0 +1,7 @@
1
+ import api from './api';
2
+
3
+ export const createStockStatus = (data) => api.post('/stock', data);
4
+ export const getStockStatuses = (params) => api.get('/stock', { params });
5
+ export const getStockStatus = (id) => api.get(`/stock/${id}`);
6
+ export const updateStockStatus = (id, data) => api.put(`/stock/${id}`, data);
7
+ export const deleteStockStatus = (id) => api.delete(`/stock/${id}`);
@@ -0,0 +1,8 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4
+ theme: {
5
+ extend: {},
6
+ },
7
+ plugins: [],
8
+ };
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ },
9
+ });
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "react-node-app",
3
+ "version": "1.0.0",
4
+ "description": "Business Web Solution (BWS) for DAB Enterprise Ltd - Full-stack inventory, sales, and stock management system with React frontend and Express/MongoDB backend",
5
+ "keywords": [
6
+ "bws",
7
+ "dab-enterprise",
8
+ "inventory",
9
+ "sales",
10
+ "stock-management",
11
+ "business-solution",
12
+ "react",
13
+ "express",
14
+ "mongodb"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "DAB Enterprise Ltd",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/ishconnlab/DABEnterprise.git"
21
+ },
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ },
25
+ "files": [
26
+ "backend-project/server.js",
27
+ "backend-project/package.json",
28
+ "backend-project/config/",
29
+ "backend-project/controllers/",
30
+ "backend-project/middleware/",
31
+ "backend-project/models/",
32
+ "backend-project/routes/",
33
+ "backend-project/utils/",
34
+ "frontend-project/src/",
35
+ "frontend-project/public/",
36
+ "frontend-project/index.html",
37
+ "frontend-project/package.json",
38
+ "frontend-project/vite.config.js",
39
+ "frontend-project/postcss.config.js",
40
+ "frontend-project/tailwind.config.js",
41
+ "README.md",
42
+ ".gitignore"
43
+ ],
44
+ "scripts": {
45
+ "install:backend": "cd backend-project && npm install",
46
+ "install:frontend": "cd frontend-project && npm install",
47
+ "postinstall": "",
48
+ "dev:backend": "cd backend-project && npm run dev",
49
+ "dev:frontend": "cd frontend-project && npm run dev",
50
+ "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
51
+ "build": "cd frontend-project && npm run build",
52
+ "start:backend": "cd backend-project && npm start",
53
+ "seed": "cd backend-project && npm run seed",
54
+ "start": "npm run seed && npm run start:backend"
55
+ },
56
+ "devDependencies": {
57
+ "concurrently": "^8.2.2"
58
+ }
59
+ }