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,184 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ HiOutlineCube,
4
+ HiOutlineShoppingCart,
5
+ HiOutlineBanknotes,
6
+ HiOutlineArchiveBox,
7
+ } from 'react-icons/hi2';
8
+ import {
9
+ BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend,
10
+ } from 'recharts';
11
+ import StatCard from '../components/StatCard';
12
+ import LoadingSpinner from '../components/LoadingSpinner';
13
+ import { getDashboardData } from '../services/reportService';
14
+
15
+ const COLORS = ['#475569', '#94a3b8', '#64748b', '#cbd5e1', '#334155'];
16
+
17
+ export default function Dashboard() {
18
+ const [data, setData] = useState(null);
19
+ const [loading, setLoading] = useState(true);
20
+
21
+ useEffect(() => {
22
+ const fetchData = async () => {
23
+ try {
24
+ const res = await getDashboardData();
25
+ setData(res.data);
26
+ } catch {
27
+ // Error handled by interceptor
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ };
32
+ fetchData();
33
+ }, []);
34
+
35
+ if (loading) return <LoadingSpinner message="Loading dashboard..." />;
36
+
37
+ const salesChartData = (data?.dailySales || []).map((sale) => ({
38
+ name: sale.productId?.productName || 'Unknown',
39
+ amount: sale.soldTotalPrice,
40
+ }));
41
+
42
+ const categoryChartData = (data?.categoryData || []).map((cat) => ({
43
+ name: cat._id,
44
+ value: cat.count,
45
+ }));
46
+
47
+ const formatRwf = (amount) =>
48
+ new Intl.NumberFormat('en-US', {
49
+ style: 'currency',
50
+ currency: 'RWF',
51
+ minimumFractionDigits: 0,
52
+ }).format(amount);
53
+
54
+ return (
55
+ <div className="space-y-6">
56
+ <div>
57
+ <h1 className="text-2xl font-bold text-slate-800">Dashboard</h1>
58
+ <p className="text-slate-500 text-sm mt-1">Overview of your business</p>
59
+ </div>
60
+
61
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
62
+ <StatCard
63
+ title="Total Products"
64
+ value={data?.totalProducts || 0}
65
+ icon={HiOutlineCube}
66
+ />
67
+ <StatCard
68
+ title="Total Sales"
69
+ value={data?.totalSales || 0}
70
+ icon={HiOutlineShoppingCart}
71
+ />
72
+ <StatCard
73
+ title="Total Revenue"
74
+ value={formatRwf(data?.totalRevenue || 0)}
75
+ icon={HiOutlineBanknotes}
76
+ />
77
+ <StatCard
78
+ title="Available Stock"
79
+ value={data?.availableStock || 0}
80
+ icon={HiOutlineArchiveBox}
81
+ />
82
+ </div>
83
+
84
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
85
+ <div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
86
+ <h3 className="text-sm font-semibold text-slate-700 mb-4">Daily Sales</h3>
87
+ {salesChartData.length > 0 ? (
88
+ <ResponsiveContainer width="100%" height={300}>
89
+ <BarChart data={salesChartData}>
90
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
91
+ <XAxis dataKey="name" tick={{ fontSize: 12 }} stroke="#94a3b8" />
92
+ <YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" />
93
+ <Tooltip
94
+ contentStyle={{
95
+ backgroundColor: '#fff',
96
+ border: '1px solid #e2e8f0',
97
+ borderRadius: '8px',
98
+ }}
99
+ />
100
+ <Bar dataKey="amount" fill="#475569" radius={[4, 4, 0, 0]} />
101
+ </BarChart>
102
+ </ResponsiveContainer>
103
+ ) : (
104
+ <p className="text-slate-400 text-sm text-center py-12">No sales data for today</p>
105
+ )}
106
+ </div>
107
+
108
+ <div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
109
+ <h3 className="text-sm font-semibold text-slate-700 mb-4">Product Categories</h3>
110
+ {categoryChartData.length > 0 ? (
111
+ <ResponsiveContainer width="100%" height={300}>
112
+ <PieChart>
113
+ <Pie
114
+ data={categoryChartData}
115
+ cx="50%"
116
+ cy="50%"
117
+ outerRadius={100}
118
+ dataKey="value"
119
+ label={({ name, percent }) =>
120
+ `${name} ${(percent * 100).toFixed(0)}%`
121
+ }
122
+ >
123
+ {categoryChartData.map((_, index) => (
124
+ <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
125
+ ))}
126
+ </Pie>
127
+ <Tooltip />
128
+ <Legend />
129
+ </PieChart>
130
+ </ResponsiveContainer>
131
+ ) : (
132
+ <p className="text-slate-400 text-sm text-center py-12">No categories found</p>
133
+ )}
134
+ </div>
135
+ </div>
136
+
137
+ <div className="bg-white rounded-xl border border-gray-200 shadow-sm">
138
+ <div className="p-6 border-b border-gray-200">
139
+ <h3 className="text-sm font-semibold text-slate-700">Recent Sales</h3>
140
+ </div>
141
+ <div className="overflow-x-auto">
142
+ <table className="w-full text-sm">
143
+ <thead>
144
+ <tr className="border-b border-gray-200">
145
+ <th className="text-left p-4 text-slate-500 font-medium">Product</th>
146
+ <th className="text-left p-4 text-slate-500 font-medium">Qty</th>
147
+ <th className="text-left p-4 text-slate-500 font-medium">Price</th>
148
+ <th className="text-left p-4 text-slate-500 font-medium">Total</th>
149
+ <th className="text-left p-4 text-slate-500 font-medium">Date</th>
150
+ </tr>
151
+ </thead>
152
+ <tbody>
153
+ {(data?.dailySales || []).length > 0 ? (
154
+ data.dailySales.map((sale) => (
155
+ <tr key={sale._id} className="border-b border-gray-100 hover:bg-slate-50">
156
+ <td className="p-4 font-medium text-slate-700">
157
+ {sale.productId?.productName || 'N/A'}
158
+ </td>
159
+ <td className="p-4 text-slate-600">{sale.soldQuantity}</td>
160
+ <td className="p-4 text-slate-600">
161
+ {formatRwf(sale.soldUnitPrice)}
162
+ </td>
163
+ <td className="p-4 text-slate-600">
164
+ {formatRwf(sale.soldTotalPrice)}
165
+ </td>
166
+ <td className="p-4 text-slate-500">
167
+ {new Date(sale.salesDate).toLocaleDateString()}
168
+ </td>
169
+ </tr>
170
+ ))
171
+ ) : (
172
+ <tr>
173
+ <td colSpan={5} className="p-8 text-center text-slate-400">
174
+ No sales recorded today
175
+ </td>
176
+ </tr>
177
+ )}
178
+ </tbody>
179
+ </table>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,111 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+ import { HiOutlineEye, HiOutlineEyeSlash } from 'react-icons/hi2';
5
+
6
+ export default function Login() {
7
+ const [username, setUsername] = useState('');
8
+ const [password, setPassword] = useState('');
9
+ const [showPassword, setShowPassword] = useState(false);
10
+ const [loading, setLoading] = useState(false);
11
+ const [error, setError] = useState('');
12
+ const { login, user } = useAuth();
13
+ const navigate = useNavigate();
14
+
15
+ useEffect(() => {
16
+ if (user) {
17
+ navigate('/dashboard', { replace: true });
18
+ }
19
+ }, [user, navigate]);
20
+
21
+ const handleSubmit = async (e) => {
22
+ e.preventDefault();
23
+ setError('');
24
+
25
+ if (!username || !password) {
26
+ setError('Please enter both username and password');
27
+ return;
28
+ }
29
+
30
+ setLoading(true);
31
+ try {
32
+ await login(username, password);
33
+ navigate('/dashboard', { replace: true });
34
+ } catch (err) {
35
+ setError(err.response?.data?.message || 'Login failed. Please try again.');
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ };
40
+
41
+ return (
42
+ <div className="min-h-screen bg-slate-50 flex items-center justify-center p-4">
43
+ <div className="w-full max-w-md">
44
+ <div className="text-center mb-8">
45
+ <h1 className="text-2xl font-bold text-slate-800">DAB Enterprise Ltd</h1>
46
+ <p className="text-slate-500 mt-1">Business Web Solution</p>
47
+ </div>
48
+
49
+ <div className="bg-white rounded-xl border border-gray-200 shadow-sm p-8">
50
+ <h2 className="text-lg font-semibold text-slate-800 mb-6">Sign In</h2>
51
+
52
+ {error && (
53
+ <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-600">
54
+ {error}
55
+ </div>
56
+ )}
57
+
58
+ <form onSubmit={handleSubmit} className="space-y-4">
59
+ <div>
60
+ <label className="block text-sm font-medium text-slate-700 mb-1">
61
+ Username
62
+ </label>
63
+ <input
64
+ type="text"
65
+ value={username}
66
+ onChange={(e) => setUsername(e.target.value)}
67
+ 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"
68
+ placeholder="Enter your username"
69
+ autoFocus
70
+ />
71
+ </div>
72
+
73
+ <div>
74
+ <label className="block text-sm font-medium text-slate-700 mb-1">
75
+ Password
76
+ </label>
77
+ <div className="relative">
78
+ <input
79
+ type={showPassword ? 'text' : 'password'}
80
+ value={password}
81
+ onChange={(e) => setPassword(e.target.value)}
82
+ className="w-full px-4 py-2.5 pr-10 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-800 focus:border-transparent bg-white"
83
+ placeholder="Enter your password"
84
+ />
85
+ <button
86
+ type="button"
87
+ onClick={() => setShowPassword(!showPassword)}
88
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
89
+ >
90
+ {showPassword ? (
91
+ <HiOutlineEyeSlash className="w-5 h-5" />
92
+ ) : (
93
+ <HiOutlineEye className="w-5 h-5" />
94
+ )}
95
+ </button>
96
+ </div>
97
+ </div>
98
+
99
+ <button
100
+ type="submit"
101
+ disabled={loading}
102
+ className="w-full py-2.5 bg-slate-800 text-white rounded-lg text-sm font-medium hover:bg-slate-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
103
+ >
104
+ {loading ? 'Signing in...' : 'Sign In'}
105
+ </button>
106
+ </form>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,192 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useForm } from 'react-hook-form';
3
+ import Swal from 'sweetalert2';
4
+ import { createProduct, getProducts } from '../services/productService';
5
+ import LoadingSpinner from '../components/LoadingSpinner';
6
+
7
+ export default function Products() {
8
+ const [products, setProducts] = useState([]);
9
+ const [loading, setLoading] = useState(true);
10
+ const [submitting, setSubmitting] = useState(false);
11
+ const [showForm, setShowForm] = useState(false);
12
+
13
+ const {
14
+ register,
15
+ handleSubmit,
16
+ reset,
17
+ watch,
18
+ formState: { errors },
19
+ } = useForm();
20
+
21
+ const quantity = watch('quantity');
22
+ const unitPrice = watch('unitPrice');
23
+ const totalPrice = (parseFloat(quantity || 0) * parseFloat(unitPrice || 0)).toFixed(2);
24
+
25
+ const fetchProducts = async () => {
26
+ setLoading(true);
27
+ try {
28
+ const res = await getProducts({ limit: 100 });
29
+ setProducts(res.data.products);
30
+ } catch {
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ useEffect(() => {
37
+ fetchProducts();
38
+ }, []);
39
+
40
+ const onSubmit = async (data) => {
41
+ setSubmitting(true);
42
+ try {
43
+ await createProduct(data);
44
+ Swal.fire({ icon: 'success', title: 'Created!', text: 'Product created successfully', timer: 1500, showConfirmButton: false, toast: true, position: 'top-end' });
45
+ reset();
46
+ setShowForm(false);
47
+ fetchProducts();
48
+ } catch {
49
+ } finally {
50
+ setSubmitting(false);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <div className="space-y-6">
56
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
57
+ <div>
58
+ <h1 className="text-2xl font-bold text-slate-800">Products</h1>
59
+ <p className="text-slate-500 text-sm mt-1">Register new products</p>
60
+ </div>
61
+ {!showForm && (
62
+ <button
63
+ onClick={() => setShowForm(true)}
64
+ 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"
65
+ >
66
+ Add Product
67
+ </button>
68
+ )}
69
+ </div>
70
+
71
+ {showForm && (
72
+ <div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
73
+ <h3 className="text-sm font-semibold text-slate-700 mb-4">New Product</h3>
74
+ <form onSubmit={handleSubmit(onSubmit)} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
75
+ <div>
76
+ <label className="block text-sm font-medium text-slate-700 mb-1">Product Name</label>
77
+ <input
78
+ {...register('productName', { required: 'Product name is required' })}
79
+ 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"
80
+ />
81
+ {errors.productName && <p className="text-red-500 text-xs mt-1">{errors.productName.message}</p>}
82
+ </div>
83
+ <div>
84
+ <label className="block text-sm font-medium text-slate-700 mb-1">Category</label>
85
+ <input
86
+ {...register('category', { required: 'Category is required' })}
87
+ 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"
88
+ />
89
+ {errors.category && <p className="text-red-500 text-xs mt-1">{errors.category.message}</p>}
90
+ </div>
91
+ <div>
92
+ <label className="block text-sm font-medium text-slate-700 mb-1">Quantity</label>
93
+ <input
94
+ type="number"
95
+ step="1"
96
+ {...register('quantity', {
97
+ required: 'Quantity is required',
98
+ min: { value: 0, message: 'Quantity cannot be negative' },
99
+ })}
100
+ 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"
101
+ />
102
+ {errors.quantity && <p className="text-red-500 text-xs mt-1">{errors.quantity.message}</p>}
103
+ </div>
104
+ <div>
105
+ <label className="block text-sm font-medium text-slate-700 mb-1">Unit Price (RWF)</label>
106
+ <input
107
+ type="number"
108
+ step="0.01"
109
+ {...register('unitPrice', {
110
+ required: 'Unit price is required',
111
+ min: { value: 0.01, message: 'Price must be > 0' },
112
+ })}
113
+ 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"
114
+ />
115
+ {errors.unitPrice && <p className="text-red-500 text-xs mt-1">{errors.unitPrice.message}</p>}
116
+ </div>
117
+ <div>
118
+ <label className="block text-sm font-medium text-slate-700 mb-1">Total Price</label>
119
+ <input
120
+ value={totalPrice}
121
+ readOnly
122
+ className="w-full px-4 py-2.5 border border-gray-200 rounded-lg text-sm bg-slate-50 text-slate-500"
123
+ />
124
+ </div>
125
+ <div className="flex items-end gap-2">
126
+ <button
127
+ type="submit"
128
+ disabled={submitting}
129
+ 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"
130
+ >
131
+ {submitting ? 'Saving...' : 'Save'}
132
+ </button>
133
+ <button
134
+ type="button"
135
+ onClick={() => { reset(); setShowForm(false); }}
136
+ 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"
137
+ >
138
+ Cancel
139
+ </button>
140
+ </div>
141
+ </form>
142
+ </div>
143
+ )}
144
+
145
+ <div className="bg-white rounded-xl border border-gray-200 shadow-sm">
146
+ <div className="p-4 border-b border-gray-200">
147
+ <h3 className="text-sm font-semibold text-slate-700">Registered Products</h3>
148
+ </div>
149
+ {loading ? (
150
+ <LoadingSpinner message="Loading products..." />
151
+ ) : (
152
+ <div className="overflow-x-auto">
153
+ <table className="w-full text-sm">
154
+ <thead>
155
+ <tr className="border-b border-gray-200 bg-slate-50">
156
+ <th className="text-left p-4 text-slate-500 font-medium">Name</th>
157
+ <th className="text-left p-4 text-slate-500 font-medium">Category</th>
158
+ <th className="text-left p-4 text-slate-500 font-medium">Qty</th>
159
+ <th className="text-left p-4 text-slate-500 font-medium">Unit Price</th>
160
+ <th className="text-left p-4 text-slate-500 font-medium">Total</th>
161
+ </tr>
162
+ </thead>
163
+ <tbody>
164
+ {products.length > 0 ? (
165
+ products.map((product) => (
166
+ <tr key={product._id} className="border-b border-gray-100 hover:bg-slate-50">
167
+ <td className="p-4 font-medium text-slate-700">{product.productName}</td>
168
+ <td className="p-4 text-slate-600">{product.category}</td>
169
+ <td className="p-4 text-slate-600">{product.quantity}</td>
170
+ <td className="p-4 text-slate-600">
171
+ {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'RWF', minimumFractionDigits: 0 }).format(product.unitPrice)}
172
+ </td>
173
+ <td className="p-4 text-slate-600">
174
+ {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'RWF', minimumFractionDigits: 0 }).format(product.totalPrice)}
175
+ </td>
176
+ </tr>
177
+ ))
178
+ ) : (
179
+ <tr>
180
+ <td colSpan={5} className="p-8 text-center text-slate-400">
181
+ No products found
182
+ </td>
183
+ </tr>
184
+ )}
185
+ </tbody>
186
+ </table>
187
+ </div>
188
+ )}
189
+ </div>
190
+ </div>
191
+ );
192
+ }