406 lines
No EOL
13 KiB
JavaScript
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>© ${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;
|
|
}; |