E-Commerce-Module/backend/src/routes/orderAdmin.js
2025-04-26 23:43:27 -05:00

406 lines
No EOL
13 KiB
JavaScript

const express = require('express');
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) => {
// Apply authentication middleware to all routes
router.use(authMiddleware);
// Get all orders (admin only)
router.get('/', async (req, res, next) => {
try {
// Check if user is admin
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
const result = await query(`
SELECT o.*,
u.email, u.first_name, u.last_name,
COUNT(oi.id) AS item_count
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN order_items oi ON o.id = oi.order_id
GROUP BY o.id, u.id
ORDER BY o.created_at DESC
`);
res.json(result.rows);
} catch (error) {
next(error);
}
});
// Get single order with items
router.get('/:id', async (req, res, next) => {
try {
const { id } = req.params;
// Check if user is admin
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
// Get order details with shipping information
const orderResult = await query(`
SELECT o.*,
u.email,
u.first_name,
u.last_name,
(SELECT EXISTS(
SELECT 1 FROM notification_logs
WHERE order_id = o.id AND notification_type = 'shipping_notification'
)) as notification_sent
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'
});
}
// Get order items with product details
const itemsResult = await query(`
SELECT oi.*,
p.name as product_name,
p.description as product_description,
pc.name as product_category,
(
SELECT json_agg(
json_build_object(
'id', pi.id,
'path', pi.image_path,
'isPrimary', pi.is_primary,
'displayOrder', pi.display_order
) ORDER BY pi.display_order
)
FROM product_images pi
WHERE pi.product_id = p.id
) AS product_images
FROM order_items oi
JOIN products p ON oi.product_id = p.id
JOIN product_categories pc ON p.category_id = pc.id
WHERE oi.order_id = $1
`, [id]);
// Combine order with items
const order = {
...orderResult.rows[0],
items: itemsResult.rows
};
res.json(order);
} catch (error) {
next(error);
}
});
// Update order status (simple version)
router.patch('/:id', async (req, res, next) => {
try {
const { id } = req.params;
const { status } = 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(', ')}`
});
}
// Update order status
const result = await query(`
UPDATE orders
SET status = $1, updated_at = NOW()
WHERE id = $2
RETURNING *
`, [status, id]);
if (result.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Order not found'
});
}
res.json({
message: 'Order status updated successfully',
order: result.rows[0]
});
} catch (error) {
next(error);
}
});
// 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 || '';
const trackingNumber = shippingData.trackingNumber;
if (trackingNumber) {
// Match exactly with the values from the dropdown
switch(shipper) {
case 'USPS':
trackingLink = `https://tools.usps.com/go/TrackConfirmAction?tLabels=${trackingNumber}`;
break;
case 'UPS':
trackingLink = `https://www.ups.com/track?tracknum=${trackingNumber}`;
break;
case 'FedEx':
trackingLink = `https://www.fedex.com/apps/fedextrack/?tracknumbers=${trackingNumber}`;
break;
case 'DHL':
trackingLink = `https://www.dhl.com/global-en/home/tracking.html?tracking-id=${trackingNumber}`;
break;
case 'Canada Post':
trackingLink = `https://www.canadapost-postescanada.ca/track-reperage/en#/search?searchFor=${trackingNumber}`;
break;
case 'Purolator':
trackingLink = `https://www.purolator.com/en/shipping/track/tracking-number/${trackingNumber}`;
break;
default:
// For "other" or any carrier not in our list
// Just make the tracking number text without a link
trackingLink = '#';
break;
}
}
// 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>&copy; ${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;
};