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.
This commit is contained in:
seaznCode 2026-03-16 20:35:36 +01:00
parent c8092eb83c
commit 11e3e384bd
5 changed files with 253 additions and 203 deletions

120
package-lock.json generated
View File

@ -21,6 +21,7 @@
"@tailwindplus/elements": "^1.0.22", "@tailwindplus/elements": "^1.0.22",
"@tailwindui/react": "^0.1.1", "@tailwindui/react": "^0.1.1",
"axios": "^1.13.5", "axios": "^1.13.5",
"canvg": "^4.0.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"country-flag-icons": "^1.6.13", "country-flag-icons": "^1.6.13",
@ -111,7 +112,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@ -451,7 +451,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
}, },
@ -475,7 +474,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
} }
@ -3360,7 +3358,6 @@
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz",
"integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.17.8", "@babel/runtime": "^7.17.8",
"@types/webxr": "*", "@types/webxr": "*",
@ -3874,15 +3871,13 @@
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.14", "version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@ -3917,7 +3912,6 @@
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz", "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
"integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==", "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0", "@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3", "@tweenjs/tween.js": "~23.1.3",
@ -3992,7 +3986,6 @@
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/scope-manager": "8.56.0",
"@typescript-eslint/types": "8.56.0", "@typescript-eslint/types": "8.56.0",
@ -4522,7 +4515,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -4992,7 +4984,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@ -5123,6 +5114,39 @@
], ],
"license": "CC-BY-4.0" "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": { "node_modules/class-variance-authority": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@ -5378,18 +5402,13 @@
"postcss": "^8.4" "postcss": "^8.4"
} }
}, },
"node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { "node_modules/css-line-break": {
"version": "7.1.1", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "utrie": "^1.0.2"
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
} }
}, },
"node_modules/css-prefers-color-scheme": { "node_modules/css-prefers-color-scheme": {
@ -5448,8 +5467,7 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
@ -5901,7 +5919,6 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -6087,7 +6104,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@ -6858,8 +6874,7 @@
"version": "3.14.2", "version": "3.14.2",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
"integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==", "integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license.", "license": "Standard 'no charge' license: https://gsap.com/standard-license."
"peer": true
}, },
"node_modules/has-bigints": { "node_modules/has-bigints": {
"version": "1.1.0", "version": "1.1.0",
@ -7719,6 +7734,26 @@
"html2canvas": "^1.0.0-rc.5" "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": { "node_modules/jsx-ast-utils": {
"version": "3.3.5", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@ -8825,8 +8860,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
@ -8840,7 +8874,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -8878,7 +8911,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -9610,19 +9642,6 @@
"node": ">=4" "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": { "node_modules/postcss-value-parser": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"performance-now": "^2.1.0" "performance-now": "^2.1.0"
} }
@ -9733,7 +9751,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -9743,7 +9760,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -9776,7 +9792,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@ -10034,7 +10049,6 @@
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md", "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": { "engines": {
"node": ">= 0.8.15" "node": ">= 0.8.15"
} }
@ -10406,7 +10420,6 @@
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=0.1.14" "node": ">=0.1.14"
} }
@ -10665,7 +10678,6 @@
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
} }
@ -10690,7 +10702,6 @@
"version": "4.1.18", "version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/tailwindcss-animate": { "node_modules/tailwindcss-animate": {
@ -10735,8 +10746,7 @@
"version": "0.182.0", "version": "0.182.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz", "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==", "integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/three-mesh-bvh": { "node_modules/three-mesh-bvh": {
"version": "0.8.3", "version": "0.8.3",
@ -11054,7 +11064,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -11476,7 +11485,6 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -22,6 +22,7 @@
"@tailwindplus/elements": "^1.0.22", "@tailwindplus/elements": "^1.0.22",
"@tailwindui/react": "^0.1.1", "@tailwindui/react": "^0.1.1",
"axios": "^1.13.5", "axios": "^1.13.5",
"canvg": "^4.0.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"country-flag-icons": "^1.6.13", "country-flag-icons": "^1.6.13",

View File

@ -11,8 +11,7 @@ export type SubscribeAboInput = {
target_user_id?: number target_user_id?: number
recipient_name?: string recipient_name?: string
recipient_email?: string recipient_email?: string
recipient_notes?: string // Customer fields
// NEW: customer fields
firstName?: string firstName?: string
lastName?: string lastName?: string
email?: string email?: string
@ -21,8 +20,22 @@ export type SubscribeAboInput = {
city?: string city?: string
country?: string country?: string
frequency?: string frequency?: string
startDate?: string // New contract / contact fields
// NEW: logged-in user id 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 referred_by?: number | string
} }
@ -48,7 +61,7 @@ export async function subscribeAbo(input: SubscribeAboInput) {
} }
// NEW: validate customer fields (required in UI) // 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 missing = requiredFields.filter(k => {
const v = (input as any)[k] const v = (input as any)[k]
return typeof v !== 'string' || v.trim() === '' return typeof v !== 'string' || v.trim() === ''
@ -62,7 +75,7 @@ export async function subscribeAbo(input: SubscribeAboInput) {
interval_count: input.interval_count ?? 1, interval_count: input.interval_count ?? 1,
is_auto_renew: input.is_auto_renew ?? true, is_auto_renew: input.is_auto_renew ?? true,
is_for_self: isForSelf, is_for_self: isForSelf,
// NEW: include customer fields // Customer fields
firstName: input.firstName, firstName: input.firstName,
lastName: input.lastName, lastName: input.lastName,
email: input.email, email: input.email,
@ -71,7 +84,25 @@ export async function subscribeAbo(input: SubscribeAboInput) {
city: input.city, city: input.city,
country: input.country?.toUpperCase?.() ?? input.country, country: input.country?.toUpperCase?.() ?? input.country,
frequency: input.frequency, 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) { if (hasItems) {
body.items = input.items!.map(i => ({ 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 (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_email) body.recipient_email = input.recipient_email
if (!isForSelf && input.recipient_name) body.recipient_name = input.recipient_name 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 // NEW: always include referred_by if provided
if (input.referred_by != null) body.referred_by = input.referred_by if (input.referred_by != null) body.referred_by = input.referred_by

View File

@ -1,6 +1,9 @@
'use client' 'use client'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { authFetch } from '../../../utils/authFetch'
const apiBase = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '')
export function useAboContractTemplateHtml() { export function useAboContractTemplateHtml() {
const [html, setHtml] = useState<string | null>(null) const [html, setHtml] = useState<string | null>(null)
@ -14,10 +17,10 @@ export function useAboContractTemplateHtml() {
setLoading(true) setLoading(true)
setError(null) setError(null)
try { try {
const res = await fetch('/templates/abo-contract-template.html', { const res = await authFetch(`${apiBase}/api/contracts/abo/active`, {
method: 'GET', method: 'GET',
headers: { Accept: 'text/html' }, headers: { Accept: 'text/html' },
cache: 'no-store', credentials: 'include',
}) })
const text = await res.text().catch(() => '') const text = await res.text().catch(() => '')

View File

@ -55,7 +55,6 @@ export default function SummaryPage() {
const [contractPdfError, setContractPdfError] = useState<string | null>(null) const [contractPdfError, setContractPdfError] = useState<string | null>(null)
const [selections, setSelections] = useState<Record<string, number>>({}); const [selections, setSelections] = useState<Record<string, number>>({});
const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<60 | 120>(120); const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<60 | 120>(120);
const [isForSelf, setIsForSelf] = useState(true);
const [signatureDataUrl, setSignatureDataUrl] = useState('') const [signatureDataUrl, setSignatureDataUrl] = useState('')
const [form, setForm] = useState({ const [form, setForm] = useState({
firstName: '', firstName: '',
@ -65,11 +64,17 @@ export default function SummaryPage() {
postalCode: '', postalCode: '',
city: '', city: '',
country: 'DE', country: 'DE',
frequency: 'monatlich', phone: '',
startDate: '', paymentMethod: 'sepa' as 'sepa' | 'card' | 'sofort',
recipientEmail: '', invoiceByEmail: true,
recipientName: '', invoiceSameAsShipping: true,
recipientNotes: '', invoiceFullName: '',
invoiceStreet: '',
invoicePostalCode: '',
invoiceCity: '',
invoicePhone: '',
invoiceEmail: '',
signingCity: '',
}); });
const [showThanks, setShowThanks] = useState(false); const [showThanks, setShowThanks] = useState(false);
const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]); 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 templateVariableNamesKey = useMemo(() => templateVariableNames.join('|'), [templateVariableNames])
const [contractVariables, setContractVariables] = useState<Record<string, string>>({}) const [contractVariables, setContractVariables] = useState<Record<string, string>>({})
// Auto-compute contract variables from form state for preview
useEffect(() => { useEffect(() => {
if (!templateVariableNamesKey) return if (!templateVariableNamesKey) return
setContractVariables(prev => { const fullName = `${form.firstName} ${form.lastName}`.trim()
let changed = false const isCompany = user?.userType === 'company' || user?.user_type === 'company'
const next: Record<string, string> = { ...prev } const invoiceSame = form.invoiceSameAsShipping
for (const name of templateVariableNames) {
if (next[name] === undefined) { const computed: Record<string, string> = {
next[name] = '' contractNumber: '(wird generiert)',
changed = true currentDate: new Date().toLocaleDateString('de-AT', { day: '2-digit', month: '2-digit', year: 'numeric' }),
} recipientName: fullName,
} recipientAddress: `${form.street}, ${form.postalCode} ${form.city}`.trim(),
return changed ? next : prev shippingCustomerClass: isCompany ? '' : 'checked',
}) shippingCompanyClass: isCompany ? 'checked' : '',
}, [templateVariableNamesKey, templateVariableNames]) 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(() => { const populatedContractHtml = useMemo(() => {
if (!contractHtml) return null if (!contractHtml) return null
@ -399,14 +434,14 @@ export default function SummaryPage() {
const taxAmountWithShipping = useMemo(() => netWithShipping * taxRate, [netWithShipping, taxRate]); const taxAmountWithShipping = useMemo(() => netWithShipping * taxRate, [netWithShipping, taxRate]);
const totalWithTax = useMemo(() => netWithShipping + taxAmountWithShipping, [netWithShipping, taxAmountWithShipping]); const totalWithTax = useMemo(() => netWithShipping + taxAmountWithShipping, [netWithShipping, taxAmountWithShipping]);
const handleInput = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleInput = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value })); setForm(prev => ({ ...prev, [name]: value }));
}; };
const handleRecipientNotes = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, checked } = e.target;
setForm(prev => ({ ...prev, [name]: value })); setForm(prev => ({ ...prev, [name]: checked }));
}; };
const fillFromLoggedInData = () => { const fillFromLoggedInData = () => {
@ -435,7 +470,7 @@ export default function SummaryPage() {
})); }));
}; };
const requiredSelfFields: Array<keyof typeof form> = [ const requiredSelfFields = [
'firstName', 'firstName',
'lastName', 'lastName',
'email', 'email',
@ -443,17 +478,16 @@ export default function SummaryPage() {
'postalCode', 'postalCode',
'city', 'city',
'country', 'country',
'frequency', ] as const
]
const hasRequiredSelfFields = requiredSelfFields.every(k => form[k].trim() !== '') const hasRequiredSelfFields = requiredSelfFields.every(k => String(form[k]).trim() !== '')
const hasRequiredGiftFields = isForSelf || form.recipientEmail.trim() !== '' const hasRequiredInvoiceFields = form.invoiceSameAsShipping || form.invoiceEmail.trim() !== ''
const canSubmit = const canSubmit =
selectedEntries.length > 0 && selectedEntries.length > 0 &&
totalPacks === requiredPacks && totalPacks === requiredPacks &&
hasRequiredSelfFields && hasRequiredSelfFields &&
hasRequiredGiftFields; hasRequiredInvoiceFields;
const backToSelection = () => router.push('/coffee-abonnements'); const backToSelection = () => router.push('/coffee-abonnements');
@ -464,10 +498,6 @@ export default function SummaryPage() {
setSubmitError(`Order must contain exactly ${requiredPacks} packs (${selectedPlanCapsules} capsules).`) setSubmitError(`Order must contain exactly ${requiredPacks} packs (${selectedPlanCapsules} capsules).`)
return return
} }
if (!isForSelf && !form.recipientEmail.trim()) {
setSubmitError('Recipient email is required when the subscription is for someone else.')
return
}
setSubmitError(null) setSubmitError(null)
setSubmitLoading(true) setSubmitLoading(true)
@ -480,8 +510,7 @@ export default function SummaryPage() {
billing_interval: 'month', billing_interval: 'month',
interval_count: 1, interval_count: 1,
is_auto_renew: true, is_auto_renew: true,
is_for_self: isForSelf, is_for_self: true,
// NEW: pass customer fields
firstName: form.firstName.trim(), firstName: form.firstName.trim(),
lastName: form.lastName.trim(), lastName: form.lastName.trim(),
email: form.email.trim(), email: form.email.trim(),
@ -489,12 +518,22 @@ export default function SummaryPage() {
postalCode: form.postalCode.trim(), postalCode: form.postalCode.trim(),
city: form.city.trim(), city: form.city.trim(),
country: form.country.trim(), country: form.country.trim(),
frequency: form.frequency.trim(), frequency: 'monatlich',
startDate: form.startDate.trim() || undefined, phone: form.phone.trim() || undefined,
recipient_email: isForSelf ? undefined : form.recipientEmail.trim(), recipientContractName: `${form.firstName} ${form.lastName}`.trim() || undefined,
recipient_name: isForSelf ? undefined : (form.recipientName.trim() || undefined), paymentMethod: form.paymentMethod,
recipient_notes: isForSelf ? undefined : (form.recipientNotes.trim() || undefined), invoiceByEmail: form.invoiceByEmail,
// NEW: always include referred_by if available 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, referred_by: typeof currentUserId === 'number' ? currentUserId : undefined,
} }
console.info('[SummaryPage] subscribeAbo payload:', payload) console.info('[SummaryPage] subscribeAbo payload:', payload)
@ -585,31 +624,6 @@ export default function SummaryPage() {
> >
Fill fields with logged in data Fill fields with logged in data
</button> </button>
{/* Toggle: For myself / For someone else */}
<div className="flex gap-2 mb-4">
<button
type="button"
onClick={() => setIsForSelf(true)}
className={`flex-1 rounded-md px-3 py-2 text-sm font-medium transition ${
isForSelf
? 'bg-[#1C2B4A] text-white shadow'
: 'border border-[#1C2B4A] text-[#1C2B4A] hover:bg-[#1C2B4A]/5'
}`}
>
For myself
</button>
<button
type="button"
onClick={() => setIsForSelf(false)}
className={`flex-1 rounded-md px-3 py-2 text-sm font-medium transition ${
!isForSelf
? 'bg-[#1C2B4A] text-white shadow'
: 'border border-[#1C2B4A] text-[#1C2B4A] hover:bg-[#1C2B4A]/5'
}`}
>
For someone else
</button>
</div>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
{/* inputs translated */} {/* inputs translated */}
<div> <div>
@ -645,57 +659,70 @@ export default function SummaryPage() {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-1">Delivery interval</label> <label className="block text-sm font-medium mb-1">Phone (optional)</label>
<select name="frequency" value={form.frequency} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"> <input name="phone" value={form.phone} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
<option value="monatlich">Monthly</option>
<option value="zweimonatlich">Every 2 months</option>
<option value="vierteljährlich">Quarterly</option>
</select>
</div> </div>
<div> </div>
<label className="block text-sm font-medium mb-1">Start date (optional)</label>
<input type="date" name="startDate" value={form.startDate} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" /> {/* Payment method */}
<div className="mt-6 border-t border-gray-200 pt-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">Payment method</h3>
<div className="flex flex-wrap gap-3">
{(['sepa', 'card', 'sofort'] as const).map(method => (
<label key={method} className={`flex items-center gap-2 rounded-md border px-4 py-2 cursor-pointer transition ${form.paymentMethod === method ? 'border-[#1C2B4A] bg-[#1C2B4A]/5 font-medium' : 'border-gray-300 hover:bg-gray-50'}`}>
<input type="radio" name="paymentMethod" value={method} checked={form.paymentMethod === method} onChange={handleInput} className="accent-[#1C2B4A]" />
{method === 'sepa' ? 'SEPA' : method === 'card' ? 'Credit Card' : 'Sofort Banking'}
</label>
))}
</div> </div>
{!isForSelf && ( <label className="mt-3 flex items-center gap-2 text-sm">
<> <input type="checkbox" name="invoiceByEmail" checked={form.invoiceByEmail} onChange={handleCheckbox} className="accent-[#1C2B4A]" />
Send invoice by email
</label>
</div>
{/* Invoice address */}
<div className="mt-6 border-t border-gray-200 pt-4">
<h3 className="text-base font-semibold text-gray-900 mb-3">Invoice address</h3>
<label className="flex items-center gap-2 text-sm mb-3">
<input type="checkbox" name="invoiceSameAsShipping" checked={form.invoiceSameAsShipping} onChange={handleCheckbox} className="accent-[#1C2B4A]" />
Same as shipping address
</label>
{!form.invoiceSameAsShipping && (
<div className="grid gap-4 sm:grid-cols-2">
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<label className="block text-sm font-medium mb-1">Recipient email</label> <label className="block text-sm font-medium mb-1">Full name</label>
<input <input name="invoiceFullName" value={form.invoiceFullName} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
type="email"
name="recipientEmail"
value={form.recipientEmail}
onChange={handleInput}
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<label className="block text-sm font-medium mb-1">Recipient name (optional)</label> <label className="block text-sm font-medium mb-1">Street & No.</label>
<input <input name="invoiceStreet" value={form.invoiceStreet} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
name="recipientName"
value={form.recipientName}
onChange={handleInput}
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
</div> </div>
<div className="sm:col-span-2"> <div>
<label className="block text-sm font-medium mb-1">Recipient note (optional)</label> <label className="block text-sm font-medium mb-1">ZIP</label>
<textarea <input name="invoicePostalCode" value={form.invoicePostalCode} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
name="recipientNotes"
value={form.recipientNotes}
onChange={handleRecipientNotes}
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
rows={3}
/>
</div> </div>
</> <div>
<label className="block text-sm font-medium mb-1">City</label>
<input name="invoiceCity" value={form.invoiceCity} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
</div>
<div>
<label className="block text-sm font-medium mb-1">Phone (optional)</label>
<input name="invoicePhone" value={form.invoicePhone} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
</div>
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input type="email" name="invoiceEmail" value={form.invoiceEmail} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
</div>
</div>
)} )}
</div> </div>
{/* Contract preview + signature (frontend only for now) */} {/* Contract preview + signature */}
<div className="mt-6 border-t border-gray-200 pt-6"> <div className="mt-6 border-t border-gray-200 pt-6">
<h3 className="text-base font-semibold text-gray-900 mb-2">Contract template preview (ABO)</h3> <h3 className="text-base font-semibold text-gray-900 mb-2">Contract preview (ABO)</h3>
<p className="text-xs text-gray-600 mb-3"> <p className="text-xs text-gray-600 mb-3">
This is the ABO contract HTML template (populated from the fields below, frontend-only). Contract variables are auto-populated from your form data.
</p> </p>
{contractLoading ? ( {contractLoading ? (
@ -707,41 +734,24 @@ export default function SummaryPage() {
Contract preview could not be loaded: {contractError} Contract preview could not be loaded: {contractError}
</div> </div>
) : populatedContractHtml ? ( ) : populatedContractHtml ? (
<> <button
{templateVariableNames.length > 0 && ( type="button"
<div className="mb-4 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3"> onClick={openContractPreview}
<div className="text-sm font-semibold text-gray-900 mb-2">Contract variables</div> className="inline-flex items-center justify-center rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90"
<div className="grid gap-4 sm:grid-cols-2"> >
{templateVariableNames.map(varName => ( Open preview
<div key={varName}> </button>
<label className="block text-xs font-medium mb-1 text-gray-700">{varName}</label>
<input
value={contractVariables[varName] ?? ''}
onChange={e =>
setContractVariables(prev => ({ ...prev, [varName]: e.target.value }))
}
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
</div>
))}
</div>
</div>
)}
<button
type="button"
onClick={openContractPreview}
className="inline-flex items-center justify-center rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90"
>
Open preview
</button>
</>
) : ( ) : (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700"> <div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
Contract template is not available. Contract template is not available.
</div> </div>
)} )}
<div className="mt-4"> <div className="mt-4 space-y-3">
<div>
<label className="block text-sm font-medium mb-1">Ort (Signing City)</label>
<input type="text" name="signingCity" value={form.signingCity} onChange={handleInput} className="w-full max-w-xs rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" placeholder="z.B. Wien" />
</div>
<SignaturePad value={signatureDataUrl} onChange={setSignatureDataUrl} /> <SignaturePad value={signatureDataUrl} onChange={setSignatureDataUrl} />
</div> </div>
</div> </div>
@ -796,9 +806,7 @@ export default function SummaryPage() {
</button> </button>
{!canSubmit && ( {!canSubmit && (
<p className="text-xs text-gray-500 mt-2"> <p className="text-xs text-gray-500 mt-2">
{isForSelf Please select coffees and fill all required buyer fields.
? 'Please select coffees and fill all required buyer fields.'
: 'Please select coffees and fill all required buyer fields plus recipient email.'}
</p> </p>
)} )}
</div> </div>
@ -884,7 +892,7 @@ export default function SummaryPage() {
</div> </div>
<h3 className="text-2xl font-bold">Thanks for your subscription!</h3> <h3 className="text-2xl font-bold">Thanks for your subscription!</h3>
<p className="mt-1 text-sm text-gray-600"> <p className="mt-1 text-sm text-gray-600">
{isForSelf ? 'Subscription created.' : 'Subscription created, invitation sent.'} Subscription created.
</p> </p>
<div className="mt-6 grid gap-3 sm:grid-cols-2"> <div className="mt-6 grid gap-3 sm:grid-cols-2">