diff --git a/eslint.config.mjs b/eslint.config.mjs
index 42817f9..719cea2 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,14 +1,25 @@
-import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
+import { dirname } from "path";
+import { fileURLToPath } from "url";
+import { FlatCompat } from "@eslint/eslintrc";
-export default [
- ...nextCoreWebVitals,
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
- 'node_modules/**',
- '.next/**',
- 'out/**',
- 'build/**',
- 'next-env.d.ts',
+ "node_modules/**",
+ ".next/**",
+ "out/**",
+ "build/**",
+ "next-env.d.ts",
],
},
];
+
+export default eslintConfig;
diff --git a/package-lock.json b/package-lock.json
index b55e904..400e880 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,15 +21,12 @@
"@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",
"country-select-js": "^2.1.0",
"gsap": "^3.14.2",
- "html2canvas": "^1.4.1",
"intl-tel-input": "^26.4.1",
- "jspdf": "^4.2.0",
"lucide-react": "^0.574.0",
"motion": "^12.34.1",
"next": "^16.1.6",
@@ -51,14 +48,14 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
- "@eslint/js": "^9.0.1",
+ "@eslint/js": "^10.0.1",
"@tailwindcss/postcss": "^4",
"@types/node": "^25",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.24",
"baseline-browser-mapping": "^2.9.19",
- "eslint": "^9.0.0",
+ "eslint": "^10.0.0",
"eslint-config-next": "^16.1.6",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^17.3.0",
@@ -559,6 +556,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/@csstools/postcss-cascade-layers/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@csstools/postcss-color-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-5.0.1.tgz",
@@ -959,6 +970,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/@csstools/postcss-is-pseudo-class/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@csstools/postcss-light-dark-function": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-3.0.0.tgz",
@@ -1437,6 +1462,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/@csstools/postcss-scope-pseudo-class/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@csstools/postcss-sign-functions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-2.0.0.tgz",
@@ -1786,61 +1825,103 @@
}
},
"node_modules/@eslint/config-array": {
- "version": "0.21.2",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
- "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz",
+ "integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/object-schema": "^2.1.7",
+ "@eslint/object-schema": "^3.0.1",
"debug": "^4.3.1",
- "minimatch": "^3.1.5"
+ "minimatch": "^10.1.1"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/balanced-match": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz",
+ "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jackspeak": "^4.2.3"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
+ "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz",
+ "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@eslint/config-helpers": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
- "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz",
+ "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/core": "^0.17.0"
+ "@eslint/core": "^1.1.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/core": {
- "version": "0.17.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
- "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz",
+ "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/eslintrc": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
- "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ajv": "^6.14.0",
+ "ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.1",
- "minimatch": "^3.1.5",
+ "minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
@@ -1864,40 +1945,48 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.39.4",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
- "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
}
},
"node_modules/@eslint/object-schema": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
- "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz",
+ "integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/plugin-kit": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
- "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz",
+ "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/core": "^0.17.0",
+ "@eslint/core": "^1.1.0",
"levn": "^0.4.1"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@floating-ui/core": {
@@ -2522,6 +2611,16 @@
"url": "https://opencollective.com/libvips"
}
},
+ "node_modules/@isaacs/cliui": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
+ "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -3743,19 +3842,6 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
- "node_modules/@tailwindcss/typography/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/@tailwindplus/elements": {
"version": "1.0.22",
"resolved": "https://registry.npmjs.org/@tailwindplus/elements/-/elements-1.0.22.tgz",
@@ -3824,6 +3910,13 @@
"integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
"license": "MIT"
},
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -3861,22 +3954,11 @@
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
"license": "MIT"
},
- "node_modules/@types/pako": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
- "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
- "license": "MIT"
- },
- "node_modules/@types/raf": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
- "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
- "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==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -3928,13 +4010,6 @@
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT"
},
- "node_modules/@types/trusted-types": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
- "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
- "license": "MIT",
- "optional": true
- },
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
@@ -4140,13 +4215,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.2"
+ "brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -4533,9 +4608,9 @@
}
},
"node_modules/ajv": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
- "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4549,35 +4624,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/ansi-styles": {
- "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"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/ansi-styles/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -4875,15 +4921,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/base64-arraybuffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
- "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -5114,39 +5151,6 @@
],
"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",
@@ -5286,18 +5290,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/core-js": {
- "version": "3.48.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
- "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
"node_modules/country-flag-icons": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.13.tgz",
@@ -5374,6 +5366,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/css-blank-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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/css-has-pseudo": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-8.0.0.tgz",
@@ -5402,13 +5408,18 @@
"postcss": "^8.4"
}
},
- "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==",
+ "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,
"license": "MIT",
"dependencies": {
- "utrie": "^1.0.2"
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
}
},
"node_modules/css-prefers-color-scheme": {
@@ -5647,16 +5658,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/dompurify": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
- "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
- "license": "(MPL-2.0 OR Apache-2.0)",
- "optional": true,
- "optionalDependencies": {
- "@types/trusted-types": "^2.0.7"
- }
- },
"node_modules/draco3d": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
@@ -5914,33 +5915,30 @@
}
},
"node_modules/eslint": {
- "version": "9.39.4",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
- "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz",
+ "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
- "@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.21.2",
- "@eslint/config-helpers": "^0.4.2",
- "@eslint/core": "^0.17.0",
- "@eslint/eslintrc": "^3.3.5",
- "@eslint/js": "9.39.4",
- "@eslint/plugin-kit": "^0.4.1",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.0",
+ "@eslint/config-helpers": "^0.5.2",
+ "@eslint/core": "^1.1.0",
+ "@eslint/plugin-kit": "^0.6.0",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
- "ajv": "^6.14.0",
- "chalk": "^4.0.0",
+ "ajv": "^6.12.4",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.4.0",
- "eslint-visitor-keys": "^4.2.1",
- "espree": "^10.4.0",
- "esquery": "^1.5.0",
+ "eslint-scope": "^9.1.0",
+ "eslint-visitor-keys": "^5.0.0",
+ "espree": "^11.1.0",
+ "esquery": "^1.7.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^8.0.0",
@@ -5950,8 +5948,7 @@
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.5",
+ "minimatch": "^10.1.1",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
@@ -5959,7 +5956,7 @@
"eslint": "bin/eslint.js"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://eslint.org/donate"
@@ -6250,17 +6247,19 @@
}
},
"node_modules/eslint-scope": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
- "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz",
+ "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -6279,19 +6278,66 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "node_modules/eslint/node_modules/balanced-match": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz",
+ "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==",
"dev": true,
- "license": "Apache-2.0",
+ "license": "MIT",
+ "dependencies": {
+ "jackspeak": "^4.2.3"
+ },
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
+ "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/eslint/node_modules/espree": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz",
+ "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz",
+ "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -6428,23 +6474,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fast-png": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
- "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
- "license": "MIT",
- "dependencies": {
- "@types/pako": "^2.0.3",
- "iobuffer": "^5.3.2",
- "pako": "^2.1.0"
- }
- },
- "node_modules/fast-png/node_modules/pako": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
- "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
- "license": "(MIT AND Zlib)"
- },
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -6543,9 +6572,9 @@
}
},
"node_modules/flatted": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
- "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"license": "ISC"
},
@@ -6889,16 +6918,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@@ -7005,19 +7024,6 @@
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
"license": "ISC"
},
- "node_modules/html2canvas": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
- "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
- "license": "MIT",
- "dependencies": {
- "css-line-break": "^2.1.0",
- "text-segmentation": "^1.0.3"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
"node_modules/hyphen": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz",
@@ -7138,12 +7144,6 @@
"site"
]
},
- "node_modules/iobuffer": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
- "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
- "license": "MIT"
- },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -7632,6 +7632,22 @@
"react": "^19.0.0"
}
},
+ "node_modules/jackspeak": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
+ "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^9.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/jay-peg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz",
@@ -7717,43 +7733,6 @@
"node": ">=6"
}
},
- "node_modules/jspdf": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz",
- "integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.28.6",
- "fast-png": "^6.2.0",
- "fflate": "^0.8.1"
- },
- "optionalDependencies": {
- "canvg": "^3.0.11",
- "core-js": "^3.6.0",
- "dompurify": "^3.3.1",
- "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",
@@ -8131,13 +8110,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/logform": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
@@ -8301,19 +8273,6 @@
"node": ">=8.6"
}
},
- "node_modules/micromatch/node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -8345,9 +8304,9 @@
}
},
"node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -8856,12 +8815,6 @@
"node-readable-to-web-readable-stream": "^0.4.2"
}
},
- "node_modules/performance-now": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
- "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
- "license": "MIT"
- },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -8869,13 +8822,13 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=12"
+ "node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -8946,6 +8899,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/postcss-attribute-case-insensitive/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-clamp": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz",
@@ -9134,6 +9101,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/postcss-custom-selectors/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-dir-pseudo-class": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-10.0.0.tgz",
@@ -9160,6 +9141,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/postcss-dir-pseudo-class/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-double-position-gradients": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-7.0.0.tgz",
@@ -9214,6 +9209,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/postcss-focus-visible/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-focus-within": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-10.0.0.tgz",
@@ -9240,6 +9249,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/postcss-focus-within/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-font-variant": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz",
@@ -9384,6 +9407,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/postcss-nesting/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-opacity-percentage": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz",
@@ -9592,6 +9629,20 @@
"postcss": "^8.4"
}
},
+ "node_modules/postcss-pseudo-class-any-link/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,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-replace-overflow-wrap": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz",
@@ -9628,7 +9679,7 @@
"postcss": "^8.4"
}
},
- "node_modules/postcss-selector-parser": {
+ "node_modules/postcss-selector-not/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==",
@@ -9642,6 +9693,19 @@
"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",
@@ -9737,15 +9801,6 @@
],
"license": "MIT"
},
- "node_modules/raf": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
- "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
- "license": "MIT",
- "dependencies": {
- "performance-now": "^2.1.0"
- }
- },
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
@@ -9949,13 +10004,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
- "license": "MIT",
- "optional": true
- },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -10044,15 +10092,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/rgbcolor": {
- "version": "1.0.1",
- "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",
- "engines": {
- "node": ">= 0.8.15"
- }
- },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -10415,15 +10454,6 @@
"node": "*"
}
},
- "node_modules/stackblur-canvas": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
- "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.1.14"
- }
- },
"node_modules/stats-gl": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
@@ -10632,19 +10662,6 @@
}
}
},
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -10673,15 +10690,6 @@
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==",
"license": "ISC"
},
- "node_modules/svg-pathdata": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
- "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/tabbable": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
@@ -10702,6 +10710,7 @@
"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": {
@@ -10733,15 +10742,6 @@
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"license": "MIT"
},
- "node_modules/text-segmentation": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
- "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
- "license": "MIT",
- "dependencies": {
- "utrie": "^1.0.2"
- }
- },
"node_modules/three": {
"version": "0.182.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
@@ -10815,6 +10815,19 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -11248,15 +11261,6 @@
"node": ">= 4"
}
},
- "node_modules/utrie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
- "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
- "license": "MIT",
- "dependencies": {
- "base64-arraybuffer": "^1.0.2"
- }
- },
"node_modules/vite-compatible-readable-stream": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
diff --git a/package.json b/package.json
index 0a5c2b0..364be82 100644
--- a/package.json
+++ b/package.json
@@ -22,15 +22,12 @@
"@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",
"country-select-js": "^2.1.0",
"gsap": "^3.14.2",
- "html2canvas": "^1.4.1",
"intl-tel-input": "^26.4.1",
- "jspdf": "^4.2.0",
"lucide-react": "^0.574.0",
"motion": "^12.34.1",
"next": "^16.1.6",
@@ -68,4 +65,4 @@
"tailwindcss": "^4.1.18",
"typescript": "^5"
}
-}
+}
\ No newline at end of file
diff --git a/public/templates/abo-contract-template.html b/public/templates/abo-contract-template.html
deleted file mode 100644
index 34f5747..0000000
--- a/public/templates/abo-contract-template.html
+++ /dev/null
@@ -1,694 +0,0 @@
-
-
-
-
-
- ABO Vertrag – Profit Planet GmbH
-
-
-
-
-
-
-
-
-
Vertrag über automatische Wiederbestellungen (ABO)
-
Bitte alle Felder vollständig ausfüllen und Zutreffendes ankreuzen.
-
-
-
-
-
An die
-
-
-
Empfänger
-
{{recipientName}}
-
-
-
Adresse
-
{{recipientAddress}}
-
-
-
-
Lieferadresse
-
-
-
-
- KUNDE
- FIRMA
-
-
Vor- und Nachname
{{shippingFullName}}
-
Adresse
{{shippingStreet}}
-
PLZ / Ort
{{shippingPostalCode}} {{shippingCity}}
-
-
-
Telefonnummer
{{shippingPhone}}
-
-
E-Mail-Adresse
{{shippingEmail}}
-
-
-
-
-
-
Rechnungsadresse: {{invoiceSameAsShippingMark}} wie Lieferadresse
-
-
-
-
-
Rechnungsadresse (falls abweichend)
-
-
-
-
- FIRMA
- KUNDE
-
-
Vor- und Nachname
{{invoiceFullName}}
-
-
PLZ / Ort
{{invoicePostalCode}} {{invoiceCity}}
-
-
-
Telefonnummer
{{invoicePhone}}
-
-
E-Mail-Adresse
{{invoiceEmail}}
-
-
-
-
- FN: {{fnNumber}}
- ATU: {{atuNumber}}
-
-
-
-
Zutreffendes bitte ankreuzen
-
-
-
- Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als Unternehmer im Sinne des § 1 Abs 1 Z 1 KSchG, das heißt, das Geschäft gehört zum Betrieb seines Unternehmens.
-
-
-
- Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als Konsument im Sinne des § 1 Abs 1 Z 2 KSchG.
-
-
-
-
Angebote
-
-
- Mindestbestellmenge für BIO Kaffee und BIO Tee und BIO Kakao beträgt pro Bestellung jeweils 120 Kapseln .
- Preis pro Kapsel € 2,97 inkl. 20% MwSt. Preise und Konditionen gemäß gültigem PROFIT PLANET GMBH Tarif.
-
-
-
-
- Tarif
- Preis pro Kapsel
-
-
-
-
- Customer without abo
- 2.97€
-
-
- Customer with abo
- 1.77€
-
-
-
-
-
-
Produktauswahl
-
-
Superfood Coffee – 60 Kapseln (bitte gewünschte Sorten ankreuzen / ergänzen)
- {{selectedProductsHtml}}
-
-
- Bei Angabe einer automatischen Wiederbestellung, gemäß den Regelungen in nachstehendem Punkt 3, erhält der Kunde in regelmäßigen Abständen,
- BEGINNEND AM (Unterzeichnung des Vertrages) vorstehend eingetragene BIO Kaffee-Teemenge für die Dauer des Vertrages oder bis zum Widerruf der automatischen Wiederbestellung.
- Der BIO Kaffee-Tee wird automatisch im monatlichen Abstand fakturiert und innerhalb von drei bis fünf Werktagen an den Kunden geliefert.
-
-
-
-
-
Zahlungsart
-
-
- Sepa
- Kreditkarte
- Sofortbanking
-
-
- Bitte senden Sie mir meine Rechnung per E-Mail zu!
-
-
-
-
-
§ 1 Vertragsgegenstand/Geltung der Allgemeinen Geschäftsbedingungen
-
(1) Die Allgemeinen Geschäftsbedingungen (AGB, siehe unten) der Profit Planet GmbH sind verbindlicher Bestandteil dieses Vertrages. Abweichende, entgegenstehende oder ergänzende Allgemeine Geschäftsbedingungen des Kunden werden nur dann und insoweit Vertragsbestandteil, als Profit Planet GmbH ihrer Geltung ausdrücklich zugestimmt hat. Dieses Zustimmungserfordernis gilt in jedem Fall, beispielsweise auch dann, wenn Profit Planet GmbH in Kenntnis der AGB des Kunden die Lieferung an ihn vorbehaltlos ausführt.
-
-
§ 2 Laufzeit des Vertrages/Zahlung per Lastschrift/Abrechnung
-
(1) Der Vertrag hat eine Laufzeit von 36 Monaten.
-
(2) Ist der Kunde Unternehmer, verlängert sich der Vertrag nach Ablauf der 36 Monate jeweils um 3 Monate, sofern er nicht von einer der Parteien mit einer Frist von 4 Wochen vor Vertragsende gekündigt wird.
-
(3) Ist der Kunde Verbraucher, wird die Profit Planet GmbH den Verbraucher spätestens drei Monate und frühestens fünf Monate vor Ablauf der Vertragsdauer in Textform auf das bevorstehende Vertragsende und die automatische Verlängerung hinweisen. Erfolgt kein solcher Hinweis, endet der Vertrag mit Ablauf der ursprünglichen Vertragsdauer. Nach rechtzeitigem Hinweis verlängert sich der Vertrag auch mit Verbrauchern um jeweils 3 Monate, wenn er nicht bis spätestens 4 Wochen vor Ablauf der jeweiligen Vertragslaufzeit von einer der Parteien gekündigt wird.
-
(4) Die Zahlung per Kreditkarte, Lastschrift/Bankeinzug, Rechnung und Nachnahme ist Voraussetzung für den Vertrag (SEPA Lastschrift-Mandat).
-
-
§ 3 Automatische Wiederbestellungen
-
(1) Bei Angabe einer automatischen Wiederbestellung durch den Kunden wird diesem im gewählten Bestellintervall BIO Kaffee, Tee gemäß der auf Seite 2,3,4 und 5 stehenden Tabelle gewählte Menge an die aktuelle Lieferadresse geschickt. Die Zusammenstellung der Kaffee Teevarietäten kann bei schriftlichem Einlangen des Änderungswunsches bis zwei Werktage von dem Kunden gewählten Versanddatum geändert werden.
-
-
§ 4 Verpflichtung zur Verwendung von Produkten der Profit Planet GmbH Vertragsstrafe/Liefervereinbarung
-
(1) Der Kunde verpflichtet sich, während der Laufzeit des Vertrags auf den von Profit Planet GmbH zur Verfügung gestellten Kaffeemaschinen ausschließlich Produkte der Profit Planet GmbH „VITAPRESSO“ zu verwenden und einzusetzen, maximal jedoch für einen Zeitraum von drei (3) und/oder fünf (5) Jahren ab Vertragsschluss.
-
(2) Der Kaffee wird wiederkehrend zugestellt laut Bestellung.
-
(3) Verstößt der Kunde gegen seine Verpflichtung aus Abs. 1, so ist die Profit Planet GmbH zur außerordentlichen fristlosen Kündigung aus wichtigem Grund berechtigt. Darüber hinaus vereinbaren die Parteien die Zahlung einer verschuldensunabhängigen Vertragsstrafe durch den Kunden an die Profit Planet GmbH in angemessener Höhe, wobei die Profit Planet GmbH die Höhe nach billigem Ermessen bestimmen wird und die Angemessenheit der Vertragsstrafe im Streitfall von dem zuständigen Gericht überprüft werden kann. Die Geltendmachung weiteren Schadensersatzes bleibt vorbehalten.
-
-
-
-
-
-
-
-
§ 5 Wartung und Reparatur
-
(1) Wartung und Reparaturen der Kaffeemaschinen sind in der Gestellung wie folgt enthalten:
-
(a) Alle Wartungsarbeiten und Reparaturen werden werktags, von Montag bis Freitag zu den üblichen Arbeitszeiten (09:00 - 17:00 Uhr) telefonisch: 0043 676 3440274 oder schriftlich an office@profit-planet.com durchgeführt. Dienstleistungen an Wochenenden und Feiertagen sind ausgeschlossen und können nur gegen einen Aufpreis vom Kunden selbst bei dem von uns benannten, autorisierten Servicepartner beauftragt werden.
-
(b) Nicht eingeschlossen sind Reparaturen, die auf mangelhafte Pflege oder eine unsachgemäße Bedienung zurückzuführen sind. Insbesondere die Nichtbeachtung der Bedienungs- und Reinigungsanleitung, die unsachgemäße oder mangelnde Entkalkung sowie Bedienungsfehler gehen zu Lasten des Kunden.
-
(c) Profit Planet GmbH behält sich vor, dem Kunden das Ersatzgerät zum UVP zu fakturieren, sofern das Ersatzgerät nicht innerhalb von 4 Wochen nach Erhalt der reparierten Maschine an den Kundendienst zurückgeschickt wurde.
-
(d) Ausdrücklich nicht von dem vertraglichen Wartungs- und Reparaturservice umfasst sind: Stellplatzwechsel, Produktumstellung, Verkostung, Umbauten, die Behebung von Störungen, die als Folge von Reparaturen oder Änderungen durch den Kunden oder durch Dritte auftreten, die Behebung von Störungen, die durch unsachgemäße Bedienung oder mangelhafte Reinigung bzw. Pflege verursacht wurden, die Behebung von Störungen, deren Ursache außerhalb der Kaffeemaschine liegen, wie Defekte in der Wasser- und Stromzufuhr, Elementarschäden, Missbrauch, andere außergewöhnliche Einwirkungen und Fremdkörper.
-
-
§ 6 Außerordentliche Kündigung
-
(1) Beide Vertragsparteien haben das Recht, diesen Vertrag außerordentlich fristlos zu kündigen, wenn die jeweils andere Vertragspartei gegen wesentliche Bestimmungen dieses Vertrages trotz Mahnung und angemessener Frist verstößt.
-
(2) Profit Planet GmbH ist insbesondere berechtigt, den Vertrag außerordentlich fristlos zu kündigen, wenn der Kunde
-
(a) mit einer Zahlung ganz oder teilweise im Verzug ist und Profit Planet GmbH dem Kunden erfolglos eine angemessene Frist zur Zahlung des rückständigen Betrages gesetzt hat, oder
-
(b) eine in diesem Vertrag vereinbarte Mindestabnahmemenge in einem Zeitraum von 6 Monaten um mehr als durchschnittlich 20% unterschritten wurde, oder
-
(c) fremde Produkte auf den gestellten Kaffeemaschinen zubereitet.
-
(3) Im Falle einer außerordentlichen fristlosen Kündigung durch Profit Planet GmbH, behält sich diese vor, dem Kunden eine Deckungsausgleichzahlung für die Restlaufzeit in Höhe von 25% der vereinbarten Mindestabnahmemenge, sowie der vertraglich vereinbarten Mietzinsen in Rechnung zu stellen. Die Geltendmachung weiteren Schadensersatzes bleibt vorbehalten. Dem Kunden bleibt der Nachweis offen, dass kein oder ein wesentlich geringerer Schaden entstanden ist.
-
-
§ 7 Eigentumsverhältnisse
-
Die gelieferten Maschinen bleiben Eigentum von Profit Planet GmbH.
-
-
-
-
-
-
-
-
§ 8 Interne Bestellsysteme und Bestellungen, Datenschutz
-
Weiteres stimme ich dem Erhalt von exklusiven Angeboten und Informationen wie folgt zu:
-
Ich stimme zu, dass die angegebenen Daten von der Profit Planet GmbH verarbeitet und zur Information über exklusive Angebote und sonstige Informationen über E-Mail-Newsletters verwendet werden. Die Zustimmung kann jederzeit per E-Mail an office@profit-planet.com widerrufen werden. Ich akzeptiere hiermit die Datenschutzbestimmungen.
-
Ich stimme zu, dass die angegebenen Daten von der Profit Planet GmbH verarbeitet und zur telefonischen Information über exklusive Angebote und sonstige Informationen verwendet werden. Die Zustimmung kann jederzeit per E-Mail an office@profit-planet.com widerrufen werden. Ich akzeptiere hiermit die Datenschutzbestimmungen.
-
Sie haben uns schon früher Ihre Zustimmung gegeben und erhalten schon Informationen und Angebote zu unseren Produkten, wollen diese Einwilligung aber jetzt widerrufen: Diese Zustimmung kann jederzeit per E-Mail an office@profit-planet.com widerrufen werden.
-
-
Mit dieser Unterschrift wird (werden) das (die) auf Seite 1 genannte(n) Gerät(e) zu genannten Konditionen übernommen.
-
Es gelten die Allgemeinen Geschäftsbedingungen der Profit Planet GmbH als vereinbart. Der Kunde erklärt hiermit ausdrücklich, dass er die Allgemeinen Geschäftsbedingungen und die Datenschutzbestimmungen gelesen hat und diesen zustimmt. Der Vertrag kommt mittels Annahme durch Profit Planet GmbH zustande und ist unter der Voraussetzung einer positiven Bonitätsprüfung gültig.
-
-
-
-
-
-
- Unterschrift Profit Planet
- {{profitplanetSignature}}
-
-
- Stempel / Unterschrift Kunde
- {{signatureImage}}
-
-
-
- fullname: {{fullName}}
-
-
- date: {{currentDate}}
-
-
-
-
-
-
-
-
-
-
-
-
Allgemeine Geschäftsbedingungen Kaffee-Service
-
-
§ 1 Geltungsbereich, Form
-
(1) Die vorliegenden Allgemeinen Geschäftsbedingungen (AGB) gelten für alle unsere Geschäftsbeziehungen zwischen unseren Kunden und uns, der Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Graz, Österreich. Die AGB gelten gegenüber Verbrauchern und Unternehmern; zwingende Verbraucherschutzbestimmungen (insbesondere nach KSchG und FAGG) gehen im Zweifel diesen AGB vor.
-
(2) Die AGB gelten insbesondere für Verträge über den Verkauf und/oder die Lieferung beweglicher Sachen („Ware“), ohne Rücksicht darauf, ob wir die Ware selbst herstellen oder bei Zulieferern einkaufen. Sofern nichts anderes vereinbart, gelten die AGB in der zum Zeitpunkt der Bestellung des Käufers gültigen bzw. jedenfalls in der ihm zuletzt in Textform mitgeteilten Fassung als Rahmenvereinbarung auch für gleichartige künftige Verträge, ohne dass wir in jedem Einzelfall wieder auf sie hinweisen müssten.
-
(3) Unsere AGB gelten ausschließlich. Abweichende, entgegenstehende oder ergänzende Allgemeine Geschäftsbedingungen des Käufers werden nur dann und insoweit Vertragsbestandteil, als wir ihrer Geltung ausdrücklich zugestimmt haben. Dieses Zustimmungserfordernis gilt in jedem Fall, beispielsweise auch dann, wenn wir in Kenntnis der AGB des Käufers die Lieferung an ihn vorbehaltlos ausführen.
-
(4) Im Einzelfall getroffene, individuelle Vereinbarungen mit dem Käufer (einschließlich Nebenabreden, Ergänzungen und Änderungen) haben in jedem Fall Vorrang vor diesen AGB. Für den Inhalt derartiger Vereinbarungen ist, vorbehaltlich des Gegenbeweises, ein schriftlicher Vertrag bzw. unsere schriftliche Bestätigung maßgebend.
-
(5) Rechtserhebliche Erklärungen und Anzeigen des Käufers in Bezug auf den Vertrag (z.B. Fristsetzung, Mängelanzeige, Rücktritt oder Minderung), sind schriftlich, d.h. in Schrift- oder Textform (z.B. Brief, E-Mail, Telefax) abzugeben. Gesetzliche Formvorschriften und weitere Nachweise insbesondere bei Zweifeln über die Legitimation des Erklärenden bleiben unberührt.
-
(6) Hinweise auf die Geltung gesetzlicher Vorschriften haben nur klarstellende Bedeutung. Auch ohne eine derartige Klarstellung gelten daher die gesetzlichen Vorschriften, soweit sie in diesen AGB nicht unmittelbar abgeändert oder ausdrücklich ausgeschlossen werden.
-
-
§ 2 Vertragsschluss
-
(1) Unsere Angebote sind freibleibend und unverbindlich. Dies gilt auch, wenn wir dem Käufer Kataloge, technische Dokumentationen (z.B. Zeichnungen, Pläne, Berechnungen, Kalkulationen, Verweisungen auf DIN-Normen), sonstige Produktbeschreibungen oder Unterlagen – auch in elektronischer Form – überlassen haben, an denen wir uns Eigentums- und Urheberrechte vorbehalten.
-
(2) Die Bestellung der Ware durch den Käufer gilt als verbindliches Vertragsangebot. Sofern sich aus der Bestellung nichts anderes ergibt, sind wir berechtigt, dieses Vertragsangebot innerhalb von 30 Tagen nach seinem Zugang bei uns anzunehmen.
-
(3) Die Annahme kann entweder schriftlich (z.B. durch Auftragsbestätigung) oder durch Auslieferung der Ware an den Käufer erklärt werden.
-
-
-
-
-
-
-
§ 3 Lieferfrist und Lieferverzug
-
(1) Die Lieferfrist wird individuell vereinbart bzw. von uns bei Annahme der Bestellung angegeben. Sofern dies nicht der Fall ist, beträgt die Lieferfrist ca. 14 – 21 Tage ab Vertragsschluss.
-
(2) Sofern wir verbindliche Lieferfristen aus Gründen, die wir nicht zu vertreten haben, nicht einhalten können (Nichtverfügbarkeit der Leistung), werden wir den Kunden hierüber unverzüglich informieren und gleichzeitig die voraussichtliche, neue Lieferfrist mitteilen. Ist die Leistung auch innerhalb der neuen Lieferfrist nicht verfügbar, sind wir berechtigt, ganz oder teilweise vom Vertrag zurückzutreten; eine bereits erbrachte Gegenleistung des Kunden werden wir unverzüglich erstatten. Als Fall der Nichtverfügbarkeit der Leistung in diesem Sinne gilt insbesondere die nicht rechtzeitige Selbstbelieferung durch unseren Zulieferer, wenn wir ein kongruentes Deckungsgeschäft abgeschlossen haben, weder uns noch unseren Zulieferer ein Verschulden trifft oder wir im Einzelfall zur Beschaffung nicht verpflichtet sind.
-
(3) Der Eintritt unseres Lieferverzugs bestimmt sich nach den gesetzlichen Vorschriften. In jedem Fall ist aber eine Mahnung durch den Kunden erforderlich.
-
(4) Die Rechte des Kunden und unsere gesetzlichen Rechte, insbesondere bei einem Ausschluss der Leistungspflicht (z.B. aufgrund Unmöglichkeit oder Unzumutbarkeit der Leistung und/oder Nacherfüllung), bleiben unberührt.
-
-
§ 4 Lieferung, Gefahrübergang, Abnahme, Annahmeverzug
-
(1) Die Lieferung erfolgt ab Lager, wo auch der Erfüllungsort für die Lieferung und eine etwaige Nacherfüllung ist. Auf Verlangen und Kosten des Käufers wird die Ware an einen anderen Bestimmungsort versandt (Versendungskauf). Soweit nicht etwas anderes vereinbart ist, sind wir berechtigt, die Art der Versendung (insbesondere Transportunternehmen, Versandweg, Verpackung) selbst zu bestimmen.
-
(2) Die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung der Ware geht spätestens mit der Übergabe auf den Käufer über. Beim Versendungskauf geht die Gefahr des zufälligen Untergangs und einer zufälligen Verschlechterung der Ware während des Transports für Unternehmer bereits mit Übergabe der Ware an den Spediteur/Frachtführer über; für Verbraucher geht die Gefahr erst über, wenn die Ware dem Verbraucher oder einem von diesem benannten Dritten (der nicht Frachtführer ist) übergeben wurde. Hat der Verbraucher den Beförderungsvertrag selbst ohne unsere Auswahlmöglichkeit beauftragt, so geht die Gefahr bereits mit Übergabe der Ware an den Beförderer über.
-
(3) Kommt der Käufer in Annahmeverzug, unterlässt er eine Mitwirkungshandlung oder verzögert sich unsere Lieferung aus anderen, vom Käufer zu vertretenden Gründen, so sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen (z.B. Lagerkosten) zu verlangen. Der Nachweis eines höheren Schadens und unsere gesetzlichen Ansprüche (insbesondere Ersatz von Mehraufwendungen, angemessene Entschädigung, Kündigung) bleiben unberührt.
-
-
-
-
§ 5 Preise und Zahlungsbedingungen
-
(1) Sofern im Einzelfall nichts anderes vereinbart ist, gelten unsere jeweils zum Zeitpunkt des Vertragsschlusses aktuellen Preise, und zwar ab Lager, zzgl. gesetzlicher Umsatzsteuer.
-
(2) Beim Versendungskauf trägt der Käufer die Transportkosten ab Lager und die Kosten einer ggf. vom Käufer gewünschten Transportversicherung. Sofern wir nicht die im Einzelfall tatsächlich entstandenen Transportkosten in Rechnung stellen, gilt eine Transportkostenpauschale (ausschließlich Transportversicherung) iHv 200 EUR als vereinbart. Etwaige Zölle, Gebühren, Steuern und sonstige öffentliche Abgaben trägt der Käufer.
-
(3) Der Kaufpreis ist fällig und zu zahlen innerhalb von 14 Tagen ab Rechnungsstellung und Lieferung bzw. Abnahme der Ware. Wir sind jedoch, auch im Rahmen einer laufenden Geschäftsbeziehung, jederzeit berechtigt, eine Lieferung ganz oder teilweise nur gegen Vorkasse durchzuführen. Einen entsprechenden Vorbehalt erklären wir spätestens mit der Auftragsbestätigung.
-
(4) Mit Ablauf vorstehender Zahlungsfrist kommt der Käufer in Verzug. Der Kaufpreis ist während des Verzugs zum jeweils geltenden gesetzlichen Verzugszinssatz zu verzinsen. Wir behalten uns die Geltendmachung eines weitergehenden Verzugsschadens vor. Gegenüber Kaufleuten bleibt unser Anspruch auf den kaufmännischen Fälligkeitszins (§ 352 UGB) unberührt.
-
(5) Dem Käufer stehen Aufrechnungs- oder Zurückbehaltungsrechte nur insoweit zu, als sein Anspruch rechtskräftig festgestellt oder unbestritten ist. Bei Mängeln der Lieferung bleiben die Gegenrechte des Käufers insbesondere gem. § 7 dieser AGB unberührt. Gegenüber Verbrauchern gilt diese Einschränkung nicht für Ansprüche, die in rechtlichem Zusammenhang mit ihrer Verbindlichkeit stehen.
-
(6) Wird nach Vertragsabschluss erkennbar, dass unser Anspruch auf Zahlung des Kaufpreises durch mangelnde Zahlungsfähigkeit oder drohende Zahlungsunfähigkeit des Käufers gefährdet ist – etwa durch Antrag auf Eröffnung eines Insolvenzverfahrens oder vergleichbare Umstände –, sind wir berechtigt, unsere Leistung zu verweigern und dem Käufer eine angemessene Frist zur Erbringung der Gegenleistung oder zur Sicherheitsleistung zu setzen. Nach fruchtlosem Ablauf dieser Frist sind wir berechtigt, vom Vertrag zurückzutreten.
-
-
-
§ 6 Eigentumsvorbehalt
-
(1) Bis zur vollständigen Bezahlung aller unserer gegenwärtigen und künftigen Forderungen aus dem Kaufvertrag und einer laufenden Geschäftsbeziehung (gesicherte Forderungen) behalten wir uns das Eigentum an den verkauften Waren vor.
-
(2) Die unter Eigentumsvorbehalt stehenden Waren dürfen vor vollständiger Bezahlung der gesicherten Forderungen weder an Dritte verpfändet, noch zur Sicherheit übereignet werden. Der Käufer hat uns unverzüglich schriftlich zu benachrichtigen, wenn ein Antrag auf Eröffnung eines Insolvenzverfahrens gestellt oder soweit Zugriffe Dritter (zB Pfändungen) auf die uns gehörenden Waren erfolgen.
-
(3) Bei vertragswidrigem Verhalten des Käufers, insbesondere bei Nichtzahlung des fälligen Kaufpreises, sind wir berechtigt, nach den gesetzlichen Vorschriften vom Vertrag zurückzutreten oder/und die Ware auf Grund des Eigentumsvorbehalts heraus zu verlangen. Das Herausgabeverlangen beinhaltet nicht zugleich die Erklärung des Rücktritts; wir sind vielmehr berechtigt, lediglich die Ware heraus zu verlangen und uns den Rücktritt vorzubehalten. Zahlt der Käufer den fälligen Kaufpreis nicht, dürfen wir diese Rechte nur geltend machen, wenn wir dem Käufer zuvor erfolglos eine angemessene Frist zur Zahlung gesetzt haben oder eine derartige Fristsetzung nach den gesetzlichen Vorschriften entbehrlich ist.
-
-
-
-
-
-
-
-
§ 7 Sachmängel
-
(1) In dringenden Fällen, z.B. bei Gefährdung der Betriebssicherheit oder zur Abwehr unverhältnismäßiger Schäden, hat der Käufer das Recht, den Mangel selbst zu beseitigen und von uns Ersatz der hierzu objektiv erforderlichen (angemessenen) Aufwendungen zu verlangen. Von einer derartigen Selbstvornahme sind wir unverzüglich – nach Möglichkeit vorher – zu benachrichtigen. Das Selbstvornahmerecht besteht nicht, wenn wir berechtigt wären, eine entsprechende Nacherfüllung nach den gesetzlichen Vorschriften zu verweigern.
-
(2) Wenn die Nacherfüllung fehlgeschlagen ist oder eine für die Nacherfüllung vom Käufer zu setzende angemessene Frist erfolglos abgelaufen oder nach den gesetzlichen Vorschriften entbehrlich ist, kann der Käufer vom Kaufvertrag zurücktreten oder den Kaufpreis mindern. Bei einem unerheblichen Mangel besteht jedoch kein Rücktrittsrecht. Bei Verbrauchern gelten unbeschadet vorstehender Regelungen die gesetzlichen Gewährleistungsrechte uneingeschränkt. Insbesondere beträgt die Gewährleistungsfrist für Verbraucher zwei Jahre ab Übergabe der Ware.
-
(3) Ansprüche des Käufers auf Schadensersatz bzw. Ersatz vergeblicher Aufwendungen bestehen auch bei Mängeln nur nach Maßgabe von § 8 und sind im Übrigen ausgeschlossen.
-
-
§ 8 Sonstige Haftung
-
(1) Soweit sich aus diesen AGB einschließlich der nachfolgenden Bestimmungen nichts anderes ergibt, haften wir bei der Verletzung vertraglicher und außervertraglicher Pflichten nach den gesetzlichen Vorschriften.
-
(2) Wir haften – gleich aus welchem Rechtsgrund – im Rahmen der Verschuldenshaftung bei Vorsatz und grober Fahrlässigkeit. Bei leichter Fahrlässigkeit haften wir nur a) für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, b) für Schäden aus der Verletzung wesentlicher Vertragspflichten (d. h. solcher Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Vertragspartner regelmäßig vertrauen darf). In diesem Fall ist unsere Haftung jedoch auf den Ersatz des typischen, vorhersehbaren Schadens begrenzt.
-
(3) Die vorstehenden Haftungsbeschränkungen gelten auch zugunsten Dritter sowie für Pflichtverletzungen durch Personen, deren Verschulden uns nach den gesetzlichen Vorschriften zuzurechnen ist. Sie gelten nicht, soweit wir einen Mangel arglistig verschwiegen oder eine Garantie für die Beschaffenheit der Ware übernommen haben sowie bei Ansprüchen nach dem Produkthaftungsgesetz.
-
(4) Soweit gesetzlich zulässig, haften wir nicht für mittelbare Schäden, Folgeschäden oder entgangenen Gewinn. Gegenüber Verbrauchern gilt dieser Haftungsausschluss nicht, soweit ein kausaler Zusammenhang mit der Verletzung wesentlicher Vertragspflichten besteht.
-
(5) Ein Rücktritt oder eine Kündigung wegen Pflichtverletzung, die nicht auf einem Mangel der Ware beruht, ist nur zulässig, wenn wir diese zu vertreten haben. Ein darüber hinausgehendes freies Rücktritts- oder Kündigungsrecht des Käufers wird – soweit rechtlich zulässig – ausgeschlossen.
-
(6) Die zwingenden Bestimmungen des Produkthaftungsgesetzes sowie die Haftung für vorsätzliches oder grob fahrlässiges Verhalten bleiben von den vorstehenden Regelungen unberührt.
-
-
§ 9 Verjährung
-
(1) Bei Verträgen mit Unternehmern im Sinne des § 1 KSchG wird die gesetzliche Gewährleistungsfrist für bewegliche Sachen gemäß § 933 ABGB auf ein Jahr ab Übergabe verkürzt. Dies gilt nicht bei Arglist oder bei Übernahme einer Garantie für die Beschaffenheit der Ware.
-
(2) Die in Abs. 1 genannte Fristverkürzung gilt nicht für Ansprüche des Käufers wegen Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, bei grob fahrlässigem oder vorsätzlichem Verhalten oder bei Ansprüchen nach dem Produkthaftungsgesetz.
-
(3) Schadenersatzansprüche wegen Mängeln (§ 933a ABGB) verjähren unabhängig von einer verkürzten Gewährleistungsfrist innerhalb der gesetzlichen Frist von drei Jahren ab Kenntnis von Schaden und Schädiger.
-
(4) Gegenüber Verbrauchern gelten uneingeschränkt die gesetzlichen Gewährleistungs- und Verjährungsfristen (§§ 922 ff ABGB, § 9 KSchG).
-
-
§ 10 Rechtswahl und Gerichtsstand
-
(1) Für sämtliche Rechtsverhältnisse zwischen uns und dem Käufer gilt ausschließlich das materielle Recht der Republik Österreich unter Ausschluss des UN-Kaufrechts (CISG) und sonstiger internationaler Kollisionsnormen, soweit zwingende Verbraucherschutzvorschriften nicht entgegenstehen.
-
(2) Ist der Käufer Unternehmer im Sinne des § 1 KSchG, so wird für alle Streitigkeiten aus oder im Zusammenhang mit diesem Vertrag einschließlich seiner Gültigkeit und Durchführung das sachlich zuständige Gericht in Graz vereinbart. Wir sind jedoch berechtigt, auch am allgemeinen Gerichtsstand des Käufers oder an einem sonst gesetzlich zulässigen Gerichtsstand Klage zu erheben.
-
(3) Gegenüber Verbrauchern gelten die gesetzlichen Gerichtsstandregelungen. Eine abweichende Gerichtsstandsvereinbarung mit Verbrauchern wird nicht getroffen.
-
-
§ 11 Schlussbestimmungen
-
Sollten einzelne Bestimmungen dieses Vertrages unwirksam oder nichtig sein oder werden, so berührt dies die Gültigkeit der übrigen Bestimmungen dieses Vertrages nicht. Die Parteien verpflichten sich, unwirksame oder nichtige Bestimmungen durch neue Bestimmungen zu ersetzen, die dem in den unwirksamen oder nichtigen Bestimmungen enthaltenen wirtschaftlichen Regelungsgehalt in rechtlich zulässiger Weise gerecht werden. Entsprechendes gilt, wenn sich in dem Vertrag eine Lücke herausstellen sollte. Zur Ausfüllung der Lücke verpflichten sich die Parteien auf die Etablierung angemessener Regelungen in diesem Vertrag hinzuwirken, die dem am nächsten kommen, was die Vertragsschließenden nach dem Sinn und Zweck dieses Vertrages bestimmt hätten, wenn der Punkt von ihnen bedacht worden wäre.
-
-
Stand der Allgemeinen Geschäftsbedingungen Profit Planet Kaffee-Service: 01.08.2025
-
-
-
-
-
-
-
-
Informationen für Verbraucher über das Rücktrittsrecht (Widerrufsrecht)
-
Widerrufsrecht: Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie (oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist) die erste Ware im Rahmen dieses Vertrages in Besitz genommen haben. Bei einem Vertrag über Dienstleistungen (z.B. Miete einer Kaffeemaschine) beginnt die Widerrufsfrist mit dem Tag des Vertragsabschlusses.
-
Ausübung: Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Graz, E-Mail: office@profit-planet.com) mittels einer eindeutigen Erklärung (z.B. ein mit der Post versandter Brief oder E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das unten angefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist. Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
-
Folgen: Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben – einschließlich etwaiger Lieferkosten (mit Ausnahme jener zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene günstigste Standardlieferung gewählt haben) – unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart. Ihnen werden wegen dieser Rückzahlung keine Entgelte berechnet.
-
Handelt es sich bei dem widerrufenen Vertrag um einen Kaufvertrag über Waren, können wir die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder Sie den Nachweis erbracht haben, dass Sie die Waren abgesandt haben – je nachdem, welcher Zeitpunkt früher eintritt. Sie haben die Waren in diesem Fall unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem Tag, an dem Sie uns über den Widerruf dieses Vertrags unterrichten, an uns zurückzusenden oder zu übergeben. Die Frist ist gewahrt, wenn Sie die Waren vor Ablauf der Frist von vierzehn Tagen absenden. Sie tragen die unmittelbaren Kosten der Rücksendung der Waren. Sie müssen für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.
-
Haben Sie verlangt, dass eine Dienstleistung (oder die regelmäßige Lieferung von Waren) während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt der Widerrufsausübung bereits erbrachten Leistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Leistungen entspricht.
-
-
Muster-Widerrufsformular
-
-
(Wenn Sie den Vertrag widerrufen wollen, können Sie dieses Formular ausfüllen und an uns zurücksenden.)
-
– An Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Graz, E-Mail: office@profit-planet.com:
-
– Hiermit widerrufe(n) ich/wir ( )
-
den von mir/uns ( ) abgeschlossenen Vertrag über den Kauf der folgenden Ware(n)/die Erbringung der folgenden Dienstleistung
-
– Bestellt am ( ) / erhalten am ( )
-
– Name des/der Verbraucher(s):
-
– Anschrift des/der Verbraucher(s):
-
– Datum:
-
– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier):
-
-
-
-
-
diff --git a/src/app/admin/contract-management/components/companySettingsPanel.tsx b/src/app/admin/contract-management/components/companySettingsPanel.tsx
index 22943b7..fa6e628 100644
--- a/src/app/admin/contract-management/components/companySettingsPanel.tsx
+++ b/src/app/admin/contract-management/components/companySettingsPanel.tsx
@@ -3,33 +3,6 @@
import { useState, useEffect } from 'react'
import useContractManagement from '../hooks/useContractManagement'
-function fileToDataUrl(file: File): Promise {
- return new Promise((resolve, reject) => {
- const reader = new FileReader()
- reader.onerror = () => reject(new Error('Failed to read file'))
- reader.onload = () => {
- const result = reader.result
- if (typeof result === 'string') resolve(result)
- else reject(new Error('Invalid file result'))
- }
- reader.readAsDataURL(file)
- })
-}
-
-function summarizeForLog(payload: Record) {
- const out: Record = {}
- for (const [k, v] of Object.entries(payload)) {
- if (typeof v === 'string' && (k.toLowerCase().includes('base64') || k.toLowerCase().includes('qr_code'))) {
- out[k] = { kind: 'base64', len: v.length, head: v.slice(0, 32) }
- } else if (typeof v === 'string' && v.length > 200) {
- out[k] = { kind: 'string', len: v.length, head: v.slice(0, 32) }
- } else {
- out[k] = v
- }
- }
- return out
-}
-
export default function CompanySettingsPanel() {
const { getCompanySettings, updateCompanySettings } = useContractManagement()
@@ -39,14 +12,9 @@ export default function CompanySettingsPanel() {
company_postal_city: '',
company_country: '',
})
- const [hasQr60, setHasQr60] = useState(false)
- const [hasQr120, setHasQr120] = useState(false)
- const [qr60DataUrl, setQr60DataUrl] = useState('')
- const [qr120DataUrl, setQr120DataUrl] = useState('')
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
- const [saveError, setSaveError] = useState('')
useEffect(() => {
getCompanySettings()
@@ -57,11 +25,6 @@ export default function CompanySettingsPanel() {
company_postal_city: data.company_postal_city || '',
company_country: data.company_country || '',
})
-
- const qr60 = (data as any)?.qr_code_60_base64 ?? (data as any)?.qrCode60Base64
- const qr120 = (data as any)?.qr_code_120_base64 ?? (data as any)?.qrCode120Base64
- setHasQr60(!!qr60)
- setHasQr120(!!qr120)
})
.catch(() => {})
.finally(() => setLoading(false))
@@ -77,81 +40,17 @@ export default function CompanySettingsPanel() {
e.preventDefault()
setSaving(true)
setSaved(false)
- setSaveError('')
try {
- // IMPORTANT: send `payload` (full strings), not the redacted log view.
- const payload: any = { ...form }
- if (qr60DataUrl) payload.qr_code_60_base64 = qr60DataUrl
- if (qr120DataUrl) payload.qr_code_120_base64 = qr120DataUrl
-
- // For logging only (redacted); never send this object.
- const logPayload: any = summarizeForLog(payload)
-
- try {
- const qr60 = payload.qr_code_60_base64
- const qr120 = payload.qr_code_120_base64
- console.info('[CompanySettingsPanel] updateCompanySettings payload', {
- logPayload,
- keys: Object.keys(payload),
- jsonLength: JSON.stringify(payload).length,
- qrFieldTypes: {
- qr_code_60_base64: qr60 ? typeof qr60 : null,
- qr_code_120_base64: qr120 ? typeof qr120 : null,
- },
- qrFieldLengths: {
- qr_code_60_base64: typeof qr60 === 'string' ? qr60.length : null,
- qr_code_120_base64: typeof qr120 === 'string' ? qr120.length : null,
- },
- })
-
- if (qr60 && typeof qr60 !== 'string') console.warn('[CompanySettingsPanel] qr_code_60_base64 is not a string!', qr60)
- if (qr120 && typeof qr120 !== 'string') console.warn('[CompanySettingsPanel] qr_code_120_base64 is not a string!', qr120)
- } catch {}
-
- await updateCompanySettings(payload)
+ await updateCompanySettings(form)
setSaved(true)
setTimeout(() => setSaved(false), 3000)
} catch {
- setSaveError('Could not save settings.')
+ // silent
} finally {
setSaving(false)
}
}
- const handleQrUpload = async (which: '60' | '120', file: File | null) => {
- setSaved(false)
- setSaveError('')
- if (!file) return
-
- // Backend accepts 10MB JSON, but base64 expands the payload.
- // Keep a conservative limit to avoid 413 Payload Too Large.
- const MAX_FILE_BYTES = 7_000_000
- if (file.size > MAX_FILE_BYTES) {
- setSaveError('QR image is too large. Please upload a smaller PNG.')
- return
- }
- if (file.type && file.type !== 'image/png') {
- setSaveError('Please upload a PNG file for the QR code.')
- return
- }
-
- try {
- const dataUrl = await fileToDataUrl(file)
- // Normalize to raw base64, to match other endpoints (e.g. company stamp upload)
- const m = dataUrl.match(/^data:(.+?);base64,(.*)$/)
- const base64 = m ? m[2] : dataUrl
- if (which === '60') {
- setQr60DataUrl(base64)
- setHasQr60(true)
- } else {
- setQr120DataUrl(base64)
- setHasQr120(true)
- }
- } catch {
- setSaveError('Could not read QR image file.')
- }
- }
-
if (loading) {
return (
@@ -222,38 +121,6 @@ export default function CompanySettingsPanel() {
-
-
-
Invoice QR Code (60 pcs)
-
handleQrUpload('60', e.target.files?.[0] || null)}
- className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-900 hover:file:bg-blue-100"
- />
-
- {qr60DataUrl ? 'Selected (will be saved on Save)' : hasQr60 ? 'Already uploaded' : 'Not uploaded'}
-
-
-
-
-
Invoice QR Code (120 pcs)
-
handleQrUpload('120', e.target.files?.[0] || null)}
- className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-900 hover:file:bg-blue-100"
- />
-
- {qr120DataUrl ? 'Selected (will be saved on Save)' : hasQr120 ? 'Already uploaded' : 'Not uploaded'}
-
-
-
-
- {saveError && (
- {saveError}
- )}
-
('en');
const [type, setType] = useState<'contract' | 'invoice' | 'other'>('contract');
- const [contractType, setContractType] = useState<'contract' | 'gdpr' | 'abo'>('contract');
+ const [contractType, setContractType] = useState<'contract' | 'gdpr'>('contract');
const [userType, setUserType] = useState<'personal' | 'company' | 'both'>('personal');
const [description, setDescription] = useState('');
@@ -55,7 +55,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
setDescription(((tpl as any)?.description as string) || ''); // FIX: DocumentTemplate may not declare `description`
setLang((tpl.lang as any) || 'en');
setType(((tpl.type as any) || 'contract') as 'contract' | 'invoice' | 'other');
- setContractType(((tpl.contract_type as any) || 'contract') as 'contract' | 'gdpr' | 'abo');
+ setContractType(((tpl.contract_type as any) || 'contract') as 'contract' | 'gdpr');
setUserType(((tpl.user_type as any) || 'both') as 'personal' | 'company' | 'both');
setEditingMeta({
id: editingTemplateId,
@@ -163,20 +163,6 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
return;
}
- try {
- console.info('[ContractEditor] doSave()', {
- editingTemplateId: editingTemplateId ?? null,
- publish,
- name: name.trim(),
- type,
- contract_type: type === 'contract' ? contractType : null,
- lang,
- user_type: type === 'invoice' ? 'both' : userType,
- descriptionLength: description ? description.length : 0,
- htmlLength: html.length,
- });
- } catch {}
-
setSaving(true);
setStatusMsg(null);
@@ -230,7 +216,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
const save = async (publish: boolean) => {
if (publish) {
let kind = type === 'contract'
- ? (contractType === 'gdpr' ? 'GDPR' : contractType === 'abo' ? 'ABO' : 'Contract')
+ ? (contractType === 'gdpr' ? 'GDPR' : 'Contract')
: type === 'invoice'
? 'Invoice'
: 'Other';
@@ -316,13 +302,12 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
{type === 'contract' && (
setContractType(e.target.value as 'contract' | 'gdpr' | 'abo')}
+ onChange={(e) => setContractType(e.target.value as 'contract' | 'gdpr')}
required
className="w-full sm:w-40 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50 shadow"
>
Contract
GDPR
- ABO
)}
{type !== 'invoice' && (
diff --git a/src/app/admin/contract-management/components/contractTemplateList.tsx b/src/app/admin/contract-management/components/contractTemplateList.tsx
index 6af3a65..7302877 100644
--- a/src/app/admin/contract-management/components/contractTemplateList.tsx
+++ b/src/app/admin/contract-management/components/contractTemplateList.tsx
@@ -100,7 +100,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
const tpl = items.find((i) => i.id === id);
if (tpl) {
const kind = tpl.type === 'contract'
- ? (tpl.contract_type === 'gdpr' ? 'GDPR' : tpl.contract_type === 'abo' ? 'ABO' : 'Contract')
+ ? (tpl.contract_type === 'gdpr' ? 'GDPR' : 'Contract')
: tpl.type === 'invoice'
? 'Invoice'
: 'Other';
@@ -172,7 +172,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
)}
{c.type === 'contract' && (
- {c.contract_type === 'gdpr' ? 'GDPR' : c.contract_type === 'abo' ? 'ABO' : 'Contract'}
+ {c.contract_type === 'gdpr' ? 'GDPR' : 'Contract'}
)}
{c.user_type && c.type !== 'invoice' && (
diff --git a/src/app/admin/contract-management/hooks/useContractManagement.ts b/src/app/admin/contract-management/hooks/useContractManagement.ts
index ff3a10e..c1f239a 100644
--- a/src/app/admin/contract-management/hooks/useContractManagement.ts
+++ b/src/app/admin/contract-management/hooks/useContractManagement.ts
@@ -5,7 +5,7 @@ export type DocumentTemplate = {
id: string;
name: string;
type?: string;
- contract_type?: 'contract' | 'gdpr' | 'abo' | null | string;
+ contract_type?: 'contract' | 'gdpr' | null | string;
lang?: 'en' | 'de' | string;
user_type?: 'personal' | 'company' | 'both' | string;
state?: 'active' | 'inactive' | string;
@@ -28,62 +28,10 @@ export type CompanyStamp = {
type Json = Record;
-function redactLongOrBase64ish(value: any) {
- if (typeof value !== 'string') return value;
- const len = value.length;
- const head = value.slice(0, 32);
- // If it's long, treat it as potentially sensitive / base64 and only log metadata.
- if (len > 200) {
- return { kind: 'string', len, head };
- }
- return value;
-}
-
-function redactJsonForLogs(input: any): any {
- if (!input || typeof input !== 'object') return redactLongOrBase64ish(input);
- if (Array.isArray(input)) return input.map(redactJsonForLogs);
- const out: Record = {};
- for (const [k, v] of Object.entries(input)) {
- if (typeof v === 'string' && (k.toLowerCase().includes('base64') || k.toLowerCase().includes('qr_code'))) {
- out[k] = { kind: 'base64', len: v.length, head: v.slice(0, 32) };
- } else {
- out[k] = redactJsonForLogs(v);
- }
- }
- return out;
-}
-
function isFormData(body: any): body is FormData {
return typeof FormData !== 'undefined' && body instanceof FormData;
}
-function safeDescribeBody(body: any) {
- if (!body) return null;
- if (isFormData(body)) {
- const entries: Record = {};
- try {
- for (const [k, v] of body.entries()) {
- if (typeof File !== 'undefined' && v instanceof File) {
- entries[k] = { kind: 'File', name: v.name, type: v.type, size: v.size };
- } else {
- // Strings only for our current usage.
- entries[k] = v;
- }
- }
- } catch (e: any) {
- return { kind: 'FormData', error: e?.message || String(e) };
- }
- return { kind: 'FormData', entries };
- }
-
- if (typeof body === 'string') {
- return { kind: 'string', preview: body.slice(0, 500), length: body.length };
- }
-
- // Avoid dumping arbitrary objects (could be huge / sensitive)
- return { kind: typeof body };
-}
-
export default function useContractManagement() {
const base = process.env.NEXT_PUBLIC_API_BASE_URL || '';
const getState = useAuthStore.getState;
@@ -109,32 +57,16 @@ export default function useContractManagement() {
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
}
- const url = `${base}${path}`;
- const method = init.method || 'GET';
-
// Debug (safe)
try {
console.debug('[CM] fetch ->', {
- url,
- method,
+ url: `${base}${path}`,
+ method: init.method || 'GET',
hasAuth: !!token,
+ tokenPrefix: token ? `${token.substring(0, 12)}...` : null,
});
} catch {}
- // EXTRA debug for document-template calls: show what we send (safe metadata only)
- if (path.startsWith('/api/document-templates')) {
- try {
- const safeHeaders = { ...headers } as Record;
- if (safeHeaders.Authorization) safeHeaders.Authorization = '[redacted]';
- console.info('[CM][document-templates] request', {
- url,
- method,
- headers: safeHeaders,
- body: safeDescribeBody(init.body),
- });
- } catch {}
- }
-
// Include cookies + Authorization on all requests
const res = await fetch(`${base}${path}`, {
credentials: 'include',
@@ -181,7 +113,7 @@ export default function useContractManagement() {
return {} as T;
}
},
- [base, getState]
+ [base]
);
// Document templates
@@ -222,7 +154,7 @@ export default function useContractManagement() {
file: File | Blob;
name: string;
type: string;
- contract_type?: 'contract' | 'gdpr' | 'abo';
+ contract_type?: 'contract' | 'gdpr';
lang: 'en' | 'de' | string;
description?: string;
user_type?: 'personal' | 'company' | 'both';
@@ -239,19 +171,6 @@ export default function useContractManagement() {
if (payload.description) fd.append('description', payload.description);
fd.append('user_type', (payload.user_type ?? 'both'));
- try {
- console.info('[CM][document-templates] uploadTemplate()', {
- name: payload.name,
- type: payload.type,
- contract_type: payload.contract_type,
- willSendContractType: payload.type === 'contract' && Boolean(payload.contract_type),
- lang: payload.lang,
- user_type: payload.user_type ?? 'both',
- descriptionLength: payload.description ? payload.description.length : 0,
- file: typeof File !== 'undefined' && file instanceof File ? { name: file.name, type: file.type, size: file.size } : null,
- });
- } catch {}
-
return authorizedFetch('/api/document-templates', { method: 'POST', body: fd });
}, [authorizedFetch]);
@@ -259,7 +178,7 @@ export default function useContractManagement() {
file?: File | Blob;
name?: string;
type?: string;
- contract_type?: 'contract' | 'gdpr' | 'abo';
+ contract_type?: 'contract' | 'gdpr';
lang?: 'en' | 'de' | string;
description?: string;
user_type?: 'personal' | 'company' | 'both';
@@ -286,7 +205,7 @@ export default function useContractManagement() {
file: File | Blob;
name?: string;
type?: string;
- contract_type?: 'contract' | 'gdpr' | 'abo';
+ contract_type?: 'contract' | 'gdpr';
lang?: 'en' | 'de' | string;
description?: string;
user_type?: 'personal' | 'company' | 'both';
@@ -305,20 +224,6 @@ export default function useContractManagement() {
if (payload.user_type !== undefined) fd.append('user_type', payload.user_type);
if (payload.state !== undefined) fd.append('state', payload.state);
- try {
- console.info('[CM][document-templates] reviseTemplate()', {
- id,
- name: payload.name,
- type: payload.type,
- contract_type: payload.contract_type,
- lang: payload.lang,
- user_type: payload.user_type,
- state: payload.state,
- descriptionLength: payload.description ? payload.description.length : 0,
- file: typeof File !== 'undefined' && file instanceof File ? { name: file.name, type: file.type, size: file.size } : null,
- });
- } catch {}
-
return authorizedFetch(`/api/document-templates/${id}/revise`, { method: 'POST', body: fd });
}, [authorizedFetch]);
@@ -540,57 +445,18 @@ export default function useContractManagement() {
}, [authorizedFetch]);
// Company settings (invoice address info)
- type CompanySettings = {
- company_name?: string
- company_street?: string
- company_postal_city?: string
- company_country?: string
- // NEW: QR codes for invoices (base64 or data URL)
- qr_code_60_base64?: string | null
- qr_code_120_base64?: string | null
- // NEW: allow camelCase too (backend supports both)
- qrCode60Base64?: string | null
- qrCode120Base64?: string | null
- }
-
const getCompanySettings = useCallback(async () => {
- return authorizedFetch('/api/admin/company-settings', { method: 'GET' });
+ return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>(
+ '/api/admin/company-settings', { method: 'GET' }
+ );
}, [authorizedFetch]);
- const updateCompanySettings = useCallback(async (data: Partial) => {
- // Debug request body in browser console (redacts base64 values)
- try {
- // IMPORTANT: `data` is the real payload object; `redacted` is for logs only.
- const json = JSON.stringify(data);
- const redacted = redactJsonForLogs(data);
- const qr60 = (data as any)?.qr_code_60_base64 ?? (data as any)?.qrCode60Base64;
- const qr120 = (data as any)?.qr_code_120_base64 ?? (data as any)?.qrCode120Base64;
- console.info('[CM][company-settings] PUT body', {
- redacted,
- jsonLength: json.length,
- keys: Object.keys(data || {}),
- qrFieldTypes: {
- qr_code_60_base64: qr60 ? typeof qr60 : null,
- qr_code_120_base64: qr120 ? typeof qr120 : null,
- },
- qrFieldLengths: {
- qr_code_60_base64: typeof qr60 === 'string' ? qr60.length : null,
- qr_code_120_base64: typeof qr120 === 'string' ? qr120.length : null,
- },
- });
-
- if (qr60 && typeof qr60 !== 'string') {
- console.warn('[CM][company-settings] qr_code_60_base64 is not a string!', qr60);
- }
- if (qr120 && typeof qr120 !== 'string') {
- console.warn('[CM][company-settings] qr_code_120_base64 is not a string!', qr120);
- }
- } catch {}
-
- return authorizedFetch('/api/admin/company-settings', {
- method: 'PUT',
- body: JSON.stringify(data),
- });
+ const updateCompanySettings = useCallback(async (data: {
+ company_name: string; company_street: string; company_postal_city: string; company_country: string;
+ }) => {
+ return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>(
+ '/api/admin/company-settings', { method: 'PUT', body: JSON.stringify(data) }
+ );
}, [authorizedFetch]);
return {
diff --git a/src/app/admin/dashboard-management/hooks/useAdminDashboardPlatforms.ts b/src/app/admin/dashboard-management/hooks/useAdminDashboardPlatforms.ts
deleted file mode 100644
index 73f5eda..0000000
--- a/src/app/admin/dashboard-management/hooks/useAdminDashboardPlatforms.ts
+++ /dev/null
@@ -1,272 +0,0 @@
-'use client'
-
-import { useCallback, useEffect, useMemo, useState } from 'react'
-import { authFetch } from '../../../utils/authFetch'
-import {
- DEFAULT_DASHBOARD_PLATFORMS,
- type DashboardPlatform,
- type DashboardPlatformColorClass,
- type DashboardPlatformIconName
-} from '../../../utils/dashboardPlatforms'
-
-type BackendPlatform = {
- id: string | number
- title: string
- href: string
- description?: string | null
- icon?: DashboardPlatformIconName | null
- color?: DashboardPlatformColorClass | null
- state?: boolean
- disabled?: boolean
- disabledText?: string | null
- sortOrder?: number | null
-}
-
-export type PlatformRow = DashboardPlatform & {
- _isNew?: boolean
-}
-
-const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '')
-const FIXED_ICON: DashboardPlatformIconName = 'LinkIcon'
-
-function createId(): string {
- if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
- return (crypto as any).randomUUID()
- }
- return `platform_${Date.now()}_${Math.random().toString(16).slice(2)}`
-}
-
-function isValidHref(href: string): boolean {
- const v = href.trim()
- if (!v) return false
- return v.startsWith('/') || v.startsWith('http://') || v.startsWith('https://')
-}
-
-function toRow(p: BackendPlatform): PlatformRow {
- return {
- id: String(p.id),
- title: typeof p.title === 'string' ? p.title : '',
- description: typeof p.description === 'string' ? p.description : '',
- href: typeof p.href === 'string' ? p.href : '',
- icon: FIXED_ICON,
- color: (p.color as DashboardPlatformColorClass) || ('bg-blue-500' as DashboardPlatformColorClass),
- isActive: typeof p.state === 'boolean' ? p.state : true,
- disabled: typeof p.disabled === 'boolean' ? p.disabled : false,
- disabledText: typeof p.disabledText === 'string' ? p.disabledText : undefined
- }
-}
-
-function toPayload(p: PlatformRow) {
- return {
- title: p.title,
- href: p.href,
- description: p.description ?? '',
- icon: FIXED_ICON,
- color: p.color,
- state: Boolean(p.isActive),
- disabled: Boolean(p.disabled),
- disabledText: p.disabledText ?? ''
- }
-}
-
-function normalizeForCompare(p: PlatformRow) {
- return {
- title: (p.title || '').trim(),
- href: (p.href || '').trim(),
- description: (p.description || '').trim(),
- icon: FIXED_ICON,
- color: p.color,
- isActive: Boolean(p.isActive),
- disabled: Boolean(p.disabled),
- disabledText: (p.disabledText || '').trim()
- }
-}
-
-function isChanged(p: PlatformRow, baselineById: Record): boolean {
- if (p._isNew) return true
- const baseline = baselineById[p.id]
- if (!baseline) return true
- const a = normalizeForCompare(p)
- const b = normalizeForCompare(baseline)
- return (
- a.title !== b.title ||
- a.href !== b.href ||
- a.description !== b.description ||
- a.color !== b.color ||
- a.isActive !== b.isActive ||
- a.disabled !== b.disabled ||
- a.disabledText !== b.disabledText
- )
-}
-
-function forceLinkIcon(rows: DashboardPlatform[]): PlatformRow[] {
- return rows.map(r => ({ ...r, icon: FIXED_ICON })) as PlatformRow[]
-}
-
-export function useAdminDashboardPlatforms() {
- const [platforms, setPlatforms] = useState(forceLinkIcon(DEFAULT_DASHBOARD_PLATFORMS))
- const [baselineById, setBaselineById] = useState>({})
- const [savedAt, setSavedAt] = useState(null)
- const [loading, setLoading] = useState(false)
- const [saving, setSaving] = useState(false)
- const [error, setError] = useState(null)
-
- const hasValidationErrors = useMemo(() => {
- return platforms.some(p => !p.title.trim() || !p.href.trim() || !isValidHref(p.href))
- }, [platforms])
-
- const reload = useCallback(async () => {
- setLoading(true)
- setError(null)
- try {
- const res = await authFetch(`${API_BASE}/api/admin/dashboard-platforms`, {
- method: 'GET',
- headers: { Accept: 'application/json' },
- credentials: 'include'
- })
- if (!res.ok) {
- const text = await res.text().catch(() => '')
- throw new Error(text || `HTTP ${res.status}`)
- }
-
- const json = (await res.json().catch(() => null)) as unknown
- const list = Array.isArray(json) ? (json as BackendPlatform[]) : []
- const rows = list.map(toRow)
- setPlatforms(rows)
- setBaselineById(Object.fromEntries(rows.map(r => [r.id, r])))
- } catch (e: any) {
- setError(e?.message || 'Failed to load platforms')
- } finally {
- setLoading(false)
- }
- }, [])
-
- useEffect(() => {
- void reload()
- }, [reload])
-
- const addPlatform = useCallback((): string => {
- const id = createId()
- setPlatforms(prev => [
- ...prev,
- {
- id,
- title: 'New Platform',
- description: '',
- href: '/dashboard',
- icon: FIXED_ICON,
- color: 'bg-blue-500' as DashboardPlatformColorClass,
- isActive: true,
- disabled: false,
- _isNew: true
- }
- ])
- return id
- }, [])
-
- const updatePlatform = useCallback((id: string, patch: Partial) => {
- setPlatforms(prev => prev.map(p => (p.id === id ? { ...p, ...patch, icon: FIXED_ICON } : p)))
- }, [])
-
- const removeNewPlatform = useCallback((id: string) => {
- setPlatforms(prev => prev.filter(p => p.id !== id))
- }, [])
-
- const setPlatformState = useCallback(async (platform: PlatformRow, state: boolean) => {
- if (platform._isNew) {
- updatePlatform(platform.id, { isActive: state })
- return
- }
-
- const prev = platform.isActive
- updatePlatform(platform.id, { isActive: state })
-
- try {
- const res = await authFetch(`${API_BASE}/api/admin/dashboard-platforms/${platform.id}/state`, {
- method: 'PATCH',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- credentials: 'include',
- body: JSON.stringify({ state })
- })
- if (!res.ok) {
- const text = await res.text().catch(() => '')
- throw new Error(text || `HTTP ${res.status}`)
- }
-
- setBaselineById(prevMap => {
- const prevBaseline = prevMap[platform.id]
- if (!prevBaseline) return prevMap
- return { ...prevMap, [platform.id]: { ...prevBaseline, isActive: state, icon: FIXED_ICON } }
- })
- } catch (e: any) {
- setError(e?.message || 'Failed to update platform state')
- updatePlatform(platform.id, { isActive: prev })
- }
- }, [updatePlatform])
-
- const save = useCallback(async () => {
- setSaving(true)
- setError(null)
- try {
- // 1) Create new platforms
- for (const platform of platforms.filter(p => p._isNew)) {
- const res = await authFetch(`${API_BASE}/api/admin/dashboard-platforms`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- credentials: 'include',
- body: JSON.stringify(toPayload(platform))
- })
- if (!res.ok) {
- const text = await res.text().catch(() => '')
- throw new Error(text || `HTTP ${res.status}`)
- }
- }
-
- // 2) Update changed existing platforms
- for (const platform of platforms.filter(p => !p._isNew && isChanged(p, baselineById))) {
- const res = await authFetch(`${API_BASE}/api/admin/dashboard-platforms/${platform.id}`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- credentials: 'include',
- body: JSON.stringify(toPayload(platform))
- })
- if (!res.ok) {
- const text = await res.text().catch(() => '')
- throw new Error(text || `HTTP ${res.status}`)
- }
- }
-
- // 3) Re-fetch list for canonical state/sort
- await reload()
- setSavedAt(Date.now())
- } catch (e: any) {
- setError(e?.message || 'Save failed')
- } finally {
- setSaving(false)
- }
- }, [baselineById, platforms, reload])
-
- return {
- platforms,
- loading,
- saving,
- error,
- savedAt,
- hasValidationErrors,
- addPlatform,
- updatePlatform,
- removeNewPlatform,
- setPlatformState,
- save,
- isValidHref,
- }
-}
diff --git a/src/app/admin/dashboard-management/page.tsx b/src/app/admin/dashboard-management/page.tsx
deleted file mode 100644
index bccb62b..0000000
--- a/src/app/admin/dashboard-management/page.tsx
+++ /dev/null
@@ -1,259 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import PageLayout from '../../components/PageLayout'
-import {
- DASHBOARD_PLATFORMS_COLOR_OPTIONS,
- type DashboardPlatform,
- type DashboardPlatformColorClass
-} from '../../utils/dashboardPlatforms'
-import { PlusIcon, TrashIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
-import { useAdminDashboardPlatforms, type PlatformRow } from './hooks/useAdminDashboardPlatforms'
-
-export default function AdminDashboardManagementPage() {
- const {
- platforms,
- loading,
- saving,
- error,
- savedAt,
- hasValidationErrors,
- addPlatform,
- updatePlatform,
- removeNewPlatform,
- setPlatformState,
- save,
- isValidHref,
- } = useAdminDashboardPlatforms()
-
- const [openById, setOpenById] = useState>({})
-
- const toggleOpen = (id: string) => {
- setOpenById(prev => ({ ...prev, [id]: !prev[id] }))
- }
-
- const addAndOpen = () => {
- const id = addPlatform()
- setOpenById(prev => ({ ...prev, [id]: true }))
- }
-
- const isOpen = (p: PlatformRow) => Boolean(openById[p.id] ?? p._isNew)
-
- return (
-
-
-
-
-
-
- {error && (
-
- {error}
-
- )}
-
- {savedAt && (
-
- Saved at {new Date(savedAt).toLocaleTimeString('de-DE')}
-
- )}
-
- {hasValidationErrors && (
-
- Please ensure every platform has a title and a valid link (must start with “/” or “http(s)://”).
-
- )}
-
-
- {loading && (
-
- Loading…
-
- )}
-
- {!loading && platforms.map(platform => (
-
-
-
-
-
{platform.title}
-
{platform.href}
-
-
- toggleOpen(platform.id)}
- disabled={saving}
- className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white text-gray-800 px-3 py-2 text-xs font-semibold hover:bg-gray-50"
- >
- {isOpen(platform) ? : }
- {isOpen(platform) ? 'Close' : 'Edit'}
-
-
- {
- if (platform._isNew) {
- removeNewPlatform(platform.id)
- return
- }
- await setPlatformState(platform, false)
- }}
- disabled={saving}
- className="inline-flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 text-red-700 px-3 py-2 text-xs font-semibold hover:bg-red-100"
- >
-
- {platform._isNew ? 'Remove' : 'Deactivate'}
-
-
-
-
- {isOpen(platform) && (
-
- )}
-
-
- ))}
-
- {!loading && platforms.length === 0 && (
-
- No platforms configured.
-
- )}
-
-
-
-
-
- )
-}
diff --git a/src/app/admin/finance-management/page.tsx b/src/app/admin/finance-management/page.tsx
index 9b559ba..9d6f73d 100644
--- a/src/app/admin/finance-management/page.tsx
+++ b/src/app/admin/finance-management/page.tsx
@@ -18,10 +18,6 @@ export default function FinanceManagementPage() {
const [diagData, setDiagData] = useState(null)
const [selectedInvoice, setSelectedInvoice] = useState(null)
const [detailModalOpen, setDetailModalOpen] = useState(false)
- const [emailDialogOpen, setEmailDialogOpen] = useState(false)
- const [reportEmail, setReportEmail] = useState('')
- const [sendingReport, setSendingReport] = useState(false)
- const [reportMsg, setReportMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
// NEW: fetch invoices from backend
const {
@@ -120,66 +116,6 @@ export default function FinanceManagementPage() {
URL.revokeObjectURL(url)
}
- const [pdfLoading, setPdfLoading] = useState(null)
-
- const viewInvoicePdf = async (inv: AdminInvoice) => {
- setPdfLoading(inv.id)
- try {
- const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''
- const res = await fetch(`${base}/api/invoices/${inv.id}/pdf`, {
- method: 'GET',
- credentials: 'include',
- headers: {
- ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
- },
- })
- if (!res.ok) {
- const body = await res.json().catch(() => ({}))
- throw new Error(body?.message || `Failed to load PDF (${res.status})`)
- }
- const blob = await res.blob()
- const blobUrl = URL.createObjectURL(blob)
- window.open(blobUrl, '_blank', 'noopener,noreferrer')
- } catch (e: any) {
- setReportMsg({ type: 'error', text: e?.message || 'Failed to load invoice PDF.' })
- } finally {
- setPdfLoading(null)
- }
- }
-
- const sendEmailReport = async () => {
- if (!reportEmail.trim()) return
- setReportMsg(null)
- setSendingReport(true)
- try {
- const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''
- const res = await fetch(`${base}/api/admin/invoices/email-report`, {
- method: 'POST',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
- },
- body: JSON.stringify({
- email: reportEmail.trim(),
- from: billFilter.from || undefined,
- to: billFilter.to || undefined,
- }),
- })
- const body = await res.json().catch(() => ({}))
- if (!res.ok || body?.success === false) {
- throw new Error(body?.message || `Request failed (${res.status})`)
- }
- setReportMsg({ type: 'success', text: `Report sent to ${reportEmail.trim()} (${body.data?.sentCount ?? 0} paid invoice(s)).` })
- setEmailDialogOpen(false)
- setReportEmail('')
- } catch (e: any) {
- setReportMsg({ type: 'error', text: e?.message || 'Failed to send email report.' })
- } finally {
- setSendingReport(false)
- }
- }
-
return (
@@ -246,7 +182,6 @@ export default function FinanceManagementPage() {
Invoices
- { setReportMsg(null); setEmailDialogOpen(true) }} className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-blue-900 font-medium hover:bg-blue-100">Send Email Report
exportBills('csv')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export CSV
exportBills('pdf')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export PDF
Reload
@@ -287,11 +222,6 @@ export default function FinanceManagementPage() {
- {reportMsg && (
-
- {reportMsg.text}
-
- )}
{invError && (
{invError}
@@ -348,7 +278,6 @@ export default function FinanceManagementPage() {
Invoice
Customer
Issued
-
Due Date
Amount
Status
Actions
@@ -357,12 +286,12 @@ export default function FinanceManagementPage() {
{invLoading ? (
<>
-
-
+
+
>
) : filteredBills.length === 0 ? (
-
+
Keine Rechnungen gefunden.
@@ -372,24 +301,6 @@ export default function FinanceManagementPage() {
{inv.invoice_number ?? inv.id}
{inv.buyer_name ?? '—'}
{inv.issued_at ? new Date(inv.issued_at).toLocaleDateString() : '—'}
-
- {(() => {
- if (!inv.due_at) return —
- const due = new Date(inv.due_at)
- const now = new Date()
- const diffDays = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
- let cls = 'bg-green-100 text-green-700' // plenty of time
- if (inv.status === 'paid') cls = 'bg-green-100 text-green-700'
- else if (diffDays < 0) cls = 'bg-red-100 text-red-700'
- else if (diffDays <= 3) cls = 'bg-red-100 text-red-700'
- else if (diffDays <= 7) cls = 'bg-amber-100 text-amber-700'
- return (
-
- {due.toLocaleDateString()}
-
- )
- })()}
-
€{Number(inv.total_gross ?? 0).toFixed(2)}{' '}
{inv.currency ?? 'EUR'}
@@ -412,18 +323,11 @@ export default function FinanceManagementPage() {
- viewInvoicePdf(inv)}
- disabled={pdfLoading === inv.id || !inv.pdf_storage_key}
- className="text-xs rounded border px-2 py-1 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {pdfLoading === inv.id ? 'Loading…' : 'View PDF'}
-
{ setSelectedInvoice(inv); setDetailModalOpen(true) }}
className="text-xs rounded border px-2 py-1 hover:bg-gray-50"
>
- Details
+ View
@@ -443,47 +347,6 @@ export default function FinanceManagementPage() {
onExport={(inv) => exportInvoice(inv)}
/>
)}
-
- {/* Email Report Dialog */}
- {emailDialogOpen && (
-
-
-
Send Email Report
-
- Only paid invoices will be included in the report, regardless of the status filter.
- {(billFilter.from || billFilter.to) && (
- The current date range filter ({billFilter.from || '…'} – {billFilter.to || '…'}) will be applied.
- )}
-
-
Recipient Email
-
setReportEmail(e.target.value)}
- placeholder="email@example.com"
- className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
- autoFocus
- onKeyDown={e => { if (e.key === 'Enter' && !sendingReport) sendEmailReport() }}
- />
-
- { setEmailDialogOpen(false); setReportEmail('') }}
- disabled={sendingReport}
- className="rounded-lg border border-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-60"
- >
- Cancel
-
-
- {sendingReport ? 'Sending…' : 'Send Report'}
-
-
-
-
- )}
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 639f8c2..b085e30 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -279,24 +279,6 @@ export default function AdminDashboardPage() {
- {/* Dashboard Management */}
- router.push('/admin/dashboard-management')}
- className="group w-full flex items-center justify-between rounded-lg border border-blue-200 bg-blue-50 hover:bg-blue-100 px-4 py-4 transform transition-transform duration-200 hover:scale-[1.02] hover:shadow-md"
- >
-
-
-
-
-
-
Dashboard Management
-
Configure dashboard platforms
-
-
-
-
-
{/* User Management (unchanged) */}
(
- path: string,
- init: RequestInit = {},
- responseType: 'json' | 'text' | 'blob' = 'json'
- ): Promise => {
- let token = getState().accessToken;
- if (!token) {
- const ok = await getState().refreshAuthToken();
- if (ok) token = getState().accessToken;
- }
-
- const headers: Record = {
- ...((init.headers as Record) || {}),
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
- };
- if (!isFormData(init.body) && init.method && init.method !== 'GET') {
- headers['Content-Type'] = headers['Content-Type'] || 'application/json';
- }
-
- const res = await fetch(`${base}${path}`, {
- credentials: 'include',
- ...init,
- headers,
- });
-
- if (!res.ok) {
- const text = await res.text().catch(() => '');
- throw new Error(text || `HTTP ${res.status}`);
- }
-
- if (responseType === 'blob') return (await res.blob()) as unknown as T;
- if (responseType === 'text') return (await res.text()) as unknown as T;
- const text = await res.text();
- try {
- return JSON.parse(text) as T;
- } catch {
- return {} as T;
- }
- },
- [base, getState]
- );
-
- const listShippingFees = useCallback(async (): Promise => {
- const res = await fetch(`${base}/api/shipping-fees`, {
- method: 'GET',
- credentials: 'include',
- });
-
- if (!res.ok) {
- const text = await res.text().catch(() => '');
- throw new Error(text || `HTTP ${res.status}`);
- }
-
- const raw = (await res.json().catch(() => [])) as any;
- if (!Array.isArray(raw)) return [];
-
- const normalizePieceCount = (v: any): CoffeeShippingFeePieceCount | null => {
- const n = typeof v === 'number' ? v : (typeof v === 'string' && /^\d+$/.test(v) ? Number(v) : NaN);
- if (n === 60 || n === 120) return n;
- return null;
- };
-
- return raw
- .map((r: any) => {
- const pieceCount = normalizePieceCount(r?.pieceCount);
- if (!pieceCount) return null;
- const price = r?.price != null && r?.price !== '' ? Number(r.price) : 0;
- return { pieceCount, price } satisfies CoffeeShippingFee;
- })
- .filter(Boolean) as CoffeeShippingFee[];
- }, [base]);
-
- const updateShippingFee = useCallback(
- async (pieceCount: CoffeeShippingFeePieceCount, price: number): Promise => {
- if (!(pieceCount === 60 || pieceCount === 120)) {
- throw new Error('pieceCount must be 60 or 120');
- }
- if (!Number.isFinite(price) || price < 0) {
- throw new Error('price must be a number >= 0');
- }
-
- const row = await authorizedFetch(`/api/admin/shipping-fees/${pieceCount}`, {
- method: 'PUT',
- body: JSON.stringify({ price }),
- });
-
- const normalized: CoffeeShippingFee = {
- pieceCount,
- price: row?.price != null && row?.price !== '' ? Number(row.price) : price,
- };
-
- return normalized;
- },
- [authorizedFetch]
- );
-
- return {
- listShippingFees,
- updateShippingFee,
- };
-}
diff --git a/src/app/admin/subscriptions/page.tsx b/src/app/admin/subscriptions/page.tsx
index 8d8c127..971ab74 100644
--- a/src/app/admin/subscriptions/page.tsx
+++ b/src/app/admin/subscriptions/page.tsx
@@ -4,48 +4,9 @@ import { PhotoIcon } from '@heroicons/react/24/solid';
import Link from 'next/link';
import PageLayout from '../../components/PageLayout';
import useCoffeeManagement, { CoffeeItem } from './hooks/useCoffeeManagement';
-import useCoffeeShippingFees, {
- CoffeeShippingFee,
- CoffeeShippingFeePieceCount,
-} from './hooks/useCoffeeShippingFees';
export default function AdminSubscriptionsPage() {
const { listProducts, setProductState, deleteProduct } = useCoffeeManagement();
- const { listShippingFees, updateShippingFee } = useCoffeeShippingFees();
-
- const formatPriceDraft = (price: number) => {
- if (!Number.isFinite(price)) return '';
- return price.toFixed(2).replace('.', ',');
- };
-
- const parsePriceDraft = (raw: string) => {
- const normalized = (raw ?? '')
- .trim()
- .replace(/\s+/g, '')
- .replace(/,/g, '.');
- if (!normalized) return NaN;
- return Number(normalized);
- };
-
- const [shippingFees, setShippingFees] = useState([]);
- const [shippingFeesLoading, setShippingFeesLoading] = useState(false);
- const [shippingFeesError, setShippingFeesError] = useState(null);
- const [shippingFeeDraft, setShippingFeeDraft] = useState>({
- 60: '',
- 120: '',
- });
- const [shippingFeeFieldError, setShippingFeeFieldError] = useState>({
- 60: null,
- 120: null,
- });
- const [shippingFeeSaving, setShippingFeeSaving] = useState>({
- 60: false,
- 120: false,
- });
- const [shippingFeeSavedAt, setShippingFeeSavedAt] = useState>({
- 60: null,
- 120: null,
- });
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
@@ -66,65 +27,9 @@ export default function AdminSubscriptionsPage() {
useEffect(() => {
load();
- loadShippingFees();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- async function loadShippingFees() {
- setShippingFeesLoading(true);
- setShippingFeesError(null);
- try {
- const list = await listShippingFees();
- setShippingFees(Array.isArray(list) ? list : []);
-
- const findPrice = (pieceCount: CoffeeShippingFeePieceCount) => {
- const row = (Array.isArray(list) ? list : []).find((r) => r.pieceCount === pieceCount);
- return row ? row.price : 0;
- };
- setShippingFeeDraft({
- 60: formatPriceDraft(findPrice(60)),
- 120: formatPriceDraft(findPrice(120)),
- });
- setShippingFeeFieldError({ 60: null, 120: null });
- } catch (e: any) {
- setShippingFeesError(e?.message ?? 'Failed to load shipping fees');
- } finally {
- setShippingFeesLoading(false);
- }
- }
-
- const saveShippingFee = async (pieceCount: CoffeeShippingFeePieceCount) => {
- if (shippingFeeSaving[pieceCount]) return;
-
- const raw = (shippingFeeDraft[pieceCount] ?? '').trim();
- const price = parsePriceDraft(raw);
- if (!Number.isFinite(price) || price < 0) {
- setShippingFeeFieldError((prev) => ({
- ...prev,
- [pieceCount]: 'Enter a valid price (≥ 0).',
- }));
- return;
- }
-
- setShippingFeeFieldError((prev) => ({ ...prev, [pieceCount]: null }));
- setShippingFeeSaving((prev) => ({ ...prev, [pieceCount]: true }));
- try {
- const updated = await updateShippingFee(pieceCount, price);
- setShippingFees((prev) => {
- const next = prev.filter((r) => r.pieceCount !== pieceCount);
- next.push(updated);
- next.sort((a, b) => a.pieceCount - b.pieceCount);
- return next;
- });
- setShippingFeeDraft((prev) => ({ ...prev, [pieceCount]: formatPriceDraft(updated.price) }));
- setShippingFeeSavedAt((prev) => ({ ...prev, [pieceCount]: Date.now() }));
- } catch (e: any) {
- setShippingFeesError(e?.message ?? 'Failed to update shipping fee');
- } finally {
- setShippingFeeSaving((prev) => ({ ...prev, [pieceCount]: false }));
- }
- };
-
const availabilityBadge = (avail: boolean) => (
{avail ? 'Available' : 'Unavailable'}
@@ -158,95 +63,6 @@ export default function AdminSubscriptionsPage() {
{error}
)}
- {/* Shipping Fees */}
-
-
-
-
Shipping Fees (ABO)
-
Edit the shipping prices for 60 and 120 pieces.
-
-
- {shippingFeesLoading ? 'Refreshing…' : 'Refresh'}
-
-
-
- {shippingFeesError && (
- {shippingFeesError}
- )}
-
-
- {([60, 120] as CoffeeShippingFeePieceCount[]).map((pieceCount) => {
- const saving = shippingFeeSaving[pieceCount];
- const savedAt = shippingFeeSavedAt[pieceCount];
- const fieldError = shippingFeeFieldError[pieceCount];
- const current = shippingFees.find((r) => r.pieceCount === pieceCount);
- const draft = shippingFeeDraft[pieceCount] ?? '';
-
- return (
-
-
-
-
-
{pieceCount} pieces
- {typeof current?.price === 'number' && Number.isFinite(current.price) ? (
-
Current: €{formatPriceDraft(current.price)}
- ) : null}
- {savedAt ? (
-
- Saved
-
- ) : null}
-
- {fieldError ? (
-
{fieldError}
- ) : (
-
Enter a price in EUR (≥ 0).
- )}
-
-
-
-
- €
- {
- const v = e.target.value;
- setShippingFeeDraft((prev) => ({ ...prev, [pieceCount]: v }));
- setShippingFeeFieldError((prev) => ({ ...prev, [pieceCount]: null }));
- }}
- placeholder="0.00"
- />
-
-
saveShippingFee(pieceCount)}
- >
- {saving ? 'Saving…' : 'Save'}
-
-
-
-
- );
- })}
-
-
-
{loading && (
Loading…
@@ -315,7 +131,7 @@ export default function AdminSubscriptionsPage() {
Delete coffee?
-
You are about to delete the coffee “{deleteTarget.title}”. This action cannot be undone.
+
You are about to delete the coffee "{deleteTarget.title}". This action cannot be undone.
({
total: allUsers.length,
admins: allUsers.filter(u => u.role === 'admin').length,
- guests: allUsers.filter(u => u.role === 'guest').length,
personal: allUsers.filter(u => u.user_type === 'personal').length,
company: allUsers.filter(u => u.user_type === 'company').length,
active: allUsers.filter(u => u.status === 'active').length,
@@ -233,7 +232,7 @@ export default function AdminUserManagementPage() {
t==='personal' ? badge('Personal','blue') : badge('Company','purple')
const roleBadge = (r: UserRole) =>
- r==='admin' ? badge('Admin','indigo') : r==='guest' ? badge('Guest','amber') : badge('User','gray')
+ r==='admin' ? badge('Admin','indigo') : badge('User','gray')
// Action handler for opening edit modal
const onEdit = (id: string) => {
@@ -257,7 +256,7 @@ export default function AdminUserManagementPage() {
{/* Statistic Section + Verify Button */}
-
+
Total Users
{stats.total}
@@ -266,10 +265,6 @@ export default function AdminUserManagementPage() {
Admins
{stats.admins}
-
-
Guests
-
{stats.guests}
-
Personal
{stats.personal}
diff --git a/src/app/coffee-abonnements/hooks/useShippingFees.ts b/src/app/coffee-abonnements/hooks/useShippingFees.ts
deleted file mode 100644
index 18a59eb..0000000
--- a/src/app/coffee-abonnements/hooks/useShippingFees.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-'use client';
-
-import { useEffect, useMemo, useState } from 'react';
-
-export type CoffeeShippingFeePieceCount = 60 | 120;
-
-export type CoffeeShippingFee = {
- pieceCount: CoffeeShippingFeePieceCount;
- price: number;
-};
-
-type ShippingFeeMap = Record
;
-
-function normalizePieceCount(v: any): CoffeeShippingFeePieceCount | null {
- const n = typeof v === 'number' ? v : (typeof v === 'string' && /^\d+$/.test(v) ? Number(v) : NaN);
- if (n === 60 || n === 120) return n;
- return null;
-}
-
-export function useShippingFees() {
- const base = process.env.NEXT_PUBLIC_API_BASE_URL || '';
-
- const [fees, setFees] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const feeByPieceCount: ShippingFeeMap = useMemo(() => {
- const map: ShippingFeeMap = { 60: 0, 120: 0 };
- for (const row of fees) {
- map[row.pieceCount] = row.price;
- }
- return map;
- }, [fees]);
-
- useEffect(() => {
- let active = true;
- (async () => {
- setLoading(true);
- setError(null);
- try {
- const res = await fetch(`${base}/api/shipping-fees`, {
- method: 'GET',
- credentials: 'include',
- });
- if (!res.ok) {
- const text = await res.text().catch(() => '');
- throw new Error(text || `HTTP ${res.status}`);
- }
-
- const raw = (await res.json().catch(() => [])) as any;
- if (!active) return;
-
- if (!Array.isArray(raw)) {
- setFees([]);
- return;
- }
-
- const next = raw
- .map((r: any) => {
- const pieceCount = normalizePieceCount(r?.pieceCount);
- if (!pieceCount) return null;
- const price = r?.price != null && r?.price !== '' ? Number(r.price) : 0;
- return {
- pieceCount,
- price: Number.isFinite(price) && price >= 0 ? price : 0,
- } satisfies CoffeeShippingFee;
- })
- .filter(Boolean) as CoffeeShippingFee[];
-
- next.sort((a, b) => a.pieceCount - b.pieceCount);
- setFees(next);
- } catch (e: any) {
- if (!active) return;
- setError(e?.message ?? 'Failed to load shipping fees');
- setFees([]);
- } finally {
- if (active) setLoading(false);
- }
- })();
-
- return () => {
- active = false;
- };
- }, [base]);
-
- return { fees, feeByPieceCount, loading, error };
-}
diff --git a/src/app/coffee-abonnements/page.tsx b/src/app/coffee-abonnements/page.tsx
index 885730d..76bbefa 100644
--- a/src/app/coffee-abonnements/page.tsx
+++ b/src/app/coffee-abonnements/page.tsx
@@ -3,7 +3,6 @@ import React, { useState, useMemo } from 'react';
import PageLayout from '../components/PageLayout';
import { useRouter } from 'next/navigation';
import { useActiveCoffees } from './hooks/getActiveCoffees';
-import { useShippingFees } from './hooks/useShippingFees';
export default function CoffeeAbonnementPage() {
const [selections, setSelections] = useState>({});
@@ -14,13 +13,6 @@ export default function CoffeeAbonnementPage() {
// Fetch active coffees from the backend
const { coffees, loading, error } = useActiveCoffees();
- // Shipping fees (per piece count)
- const { feeByPieceCount, loading: shippingLoading, error: shippingError } = useShippingFees();
- const shippingFeeFor60 = feeByPieceCount[60] ?? 0;
- const shippingFeeFor120 = feeByPieceCount[120] ?? 0;
- const selectedShippingFee = feeByPieceCount[selectedPlanCapsules] ?? 0;
- const isFreeShippingSelected = Number(selectedShippingFee) === 0;
-
const selectedEntries = useMemo(
() =>
Object.entries(selections).map(([id, qty]) => {
@@ -40,11 +32,6 @@ export default function CoffeeAbonnementPage() {
[selectedEntries]
);
- const totalNetWithShipping = useMemo(
- () => totalPrice + (Number.isFinite(selectedShippingFee) ? selectedShippingFee : 0),
- [totalPrice, selectedShippingFee]
- );
-
// NEW: enforce selected plan size (60 or 120 capsules)
const totalCapsules = useMemo(
() => selectedEntries.reduce((sum, entry) => sum + entry.quantity, 0),
@@ -125,22 +112,7 @@ export default function CoffeeAbonnementPage() {
: 'border-gray-300 hover:bg-gray-50'
}`}
>
-
-
60 piece abo
- {shippingLoading ? (
-
- Shipping…
-
- ) : shippingFeeFor60 === 0 ? (
-
- FREE SHIPPING
-
- ) : (
-
- Shipping €{shippingFeeFor60.toFixed(2)}
-
- )}
-
+ 60 piece abo
6 packs of 10 capsules
-
-
120 piece abo
- {shippingLoading ? (
-
- Shipping…
-
- ) : shippingFeeFor120 === 0 ? (
-
- FREE SHIPPING
-
- ) : (
-
- Shipping €{shippingFeeFor120.toFixed(2)}
-
- )}
-
+ 120 piece abo
12 packs of 10 capsules
-
- {shippingError && (
-
- Shipping fees could not be loaded: {shippingError}
-
- )}
2. Choose coffees & quantities
@@ -347,25 +298,10 @@ export default function CoffeeAbonnementPage() {
))}
-
- {/* Shipping */}
-
- Shipping
-
- {shippingLoading ? (
- 'Loading…'
- ) : isFreeShippingSelected ? (
- 'FREE SHIPPING'
- ) : (
- `€${selectedShippingFee.toFixed(2)}`
- )}
-
-
-
Total (net)
- €{totalNetWithShipping.toFixed(2)}
+ €{totalPrice.toFixed(2)}
diff --git a/src/app/coffee-abonnements/summary/components/SignaturePad.tsx b/src/app/coffee-abonnements/summary/components/SignaturePad.tsx
deleted file mode 100644
index 7d743f8..0000000
--- a/src/app/coffee-abonnements/summary/components/SignaturePad.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-'use client'
-
-import React, { useEffect, useRef } from 'react'
-
-type Props = {
- value: string
- onChange: (dataUrl: string) => void
- className?: string
- required?: boolean
- error?: string | null
-}
-
-export default function SignaturePad({ value, onChange, className, required = false, error = null }: Props) {
- const canvasRef = useRef(null)
- const isDrawing = useRef(false)
-
- const getPos = (e: React.MouseEvent | React.TouchEvent) => {
- const canvas = canvasRef.current
- if (!canvas) return { x: 0, y: 0 }
- const rect = canvas.getBoundingClientRect()
-
- const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
- const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY
-
- return { x: clientX - rect.left, y: clientY - rect.top }
- }
-
- const setupCanvas = () => {
- const canvas = canvasRef.current
- if (!canvas) return
- const rect = canvas.getBoundingClientRect()
- const dpr = window.devicePixelRatio || 1
- canvas.width = rect.width * dpr
- canvas.height = rect.height * dpr
-
- const ctx = canvas.getContext('2d')
- if (!ctx) return
-
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
- ctx.lineWidth = 2
- ctx.lineCap = 'round'
- ctx.strokeStyle = '#1C2B4A'
-
- // If we already have a signature value, redraw it after resize.
- if (value) {
- const img = new Image()
- img.onload = () => {
- try {
- ctx.clearRect(0, 0, rect.width, rect.height)
- ctx.drawImage(img, 0, 0, rect.width, rect.height)
- } catch {}
- }
- img.src = value
- }
- }
-
- useEffect(() => {
- setupCanvas()
-
- const onResize = () => setupCanvas()
- window.addEventListener('resize', onResize)
- return () => window.removeEventListener('resize', onResize)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- // If parent sets a new value (e.g., cleared externally), reflect it.
- useEffect(() => {
- const canvas = canvasRef.current
- if (!canvas) return
- const ctx = canvas.getContext('2d')
- if (!ctx) return
-
- const rect = canvas.getBoundingClientRect()
- ctx.clearRect(0, 0, rect.width, rect.height)
-
- if (!value) return
-
- const img = new Image()
- img.onload = () => {
- try {
- ctx.drawImage(img, 0, 0, rect.width, rect.height)
- } catch {}
- }
- img.src = value
- }, [value])
-
- const startDrawing = (e: React.MouseEvent | React.TouchEvent) => {
- e.preventDefault()
- const canvas = canvasRef.current
- if (!canvas) return
- const ctx = canvas.getContext('2d')
- if (!ctx) return
-
- const { x, y } = getPos(e)
- ctx.beginPath()
- ctx.moveTo(x, y)
- isDrawing.current = true
- }
-
- const draw = (e: React.MouseEvent | React.TouchEvent) => {
- e.preventDefault()
- if (!isDrawing.current) return
-
- const canvas = canvasRef.current
- if (!canvas) return
- const ctx = canvas.getContext('2d')
- if (!ctx) return
-
- const { x, y } = getPos(e)
- ctx.lineTo(x, y)
- ctx.stroke()
- }
-
- const endDrawing = () => {
- if (!isDrawing.current) return
- isDrawing.current = false
-
- const canvas = canvasRef.current
- if (!canvas) return
-
- try {
- onChange(canvas.toDataURL('image/png'))
- } catch {
- onChange('')
- }
- }
-
- const clear = () => {
- const canvas = canvasRef.current
- if (!canvas) return
- const ctx = canvas.getContext('2d')
- if (!ctx) return
-
- const rect = canvas.getBoundingClientRect()
- ctx.clearRect(0, 0, rect.width, rect.height)
- onChange('')
- }
-
- return (
-
-
-
- Signature{required ? ' *' : ''}
-
-
- Clear
-
-
-
-
-
-
- {error || (value ? 'Signature captured.' : 'Draw your signature in the box.')}
-
-
- )
-}
diff --git a/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts
index 4f6537c..203f44e 100644
--- a/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts
+++ b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts
@@ -11,7 +11,8 @@ export type SubscribeAboInput = {
target_user_id?: number
recipient_name?: string
recipient_email?: string
- // Customer fields
+ recipient_notes?: string
+ // NEW: customer fields
firstName?: string
lastName?: string
email?: string
@@ -20,22 +21,8 @@ export type SubscribeAboInput = {
city?: string
country?: string
frequency?: string
- // 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
+ startDate?: string
+ // NEW: logged-in user id
referred_by?: number | string
}
@@ -61,7 +48,7 @@ export async function subscribeAbo(input: SubscribeAboInput) {
}
// NEW: validate customer fields (required in UI)
- const requiredFields = ['firstName','lastName','email','street','postalCode','city','country'] as const
+ const requiredFields = ['firstName','lastName','email','street','postalCode','city','country','frequency'] as const
const missing = requiredFields.filter(k => {
const v = (input as any)[k]
return typeof v !== 'string' || v.trim() === ''
@@ -70,20 +57,12 @@ export async function subscribeAbo(input: SubscribeAboInput) {
throw new Error(`Missing required fields: ${missing.join(', ')}`)
}
- if (typeof input.signingCity !== 'string' || input.signingCity.trim() === '') {
- throw new Error('signingCity is required')
- }
-
- if (typeof input.signatureDataUrl !== 'string' || input.signatureDataUrl.trim() === '') {
- throw new Error('signatureDataUrl is required')
- }
-
const body: any = {
billing_interval: input.billing_interval ?? 'month',
interval_count: input.interval_count ?? 1,
is_auto_renew: input.is_auto_renew ?? true,
is_for_self: isForSelf,
- // Customer fields
+ // NEW: include customer fields
firstName: input.firstName,
lastName: input.lastName,
email: input.email,
@@ -92,25 +71,7 @@ export async function subscribeAbo(input: SubscribeAboInput) {
city: input.city,
country: input.country?.toUpperCase?.() ?? input.country,
frequency: input.frequency,
- // 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
+ startDate: input.startDate || undefined,
}
if (hasItems) {
body.items = input.items!.map(i => ({
@@ -131,6 +92,7 @@ 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/useAboActiveContractHtml.ts b/src/app/coffee-abonnements/summary/hooks/useAboActiveContractHtml.ts
deleted file mode 100644
index cb9cf38..0000000
--- a/src/app/coffee-abonnements/summary/hooks/useAboActiveContractHtml.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-'use client'
-
-import { useEffect, useState } from 'react'
-import { authFetch } from '../../../utils/authFetch'
-
-const apiBase = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '')
-
-export function useAboActiveContractHtml() {
- const [html, setHtml] = useState(null)
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let active = true
-
- ;(async () => {
- setLoading(true)
- setError(null)
- try {
- const url = `${apiBase}/api/contracts/abo/active`
- const res = await authFetch(url, {
- method: 'GET',
- headers: { Accept: 'application/json' },
- credentials: 'include',
- })
-
- const ct = res.headers.get('content-type') || ''
- const isJson = ct.includes('application/json')
-
- try {
- console.info('[useAboActiveContractHtml] response meta', {
- url,
- status: res.status,
- ok: res.ok,
- contentType: ct,
- isJson,
- })
- } catch {}
-
- if (isJson) {
- const payload: any = await res.json().catch(() => null)
-
- try {
- console.info('[useAboActiveContractHtml] response json keys', payload && typeof payload === 'object' ? Object.keys(payload) : payload)
- } catch {}
-
- if (!res.ok) {
- const msg = payload?.message || payload?.error || `Failed to load contract: ${res.status}`
- throw new Error(msg)
- }
-
- const foundRaw = payload?.found ?? payload?.data?.found
- const found = typeof foundRaw === 'boolean' ? foundRaw : undefined
-
- const htmlRaw = payload?.html ?? payload?.data?.html
- const htmlValue = typeof htmlRaw === 'string' ? htmlRaw : ''
-
- const isFound = (typeof found === 'boolean') ? found : Boolean(htmlValue)
-
- try {
- console.info('[useAboActiveContractHtml] parsed json', {
- success: payload?.success,
- found: isFound,
- htmlLength: htmlValue ? htmlValue.length : 0,
- htmlPreview: htmlValue ? `${htmlValue.slice(0, 200)}${htmlValue.length > 200 ? '…' : ''}` : '',
- })
- } catch {}
-
- if (active) setHtml(isFound && htmlValue ? htmlValue : null)
- return
- }
-
- // Fallback: older endpoints returned raw HTML.
- const text = await res.text().catch(() => '')
-
- try {
- console.info('[useAboActiveContractHtml] response text preview', {
- status: res.status,
- ok: res.ok,
- textLength: text.length,
- textPreview: text ? `${text.slice(0, 200)}${text.length > 200 ? '…' : ''}` : '',
- })
- } catch {}
-
- if (!res.ok) {
- throw new Error(text || `Failed to load contract: ${res.status}`)
- }
- if (active) setHtml(text || null)
- } catch (e: any) {
- if (active) {
- setHtml(null)
- setError(e?.message || 'Failed to load contract preview.')
- }
- } finally {
- if (active) setLoading(false)
- }
- })()
-
- return () => {
- active = false
- }
- }, [])
-
- return { html, loading, error }
-}
diff --git a/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts b/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts
deleted file mode 100644
index 3c2d842..0000000
--- a/src/app/coffee-abonnements/summary/hooks/useAboContractTemplateHtml.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-'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)
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let active = true
-
- ;(async () => {
- setLoading(true)
- setError(null)
- try {
- const res = await authFetch(`${apiBase}/api/contracts/abo/active`, {
- method: 'GET',
- headers: { Accept: 'text/html' },
- credentials: 'include',
- })
-
- const text = await res.text().catch(() => '')
-
- if (!res.ok) {
- throw new Error(text || `Failed to load contract template: ${res.status}`)
- }
-
- if (active) setHtml(text || null)
- } catch (e: any) {
- if (active) {
- setHtml(null)
- setError(e?.message || 'Failed to load contract template preview.')
- }
- } finally {
- if (active) setLoading(false)
- }
- })()
-
- return () => {
- active = false
- }
- }, [])
-
- return { html, loading, error }
-}
diff --git a/src/app/coffee-abonnements/summary/page.tsx b/src/app/coffee-abonnements/summary/page.tsx
index f87ad1c..b3b549d 100644
--- a/src/app/coffee-abonnements/summary/page.tsx
+++ b/src/app/coffee-abonnements/summary/page.tsx
@@ -1,62 +1,19 @@
'use client';
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import PageLayout from '../../components/PageLayout';
import { useRouter } from 'next/navigation';
import { useActiveCoffees } from '../hooks/getActiveCoffees';
import { getStandardVatRate, getVatRates } from './hooks/getTaxRate';
import { subscribeAbo } from './hooks/subscribeAbo';
import useAuthStore from '../../store/authStore'
-import { useShippingFees } from '../hooks/useShippingFees';
-import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml'
-import SignaturePad from './components/SignaturePad'
-import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog'
-import { createReferralLink } from '../../referral-management/hooks/generateReferralLink'
-
-const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A']; // dark blue palette
-
-function extractTemplateVariables(templateHtml: string | null | undefined): string[] {
- if (!templateHtml) return []
- const vars = new Set()
- const re = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g
- let match: RegExpExecArray | null
- while ((match = re.exec(templateHtml)) !== null) {
- if (match[1]) vars.add(match[1])
- }
- return Array.from(vars).sort((a, b) => a.localeCompare(b))
-}
-
-function escapeHtml(value: string): string {
- return value
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''')
-}
-
-function hashString(value: string): number {
- // djb2
- let hash = 5381
- for (let i = 0; i < value.length; i++) {
- hash = ((hash << 5) + hash) ^ value.charCodeAt(i)
- }
- return hash >>> 0
-}
export default function SummaryPage() {
const router = useRouter();
const { coffees, loading, error } = useActiveCoffees();
const user = useAuthStore(state => state.user)
- const { feeByPieceCount, loading: shippingLoading, error: shippingError } = useShippingFees();
- const { html: contractHtml, loading: contractLoading, error: contractError } = useAboContractTemplateHtml()
- const [isContractPreviewOpen, setIsContractPreviewOpen] = useState(false)
- const [contractPdfUrl, setContractPdfUrl] = useState('')
- const [contractPdfKey, setContractPdfKey] = useState('')
- const [contractPdfLoading, setContractPdfLoading] = useState(false)
- const [contractPdfError, setContractPdfError] = useState(null)
const [selections, setSelections] = useState>({});
const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<60 | 120>(120);
- const [signatureDataUrl, setSignatureDataUrl] = useState('')
+ const [isForSelf, setIsForSelf] = useState(true);
const [form, setForm] = useState({
firstName: '',
lastName: '',
@@ -65,284 +22,19 @@ export default function SummaryPage() {
postalCode: '',
city: '',
country: 'DE',
- phone: '',
- paymentMethod: 'sepa' as 'sepa' | 'card' | 'sofort',
- invoiceByEmail: true,
- invoiceSameAsShipping: true,
- invoiceFullName: '',
- invoiceStreet: '',
- invoicePostalCode: '',
- invoiceCity: '',
- invoicePhone: '',
- invoiceEmail: '',
- signingCity: '',
+ frequency: 'monatlich',
+ startDate: '',
+ recipientEmail: '',
+ recipientName: '',
+ recipientNotes: '',
});
const [showThanks, setShowThanks] = useState(false);
- const [guestMailtoHref, setGuestMailtoHref] = useState('')
- const [guestInviteLink, setGuestInviteLink] = useState('')
const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]);
const [taxRate, setTaxRate] = useState(0.07); // minimal fallback only
const [vatRates, setVatRates] = useState<{ code: string; rate: number | null }[]>([]);
const [submitError, setSubmitError] = useState(null);
const [submitLoading, setSubmitLoading] = useState(false);
- const initialCountryRef = useRef(form.country)
-
- const templateVariableNames = useMemo(() => extractTemplateVariables(contractHtml), [contractHtml])
- const templateVariableNamesKey = useMemo(() => templateVariableNames.join('|'), [templateVariableNames])
- const [contractVariables, setContractVariables] = useState>({})
-
- // Auto-compute contract variables from form state for preview
- useEffect(() => {
- if (!templateVariableNamesKey) return
- 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
- // Replace placeholders with escaped user-entered values so placeholders never show.
- return contractHtml.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_whole, varName: string) => {
- if (varName === 'signatureImage' && !contractVariables[varName] && signatureDataUrl) {
- const safeUrl = String(signatureDataUrl)
- if (safeUrl.startsWith('data:image/')) {
- const src = escapeHtml(safeUrl)
- return ` `
- }
- }
-
- const value = contractVariables[varName] ?? ''
- return escapeHtml(String(value))
- })
- }, [contractHtml, contractVariables, signatureDataUrl])
-
- const contractPdfCacheKey = useMemo(() => {
- if (!populatedContractHtml) return ''
- return String(hashString(populatedContractHtml))
- }, [populatedContractHtml])
-
- useEffect(() => {
- // Cleanup blob URL when it changes or on unmount.
- return () => {
- if (contractPdfUrl) URL.revokeObjectURL(contractPdfUrl)
- }
- }, [contractPdfUrl])
-
- const closeContractPreview = () => {
- setIsContractPreviewOpen(false)
- setContractPdfError(null)
- setContractPdfLoading(false)
- setContractPdfKey('')
- if (contractPdfUrl) {
- URL.revokeObjectURL(contractPdfUrl)
- setContractPdfUrl('')
- }
- }
-
- const openContractPreview = async () => {
- if (!populatedContractHtml) return
-
- setIsContractPreviewOpen(true)
- setContractPdfError(null)
-
- // Reuse only if the populated HTML hasn't changed.
- if (contractPdfUrl && contractPdfKey === contractPdfCacheKey) return
-
- if (contractPdfUrl) {
- URL.revokeObjectURL(contractPdfUrl)
- setContractPdfUrl('')
- }
-
- setContractPdfLoading(true)
- try {
- const [jsPdfMod, html2canvasMod] = await Promise.all([import('jspdf'), import('html2canvas')])
- const jsPDF: any = (jsPdfMod as any).jsPDF || (jsPdfMod as any).default
- const html2canvas: any = (html2canvasMod as any).default || html2canvasMod
-
- const parser = new DOMParser()
- const doc = parser.parseFromString(populatedContractHtml, 'text/html')
- const styles = Array.from(doc.querySelectorAll('style'))
- .map(s => s.textContent || '')
- .join('\n')
- const bodyHtml = doc.body?.innerHTML || ''
-
- const wrapper = document.createElement('div')
- wrapper.style.position = 'fixed'
- wrapper.style.left = '-10000px'
- wrapper.style.top = '0'
- wrapper.style.width = '794px' // approx A4 width at 96dpi
- wrapper.style.background = '#ffffff'
- wrapper.innerHTML = `${bodyHtml}`
- document.body.appendChild(wrapper)
-
- try {
- const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' })
-
- const pageWidth = pdf.internal.pageSize.getWidth()
- const pageHeight = pdf.internal.pageSize.getHeight()
-
- const marginX = 24
- const marginTop = 24
- const marginBottom = 24
-
- const usableWidth = pageWidth - marginX * 2
- const usableHeight = pageHeight - marginTop - marginBottom
-
- const renderCanvasToPdf = (canvas: HTMLCanvasElement, pageIndex: number) => {
- const imgWidthPt = usableWidth
- const pxPerPt = canvas.width / imgWidthPt
- const sliceHeightPx = Math.max(1, Math.floor(usableHeight * pxPerPt))
-
- let yPx = 0
- let sliceIndex = 0
- while (yPx < canvas.height) {
- const remainingPx = canvas.height - yPx
- const currentSliceHeightPx = Math.min(sliceHeightPx, remainingPx)
-
- const sliceCanvas = document.createElement('canvas')
- sliceCanvas.width = canvas.width
- sliceCanvas.height = currentSliceHeightPx
- const ctx = sliceCanvas.getContext('2d')
- if (!ctx) break
-
- ctx.drawImage(
- canvas,
- 0,
- yPx,
- canvas.width,
- currentSliceHeightPx,
- 0,
- 0,
- canvas.width,
- currentSliceHeightPx
- )
-
- const imgData = sliceCanvas.toDataURL('image/png')
- const imgHeightPt = currentSliceHeightPx / pxPerPt
-
- const isFirstOverall = pageIndex === 0 && sliceIndex === 0
- if (!isFirstOverall) pdf.addPage()
- pdf.addImage(imgData, 'PNG', marginX, marginTop, imgWidthPt, imgHeightPt)
-
- yPx += currentSliceHeightPx
- sliceIndex++
- }
- }
-
- // Split at explicit .pageBreak markers (in document order, even when nested)
- // to avoid cutting content between pages.
- const docRoot = wrapper.querySelector('.doc') as HTMLElement | null
- const pageRoot = docRoot ?? wrapper
- const breakEls = Array.from(pageRoot.querySelectorAll('.pageBreak')) as HTMLElement[]
-
- if (breakEls.length === 0) {
- const canvas: HTMLCanvasElement = await html2canvas(wrapper, {
- scale: Math.min(2, window.devicePixelRatio || 1),
- backgroundColor: '#ffffff',
- useCORS: true,
- })
- renderCanvasToPdf(canvas, 0)
- } else {
- const range = document.createRange()
- range.setStart(pageRoot, 0)
-
- const fragments: DocumentFragment[] = []
- for (const br of breakEls) {
- range.setEndBefore(br)
- const frag = range.cloneContents()
- if (frag.childNodes.length > 0) fragments.push(frag)
- range.setStartAfter(br)
- }
- range.setEnd(pageRoot, pageRoot.childNodes.length)
- const lastFrag = range.cloneContents()
- if (lastFrag.childNodes.length > 0) fragments.push(lastFrag)
-
- if (fragments.length === 0) {
- const canvas: HTMLCanvasElement = await html2canvas(wrapper, {
- scale: Math.min(2, window.devicePixelRatio || 1),
- backgroundColor: '#ffffff',
- useCORS: true,
- })
- renderCanvasToPdf(canvas, 0)
- } else {
- for (let pageIndex = 0; pageIndex < fragments.length; pageIndex++) {
- const pageWrapper = document.createElement('div')
- pageWrapper.style.position = 'fixed'
- pageWrapper.style.left = '-10000px'
- pageWrapper.style.top = '0'
- pageWrapper.style.width = '794px'
- pageWrapper.style.background = '#ffffff'
- pageWrapper.innerHTML = ``
-
- const pageDoc = document.createElement('div')
- pageDoc.className = docRoot?.className || 'doc'
- pageDoc.appendChild(fragments[pageIndex])
- pageWrapper.appendChild(pageDoc)
- document.body.appendChild(pageWrapper)
- try {
- const canvas: HTMLCanvasElement = await html2canvas(pageWrapper, {
- scale: Math.min(2, window.devicePixelRatio || 1),
- backgroundColor: '#ffffff',
- useCORS: true,
- })
- renderCanvasToPdf(canvas, pageIndex)
- } finally {
- document.body.removeChild(pageWrapper)
- }
- }
- }
- }
-
- const blob = pdf.output('blob') as Blob
- const url = URL.createObjectURL(blob)
- setContractPdfUrl(url)
- setContractPdfKey(contractPdfCacheKey)
- } finally {
- document.body.removeChild(wrapper)
- }
- } catch (e: any) {
- setContractPdfError(e?.message || 'Failed to generate PDF preview.')
- } finally {
- setContractPdfLoading(false)
- }
- }
+ const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A']; // dark blue palette
useEffect(() => {
try {
@@ -404,19 +96,18 @@ export default function SummaryPage() {
useEffect(() => {
let active = true;
(async () => {
- const mountCountry = initialCountryRef.current
- console.info('[SummaryPage] Loading vat rates (mount). country:', mountCountry)
+ console.info('[SummaryPage] Loading vat rates (mount). country:', form.country)
const list = await getVatRates();
if (!active) return;
console.info('[SummaryPage] getVatRates result count:', list.length)
setVatRates(list);
- const upper = mountCountry.toUpperCase();
+ const upper = form.country.toUpperCase();
const match = list.find(r => r.code === upper);
if (match?.rate != null) {
console.info('[SummaryPage] Initial taxRate from list:', match.rate, 'country:', upper)
setTaxRate(match.rate);
} else {
- const rate = await getStandardVatRate(mountCountry);
+ const rate = await getStandardVatRate(form.country);
console.info('[SummaryPage] Fallback taxRate via getStandardVatRate:', rate, 'country:', upper)
setTaxRate(rate ?? 0.07);
}
@@ -447,29 +138,17 @@ export default function SummaryPage() {
() => selectedEntries.reduce((sum, e) => sum + (e.quantity / 10) * e.coffee.pricePer10, 0),
[selectedEntries]
);
-
- const shippingFee = useMemo(() => {
- const v = feeByPieceCount[selectedPlanCapsules];
- return Number.isFinite(Number(v)) ? Number(v) : 0;
- }, [feeByPieceCount, selectedPlanCapsules]);
-
- const netWithShipping = useMemo(
- () => totalPrice + shippingFee,
- [totalPrice, shippingFee]
- );
-
const taxAmount = useMemo(() => totalPrice * taxRate, [totalPrice, taxRate]);
- const taxAmountWithShipping = useMemo(() => netWithShipping * taxRate, [netWithShipping, taxRate]);
- const totalWithTax = useMemo(() => netWithShipping + taxAmountWithShipping, [netWithShipping, taxAmountWithShipping]);
+ const totalWithTax = useMemo(() => totalPrice + taxAmount, [totalPrice, taxRate, taxAmount]);
- const handleInput = (e: React.ChangeEvent) => {
+ const handleInput = (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 handleRecipientNotes = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setForm(prev => ({ ...prev, [name]: value }));
};
const fillFromLoggedInData = () => {
@@ -498,7 +177,7 @@ export default function SummaryPage() {
}));
};
- const requiredSelfFields = [
+ const requiredSelfFields: Array = [
'firstName',
'lastName',
'email',
@@ -506,20 +185,17 @@ export default function SummaryPage() {
'postalCode',
'city',
'country',
- ] as const
+ 'frequency',
+ ]
- const hasRequiredSelfFields = requiredSelfFields.every(k => String(form[k]).trim() !== '')
- const hasRequiredInvoiceFields = form.invoiceSameAsShipping || form.invoiceEmail.trim() !== ''
- const hasSigningCity = form.signingCity.trim() !== ''
- const hasSignature = signatureDataUrl.trim() !== ''
+ const hasRequiredSelfFields = requiredSelfFields.every(k => form[k].trim() !== '')
+ const hasRequiredGiftFields = isForSelf || form.recipientEmail.trim() !== ''
const canSubmit =
selectedEntries.length > 0 &&
totalPacks === requiredPacks &&
hasRequiredSelfFields &&
- hasRequiredInvoiceFields &&
- hasSigningCity &&
- hasSignature;
+ hasRequiredGiftFields;
const backToSelection = () => router.push('/coffee-abonnements');
@@ -530,14 +206,8 @@ export default function SummaryPage() {
setSubmitError(`Order must contain exactly ${requiredPacks} packs (${selectedPlanCapsules} capsules).`)
return
}
-
- if (!hasSigningCity) {
- setSubmitError('Signing city is required.')
- return
- }
-
- if (!hasSignature) {
- setSubmitError('Signature is required.')
+ if (!isForSelf && !form.recipientEmail.trim()) {
+ setSubmitError('Recipient email is required when the subscription is for someone else.')
return
}
@@ -552,7 +222,8 @@ export default function SummaryPage() {
billing_interval: 'month',
interval_count: 1,
is_auto_renew: true,
- is_for_self: true,
+ is_for_self: isForSelf,
+ // NEW: pass customer fields
firstName: form.firstName.trim(),
lastName: form.lastName.trim(),
email: form.email.trim(),
@@ -560,32 +231,18 @@ export default function SummaryPage() {
postalCode: form.postalCode.trim(),
city: form.city.trim(),
country: form.country.trim(),
- 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,
+ 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
referred_by: typeof currentUserId === 'number' ? currentUserId : undefined,
}
console.info('[SummaryPage] subscribeAbo payload:', payload)
// NEW: explicit JSON preview to match request body
console.info('[SummaryPage] subscribeAbo payload JSON:', JSON.stringify(payload))
await subscribeAbo(payload)
-
- setGuestMailtoHref('')
- setGuestInviteLink('')
-
setShowThanks(true);
try { sessionStorage.removeItem('coffeeSelections'); } catch {}
try { sessionStorage.removeItem('coffeeAboSizeCapsules'); } catch {}
@@ -670,6 +327,7 @@ export default function SummaryPage() {
>
Fill fields with logged in data
+ {/* "For someone else" is disabled for now — only self-subscriptions */}
{/* inputs translated */}
@@ -705,142 +363,51 @@ export default function SummaryPage() {
- Phone (optional)
-
+ Delivery interval
+
+ Monthly
+ Every 2 months
+ Quarterly
+
-
-
- {/* Payment method */}
-
-
Payment method
-
-
- {/* Invoice address */}
-
-
Invoice address
-
-
- Same as shipping address
-
- {!form.invoiceSameAsShipping && (
-
- )}
-
-
- {/* Contract preview + signature */}
-
-
Contract preview (ABO)
-
- Contract variables are auto-populated from your form data.
-
-
- {contractLoading ? (
-
- Loading contract preview…
-
- ) : contractError ? (
-
- Contract preview could not be loaded: {contractError}
-
- ) : populatedContractHtml ? (
-
- Open preview
-
- ) : (
-
- Contract template is not available.
-
- )}
-
-
-
-
Ort (Signing City) *
-
- {!hasSigningCity && submitError && (
-
Ort ist erforderlich.
- )}
-
-
-
-
-
-
- ABO contract preview (PDF)
-
- {contractPdfError ? (
-
- PDF preview could not be generated: {contractPdfError}
-
- ) : contractPdfLoading ? (
-
- Generating PDF preview…
-
- ) : contractPdfUrl ? (
-
-
- ) : (
-
- No PDF preview available.
+
+ Recipient name (optional)
+
- )}
-
-
-
- Close
-
-
-
-
+
+ Recipient note (optional)
+
+
+ >
+ )}
+
{!canSubmit && (
- Please select coffees and fill all required buyer fields, signing city, and signature.
+ {isForSelf
+ ? 'Please select coffees and fill all required buyer fields.'
+ : 'Please select coffees and fill all required buyer fields plus recipient email.'}
)}
@@ -876,34 +445,13 @@ export default function SummaryPage() {
€{((entry.quantity / 10) * entry.coffee.pricePer10).toFixed(2)}
))}
-
- {/* Shipping */}
-
- Shipping
-
- {shippingLoading ? (
- 'Loading…'
- ) : shippingFee === 0 ? (
- 'FREE SHIPPING'
- ) : (
- `€${shippingFee.toFixed(2)}`
- )}
-
-
-
- {shippingError && (
-
- Shipping fees could not be loaded: {shippingError}
-
- )}
-
Total (net)
- €{netWithShipping.toFixed(2)}
+ €{totalPrice.toFixed(2)}
Tax ({(taxRate * 100).toFixed(1)}%)
- €{taxAmountWithShipping.toFixed(2)}
+ €{taxAmount.toFixed(2)}
Total incl. tax
@@ -941,7 +489,7 @@ export default function SummaryPage() {
Thanks for your subscription!
- Subscription created.
+ {isForSelf ? 'Subscription created.' : 'Subscription created, invitation sent.'}
diff --git a/src/app/components/dialog.tsx b/src/app/components/dialog.tsx
index 4189f46..eb55aa3 100644
--- a/src/app/components/dialog.tsx
+++ b/src/app/components/dialog.tsx
@@ -25,13 +25,13 @@ export function Dialog({
'as' | 'className'
>) {
return (
-
+
-
+
)}
- >
- )}
- {userPresent && DISPLAY_ABONEMENTS && hasSubscribePerm && (
- router.push('/coffee-abonnements')}
- className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
- >
- Coffee Abonnements
-
+ {DISPLAY_ABONEMENTS && hasSubscribePerm && (
+ router.push('/coffee-abonnements')}
+ className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
+ >
+ Coffee Abonnements
+
+ )}
+ >
)}
{/* Information dropdown already removed here */}
@@ -737,16 +737,16 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
Personal Matrix
)}
+ {DISPLAY_ABONEMENTS && hasSubscribePerm && (
+ { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
+ className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
+ >
+ Coffee Abonnements
+
+ )}
>
)}
- {DISPLAY_ABONEMENTS && hasSubscribePerm && (
- { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
- className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
- >
- Coffee Abonnements
-
- )}
{/* Admin navigation – LAST */}
{isAdmin && (
@@ -763,12 +763,6 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
>
Dashboard
- { router.push('/admin/dashboard-management'); setMobileMenuOpen(false); }}
- className="w-full text-left rounded-lg px-2 py-1.5 text-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
- >
- Dashboard Management
-
{ router.push('/admin/user-verify'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 7979bc4..bfddc4e 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -1,7 +1,6 @@
'use client'
import { useEffect, useState, useCallback, useRef } from 'react'
-import type { ComponentType, SVGProps } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
@@ -16,20 +15,12 @@ import {
StarIcon,
LinkIcon
} from '@heroicons/react/24/outline'
-import {
- DEFAULT_DASHBOARD_PLATFORMS,
- loadDashboardPlatforms,
- subscribeDashboardPlatformsUpdated,
- type DashboardPlatform,
- type DashboardPlatformIconName
-} from '../utils/dashboardPlatforms'
export default function DashboardPage() {
const router = useRouter()
const user = useAuthStore(state => state.user)
const isAuthReady = useAuthStore(state => state.isAuthReady)
const isShopEnabled = process.env.NEXT_PUBLIC_SHOW_SHOP !== 'false'
- const [platforms, setPlatforms] = useState(DEFAULT_DASHBOARD_PLATFORMS)
const [isMobile, setIsMobile] = useState(false)
const [latestNews, setLatestNews] = useState>([])
const [newsLoading, setNewsLoading] = useState(false)
@@ -59,11 +50,6 @@ export default function DashboardPage() {
}
}, [])
- useEffect(() => {
- setPlatforms(loadDashboardPlatforms())
- return subscribeDashboardPlatformsUpdated(() => setPlatforms(loadDashboardPlatforms()))
- }, [])
-
useEffect(() => {
let active = true
;(async () => {
@@ -92,20 +78,16 @@ export default function DashboardPage() {
}
}, [isAuthReady, user, router])
- // NEW: block dashboard unless all quickaction steps are completed
- // For guest users: only email verification is required
- // For regular users: all 4 steps must be completed
+ // NEW: block dashboard unless all 4 quickaction steps are completed
useEffect(() => {
if (!isAuthReady || !user) return
if (statusLoading || !userStatus) return
- const isGuest = user?.role === 'guest'
- const allDone = isGuest
- ? !!userStatus.email_verified
- : !!userStatus.email_verified &&
- !!userStatus.documents_uploaded &&
- !!userStatus.profile_completed &&
- !!userStatus.contract_signed
+ const allDone =
+ !!userStatus.email_verified &&
+ !!userStatus.documents_uploaded &&
+ !!userStatus.profile_completed &&
+ !!userStatus.contract_signed
if (!allDone) smoothReplace('/quickaction-dashboard')
}, [isAuthReady, user, statusLoading, userStatus, smoothReplace])
@@ -134,15 +116,13 @@ export default function DashboardPage() {
)
}
- // Final guard (don't render dashboard if not all done)
+ // NEW: final guard (don’t render dashboard if not all done)
if (!userStatus) return null
- const isGuestUser = user?.role === 'guest'
- const allDone = isGuestUser
- ? !!userStatus.email_verified
- : !!userStatus.email_verified &&
- !!userStatus.documents_uploaded &&
- !!userStatus.profile_completed &&
- !!userStatus.contract_signed
+ const allDone =
+ !!userStatus.email_verified &&
+ !!userStatus.documents_uploaded &&
+ !!userStatus.profile_completed &&
+ !!userStatus.contract_signed
if (!allDone) return null
// Get user name
@@ -155,12 +135,39 @@ export default function DashboardPage() {
return 'User'
}
- const icons: Record>> = {
- ShoppingBagIcon,
- LinkIcon,
- UsersIcon,
- UserCircleIcon
- }
+ // Quick actions
+ const quickActions = [
+ {
+ title: 'Browse Shop',
+ description: 'Explore sustainable products',
+ icon: ShoppingBagIcon,
+ href: '/shop',
+ color: 'bg-blue-500',
+ disabled: !isShopEnabled,
+ disabledText: 'This is currently disabled.'
+ },
+ {
+ title: 'Browse Affiliate Links',
+ description: 'Discover affiliate offers and links',
+ icon: LinkIcon,
+ href: '/affiliate-links',
+ color: 'bg-teal-500'
+ },
+ {
+ title: 'Referral Management',
+ description: 'Create and manage referral links',
+ icon: UsersIcon,
+ href: '/referral-management',
+ color: 'bg-amber-600'
+ },
+ {
+ title: 'Edit Profile',
+ description: 'Update your information',
+ icon: UserCircleIcon,
+ href: '/profile',
+ color: 'bg-purple-500'
+ }
+ ]
const content = (
@@ -180,62 +187,55 @@ export default function DashboardPage() {
{/* Quick Actions */}
-
Platforms
+
Quick Actions
- {platforms.filter(p => p.isActive).map((platform) => {
- const Icon = icons[platform.icon]
- const disabledByEnv = platform.href === '/shop' && !isShopEnabled
- const isDisabled = Boolean(platform.disabled) || disabledByEnv
- const disabledText = disabledByEnv ? 'This is currently disabled.' : platform.disabledText
-
- return (
+ {quickActions.map((action, index) => (
{
- if (!isDisabled) {
- router.push(platform.href)
+ if (!action.disabled) {
+ router.push(action.href)
}
}}
- disabled={isDisabled}
+ disabled={Boolean(action.disabled)}
className={`bg-white rounded-lg p-6 border border-gray-200 text-left group transition-all duration-200 ${
- isDisabled
+ action.disabled
? 'opacity-60 cursor-not-allowed'
: 'shadow-sm hover:shadow-lg hover:-translate-y-1 hover:-translate-y-1 hover:-translate-y-1 transform hover:-translate-y-1'
}`}
>
- {platform.title}
+ {action.title}
- {platform.description}
+ {action.description}
- {isDisabled && disabledText && (
+ {action.disabled && action.disabledText && (
- {disabledText}
+ {action.disabledText}
)}
- )
- })}
+ ))}
diff --git a/src/app/hooks/usePublicDashboardPlatforms.ts b/src/app/hooks/usePublicDashboardPlatforms.ts
deleted file mode 100644
index 731a72b..0000000
--- a/src/app/hooks/usePublicDashboardPlatforms.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { useEffect, useMemo, useState } from 'react'
-import {
- DASHBOARD_PLATFORMS_COLOR_OPTIONS,
- DASHBOARD_PLATFORMS_ICON_OPTIONS,
- type DashboardPlatform,
- type DashboardPlatformColorClass,
- type DashboardPlatformIconName
-} from '../utils/dashboardPlatforms'
-
-type BackendPlatform = {
- id: string | number
- title: string
- href: string
- description?: string | null
- icon?: DashboardPlatformIconName | null
- color?: DashboardPlatformColorClass | null
- state?: boolean
- disabled?: boolean
- disabledText?: string | null
- sortOrder?: number | null
-}
-
-const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '')
-
-function toPlatform(p: BackendPlatform): DashboardPlatform | null {
- const title = typeof p.title === 'string' ? p.title : ''
- const href = typeof p.href === 'string' ? p.href : ''
- if (!title.trim() || !href.trim()) return null
-
- const icon: DashboardPlatformIconName =
- p.icon && DASHBOARD_PLATFORMS_ICON_OPTIONS.some(o => o.value === p.icon)
- ? p.icon
- : 'LinkIcon'
-
- const color: DashboardPlatformColorClass =
- p.color && DASHBOARD_PLATFORMS_COLOR_OPTIONS.some(o => o.value === p.color)
- ? p.color
- : 'bg-blue-500'
-
- return {
- id: String(p.id),
- title,
- href,
- description: typeof p.description === 'string' ? p.description : '',
- icon,
- color,
- isActive: typeof p.state === 'boolean' ? p.state : true,
- disabled: typeof p.disabled === 'boolean' ? p.disabled : false,
- disabledText: typeof p.disabledText === 'string' ? p.disabledText : undefined
- }
-}
-
-export function usePublicDashboardPlatforms() {
- const [platforms, setPlatforms] = useState
([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let active = true
- ;(async () => {
- setLoading(true)
- setError(null)
- try {
- const res = await fetch(`${API_BASE}/api/dashboard-platforms`, {
- method: 'GET',
- headers: { Accept: 'application/json' },
- credentials: 'include'
- })
-
- if (!res.ok) {
- const text = await res.text().catch(() => '')
- throw new Error(text || `HTTP ${res.status}`)
- }
-
- const json = (await res.json().catch(() => null)) as unknown
- const list = Array.isArray(json) ? (json as BackendPlatform[]) : []
- const normalized = list
- .map(toPlatform)
- .filter((x): x is DashboardPlatform => Boolean(x))
-
- if (active) setPlatforms(normalized)
- } catch (e: any) {
- if (active) setError(e?.message || 'Failed to load platforms')
- } finally {
- if (active) setLoading(false)
- }
- })()
-
- return () => {
- active = false
- }
- }, [])
-
- const activePlatforms = useMemo(() => platforms.filter(p => p.isActive), [platforms])
-
- return {
- platforms: activePlatforms,
- loading,
- error
- }
-}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 1874e3c..b3cf745 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -2,28 +2,26 @@
import { useRef, useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
+import { gsap } from 'gsap';
import PageLayout from './components/PageLayout';
import Crosshair from './components/Crosshair';
import Waves from './components/background/waves';
-import { usePublicDashboardPlatforms } from './hooks/usePublicDashboardPlatforms';
-import {
- LinkIcon,
- ShoppingBagIcon,
- UserCircleIcon,
- UsersIcon,
-} from '@heroicons/react/24/outline';
-import type { ComponentType, SVGProps } from 'react';
-import type { DashboardPlatformIconName } from './utils/dashboardPlatforms';
+import SplitText from './components/SplitText';
export default function HomePage() {
const containerRef = useRef(null);
+ const [isHover, setIsHover] = useState(false);
const [isMobile, setIsMobile] = useState(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia('(max-width: 768px)').matches;
});
const router = useRouter();
- const isShopEnabled = process.env.NEXT_PUBLIC_SHOW_SHOP !== 'false';
- const { platforms, loading, error } = usePublicDashboardPlatforms();
+
+ // Mobile: instantly redirect to login
+ useEffect(() => {
+ if (!isMobile) return;
+ router.replace('/login');
+ }, [isMobile, router]);
// Keep breakpoint updated (resize/orientation)
useEffect(() => {
@@ -37,28 +35,41 @@ export default function HomePage() {
};
}, []);
- const icons: Record>> = {
- ShoppingBagIcon,
- LinkIcon,
- UsersIcon,
- UserCircleIcon,
- };
-
- const goTo = (href: string) => {
- const trimmed = (href || '').trim();
- if (!trimmed) return;
- if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
- window.location.href = trimmed;
+ const handleLoginClick = () => {
+ // Mobile: no page fade animation
+ if (isMobile || !containerRef.current) {
+ router.push('/login');
return;
}
- router.push(trimmed);
+
+ gsap.to(containerRef.current, {
+ opacity: 0,
+ duration: 0.6,
+ ease: 'power2.out',
+ onComplete: () => router.push('/login'),
+ });
};
+ // Ensure LOGIN never stays stuck after scrolling / wheel (desktop only)
+ useEffect(() => {
+ if (isMobile) return;
+ const resetHover = () => setIsHover(false);
+ window.addEventListener('wheel', resetHover, { passive: true });
+ window.addEventListener('scroll', resetHover, { passive: true });
+ return () => {
+ window.removeEventListener('wheel', resetHover);
+ window.removeEventListener('scroll', resetHover);
+ };
+ }, [isMobile]);
+
+ // Prevent any home UI flash on mobile
+ if (isMobile) return null;
+
return (
{/* Waves background */}
-
-
-
-
-
-
-
-
- Welcome
-
-
- Profit Planet
-
-
- Pick a platform to continue.
-
-
-
-
-
-
-
-
-
Platforms
-
Navigation shortcuts
-
-
-
-
- {loading && (
-
- Loading…
-
- )}
-
- {error && (
-
- {error}
-
- )}
-
- {!loading && !error && (
-
- {platforms.map((platform) => {
- const Icon = icons[platform.icon] || LinkIcon;
- const disabledByEnv = platform.href === '/shop' && !isShopEnabled;
- const isDisabled = Boolean(platform.disabled) || disabledByEnv;
- const disabledText = disabledByEnv ? 'This is currently disabled.' : platform.disabledText;
-
- return (
-
{
- if (!isDisabled) {
- goTo(platform.href);
- }
- }}
- disabled={isDisabled}
- className={`rounded-2xl border text-left p-5 transition-all duration-200 ${
- isDisabled
- ? 'border-gray-200 bg-white opacity-60 cursor-not-allowed'
- : 'group border-gray-200 bg-white shadow-sm hover:shadow-md hover:-translate-y-0.5'
- }`}
- >
-
-
-
-
-
-
-
- {platform.title}
-
-
-
-
- {platform.description}
-
-
- {isDisabled && disabledText && (
-
- {disabledText}
-
- )}
-
-
-
- );
- })}
-
- {platforms.length === 0 && (
-
- No platforms available.
-
- )}
-
- )}
-
-
-
-
-
+
{/* No parallax/crosshair on mobile */}
{!isMobile &&
}
diff --git a/src/app/profile/components/financeInvoices.tsx b/src/app/profile/components/financeInvoices.tsx
index 22ca016..f055ba7 100644
--- a/src/app/profile/components/financeInvoices.tsx
+++ b/src/app/profile/components/financeInvoices.tsx
@@ -28,10 +28,8 @@ const isAbsUrl = (url: string) => /^https?:\/\//i.test(url)
const resolveInvoiceUrl = (invoice: AboInvoice) => {
const raw = invoice.pdfUrl || invoice.downloadUrl || invoice.htmlUrl || invoice.fileUrl
- if (raw) return isAbsUrl(raw) ? raw : `${BASE_URL}${raw.startsWith('/') ? '' : '/'}${raw}`
- // Fallback: use the backend PDF proxy endpoint if an id is available
- if (invoice.id) return `${BASE_URL}/api/invoices/${invoice.id}/pdf`
- return null
+ if (!raw) return null
+ return isAbsUrl(raw) ? raw : `${BASE_URL}${raw.startsWith('/') ? '' : '/'}${raw}`
}
type UiLifecycleStatus = 'issued' | 'ongoing' | 'finished' | 'pause' | 'cancelled'
@@ -82,25 +80,14 @@ export default function FinanceInvoices({ abonementId }: Props) {
const [busyId, setBusyId] = React.useState
(null)
const [actionError, setActionError] = React.useState(null)
- const onView = async (invoice: AboInvoice) => {
+ const onView = (invoice: AboInvoice) => {
setActionError(null)
const url = resolveInvoiceUrl(invoice)
if (!url) {
setActionError('No view URL is available for this invoice.')
return
}
- setBusyId(invoice.id)
- try {
- const res = await authFetch(url, { method: 'GET' })
- if (!res.ok) throw new Error(`Failed to load PDF: ${res.status}`)
- const blob = await res.blob()
- const blobUrl = URL.createObjectURL(blob)
- window.open(blobUrl, '_blank', 'noopener,noreferrer')
- } catch (e: any) {
- setActionError(e?.message || 'Failed to load invoice PDF.')
- } finally {
- setBusyId(null)
- }
+ window.open(url, '_blank', 'noopener,noreferrer')
}
const onDownload = async (invoice: AboInvoice) => {
diff --git a/src/app/profile/components/mediaSection.tsx b/src/app/profile/components/mediaSection.tsx
index 10a821e..2a5c3bc 100644
--- a/src/app/profile/components/mediaSection.tsx
+++ b/src/app/profile/components/mediaSection.tsx
@@ -23,14 +23,8 @@ export default function MediaSection({ documents }: { documents: any[] }) {
{doc.type}
{doc.uploaded}
- {doc.signedUrl ? (
- <>
- Download
- Preview
- >
- ) : (
- No file
- )}
+ Download
+ Preview
))}
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx
index a8d2526..0b88028 100644
--- a/src/app/profile/page.tsx
+++ b/src/app/profile/page.tsx
@@ -184,28 +184,7 @@ export default function ProfilePage() {
}
}, [profileDataApi, user, progressPercent])
- const documents = React.useMemo(() => {
- const contracts = Array.isArray(mediaData?.contracts) ? mediaData.contracts : []
- const idDocs = Array.isArray(mediaData?.idDocuments) ? mediaData.idDocuments : []
-
- const contractItems = contracts.map((doc: any) => ({
- id: `contract-${doc.id}`,
- name: doc.original_filename || 'Contract',
- type: 'Contract',
- uploaded: doc.created_at ? new Date(doc.created_at).toLocaleDateString() : '-',
- signedUrl: doc.signedUrl,
- }))
-
- const idDocItems = idDocs.filter((d: any) => d.object_storage_id).map((doc: any) => ({
- id: `id-${doc.user_id_document_id}-${doc.side}`,
- name: doc.original_filename || `ID Document (${doc.side})`,
- type: `${doc.id_type || 'ID'} – ${doc.side}`,
- uploaded: doc.expiry_date ? new Date(doc.expiry_date).toLocaleDateString() : '-',
- signedUrl: doc.signedUrl,
- }))
-
- return [...contractItems, ...idDocItems]
- }, [mediaData])
+ const documents = Array.isArray(mediaData?.documents) ? mediaData.documents : []
useEffect(() => {
if (showRefreshing && !profileLoading && !mediaLoading && !completionLoading) {
diff --git a/src/app/quickaction-dashboard/page.tsx b/src/app/quickaction-dashboard/page.tsx
index 2a714f9..dc5729f 100644
--- a/src/app/quickaction-dashboard/page.tsx
+++ b/src/app/quickaction-dashboard/page.tsx
@@ -90,13 +90,8 @@ export default function QuickActionDashboardPage() {
const additionalInfo = userStatus?.profile_completed || false
const contractSigned = userStatus?.contract_signed || false
- // Detect guest user — guests only need email verification
- const isGuest = isClient && user?.role === 'guest'
-
- // For guests: only email verification matters. For regular users: all 4 steps.
- const allDone = isGuest
- ? emailVerified
- : emailVerified && idUploaded && additionalInfo && contractSigned
+ // NEW: if everything is done, quickaction-dashboard is no longer accessible
+ const allDone = emailVerified && idUploaded && additionalInfo && contractSigned
// NEW: smooth redirect (prevents snappy double navigation)
const [redirectTo, setRedirectTo] = useState(null)
const redirectOnceRef = useRef(false)
@@ -172,47 +167,36 @@ export default function QuickActionDashboardPage() {
sessionStorage.setItem(sessionKey, '1')
}, [isClient, loading, userStatus, isTutorialOpen, user, accessToken, getNextTutorialStep])
- // For guests: only show email verification step. For regular users: all 4 steps.
- const statusItems: StatusItem[] = isGuest
- ? [
- {
- key: 'email',
- label: 'Email Verification',
- description: emailVerified ? 'Verified' : 'Missing',
- complete: emailVerified,
- icon: EnvelopeOpenIcon
- }
- ]
- : [
- {
- key: 'email',
- label: 'Email Verification',
- description: emailVerified ? 'Verified' : 'Missing',
- complete: emailVerified,
- icon: EnvelopeOpenIcon
- },
- {
- key: 'id',
- label: 'ID Document',
- description: idUploaded ? 'Uploaded' : 'Missing',
- complete: idUploaded,
- icon: IdentificationIcon
- },
- {
- key: 'info',
- label: 'Additional Info',
- description: additionalInfo ? 'Completed' : 'Missing',
- complete: additionalInfo,
- icon: InformationCircleIcon
- },
- {
- key: 'contract',
- label: 'Contract',
- description: contractSigned ? 'Signed' : 'Missing',
- complete: contractSigned,
- icon: DocumentCheckIcon
- }
- ]
+ const statusItems: StatusItem[] = [
+ {
+ key: 'email',
+ label: 'Email Verification',
+ description: emailVerified ? 'Verified' : 'Missing',
+ complete: emailVerified,
+ icon: EnvelopeOpenIcon
+ },
+ {
+ key: 'id',
+ label: 'ID Document',
+ description: idUploaded ? 'Uploaded' : 'Missing',
+ complete: idUploaded,
+ icon: IdentificationIcon
+ },
+ {
+ key: 'info',
+ label: 'Additional Info',
+ description: additionalInfo ? 'Completed' : 'Missing',
+ complete: additionalInfo,
+ icon: InformationCircleIcon
+ },
+ {
+ key: 'contract',
+ label: 'Contract',
+ description: contractSigned ? 'Signed' : 'Missing',
+ complete: contractSigned,
+ icon: DocumentCheckIcon
+ }
+ ]
// Action handlers - navigate to proper QuickAction pages with tutorial callback
const handleVerifyEmail = useCallback(() => {
@@ -329,11 +313,7 @@ export default function QuickActionDashboardPage() {
Welcome{isClient && user?.firstName ? `, ${user.firstName}` : ''}!
- {isGuest
- ? 'Guest Account'
- : isClient && user?.userType === 'company'
- ? 'Company Account'
- : 'Personal Account'}
+ {isClient && user?.userType === 'company' ? 'Company Account' : 'Personal Account'}
{loading && Loading status...
}
{error && (
@@ -370,14 +350,11 @@ export default function QuickActionDashboardPage() {
{/* Status Overview */}
- {isGuest ? 'Email Verification Status' : 'Status Overview'}
+ Status Overview
- {/* Guest: single centered card. Regular: 2x2 / 4-col grid */}
-
+ {/* CHANGED: mobile 2x2 grid */}
+
{statusItems.map(item => {
const CompleteIcon = item.complete ? CheckCircleIcon : XCircleIcon
return (
@@ -385,7 +362,7 @@ export default function QuickActionDashboardPage() {
key={item.key}
className={`rounded-lg px-3 py-4 sm:px-4 sm:py-6 flex flex-col items-center text-center border transition-colors ${
item.complete ? 'bg-emerald-50 border-emerald-100' : 'bg-rose-50 border-rose-100'
- } ${isGuest ? 'w-full max-w-xs' : ''}`}
+ }`}
>
- {isGuest ? 'Action Required' : 'Quick Actions'}
+ Quick Actions
- {/* Tutorial button — only for regular users */}
- {!isGuest && (
-
-
- Tutorial
- {!hasSeenTutorial && (
-
- )}
-
- )}
+
+
+ Tutorial
+ {!hasSeenTutorial && (
+
+ )}
+
- {isGuest ? (
- /* ── Guest view: single email verification action ── */
-
-
-
- Please verify your email address to activate your guest account and access your subscriptions.
+ {/* CHANGED: mobile 2x2 grid (order already matches desired layout) */}
+
+ {/* Email Verification */}
+
+
+
+ {emailVerified ? 'Email Verified' : 'Verify Email'}
+
+ {/* NEW: resend feedback (only when not verified) */}
+ {!emailVerified && (
+
+ {resendRemainingSec > 0
+ ? `Resend available in ${formatMmSs(resendRemainingSec)}`
+ : 'You can request a new code now'}
-
-
- {emailVerified ? 'Email Verified ✓' : 'Verify Email'}
-
- {!emailVerified && (
-
- {resendRemainingSec > 0
- ? `Resend available in ${formatMmSs(resendRemainingSec)}`
- : 'You can request a new code now'}
-
- )}
-
+ )}
- ) : (
- /* ── Regular user view: all 4 quick action buttons ── */
-
- {/* Email Verification */}
-
-
-
- {emailVerified ? 'Email Verified' : 'Verify Email'}
-
- {!emailVerified && (
-
- {resendRemainingSec > 0
- ? `Resend available in ${formatMmSs(resendRemainingSec)}`
- : 'You can request a new code now'}
-
- )}
-
- {/* ID Upload */}
+ {/* ID Upload */}
+
+
+ {idUploaded ? 'ID Uploaded' : 'Upload ID Document'}
+
+
+ {/* Additional Info */}
+
+
+ {additionalInfo ? 'Profile Completed' : 'Complete Profile'}
+
+
+ {/* Sign Contract */}
+
-
- {idUploaded ? 'ID Uploaded' : 'Upload ID Document'}
+
+ {contractSigned ? 'Contract Signed' : 'Sign Contract'}
-
- {/* Additional Info */}
-
-
- {additionalInfo ? 'Profile Completed' : 'Complete Profile'}
-
-
- {/* Sign Contract */}
-
-
-
- {contractSigned ? 'Contract Signed' : 'Sign Contract'}
-
- {!canSignContract && !contractSigned && (
-
- Complete previous steps (email, ID, profile) before signing the contract.
-
- )}
-
+ {!canSignContract && !contractSigned && (
+
+ Complete previous steps (email, ID, profile) before signing the contract.
+
+ )}
- )}
+
{/* Latest News */}
diff --git a/src/app/quickaction-dashboard/register-email-verify/page.tsx b/src/app/quickaction-dashboard/register-email-verify/page.tsx
index 43c5e57..bd046b8 100644
--- a/src/app/quickaction-dashboard/register-email-verify/page.tsx
+++ b/src/app/quickaction-dashboard/register-email-verify/page.tsx
@@ -236,9 +236,7 @@ export default function EmailVerifyPage() {
message: 'Your email has been verified successfully.'
})
await refreshStatus()
- // Guests go directly to dashboard after email verification
- const isGuest = user?.role === 'guest'
- window.location.href = isGuest ? '/dashboard' : '/quickaction-dashboard?tutorial=true'
+ window.location.href = '/quickaction-dashboard?tutorial=true'
} else {
const msg = data.error || 'Verification failed. Please try again.'
setError(msg)
@@ -347,22 +345,18 @@ export default function EmailVerifyPage() {
useEffect(() => {
if (statusLoading || !userStatus) return
- const isGuest = user?.role === 'guest'
- const allDone = isGuest
- ? !!userStatus.email_verified
- : !!userStatus.email_verified &&
- !!userStatus.documents_uploaded &&
- !!userStatus.profile_completed &&
- !!userStatus.contract_signed
+ const allDone =
+ !!userStatus.email_verified &&
+ !!userStatus.documents_uploaded &&
+ !!userStatus.profile_completed &&
+ !!userStatus.contract_signed
if (allDone) {
- smoothReplace('/dashboard')
+ smoothReplace('/dashboard') // CHANGED
} else if (userStatus.email_verified) {
- // Regular users go back to quickaction dashboard for remaining steps
- // Guests should never reach here since allDone covers them
- smoothReplace('/quickaction-dashboard')
+ smoothReplace('/quickaction-dashboard') // CHANGED
}
- }, [statusLoading, userStatus, user, smoothReplace])
+ }, [statusLoading, userStatus, smoothReplace])
// NEW: must be logged in
useEffect(() => {
diff --git a/src/app/utils/dashboardPlatforms.ts b/src/app/utils/dashboardPlatforms.ts
deleted file mode 100644
index 3051a3e..0000000
--- a/src/app/utils/dashboardPlatforms.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-export type DashboardPlatformIconName =
- | 'ShoppingBagIcon'
- | 'LinkIcon'
- | 'UsersIcon'
- | 'UserCircleIcon'
-
-export type DashboardPlatformColorClass =
- | 'bg-blue-500'
- | 'bg-teal-500'
- | 'bg-amber-600'
- | 'bg-purple-500'
-
-export type DashboardPlatform = {
- id: string
- title: string
- description: string
- href: string
- icon: DashboardPlatformIconName
- color: DashboardPlatformColorClass
- isActive: boolean
- disabled?: boolean
- disabledText?: string
-}
-
-export const DASHBOARD_PLATFORMS_STORAGE_KEY = 'pp.dashboardPlatforms.v1'
-export const DASHBOARD_PLATFORMS_UPDATED_EVENT = 'pp:dashboard-platforms-updated'
-
-export const DASHBOARD_PLATFORMS_ICON_OPTIONS: Array<{ value: DashboardPlatformIconName; label: string }> = [
- { value: 'ShoppingBagIcon', label: 'Shopping Bag' },
- { value: 'LinkIcon', label: 'Link' },
- { value: 'UsersIcon', label: 'Users' },
- { value: 'UserCircleIcon', label: 'User Profile' }
-]
-
-export const DASHBOARD_PLATFORMS_COLOR_OPTIONS: Array<{ value: DashboardPlatformColorClass; label: string }> = [
- { value: 'bg-blue-500', label: 'Blue' },
- { value: 'bg-teal-500', label: 'Teal' },
- { value: 'bg-amber-600', label: 'Amber' },
- { value: 'bg-purple-500', label: 'Purple' }
-]
-
-export const DEFAULT_DASHBOARD_PLATFORMS: DashboardPlatform[] = [
- {
- id: 'shop',
- title: 'Browse Shop',
- description: 'Explore sustainable products',
- icon: 'ShoppingBagIcon',
- href: '/shop',
- color: 'bg-blue-500',
- isActive: true,
- disabledText: 'This is currently disabled.'
- },
- {
- id: 'affiliate-links',
- title: 'Browse Affiliate Links',
- description: 'Discover affiliate offers and links',
- icon: 'LinkIcon',
- href: '/affiliate-links',
- color: 'bg-teal-500',
- isActive: true
- },
- {
- id: 'referral-management',
- title: 'Referral Management',
- description: 'Create and manage referral links',
- icon: 'UsersIcon',
- href: '/referral-management',
- color: 'bg-amber-600',
- isActive: true
- },
- {
- id: 'profile',
- title: 'Edit Profile',
- description: 'Update your information',
- icon: 'UserCircleIcon',
- href: '/profile',
- color: 'bg-purple-500',
- isActive: true
- }
-]
-
-function isRecord(value: unknown): value is Record
{
- return !!value && typeof value === 'object' && !Array.isArray(value)
-}
-
-function createFallbackId(): string {
- return `platform_${Date.now()}_${Math.random().toString(16).slice(2)}`
-}
-
-function normalizePlatform(input: unknown): DashboardPlatform | null {
- if (!isRecord(input)) return null
-
- const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : createFallbackId()
- const title = typeof input.title === 'string' ? input.title : ''
- const description = typeof input.description === 'string' ? input.description : ''
- const href = typeof input.href === 'string' ? input.href : ''
- const icon = input.icon as DashboardPlatformIconName
- const color = input.color as DashboardPlatformColorClass
- const isActive = typeof input.isActive === 'boolean' ? input.isActive : true
- const disabled = typeof input.disabled === 'boolean' ? input.disabled : false
- const disabledText = typeof input.disabledText === 'string' ? input.disabledText : undefined
-
- const iconOk = DASHBOARD_PLATFORMS_ICON_OPTIONS.some(o => o.value === icon)
- const colorOk = DASHBOARD_PLATFORMS_COLOR_OPTIONS.some(o => o.value === color)
-
- if (!title.trim() || !href.trim() || !iconOk || !colorOk) return null
-
- return {
- id,
- title,
- description,
- href,
- icon,
- color,
- isActive,
- disabled,
- disabledText
- }
-}
-
-export function loadDashboardPlatforms(): DashboardPlatform[] {
- if (typeof window === 'undefined') return DEFAULT_DASHBOARD_PLATFORMS
-
- try {
- const raw = window.localStorage.getItem(DASHBOARD_PLATFORMS_STORAGE_KEY)
- if (!raw) return DEFAULT_DASHBOARD_PLATFORMS
-
- const parsed = JSON.parse(raw) as unknown
- if (!Array.isArray(parsed)) return DEFAULT_DASHBOARD_PLATFORMS
-
- const normalized = parsed
- .map(normalizePlatform)
- .filter((x): x is DashboardPlatform => Boolean(x))
-
- return normalized.length ? normalized : DEFAULT_DASHBOARD_PLATFORMS
- } catch {
- return DEFAULT_DASHBOARD_PLATFORMS
- }
-}
-
-export function saveDashboardPlatforms(platforms: DashboardPlatform[]): void {
- if (typeof window === 'undefined') return
-
- window.localStorage.setItem(DASHBOARD_PLATFORMS_STORAGE_KEY, JSON.stringify(platforms))
- window.dispatchEvent(new Event(DASHBOARD_PLATFORMS_UPDATED_EVENT))
-}
-
-export function subscribeDashboardPlatformsUpdated(callback: () => void): () => void {
- if (typeof window === 'undefined') return () => {}
-
- const handler = () => callback()
- window.addEventListener(DASHBOARD_PLATFORMS_UPDATED_EVENT, handler)
- window.addEventListener('storage', handler)
-
- return () => {
- window.removeEventListener(DASHBOARD_PLATFORMS_UPDATED_EVENT, handler)
- window.removeEventListener('storage', handler)
- }
-}