orders page
This commit is contained in:
parent
82b3f2e1d1
commit
f251fdf823
8 changed files with 814 additions and 23 deletions
|
|
@ -20,6 +20,7 @@ const productAdminRoutes = require('./routes/productAdmin');
|
|||
const categoryAdminRoutes = require('./routes/categoryAdmin'); // Add category admin routes
|
||||
const usersAdminRoutes = require('./routes/userAdmin');
|
||||
const ordersAdminRoutes = require('./routes/orderAdmin');
|
||||
const userOrdersRoutes = require('./routes/userOrders');
|
||||
// Create Express app
|
||||
const app = express();
|
||||
const port = config.port || 4000;
|
||||
|
|
@ -245,6 +246,7 @@ app.delete('/api/image/product/:filename', adminAuthMiddleware(pool, query), (re
|
|||
app.use('/api/admin/settings', settingsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
|
||||
app.use('/api/products', productRoutes(pool, query));
|
||||
app.use('/api/auth', authRoutes(pool, query));
|
||||
app.use('/api/user/orders', userOrdersRoutes(pool, query, authMiddleware(pool, query)));
|
||||
app.use('/api/cart', cartRoutes(pool, query, authMiddleware(pool, query)));
|
||||
app.use('/api/admin/products', productAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
|
||||
|
||||
|
|
|
|||
|
|
@ -60,9 +60,16 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
});
|
||||
}
|
||||
|
||||
// Get order details
|
||||
// Get order details with shipping information
|
||||
const orderResult = await query(`
|
||||
SELECT o.*, u.email, u.first_name, u.last_name
|
||||
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
|
||||
|
|
|
|||
103
backend/src/routes/userOrders.js
Normal file
103
backend/src/routes/userOrders.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// Create a new file called userOrders.js in the routes directory
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
module.exports = (pool, query, authMiddleware) => {
|
||||
// Apply authentication middleware to all routes
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Get all orders for the authenticated user
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
// Get orders with basic information
|
||||
const result = await query(`
|
||||
SELECT
|
||||
o.id,
|
||||
o.status,
|
||||
o.total_amount,
|
||||
o.shipping_address,
|
||||
o.created_at,
|
||||
o.updated_at,
|
||||
o.payment_completed,
|
||||
o.shipping_date,
|
||||
COUNT(oi.id) AS item_count,
|
||||
CASE WHEN o.shipping_info IS NOT NULL THEN true ELSE false END AS has_shipping_info,
|
||||
(SELECT SUM(quantity) FROM order_items WHERE order_id = o.id) AS total_items
|
||||
FROM orders o
|
||||
LEFT JOIN order_items oi ON o.id = oi.order_id
|
||||
WHERE o.user_id = $1
|
||||
GROUP BY o.id
|
||||
ORDER BY o.created_at DESC
|
||||
`, [userId]);
|
||||
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get a single order with details for the authenticated user
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
// Get order with verification that it belongs to the current user
|
||||
const orderResult = await query(`
|
||||
SELECT o.*
|
||||
FROM orders o
|
||||
WHERE o.id = $1 AND o.user_id = $2
|
||||
`, [id, userId]);
|
||||
|
||||
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.id,
|
||||
oi.product_id,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase,
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
|
|
@ -11,24 +11,25 @@ import AuthLayout from './layouts/AuthLayout';
|
|||
import AdminLayout from './layouts/AdminLayout';
|
||||
|
||||
// Pages - lazy loaded to reduce initial bundle size
|
||||
const HomePage = lazy(() => import('./pages/HomePage'));
|
||||
const ProductsPage = lazy(() => import('./pages/ProductsPage'));
|
||||
const ProductDetailPage = lazy(() => import('./pages/ProductDetailPage'));
|
||||
const CartPage = lazy(() => import('./pages/CartPage'));
|
||||
const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
|
||||
const PaymentSuccessPage = lazy(() => import('./pages/PaymentSuccessPage'));
|
||||
const PaymentCancelPage = lazy(() => import('./pages/PaymentCancelPage'));
|
||||
const LoginPage = lazy(() => import('./pages/LoginPage'));
|
||||
const RegisterPage = lazy(() => import('./pages/RegisterPage'));
|
||||
const VerifyPage = lazy(() => import('./pages/VerifyPage'));
|
||||
const AdminDashboardPage = lazy(() => import('./pages/Admin/DashboardPage'));
|
||||
const AdminProductsPage = lazy(() => import('./pages/Admin/ProductsPage'));
|
||||
const AdminProductEditPage = lazy(() => import('./pages/Admin/ProductEditPage'));
|
||||
const AdminCategoriesPage = lazy(() => import('./pages/Admin/CategoriesPage'));
|
||||
const AdminCustomersPage = lazy(() => import('./pages/Admin/CustomersPage'));
|
||||
const AdminOrdersPage = lazy(() => import('./pages/Admin/OrdersPage'));
|
||||
const AdminSettingsPage = lazy(() => import('./pages/Admin/SettingsPage'));
|
||||
const NotFoundPage = lazy(() => import('./pages/NotFoundPage'));
|
||||
const HomePage = lazy(() => import('@pages/HomePage'));
|
||||
const ProductsPage = lazy(() => import('@pages/ProductsPage'));
|
||||
const ProductDetailPage = lazy(() => import('@pages/ProductDetailPage'));
|
||||
const CartPage = lazy(() => import('@pages/CartPage'));
|
||||
const CheckoutPage = lazy(() => import('@pages/CheckoutPage'));
|
||||
const PaymentSuccessPage = lazy(() => import('@pages/PaymentSuccessPage'));
|
||||
const PaymentCancelPage = lazy(() => import('@pages/PaymentCancelPage'));
|
||||
const LoginPage = lazy(() => import('@pages/LoginPage'));
|
||||
const RegisterPage = lazy(() => import('@pages/RegisterPage'));
|
||||
const VerifyPage = lazy(() => import('@pages/VerifyPage'));
|
||||
const AdminDashboardPage = lazy(() => import('@pages/Admin/DashboardPage'));
|
||||
const AdminProductsPage = lazy(() => import('@pages/Admin/ProductsPage'));
|
||||
const AdminProductEditPage = lazy(() => import('@pages/Admin/ProductEditPage'));
|
||||
const AdminCategoriesPage = lazy(() => import('@pages/Admin/CategoriesPage'));
|
||||
const AdminCustomersPage = lazy(() => import('@pages/Admin/CustomersPage'));
|
||||
const AdminOrdersPage = lazy(() => import('@pages/Admin/OrdersPage'));
|
||||
const AdminSettingsPage = lazy(() => import('@pages/Admin/SettingsPage'));
|
||||
const UserOrdersPage = lazy(() => import('@pages/UserOrdersPage'));
|
||||
const NotFoundPage = lazy(() => import('@pages/NotFoundPage'));
|
||||
|
||||
// Loading component for suspense fallback
|
||||
const LoadingComponent = () => (
|
||||
|
|
@ -45,6 +46,11 @@ function App() {
|
|||
<Routes>
|
||||
{/* Main routes with MainLayout */}
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route path="account/orders" element={
|
||||
<ProtectedRoute>
|
||||
<UserOrdersPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route index element={<HomePage />} />
|
||||
<Route path="products" element={<ProductsPage />} />
|
||||
<Route path="products/:id" element={<ProductDetailPage />} />
|
||||
|
|
|
|||
|
|
@ -269,3 +269,39 @@ export const useCheckout = () => {
|
|||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Hook for fetching user's orders
|
||||
*/
|
||||
export const useUserOrders = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['user-orders', user],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/user/orders');
|
||||
return response.data;
|
||||
},
|
||||
enabled: !!user,
|
||||
staleTime: 60000 // 1 minute
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for fetching a single user order by ID
|
||||
* @param {string} id - Order ID
|
||||
*/
|
||||
export const useUserOrder = (id) => {
|
||||
const { user } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['user-order', id],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get(`/user/orders/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
enabled: !!id && !!user
|
||||
});
|
||||
};
|
||||
|
|
@ -14,6 +14,7 @@ import LogoutIcon from '@mui/icons-material/Logout';
|
|||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import Brightness4Icon from '@mui/icons-material/Brightness4';
|
||||
import Brightness7Icon from '@mui/icons-material/Brightness7';
|
||||
import ReceiptIcon from '@mui/icons-material/Receipt';
|
||||
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
||||
import { useAuth, useCart, useDarkMode } from '../hooks/reduxHooks';
|
||||
import Footer from '../components/Footer';
|
||||
|
|
@ -37,11 +38,22 @@ const MainLayout = () => {
|
|||
navigate('/');
|
||||
};
|
||||
|
||||
const mainMenu = [
|
||||
let mainMenu = [
|
||||
{ text: 'Home', icon: <HomeIcon />, path: '/' },
|
||||
{ text: 'Products', icon: <CategoryIcon />, path: '/products' },
|
||||
{ text: 'Cart', icon: <ShoppingCartIcon />, path: '/cart', badge: itemCount > 0 ? itemCount : null },
|
||||
];
|
||||
if (isAuthenticated) {
|
||||
mainMenu.push(
|
||||
{ text: 'My Orders', icon: <ReceiptIcon />, path: '/account/orders' }
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated && isAdmin) {
|
||||
mainMenu.push(
|
||||
{ text: 'Admin Dashboard', icon: <DashboardIcon />, path: '/admin' }
|
||||
);
|
||||
}
|
||||
|
||||
const authMenu = isAuthenticated ?
|
||||
[
|
||||
|
|
|
|||
|
|
@ -32,13 +32,15 @@ import {
|
|||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Link
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
Clear as ClearIcon,
|
||||
Visibility as ViewIcon,
|
||||
LocalShipping as ShippingIcon
|
||||
LocalShipping as ShippingIcon,
|
||||
CheckCircle as CheckCircleIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import apiClient from '@services/api';
|
||||
|
|
@ -441,6 +443,155 @@ const AdminOrdersPage = () => {
|
|||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Shipping Information - Only show for shipped orders */}
|
||||
{orderDetails && orderDetails.status === 'shipped' && orderDetails.shipping_info && (
|
||||
<Grid item xs={12} sx={{ mt: 2 }}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Shipping Information
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{(() => {
|
||||
// Parse shipping info from JSON if needed
|
||||
let shippingInfo = orderDetails.shipping_info;
|
||||
if (typeof shippingInfo === 'string') {
|
||||
try {
|
||||
shippingInfo = JSON.parse(shippingInfo);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse shipping info:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tracking link
|
||||
let trackingLink = '#';
|
||||
const shipper = shippingInfo.shipper || '';
|
||||
const trackingNumber = shippingInfo.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:
|
||||
trackingLink = '#';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Carrier:
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{shippingInfo.shipper || 'Not specified'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Tracking Number:
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{trackingNumber ? (
|
||||
<Link href={trackingLink} target="_blank" rel="noopener noreferrer">
|
||||
{trackingNumber}
|
||||
</Link>
|
||||
) : (
|
||||
'Not available'
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{shippingInfo.shipmentId && (
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Shipment ID:
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{shippingInfo.shipmentId}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Shipped Date:
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{orderDetails.shipping_date
|
||||
? formatDate(orderDetails.shipping_date)
|
||||
: (shippingInfo.shippedDate
|
||||
? formatDate(shippingInfo.shippedDate)
|
||||
: 'Not available')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{shippingInfo.estimatedDelivery && (
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Estimated Delivery:
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{shippingInfo.estimatedDelivery}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{shippingInfo.customerMessage && (
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Message to Customer:
|
||||
</Typography>
|
||||
<Paper variant="outlined" sx={{ p: 2, mt: 1, bgcolor: 'background.paper' }}>
|
||||
<Typography variant="body2">
|
||||
{shippingInfo.customerMessage}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Check if a notification was sent */}
|
||||
{orderDetails.notification_sent && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon />}
|
||||
label="Shipping notification email sent"
|
||||
color="success"
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
|
||||
{/* Order Items */}
|
||||
<Grid item xs={12}>
|
||||
<Card variant="outlined">
|
||||
|
|
|
|||
474
frontend/src/pages/UserOrdersPage.jsx
Normal file
474
frontend/src/pages/UserOrdersPage.jsx
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TablePagination,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Link
|
||||
} from '@mui/material';
|
||||
import { format } from 'date-fns';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useUserOrders, useUserOrder } from '../hooks/apiHooks';
|
||||
import ProductImage from '../components/ProductImage';
|
||||
|
||||
const UserOrdersPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [viewDialogOpen, setViewDialogOpen] = useState(false);
|
||||
const [selectedOrderId, setSelectedOrderId] = useState(null);
|
||||
|
||||
// Fetch user's orders
|
||||
const { data: orders, isLoading, error } = useUserOrders();
|
||||
|
||||
// Fetch selected order details when dialog is open
|
||||
const {
|
||||
data: orderDetails,
|
||||
isLoading: isLoadingDetails,
|
||||
} = useUserOrder(selectedOrderId);
|
||||
|
||||
// Handle pagination
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
// Handle opening order details dialog
|
||||
const handleViewOrder = (orderId) => {
|
||||
setSelectedOrderId(orderId);
|
||||
setViewDialogOpen(true);
|
||||
};
|
||||
|
||||
// Close view dialog
|
||||
const handleCloseViewDialog = () => {
|
||||
setViewDialogOpen(false);
|
||||
setSelectedOrderId(null);
|
||||
};
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return 'N/A';
|
||||
try {
|
||||
return format(new Date(dateString), 'MMM d, yyyy h:mm a');
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
// Get status chip color
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'default';
|
||||
case 'processing':
|
||||
return 'primary';
|
||||
case 'shipped':
|
||||
return 'info';
|
||||
case 'delivered':
|
||||
return 'success';
|
||||
case 'cancelled':
|
||||
return 'error';
|
||||
case 'refunded':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" sx={{ my: 2 }}>
|
||||
Error loading your orders: {error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
// Paginated orders
|
||||
const paginatedOrders = orders?.slice(
|
||||
page * rowsPerPage,
|
||||
page * rowsPerPage + rowsPerPage
|
||||
) || [];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
My Orders
|
||||
</Typography>
|
||||
|
||||
{/* Orders Table */}
|
||||
<Paper variant="outlined">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Order ID</TableCell>
|
||||
<TableCell>Date</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Total</TableCell>
|
||||
<TableCell>Items</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paginatedOrders.length > 0 ? (
|
||||
paginatedOrders.map((order) => (
|
||||
<TableRow key={order.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
|
||||
{order.id.substring(0, 8)}...
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(order.created_at)}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={order.status}
|
||||
color={getStatusColor(order.status)}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
${parseFloat(order.total_amount).toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell>{order.total_items || 0}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => handleViewOrder(order.id)}
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} align="center">
|
||||
<Typography variant="body1" py={3}>
|
||||
You haven't placed any orders yet.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate('/products')}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Browse Products
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{orders && orders.length > 0 && (
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={orders.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Order Details Dialog */}
|
||||
<Dialog
|
||||
open={viewDialogOpen}
|
||||
onClose={handleCloseViewDialog}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
Order Details
|
||||
{orderDetails && (
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
||||
Order ID: {orderDetails.id}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{isLoadingDetails ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : orderDetails ? (
|
||||
<Grid container spacing={3}>
|
||||
{/* Order Summary */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Order Summary
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Date:
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Typography variant="body2">
|
||||
{formatDate(orderDetails.created_at)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Status:
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Chip
|
||||
label={orderDetails.status}
|
||||
color={getStatusColor(orderDetails.status)}
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Total:
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
${parseFloat(orderDetails.total_amount).toFixed(2)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Shipping Address */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Shipping Address
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-line' }}>
|
||||
{orderDetails.shipping_address}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Shipping Information - Show only for shipped orders */}
|
||||
{orderDetails.status === 'shipped' && orderDetails.shipping_info && (
|
||||
<Grid item xs={12}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Shipping Information
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{(() => {
|
||||
// Parse shipping info from JSON if needed
|
||||
let shippingInfo = orderDetails.shipping_info;
|
||||
if (typeof shippingInfo === 'string') {
|
||||
try {
|
||||
shippingInfo = JSON.parse(shippingInfo);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse shipping info:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tracking link
|
||||
let trackingLink = '#';
|
||||
const shipper = shippingInfo.shipper || '';
|
||||
const trackingNumber = shippingInfo.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:
|
||||
trackingLink = '#';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Shipping Carrier:
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{shippingInfo.shipper || 'Not specified'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Tracking Number:
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{trackingNumber ? (
|
||||
<Link href={trackingLink} target="_blank" rel="noopener noreferrer">
|
||||
{trackingNumber}
|
||||
</Link>
|
||||
) : (
|
||||
'Not available'
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Shipped Date:
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{orderDetails.shipping_date
|
||||
? formatDate(orderDetails.shipping_date)
|
||||
: (shippingInfo.shippedDate
|
||||
? formatDate(shippingInfo.shippedDate)
|
||||
: 'Not available')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{shippingInfo.estimatedDelivery && (
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Estimated Delivery:
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{shippingInfo.estimatedDelivery}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
})()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Order Items */}
|
||||
<Grid item xs={12}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Order Items
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<List>
|
||||
{orderDetails.items && orderDetails.items.map((item, index) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<ListItem alignItems="flex-start">
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={2} sm={1}>
|
||||
<ProductImage
|
||||
images={item.product_images}
|
||||
alt={item.product_name}
|
||||
sx={{ width: 50, height: 50, borderRadius: 1 }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={10} sm={7}>
|
||||
<ListItemText
|
||||
primary={item.product_name}
|
||||
secondary={
|
||||
<React.Fragment>
|
||||
<Typography variant="caption" component="span" color="text.primary">
|
||||
Category: {item.product_category}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4} sm={2} textAlign="center">
|
||||
<Typography variant="body2">
|
||||
${parseFloat(item.price_at_purchase).toFixed(2)} × {item.quantity}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={8} sm={2} textAlign="right">
|
||||
<Typography variant="body1" fontWeight="bold">
|
||||
${(parseFloat(item.price_at_purchase) * item.quantity).toFixed(2)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
{index < orderDetails.items.length - 1 && <Divider />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography color="error">Failed to load order details</Typography>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseViewDialog}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserOrdersPage;
|
||||
Loading…
Reference in a new issue