- Implemented a tracking shipment link in the OrdersTab component for DHL deliveries, enhancing user experience by allowing direct access to shipment tracking. - Added 'trackShipment' translation key across multiple languages to support the new feature. - Updated existing translations for consistency and improved localization in the orders module.
360 lines
12 KiB
JavaScript
360 lines
12 KiB
JavaScript
import React, { useState, useEffect, useCallback } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { withI18n } from "../../i18n/withTranslation.js";
|
|
import {
|
|
Box,
|
|
Paper,
|
|
Alert,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
IconButton,
|
|
Tooltip,
|
|
CircularProgress,
|
|
Typography,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Button,
|
|
} from "@mui/material";
|
|
import SearchIcon from "@mui/icons-material/Search";
|
|
import CancelIcon from "@mui/icons-material/Cancel";
|
|
import OrderDetailsDialog from "./OrderDetailsDialog.js";
|
|
|
|
// Constants
|
|
const getStatusTranslation = (status, t) => {
|
|
const statusMap = {
|
|
new: t ? t('orders.status.new') : "in Bearbeitung",
|
|
pending: t ? t('orders.status.pending') : "Neu",
|
|
processing: t ? t('orders.status.processing') : "in Bearbeitung",
|
|
paid: t ? t('orders.status.paid') : "Bezahlt",
|
|
cancelled: t ? t('orders.status.cancelled') : "Storniert",
|
|
shipped: t ? t('orders.status.shipped') : "Verschickt",
|
|
delivered: t ? t('orders.status.delivered') : "Geliefert",
|
|
};
|
|
return statusMap[status] || status;
|
|
};
|
|
|
|
const statusEmojis = {
|
|
new: "⚙️",
|
|
pending: "⏳",
|
|
processing: "🔄",
|
|
paid: "🏦",
|
|
cancelled: "❌",
|
|
shipped: "🚚",
|
|
delivered: "✅",
|
|
};
|
|
|
|
const statusColors = {
|
|
new: "#ed6c02", // orange
|
|
pending: "#ff9800", // orange for pending
|
|
processing: "#2196f3", // blue for processing
|
|
paid: "#2e7d32", // green
|
|
cancelled: "#d32f2f", // red for cancelled
|
|
shipped: "#2e7d32", // green
|
|
delivered: "#2e7d32", // green
|
|
};
|
|
|
|
const currencyFormatter = new Intl.NumberFormat("de-DE", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
});
|
|
|
|
// Orders Tab Content Component
|
|
const OrdersTab = ({ orderIdFromHash, t }) => {
|
|
const [orders, setOrders] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [selectedOrder, setSelectedOrder] = useState(null);
|
|
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
|
|
const [cancelConfirmOpen, setCancelConfirmOpen] = useState(false);
|
|
const [orderToCancel, setOrderToCancel] = useState(null);
|
|
const [isCancelling, setIsCancelling] = useState(false);
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const handleViewDetails = useCallback(
|
|
(orderId) => {
|
|
const orderToView = orders.find((order) => order.orderId === orderId);
|
|
if (orderToView) {
|
|
setSelectedOrder(orderToView);
|
|
setIsDetailsDialogOpen(true);
|
|
// Update the hash to include the order ID
|
|
navigate(`/profile#${orderId}`, { replace: true });
|
|
}
|
|
},
|
|
[orders, navigate]
|
|
);
|
|
|
|
const fetchOrders = useCallback(() => {
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
window.socketManager.emit("getOrders", (response) => {
|
|
if (response.success) {
|
|
setOrders(response.orders);
|
|
} else {
|
|
setError(response.error || "Failed to fetch orders.");
|
|
}
|
|
setLoading(false);
|
|
});
|
|
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchOrders();
|
|
}, [fetchOrders]);
|
|
|
|
useEffect(() => {
|
|
if (orderIdFromHash && orders.length > 0) {
|
|
handleViewDetails(orderIdFromHash);
|
|
}
|
|
}, [orderIdFromHash, orders, handleViewDetails]);
|
|
|
|
const getStatusDisplay = (status) => {
|
|
return getStatusTranslation(status, t);
|
|
};
|
|
|
|
const getStatusEmoji = (status) => {
|
|
return statusEmojis[status] || "❓";
|
|
};
|
|
|
|
const getStatusColor = (status) => {
|
|
return statusColors[status] || "#757575"; // default gray
|
|
};
|
|
|
|
const handleCloseDetailsDialog = () => {
|
|
setIsDetailsDialogOpen(false);
|
|
setSelectedOrder(null);
|
|
navigate("/profile#orders", { replace: true });
|
|
};
|
|
|
|
// Check if order can be cancelled
|
|
const isOrderCancelable = (order) => {
|
|
const cancelableStatuses = ['new', 'pending', 'processing'];
|
|
return cancelableStatuses.includes(order.status);
|
|
};
|
|
|
|
// Handle cancel button click
|
|
const handleCancelClick = (order) => {
|
|
setOrderToCancel(order);
|
|
setCancelConfirmOpen(true);
|
|
};
|
|
|
|
// Handle cancel confirmation
|
|
const handleConfirmCancel = () => {
|
|
if (!orderToCancel) return;
|
|
|
|
setIsCancelling(true);
|
|
window.socketManager.emit('cancelOrder', { orderId: orderToCancel.orderId }, (response) => {
|
|
setIsCancelling(false);
|
|
setCancelConfirmOpen(false);
|
|
|
|
if (response.success) {
|
|
console.log('Order cancelled:', response.orderId);
|
|
// Refresh orders list
|
|
fetchOrders();
|
|
} else {
|
|
setError(response.error || 'Failed to cancel order');
|
|
}
|
|
|
|
setOrderToCancel(null);
|
|
});
|
|
};
|
|
|
|
// Handle cancel dialog close
|
|
const handleCancelDialogClose = () => {
|
|
if (!isCancelling) {
|
|
setCancelConfirmOpen(false);
|
|
setOrderToCancel(null);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Box sx={{ p: { xs: 1, sm: 3 }, display: "flex", justifyContent: "center" }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
|
<Alert severity="error">{error}</Alert>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
|
{orders.length > 0 ? (
|
|
<TableContainer component={Paper}>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>{t ? t('orders.table.orderNumber') : 'Bestellnummer'}</TableCell>
|
|
<TableCell>{t ? t('orders.table.date') : 'Datum'}</TableCell>
|
|
<TableCell>{t ? t('orders.table.status') : 'Status'}</TableCell>
|
|
<TableCell>{t ? t('orders.table.items') : 'Artikel'}</TableCell>
|
|
<TableCell align="right">{t ? t('orders.table.total') : 'Summe'}</TableCell>
|
|
<TableCell align="center">{t ? t('orders.table.actions') : 'Aktionen'}</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{orders.map((order) => {
|
|
const displayStatus = getStatusDisplay(order.status);
|
|
const total = order.items.reduce(
|
|
(acc, item) => acc + item.price * item.quantity_ordered,
|
|
0
|
|
);
|
|
return (
|
|
<TableRow key={order.orderId} hover>
|
|
<TableCell>{order.orderId}</TableCell>
|
|
<TableCell>
|
|
{new Date(order.created_at).toLocaleDateString()}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "8px",
|
|
color: getStatusColor(order.status),
|
|
}}
|
|
>
|
|
<span style={{ fontSize: "1.2rem" }}>
|
|
{getStatusEmoji(order.status)}
|
|
</span>
|
|
<Typography
|
|
variant="body2"
|
|
component="span"
|
|
sx={{ fontWeight: "medium" }}
|
|
>
|
|
{displayStatus}
|
|
</Typography>
|
|
</Box>
|
|
{order.delivery_method === 'DHL' && order.trackingCode && (
|
|
<Box sx={{ mt: 0.5 }}>
|
|
<a
|
|
href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${order.trackingCode}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{ fontSize: '0.85rem', color: '#d40511' }}
|
|
>
|
|
📦 {t ? t('orders.trackShipment') : 'Sendung verfolgen'}
|
|
</a>
|
|
</Box>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{order.items
|
|
.filter(item => {
|
|
// Exclude delivery items - backend uses deliveryMethod ID as item name
|
|
const itemName = item.name || '';
|
|
return itemName !== 'DHL' &&
|
|
itemName !== 'DPD' &&
|
|
itemName !== 'Sperrgut' &&
|
|
itemName !== 'Abholung';
|
|
})
|
|
.reduce(
|
|
(acc, item) => acc + item.quantity_ordered,
|
|
0
|
|
)}
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
{currencyFormatter.format(total)}
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'center' }}>
|
|
<Tooltip title={t ? t('orders.tooltips.viewDetails') : 'Details anzeigen'}>
|
|
<IconButton
|
|
size="small"
|
|
color="primary"
|
|
onClick={() => handleViewDetails(order.orderId)}
|
|
aria-label={t ? t('orders.tooltips.viewDetails') : 'Details anzeigen'}
|
|
>
|
|
<SearchIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
{isOrderCancelable(order) && (
|
|
<Tooltip title={t ? t('orders.tooltips.cancelOrder') : 'Bestellung stornieren'}>
|
|
<IconButton
|
|
size="small"
|
|
color="error"
|
|
onClick={() => handleCancelClick(order)}
|
|
aria-label={t ? t('orders.tooltips.cancelOrder') : 'Bestellung stornieren'}
|
|
>
|
|
<CancelIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
) : (
|
|
<Alert severity="info">
|
|
{t ? t('orders.noOrders') : 'Sie haben noch keine Bestellungen aufgegeben.'}
|
|
</Alert>
|
|
)}
|
|
<OrderDetailsDialog
|
|
open={isDetailsDialogOpen}
|
|
onClose={handleCloseDetailsDialog}
|
|
order={selectedOrder}
|
|
/>
|
|
|
|
{/* Cancel Confirmation Dialog */}
|
|
<Dialog
|
|
open={cancelConfirmOpen}
|
|
onClose={handleCancelDialogClose}
|
|
maxWidth="sm"
|
|
fullWidth
|
|
>
|
|
<DialogTitle>
|
|
{t ? t('orders.cancelConfirm.title') : 'Bestellung stornieren'}
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<Typography>
|
|
{t ? t('orders.cancelConfirm.message') : 'Sind Sie sicher, dass Sie diese Bestellung stornieren möchten?'}
|
|
</Typography>
|
|
{orderToCancel && (
|
|
<Typography variant="body2" sx={{ mt: 1, fontWeight: 'bold' }}>
|
|
{t ? t('orders.table.orderNumber') : 'Bestellnummer'}: {orderToCancel.orderId}
|
|
</Typography>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
onClick={handleCancelDialogClose}
|
|
disabled={isCancelling}
|
|
>
|
|
{t ? t('common.cancel') : 'Abbrechen'}
|
|
</Button>
|
|
<Button
|
|
onClick={handleConfirmCancel}
|
|
color="error"
|
|
variant="contained"
|
|
disabled={isCancelling}
|
|
>
|
|
{isCancelling
|
|
? (t ? t('orders.cancelConfirm.cancelling') : 'Wird storniert...')
|
|
: (t ? t('orders.cancelConfirm.confirm') : 'Stornieren')
|
|
}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default withI18n()(OrdersTab);
|