diff --git a/package-lock.json b/package-lock.json index 689d4fa..47f751f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,12 +22,14 @@ "i18next": "^25.3.2", "i18next-browser-languagedetector": "^8.2.0", "openai": "^4.0.0", + "qrcode": "^1.5.4", "react": "^19.1.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", "react-router-dom": "^7.6.2", "sanitize-html": "^2.17.0", + "sepa-payment-qr-code": "^2.0.2", "sharp": "^0.34.2", "socket.io-client": "^4.7.5" }, @@ -3982,7 +3984,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3992,7 +3993,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4553,6 +4553,15 @@ "tslib": "^2.0.3" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001757", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", @@ -4690,6 +4699,17 @@ "node": ">=0.10.0" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -5252,6 +5272,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -5400,6 +5429,12 @@ "dev": true, "license": "MIT" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -5609,6 +5644,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -6933,6 +6974,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -7588,6 +7638,12 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/iban": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/iban/-/iban-0.0.14.tgz", + "integrity": "sha512-+rocNKk+Ga9m8Lr9fTMWd+87JnsBrucm0ZsIx5ROOarZlaDLmd+FKdbtvb0XyoBw9GAFOYG2GuLqoNB16d+p3w==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7982,6 +8038,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", @@ -9420,7 +9485,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9511,7 +9575,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9667,6 +9730,15 @@ "node": ">=4" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9879,6 +9951,23 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -10338,6 +10427,15 @@ "entities": "^2.0.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -10348,6 +10446,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -10782,6 +10886,18 @@ "node": ">= 0.8" } }, + "node_modules/sepa-payment-qr-code": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sepa-payment-qr-code/-/sepa-payment-qr-code-2.0.2.tgz", + "integrity": "sha512-TyyONY2Lzo4cjCTgSwAyb2oXYyWXvSaeSY0dIlsiXMpdTEAsR/ONB5jyE7/04+nPBGrLTpKKT3q1dw2LRPcz/g==", + "license": "ISC", + "dependencies": { + "iban": "0.0.14" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -10894,6 +11010,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-cookie-parser": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", @@ -11423,6 +11545,20 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -11525,7 +11661,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12724,6 +12859,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -12763,6 +12904,20 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -12872,6 +13027,12 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -12888,6 +13049,93 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 361cfa0..dca1ca2 100644 --- a/package.json +++ b/package.json @@ -40,12 +40,14 @@ "i18next": "^25.3.2", "i18next-browser-languagedetector": "^8.2.0", "openai": "^4.0.0", + "qrcode": "^1.5.4", "react": "^19.1.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", "react-router-dom": "^7.6.2", "sanitize-html": "^2.17.0", + "sepa-payment-qr-code": "^2.0.2", "sharp": "^0.34.2", "socket.io-client": "^4.7.5" }, diff --git a/src/components/LoginComponent.js b/src/components/LoginComponent.js index 9a366cc..d0fb9a0 100644 --- a/src/components/LoginComponent.js +++ b/src/components/LoginComponent.js @@ -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 { {this.props.t ? this.props.t('auth.menu.checkout') : 'Bestellabschluss'} - - {this.props.t ? this.props.t('auth.menu.orders') : 'Bestellungen'} + + {this.props.t ? this.props.t('auth.menu.orders') : 'Bestellungen'} + {pendingWirePaymentOrders ? ( + + [!] + + ) : null} {this.props.t ? this.props.t('auth.menu.settings') : 'Einstellungen'} diff --git a/src/components/profile/OrdersTab.js b/src/components/profile/OrdersTab.js index fb9cbf2..a3ca371 100644 --- a/src/components/profile/OrdersTab.js +++ b/src/components/profile/OrdersTab.js @@ -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 ( - - {order.orderId} - - {new Date(order.created_at).toLocaleDateString()} - - - - - {getStatusEmoji(order.status)} - - + + {order.orderId} + + {new Date(order.created_at).toLocaleDateString()} + + + - {displayStatus} - - - {order.delivery_method === 'DHL' && order.trackingCode && ( - - + {getStatusEmoji(order.status)} + + - 📦 {t ? t('orders.trackShipment') : 'Sendung verfolgen'} - + {displayStatus} + - )} - - - {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 && ( + + + 📦 {t ? t('orders.trackShipment') : 'Sendung verfolgen'} + + )} - - - {currencyFormatter.format(total)} - - - - - handleViewDetails(order.orderId)} - aria-label={t ? t('orders.tooltips.viewDetails') : 'Details anzeigen'} - > - - - - {isOrderCancelable(order) && ( - + + + {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 + )} + + + {currencyFormatter.format(total)} + + + + 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'} > - + - )} - - - + {isOrderCancelable(order) && ( + + handleCancelClick(order)} + aria-label={t ? t('orders.tooltips.cancelOrder') : 'Bestellung stornieren'} + > + + + + )} + + + + {isWireGirocodeEligible(order) && ( + + `1px solid ${theme.palette.divider}`, + }} + > + + + + )} + ); })} diff --git a/src/components/profile/WireOrderGirocode.js b/src/components/profile/WireOrderGirocode.js new file mode 100644 index 0000000..d5a0afb --- /dev/null +++ b/src/components/profile/WireOrderGirocode.js @@ -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 ( + + + + {hint} + + {!dataUrl && !genError && ( + + + + )} + {genError && ( + + {t + ? t("orders.girocode.error") + : "QR-Code konnte nicht erzeugt werden."} + + )} + {dataUrl && ( + {alt} + )} + + + + + {paymentPending} + + + {payToAccount} + + + {holder} + + + {ibanLine} + + + {bicLine} + + + {amountLine} + + + {purposeLine} + + + + ); +}; + +export default WireOrderGirocode; diff --git a/src/config/wireGirocodeRecipient.js b/src/config/wireGirocodeRecipient.js new file mode 100644 index 0000000..0d58d8d --- /dev/null +++ b/src/config/wireGirocodeRecipient.js @@ -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", +}; diff --git a/src/i18n/locales/ar/orders.js b/src/i18n/locales/ar/orders.js index 652c2ac..8019bc5 100644 --- a/src/i18n/locales/ar/orders.js +++ b/src/i18n/locales/ar/orders.js @@ -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": "عنوان التوصيل", diff --git a/src/i18n/locales/bg/orders.js b/src/i18n/locales/bg/orders.js index da6265d..3657df7 100644 --- a/src/i18n/locales/bg/orders.js +++ b/src/i18n/locales/bg/orders.js @@ -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": "Адрес за доставка", diff --git a/src/i18n/locales/cs/orders.js b/src/i18n/locales/cs/orders.js index 6e33493..ec8ad7e 100644 --- a/src/i18n/locales/cs/orders.js +++ b/src/i18n/locales/cs/orders.js @@ -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", diff --git a/src/i18n/locales/de/orders.js b/src/i18n/locales/de/orders.js index c181dd0..81a3c9e 100644 --- a/src/i18n/locales/de/orders.js +++ b/src/i18n/locales/de/orders.js @@ -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", diff --git a/src/i18n/locales/el/orders.js b/src/i18n/locales/el/orders.js index d0b42d8..b4ce934 100644 --- a/src/i18n/locales/el/orders.js +++ b/src/i18n/locales/el/orders.js @@ -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": "Διεύθυνση παράδοσης", diff --git a/src/i18n/locales/en/orders.js b/src/i18n/locales/en/orders.js index fef0be7..c29d13d 100644 --- a/src/i18n/locales/en/orders.js +++ b/src/i18n/locales/en/orders.js @@ -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 diff --git a/src/i18n/locales/es/orders.js b/src/i18n/locales/es/orders.js index 9e2c48c..eef7e45 100644 --- a/src/i18n/locales/es/orders.js +++ b/src/i18n/locales/es/orders.js @@ -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", diff --git a/src/i18n/locales/fr/orders.js b/src/i18n/locales/fr/orders.js index ba24e02..0846b7d 100644 --- a/src/i18n/locales/fr/orders.js +++ b/src/i18n/locales/fr/orders.js @@ -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", diff --git a/src/i18n/locales/hr/orders.js b/src/i18n/locales/hr/orders.js index da05a30..c8e3c18 100644 --- a/src/i18n/locales/hr/orders.js +++ b/src/i18n/locales/hr/orders.js @@ -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", diff --git a/src/i18n/locales/hu/orders.js b/src/i18n/locales/hu/orders.js index 86b65cb..2c744c0 100644 --- a/src/i18n/locales/hu/orders.js +++ b/src/i18n/locales/hu/orders.js @@ -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", diff --git a/src/i18n/locales/it/orders.js b/src/i18n/locales/it/orders.js index 85976a8..b864c52 100644 --- a/src/i18n/locales/it/orders.js +++ b/src/i18n/locales/it/orders.js @@ -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", diff --git a/src/i18n/locales/pl/orders.js b/src/i18n/locales/pl/orders.js index ab2afb5..830eada 100644 --- a/src/i18n/locales/pl/orders.js +++ b/src/i18n/locales/pl/orders.js @@ -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", diff --git a/src/i18n/locales/ro/orders.js b/src/i18n/locales/ro/orders.js index da6967c..e63f38f 100644 --- a/src/i18n/locales/ro/orders.js +++ b/src/i18n/locales/ro/orders.js @@ -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", diff --git a/src/i18n/locales/ru/orders.js b/src/i18n/locales/ru/orders.js index 0f55c7b..0668769 100644 --- a/src/i18n/locales/ru/orders.js +++ b/src/i18n/locales/ru/orders.js @@ -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": "Адрес доставки", diff --git a/src/i18n/locales/sk/orders.js b/src/i18n/locales/sk/orders.js index ee603f2..7450cae 100644 --- a/src/i18n/locales/sk/orders.js +++ b/src/i18n/locales/sk/orders.js @@ -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", diff --git a/src/i18n/locales/sl/orders.js b/src/i18n/locales/sl/orders.js index edca3a2..1071626 100644 --- a/src/i18n/locales/sl/orders.js +++ b/src/i18n/locales/sl/orders.js @@ -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", diff --git a/src/i18n/locales/sq/orders.js b/src/i18n/locales/sq/orders.js index 03ec900..880f3a6 100644 --- a/src/i18n/locales/sq/orders.js +++ b/src/i18n/locales/sq/orders.js @@ -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", diff --git a/src/i18n/locales/sr/orders.js b/src/i18n/locales/sr/orders.js index b7718c3..9cf3c3c 100644 --- a/src/i18n/locales/sr/orders.js +++ b/src/i18n/locales/sr/orders.js @@ -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", diff --git a/src/i18n/locales/sv/orders.js b/src/i18n/locales/sv/orders.js index b5af8e1..872651d 100644 --- a/src/i18n/locales/sv/orders.js +++ b/src/i18n/locales/sv/orders.js @@ -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", diff --git a/src/i18n/locales/tr/orders.js b/src/i18n/locales/tr/orders.js index 154e5f2..f313756 100644 --- a/src/i18n/locales/tr/orders.js +++ b/src/i18n/locales/tr/orders.js @@ -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", diff --git a/src/i18n/locales/uk/orders.js b/src/i18n/locales/uk/orders.js index 63e6e85..a07e3c5 100644 --- a/src/i18n/locales/uk/orders.js +++ b/src/i18n/locales/uk/orders.js @@ -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": "Адреса доставки", diff --git a/src/i18n/locales/zh/orders.js b/src/i18n/locales/zh/orders.js index da0dd6d..6a6352b 100644 --- a/src/i18n/locales/zh/orders.js +++ b/src/i18n/locales/zh/orders.js @@ -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": "收货地址", diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 03baa97..a914bba 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -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 = () => { - - {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')) + + {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 && ( + [!] + )} + ) : (t ? t('auth.profile') : 'Mein Profil') } @@ -266,7 +305,16 @@ const ProfilePage = () => { }} /> + {t ? t('auth.menu.orders') : 'Bestellungen'} + {wirePaymentPending && ( + + [!] + + )} + + } sx={{ color: tabValue === 1 ? '#2e7d32' : 'inherit', fontWeight: 'bold' diff --git a/src/utils/wireGirocode.js b/src/utils/wireGirocode.js new file mode 100644 index 0000000..d8c5ad5 --- /dev/null +++ b/src/utils/wireGirocode.js @@ -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} 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", + }, + }); +} diff --git a/src/utils/wireGirocodeEligibility.js b/src/utils/wireGirocodeEligibility.js new file mode 100644 index 0000000..0bf1609 --- /dev/null +++ b/src/utils/wireGirocodeEligibility.js @@ -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";