feat: Integrate Girocode functionality for wire payments in OrdersTab and LoginComponent, enhance user experience with pending payment notifications, and update translations across multiple locales

This commit is contained in:
sebseb7
2026-03-24 00:48:22 +01:00
parent f47fbc5c39
commit a9bf1aee5f
31 changed files with 969 additions and 93 deletions

View File

@@ -23,6 +23,10 @@ import CartSyncDialog from './CartSyncDialog.js';
import { localAndArchiveServer, mergeCarts } from '../utils/cartUtils.js';
import config from '../config.js';
import { withI18n } from '../i18n/withTranslation.js';
import {
hasPendingWirePaymentOrder,
WIRE_PAYMENT_PENDING_EVENT,
} from '../utils/wireGirocodeEligibility.js';
import GoogleIcon from '@mui/icons-material/Google';
// Lazy load GoogleAuthProvider
@@ -117,10 +121,28 @@ export class LoginComponent extends Component {
localCartSync: [],
serverCartSync: [],
pendingNavigate: null,
privacyConfirmed: sessionStorage.getItem('privacyConfirmed') === 'true'
privacyConfirmed: sessionStorage.getItem('privacyConfirmed') === 'true',
pendingWirePaymentOrders: false
};
}
refreshPendingWireOrders = () => {
if (typeof window === 'undefined' || !window.socketManager) return;
window.socketManager.emit('getOrders', (response) => {
if (response.success && Array.isArray(response.orders)) {
this.setState({
pendingWirePaymentOrders: hasPendingWirePaymentOrder(response.orders),
});
}
});
};
handleWirePaymentPendingEvent = (e) => {
if (e.detail && typeof e.detail.pending === 'boolean') {
this.setState({ pendingWirePaymentOrders: e.detail.pending });
}
};
componentDidMount() {
// Make the open function available globally
window.openLoginDrawer = this.handleOpen;
@@ -128,17 +150,26 @@ export class LoginComponent extends Component {
if (this.props.open) {
this.setState({ open: true });
}
if (this.state.isLoggedIn) {
this.refreshPendingWireOrders();
}
window.addEventListener(WIRE_PAYMENT_PENDING_EVENT, this.handleWirePaymentPendingEvent);
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps, prevState) {
if (this.props.open !== prevProps.open) {
this.setState({ open: this.props.open });
}
if (this.state.isLoggedIn && !prevState.isLoggedIn) {
this.refreshPendingWireOrders();
}
}
componentWillUnmount() {
// Cleanup function to remove global reference when component unmounts
window.openLoginDrawer = undefined;
window.removeEventListener(WIRE_PAYMENT_PENDING_EVENT, this.handleWirePaymentPendingEvent);
}
resetForm = () => {
@@ -308,6 +339,7 @@ export class LoginComponent extends Component {
handleUserMenuClick = (event) => {
this.setState({ anchorEl: event.currentTarget });
this.refreshPendingWireOrders();
};
handleUserMenuClose = () => {
@@ -326,6 +358,7 @@ export class LoginComponent extends Component {
isLoggedIn: false,
isAdmin: false,
anchorEl: null,
pendingWirePaymentOrders: false,
});
}
});
@@ -480,7 +513,8 @@ export class LoginComponent extends Component {
cartSyncOpen,
localCartSync,
serverCartSync,
privacyConfirmed
privacyConfirmed,
pendingWirePaymentOrders
} = this.state;
const { open: openProp, handleClose: handleCloseProp } = this.props;
@@ -520,8 +554,13 @@ export class LoginComponent extends Component {
<MenuItem component={Link} to="/profile#cart" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>
{this.props.t ? this.props.t('auth.menu.checkout') : 'Bestellabschluss'}
</MenuItem>
<MenuItem component={Link} to="/profile#orders" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>
{this.props.t ? this.props.t('auth.menu.orders') : 'Bestellungen'}
<MenuItem component={Link} to="/profile#orders" onClick={this.handleUserMenuClose} sx={{ pl: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<span>{this.props.t ? this.props.t('auth.menu.orders') : 'Bestellungen'}</span>
{pendingWirePaymentOrders ? (
<Typography component="span" sx={{ color: 'error.main', fontWeight: 700, fontSize: '0.875rem', flexShrink: 0 }} aria-label="!">
[!]
</Typography>
) : null}
</MenuItem>
<MenuItem component={Link} to="/profile#settings" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>
{this.props.t ? this.props.t('auth.menu.settings') : 'Einstellungen'}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, Fragment } from "react";
import { useNavigate } from "react-router-dom";
import { withI18n } from "../../i18n/withTranslation.js";
import {
@@ -24,6 +24,12 @@ import {
import SearchIcon from "@mui/icons-material/Search";
import CancelIcon from "@mui/icons-material/Cancel";
import OrderDetailsDialog from "./OrderDetailsDialog.js";
import WireOrderGirocode from "./WireOrderGirocode.js";
import {
isWireGirocodeEligible,
hasPendingWirePaymentOrder,
WIRE_PAYMENT_PENDING_EVENT,
} from "../../utils/wireGirocodeEligibility.js";
// Constants
const getStatusTranslation = (status, t) => {
@@ -100,8 +106,20 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
window.socketManager.emit("getOrders", (response) => {
if (response.success) {
setOrders(response.orders);
window.dispatchEvent(
new CustomEvent(WIRE_PAYMENT_PENDING_EVENT, {
detail: {
pending: hasPendingWirePaymentOrder(response.orders),
},
})
);
} else {
setError(response.error || "Failed to fetch orders.");
window.dispatchEvent(
new CustomEvent(WIRE_PAYMENT_PENDING_EVENT, {
detail: { pending: false },
})
);
}
setLoading(false);
});
@@ -216,89 +234,107 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
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" }}
<Fragment key={order.orderId}>
<TableRow 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),
}}
>
{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' }}
<span style={{ fontSize: "1.2rem" }}>
{getStatusEmoji(order.status)}
</span>
<Typography
variant="body2"
component="span"
sx={{ fontWeight: "medium" }}
>
📦 {t ? t('orders.trackShipment') : 'Sendung verfolgen'}
</a>
{displayStatus}
</Typography>
</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
{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 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'}>
</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="error"
onClick={() => handleCancelClick(order)}
aria-label={t ? t('orders.tooltips.cancelOrder') : 'Bestellung stornieren'}
color="primary"
onClick={() => handleViewDetails(order.orderId)}
aria-label={t ? t('orders.tooltips.viewDetails') : 'Details anzeigen'}
>
<CancelIcon />
<SearchIcon />
</IconButton>
</Tooltip>
)}
</Box>
</TableCell>
</TableRow>
{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>
{isWireGirocodeEligible(order) && (
<TableRow>
<TableCell
colSpan={6}
sx={{
py: 2,
px: { xs: 1, sm: 2 },
verticalAlign: "top",
bgcolor: "action.hover",
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
}}
>
<WireOrderGirocode order={order} t={t} />
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>

View File

@@ -0,0 +1,177 @@
import React, { useEffect, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Box, Typography, CircularProgress } from "@mui/material";
import { buildWireGirocodeDataUrl } from "../../utils/wireGirocode.js";
import {
orderGrossTotal,
isWireGirocodeEligible,
} from "../../utils/wireGirocodeEligibility.js";
import { WIRE_GIROCODE_RECIPIENT } from "../../config/wireGirocodeRecipient.js";
/**
* Full-width row content: Girocode QR + bank details (pending + wire only).
*/
const WireOrderGirocode = ({ order, t }) => {
const { i18n } = useTranslation();
const [dataUrl, setDataUrl] = useState(null);
const [genError, setGenError] = useState(false);
const eligible = isWireGirocodeEligible(order);
const amount = useMemo(() => {
const raw = orderGrossTotal(order);
return Math.round(raw * 100) / 100;
}, [order]);
const amountFormatted = useMemo(() => {
const locale = (i18n.language || "de").replace("_", "-");
return new Intl.NumberFormat(locale, {
style: "currency",
currency: "EUR",
}).format(amount);
}, [amount, i18n.language]);
useEffect(() => {
if (!eligible || amount < 0.01) {
setDataUrl(null);
setGenError(false);
return;
}
let cancelled = false;
setDataUrl(null);
setGenError(false);
buildWireGirocodeDataUrl({
amount,
reference: order.orderId,
})
.then((url) => {
if (!cancelled) setDataUrl(url);
})
.catch(() => {
if (!cancelled) setGenError(true);
});
return () => {
cancelled = true;
};
}, [eligible, amount, order.orderId]);
if (!eligible) {
return null;
}
const hint = t
? t("orders.girocode.hint")
: "Mit Ihrer Banking-App scannen, um zu bezahlen.";
const alt = t
? t("orders.girocode.alt")
: "Girocode für die Überweisung";
const paymentPending = t
? t("orders.girocode.paymentPending")
: "Diese Bestellung wartet auf Ihre Überweisung.";
const payToAccount = t
? t("orders.girocode.payToAccount")
: "Bitte überweisen Sie den Betrag auf folgendes Konto:";
const holder = t
? t("orders.girocode.holder", { name: WIRE_GIROCODE_RECIPIENT.name })
: `Kontoinhaber: ${WIRE_GIROCODE_RECIPIENT.name}`;
const ibanLine = t
? t("orders.girocode.iban", { iban: WIRE_GIROCODE_RECIPIENT.iban })
: `IBAN: ${WIRE_GIROCODE_RECIPIENT.iban}`;
const bicLine = t
? t("orders.girocode.bic", { bic: WIRE_GIROCODE_RECIPIENT.bic })
: `BIC: ${WIRE_GIROCODE_RECIPIENT.bic}`;
const amountLine = t
? t("orders.girocode.amount", { amount: amountFormatted })
: `Betrag: ${amountFormatted}`;
const purposeLine = t
? t("orders.girocode.purpose", { orderId: order.orderId })
: `Verwendungszweck: ${order.orderId}`;
return (
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", sm: "row" },
gap: { xs: 2, sm: 3 },
alignItems: "flex-start",
}}
>
<Box sx={{ flexShrink: 0, width: { xs: "100%", sm: "auto" } }}>
<Typography
variant="caption"
component="div"
sx={{ display: "block", mb: 0.5, lineHeight: 1.35, maxWidth: 220 }}
>
{hint}
</Typography>
{!dataUrl && !genError && (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: 200,
height: 200,
}}
>
<CircularProgress size={28} />
</Box>
)}
{genError && (
<Typography variant="caption" color="text.secondary" component="div">
{t
? t("orders.girocode.error")
: "QR-Code konnte nicht erzeugt werden."}
</Typography>
)}
{dataUrl && (
<img
src={dataUrl}
width={200}
height={200}
alt={alt}
style={{ display: "block", borderRadius: 4 }}
/>
)}
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="body2" sx={{ mb: 1.5, lineHeight: 1.5 }}>
{paymentPending}
</Typography>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 600 }}>
{payToAccount}
</Typography>
<Typography variant="body2" component="div" sx={{ lineHeight: 1.6 }}>
{holder}
</Typography>
<Typography
variant="body2"
component="div"
sx={{
lineHeight: 1.6,
wordBreak: "break-all",
fontFamily: "ui-monospace, monospace",
}}
>
{ibanLine}
</Typography>
<Typography variant="body2" component="div" sx={{ lineHeight: 1.6 }}>
{bicLine}
</Typography>
<Typography variant="body2" component="div" sx={{ mt: 1.5, lineHeight: 1.6 }}>
{amountLine}
</Typography>
<Typography variant="body2" component="div" sx={{ lineHeight: 1.6 }}>
{purposeLine}
</Typography>
</Box>
</Box>
);
};
export default WireOrderGirocode;

