diff --git a/backend/src/routes/orderAdmin.js b/backend/src/routes/orderAdmin.js
index c0eaee3..bbb6a56 100644
--- a/backend/src/routes/orderAdmin.js
+++ b/backend/src/routes/orderAdmin.js
@@ -1,11 +1,25 @@
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
+ // Get all orders (admin only)
router.get('/', async (req, res, next) => {
try {
// 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) => {
try {
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 => `
+
+ | ${item.product_name} |
+ ${item.quantity} |
+ $${parseFloat(item.price_at_purchase).toFixed(2)} |
+ $${(parseFloat(item.price_at_purchase) * item.quantity).toFixed(2)} |
+
+ `).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 = `
+
+
+
Your Order Has Shipped!
+
Order #${order.id.substring(0, 8)}
+
+
+
+
Hello ${order.first_name},
+
+
Good news! Your order has been shipped and is on its way to you.
+
+ ${shippingData.customerMessage ? `
Message from our team: ${shippingData.customerMessage}
` : ''}
+
+
+
Shipping Details
+
Carrier: ${shippingData.shipper || 'Standard Shipping'}
+
Tracking Number: ${shippingData.trackingNumber}
+
Shipped On: ${shippedDate}
+ ${shippingData.estimatedDelivery ? `
Estimated Delivery: ${shippingData.estimatedDelivery}
` : ''}
+
+
+
+
Order Summary
+
+
+
+ | Item |
+ Qty |
+ Price |
+ Total |
+
+
+
+ ${itemsHtml}
+
+
+
+ | Total: |
+ $${orderTotal.toFixed(2)} |
+
+
+
+
+
+
+
Thank you for your purchase! If you have any questions, please contact our customer service.
+
+
+
+
+
© ${new Date().getFullYear()} Rocks, Bones & Sticks. All rights reserved.
+
+
+ `;
+
+ // 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;
};
\ No newline at end of file
diff --git a/backend/src/routes/stripePayment.js b/backend/src/routes/stripePayment.js
index ab2ce7a..7ccc715 100644
--- a/backend/src/routes/stripePayment.js
+++ b/backend/src/routes/stripePayment.js
@@ -6,6 +6,8 @@ const config = require('../config');
module.exports = (pool, query, authMiddleware) => {
// 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) => {
// This needs to be called with raw body data
const payload = req.body;
diff --git a/db/init/11-notifications.sql b/db/init/11-notifications.sql
new file mode 100644
index 0000000..ba40e2a
--- /dev/null
+++ b/db/init/11-notifications.sql
@@ -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);
\ No newline at end of file
diff --git a/frontend/src/components/OrderStatusDialog.jsx b/frontend/src/components/OrderStatusDialog.jsx
new file mode 100644
index 0000000..b22da7e
--- /dev/null
+++ b/frontend/src/components/OrderStatusDialog.jsx
@@ -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 (
+
+ );
+};
+
+export default OrderStatusDialog;
\ No newline at end of file
diff --git a/frontend/src/hooks/adminHooks.js b/frontend/src/hooks/adminHooks.js
index 8aa609f..088813f 100644
--- a/frontend/src/hooks/adminHooks.js
+++ b/frontend/src/hooks/adminHooks.js
@@ -104,7 +104,19 @@ export const useUpdateOrderStatus = () => {
const notification = useNotification();
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) => {
queryClient.invalidateQueries({ queryKey: ['admin-orders'] });
queryClient.invalidateQueries({ queryKey: ['admin-order', data.order.id] });
diff --git a/frontend/src/pages/Admin/OrdersPage.jsx b/frontend/src/pages/Admin/OrdersPage.jsx
index 7149cb0..75c67ac 100644
--- a/frontend/src/pages/Admin/OrdersPage.jsx
+++ b/frontend/src/pages/Admin/OrdersPage.jsx
@@ -41,9 +41,10 @@ import {
LocalShipping as ShippingIcon
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import apiClient from '../../services/api';
+import apiClient from '@services/api';
import { format } from 'date-fns';
-import ProductImage from '../../components/ProductImage';
+import ProductImage from '@components/ProductImage';
+import OrderStatusDialog from '@components/OrderStatusDialog';
const AdminOrdersPage = () => {
const [page, setPage] = useState(0);
@@ -169,7 +170,6 @@ const AdminOrdersPage = () => {
// Handle opening status change dialog
const handleOpenStatusDialog = (order) => {
setSelectedOrder(order);
- setNewStatus(order.status);
setStatusDialogOpen(true);
};
@@ -520,41 +520,11 @@ const AdminOrdersPage = () => {
{/* Change Status Dialog */}
-
+ order={selectedOrder}
+ />
);
};
diff --git a/frontend/src/services/adminService.js b/frontend/src/services/adminService.js
index a511e17..74843d6 100644
--- a/frontend/src/services/adminService.js
+++ b/frontend/src/services/adminService.js
@@ -102,5 +102,26 @@ export const adminOrderService = {
} catch (error) {
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' };
+ }
}
};
\ No newline at end of file