order status notifications
This commit is contained in:
parent
f138152678
commit
c2a5eea983
7 changed files with 479 additions and 39 deletions
|
|
@ -1,11 +1,25 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
// Helper function to create email transporter
|
||||||
|
const createTransporter = () => {
|
||||||
|
return nodemailer.createTransport({
|
||||||
|
host: config.email.host,
|
||||||
|
port: config.email.port,
|
||||||
|
auth: {
|
||||||
|
user: config.email.user,
|
||||||
|
pass: config.email.pass
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = (pool, query, authMiddleware) => {
|
module.exports = (pool, query, authMiddleware) => {
|
||||||
// Apply authentication middleware to all routes
|
// Apply authentication middleware to all routes
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
// Get all orders
|
// Get all orders (admin only)
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
// Check if user is admin
|
// Check if user is admin
|
||||||
|
|
@ -97,7 +111,7 @@ module.exports = (pool, query, authMiddleware) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update order status
|
// Update order status (simple version)
|
||||||
router.patch('/:id', async (req, res, next) => {
|
router.patch('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
@ -144,5 +158,225 @@ module.exports = (pool, query, authMiddleware) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update order status with shipping information and send notification
|
||||||
|
router.patch('/:id/shipping', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { status, shippingData, sendNotification } = req.body;
|
||||||
|
|
||||||
|
// Check if user is admin
|
||||||
|
if (!req.user.is_admin) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: true,
|
||||||
|
message: 'Admin access required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate status
|
||||||
|
const validStatuses = ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
|
||||||
|
if (!validStatuses.includes(status)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: true,
|
||||||
|
message: `Invalid status. Must be one of: ${validStatuses.join(', ')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get order with customer information before updating
|
||||||
|
const orderResult = await query(`
|
||||||
|
SELECT o.*, u.email, u.first_name, u.last_name
|
||||||
|
FROM orders o
|
||||||
|
JOIN users u ON o.user_id = u.id
|
||||||
|
WHERE o.id = $1
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
if (orderResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: true,
|
||||||
|
message: 'Order not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = orderResult.rows[0];
|
||||||
|
|
||||||
|
// Begin transaction
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// Store shipping information as JSON in the database
|
||||||
|
const updatedOrder = await client.query(`
|
||||||
|
UPDATE orders
|
||||||
|
SET
|
||||||
|
status = $1,
|
||||||
|
updated_at = NOW(),
|
||||||
|
shipping_info = $2,
|
||||||
|
shipping_date = $3
|
||||||
|
WHERE id = $4
|
||||||
|
RETURNING *
|
||||||
|
`, [
|
||||||
|
status,
|
||||||
|
JSON.stringify(shippingData),
|
||||||
|
new Date(),
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If status is 'shipped' and notification requested, send email
|
||||||
|
if (status === 'shipped' && sendNotification) {
|
||||||
|
// Get order items for the email
|
||||||
|
const itemsResult = await client.query(`
|
||||||
|
SELECT oi.*, p.name as product_name, p.price as original_price
|
||||||
|
FROM order_items oi
|
||||||
|
JOIN products p ON oi.product_id = p.id
|
||||||
|
WHERE oi.order_id = $1
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
const orderItems = itemsResult.rows;
|
||||||
|
|
||||||
|
// Send email notification
|
||||||
|
await sendShippingNotification(
|
||||||
|
order,
|
||||||
|
orderItems,
|
||||||
|
shippingData
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log the notification in the database
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO notification_logs (order_id, notification_type, sent_at)
|
||||||
|
VALUES ($1, $2, NOW())
|
||||||
|
`, [id, 'shipping_notification']);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Order status updated successfully',
|
||||||
|
order: updatedOrder.rows[0]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating order status with shipping:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to send shipping notification email
|
||||||
|
async function sendShippingNotification(order, orderItems, shippingData) {
|
||||||
|
try {
|
||||||
|
const transporter = createTransporter();
|
||||||
|
|
||||||
|
// Calculate order total
|
||||||
|
const orderTotal = orderItems.reduce((sum, item) => {
|
||||||
|
return sum + (parseFloat(item.price_at_purchase) * item.quantity);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Format shipping date
|
||||||
|
const shippedDate = new Date(shippingData.shippedDate || new Date()).toLocaleDateString();
|
||||||
|
|
||||||
|
// Generate items HTML table
|
||||||
|
const itemsHtml = orderItems.map(item => `
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border-bottom: 1px solid #eee;">${item.product_name}</td>
|
||||||
|
<td style="padding: 10px; border-bottom: 1px solid #eee;">${item.quantity}</td>
|
||||||
|
<td style="padding: 10px; border-bottom: 1px solid #eee;">$${parseFloat(item.price_at_purchase).toFixed(2)}</td>
|
||||||
|
<td style="padding: 10px; border-bottom: 1px solid #eee;">$${(parseFloat(item.price_at_purchase) * item.quantity).toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Generate carrier tracking link
|
||||||
|
let trackingLink = '#';
|
||||||
|
const shipper = shippingData.shipper?.toLowerCase() || '';
|
||||||
|
const trackingNumber = shippingData.trackingNumber;
|
||||||
|
|
||||||
|
if (trackingNumber) {
|
||||||
|
if (shipper.includes('usps')) {
|
||||||
|
trackingLink = `https://tools.usps.com/go/TrackConfirmAction?tLabels=${trackingNumber}`;
|
||||||
|
} else if (shipper.includes('ups')) {
|
||||||
|
trackingLink = `https://www.ups.com/track?tracknum=${trackingNumber}`;
|
||||||
|
} else if (shipper.includes('fedex')) {
|
||||||
|
trackingLink = `https://www.fedex.com/apps/fedextrack/?tracknumbers=${trackingNumber}`;
|
||||||
|
} else if (shipper.includes('dhl')) {
|
||||||
|
trackingLink = `https://www.dhl.com/global-en/home/tracking.html?tracking-id=${trackingNumber}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build email HTML
|
||||||
|
const emailHtml = `
|
||||||
|
<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: #333;">Your Order Has Shipped!</h1>
|
||||||
|
<p style="font-size: 16px;">Order #${order.id.substring(0, 8)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<p>Hello ${order.first_name},</p>
|
||||||
|
|
||||||
|
<p>Good news! Your order has been shipped and is on its way to you.</p>
|
||||||
|
|
||||||
|
${shippingData.customerMessage ? `<p><strong>Message from our team:</strong> ${shippingData.customerMessage}</p>` : ''}
|
||||||
|
|
||||||
|
<div style="background-color: #f8f8f8; padding: 15px; margin: 20px 0; border-left: 4px solid #4caf50;">
|
||||||
|
<h3 style="margin-top: 0;">Shipping Details</h3>
|
||||||
|
<p><strong>Carrier:</strong> ${shippingData.shipper || 'Standard Shipping'}</p>
|
||||||
|
<p><strong>Tracking Number:</strong> <a href="${trackingLink}" target="_blank">${shippingData.trackingNumber}</a></p>
|
||||||
|
<p><strong>Shipped On:</strong> ${shippedDate}</p>
|
||||||
|
${shippingData.estimatedDelivery ? `<p><strong>Estimated Delivery:</strong> ${shippingData.estimatedDelivery}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 30px;">
|
||||||
|
<h3>Order Summary</h3>
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f2f2f2;">
|
||||||
|
<th style="padding: 10px; text-align: left;">Item</th>
|
||||||
|
<th style="padding: 10px; text-align: left;">Qty</th>
|
||||||
|
<th style="padding: 10px; text-align: left;">Price</th>
|
||||||
|
<th style="padding: 10px; text-align: left;">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${itemsHtml}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" style="padding: 10px; text-align: right;"><strong>Total:</strong></td>
|
||||||
|
<td style="padding: 10px;"><strong>$${orderTotal.toFixed(2)}</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 30px; border-top: 1px solid #eee; padding-top: 20px;">
|
||||||
|
<p>Thank you for your purchase! If you have any questions, please contact our customer service.</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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: config.email.reply,
|
||||||
|
to: order.email,
|
||||||
|
subject: `Your Order #${order.id.substring(0, 8)} Has Shipped!`,
|
||||||
|
html: emailHtml
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Shipping notification email sent to ${order.email}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending shipping notification:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|
@ -6,6 +6,8 @@ const config = require('../config');
|
||||||
module.exports = (pool, query, authMiddleware) => {
|
module.exports = (pool, query, authMiddleware) => {
|
||||||
|
|
||||||
// Webhook to handle events from Stripe
|
// Webhook to handle events from Stripe
|
||||||
|
// before authmiddleware since stripe
|
||||||
|
// and json processing to prevent express from processing buffer as json obj
|
||||||
router.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
|
router.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
|
||||||
// This needs to be called with raw body data
|
// This needs to be called with raw body data
|
||||||
const payload = req.body;
|
const payload = req.body;
|
||||||
|
|
|
||||||
16
db/init/11-notifications.sql
Normal file
16
db/init/11-notifications.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- Add shipping related columns to the orders table
|
||||||
|
ALTER TABLE orders ADD COLUMN IF NOT EXISTS shipping_info JSONB;
|
||||||
|
ALTER TABLE orders ADD COLUMN IF NOT EXISTS shipping_date TIMESTAMP;
|
||||||
|
|
||||||
|
-- Create a notification logs table to track emails sent
|
||||||
|
CREATE TABLE IF NOT EXISTS notification_logs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
order_id UUID NOT NULL REFERENCES orders(id),
|
||||||
|
notification_type VARCHAR(50) NOT NULL,
|
||||||
|
sent_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
status VARCHAR(20) DEFAULT 'success',
|
||||||
|
error_message TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create an index on order_id for faster lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notification_logs_order_id ON notification_logs(order_id);
|
||||||
185
frontend/src/components/OrderStatusDialog.jsx
Normal file
185
frontend/src/components/OrderStatusDialog.jsx
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
CircularProgress,
|
||||||
|
Grid,
|
||||||
|
Typography,
|
||||||
|
Divider,
|
||||||
|
Box
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useUpdateOrderStatus } from '@hooks/adminHooks';
|
||||||
|
|
||||||
|
const OrderStatusDialog = ({ open, onClose, order }) => {
|
||||||
|
const [status, setStatus] = useState(order ? order.status : 'pending');
|
||||||
|
const [shippingData, setShippingData] = useState({
|
||||||
|
shipper: '',
|
||||||
|
trackingNumber: '',
|
||||||
|
shipmentId: '',
|
||||||
|
estimatedDelivery: '',
|
||||||
|
customerMessage: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateOrderStatus = useUpdateOrderStatus();
|
||||||
|
|
||||||
|
const handleStatusChange = (e) => {
|
||||||
|
setStatus(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShippingDataChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setShippingData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// For shipped status, require tracking number
|
||||||
|
if (status === 'shipped' && !shippingData.trackingNumber) {
|
||||||
|
alert('Please enter a tracking number');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOrderStatus.mutate({
|
||||||
|
orderId: order.id,
|
||||||
|
status,
|
||||||
|
...(status === 'shipped' && {
|
||||||
|
shippingData: {
|
||||||
|
...shippingData,
|
||||||
|
// Add current date as shipped date if not provided
|
||||||
|
shippedDate: new Date().toISOString().split('T')[0]
|
||||||
|
},
|
||||||
|
sendNotification: true
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||||
|
<DialogTitle>Update Order Status</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="status-select-label">Status</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="status-select-label"
|
||||||
|
value={status}
|
||||||
|
label="Status"
|
||||||
|
onChange={handleStatusChange}
|
||||||
|
>
|
||||||
|
<MenuItem value="pending">Pending</MenuItem>
|
||||||
|
<MenuItem value="processing">Processing</MenuItem>
|
||||||
|
<MenuItem value="shipped">Shipped</MenuItem>
|
||||||
|
<MenuItem value="delivered">Delivered</MenuItem>
|
||||||
|
<MenuItem value="cancelled">Cancelled</MenuItem>
|
||||||
|
<MenuItem value="refunded">Refunded</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{status === 'shipped' && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<Typography variant="subtitle1" gutterBottom>
|
||||||
|
Shipping Information
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" paragraph>
|
||||||
|
This information will be sent to the customer via email.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Shipping Carrier"
|
||||||
|
name="shipper"
|
||||||
|
value={shippingData.shipper}
|
||||||
|
onChange={handleShippingDataChange}
|
||||||
|
placeholder="USPS, FedEx, UPS, etc."
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
label="Tracking Number"
|
||||||
|
name="trackingNumber"
|
||||||
|
value={shippingData.trackingNumber}
|
||||||
|
onChange={handleShippingDataChange}
|
||||||
|
placeholder="Enter tracking number"
|
||||||
|
error={status === 'shipped' && !shippingData.trackingNumber}
|
||||||
|
helperText={status === 'shipped' && !shippingData.trackingNumber ? 'Tracking number is required' : ''}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Shipment ID"
|
||||||
|
name="shipmentId"
|
||||||
|
value={shippingData.shipmentId}
|
||||||
|
onChange={handleShippingDataChange}
|
||||||
|
placeholder="Optional internal reference"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Estimated Delivery"
|
||||||
|
name="estimatedDelivery"
|
||||||
|
value={shippingData.estimatedDelivery}
|
||||||
|
onChange={handleShippingDataChange}
|
||||||
|
placeholder="e.g., 3-5 business days"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
label="Message to Customer"
|
||||||
|
name="customerMessage"
|
||||||
|
value={shippingData.customerMessage}
|
||||||
|
onChange={handleShippingDataChange}
|
||||||
|
placeholder="Add a personal message to the shipping notification email (optional)"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={updateOrderStatus.isLoading}
|
||||||
|
>
|
||||||
|
{updateOrderStatus.isLoading ? <CircularProgress size={24} /> : 'Save'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderStatusDialog;
|
||||||
|
|
@ -104,7 +104,19 @@ export const useUpdateOrderStatus = () => {
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ orderId, status }) => adminOrderService.updateOrderStatus(orderId, status),
|
mutationFn: ({ orderId, status, shippingData, sendNotification }) => {
|
||||||
|
// If shipping data is provided, include it in the request
|
||||||
|
if (shippingData && sendNotification) {
|
||||||
|
return adminOrderService.updateOrderStatusWithShipping(
|
||||||
|
orderId,
|
||||||
|
status,
|
||||||
|
shippingData,
|
||||||
|
sendNotification
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Otherwise just update the status
|
||||||
|
return adminOrderService.updateOrderStatus(orderId, status);
|
||||||
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin-orders'] });
|
queryClient.invalidateQueries({ queryKey: ['admin-orders'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin-order', data.order.id] });
|
queryClient.invalidateQueries({ queryKey: ['admin-order', data.order.id] });
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,10 @@ import {
|
||||||
LocalShipping as ShippingIcon
|
LocalShipping as ShippingIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import apiClient from '../../services/api';
|
import apiClient from '@services/api';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import ProductImage from '../../components/ProductImage';
|
import ProductImage from '@components/ProductImage';
|
||||||
|
import OrderStatusDialog from '@components/OrderStatusDialog';
|
||||||
|
|
||||||
const AdminOrdersPage = () => {
|
const AdminOrdersPage = () => {
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
|
@ -169,7 +170,6 @@ const AdminOrdersPage = () => {
|
||||||
// Handle opening status change dialog
|
// Handle opening status change dialog
|
||||||
const handleOpenStatusDialog = (order) => {
|
const handleOpenStatusDialog = (order) => {
|
||||||
setSelectedOrder(order);
|
setSelectedOrder(order);
|
||||||
setNewStatus(order.status);
|
|
||||||
setStatusDialogOpen(true);
|
setStatusDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -520,41 +520,11 @@ const AdminOrdersPage = () => {
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Change Status Dialog */}
|
{/* Change Status Dialog */}
|
||||||
<Dialog
|
<OrderStatusDialog
|
||||||
open={statusDialogOpen}
|
open={statusDialogOpen}
|
||||||
onClose={() => setStatusDialogOpen(false)}
|
onClose={() => setStatusDialogOpen(false)}
|
||||||
>
|
order={selectedOrder}
|
||||||
<DialogTitle>Change Order Status</DialogTitle>
|
/>
|
||||||
<DialogContent>
|
|
||||||
<FormControl fullWidth sx={{ mt: 2 }}>
|
|
||||||
<InputLabel id="status-select-label">Status</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="status-select-label"
|
|
||||||
value={newStatus}
|
|
||||||
label="Status"
|
|
||||||
onChange={handleStatusChange}
|
|
||||||
>
|
|
||||||
<MenuItem value="pending">Pending</MenuItem>
|
|
||||||
<MenuItem value="processing">Processing</MenuItem>
|
|
||||||
<MenuItem value="shipped">Shipped</MenuItem>
|
|
||||||
<MenuItem value="delivered">Delivered</MenuItem>
|
|
||||||
<MenuItem value="cancelled">Cancelled</MenuItem>
|
|
||||||
<MenuItem value="refunded">Refunded</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => setStatusDialogOpen(false)}>Cancel</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveStatus}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
disabled={updateOrderStatus.isLoading}
|
|
||||||
>
|
|
||||||
{updateOrderStatus.isLoading ? <CircularProgress size={24} /> : 'Save'}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -102,5 +102,26 @@ export const adminOrderService = {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error.response?.data || { message: 'An unknown error occurred' };
|
throw error.response?.data || { message: 'An unknown error occurred' };
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an order's status with shipping information (admin only)
|
||||||
|
* @param {string} id - Order ID
|
||||||
|
* @param {string} status - New order status
|
||||||
|
* @param {Object} shippingData - Shipping information
|
||||||
|
* @param {boolean} sendNotification - Whether to send email notification
|
||||||
|
* @returns {Promise} Promise with the API response
|
||||||
|
*/
|
||||||
|
updateOrderStatusWithShipping: async (id, status, shippingData, sendNotification = true) => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.patch(`/admin/orders/${id}/shipping`, {
|
||||||
|
status,
|
||||||
|
shippingData,
|
||||||
|
sendNotification
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response?.data || { message: 'An unknown error occurred' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Loading…
Reference in a new issue