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:
2025-11-14 17:56:33 +01:00
parent f165a91e3c
commit 50cac8ac24
30 changed files with 821 additions and 107 deletions

273
package-lock.json generated
View File

@@ -16,8 +16,9 @@
"@angular/material": "^20.2.9", "@angular/material": "^20.2.9",
"@angular/platform-browser": "^20.3.0", "@angular/platform-browser": "^20.3.0",
"@angular/router": "^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", "@tailwindcss/postcss": "^4.1.14",
"angularx-qrcode": "^20.0.0",
"ngx-mask": "^20.0.3", "ngx-mask": "^20.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
@@ -1401,9 +1402,9 @@
} }
}, },
"node_modules/@infinimotion/model-frontend": { "node_modules/@infinimotion/model-frontend": {
"version": "0.0.89", "version": "0.0.102",
"resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.89/model-frontend-0.0.89.tgz", "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.102/model-frontend-0.0.102.tgz",
"integrity": "sha512-lvvQy8RWs41Bz52uBgsUKkwn8teGlgxlmG8Rvsgkh+v1IMVWFWVQmfMS7Rznd0lCZRgK1ByihH80X9eAN12idA==", "integrity": "sha512-NJV9bSBubdOZ1GBIe9To3o/hh6AZscJcTyaZY2nGmMxH+GhtvO1AHmjhrQeRjwAKFiwZMEwEg4ktFiOAp3MTMQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@inquirer/ansi": { "node_modules/@inquirer/ansi": {
@@ -3995,6 +3996,19 @@
"node": ">= 14.0.0" "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": { "node_modules/ansi-escapes": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
@@ -4387,6 +4401,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/caniuse-lite": {
"version": "1.0.30001754", "version": "1.0.30001754",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
@@ -4547,7 +4570,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
@@ -4560,7 +4582,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/colorette": { "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": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -4843,6 +4873,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/dom-serialize": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
@@ -5447,6 +5483,19 @@
"node": ">= 0.8" "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": { "node_modules/flatted": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
@@ -5586,7 +5635,6 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
@@ -7136,6 +7184,18 @@
"@lmdb/lmdb-win32-x64": "3.4.2" "@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": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -8159,6 +8219,33 @@
"license": "MIT", "license": "MIT",
"optional": true "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": { "node_modules/p-map": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
@@ -8172,6 +8259,15 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "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": ">= 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": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -8426,6 +8531,15 @@
"node": ">=16.20.0" "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": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -8516,6 +8630,125 @@
"node": ">=0.9" "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": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -8583,7 +8816,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -8599,6 +8831,12 @@
"node": ">=0.10.0" "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": { "node_modules/requires-port": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -8867,6 +9105,12 @@
"node": ">= 18" "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": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -10009,11 +10253,16 @@
"node": ">= 8" "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": { "node_modules/wrap-ansi": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
@@ -10118,7 +10367,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -10128,7 +10376,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
@@ -10144,14 +10391,12 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -10161,7 +10406,6 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
@@ -10176,7 +10420,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"

View File

@@ -30,8 +30,9 @@
"@angular/material": "^20.2.9", "@angular/material": "^20.2.9",
"@angular/platform-browser": "^20.3.0", "@angular/platform-browser": "^20.3.0",
"@angular/router": "^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", "@tailwindcss/postcss": "^4.1.14",
"angularx-qrcode": "^20.0.0",
"ngx-mask": "^20.0.3", "ngx-mask": "^20.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",

View File

@@ -7,6 +7,7 @@ import { AppRoutingModule } from './app-routing-module';
import { App } from './app'; import { App } from './app';
import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask';
import { QRCodeComponent } from 'angularx-qrcode';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs'; 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 { OrderComponent } from './order/order.component';
import { SeatSelectionComponent } from './seat-selection/seat-selection.component'; import { SeatSelectionComponent } from './seat-selection/seat-selection.component';
import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.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({ @NgModule({
@@ -92,6 +99,12 @@ import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.comp
OrderComponent, OrderComponent,
SeatSelectionComponent, SeatSelectionComponent,
NoSeatsInHallComponent, NoSeatsInHallComponent,
ReservationSuccessComponent,
ReservationFailedComponent,
PurchaseSuccessComponent,
PurchaseFailedComponent,
TicketSmallComponent,
TicketListComponent,
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,
@@ -119,6 +132,7 @@ import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.comp
MatStepperModule, MatStepperModule,
NgxMaskDirective, NgxMaskDirective,
NgxMaskPipe, NgxMaskPipe,
QRCodeComponent,
], ],
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),

View File

@@ -13,7 +13,7 @@
Erscheinungsjahr: {{ movie().year }} Erscheinungsjahr: {{ movie().year }}
</h2> </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> <mat-icon>{{ buttonIcon }}</mat-icon>
{{ buttonText }} {{ buttonText }}
</button> </button>

View File

@@ -1,6 +1,6 @@
<div class="w-full h-full relative"> <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"> <div class="w-full h-full flex items-center justify-center">
<mat-progress-spinner <mat-progress-spinner
mode="indeterminate" mode="indeterminate"
@@ -17,6 +17,16 @@
></app-performance-info> ></app-performance-info>
</div> </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-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
<mat-step> <mat-step>
<ng-template matStepLabel>Warenkorb</ng-template> <ng-template matStepLabel>Warenkorb</ng-template>
@@ -48,8 +58,8 @@
<!-- Buttons --> <!-- Buttons -->
<div class="flex space-x-5 mt-10"> <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="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">Buchen</button> <button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="purchaseClicked()">Kaufen</button>
</div> </div>
</mat-step> </mat-step>
@@ -59,6 +69,16 @@
<div class="performance-info-space"></div> <div class="performance-info-space"></div>
@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 {
<!-- Name --> <!-- Name -->
<mat-form-field class="w-full mt-8"> <mat-form-field class="w-full mt-8">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
@@ -82,9 +102,12 @@
<!-- Buttons --> <!-- Buttons -->
<div class="flex space-x-5 mt-10"> <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="button" mat-button matButton="outlined" (click)="stepper.reset()" class="w-1/3" [disabled]="isSubmitting">Zurück</button>
<button type="submit" mat-button matButton="filled" matStepperNext (click)="stupidCheckboxWorkaround()" class="w-2/3">Sitzplätze reservieren</button> <button type="submit" mat-button matButton="filled" (click)="nextPhaseButtonClicked(stepper)" class="w-2/3" [disabled]="isSubmitting">{{ secondPhaseButtonText }}</button>
</div> </div>
}
</form> </form>
</mat-step> </mat-step>
<mat-step [stepControl]="paymentForm"> <mat-step [stepControl]="paymentForm">
@@ -93,6 +116,16 @@
<div class="performance-info-space"></div> <div class="performance-info-space"></div>
@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 {
<!-- Card Number --> <!-- Card Number -->
<mat-form-field class="w-full mt-8"> <mat-form-field class="w-full mt-8">
<mat-label>Kartennummer</mat-label> <mat-label>Kartennummer</mat-label>
@@ -146,13 +179,14 @@
<!-- Buttons --> <!-- Buttons -->
<div class="flex space-x-4 mt-8"> <div class="flex space-x-4 mt-8">
<button mat-stroked-button color="primary" matStepperPrevious type="button" class="w-1/3"> <button mat-stroked-button color="primary" matStepperPrevious type="button" [disabled]="isSubmitting" class="w-1/3">
Zurück Zurück
</button> </button>
<button mat-flat-button color="accent" class="w-2/3" matStepperNext type="submit"> <button mat-flat-button color="accent" matStepperNext type="submit" [disabled]="isSubmitting" (click)="makePurchase()" class="w-2/3">
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen {{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
</button> </button>
</div> </div>
}
</form> </form>
</mat-step> </mat-step>

View File

@@ -1,9 +1,12 @@
import { SelectedSeatsService } from './../selected-seats.service'; import { SelectedSeatsService } from './../selected-seats.service';
import { LoadingService } from './../loading.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 { Component, computed, inject, input } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { HttpService } from '../http.service';
import { catchError, tap, finalize } from 'rxjs';
import { MatStepper } from '@angular/material/stepper';
@Component({ @Component({
selector: 'app-order', selector: 'app-order',
@@ -17,8 +20,17 @@ export class OrderComponent {
submitted = false; submitted = false;
performance = input<Vorstellung>();
seatCategories = input.required<Sitzkategorie[]>();
loadingService = inject(LoadingService);
private httpService = inject(HttpService)
private selectedSeatsService = inject(SelectedSeatsService);
constructor(private fb: FormBuilder) {} constructor(private fb: FormBuilder) {}
// Form-Validation
ngOnInit(): void { ngOnInit(): void {
this.paymentForm = this.fb.group({ this.paymentForm = this.fb.group({
cardNumber: ['', [Validators.required, Validators.pattern(/^\d{16}$/)]], cardNumber: ['', [Validators.required, Validators.pattern(/^\d{16}$/)]],
@@ -37,23 +49,31 @@ export class OrderComponent {
get fPayment() { return this.paymentForm.controls; } get fPayment() { return this.paymentForm.controls; }
onSubmit() { onSubmit() {
if (this.dataForm.invalid) return;
if (this.paymentForm.invalid) return; if (this.paymentForm.invalid) return;
console.log('Zahlungsdaten:', this.paymentForm.value);
} }
onStepChange(event: StepperSelectionEvent) { onStepChange(event: StepperSelectionEvent) {
this.submitted = false; this.submitted = false;
if(event.selectedIndex != 0) {
this.selectedSeatsService.setSeatIsSelectableFalse()
} else {
this.selectedSeatsService.setSeatIsSelectableTrue()
}
} }
stupidCheckboxWorkaround() { nextPhaseButtonClicked(stepper: MatStepper) {
this.submitted = true; 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(() => totalPrice = computed(() =>
this.selectedSeatsService.getSelectedSeatsList().reduce((sum, seat) => sum + seat.row.category.price, 0) this.selectedSeatsService.getSelectedSeatsList().reduce((sum, seat) => sum + seat.row.category.price, 0)
@@ -67,4 +87,138 @@ export class OrderComponent {
return `${(price / 100).toFixed(2)}`; 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;
}
} }

View 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>

View 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 {
}

View 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>

View 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[]>();
}

View 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>

View 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 {
}

View File

@@ -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>

View 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
}
}

View File

@@ -0,0 +1,15 @@
@keyframes blink {
0% {
color: #6366f1;
}
50% {
color: #ffde05;
}
100% {
color: #6366f1;
}
}
.blink {
animation: blink 1s ease-in-out infinite;
}

View File

@@ -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"> <button (click)="updateSelectedSeats(this.seat())" [disabled]="state() == TheaterSeatState.BOOKED || state() == TheaterSeatState.RESERVED || !seatService.getSeatIsSelectable()" class="mx-0.5">
<mat-icon [ngStyle]="{color: isSelectedAndAvaliable() ? '#6366f1': getSeatStateColor() }" style="font-size: 30px; width: 30px; height: 30px"> <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 }} {{ seat().row.category.icon }}
</mat-icon> </mat-icon>
<!-- [ngClass]="{'blink': isSelectedAndAvaliable() && !seatService.getSeatIsSelectable()}" -->
</button> </button>

View File

@@ -19,17 +19,29 @@ export class SeatComponent{
protected readonly TheaterSeatState = TheaterSeatState; protected readonly TheaterSeatState = TheaterSeatState;
getSeatStateColor(): string { getSeatStateColor(): string {
if (!this.seatService.getSeatIsSelectable()) return 'gray'
switch (this.state()) { switch (this.state()) {
case TheaterSeatState.RESERVED: case TheaterSeatState.RESERVED:
return 'orange'; return '#d6c9a9';
case TheaterSeatState.BOOKED: case TheaterSeatState.BOOKED:
return 'red'; return '#d9abab';
default: default:
case TheaterSeatState.AVAILABLE: case TheaterSeatState.AVAILABLE:
return 'black'; 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 { updateSelectedSeats(selectedSeat: Sitzplatz) : void {
if(!this.selected){ if(!this.selected){
this.seatService.pushSelectedSeat(selectedSeat); this.seatService.pushSelectedSeat(selectedSeat);

View File

@@ -34,7 +34,7 @@ export class SelectedSeatsService {
this.selectedSeatsSignal.set([]); this.selectedSeatsSignal.set([]);
} }
getSeatIsSelected(): boolean{ getSeatIsSelectable(): boolean{
return this.seatIsSelectable; return this.seatIsSelectable;
} }

View File

@@ -27,6 +27,7 @@ export class TheaterOverlayComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.showId = Number(this.route.snapshot.paramMap.get('id')!); this.showId = Number(this.route.snapshot.paramMap.get('id')!);
this.selectedSeatService.clearSelectedSeatsList(); this.selectedSeatService.clearSelectedSeatsList();
this.selectedSeatService.setSeatIsSelectableTrue();
this.loadPerformanceAndSeats(); this.loadPerformanceAndSeats();
} }

View 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;
}

View 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>

View 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[]>();
}

View 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>

View 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);
}
}

View File

@@ -66,6 +66,47 @@ html.dark {
color: white; 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 { body {
// Default the application to a light color theme. This can be changed to // 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 // `dark` to enable the dark color theme, or to `light dark` to defer to the