Add reservation and purchase functionality
Introduces ReservationSuccess, ReservationFailed, PurchaseSuccess, PurchaseFailed, TicketSmall, and TicketList components for handling and displaying reservation and purchase outcomes. Updates order flow logic in OrderComponent to support reservation and purchase states, disables/enables form inputs during submission, and integrates new UI feedback. Also adds angularx-qrcode dependency and updates @infinimotion/model-frontend version.
This commit is contained in:
273
package-lock.json
generated
273
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
Erscheinungsjahr: {{ movie().year }}
|
||||
</h2>
|
||||
|
||||
<button matFab extended class="mb-3" (click)="importMovie(movie().imdbID, movie().title)" [disabled]="this.buttonDisabled">
|
||||
<button matFab extended class="mb-3" (click)="importMovie(movie().imdbID!, movie().title!)" [disabled]="this.buttonDisabled">
|
||||
<mat-icon>{{ buttonIcon }}</mat-icon>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="w-full h-full relative">
|
||||
|
||||
@if (loadingService.loading$ | async){
|
||||
@if (!performance() && (loadingService.loading$ | async)){
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<mat-progress-spinner
|
||||
mode="indeterminate"
|
||||
@@ -17,6 +17,16 @@
|
||||
></app-performance-info>
|
||||
</div>
|
||||
|
||||
@if(isSubmitting) {
|
||||
<div class="absolute top-55 z-25 w-full px-6 my-auto">
|
||||
<mat-progress-spinner
|
||||
class="m-auto"
|
||||
mode="indeterminate"
|
||||
diameter="100"
|
||||
></mat-progress-spinner>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||
@@ -48,8 +58,8 @@
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button mat-button matButton="outlined" matStepperNext class="w-1/2" [disabled]="totalSeats()==0">Reservieren</button>
|
||||
<button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalSeats()==0">Buchen</button>
|
||||
<button mat-button matButton="outlined" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="reservationClicked()">Reservieren</button>
|
||||
<button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="purchaseClicked()">Kaufen</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
@@ -59,32 +69,45 @@
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
<!-- Name -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" placeholder="Max Mustermann" />
|
||||
@if (fData['name'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
@if (seatsReserved && !isSubmitting) {
|
||||
<div class="h-4"></div>
|
||||
@if (successful) {
|
||||
<app-reservation-success [order]="this.createdOrder"></app-reservation-success>
|
||||
} @else {
|
||||
<app-reservation-failed></app-reservation-failed>
|
||||
}
|
||||
}
|
||||
@else {
|
||||
|
||||
<!-- E-Mail -->
|
||||
<mat-form-field class="w-full mt-2">
|
||||
<mat-label>E-Mail Adresse</mat-label>
|
||||
<input matInput formControlName="email" placeholder="max.mustermann@edu.fhdw.de" />
|
||||
@if (fData['email'].hasError('email')) { <mat-error>Ungültige E-Mail-Adresse</mat-error> }
|
||||
</mat-form-field>
|
||||
<!-- Name -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" placeholder="Max Mustermann" />
|
||||
@if (fData['name'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div class="w-full my-4">
|
||||
<mat-checkbox required formControlName="accept" class="checkbox-invalid" [class]="{ 'checkbox-invalid': submitted && fData['accept'].hasError('required') }">
|
||||
Ich akzeptiere die AGB und die Datenbestimmung
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<!-- E-Mail -->
|
||||
<mat-form-field class="w-full mt-2">
|
||||
<mat-label>E-Mail Adresse</mat-label>
|
||||
<input matInput formControlName="email" placeholder="max.mustermann@edu.fhdw.de" />
|
||||
@if (fData['email'].hasError('email')) { <mat-error>Ungültige E-Mail-Adresse</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div class="w-full my-4">
|
||||
<mat-checkbox required formControlName="accept" class="checkbox-invalid" [class]="{ 'checkbox-invalid': submitted && fData['accept'].hasError('required') }">
|
||||
Ich akzeptiere die AGB und die Datenbestimmung
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button type="button" mat-button matButton="outlined" (click)="stepper.reset()" class="w-1/3" [disabled]="isSubmitting">Zurück</button>
|
||||
<button type="submit" mat-button matButton="filled" (click)="nextPhaseButtonClicked(stepper)" class="w-2/3" [disabled]="isSubmitting">{{ secondPhaseButtonText }}</button>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button type="button" mat-button matButton="outlined" (click)="stepper.reset()" class="w-1/3">Zurück</button>
|
||||
<button type="submit" mat-button matButton="filled" matStepperNext (click)="stupidCheckboxWorkaround()" class="w-2/3">Sitzplätze reservieren</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
<mat-step [stepControl]="paymentForm">
|
||||
@@ -93,66 +116,77 @@
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
<!-- Card Number -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Kartennummer</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="cardNumber"
|
||||
mask="0000 0000 0000 0000"
|
||||
placeholder="1111 2222 3333 4444"
|
||||
/>
|
||||
@if (fPayment['cardNumber'].hasError('pattern')) { <mat-error>Ungültige Kartennummer</mat-error> }
|
||||
@if (seatsPurchased && !isSubmitting) {
|
||||
<div class="h-4"></div>
|
||||
@if (successful) {
|
||||
<app-purchase-success [tickets]="createdTickets"></app-purchase-success>
|
||||
} @else {
|
||||
<app-purchase-failed></app-purchase-failed>
|
||||
}
|
||||
}
|
||||
@else {
|
||||
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Card Name -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Kartenname</mat-label>
|
||||
<input matInput formControlName="cardName" />
|
||||
@if (fPayment['cardName'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Expiry & CVV -->
|
||||
<div class="flex space-x-4">
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>Gültig bis (MM/YY)</mat-label>
|
||||
<!-- Card Number -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Kartennummer</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="expiry"
|
||||
mask="00/00"
|
||||
placeholder="MM/YY"
|
||||
[dropSpecialCharacters]="false"
|
||||
formControlName="cardNumber"
|
||||
mask="0000 0000 0000 0000"
|
||||
placeholder="1111 2222 3333 4444"
|
||||
/>
|
||||
@if (fPayment['expiry'].hasError('pattern')) { <mat-error>Ungültiges Format</mat-error> }
|
||||
@if (fPayment['cardNumber'].hasError('pattern')) { <mat-error>Ungültige Kartennummer</mat-error> }
|
||||
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>CVV</mat-label>
|
||||
<input matInput type="password" maxlength="4" formControlName="cvv" />
|
||||
@if (fPayment['cvv'].hasError('pattern')) { <mat-error>3–4 Ziffernt</mat-error> }
|
||||
<!-- Card Name -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Kartenname</mat-label>
|
||||
<input matInput formControlName="cardName" />
|
||||
@if (fPayment['cardName'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex w-full space-x-2 mt-2 items-center">
|
||||
<mat-icon class="material-symbols-outlined opacity-50" style="font-size: 32px; width: 32px; height: 32px">
|
||||
encrypted
|
||||
</mat-icon>
|
||||
<p class="text-sm opacity-75">
|
||||
Ihre Zahlung wird sicher über unsere Partner verarbeitet.<br>Wir speichern keine Zahlungsinformationen.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Expiry & CVV -->
|
||||
<div class="flex space-x-4">
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>Gültig bis (MM/YY)</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="expiry"
|
||||
mask="00/00"
|
||||
placeholder="MM/YY"
|
||||
[dropSpecialCharacters]="false"
|
||||
/>
|
||||
@if (fPayment['expiry'].hasError('pattern')) { <mat-error>Ungültiges Format</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-4 mt-8">
|
||||
<button mat-stroked-button color="primary" matStepperPrevious type="button" class="w-1/3">
|
||||
Zurück
|
||||
</button>
|
||||
<button mat-flat-button color="accent" class="w-2/3" matStepperNext type="submit">
|
||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||
</button>
|
||||
</div>
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>CVV</mat-label>
|
||||
<input matInput type="password" maxlength="4" formControlName="cvv" />
|
||||
@if (fPayment['cvv'].hasError('pattern')) { <mat-error>3–4 Ziffernt</mat-error> }
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex w-full space-x-2 mt-2 items-center">
|
||||
<mat-icon class="material-symbols-outlined opacity-50" style="font-size: 32px; width: 32px; height: 32px">
|
||||
encrypted
|
||||
</mat-icon>
|
||||
<p class="text-sm opacity-75">
|
||||
Ihre Zahlung wird sicher über unsere Partner verarbeitet.<br>Wir speichern keine Zahlungsinformationen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-4 mt-8">
|
||||
<button mat-stroked-button color="primary" matStepperPrevious type="button" [disabled]="isSubmitting" class="w-1/3">
|
||||
Zurück
|
||||
</button>
|
||||
<button mat-flat-button color="accent" matStepperNext type="submit" [disabled]="isSubmitting" (click)="makePurchase()" class="w-2/3">
|
||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
|
||||
@@ -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<Vorstellung>();
|
||||
seatCategories = input.required<Sitzkategorie[]>();
|
||||
|
||||
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<Vorstellung>();
|
||||
seatCategories = input.required<Sitzkategorie[]>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
11
src/app/purchase-failed/purchase-failed.component.html
Normal file
11
src/app/purchase-failed/purchase-failed.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="bg-red-100 text-red-500 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
|
||||
<mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px">
|
||||
warning
|
||||
</mat-icon>
|
||||
<h1 class="text-xl font-bold">Kauf fehlgeschlagen!</h1>
|
||||
<p class="text-center">Leider konnten Ihre Sitzplätze nicht gebucht werden.<br>Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu kaufen.</p>
|
||||
|
||||
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
|
||||
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80">Zurück zur Programmauswahl</button>
|
||||
</div>
|
||||
|
||||
11
src/app/purchase-failed/purchase-failed.component.ts
Normal file
11
src/app/purchase-failed/purchase-failed.component.ts
Normal file
@@ -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 {
|
||||
|
||||
}
|
||||
11
src/app/purchase-success/purchase-success.component.html
Normal file
11
src/app/purchase-success/purchase-success.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
|
||||
<h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1>
|
||||
<p class="text-center">Ihre Sitzplätze wurden erfolgreich gebucht.</p>
|
||||
|
||||
<app-ticket-list [tickets]="tickets()" class="w-8/10"></app-ticket-list>
|
||||
|
||||
<button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-2">Tickets herunterladen</button>
|
||||
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="success-button w-80">Zurück zur Programmauswahl</button>
|
||||
|
||||
</div>
|
||||
|
||||
12
src/app/purchase-success/purchase-success.component.ts
Normal file
12
src/app/purchase-success/purchase-success.component.ts
Normal file
@@ -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<Eintrittskarte[]>();
|
||||
}
|
||||
11
src/app/reservation-failed/reservation-failed.component.html
Normal file
11
src/app/reservation-failed/reservation-failed.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="bg-red-100 text-red-500 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
|
||||
<mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px">
|
||||
warning
|
||||
</mat-icon>
|
||||
<h1 class="text-xl font-bold">Reservierung fehlgeschlagen!</h1>
|
||||
<p class="text-center">Leider konnten Ihre Sitzplätze nicht reserviert werden.<br>Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.</p>
|
||||
|
||||
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
|
||||
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80">Zurück zur Programmauswahl</button>
|
||||
</div>
|
||||
|
||||
11
src/app/reservation-failed/reservation-failed.component.ts
Normal file
11
src/app/reservation-failed/reservation-failed.component.ts
Normal file
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
|
||||
<h1 class="text-xl font-bold">Reservierung erfolgreich!</h1>
|
||||
<!-- <p class="text-center">Ihre Sitzplätze wurden reserviert. </p> -->
|
||||
|
||||
<p class="text-center">Ihre Sitzplätze wurden erfolgreich reserviert. Bitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln.</p>
|
||||
<div class="bg-white text-5xl font-mono rounded-md shadow-sm w-fit h-fit p-4 py-2 my-4">
|
||||
<strong>{{ order().code }}</strong>
|
||||
</div>
|
||||
|
||||
<button routerLink="/schedule" mat-button matButton="filled" class="success-button mt-4 w-80">Zurück zur Programmauswahl</button>
|
||||
<button [disabled]="true" mat-button matButton="outlined" color="accent" class="success-button w-80">Tickets jetzt online bezahlen</button>
|
||||
<div class="text-green-500 cursor-pointer w-fit mt-1" (click)="cancelReservation()">
|
||||
Reservierung stornieren
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
16
src/app/reservation-success/reservation-success.component.ts
Normal file
16
src/app/reservation-success/reservation-success.component.ts
Normal file
@@ -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<Bestellung>();
|
||||
|
||||
cancelReservation() {
|
||||
// Logic to cancel the reservation
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
@keyframes blink {
|
||||
0% {
|
||||
color: #6366f1;
|
||||
}
|
||||
50% {
|
||||
color: #ffde05;
|
||||
}
|
||||
100% {
|
||||
color: #6366f1;
|
||||
}
|
||||
}
|
||||
|
||||
.blink {
|
||||
animation: blink 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<button (click)="updateSelectedSeats(this.seat())" [disabled]="state() == TheaterSeatState.BOOKED || state() == TheaterSeatState.RESERVED || !seatService.getSeatIsSelected()" class="mx-0.5 hover:opacity-50">
|
||||
<mat-icon [ngStyle]="{color: isSelectedAndAvaliable() ? '#6366f1': getSeatStateColor() }" style="font-size: 30px; width: 30px; height: 30px">
|
||||
<button (click)="updateSelectedSeats(this.seat())" [disabled]="state() == TheaterSeatState.BOOKED || state() == TheaterSeatState.RESERVED || !seatService.getSeatIsSelectable()" class="mx-0.5">
|
||||
<mat-icon
|
||||
[class]="isHoverable()? 'hover:opacity-50' : ''"
|
||||
[ngStyle]="{color: isSelectedAndAvaliable() ? '#6366f1': getSeatStateColor() }"
|
||||
[style]="!seatService.getSeatIsSelectable()? 'transition: color 0.5s ease, transform 0.3s ease-in-out;' : ''"
|
||||
style="font-size: 30px; width: 30px; height: 30px;">
|
||||
{{ seat().row.category.icon }}
|
||||
</mat-icon>
|
||||
<!-- [ngClass]="{'blink': isSelectedAndAvaliable() && !seatService.getSeatIsSelectable()}" -->
|
||||
</button>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -34,7 +34,7 @@ export class SelectedSeatsService {
|
||||
this.selectedSeatsSignal.set([]);
|
||||
}
|
||||
|
||||
getSeatIsSelected(): boolean{
|
||||
getSeatIsSelectable(): boolean{
|
||||
return this.seatIsSelectable;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
41
src/app/ticket-list/ticket-list.component.css
Normal file
41
src/app/ticket-list/ticket-list.component.css
Normal file
@@ -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;
|
||||
}
|
||||
9
src/app/ticket-list/ticket-list.component.html
Normal file
9
src/app/ticket-list/ticket-list.component.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="ticket-container">
|
||||
@for (ticket of tickets(); track $index) {
|
||||
<app-ticket-small [ticket]="ticket"></app-ticket-small>
|
||||
@if ($index + 1 != tickets().length) {
|
||||
<div class="h-2"></div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
12
src/app/ticket-list/ticket-list.component.ts
Normal file
12
src/app/ticket-list/ticket-list.component.ts
Normal file
@@ -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<Eintrittskarte[]>();
|
||||
}
|
||||
0
src/app/ticket-small/ticket-small.component.css
Normal file
0
src/app/ticket-small/ticket-small.component.css
Normal file
16
src/app/ticket-small/ticket-small.component.html
Normal file
16
src/app/ticket-small/ticket-small.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="bg-white rounded-md shadow-sm w-full h-fit p-1 flex space-x-1 justify-between items-center">
|
||||
<mat-icon class="opacity-50" style="font-size: 40px; width: 40px; height: 40px">
|
||||
local_activity
|
||||
</mat-icon>
|
||||
<div class="flex flex-col flex-1 text-sm leading-tight ml-1">
|
||||
<p>{{ticket().seat.row.category.name}} • Reihe {{convertIntoRowName(ticket().seat.row.position)}} Platz {{ticket().seat.position}}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<p>Ticketcode:</p>
|
||||
<p class="font-mono">
|
||||
<strong>{{ ticket().code }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<qrcode [qrdata]="ticket().code" [width]="42" [errorCorrectionLevel]="'M'"></qrcode>
|
||||
</div>
|
||||
|
||||
16
src/app/ticket-small/ticket-small.component.ts
Normal file
16
src/app/ticket-small/ticket-small.component.ts
Normal file
@@ -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<Eintrittskarte>();
|
||||
|
||||
convertIntoRowName(n: number): string {
|
||||
return String.fromCharCode(64 + n);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user