orders page

This commit is contained in:
2ManyProjects 2025-04-26 23:43:27 -05:00
parent 82b3f2e1d1
commit f251fdf823
8 changed files with 814 additions and 23 deletions

View file

@ -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)));

View file

@ -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

View 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;
};

View file

@ -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 />} />

View file

@ -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
});
};

View file

@ -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 ?
[

View file

@ -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">

View 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;