View File

@@ -0,0 +1,8 @@
/**
* SEPA Girocode recipient — must match server-side wire-transfer emails / QR attachments.
*/
export const WIRE_GIROCODE_RECIPIENT = {
name: "Max Schön",
iban: "DE35850503000221239693",
bic: "OSDDDE81XXX",
};

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "لم تقم بوضع أي طلبات بعد.",
"trackShipment": "تتبع الشحنة",
"girocode": {
"hint": "امسح الرمز بتطبيق البنك للدفع.",
"alt": "رمز Girocode للتحويل البنكي",
"error": "تعذر إنشاء رمز الاستجابة السريعة.",
"paymentPending": "هذا الطلب في انتظار التحويل البنكي.",
"payToAccount": "يرجى تحويل المبلغ إلى الحساب التالي:",
"holder": "صاحب الحساب: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "المبلغ: {{amount}}",
"purpose": "المرجع: {{orderId}}"
},
"details": {
"title": "تفاصيل الطلب: {{orderId}}",
"deliveryAddress": "عنوان التوصيل",

View File

@@ -26,7 +26,19 @@ export default {
"cancelOrder": "Отмени поръчката"
},
"noOrders": "Все още не сте направили поръчки.",
"trackShipment": "Проследи пратката",
"trackShipment": "Проследи пратката",
"girocode": {
"hint": "Сканирайте с банковото си приложение, за да платите.",
"alt": "Girocode за банков превод",
"error": "QR кодът не можа да бъде генериран.",
"paymentPending": "Тази поръчка очаква вашия банков превод.",
"payToAccount": "Моля, преведете сумата по следната сметка:",
"holder": "Титуляр: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Сума: {{amount}}",
"purpose": "Основание: {{orderId}}"
},
"details": {
"title": "Подробности за поръчка: {{orderId}}",
"deliveryAddress": "Адрес за доставка",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Ještě jste neprovedli žádné objednávky.",
"trackShipment": "Sledovat zásilku",
"girocode": {
"hint": "Naskenujte bankovní aplikací a zaplaťte.",
"alt": "Girocode pro bankovní převod",
"error": "QR kód se nepodařilo vygenerovat.",
"paymentPending": "Tato objednávka čeká na váš bankovní převod.",
"payToAccount": "Prosím převeďte částku na následující účet:",
"holder": "Majitel účtu: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Částka: {{amount}}",
"purpose": "Zpráva pro příjemce: {{orderId}}"
},
"details": {
"title": "Detaily objednávky: {{orderId}}",
"deliveryAddress": "Dodací adresa",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Sie haben noch keine Bestellungen aufgegeben.",
"trackShipment": "Sendung verfolgen",
"girocode": {
"hint": "Mit Ihrer Banking-App scannen, um zu bezahlen.",
"alt": "Girocode für die Überweisung",
"error": "QR-Code konnte nicht erzeugt werden.",
"paymentPending": "Diese Bestellung wartet auf Ihre Überweisung.",
"payToAccount": "Bitte überweisen Sie den Betrag auf folgendes Konto:",
"holder": "Kontoinhaber: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Betrag: {{amount}}",
"purpose": "Verwendungszweck: {{orderId}}"
},
"details": {
"title": "Bestelldetails: {{orderId}}",
"deliveryAddress": "Lieferadresse",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Δεν έχετε κάνει ακόμα καμία παραγγελία.",
"trackShipment": "Παρακολούθηση αποστολής",
"girocode": {
"hint": "Σαρώστε με την εφαρμογή της τράπεζάς σας για πληρωμή.",
"alt": "Girocode για τραπεζική μεταφορά",
"error": "Δεν ήταν δυνατή η δημιουργία QR.",
"paymentPending": "Η παραγγελία περιμένει την τραπεζική σας μεταφορά.",
"payToAccount": "Μεταφέρετε το ποσό στον ακόλουθο λογαριασμό:",
"holder": "Δικαιούχος: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Ποσό: {{amount}}",
"purpose": "Αιτιολογία: {{orderId}}"
},
"details": {
"title": "Λεπτομέρειες παραγγελίας: {{orderId}}",
"deliveryAddress": "Διεύθυνση παράδοσης",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "You have not placed any orders yet.", // Sie haben noch keine Bestellungen aufgegeben.
"trackShipment": "Track shipment", // Sendung verfolgen
"girocode": {
"hint": "Scan with your banking app to pay.", // Mit Ihrer Banking-App scannen...
"alt": "Girocode for bank transfer", // Girocode für die Überweisung
"error": "Could not generate QR code.", // QR-Code konnte nicht erzeugt werden.
"paymentPending": "This order is awaiting your bank transfer.", // Diese Bestellung wartet...
"payToAccount": "Please transfer the amount to the following account:", // Bitte überweisen...
"holder": "Account holder: {{name}}", // Kontoinhaber
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Amount: {{amount}}", // Betrag
"purpose": "Payment reference: {{orderId}}" // Verwendungszweck
},
"details": {
"title": "Order details: {{orderId}}", // Bestelldetails: {{orderId}}
"deliveryAddress": "Delivery address", // Lieferadresse

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Aún no has realizado ningún pedido.",
"trackShipment": "Rastrear envío",
"girocode": {
"hint": "Escanee con la app de su banco para pagar.",
"alt": "Girocode para transferencia bancaria",
"error": "No se pudo generar el código QR.",
"paymentPending": "Este pedido está pendiente de su transferencia bancaria.",
"payToAccount": "Transfiera el importe a la siguiente cuenta:",
"holder": "Titular de la cuenta: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Importe: {{amount}}",
"purpose": "Concepto: {{orderId}}"
},
"details": {
"title": "Detalles del pedido: {{orderId}}",
"deliveryAddress": "Dirección de entrega",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Vous n'avez pas encore passé de commandes.",
"trackShipment": "Suivre l'envoi",
"girocode": {
"hint": "Scannez avec l'application de votre banque pour payer.",
"alt": "Girocode pour virement bancaire",
"error": "Impossible de générer le code QR.",
"paymentPending": "Cette commande attend votre virement bancaire.",
"payToAccount": "Veuillez virer le montant sur le compte suivant :",
"holder": "Titulaire du compte : {{name}}",
"iban": "IBAN : {{iban}}",
"bic": "BIC : {{bic}}",
"amount": "Montant : {{amount}}",
"purpose": "Libellé : {{orderId}}"
},
"details": {
"title": "Détails de la commande : {{orderId}}",
"deliveryAddress": "Adresse de livraison",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Još niste izvršili nijednu narudžbu.",
"trackShipment": "Prati pošiljku",
"girocode": {
"hint": "Skenirajte bankovnom aplikacijom za plaćanje.",
"alt": "Girocode za bankovni prijenos",
"error": "QR kod nije moguće generirati.",
"paymentPending": "Ova narudžba čeka vaš bankovni prijenos.",
"payToAccount": "Molimo uplatite iznos na sljedeći račun:",
"holder": "Vlasnik računa: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Iznos: {{amount}}",
"purpose": "Svrha plaćanja: {{orderId}}"
},
"details": {
"title": "Detalji narudžbe: {{orderId}}",
"deliveryAddress": "Adresa dostave",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Még nem adott le rendelést.",
"trackShipment": "Szállítmány követése",
"girocode": {
"hint": "Olvasd be a banki alkalmazásoddal a fizetéshez.",
"alt": "Girocode átutaláshoz",
"error": "A QR-kód nem hozható létre.",
"paymentPending": "A rendelés a banki átutalásodra vár.",
"payToAccount": "Kérjük, utald át az összeget a következő számlára:",
"holder": "Számlatulajdonos: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Összeg: {{amount}}",
"purpose": "Közlemény: {{orderId}}"
},
"details": {
"title": "Rendelés részletei: {{orderId}}",
"deliveryAddress": "Szállítási cím",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Non hai ancora effettuato ordini.",
"trackShipment": "Traccia spedizione",
"girocode": {
"hint": "Scansiona con l'app della tua banca per pagare.",
"alt": "Girocode per bonifico bancario",
"error": "Impossibile generare il codice QR.",
"paymentPending": "Questo ordine è in attesa del bonifico bancario.",
"payToAccount": "Trasferisci l'importo sul seguente conto:",
"holder": "Intestatario: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Importo: {{amount}}",
"purpose": "Causale: {{orderId}}"
},
"details": {
"title": "Dettagli ordine: {{orderId}}",
"deliveryAddress": "Indirizzo di consegna",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Nie złożyłeś jeszcze żadnych zamówień.",
"trackShipment": "Śledź przesyłkę",
"girocode": {
"hint": "Zeskanuj aplikacją bankową, aby zapłacić.",
"alt": "Girocode do przelewu",
"error": "Nie udało się wygenerować kodu QR.",
"paymentPending": "To zamówienie oczekuje na przelew.",
"payToAccount": "Przelej kwotę na następujące konto:",
"holder": "Właściciel konta: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Kwota: {{amount}}",
"purpose": "Tytuł: {{orderId}}"
},
"details": {
"title": "Szczegóły zamówienia: {{orderId}}",
"deliveryAddress": "Adres dostawy",

View File

@@ -26,7 +26,19 @@ export default {
"cancelOrder": "Anulează comanda"
},
"noOrders": "Nu ați plasat încă nicio comandă.",
"trackShipment": "Urmărește expedierea",
"trackShipment": "Urmărește expedierea",
"girocode": {
"hint": "Scanează cu aplicația băncii pentru a plăti.",
"alt": "Girocode pentru transfer bancar",
"error": "Codul QR nu a putut fi generat.",
"paymentPending": "Comanda așteaptă transferul bancar.",
"payToAccount": "Vă rugăm să transferați suma în contul următor:",
"holder": "Titular cont: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Sumă: {{amount}}",
"purpose": "Detalii plată: {{orderId}}"
},
"details": {
"title": "Detalii comandă: {{orderId}}",
"deliveryAddress": "Adresa de livrare",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Вы еще не сделали ни одного заказа.",
"trackShipment": "Отследить отправление",
"girocode": {
"hint": "Отсканируйте в приложении банка для оплаты.",
"alt": "Girocode для банковского перевода",
"error": "Не удалось создать QR-код.",
"paymentPending": "Заказ ожидает банковского перевода.",
"payToAccount": "Переведите сумму на следующий счёт:",
"holder": "Получатель: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Сумма: {{amount}}",
"purpose": "Назначение платежа: {{orderId}}"
},
"details": {
"title": "Детали заказа: {{orderId}}",
"deliveryAddress": "Адрес доставки",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Ešte ste neuskutočnili žiadne objednávky.",
"trackShipment": "Sledovať zásielku",
"girocode": {
"hint": "Naskenujte bankovou aplikáciou a zaplaťte.",
"alt": "Girocode pre bankový prevod",
"error": "QR kód sa nepodarilo vygenerovať.",
"paymentPending": "Táto objednávka čaká na váš bankový prevod.",
"payToAccount": "Prosím preveďte sumu na nasledujúci účet:",
"holder": "Majiteľ účtu: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Suma: {{amount}}",
"purpose": "Správa pre príjemcu: {{orderId}}"
},
"details": {
"title": "Detaily objednávky: {{orderId}}",
"deliveryAddress": "Dodacia adresa",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Še niste oddali nobenega naročila.",
"trackShipment": "Sledi pošiljki",
"girocode": {
"hint": "Skenirajte z bančno aplikacijo za plačilo.",
"alt": "Girocode za bančno nakazilo",
"error": "QR kode ni bilo mogoče ustvariti.",
"paymentPending": "To naročilo čaka na vaše bančno nakazilo.",
"payToAccount": "Prosimo, nakažite znesek na naslednji račun:",
"holder": "Imetnik računa: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Znesek: {{amount}}",
"purpose": "Namen plačila: {{orderId}}"
},
"details": {
"title": "Podrobnosti naročila: {{orderId}}",
"deliveryAddress": "Naslov za dostavo",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Nuk keni bërë ende asnjë porosi.",
"trackShipment": "Ndjek dërgesën",
"girocode": {
"hint": "Skanoni me aplikacionin e bankës për të paguar.",
"alt": "Girocode për transfertë bankare",
"error": "Kodi QR nuk mund të gjenerohej.",
"paymentPending": "Ky porosi pret transfertën tuaj bankare.",
"payToAccount": "Ju lutemi transferoni shumën në llogarinë e mëposhtme:",
"holder": "Titullari i llogarisë: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Shuma: {{amount}}",
"purpose": "Qëllimi i pagesës: {{orderId}}"
},
"details": {
"title": "Detajet e porosisë: {{orderId}}",
"deliveryAddress": "Adresa e dorëzimit",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Još niste napravili nijednu porudžbinu.",
"trackShipment": "Prati pošiljku",
"girocode": {
"hint": "Skenirajte bankovnom aplikacijom da platite.",
"alt": "Girocode za bankovni transfer",
"error": "QR kod nije moguće generisati.",
"paymentPending": "Ova porudžbina čeka vaš bankovni transfer.",
"payToAccount": "Molimo uplatite iznos na sledeći račun:",
"holder": "Vlasnik računa: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Iznos: {{amount}}",
"purpose": "Svrha uplate: {{orderId}}"
},
"details": {
"title": "Detalji porudžbine: {{orderId}}",
"deliveryAddress": "Adresa za isporuku",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Du har inte lagt några beställningar än.",
"trackShipment": "Spåra försändelse",
"girocode": {
"hint": "Skanna med din bankapp för att betala.",
"alt": "Girocode för banköverföring",
"error": "QR-koden kunde inte genereras.",
"paymentPending": "Denna beställning väntar på din banköverföring.",
"payToAccount": "Överför beloppet till följande konto:",
"holder": "Kontoinnehavare: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Belopp: {{amount}}",
"purpose": "Meddelande: {{orderId}}"
},
"details": {
"title": "Orderdetaljer: {{orderId}}",
"deliveryAddress": "Leveransadress",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Henüz sipariş vermediniz.",
"trackShipment": "Gönderiyi takip et",
"girocode": {
"hint": "Ödemek için banka uygulamanızla taratın.",
"alt": "Banka havalesi için Girocode",
"error": "QR kodu oluşturulamadı.",
"paymentPending": "Bu sipariş banka havalesini bekliyor.",
"payToAccount": "Lütfen tutarı aşağıdaki hesaba gönderin:",
"holder": "Hesap sahibi: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Tutar: {{amount}}",
"purpose": "Açıklama: {{orderId}}"
},
"details": {
"title": "Sipariş detayları: {{orderId}}",
"deliveryAddress": "Teslimat adresi",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "Ви ще не робили замовлень.",
"trackShipment": "Відстежити відправлення",
"girocode": {
"hint": "Відскануйте в додатку банку для оплати.",
"alt": "Girocode для банківського переказу",
"error": "Не вдалося згенерувати QR-код.",
"paymentPending": "Замовлення очікує на банківський переказ.",
"payToAccount": "Перекажіть суму на такий рахунок:",
"holder": "Власник рахунку: {{name}}",
"iban": "IBAN: {{iban}}",
"bic": "BIC: {{bic}}",
"amount": "Сума: {{amount}}",
"purpose": "Призначення платежу: {{orderId}}"
},
"details": {
"title": "Деталі замовлення: {{orderId}}",
"deliveryAddress": "Адреса доставки",

View File

@@ -27,6 +27,18 @@ export default {
},
"noOrders": "您还没有下过任何订单。",
"trackShipment": "跟踪发货",
"girocode": {
"hint": "使用银行应用扫码付款。",
"alt": "银行转账 Girocode",
"error": "无法生成二维码。",
"paymentPending": "此订单等待您完成银行转账。",
"payToAccount": "请将款项汇至以下账户:",
"holder": "账户持有人:{{name}}",
"iban": "IBAN{{iban}}",
"bic": "BIC{{bic}}",
"amount": "金额:{{amount}}",
"purpose": "备注/用途:{{orderId}}"
},
"details": {
"title": "订单详情: {{orderId}}",
"deliveryAddress": "收货地址",

View File

@@ -8,6 +8,8 @@ import {
Tab,
CircularProgress
} from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
import { useLocation, useNavigate, Navigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -16,18 +18,25 @@ import OrdersTab from '../components/profile/OrdersTab.js';
import SettingsTab from '../components/profile/SettingsTab.js';
import CartTab from '../components/profile/CartTab.js';
import LoginComponent from '../components/LoginComponent.js';
import {
hasPendingWirePaymentOrder,
WIRE_PAYMENT_PENDING_EVENT,
} from '../utils/wireGirocodeEligibility.js';
// Functional Profile Page Component
const ProfilePage = () => {
const location = useLocation();
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [tabValue, setTabValue] = useState(0);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [showLogin, setShowLogin] = useState(false);
const [orderIdFromHash, setOrderIdFromHash] = useState(null);
const [paymentCompletion, setPaymentCompletion] = useState(null);
const [wirePaymentPending, setWirePaymentPending] = useState(false);
// @note Check for payment completion parameters from Stripe and Mollie redirects
useEffect(() => {
@@ -127,6 +136,31 @@ const ProfilePage = () => {
}
}, [location.hash, navigate]);
useEffect(() => {
if (!user || typeof window === 'undefined' || !window.socketManager) {
setWirePaymentPending(false);
return;
}
const fetchWirePending = () => {
window.socketManager.emit('getOrders', (response) => {
if (response.success && Array.isArray(response.orders)) {
setWirePaymentPending(hasPendingWirePaymentOrder(response.orders));
}
});
};
fetchWirePending();
const onWirePending = (e) => {
if (e.detail && typeof e.detail.pending === 'boolean') {
setWirePaymentPending(e.detail.pending);
}
};
window.addEventListener(WIRE_PAYMENT_PENDING_EVENT, onWirePending);
return () => window.removeEventListener(WIRE_PAYMENT_PENDING_EVENT, onWirePending);
}, [user]);
useEffect(() => {
const checkUserLoggedIn = () => {
const storedUser = sessionStorage.getItem('user');
@@ -229,11 +263,16 @@ const ProfilePage = () => {
<Container maxWidth="md" sx={{ py: { xs: 0, sm: 4 }, px: { xs: 0, sm: 3 } }}>
<Paper elevation={{ xs: 0, sm: 2 }} sx={{ borderRadius: { xs: 0, sm: 2 }, overflow: 'hidden' }}>
<Box sx={{ bgcolor: '#2e7d32', p: { xs: 2, sm: 3 }, color: 'white' }}>
<Typography variant="h5" fontWeight="bold">
{window.innerWidth < 600 ?
(tabValue === 0 ? (t ? t('auth.menu.checkout') : 'Bestellabschluss') :
tabValue === 1 ? (t ? t('auth.menu.orders') : 'Bestellungen') :
tabValue === 2 ? (t ? t('auth.menu.settings') : 'Einstellungen') : (t ? t('auth.profile') : 'Mein Profil'))
<Typography variant="h5" fontWeight="bold" component="div" sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexWrap: 'wrap' }}>
{isMobile ?
(<>
{tabValue === 0 ? (t ? t('auth.menu.checkout') : 'Bestellabschluss') :
tabValue === 1 ? (t ? t('auth.menu.orders') : 'Bestellungen') :
tabValue === 2 ? (t ? t('auth.menu.settings') : 'Einstellungen') : (t ? t('auth.profile') : 'Mein Profil')}
{tabValue === 1 && wirePaymentPending && (
<Box component="span" sx={{ color: 'error.main', fontWeight: 700 }} aria-label="Ausstehende Überweisung">[!]</Box>
)}
</>)
: (t ? t('auth.profile') : 'Mein Profil')
}
</Typography>
@@ -266,7 +305,16 @@ const ProfilePage = () => {
}}
/>
<Tab
label={t ? t('auth.menu.orders') : 'Bestellungen'}
label={
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 0.5 }}>
<span>{t ? t('auth.menu.orders') : 'Bestellungen'}</span>
{wirePaymentPending && (
<Box component="span" sx={{ color: 'error.main', fontWeight: 700, fontSize: '0.85em', lineHeight: 1 }} aria-hidden>
[!]
</Box>
)}
</Box>
}
sx={{
color: tabValue === 1 ? '#2e7d32' : 'inherit',
fontWeight: 'bold'

27
src/utils/wireGirocode.js Normal file
View File

@@ -0,0 +1,27 @@
import generateQrCode from "sepa-payment-qr-code";
import QRCode from "qrcode";
import { WIRE_GIROCODE_RECIPIENT } from "../config/wireGirocodeRecipient.js";
/**
* @param {{ amount: number, reference: string }} options
* @returns {Promise<string>} data URL for a PNG QR image
*/
export async function buildWireGirocodeDataUrl({ amount, reference }) {
const epcText = generateQrCode({
name: WIRE_GIROCODE_RECIPIENT.name,
iban: WIRE_GIROCODE_RECIPIENT.iban,
bic: WIRE_GIROCODE_RECIPIENT.bic,
amount,
unstructuredReference: String(reference),
});
return QRCode.toDataURL(epcText, {
errorCorrectionLevel: "M",
width: 200,
margin: 1,
color: {
dark: "#000000",
light: "#FFFFFF",
},
});
}

View File

@@ -0,0 +1,27 @@
export function orderGrossTotal(order) {
return order.items.reduce(
(acc, item) => acc + item.price * item.quantity_ordered,
0
);
}
export function getPaymentMethodRaw(order) {
return String(order.paymentMethod || order.payment_method || "").toLowerCase();
}
/** Pending wire-transfer orders that still need payment (show Girocode row). */
export function isWireGirocodeEligible(order) {
const amount = Math.round(orderGrossTotal(order) * 100) / 100;
return (
order.status === "pending" &&
getPaymentMethodRaw(order) === "wire" &&
amount >= 0.01
);
}
export function hasPendingWirePaymentOrder(orders) {
return Array.isArray(orders) && orders.some(isWireGirocodeEligible);
}
/** Browser CustomEvent name: detail.pending is boolean */
export const WIRE_PAYMENT_PENDING_EVENT = "wirePaymentOrdersPending";