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.
- package/.gitignore +6 -0
- package/README.md +0 -0
- package/backend-project/config/db.js +13 -0
- package/backend-project/controllers/authController.js +63 -0
- package/backend-project/controllers/productController.js +113 -0
- package/backend-project/controllers/reportController.js +93 -0
- package/backend-project/controllers/saleController.js +91 -0
- package/backend-project/controllers/stockController.js +126 -0
- package/backend-project/middleware/auth.js +8 -0
- package/backend-project/models/Product.js +40 -0
- package/backend-project/models/Sale.js +38 -0
- package/backend-project/models/StockStatus.js +35 -0
- package/backend-project/models/User.js +24 -0
- package/backend-project/package.json +24 -0
- package/backend-project/routes/authRoutes.js +9 -0
- package/backend-project/routes/productRoutes.js +9 -0
- package/backend-project/routes/reportRoutes.js +14 -0
- package/backend-project/routes/saleRoutes.js +8 -0
- package/backend-project/routes/stockRoutes.js +18 -0
- package/backend-project/server.js +55 -0
- package/backend-project/utils/seed.js +44 -0
- package/frontend-project/index.html +13 -0
- package/frontend-project/package.json +32 -0
- package/frontend-project/postcss.config.js +6 -0
- package/frontend-project/src/App.jsx +7 -0
- package/frontend-project/src/components/LoadingSpinner.jsx +8 -0
- package/frontend-project/src/components/Pagination.jsx +74 -0
- package/frontend-project/src/components/ProtectedRoute.jsx +22 -0
- package/frontend-project/src/components/SearchBar.jsx +16 -0
- package/frontend-project/src/components/StatCard.jsx +20 -0
- package/frontend-project/src/context/AuthContext.jsx +60 -0
- package/frontend-project/src/index.css +3 -0
- package/frontend-project/src/layouts/MainLayout.jsx +12 -0
- package/frontend-project/src/layouts/Sidebar.jsx +96 -0
- package/frontend-project/src/main.jsx +16 -0
- package/frontend-project/src/pages/Dashboard.jsx +184 -0
- package/frontend-project/src/pages/Login.jsx +111 -0
- package/frontend-project/src/pages/Products.jsx +192 -0
- package/frontend-project/src/pages/Reports.jsx +320 -0
- package/frontend-project/src/pages/Sales.jsx +150 -0
- package/frontend-project/src/pages/StockStatus.jsx +306 -0
- package/frontend-project/src/routes/AppRoutes.jsx +66 -0
- package/frontend-project/src/services/api.js +52 -0
- package/frontend-project/src/services/authService.js +5 -0
- package/frontend-project/src/services/productService.js +4 -0
- package/frontend-project/src/services/reportService.js +5 -0
- package/frontend-project/src/services/saleService.js +3 -0
- package/frontend-project/src/services/stockService.js +7 -0
- package/frontend-project/tailwind.config.js +8 -0
- package/frontend-project/vite.config.js +9 -0
- package/package.json +59 -0
package/.gitignore
ADDED
package/README.md
ADDED
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const connectDB = async () => {
|
|
4
|
+
try {
|
|
5
|
+
const conn = await mongoose.connect(process.env.MONGO_URI);
|
|
6
|
+
console.log(`MongoDB Connected: ${conn.connection.host}`);
|
|
7
|
+
} catch (error) {
|
|
8
|
+
console.error(`MongoDB Connection Error: ${error.message}`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports = connectDB;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
|
+
const User = require('../models/User');
|
|
3
|
+
|
|
4
|
+
exports.login = async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const { username, password } = req.body;
|
|
7
|
+
|
|
8
|
+
if (!username || !password) {
|
|
9
|
+
return res.status(400).json({ message: 'Username and password are required' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const user = await User.findOne({ username });
|
|
13
|
+
if (!user) {
|
|
14
|
+
return res.status(401).json({ message: 'Invalid username or password' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const isMatch = await bcrypt.compare(password, user.password);
|
|
18
|
+
if (!isMatch) {
|
|
19
|
+
return res.status(401).json({ message: 'Invalid username or password' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
req.session.userId = user._id;
|
|
23
|
+
req.session.role = user.role;
|
|
24
|
+
req.session.username = user.username;
|
|
25
|
+
|
|
26
|
+
res.json({
|
|
27
|
+
message: 'Login successful',
|
|
28
|
+
user: {
|
|
29
|
+
id: user._id,
|
|
30
|
+
username: user.username,
|
|
31
|
+
role: user.role,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Login error:', error);
|
|
36
|
+
res.status(500).json({ message: 'Server error' });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
exports.logout = (req, res) => {
|
|
41
|
+
req.session.destroy((err) => {
|
|
42
|
+
if (err) {
|
|
43
|
+
return res.status(500).json({ message: 'Logout failed' });
|
|
44
|
+
}
|
|
45
|
+
res.clearCookie('connect.sid');
|
|
46
|
+
res.json({ message: 'Logout successful' });
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
exports.checkSession = (req, res) => {
|
|
51
|
+
if (req.session && req.session.userId) {
|
|
52
|
+
res.json({
|
|
53
|
+
authenticated: true,
|
|
54
|
+
user: {
|
|
55
|
+
id: req.session.userId,
|
|
56
|
+
username: req.session.username,
|
|
57
|
+
role: req.session.role,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
res.json({ authenticated: false });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const Product = require('../models/Product');
|
|
2
|
+
|
|
3
|
+
exports.createProduct = async (req, res) => {
|
|
4
|
+
try {
|
|
5
|
+
const { productName, category, quantity, unitPrice } = req.body;
|
|
6
|
+
|
|
7
|
+
const product = new Product({
|
|
8
|
+
productName,
|
|
9
|
+
category,
|
|
10
|
+
quantity,
|
|
11
|
+
unitPrice,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const saved = await product.save();
|
|
15
|
+
res.status(201).json(saved);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error.name === 'ValidationError') {
|
|
18
|
+
const messages = Object.values(error.errors).map((e) => e.message);
|
|
19
|
+
return res.status(400).json({ message: messages.join(', ') });
|
|
20
|
+
}
|
|
21
|
+
console.error('Create product error:', error);
|
|
22
|
+
res.status(500).json({ message: 'Server error' });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
exports.getProducts = async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const { search, page = 1, limit = 10 } = req.query;
|
|
29
|
+
const query = {};
|
|
30
|
+
|
|
31
|
+
if (search) {
|
|
32
|
+
query.$or = [
|
|
33
|
+
{ productName: { $regex: search, $options: 'i' } },
|
|
34
|
+
{ category: { $regex: search, $options: 'i' } },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const total = await Product.countDocuments(query);
|
|
39
|
+
const products = await Product.find(query)
|
|
40
|
+
.sort({ createdAt: -1 })
|
|
41
|
+
.skip((page - 1) * limit)
|
|
42
|
+
.limit(parseInt(limit));
|
|
43
|
+
|
|
44
|
+
res.json({
|
|
45
|
+
products,
|
|
46
|
+
total,
|
|
47
|
+
page: parseInt(page),
|
|
48
|
+
pages: Math.ceil(total / limit),
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Get products error:', error);
|
|
52
|
+
res.status(500).json({ message: 'Server error' });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
exports.getProduct = async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const product = await Product.findById(req.params.id);
|
|
59
|
+
if (!product) {
|
|
60
|
+
return res.status(404).json({ message: 'Product not found' });
|
|
61
|
+
}
|
|
62
|
+
res.json(product);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Get product error:', error);
|
|
65
|
+
res.status(500).json({ message: 'Server error' });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
exports.updateProduct = async (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const { productName, category, quantity, unitPrice } = req.body;
|
|
72
|
+
|
|
73
|
+
const updateData = {};
|
|
74
|
+
if (productName !== undefined) updateData.productName = productName;
|
|
75
|
+
if (category !== undefined) updateData.category = category;
|
|
76
|
+
if (quantity !== undefined) updateData.quantity = quantity;
|
|
77
|
+
if (unitPrice !== undefined) updateData.unitPrice = unitPrice;
|
|
78
|
+
if (quantity !== undefined || unitPrice !== undefined) {
|
|
79
|
+
const existing = await Product.findById(req.params.id);
|
|
80
|
+
updateData.totalPrice = (quantity ?? existing.quantity) * (unitPrice ?? existing.unitPrice);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const product = await Product.findByIdAndUpdate(req.params.id, updateData, {
|
|
84
|
+
new: true,
|
|
85
|
+
runValidators: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!product) {
|
|
89
|
+
return res.status(404).json({ message: 'Product not found' });
|
|
90
|
+
}
|
|
91
|
+
res.json(product);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error.name === 'ValidationError') {
|
|
94
|
+
const messages = Object.values(error.errors).map((e) => e.message);
|
|
95
|
+
return res.status(400).json({ message: messages.join(', ') });
|
|
96
|
+
}
|
|
97
|
+
console.error('Update product error:', error);
|
|
98
|
+
res.status(500).json({ message: 'Server error' });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
exports.deleteProduct = async (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const product = await Product.findByIdAndDelete(req.params.id);
|
|
105
|
+
if (!product) {
|
|
106
|
+
return res.status(404).json({ message: 'Product not found' });
|
|
107
|
+
}
|
|
108
|
+
res.json({ message: 'Product deleted successfully' });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Delete product error:', error);
|
|
111
|
+
res.status(500).json({ message: 'Server error' });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const Sale = require('../models/Sale');
|
|
2
|
+
const StockStatus = require('../models/StockStatus');
|
|
3
|
+
const Product = require('../models/Product');
|
|
4
|
+
|
|
5
|
+
exports.getDailySalesReport = async (req, res) => {
|
|
6
|
+
try {
|
|
7
|
+
const { date, startDate, endDate } = req.query;
|
|
8
|
+
const query = {};
|
|
9
|
+
|
|
10
|
+
if (date) {
|
|
11
|
+
const start = new Date(date);
|
|
12
|
+
start.setHours(0, 0, 0, 0);
|
|
13
|
+
const end = new Date(date);
|
|
14
|
+
end.setHours(23, 59, 59, 999);
|
|
15
|
+
query.salesDate = { $gte: start, $lte: end };
|
|
16
|
+
} else if (startDate && endDate) {
|
|
17
|
+
const start = new Date(startDate);
|
|
18
|
+
start.setHours(0, 0, 0, 0);
|
|
19
|
+
const end = new Date(endDate);
|
|
20
|
+
end.setHours(23, 59, 59, 999);
|
|
21
|
+
query.salesDate = { $gte: start, $lte: end };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sales = await Sale.find(query)
|
|
25
|
+
.populate('productId', 'productName category')
|
|
26
|
+
.sort({ salesDate: -1 });
|
|
27
|
+
|
|
28
|
+
const totalRevenue = sales.reduce((sum, sale) => sum + sale.soldTotalPrice, 0);
|
|
29
|
+
|
|
30
|
+
res.json({ sales, totalRevenue, count: sales.length });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Daily sales report error:', error);
|
|
33
|
+
res.status(500).json({ message: 'Server error' });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
exports.getStockStatusReport = async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const { date } = req.query;
|
|
40
|
+
const query = {};
|
|
41
|
+
|
|
42
|
+
if (date) {
|
|
43
|
+
const start = new Date(date);
|
|
44
|
+
start.setHours(0, 0, 0, 0);
|
|
45
|
+
const end = new Date(date);
|
|
46
|
+
end.setHours(23, 59, 59, 999);
|
|
47
|
+
query.generatedAt = { $gte: start, $lte: end };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const stockStatuses = await StockStatus.find(query)
|
|
51
|
+
.populate('productId', 'productName category')
|
|
52
|
+
.sort({ generatedAt: -1 });
|
|
53
|
+
|
|
54
|
+
res.json({ stockStatuses, count: stockStatuses.length });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Stock status report error:', error);
|
|
57
|
+
res.status(500).json({ message: 'Server error' });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
exports.getDashboardData = async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const totalProducts = await Product.countDocuments();
|
|
64
|
+
const totalSalesCount = await Sale.countDocuments();
|
|
65
|
+
const allSales = await Sale.find();
|
|
66
|
+
const totalRevenue = allSales.reduce((sum, s) => sum + s.soldTotalPrice, 0);
|
|
67
|
+
const products = await Product.find();
|
|
68
|
+
const availableStock = products.reduce((sum, p) => sum + p.quantity, 0);
|
|
69
|
+
|
|
70
|
+
const today = new Date();
|
|
71
|
+
today.setHours(0, 0, 0, 0);
|
|
72
|
+
const dailySales = await Sale.find({ salesDate: { $gte: today } })
|
|
73
|
+
.populate('productId', 'productName')
|
|
74
|
+
.sort({ salesDate: -1 })
|
|
75
|
+
.limit(10);
|
|
76
|
+
|
|
77
|
+
const categoryData = await Product.aggregate([
|
|
78
|
+
{ $group: { _id: '$category', count: { $sum: 1 } } },
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
res.json({
|
|
82
|
+
totalProducts,
|
|
83
|
+
totalSales: totalSalesCount,
|
|
84
|
+
totalRevenue,
|
|
85
|
+
availableStock,
|
|
86
|
+
dailySales,
|
|
87
|
+
categoryData,
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Dashboard data error:', error);
|
|
91
|
+
res.status(500).json({ message: 'Server error' });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const Sale = require('../models/Sale');
|
|
2
|
+
const Product = require('../models/Product');
|
|
3
|
+
const StockStatus = require('../models/StockStatus');
|
|
4
|
+
|
|
5
|
+
exports.createSale = async (req, res) => {
|
|
6
|
+
try {
|
|
7
|
+
const { productId, soldQuantity, soldUnitPrice, salesDate } = req.body;
|
|
8
|
+
|
|
9
|
+
const product = await Product.findById(productId);
|
|
10
|
+
if (!product) {
|
|
11
|
+
return res.status(404).json({ message: 'Product not found' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (soldQuantity > product.quantity) {
|
|
15
|
+
return res.status(400).json({
|
|
16
|
+
message: `Insufficient stock. Available: ${product.quantity}, Requested: ${soldQuantity}`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sale = new Sale({
|
|
21
|
+
productId,
|
|
22
|
+
soldQuantity,
|
|
23
|
+
soldUnitPrice,
|
|
24
|
+
salesDate,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const savedSale = await sale.save();
|
|
28
|
+
|
|
29
|
+
product.quantity -= soldQuantity;
|
|
30
|
+
product.totalPrice = product.quantity * product.unitPrice;
|
|
31
|
+
await product.save();
|
|
32
|
+
|
|
33
|
+
let stockStatus = await StockStatus.findOne({ productId });
|
|
34
|
+
if (stockStatus) {
|
|
35
|
+
stockStatus.soldQuantity += soldQuantity;
|
|
36
|
+
await stockStatus.save();
|
|
37
|
+
} else {
|
|
38
|
+
stockStatus = new StockStatus({
|
|
39
|
+
productId,
|
|
40
|
+
availableQuantity: product.quantity + soldQuantity,
|
|
41
|
+
soldQuantity,
|
|
42
|
+
generatedAt: new Date(),
|
|
43
|
+
});
|
|
44
|
+
await stockStatus.save();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
res.status(201).json(savedSale);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.name === 'ValidationError') {
|
|
50
|
+
const messages = Object.values(error.errors).map((e) => e.message);
|
|
51
|
+
return res.status(400).json({ message: messages.join(', ') });
|
|
52
|
+
}
|
|
53
|
+
console.error('Create sale error:', error);
|
|
54
|
+
res.status(500).json({ message: 'Server error' });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
exports.getSales = async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const { page = 1, limit = 10 } = req.query;
|
|
61
|
+
const total = await Sale.countDocuments();
|
|
62
|
+
const sales = await Sale.find()
|
|
63
|
+
.populate('productId', 'productName category')
|
|
64
|
+
.sort({ createdAt: -1 })
|
|
65
|
+
.skip((page - 1) * limit)
|
|
66
|
+
.limit(parseInt(limit));
|
|
67
|
+
|
|
68
|
+
res.json({
|
|
69
|
+
sales,
|
|
70
|
+
total,
|
|
71
|
+
page: parseInt(page),
|
|
72
|
+
pages: Math.ceil(total / limit),
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Get sales error:', error);
|
|
76
|
+
res.status(500).json({ message: 'Server error' });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
exports.getSale = async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const sale = await Sale.findById(req.params.id).populate('productId', 'productName category');
|
|
83
|
+
if (!sale) {
|
|
84
|
+
return res.status(404).json({ message: 'Sale not found' });
|
|
85
|
+
}
|
|
86
|
+
res.json(sale);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Get sale error:', error);
|
|
89
|
+
res.status(500).json({ message: 'Server error' });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const StockStatus = require('../models/StockStatus');
|
|
2
|
+
const Product = require('../models/Product');
|
|
3
|
+
|
|
4
|
+
exports.createStockStatus = async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const { productId, availableQuantity, soldQuantity, generatedAt } = req.body;
|
|
7
|
+
|
|
8
|
+
const product = await Product.findById(productId);
|
|
9
|
+
if (!product) {
|
|
10
|
+
return res.status(404).json({ message: 'Product not found' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const stockStatus = new StockStatus({
|
|
14
|
+
productId,
|
|
15
|
+
availableQuantity,
|
|
16
|
+
soldQuantity,
|
|
17
|
+
generatedAt: generatedAt || new Date(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const saved = await stockStatus.save();
|
|
21
|
+
const populated = await StockStatus.findById(saved._id).populate('productId', 'productName category');
|
|
22
|
+
res.status(201).json(populated);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error.name === 'ValidationError') {
|
|
25
|
+
const messages = Object.values(error.errors).map((e) => e.message);
|
|
26
|
+
return res.status(400).json({ message: messages.join(', ') });
|
|
27
|
+
}
|
|
28
|
+
console.error('Create stock status error:', error);
|
|
29
|
+
res.status(500).json({ message: 'Server error' });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
exports.getStockStatuses = async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
const { search, page = 1, limit = 10 } = req.query;
|
|
36
|
+
const query = {};
|
|
37
|
+
|
|
38
|
+
if (search) {
|
|
39
|
+
const products = await Product.find({
|
|
40
|
+
productName: { $regex: search, $options: 'i' },
|
|
41
|
+
}).select('_id');
|
|
42
|
+
const productIds = products.map((p) => p._id);
|
|
43
|
+
query.productId = { $in: productIds };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const total = await StockStatus.countDocuments(query);
|
|
47
|
+
const stockStatuses = await StockStatus.find(query)
|
|
48
|
+
.populate('productId', 'productName category')
|
|
49
|
+
.sort({ generatedAt: -1 })
|
|
50
|
+
.skip((page - 1) * limit)
|
|
51
|
+
.limit(parseInt(limit));
|
|
52
|
+
|
|
53
|
+
res.json({
|
|
54
|
+
stockStatuses,
|
|
55
|
+
total,
|
|
56
|
+
page: parseInt(page),
|
|
57
|
+
pages: Math.ceil(total / limit),
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Get stock statuses error:', error);
|
|
61
|
+
res.status(500).json({ message: 'Server error' });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
exports.getStockStatus = async (req, res) => {
|
|
66
|
+
try {
|
|
67
|
+
const stockStatus = await StockStatus.findById(req.params.id).populate(
|
|
68
|
+
'productId',
|
|
69
|
+
'productName category'
|
|
70
|
+
);
|
|
71
|
+
if (!stockStatus) {
|
|
72
|
+
return res.status(404).json({ message: 'Stock status not found' });
|
|
73
|
+
}
|
|
74
|
+
res.json(stockStatus);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Get stock status error:', error);
|
|
77
|
+
res.status(500).json({ message: 'Server error' });
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
exports.updateStockStatus = async (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
const { availableQuantity, soldQuantity, generatedAt } = req.body;
|
|
84
|
+
|
|
85
|
+
const updateData = {};
|
|
86
|
+
if (availableQuantity !== undefined) updateData.availableQuantity = availableQuantity;
|
|
87
|
+
if (soldQuantity !== undefined) updateData.soldQuantity = soldQuantity;
|
|
88
|
+
if (generatedAt !== undefined) updateData.generatedAt = generatedAt;
|
|
89
|
+
if (availableQuantity !== undefined || soldQuantity !== undefined) {
|
|
90
|
+
const existing = await StockStatus.findById(req.params.id);
|
|
91
|
+
updateData.remainingQuantity =
|
|
92
|
+
(availableQuantity ?? existing.availableQuantity) -
|
|
93
|
+
(soldQuantity ?? existing.soldQuantity);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const stockStatus = await StockStatus.findByIdAndUpdate(req.params.id, updateData, {
|
|
97
|
+
new: true,
|
|
98
|
+
runValidators: true,
|
|
99
|
+
}).populate('productId', 'productName category');
|
|
100
|
+
|
|
101
|
+
if (!stockStatus) {
|
|
102
|
+
return res.status(404).json({ message: 'Stock status not found' });
|
|
103
|
+
}
|
|
104
|
+
res.json(stockStatus);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error.name === 'ValidationError') {
|
|
107
|
+
const messages = Object.values(error.errors).map((e) => e.message);
|
|
108
|
+
return res.status(400).json({ message: messages.join(', ') });
|
|
109
|
+
}
|
|
110
|
+
console.error('Update stock status error:', error);
|
|
111
|
+
res.status(500).json({ message: 'Server error' });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
exports.deleteStockStatus = async (req, res) => {
|
|
116
|
+
try {
|
|
117
|
+
const stockStatus = await StockStatus.findByIdAndDelete(req.params.id);
|
|
118
|
+
if (!stockStatus) {
|
|
119
|
+
return res.status(404).json({ message: 'Stock status not found' });
|
|
120
|
+
}
|
|
121
|
+
res.json({ message: 'Stock status deleted successfully' });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Delete stock status error:', error);
|
|
124
|
+
res.status(500).json({ message: 'Server error' });
|
|
125
|
+
}
|
|
126
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const productSchema = new mongoose.Schema({
|
|
4
|
+
productName: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: [true, 'Product name is required'],
|
|
7
|
+
trim: true,
|
|
8
|
+
},
|
|
9
|
+
category: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: [true, 'Category is required'],
|
|
12
|
+
trim: true,
|
|
13
|
+
},
|
|
14
|
+
quantity: {
|
|
15
|
+
type: Number,
|
|
16
|
+
required: [true, 'Quantity is required'],
|
|
17
|
+
min: [0, 'Quantity cannot be negative'],
|
|
18
|
+
default: 0,
|
|
19
|
+
},
|
|
20
|
+
unitPrice: {
|
|
21
|
+
type: Number,
|
|
22
|
+
required: [true, 'Unit price is required'],
|
|
23
|
+
min: [0.01, 'Unit price must be greater than 0'],
|
|
24
|
+
},
|
|
25
|
+
totalPrice: {
|
|
26
|
+
type: Number,
|
|
27
|
+
default: 0,
|
|
28
|
+
},
|
|
29
|
+
createdAt: {
|
|
30
|
+
type: Date,
|
|
31
|
+
default: Date.now,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
productSchema.pre('save', function (next) {
|
|
36
|
+
this.totalPrice = this.quantity * this.unitPrice;
|
|
37
|
+
next();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
module.exports = mongoose.model('Product', productSchema);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const saleSchema = new mongoose.Schema({
|
|
4
|
+
productId: {
|
|
5
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
6
|
+
ref: 'Product',
|
|
7
|
+
required: [true, 'Product is required'],
|
|
8
|
+
},
|
|
9
|
+
soldQuantity: {
|
|
10
|
+
type: Number,
|
|
11
|
+
required: [true, 'Sold quantity is required'],
|
|
12
|
+
min: [1, 'Sold quantity must be at least 1'],
|
|
13
|
+
},
|
|
14
|
+
soldUnitPrice: {
|
|
15
|
+
type: Number,
|
|
16
|
+
required: [true, 'Sold unit price is required'],
|
|
17
|
+
min: [0.01, 'Price must be greater than 0'],
|
|
18
|
+
},
|
|
19
|
+
soldTotalPrice: {
|
|
20
|
+
type: Number,
|
|
21
|
+
default: 0,
|
|
22
|
+
},
|
|
23
|
+
salesDate: {
|
|
24
|
+
type: Date,
|
|
25
|
+
required: [true, 'Sales date is required'],
|
|
26
|
+
},
|
|
27
|
+
createdAt: {
|
|
28
|
+
type: Date,
|
|
29
|
+
default: Date.now,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
saleSchema.pre('save', function (next) {
|
|
34
|
+
this.soldTotalPrice = this.soldQuantity * this.soldUnitPrice;
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
module.exports = mongoose.model('Sale', saleSchema);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const stockStatusSchema = new mongoose.Schema({
|
|
4
|
+
productId: {
|
|
5
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
6
|
+
ref: 'Product',
|
|
7
|
+
required: [true, 'Product is required'],
|
|
8
|
+
},
|
|
9
|
+
availableQuantity: {
|
|
10
|
+
type: Number,
|
|
11
|
+
required: [true, 'Available quantity is required'],
|
|
12
|
+
min: 0,
|
|
13
|
+
},
|
|
14
|
+
soldQuantity: {
|
|
15
|
+
type: Number,
|
|
16
|
+
required: [true, 'Sold quantity is required'],
|
|
17
|
+
min: 0,
|
|
18
|
+
default: 0,
|
|
19
|
+
},
|
|
20
|
+
remainingQuantity: {
|
|
21
|
+
type: Number,
|
|
22
|
+
default: 0,
|
|
23
|
+
},
|
|
24
|
+
generatedAt: {
|
|
25
|
+
type: Date,
|
|
26
|
+
default: Date.now,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
stockStatusSchema.pre('save', function (next) {
|
|
31
|
+
this.remainingQuantity = this.availableQuantity - this.soldQuantity;
|
|
32
|
+
next();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
module.exports = mongoose.model('StockStatus', stockStatusSchema);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const userSchema = new mongoose.Schema({
|
|
4
|
+
username: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: [true, 'Username is required'],
|
|
7
|
+
unique: true,
|
|
8
|
+
trim: true,
|
|
9
|
+
},
|
|
10
|
+
password: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: [true, 'Password is required'],
|
|
13
|
+
},
|
|
14
|
+
role: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: 'admin',
|
|
17
|
+
},
|
|
18
|
+
createdAt: {
|
|
19
|
+
type: Date,
|
|
20
|
+
default: Date.now,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
module.exports = mongoose.model('User', userSchema);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bws-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Business Web Solution - Backend API (Express + MongoDB)",
|
|
5
|
+
"keywords": ["bws", "api", "express", "mongodb", "inventory"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "server.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node server.js",
|
|
10
|
+
"dev": "nodemon server.js",
|
|
11
|
+
"seed": "node utils/seed.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"bcryptjs": "^2.4.3",
|
|
15
|
+
"cors": "^2.8.5",
|
|
16
|
+
"dotenv": "^16.3.1",
|
|
17
|
+
"express": "^4.18.2",
|
|
18
|
+
"express-session": "^1.17.3",
|
|
19
|
+
"mongoose": "^8.0.3"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"nodemon": "^3.0.2"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { login, logout, checkSession } = require('../controllers/authController');
|
|
4
|
+
|
|
5
|
+
router.post('/login', login);
|
|
6
|
+
router.post('/logout', logout);
|
|
7
|
+
router.get('/session', checkSession);
|
|
8
|
+
|
|
9
|
+
module.exports = router;
|