Low inventory notification support
This commit is contained in:
parent
d9953baa19
commit
119e9adfec
7 changed files with 565 additions and 44 deletions
|
|
@ -10,6 +10,10 @@ const adminAuthMiddleware = require('./middleware/adminAuth');
|
|||
const settingsAdminRoutes = require('./routes/settingsAdmin');
|
||||
const SystemSettings = require('./models/SystemSettings');
|
||||
const fs = require('fs');
|
||||
// services
|
||||
|
||||
const notificationService = require('./services/notificationService');
|
||||
|
||||
|
||||
// routes
|
||||
const stripePaymentRoutes = require('./routes/stripePayment');
|
||||
|
|
@ -96,7 +100,28 @@ pool.connect()
|
|||
console.error('Error loading settings from database:', error);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Database connection error:', err));
|
||||
.catch(err => console.error('Database connection error:', err))
|
||||
.finally((() => {
|
||||
|
||||
let timeInterval = 60 * 1000;
|
||||
if (config.environment === 'prod') {
|
||||
console.log('Setting up scheduled tasks for production environment, 30 mins');
|
||||
timeInterval = 30 * 60 * 1000;
|
||||
}else {
|
||||
console.log('Setting up scheduled tasks for development environment, 2 mins');
|
||||
timeInterval = 2 * 60 * 1000;
|
||||
}
|
||||
// Process stock notifications every 30 minutes
|
||||
setInterval(async () => {
|
||||
try {
|
||||
console.log('Processing low stock notifications...');
|
||||
const processedCount = await notificationService.processLowStockNotifications(pool, query);
|
||||
console.log(`Processed ${processedCount} low stock notifications`);
|
||||
} catch (error) {
|
||||
console.error('Error processing low stock notifications:', error);
|
||||
}
|
||||
}, timeInterval);
|
||||
}));
|
||||
|
||||
|
||||
// Handle SSL proxy headers
|
||||
|
|
|
|||
|
|
@ -811,6 +811,36 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
|
||||
await client.query('COMMIT');
|
||||
|
||||
for (const item of cartItemsResult.rows) {
|
||||
// Get product details including notification settings
|
||||
const productWithNotification = await client.query(
|
||||
`SELECT stock_quantity, stock_notification
|
||||
FROM products
|
||||
WHERE id = $1 AND stock_notification IS NOT NULL`,
|
||||
[item.product_id]
|
||||
);
|
||||
|
||||
if (productWithNotification.rows.length > 0) {
|
||||
const product = productWithNotification.rows[0];
|
||||
|
||||
// Check if notification is enabled and stock is below threshold
|
||||
if (product.stock_notification &&
|
||||
product.stock_notification.enabled === true &&
|
||||
product.stock_notification.threshold &&
|
||||
product.stock_quantity <= product.stock_notification.threshold) {
|
||||
|
||||
// Log the notification - it will be picked up by the notification service
|
||||
await client.query(
|
||||
`INSERT INTO notification_logs
|
||||
(order_id, notification_type, sent_at, status)
|
||||
VALUES ($1, $2, NOW(), $3)`,
|
||||
[orderId, 'low_stock_alert', 'pending']
|
||||
);
|
||||
|
||||
console.log(`Low stock notification queued for product ${item.product_id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Order completed successfully',
|
||||
|
|
|
|||
|
|
@ -151,6 +151,57 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/stock-notification', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { enabled, email, threshold } = req.body;
|
||||
|
||||
// Check if user is admin
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
message: 'Admin access required'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if product exists
|
||||
const productCheck = await query(
|
||||
'SELECT * FROM products WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (productCheck.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
message: 'Product not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Store notification settings as JSONB
|
||||
const notificationSettings = {
|
||||
enabled,
|
||||
email: email || null,
|
||||
threshold: threshold || 0
|
||||
};
|
||||
|
||||
// Update product with notification settings
|
||||
const result = await query(
|
||||
`UPDATE products
|
||||
SET stock_notification = $1
|
||||
WHERE id = $2
|
||||
RETURNING *`,
|
||||
[JSON.stringify(notificationSettings), id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: 'Stock notification settings updated successfully',
|
||||
product: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Update an existing product
|
||||
router.put('/:id', async (req, res, next) => {
|
||||
|
|
|
|||
174
backend/src/services/notificationService.js
Normal file
174
backend/src/services/notificationService.js
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// Create a new file: src/services/notificationService.js
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
const config = require('../config');
|
||||
|
||||
/**
|
||||
* Service for handling notifications including stock alerts
|
||||
*/
|
||||
const notificationService = {
|
||||
/**
|
||||
* Create email transporter
|
||||
* @returns {Object} Configured nodemailer transporter
|
||||
*/
|
||||
createTransporter() {
|
||||
return nodemailer.createTransport({
|
||||
host: config.email.host,
|
||||
port: config.email.port,
|
||||
auth: {
|
||||
user: config.email.user,
|
||||
pass: config.email.pass
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Process pending low stock notifications
|
||||
* @param {Object} pool - Database connection pool
|
||||
* @param {Function} query - Database query function
|
||||
* @returns {Promise<number>} Number of notifications processed
|
||||
*/
|
||||
async processLowStockNotifications(pool, query) {
|
||||
const client = await pool.connect();
|
||||
let processedCount = 0;
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Get pending low stock notifications
|
||||
const pendingNotifications = await client.query(`
|
||||
SELECT id
|
||||
FROM notification_logs
|
||||
WHERE notification_type = 'low_stock_alert'
|
||||
AND status = 'pending'
|
||||
LIMIT 50
|
||||
`);
|
||||
|
||||
if (pendingNotifications.rows.length === 0) {
|
||||
await client.query('COMMIT');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get products with current stock below threshold
|
||||
const lowStockProducts = await client.query(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.stock_quantity,
|
||||
p.stock_notification
|
||||
FROM products p
|
||||
WHERE
|
||||
p.stock_notification IS NOT NULL
|
||||
AND p.stock_notification->>'enabled' = 'true'
|
||||
AND p.stock_notification->>'email' IS NOT NULL
|
||||
AND p.stock_quantity <= (p.stock_notification->>'threshold')::int
|
||||
`);
|
||||
|
||||
if (lowStockProducts.rows.length === 0) {
|
||||
// Mark notifications as processed with no action
|
||||
const notificationIds = pendingNotifications.rows.map(n => n.id);
|
||||
await client.query(`
|
||||
UPDATE notification_logs
|
||||
SET status = 'completed', error_message = 'No eligible products found'
|
||||
WHERE id = ANY($1)
|
||||
`, [notificationIds]);
|
||||
|
||||
await client.query('COMMIT');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Initialize email transporter
|
||||
const transporter = this.createTransporter();
|
||||
|
||||
// Send notifications for each low stock product
|
||||
for (const product of lowStockProducts.rows) {
|
||||
const notification = JSON.parse(product.stock_notification);
|
||||
|
||||
try {
|
||||
// Send email notification
|
||||
await transporter.sendMail({
|
||||
from: config.email.reply,
|
||||
to: notification.email,
|
||||
subject: `Low Stock Alert: ${product.name}`,
|
||||
html: this.generateLowStockEmailTemplate(product)
|
||||
});
|
||||
|
||||
// Mark one notification as processed
|
||||
if (pendingNotifications.rows[processedCount]) {
|
||||
await client.query(`
|
||||
UPDATE notification_logs
|
||||
SET status = 'success'
|
||||
WHERE id = $1
|
||||
`, [pendingNotifications.rows[processedCount].id]);
|
||||
|
||||
processedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error sending low stock notification for product ${product.id}:`, error);
|
||||
|
||||
// Mark notification as failed
|
||||
if (pendingNotifications.rows[processedCount]) {
|
||||
await client.query(`
|
||||
UPDATE notification_logs
|
||||
SET status = 'failed', error_message = $2
|
||||
WHERE id = $1
|
||||
`, [pendingNotifications.rows[processedCount].id, error.message]);
|
||||
|
||||
processedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return processedCount;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('Error processing low stock notifications:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate email template for low stock notification
|
||||
* @param {Object} product - Product with low stock
|
||||
* @returns {string} HTML email template
|
||||
*/
|
||||
generateLowStockEmailTemplate(product) {
|
||||
const stockNotification = JSON.parse(product.stock_notification);
|
||||
const threshold = stockNotification.threshold || 0;
|
||||
|
||||
return `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background-color: #f8f8f8; padding: 20px; text-align: center;">
|
||||
<h1 style="color: #ff6b6b;">Low Stock Alert</h1>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>This is an automated notification to inform you that the following product is running low on stock:</p>
|
||||
|
||||
<div style="background-color: #f8f8f8; padding: 15px; margin: 20px 0; border-left: 4px solid #ff6b6b;">
|
||||
<h3 style="margin-top: 0;">${product.name}</h3>
|
||||
<p><strong>Current Stock:</strong> ${product.stock_quantity}</p>
|
||||
<p><strong>Threshold:</strong> ${threshold}</p>
|
||||
</div>
|
||||
|
||||
<p>You might want to restock this item soon to avoid running out of inventory.</p>
|
||||
|
||||
<div style="margin-top: 30px; border-top: 1px solid #eee; padding-top: 20px;">
|
||||
<p>This is an automated notification. You received this because you set up stock notifications for this product.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #333; color: white; padding: 15px; text-align: center; font-size: 12px;">
|
||||
<p>© ${new Date().getFullYear()} Rocks, Bones & Sticks. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = notificationService;
|
||||
52
db/init/14-product-notifications.sql
Normal file
52
db/init/14-product-notifications.sql
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
ALTER TABLE products ADD COLUMN IF NOT EXISTS stock_notification JSONB;
|
||||
|
||||
-- Create a function to send email notifications when stock drops below threshold
|
||||
CREATE OR REPLACE FUNCTION notify_low_stock()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Check if notification is enabled and new stock is below threshold
|
||||
IF (NEW.stock_notification IS NOT NULL AND
|
||||
NEW.stock_notification->>'enabled' = 'true' AND
|
||||
NEW.stock_notification->>'email' IS NOT NULL AND
|
||||
(NEW.stock_notification->>'threshold')::int > 0 AND
|
||||
NEW.stock_quantity <= (NEW.stock_notification->>'threshold')::int AND
|
||||
(OLD.stock_quantity IS NULL OR OLD.stock_quantity > (NEW.stock_notification->>'threshold')::int)) THEN
|
||||
|
||||
-- Insert notification record into a notification log table
|
||||
INSERT INTO notification_logs (
|
||||
order_id, -- Using NULL as this isn't tied to a specific order
|
||||
notification_type,
|
||||
sent_at,
|
||||
status
|
||||
) VALUES (
|
||||
NULL,
|
||||
'low_stock_alert',
|
||||
NOW(),
|
||||
'pending'
|
||||
);
|
||||
|
||||
-- Note: The actual email sending will be handled by a backend process
|
||||
-- that periodically checks for pending notifications
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger to check stock level on update
|
||||
CREATE TRIGGER check_stock_level_on_update
|
||||
AFTER UPDATE OF stock_quantity ON products
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_low_stock();
|
||||
|
||||
-- Create trigger to check stock level on insert (though less common)
|
||||
CREATE TRIGGER check_stock_level_on_insert
|
||||
AFTER INSERT ON products
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_low_stock();
|
||||
-- Add stock_notification column to products table
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS stock_notification JSONB;
|
||||
|
||||
-- Create index for faster lookups of products with notifications
|
||||
CREATE INDEX IF NOT EXISTS idx_products_stock_notification ON products ((stock_notification IS NOT NULL))
|
||||
WHERE stock_notification IS NOT NULL;
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
ROCKS/
|
||||
Rocks/
|
||||
├── backend/
|
||||
│ ├── src/
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── shippingService.js
|
||||
│ │ │ └── notificationService.js
|
||||
│ │ │ └── shippingService.js
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── cart.js
|
||||
│ │ │ ├── stripePayment.js
|
||||
│ │ │ ├── shipping.js
|
||||
│ │ │ ├── settingsAdmin.js
|
||||
│ │ │ ├── cart.js
|
||||
│ │ │ ├── userOrders.js
|
||||
│ │ │ ├── orderAdmin.js
|
||||
│ │ │ ├── stripePayment.js
|
||||
│ │ │ ├── auth.js
|
||||
│ │ │ ├── userAdmin.js
|
||||
│ │ │ ├── products.js
|
||||
|
|
@ -24,16 +25,16 @@ ROCKS/
|
|||
│ │ │ ├── auth.js
|
||||
│ │ │ └── adminAuth.js
|
||||
│ │ └── db/
|
||||
│ │ └── index.js
|
||||
│ ├── index.js
|
||||
│ ├── config.js
|
||||
│ │ ├── index.js
|
||||
│ │ ├── index.js
|
||||
│ │ └── config.js
|
||||
│ ├── node_modules/
|
||||
│ ├── public/
|
||||
│ │ └── uploads/
|
||||
│ │ └── products/
|
||||
│ ├── .env
|
||||
│ ├── package.json
|
||||
│ ├── package-lock.json
|
||||
│ ├── .env
|
||||
│ ├── Dockerfile
|
||||
│ ├── README.md
|
||||
│ └── .gitignore
|
||||
|
|
@ -109,32 +110,34 @@ ROCKS/
|
|||
│ │ ├── App.jsx
|
||||
│ │ ├── config.js
|
||||
│ │ └── main.jsx
|
||||
│ ├── public/
|
||||
│ │ ├── favicon.svg
|
||||
│ │ └── index.html
|
||||
│ ├── package-lock.json
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js
|
||||
│ ├── Dockerfile
|
||||
│ ├── nginx.conf
|
||||
│ ├── README.md
|
||||
│ ├── .env
|
||||
│ └── setup-frontend.sh
|
||||
│ └── public/
|
||||
│ ├── favicon.svg
|
||||
│ ├── package-lock.json
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js
|
||||
│ ├── Dockerfile
|
||||
│ ├── nginx.conf
|
||||
│ ├── index.html
|
||||
│ ├── README.md
|
||||
│ ├── .env
|
||||
│ └── setup-frontend.sh
|
||||
├── db/
|
||||
│ └── init/
|
||||
│ ├── 12-shipping-orders.sql
|
||||
│ ├── 09-system-settings.sql
|
||||
│ ├── 11-notifications.sql
|
||||
│ ├── 10-payment.sql
|
||||
│ ├── 08-create-email.sql
|
||||
│ ├── 07-user-keys.sql
|
||||
│ ├── 06-product-categories.sql
|
||||
│ ├── 05-admin-role.sql
|
||||
│ ├── 02-seed.sql
|
||||
│ ├── 04-product-images.sql
|
||||
│ ├── 03-api-key.sql
|
||||
│ └── 01-schema.sql
|
||||
├── test/
|
||||
│ ├── init/
|
||||
│ │ ├── 14-product-notifications.sql
|
||||
│ │ ├── 13-cart-metadata.sql
|
||||
│ │ ├── 12-shipping-orders.sql
|
||||
│ │ ├── 09-system-settings.sql
|
||||
│ │ ├── 11-notifications.sql
|
||||
│ │ ├── 10-payment.sql
|
||||
│ │ ├── 08-create-email.sql
|
||||
│ │ ├── 07-user-keys.sql
|
||||
│ │ ├── 06-product-categories.sql
|
||||
│ │ ├── 05-admin-role.sql
|
||||
│ │ ├── 02-seed.sql
|
||||
│ │ ├── 04-product-images.sql
|
||||
│ │ ├── 03-api-key.sql
|
||||
│ │ └── 01-schema.sql
|
||||
│ └── test/
|
||||
├── fileStructure.txt
|
||||
├── docker-compose.yml
|
||||
└── .gitignore
|
||||
|
|
@ -17,12 +17,17 @@ import {
|
|||
CircularProgress,
|
||||
Alert,
|
||||
Snackbar,
|
||||
Autocomplete
|
||||
Autocomplete,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Card,
|
||||
CardContent,
|
||||
} from '@mui/material';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
||||
import ImageUploader from '@components/ImageUploader';
|
||||
import apiClient from '@services/api';
|
||||
import { useAuth } from '@hooks/reduxHooks';
|
||||
|
|
@ -32,8 +37,9 @@ const ProductEditPage = () => {
|
|||
const id = pathname.split('/').pop();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { apiKey } = useAuth();
|
||||
const { apiKey, userData } = useAuth();
|
||||
const isNewProduct = id === 'new';
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
|
|
@ -53,6 +59,11 @@ const ProductEditPage = () => {
|
|||
images: []
|
||||
});
|
||||
|
||||
// Notification state
|
||||
const [notificationEnabled, setNotificationEnabled] = useState(false);
|
||||
const [notificationEmail, setNotificationEmail] = useState('');
|
||||
const [stockThreshold, setStockThreshold] = useState(1);
|
||||
|
||||
// Validation state
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
|
|
@ -144,6 +155,27 @@ const ProductEditPage = () => {
|
|||
}
|
||||
});
|
||||
|
||||
// Save stock notification settings
|
||||
const saveStockNotification = useMutation({
|
||||
mutationFn: async (notificationData) => {
|
||||
return await apiClient.post(`/admin/products/${id}/stock-notification`, notificationData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setNotification({
|
||||
open: true,
|
||||
message: 'Stock notification settings saved!',
|
||||
severity: 'success'
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setNotification({
|
||||
open: true,
|
||||
message: `Failed to save notification settings: ${error.message}`,
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle form changes
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
|
@ -154,17 +186,38 @@ const ProductEditPage = () => {
|
|||
setErrors(prev => ({ ...prev, [name]: null }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Handle tags change
|
||||
const handleTagsChange = (event, newTags) => {
|
||||
setFormData(prev => ({ ...prev, tags: newTags }));
|
||||
};
|
||||
|
||||
|
||||
// Handle images change
|
||||
const handleImagesChange = (newImages) => {
|
||||
setFormData(prev => ({ ...prev, images: newImages }));
|
||||
};
|
||||
|
||||
// Handle notification checkbox change
|
||||
const handleNotificationToggle = (event) => {
|
||||
setNotificationEnabled(event.target.checked);
|
||||
};
|
||||
|
||||
// Handle notification email change
|
||||
const handleNotificationEmailChange = (e) => {
|
||||
setNotificationEmail(e.target.value);
|
||||
};
|
||||
|
||||
// Handle stock threshold change
|
||||
const handleStockThresholdChange = (e) => {
|
||||
let value = parseInt(e.target.value);
|
||||
if (isNaN(value) || value < 1) {
|
||||
value = 1;
|
||||
} else if (value > parseInt(formData.stockQuantity)) {
|
||||
value = parseInt(formData.stockQuantity);
|
||||
}
|
||||
setStockThreshold(value);
|
||||
};
|
||||
|
||||
// Validate form
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
|
@ -204,11 +257,32 @@ const ProductEditPage = () => {
|
|||
if (formData.heightCm && (isNaN(formData.heightCm) || parseFloat(formData.heightCm) <= 0)) {
|
||||
newErrors.heightCm = 'Height must be a positive number';
|
||||
}
|
||||
|
||||
// Validate notification settings if enabled
|
||||
if (notificationEnabled) {
|
||||
if (!notificationEmail) {
|
||||
newErrors.notificationEmail = 'Email is required for notifications';
|
||||
} else {
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(notificationEmail)) {
|
||||
newErrors.notificationEmail = 'Please enter a valid email address';
|
||||
}
|
||||
}
|
||||
|
||||
if (!stockThreshold) {
|
||||
newErrors.stockThreshold = 'Threshold is required for notifications';
|
||||
} else if (isNaN(stockThreshold) || stockThreshold < 1) {
|
||||
newErrors.stockThreshold = 'Threshold must be a positive number';
|
||||
} else if (formData.stockQuantity && stockThreshold > parseInt(formData.stockQuantity)) {
|
||||
newErrors.stockThreshold = `Threshold cannot exceed current stock (${formData.stockQuantity})`;
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -234,18 +308,43 @@ const ProductEditPage = () => {
|
|||
tags: formData.tags.map(tag => typeof tag === 'string' ? tag : tag.name)
|
||||
};
|
||||
|
||||
// Add notification data if enabled
|
||||
if (notificationEnabled && !isNewProduct) {
|
||||
productData.stockNotification = {
|
||||
enabled: true,
|
||||
email: notificationEmail,
|
||||
threshold: stockThreshold
|
||||
};
|
||||
}
|
||||
|
||||
if (isNewProduct) {
|
||||
createProduct.mutate(productData);
|
||||
} else {
|
||||
updateProduct.mutate({ id, productData });
|
||||
|
||||
// Save notification settings separately
|
||||
if (notificationEnabled) {
|
||||
saveStockNotification.mutate({
|
||||
enabled: true,
|
||||
email: notificationEmail,
|
||||
threshold: stockThreshold
|
||||
});
|
||||
} else {
|
||||
// Disable notifications if checkbox is unchecked
|
||||
saveStockNotification.mutate({
|
||||
enabled: false,
|
||||
email: '',
|
||||
threshold: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Handle notification close
|
||||
const handleNotificationClose = () => {
|
||||
setNotification(prev => ({ ...prev, open: false }));
|
||||
};
|
||||
|
||||
|
||||
// Load product data when available
|
||||
useEffect(() => {
|
||||
if (product && !isNewProduct) {
|
||||
|
|
@ -270,9 +369,22 @@ const ProductEditPage = () => {
|
|||
displayOrder: img.displayOrder || img.display_order || 0
|
||||
})) : []
|
||||
});
|
||||
}
|
||||
}, [product, isNewProduct]);
|
||||
|
||||
// Load notification settings if available
|
||||
if (product.stock_notification) {
|
||||
setNotificationEnabled(product.stock_notification.enabled);
|
||||
setNotificationEmail(product.stock_notification.email || userData?.email || '');
|
||||
setStockThreshold(product.stock_notification.threshold || 1);
|
||||
} else {
|
||||
// Default to admin's email
|
||||
setNotificationEmail(userData?.email || '');
|
||||
}
|
||||
} else {
|
||||
// For new products, set default notification email to admin's email
|
||||
setNotificationEmail(userData?.email || '');
|
||||
}
|
||||
}, [product, isNewProduct, userData]);
|
||||
|
||||
// Format tag objects for Autocomplete component
|
||||
const formatTags = () => {
|
||||
if (!allTags) return [];
|
||||
|
|
@ -280,7 +392,7 @@ const ProductEditPage = () => {
|
|||
};
|
||||
|
||||
const isLoading = categoriesLoading || tagsLoading || (productLoading && !isNewProduct);
|
||||
const isSaving = createProduct.isLoading || updateProduct.isLoading;
|
||||
const isSaving = createProduct.isLoading || updateProduct.isLoading || saveStockNotification.isLoading;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -403,6 +515,80 @@ const ProductEditPage = () => {
|
|||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Stock Notification Section */}
|
||||
{!isNewProduct && (
|
||||
<>
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Stock Level Notifications
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Card variant="outlined" sx={{ bgcolor: 'background.paper' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<NotificationsActiveIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="subtitle1">
|
||||
Get notified when stock is running low
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={notificationEnabled}
|
||||
onChange={handleNotificationToggle}
|
||||
name="notificationEnabled"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Enable stock level notifications"
|
||||
/>
|
||||
|
||||
{notificationEnabled && (
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Notification Email"
|
||||
name="notificationEmail"
|
||||
type="email"
|
||||
value={notificationEmail}
|
||||
onChange={handleNotificationEmailChange}
|
||||
error={!!errors.notificationEmail}
|
||||
helperText={errors.notificationEmail}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Stock Threshold"
|
||||
name="stockThreshold"
|
||||
type="number"
|
||||
value={stockThreshold}
|
||||
onChange={handleStockThresholdChange}
|
||||
error={!!errors.stockThreshold}
|
||||
helperText={errors.stockThreshold || "You'll be notified when stock falls below this number"}
|
||||
required
|
||||
InputProps={{
|
||||
inputProps: {
|
||||
min: 1,
|
||||
max: formData.stockQuantity ? parseInt(formData.stockQuantity) : 999
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
</Grid>
|
||||
|
|
|
|||
Loading…
Reference in a new issue