diff --git a/package-lock.json b/package-lock.json index 0000198..9dbe2fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,9 @@ "@angular/material": "^20.2.9", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", - "@infinimotion/model-frontend": "^0.0.89", + "@infinimotion/model-frontend": "^0.0.102", "@tailwindcss/postcss": "^4.1.14", + "angularx-qrcode": "^20.0.0", "ngx-mask": "^20.0.3", "postcss": "^8.5.6", "rxjs": "~7.8.0", @@ -1401,9 +1402,9 @@ } }, "node_modules/@infinimotion/model-frontend": { - "version": "0.0.89", - "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.89/model-frontend-0.0.89.tgz", - "integrity": "sha512-lvvQy8RWs41Bz52uBgsUKkwn8teGlgxlmG8Rvsgkh+v1IMVWFWVQmfMS7Rznd0lCZRgK1ByihH80X9eAN12idA==", + "version": "0.0.102", + "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.102/model-frontend-0.0.102.tgz", + "integrity": "sha512-NJV9bSBubdOZ1GBIe9To3o/hh6AZscJcTyaZY2nGmMxH+GhtvO1AHmjhrQeRjwAKFiwZMEwEg4ktFiOAp3MTMQ==", "license": "ISC" }, "node_modules/@inquirer/ansi": { @@ -3995,6 +3996,19 @@ "node": ">= 14.0.0" } }, + "node_modules/angularx-qrcode": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-20.0.0.tgz", + "integrity": "sha512-WZolRZztQsQxOXqodNSDicxPWNO79t/AT4wts+DxwYdtdXb1RELfZjtax9oGMQQ6mEZ6bwk5GqBGEDB3Y+cSqw==", + "license": "MIT", + "dependencies": { + "qrcode": "1.5.4", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": "^20.0.0" + } + }, "node_modules/ansi-escapes": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", @@ -4387,6 +4401,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001754", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", @@ -4547,7 +4570,6 @@ "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" @@ -4560,7 +4582,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -4806,6 +4827,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4843,6 +4873,12 @@ "dev": true, "license": "MIT" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -5447,6 +5483,19 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", @@ -5586,7 +5635,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -7136,6 +7184,18 @@ "@lmdb/lmdb-win32-x64": "3.4.2" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -8159,6 +8219,33 @@ "license": "MIT", "optional": true }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -8172,6 +8259,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -8322,6 +8418,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -8426,6 +8531,15 @@ "node": ">=16.20.0" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -8516,6 +8630,125 @@ "node": ">=0.9" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/qrcode/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -8583,7 +8816,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8599,6 +8831,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -8867,6 +9105,12 @@ "node": ">= 18" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -10009,11 +10253,16 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -10118,7 +10367,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10128,7 +10376,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -10144,14 +10391,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10161,7 +10406,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10176,7 +10420,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" diff --git a/package.json b/package.json index 0a846b3..41b72cb 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,9 @@ "@angular/material": "^20.2.9", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", - "@infinimotion/model-frontend": "^0.0.89", + "@infinimotion/model-frontend": "^0.0.102", "@tailwindcss/postcss": "^4.1.14", + "angularx-qrcode": "^20.0.0", "ngx-mask": "^20.0.3", "postcss": "^8.5.6", "rxjs": "~7.8.0", diff --git a/src/app/app-module.ts b/src/app/app-module.ts index f692e72..2b87502 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -7,6 +7,7 @@ import { AppRoutingModule } from './app-routing-module'; import { App } from './app'; import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; +import { QRCodeComponent } from 'angularx-qrcode'; import { MatIconModule } from '@angular/material/icon'; import { MatTabsModule } from '@angular/material/tabs'; @@ -55,6 +56,12 @@ import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component'; import { OrderComponent } from './order/order.component'; import { SeatSelectionComponent } from './seat-selection/seat-selection.component'; import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.component'; +import { ReservationSuccessComponent } from './reservation-success/reservation-success.component'; +import { ReservationFailedComponent } from './reservation-failed/reservation-failed.component'; +import { PurchaseSuccessComponent } from './purchase-success/purchase-success.component'; +import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.component'; +import { TicketSmallComponent } from './ticket-small/ticket-small.component'; +import { TicketListComponent } from './ticket-list/ticket-list.component'; @NgModule({ @@ -92,6 +99,12 @@ import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.comp OrderComponent, SeatSelectionComponent, NoSeatsInHallComponent, + ReservationSuccessComponent, + ReservationFailedComponent, + PurchaseSuccessComponent, + PurchaseFailedComponent, + TicketSmallComponent, + TicketListComponent, ], imports: [ AppRoutingModule, @@ -119,6 +132,7 @@ import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.comp MatStepperModule, NgxMaskDirective, NgxMaskPipe, + QRCodeComponent, ], providers: [ provideBrowserGlobalErrorListeners(), diff --git a/src/app/movie-import-search-info/movie-import-search-info.component.html b/src/app/movie-import-search-info/movie-import-search-info.component.html index fa3bab7..0fe7563 100644 --- a/src/app/movie-import-search-info/movie-import-search-info.component.html +++ b/src/app/movie-import-search-info/movie-import-search-info.component.html @@ -13,7 +13,7 @@ Erscheinungsjahr: {{ movie().year }} - diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index dce14e7..2b5831e 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -1,6 +1,6 @@
-@if (loadingService.loading$ | async){ +@if (!performance() && (loadingService.loading$ | async)){
+ @if(isSubmitting) { +
+ +
+ } + Warenkorb @@ -48,8 +58,8 @@
- - + +
@@ -59,32 +69,45 @@
- - - Name - - @if (fData['name'].hasError('minlength')) { Mindestens 3 Zeichen } - + @if (seatsReserved && !isSubmitting) { +
+ @if (successful) { + + } @else { + + } + } + @else { - - - E-Mail Adresse - - @if (fData['email'].hasError('email')) { Ungültige E-Mail-Adresse } - + + + Name + + @if (fData['name'].hasError('minlength')) { Mindestens 3 Zeichen } + - -
- - Ich akzeptiere die AGB und die Datenbestimmung - -
+ + + E-Mail Adresse + + @if (fData['email'].hasError('email')) { Ungültige E-Mail-Adresse } + + + +
+ + Ich akzeptiere die AGB und die Datenbestimmung + +
+ + +
+ + +
+ + } - -
- - -
@@ -93,66 +116,77 @@
- - - Kartennummer - - @if (fPayment['cardNumber'].hasError('pattern')) { Ungültige Kartennummer } + @if (seatsPurchased && !isSubmitting) { +
+ @if (successful) { + + } @else { + + } + } + @else { -
- - - - Kartenname - - @if (fPayment['cardName'].hasError('minlength')) { Mindestens 3 Zeichen } - - - -
- - Gültig bis (MM/YY) + + + Kartennummer - @if (fPayment['expiry'].hasError('pattern')) { Ungültiges Format } + @if (fPayment['cardNumber'].hasError('pattern')) { Ungültige Kartennummer } + - - CVV - - @if (fPayment['cvv'].hasError('pattern')) { 3–4 Ziffernt } + + + Kartenname + + @if (fPayment['cardName'].hasError('minlength')) { Mindestens 3 Zeichen } -
- -
- - encrypted - -

- Ihre Zahlung wird sicher über unsere Partner verarbeitet.
Wir speichern keine Zahlungsinformationen. -

-
+ +
+ + Gültig bis (MM/YY) + + @if (fPayment['expiry'].hasError('pattern')) { Ungültiges Format } + - -
- - -
+ + CVV + + @if (fPayment['cvv'].hasError('pattern')) { 3–4 Ziffernt } + +
+ + +
+ + encrypted + +

+ Ihre Zahlung wird sicher über unsere Partner verarbeitet.
Wir speichern keine Zahlungsinformationen. +

+
+ + +
+ + +
+ }
diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index f1c0989..1449912 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -1,9 +1,12 @@ import { SelectedSeatsService } from './../selected-seats.service'; import { LoadingService } from './../loading.service'; -import { Sitzkategorie, Vorstellung } from '@infinimotion/model-frontend'; +import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend'; import { Component, computed, inject, input } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; +import { HttpService } from '../http.service'; +import { catchError, tap, finalize } from 'rxjs'; +import { MatStepper } from '@angular/material/stepper'; @Component({ selector: 'app-order', @@ -17,8 +20,17 @@ export class OrderComponent { submitted = false; + performance = input(); + seatCategories = input.required(); + + loadingService = inject(LoadingService); + private httpService = inject(HttpService) + private selectedSeatsService = inject(SelectedSeatsService); + constructor(private fb: FormBuilder) {} + // Form-Validation + ngOnInit(): void { this.paymentForm = this.fb.group({ cardNumber: ['', [Validators.required, Validators.pattern(/^\d{16}$/)]], @@ -37,23 +49,31 @@ export class OrderComponent { get fPayment() { return this.paymentForm.controls; } onSubmit() { + if (this.dataForm.invalid) return; if (this.paymentForm.invalid) return; - console.log('Zahlungsdaten:', this.paymentForm.value); } onStepChange(event: StepperSelectionEvent) { this.submitted = false; + + if(event.selectedIndex != 0) { + this.selectedSeatsService.setSeatIsSelectableFalse() + } else { + this.selectedSeatsService.setSeatIsSelectableTrue() + } } - stupidCheckboxWorkaround() { + nextPhaseButtonClicked(stepper: MatStepper) { this.submitted = true; + if (this.dataForm.invalid) return; + + if (this.submissionMode === "reservation") { + this.makeReservation(); + } else if (this.submissionMode === "purchase") { + stepper.next(); + } } - performance = input(); - seatCategories = input.required(); - - loadingService = inject(LoadingService); - private selectedSeatsService = inject(SelectedSeatsService); totalPrice = computed(() => this.selectedSeatsService.getSelectedSeatsList().reduce((sum, seat) => sum + seat.row.category.price, 0) @@ -67,4 +87,138 @@ export class OrderComponent { return `${(price / 100).toFixed(2)} €`; } + isSubmitting: boolean = false; + secondPhaseButtonText: string = "Loading..." + submissionMode!: 'reservation' | 'purchase'; + + seatsReserved: boolean = false; + seatsPurchased: boolean = false; + successful: boolean = false; + + reservationClicked() { + this.submissionMode = "reservation"; + if (this.totalSeats() > 1) { + this.secondPhaseButtonText = "Sitzplätze reservieren" + } else { + this.secondPhaseButtonText = "Sitzplatz reservieren" + } + } + + purchaseClicked() { + this.submissionMode = "purchase" + this.secondPhaseButtonText = "Weiter zur Zahlung" + } + + makeReservation() { + this.loadingService.show(); + this.disableInputs() + + const order = this.generateNewOrderObject(this.dataForm.value.email, false); + const seats = this.selectedSeatsService.getSelectedSeatsList(); + const performance = this.performance()!; + this.successful = true; + this.sendToBackend(order, seats, performance); + this.seatsReserved = true; + } + + makePurchase() { + this.loadingService.show(); + this.disableInputs() + + const order = this.generateNewOrderObject(this.dataForm.value.email, true); + const seats = this.selectedSeatsService.getSelectedSeatsList(); + const performance = this.performance()!; + this.successful = true; + this.sendToBackend(order, seats, performance); + this.seatsPurchased = true; + } + + createdOrder!: Bestellung; + createdTickets!: Eintrittskarte[]; + + sendToBackend(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung) { + this.httpService.addOrder(order).pipe( + tap(createdOrder => { + this.createdOrder = createdOrder; + + const ticketCreations = seats.map(seat => { + const ticket = this.generateNewTicketObject(performance, seat, createdOrder); + return this.httpService.addTicket(ticket); + }); + + Promise.all(ticketCreations.map(obs => obs.toPromise())) + .then(createdTickets => { + this.createdTickets = createdTickets.filter( + (ticket): ticket is Eintrittskarte => ticket !== undefined + ); + + this.loadingService.hide(); + this.enableInputs(); + }) + .catch(err => { + this.loadingService.showError(err); + this.successful = false; + console.error('Fehler beim Anlegen der Eintrittskarten', err); + }); + }), + catchError(err => { + this.loadingService.showError(err); + this.successful = false; + console.error('Fehler beim Anlegen der Bestellung', err); + return []; + }), + finalize(() => { + this.enableInputs(); + }) + ).subscribe(); + } + + + + + private generateCode(length: number = 6): string { + const chars = "ABCDEFGHJKLMNPQRSUVWXYZ23456789"; + let result = ""; + + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * chars.length); + result += chars[randomIndex]; + } + + return result; + } + + private generateNewOrderObject(mail: string, isBooked: boolean): Bestellung { + return{ + id: 0, // Wird durch Backend gesetzt + mail: mail, + code: this.generateCode(length=6), + reserved: new Date(), + booked: isBooked ? new Date() : null, + cancelled: null, + }; + } + + private generateNewTicketObject(show: Vorstellung, seat: Sitzplatz, order: Bestellung): Eintrittskarte { + return { + id: 0, // Wird durch Backend gesetzt + code: 'T' + this.generateCode(length=7), + show: show, + seat: seat, + order: order + }; + } + + private disableInputs() { + this.dataForm.disable(); + this.paymentForm.disable(); + this.isSubmitting = true; + } + + private enableInputs() { + this.dataForm.enable(); + this.paymentForm.enable(); + this.isSubmitting = false; + } + } diff --git a/src/app/purchase-failed/purchase-failed.component.css b/src/app/purchase-failed/purchase-failed.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/purchase-failed/purchase-failed.component.html b/src/app/purchase-failed/purchase-failed.component.html new file mode 100644 index 0000000..564513a --- /dev/null +++ b/src/app/purchase-failed/purchase-failed.component.html @@ -0,0 +1,11 @@ +
+ + warning + +

Kauf fehlgeschlagen!

+

Leider konnten Ihre Sitzplätze nicht gebucht werden.
Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu kaufen.

+ + + +
+ diff --git a/src/app/purchase-failed/purchase-failed.component.ts b/src/app/purchase-failed/purchase-failed.component.ts new file mode 100644 index 0000000..619d3e9 --- /dev/null +++ b/src/app/purchase-failed/purchase-failed.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-purchase-failed', + standalone: false, + templateUrl: './purchase-failed.component.html', + styleUrl: './purchase-failed.component.css', +}) +export class PurchaseFailedComponent { + +} diff --git a/src/app/purchase-success/purchase-success.component.css b/src/app/purchase-success/purchase-success.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/purchase-success/purchase-success.component.html b/src/app/purchase-success/purchase-success.component.html new file mode 100644 index 0000000..4d40726 --- /dev/null +++ b/src/app/purchase-success/purchase-success.component.html @@ -0,0 +1,11 @@ +
+

Vielen Dank für Ihren Einkauf!

+

Ihre Sitzplätze wurden erfolgreich gebucht.

+ + + + + + +
+ diff --git a/src/app/purchase-success/purchase-success.component.ts b/src/app/purchase-success/purchase-success.component.ts new file mode 100644 index 0000000..8dad591 --- /dev/null +++ b/src/app/purchase-success/purchase-success.component.ts @@ -0,0 +1,12 @@ +import { Eintrittskarte } from '@infinimotion/model-frontend'; +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-purchase-success', + standalone: false, + templateUrl: './purchase-success.component.html', + styleUrl: './purchase-success.component.css', +}) +export class PurchaseSuccessComponent { + tickets = input.required(); +} diff --git a/src/app/reservation-failed/reservation-failed.component.css b/src/app/reservation-failed/reservation-failed.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/reservation-failed/reservation-failed.component.html b/src/app/reservation-failed/reservation-failed.component.html new file mode 100644 index 0000000..792a731 --- /dev/null +++ b/src/app/reservation-failed/reservation-failed.component.html @@ -0,0 +1,11 @@ +
+ + warning + +

Reservierung fehlgeschlagen!

+

Leider konnten Ihre Sitzplätze nicht reserviert werden.
Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.

+ + + +
+ diff --git a/src/app/reservation-failed/reservation-failed.component.ts b/src/app/reservation-failed/reservation-failed.component.ts new file mode 100644 index 0000000..7693d1e --- /dev/null +++ b/src/app/reservation-failed/reservation-failed.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-reservation-failed', + standalone: false, + templateUrl: './reservation-failed.component.html', + styleUrl: './reservation-failed.component.css', +}) +export class ReservationFailedComponent { + +} diff --git a/src/app/reservation-success/reservation-success.component.css b/src/app/reservation-success/reservation-success.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/reservation-success/reservation-success.component.html b/src/app/reservation-success/reservation-success.component.html new file mode 100644 index 0000000..0a463a0 --- /dev/null +++ b/src/app/reservation-success/reservation-success.component.html @@ -0,0 +1,17 @@ +
+

Reservierung erfolgreich!

+ + +

Ihre Sitzplätze wurden erfolgreich reserviert. Bitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln.

+
+ {{ order().code }} +
+ + + +
+ Reservierung stornieren +
+ +
+ diff --git a/src/app/reservation-success/reservation-success.component.ts b/src/app/reservation-success/reservation-success.component.ts new file mode 100644 index 0000000..8754ae6 --- /dev/null +++ b/src/app/reservation-success/reservation-success.component.ts @@ -0,0 +1,16 @@ +import { Bestellung } from '@infinimotion/model-frontend'; +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-reservation-success', + standalone: false, + templateUrl: './reservation-success.component.html', + styleUrl: './reservation-success.component.css', +}) +export class ReservationSuccessComponent { + order = input.required(); + + cancelReservation() { + // Logic to cancel the reservation + } +} diff --git a/src/app/seat/seat.component.css b/src/app/seat/seat.component.css index e69de29..964cf92 100644 --- a/src/app/seat/seat.component.css +++ b/src/app/seat/seat.component.css @@ -0,0 +1,15 @@ +@keyframes blink { + 0% { + color: #6366f1; + } + 50% { + color: #ffde05; + } + 100% { + color: #6366f1; + } +} + +.blink { + animation: blink 1s ease-in-out infinite; +} diff --git a/src/app/seat/seat.component.html b/src/app/seat/seat.component.html index f115ccc..57ca2a1 100644 --- a/src/app/seat/seat.component.html +++ b/src/app/seat/seat.component.html @@ -1,6 +1,11 @@ - diff --git a/src/app/seat/seat.component.ts b/src/app/seat/seat.component.ts index e029b90..06b3e18 100644 --- a/src/app/seat/seat.component.ts +++ b/src/app/seat/seat.component.ts @@ -19,17 +19,29 @@ export class SeatComponent{ protected readonly TheaterSeatState = TheaterSeatState; getSeatStateColor(): string { + if (!this.seatService.getSeatIsSelectable()) return 'gray' switch (this.state()) { case TheaterSeatState.RESERVED: - return 'orange'; + return '#d6c9a9'; case TheaterSeatState.BOOKED: - return 'red'; + return '#d9abab'; default: case TheaterSeatState.AVAILABLE: return 'black'; } } + isHoverable(): boolean { + switch (this.state()) { + default: + case TheaterSeatState.AVAILABLE: + return this.seatService.getSeatIsSelectable(); + case TheaterSeatState.RESERVED: + case TheaterSeatState.BOOKED: + return false; + } + } + updateSelectedSeats(selectedSeat: Sitzplatz) : void { if(!this.selected){ this.seatService.pushSelectedSeat(selectedSeat); diff --git a/src/app/selected-seats.service.ts b/src/app/selected-seats.service.ts index 329ea0b..5faed31 100644 --- a/src/app/selected-seats.service.ts +++ b/src/app/selected-seats.service.ts @@ -34,7 +34,7 @@ export class SelectedSeatsService { this.selectedSeatsSignal.set([]); } - getSeatIsSelected(): boolean{ + getSeatIsSelectable(): boolean{ return this.seatIsSelectable; } diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index 1fd7c20..dce8dcc 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -27,6 +27,7 @@ export class TheaterOverlayComponent implements OnInit { ngOnInit() { this.showId = Number(this.route.snapshot.paramMap.get('id')!); this.selectedSeatService.clearSelectedSeatsList(); + this.selectedSeatService.setSeatIsSelectableTrue(); this.loadPerformanceAndSeats(); } diff --git a/src/app/ticket-list/ticket-list.component.css b/src/app/ticket-list/ticket-list.component.css new file mode 100644 index 0000000..c86a2b9 --- /dev/null +++ b/src/app/ticket-list/ticket-list.component.css @@ -0,0 +1,41 @@ +.ticket-container { + max-height: 200px; + overflow-y: auto; + padding: 8px; + position: relative; +} + +.ticket-container::before, +.ticket-container::after { + content: ''; + position: sticky; + left: 0; + right: 0; + height: 20px; + pointer-events: none; + z-index: 2; +} + +.ticket-container::before { + top: 0; + background: linear-gradient(to bottom, rgba(0,0,0,0.1), transparent); +} + +.ticket-container::after { + bottom: 0; + background: linear-gradient(to top, rgba(0,0,0,0.1), transparent); +} + + +.ticket-container::-webkit-scrollbar { + width: 8px; +} + +.ticket-container::-webkit-scrollbar-track { + background: transparent; +} + +.ticket-container::-webkit-scrollbar-thumb { + background-color: rgba(0,0,0,0.2); + border-radius: 4px; +} diff --git a/src/app/ticket-list/ticket-list.component.html b/src/app/ticket-list/ticket-list.component.html new file mode 100644 index 0000000..a03fe3e --- /dev/null +++ b/src/app/ticket-list/ticket-list.component.html @@ -0,0 +1,9 @@ +
+ @for (ticket of tickets(); track $index) { + + @if ($index + 1 != tickets().length) { +
+ } + } +
+ diff --git a/src/app/ticket-list/ticket-list.component.ts b/src/app/ticket-list/ticket-list.component.ts new file mode 100644 index 0000000..434b9ab --- /dev/null +++ b/src/app/ticket-list/ticket-list.component.ts @@ -0,0 +1,12 @@ +import { Component, input } from '@angular/core'; +import { Eintrittskarte } from '@infinimotion/model-frontend'; + +@Component({ + selector: 'app-ticket-list', + standalone: false, + templateUrl: './ticket-list.component.html', + styleUrl: './ticket-list.component.css', +}) +export class TicketListComponent { + tickets = input.required(); +} diff --git a/src/app/ticket-small/ticket-small.component.css b/src/app/ticket-small/ticket-small.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/ticket-small/ticket-small.component.html b/src/app/ticket-small/ticket-small.component.html new file mode 100644 index 0000000..a0db66d --- /dev/null +++ b/src/app/ticket-small/ticket-small.component.html @@ -0,0 +1,16 @@ +
+ + local_activity + +
+

{{ticket().seat.row.category.name}} • Reihe {{convertIntoRowName(ticket().seat.row.position)}} Platz {{ticket().seat.position}}

+
+

Ticketcode:

+

+ {{ ticket().code }} +

+
+
+ +
+ diff --git a/src/app/ticket-small/ticket-small.component.ts b/src/app/ticket-small/ticket-small.component.ts new file mode 100644 index 0000000..30820c2 --- /dev/null +++ b/src/app/ticket-small/ticket-small.component.ts @@ -0,0 +1,16 @@ +import { Eintrittskarte } from '@infinimotion/model-frontend'; +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-ticket-small', + standalone: false, + templateUrl: './ticket-small.component.html', + styleUrl: './ticket-small.component.css', +}) +export class TicketSmallComponent { + ticket = input.required(); + + convertIntoRowName(n: number): string { + return String.fromCharCode(64 + n); + } +} diff --git a/src/custom-theme.scss b/src/custom-theme.scss index 640a19c..6eddae9 100644 --- a/src/custom-theme.scss +++ b/src/custom-theme.scss @@ -66,6 +66,47 @@ html.dark { color: white; } + + +.error-button { + @include mat.button-overrides(( + outlined-ripple-color: rgba(255, 0, 0, 0.1), + )); + + &.mdc-button:not(.mat-mdc-outlined-button):not(:disabled) { + background-color: var(--color-red-500) !important; + } + + &.mdc-button.mat-mdc-outlined-button:not(:disabled) { + color: var(--color-red-500) !important; + } + + &.mdc-button.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before { + background-color: var(--color-red-500) !important; + } +} + +.success-button { + @include mat.button-overrides(( + outlined-ripple-color: rgba(0, 255, 0, 0.1), + )); + + &.mdc-button:not(.mat-mdc-outlined-button):not(:disabled) { + background-color: var(--color-green-500) !important; + } + + &.mdc-button.mat-mdc-outlined-button:not(:disabled) { + color: var(--mat-sys-on-surface) !important; + } + + &.mdc-button.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before { + background-color: var(--color-green-500) !important; + } +} + + + + body { // Default the application to a light color theme. This can be changed to // `dark` to enable the dark color theme, or to `light dark` to defer to the