Files
reactShop/src/components/profile/OrdersTab.js
sebseb7 2d6c8ff25f feat(Orders): add tracking shipment link and update translations
- 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.
2025-11-29 14:05:59 +01:00

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