From 11e3e384bded2ad1a3a28e69753bd9fcb614b956 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Mon, 16 Mar 2026 20:35:36 +0100 Subject: [PATCH] feat: add customer fields to subscription and update invoice handling - Added new customer fields in SubscribeAboInput type including phone, recipient contract name, and invoice details. - Updated subscription validation to include new required fields. - Modified the subscribeAbo function to handle new customer and invoice fields. - Enhanced the SummaryPage component to manage new form fields for invoice address and payment method. - Removed the toggle for "For myself / For someone else" as the logic has been simplified. - Updated contract template handling to reflect changes in the form data structure. --- package-lock.json | 120 ++++---- package.json | 1 + .../summary/hooks/subscribeAbo.ts | 46 ++- .../hooks/useAboContractTemplateHtml.ts | 7 +- src/app/coffee-abonnements/summary/page.tsx | 282 +++++++++--------- 5 files changed, 253 insertions(+), 203 deletions(-) diff --git a/package-lock.json b/package-lock.json index c48b1f7..b55e904 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@tailwindplus/elements": "^1.0.22", "@tailwindui/react": "^0.1.1", "axios": "^1.13.5", + "canvg": "^4.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "country-flag-icons": "^1.6.13", @@ -111,7 +112,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -451,7 +451,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -475,7 +474,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -3360,7 +3358,6 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", @@ -3874,15 +3871,13 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3917,7 +3912,6 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz", "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==", "license": "MIT", - "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -3992,7 +3986,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -4522,7 +4515,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4992,7 +4984,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5123,6 +5114,39 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-4.0.3.tgz", + "integrity": "sha512-fKzMoMBwus3CWo1Uy8XJc4tqqn98RoRrGV6CsIkaNiQT5lOeHuMh4fOt+LXLzn2Wqtr4p/c2TOLz4xtu4oBlFA==", + "license": "MIT", + "dependencies": { + "@types/raf": "^3.4.0", + "raf": "^3.4.1", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -5378,18 +5402,13 @@ "postcss": "^8.4" } }, - "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" + "utrie": "^1.0.2" } }, "node_modules/css-prefers-color-scheme": { @@ -5448,8 +5467,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -5901,7 +5919,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6087,7 +6104,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6858,8 +6874,7 @@ "version": "3.14.2", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", "integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==", - "license": "Standard 'no charge' license: https://gsap.com/standard-license.", - "peer": true + "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, "node_modules/has-bigints": { "version": "1.1.0", @@ -7719,6 +7734,26 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jspdf/node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -8825,8 +8860,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -8840,7 +8874,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8878,7 +8911,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9610,19 +9642,6 @@ "node": ">=4" } }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -9723,7 +9742,6 @@ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", "license": "MIT", - "optional": true, "dependencies": { "performance-now": "^2.1.0" } @@ -9733,7 +9751,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9743,7 +9760,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9776,7 +9792,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -10034,7 +10049,6 @@ "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", - "optional": true, "engines": { "node": ">= 0.8.15" } @@ -10406,7 +10420,6 @@ "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", "license": "MIT", - "optional": true, "engines": { "node": ">=0.1.14" } @@ -10665,7 +10678,6 @@ "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", "license": "MIT", - "optional": true, "engines": { "node": ">=12.0.0" } @@ -10690,7 +10702,6 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, "license": "MIT" }, "node_modules/tailwindcss-animate": { @@ -10735,8 +10746,7 @@ "version": "0.182.0", "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz", "integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -11054,7 +11064,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11476,7 +11485,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 0775587..0a5c2b0 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@tailwindplus/elements": "^1.0.22", "@tailwindui/react": "^0.1.1", "axios": "^1.13.5", + "canvg": "^4.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "country-flag-icons": "^1.6.13", diff --git a/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts index 203f44e..1b6637b 100644 --- a/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts +++ b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts @@ -11,8 +11,7 @@ export type SubscribeAboInput = { target_user_id?: number recipient_name?: string recipient_email?: string - recipient_notes?: string - // NEW: customer fields + // Customer fields firstName?: string lastName?: string email?: string @@ -21,8 +20,22 @@ export type SubscribeAboInput = { city?: string country?: string frequency?: string - startDate?: string - // NEW: logged-in user id + // New contract / contact fields + phone?: string + recipientContractName?: string + recipientAddress?: string + paymentMethod?: string + invoiceByEmail?: boolean + invoiceSameAsShipping?: boolean + invoiceFullName?: string + invoiceStreet?: string + invoicePostalCode?: string + invoiceCity?: string + invoicePhone?: string + invoiceEmail?: string + signingCity?: string + signatureDataUrl?: string + // logged-in user id referred_by?: number | string } @@ -48,7 +61,7 @@ export async function subscribeAbo(input: SubscribeAboInput) { } // NEW: validate customer fields (required in UI) - const requiredFields = ['firstName','lastName','email','street','postalCode','city','country','frequency'] as const + const requiredFields = ['firstName','lastName','email','street','postalCode','city','country'] as const const missing = requiredFields.filter(k => { const v = (input as any)[k] return typeof v !== 'string' || v.trim() === '' @@ -62,7 +75,7 @@ export async function subscribeAbo(input: SubscribeAboInput) { interval_count: input.interval_count ?? 1, is_auto_renew: input.is_auto_renew ?? true, is_for_self: isForSelf, - // NEW: include customer fields + // Customer fields firstName: input.firstName, lastName: input.lastName, email: input.email, @@ -71,7 +84,25 @@ export async function subscribeAbo(input: SubscribeAboInput) { city: input.city, country: input.country?.toUpperCase?.() ?? input.country, frequency: input.frequency, - startDate: input.startDate || undefined, + // New contract / contact fields + phone: input.phone || undefined, + recipientContractName: input.recipientContractName || undefined, + recipientAddress: input.recipientAddress || undefined, + paymentMethod: input.paymentMethod || undefined, + invoiceByEmail: input.invoiceByEmail ?? false, + invoiceSameAsShipping: input.invoiceSameAsShipping ?? true, + signingCity: input.signingCity || undefined, + signatureDataUrl: input.signatureDataUrl || undefined, + } + + // Include invoice address fields when not same as shipping + if (!body.invoiceSameAsShipping) { + body.invoiceFullName = input.invoiceFullName || undefined + body.invoiceStreet = input.invoiceStreet || undefined + body.invoicePostalCode = input.invoicePostalCode || undefined + body.invoiceCity = input.invoiceCity || undefined + body.invoicePhone = input.invoicePhone || undefined + body.invoiceEmail = input.invoiceEmail || undefined } if (hasItems) { body.items = input.items!.map(i => ({ @@ -92,7 +123,6 @@ export async function subscribeAbo(input: SubscribeAboInput) { if (input.target_user_id != null) body.target_user_id = input.target_user_id if (!isForSelf && input.recipient_email) body.recipient_email = input.recipient_email if (!isForSelf && input.recipient_name) body.recipient_name = input.recipient_name - if (!isForSelf && input.recipient_notes) body.recipient_notes = input.recipient_notes // NEW: always include referred_by if provided if (input.referred_by != null) body.referred_by = input.referred_by diff --git a/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts b/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts index 63fa9b8..3c2d842 100644 --- a/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts +++ b/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts @@ -1,6 +1,9 @@ 'use client' import { useEffect, useState } from 'react' +import { authFetch } from '../../../utils/authFetch' + +const apiBase = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '') export function useAboContractTemplateHtml() { const [html, setHtml] = useState(null) @@ -14,10 +17,10 @@ export function useAboContractTemplateHtml() { setLoading(true) setError(null) try { - const res = await fetch('/templates/abo-contract-template.html', { + const res = await authFetch(`${apiBase}/api/contracts/abo/active`, { method: 'GET', headers: { Accept: 'text/html' }, - cache: 'no-store', + credentials: 'include', }) const text = await res.text().catch(() => '') diff --git a/src/app/coffee-abonnements/summary/page.tsx b/src/app/coffee-abonnements/summary/page.tsx index cf8f307..3168b54 100644 --- a/src/app/coffee-abonnements/summary/page.tsx +++ b/src/app/coffee-abonnements/summary/page.tsx @@ -55,7 +55,6 @@ export default function SummaryPage() { const [contractPdfError, setContractPdfError] = useState(null) const [selections, setSelections] = useState>({}); const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<60 | 120>(120); - const [isForSelf, setIsForSelf] = useState(true); const [signatureDataUrl, setSignatureDataUrl] = useState('') const [form, setForm] = useState({ firstName: '', @@ -65,11 +64,17 @@ export default function SummaryPage() { postalCode: '', city: '', country: 'DE', - frequency: 'monatlich', - startDate: '', - recipientEmail: '', - recipientName: '', - recipientNotes: '', + phone: '', + paymentMethod: 'sepa' as 'sepa' | 'card' | 'sofort', + invoiceByEmail: true, + invoiceSameAsShipping: true, + invoiceFullName: '', + invoiceStreet: '', + invoicePostalCode: '', + invoiceCity: '', + invoicePhone: '', + invoiceEmail: '', + signingCity: '', }); const [showThanks, setShowThanks] = useState(false); const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]); @@ -83,20 +88,50 @@ export default function SummaryPage() { const templateVariableNamesKey = useMemo(() => templateVariableNames.join('|'), [templateVariableNames]) const [contractVariables, setContractVariables] = useState>({}) + // Auto-compute contract variables from form state for preview useEffect(() => { if (!templateVariableNamesKey) return - setContractVariables(prev => { - let changed = false - const next: Record = { ...prev } - for (const name of templateVariableNames) { - if (next[name] === undefined) { - next[name] = '' - changed = true - } - } - return changed ? next : prev - }) - }, [templateVariableNamesKey, templateVariableNames]) + const fullName = `${form.firstName} ${form.lastName}`.trim() + const isCompany = user?.userType === 'company' || user?.user_type === 'company' + const invoiceSame = form.invoiceSameAsShipping + + const computed: Record = { + contractNumber: '(wird generiert)', + currentDate: new Date().toLocaleDateString('de-AT', { day: '2-digit', month: '2-digit', year: 'numeric' }), + recipientName: fullName, + recipientAddress: `${form.street}, ${form.postalCode} ${form.city}`.trim(), + shippingCustomerClass: isCompany ? '' : 'checked', + shippingCompanyClass: isCompany ? 'checked' : '', + shippingFullName: fullName, + shippingStreet: form.street, + shippingPostalCode: form.postalCode, + shippingCity: form.city, + shippingPhone: form.phone, + shippingEmail: form.email, + invoiceSameAsShippingMark: invoiceSame ? '✓' : '', + invoiceCompanyClass: isCompany ? 'checked' : '', + invoiceCustomerClass: isCompany ? '' : 'checked', + invoiceFullName: invoiceSame ? fullName : form.invoiceFullName, + invoiceStreet: invoiceSame ? form.street : form.invoiceStreet, + invoicePostalCode: invoiceSame ? form.postalCode : form.invoicePostalCode, + invoiceCity: invoiceSame ? form.city : form.invoiceCity, + invoicePhone: invoiceSame ? form.phone : form.invoicePhone, + invoiceEmail: invoiceSame ? form.email : form.invoiceEmail, + fnCheckedClass: '', + fnNumber: '', + atuCheckedClass: '', + atuNumber: '', + entrepreneurClass: isCompany ? 'checked' : '', + consumerClass: isCompany ? '' : 'checked', + paymentSepaClass: form.paymentMethod === 'sepa' ? 'checked' : '', + paymentCardClass: form.paymentMethod === 'card' ? 'checked' : '', + paymentSofortClass: form.paymentMethod === 'sofort' ? 'checked' : '', + invoiceByEmailClass: form.invoiceByEmail ? 'checked' : '', + signingCity: form.signingCity, + fullName, + } + setContractVariables(computed) + }, [templateVariableNamesKey, form, user, signatureDataUrl]) const populatedContractHtml = useMemo(() => { if (!contractHtml) return null @@ -399,14 +434,14 @@ export default function SummaryPage() { const taxAmountWithShipping = useMemo(() => netWithShipping * taxRate, [netWithShipping, taxRate]); const totalWithTax = useMemo(() => netWithShipping + taxAmountWithShipping, [netWithShipping, taxAmountWithShipping]); - const handleInput = (e: React.ChangeEvent) => { + const handleInput = (e: React.ChangeEvent) => { const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); }; - const handleRecipientNotes = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setForm(prev => ({ ...prev, [name]: value })); + const handleCheckbox = (e: React.ChangeEvent) => { + const { name, checked } = e.target; + setForm(prev => ({ ...prev, [name]: checked })); }; const fillFromLoggedInData = () => { @@ -435,7 +470,7 @@ export default function SummaryPage() { })); }; - const requiredSelfFields: Array = [ + const requiredSelfFields = [ 'firstName', 'lastName', 'email', @@ -443,17 +478,16 @@ export default function SummaryPage() { 'postalCode', 'city', 'country', - 'frequency', - ] + ] as const - const hasRequiredSelfFields = requiredSelfFields.every(k => form[k].trim() !== '') - const hasRequiredGiftFields = isForSelf || form.recipientEmail.trim() !== '' + const hasRequiredSelfFields = requiredSelfFields.every(k => String(form[k]).trim() !== '') + const hasRequiredInvoiceFields = form.invoiceSameAsShipping || form.invoiceEmail.trim() !== '' const canSubmit = selectedEntries.length > 0 && totalPacks === requiredPacks && hasRequiredSelfFields && - hasRequiredGiftFields; + hasRequiredInvoiceFields; const backToSelection = () => router.push('/coffee-abonnements'); @@ -464,10 +498,6 @@ export default function SummaryPage() { setSubmitError(`Order must contain exactly ${requiredPacks} packs (${selectedPlanCapsules} capsules).`) return } - if (!isForSelf && !form.recipientEmail.trim()) { - setSubmitError('Recipient email is required when the subscription is for someone else.') - return - } setSubmitError(null) setSubmitLoading(true) @@ -480,8 +510,7 @@ export default function SummaryPage() { billing_interval: 'month', interval_count: 1, is_auto_renew: true, - is_for_self: isForSelf, - // NEW: pass customer fields + is_for_self: true, firstName: form.firstName.trim(), lastName: form.lastName.trim(), email: form.email.trim(), @@ -489,12 +518,22 @@ export default function SummaryPage() { postalCode: form.postalCode.trim(), city: form.city.trim(), country: form.country.trim(), - frequency: form.frequency.trim(), - startDate: form.startDate.trim() || undefined, - recipient_email: isForSelf ? undefined : form.recipientEmail.trim(), - recipient_name: isForSelf ? undefined : (form.recipientName.trim() || undefined), - recipient_notes: isForSelf ? undefined : (form.recipientNotes.trim() || undefined), - // NEW: always include referred_by if available + frequency: 'monatlich', + phone: form.phone.trim() || undefined, + recipientContractName: `${form.firstName} ${form.lastName}`.trim() || undefined, + paymentMethod: form.paymentMethod, + invoiceByEmail: form.invoiceByEmail, + invoiceSameAsShipping: form.invoiceSameAsShipping, + ...(!form.invoiceSameAsShipping ? { + invoiceFullName: form.invoiceFullName.trim() || undefined, + invoiceStreet: form.invoiceStreet.trim() || undefined, + invoicePostalCode: form.invoicePostalCode.trim() || undefined, + invoiceCity: form.invoiceCity.trim() || undefined, + invoicePhone: form.invoicePhone.trim() || undefined, + invoiceEmail: form.invoiceEmail.trim() || undefined, + } : {}), + signingCity: form.signingCity.trim() || undefined, + signatureDataUrl: signatureDataUrl || undefined, referred_by: typeof currentUserId === 'number' ? currentUserId : undefined, } console.info('[SummaryPage] subscribeAbo payload:', payload) @@ -585,31 +624,6 @@ export default function SummaryPage() { > Fill fields with logged in data - {/* Toggle: For myself / For someone else */} -
- - -
{/* inputs translated */}
@@ -645,57 +659,70 @@ export default function SummaryPage() {
- - + +
-
- - +
+ + {/* Payment method */} +
+

Payment method

+
+ {(['sepa', 'card', 'sofort'] as const).map(method => ( + + ))}
- {!isForSelf && ( - <> + +
+ + {/* Invoice address */} +
+

Invoice address

+ + {!form.invoiceSameAsShipping && ( +
- - + +
- - + +
-
- -