Add ticket validation feature for employees

Introduces ticket validation and result components for employee access control, including UI for scanning and manual code entry. Updates routing, guards, and navigation for mobile support and improves ticket status handling. Also adds ngx-scanner-qrcode dependency and minor UI fixes.
This commit is contained in:
2025-11-28 01:38:12 +01:00
parent 9e2e5c5a1d
commit 1f9c84ea36
17 changed files with 403 additions and 64 deletions

71
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^3.0.4", "jspdf": "^3.0.4",
"ngx-mask": "^20.0.3", "ngx-mask": "^20.0.3",
"ngx-scanner-qrcode": "^1.7.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
@@ -446,7 +447,6 @@
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.14.tgz", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.14.tgz",
"integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==", "integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"parse5": "^8.0.0", "parse5": "^8.0.0",
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -497,7 +497,6 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.13.tgz", "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.13.tgz",
"integrity": "sha512-Jy+Qu6760TZyiDJX0+fNzkc70+lwF9ojdkIyCso/Lvbx1v3Fki0+9Wui7Vge56hknkr05xXg1aEUeqMN0966Lg==", "integrity": "sha512-Jy+Qu6760TZyiDJX0+fNzkc70+lwF9ojdkIyCso/Lvbx1v3Fki0+9Wui7Vge56hknkr05xXg1aEUeqMN0966Lg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -514,7 +513,6 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.13.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.13.tgz",
"integrity": "sha512-YEjzHxz9laEcC2YPBA7L09Ys8UIuPrRiBZcGCrOXzXmPATHGYuxqYuhZ8iKmKV0PG/4pP2fxD3Mv5wN0cBaOWg==", "integrity": "sha512-YEjzHxz9laEcC2YPBA7L09Ys8UIuPrRiBZcGCrOXzXmPATHGYuxqYuhZ8iKmKV0PG/4pP2fxD3Mv5wN0cBaOWg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -528,7 +526,6 @@
"integrity": "sha512-Cou3G8C60eKpD93SKBJRG5pa/xpmMHe6sc2aanWjneGWjZq1kR4v5eQwwr8LUByIsafcqxHGT7+q1bYXT2p2DQ==", "integrity": "sha512-Cou3G8C60eKpD93SKBJRG5pa/xpmMHe6sc2aanWjneGWjZq1kR4v5eQwwr8LUByIsafcqxHGT7+q1bYXT2p2DQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/core": "7.28.3", "@babel/core": "7.28.3",
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.14",
@@ -561,7 +558,6 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.13.tgz", "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.13.tgz",
"integrity": "sha512-12Kou+WAIjAUSG5TkDbypV2kreJ105VylAjlQ09bCvsGNTHjezGgahFa/tLz7iyrozhuivtGiQtiDaYsc79ysw==", "integrity": "sha512-12Kou+WAIjAUSG5TkDbypV2kreJ105VylAjlQ09bCvsGNTHjezGgahFa/tLz7iyrozhuivtGiQtiDaYsc79ysw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -587,7 +583,6 @@
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.13.tgz", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.13.tgz",
"integrity": "sha512-9vu9MCHJtgXvgPH+ZgXN46N3gpBBAckcmG62P7U+9BKivWvv3rEvkgX+4HvO+Pm2D6x/Jy1xbiQuVq9EDGPSNA==", "integrity": "sha512-9vu9MCHJtgXvgPH+ZgXN46N3gpBBAckcmG62P7U+9BKivWvv3rEvkgX+4HvO+Pm2D6x/Jy1xbiQuVq9EDGPSNA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -623,7 +618,6 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.13.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.13.tgz",
"integrity": "sha512-KyJzzpD4jMPGotDgVHF0cz9psjlVg6wYQrhuWcLeE97VUvp+CdwdOJ9tlxDlGE5tYZ0JrQxAT0l5qdcr6K9iNQ==", "integrity": "sha512-KyJzzpD4jMPGotDgVHF0cz9psjlVg6wYQrhuWcLeE97VUvp+CdwdOJ9tlxDlGE5tYZ0JrQxAT0l5qdcr6K9iNQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -690,7 +684,6 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@@ -1656,7 +1649,6 @@
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@inquirer/checkbox": "^4.2.1", "@inquirer/checkbox": "^4.2.1",
"@inquirer/confirm": "^5.1.14", "@inquirer/confirm": "^5.1.14",
@@ -3909,7 +3901,6 @@
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -4208,37 +4199,28 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "^3.1.2", "bytes": "^3.1.2",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"debug": "^4.4.0", "debug": "^4.4.3",
"http-errors": "^2.0.0", "http-errors": "^2.0.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.7.0",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"qs": "^6.14.0", "qs": "^6.14.0",
"raw-body": "^3.0.0", "raw-body": "^3.0.1",
"type-is": "^2.0.0" "type-is": "^2.0.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}
},
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}, },
"engines": { "funding": {
"node": ">=0.10.0" "type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/boolbase": { "node_modules/boolbase": {
@@ -4292,7 +4274,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.25", "baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754", "caniuse-lite": "^1.0.30001754",
@@ -5473,7 +5454,6 @@
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
@@ -6473,8 +6453,7 @@
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz",
"integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.6.1", "version": "2.6.1",
@@ -6585,7 +6564,6 @@
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "1.5.0", "@colors/colors": "1.5.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
@@ -7302,7 +7280,6 @@
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cli-truncate": "^4.0.0", "cli-truncate": "^4.0.0",
"colorette": "^2.0.20", "colorette": "^2.0.20",
@@ -7971,6 +7948,19 @@
"@angular/forms": ">=14.0.0" "@angular/forms": ">=14.0.0"
} }
}, },
"node_modules/ngx-scanner-qrcode": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/ngx-scanner-qrcode/-/ngx-scanner-qrcode-1.7.6.tgz",
"integrity": "sha512-4AcRh+ozX0Arf97Xr1OmYRJUngHZDuU6b5pb9jsmM1Y/cpZX3rbI6mBQjsev65bm4UgDe+7naRgiVY07+K+vtw==",
"license": "LGPL-2.1+",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0 || ^23.0.0 || ^24.0.0",
"@angular/core": "^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0 || ^23.0.0 || ^24.0.0"
}
},
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
@@ -9208,7 +9198,6 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
@@ -9244,7 +9233,6 @@
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^5.0.2", "immutable": "^5.0.2",
@@ -10128,8 +10116,7 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD", "license": "0BSD"
"peer": true
}, },
"node_modules/tuf-js": { "node_modules/tuf-js": {
"version": "3.1.0", "version": "3.1.0",
@@ -10167,7 +10154,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -10363,7 +10349,6 @@
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -10766,7 +10751,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -10785,8 +10769,7 @@
"version": "0.15.1", "version": "0.15.1",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
"license": "MIT", "license": "MIT"
"peer": true
} }
} }
} }

View File

@@ -37,6 +37,7 @@
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^3.0.4", "jspdf": "^3.0.4",
"ngx-mask": "^20.0.3", "ngx-mask": "^20.0.3",
"ngx-scanner-qrcode": "^1.7.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",

View File

@@ -27,7 +27,7 @@ import { MatBadgeModule } from '@angular/material/badge';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import {MatSelectModule} from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { HeaderComponent } from './header/header.component'; import { HeaderComponent } from './header/header.component';
@@ -79,7 +79,8 @@ import { PricelistComponent } from './pricelist/pricelist.component';
import { TheaterLayoutDesignerComponent } from './theater-layout-designer/theater-layout-designer.component'; import { TheaterLayoutDesignerComponent } from './theater-layout-designer/theater-layout-designer.component';
import { PdfTicketComponent } from './pdf-ticket/pdf-ticket.component'; import { PdfTicketComponent } from './pdf-ticket/pdf-ticket.component';
import { TestComponent } from './test/test.component'; import { TestComponent } from './test/test.component';
import { TicketValidationComponent } from './ticket-validation/ticket-validation.component';
import { TicketValidationResultComponent } from './ticket-validation-result/ticket-validation-result.component';
@NgModule({ @NgModule({
@@ -133,6 +134,8 @@ import { TestComponent } from './test/test.component';
PricelistComponent, PricelistComponent,
TheaterLayoutDesignerComponent, TheaterLayoutDesignerComponent,
TestComponent, TestComponent,
TicketValidationComponent,
TicketValidationResultComponent,
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,
@@ -167,7 +170,7 @@ import { TestComponent } from './test/test.component';
MatTableModule, MatTableModule,
MatSelectModule, MatSelectModule,
MatSortModule, MatSortModule,
PdfTicketComponent PdfTicketComponent,
], ],
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
@@ -178,4 +181,5 @@ import { TestComponent } from './test/test.component';
], ],
bootstrap: [App] bootstrap: [App]
}) })
export class AppModule { } export class AppModule {
}

View File

@@ -60,7 +60,7 @@ const routes: Routes = [
path: 'employee/validation/ticket', path: 'employee/validation/ticket',
component: TicketValidationComponent, component: TicketValidationComponent,
canActivate: [AuthGuard], canActivate: [AuthGuard],
data: { roles: ['employee'] }, data: { roles: ['employee'], allowMobile: true },
}, },
{ {
path: 'employee/validation/ticket/:ticketId', path: 'employee/validation/ticket/:ticketId',

View File

@@ -33,7 +33,7 @@ export class AuthGuard implements CanActivate {
const dialogRef = this.dialog.open(LoginDialog, { const dialogRef = this.dialog.open(LoginDialog, {
disableClose: true, disableClose: true,
backdropClass: 'backdropBackground', backdropClass: 'backdropBackground',
data: { user }, data: { user, allowMobile: true },
}); });
const result = await firstValueFrom(dialogRef.afterClosed()); const result = await firstValueFrom(dialogRef.afterClosed());

View File

@@ -1,4 +1,4 @@
import { AuthService, User, UserRole } from './../auth.service'; import { AuthService, UserRole } from './../auth.service';
import { Component, inject, computed } from '@angular/core'; import { Component, inject, computed } from '@angular/core';
@Component({ @Component({
@@ -13,9 +13,9 @@ export class NavbarComponent {
{ label: 'Preise', path: '/prices', auth: null }, { label: 'Preise', path: '/prices', auth: null },
{ label: 'Bezahlen', path: '/checkout/order', auth: null }, { label: 'Bezahlen', path: '/checkout/order', auth: null },
{ label: 'Einlasskontrolle', path: '/employee/validation/ticket', auth: ['employee'] }, { label: 'Einlasskontrolle', path: '/employee/validation/ticket', auth: ['employee'] },
{ label: 'Film importieren', path: '/admin/movie-importer', auth: ['admin']},
{ label: 'Statistiken', path: '/admin/statistics', auth: ['admin'] }, { label: 'Statistiken', path: '/admin/statistics', auth: ['admin'] },
{ label: 'Saal-Designer', path: '/admin/designer', auth: ['admin'] }, { label: 'Saal-Designer', path: '/admin/designer', auth: ['admin'] },
{ label: 'Film-Importer', path: '/admin/movie-importer', auth: ['admin']},
]; ];
private auth = inject(AuthService); private auth = inject(AuthService);

View File

@@ -88,14 +88,14 @@ export class PayForOrderComponent implements OnInit {
return; return;
} }
const order = orders[0]; const order = orders[0];
if (order.booked) {
this.formControl.setErrors({ alreadyBooked: true });
return;
}
if (order.cancelled) { if (order.cancelled) {
this.formControl.setErrors({ cancelled: true }); this.formControl.setErrors({ cancelled: true });
return; return;
} }
if (order.booked) {
this.formControl.setErrors({ alreadyBooked: true });
return;
}
this.router.navigate(['/checkout/order', order.code]); this.router.navigate(['/checkout/order', order.code]);
}), }),
catchError(err => { catchError(err => {

View File

@@ -2,7 +2,7 @@
<h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1> <h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1>
<p class="text-center">{{ infoText }}</p> <p class="text-center">{{ infoText }}</p>
<app-ticket-list [tickets]="tickets()" class="w-8/10 my-4"></app-ticket-list> <app-ticket-list [tickets]="tickets()" class="w-fit my-4"></app-ticket-list>
<button mat-button type="button" [disabled]="isGenerating" matButton="filled" class="success-button w-80 mt-4" (click)="downloadTickets()">{{ getButtonText() }}</button> <button mat-button type="button" [disabled]="isGenerating" matButton="filled" class="success-button w-80 mt-4" (click)="downloadTickets()">{{ getButtonText() }}</button>
<button routerLink="/schedule" type="button" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zur Programmauswahl</button> <button routerLink="/schedule" type="button" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zur Programmauswahl</button>

View File

@@ -12,7 +12,7 @@
<button routerLink="/checkout/order/{{ order().code }}" type="button" mat-button matButton="filled" color="accent" class="success-button mt-2 w-80">{{ buttonText }}</button> <button routerLink="/checkout/order/{{ order().code }}" type="button" mat-button matButton="filled" color="accent" class="success-button mt-2 w-80">{{ buttonText }}</button>
<button routerLink="/schedule" type="button" mat-button matButton="outlined" class="success-button mb-4 w-80 mt-1">Zurück zur Programmauswahl</button> <button routerLink="/schedule" type="button" mat-button matButton="outlined" class="success-button mb-4 w-80 mt-1">Zurück zur Programmauswahl</button>
<div [routerLink]="['/checkout/order', order().code]" [queryParams]="{ action: 'cancel' }" class="text-green-500 cursor-pointer w-fit mt-2"> <div [routerLink]="['/checkout/order', order().code]" [queryParams]="{ action: 'cancel' }" class="text-green-500 cursor-pointer w-fit mt-2">
Reservierung stornieren Reservierung stornieren
</div> </div>

View File

@@ -6,12 +6,7 @@
<div class="flex flex-col flex-1 text-sm leading-tight ml-1"> <div class="flex flex-col flex-1 text-sm leading-tight ml-1">
<p> <p>
{{ ticket().seat.row.category.name }} {{ ticket().seat.row.category.name }}
@if (ticket().seat.row.category.name.length > 10) { • Reihe
<br>
}
@else {
} Reihe
<span class="font-mono"><strong>{{ convertIntoRowName(ticket().seat.row.position) }}</strong></span> <span class="font-mono"><strong>{{ convertIntoRowName(ticket().seat.row.position) }}</strong></span>
Platz Platz
<span class="font-mono"><strong>{{ ticket().seat.position }}</strong></span> <span class="font-mono"><strong>{{ ticket().seat.position }}</strong></span>

View File

@@ -0,0 +1,118 @@
@switch (result()) {
@case ('nothing') {
<div class="w-full h-full p-6 py-8 items-center justify-center flex flex-col space-y-2 text-center">
<mat-icon class="mb-4 material-icons-outlined" style="font-size: 100px; width: 100px; height: 100px;">qr_code_scanner</mat-icon>
<h1 class="text-xl font-bold">Bitte Ticket scannen<br>oder Ticketcode eingeben</h1>
</div>
}
@case ('invalid') {
<div class="bg-red-200 text-red-600 rounded-md shadow-sm w-full h-full p-6 py-8 items-center justify-center flex flex-col space-y-2 text-center">
<mat-icon class="mb-4 material-icons-outlined" style="font-size: 100px; width: 100px; height: 100px;">cancel</mat-icon>
<h1 class="text-2xl font-bold">Ticket ungültig!</h1>
<p class="text-center">Unter der angegebenen Ticketnummer konnte keine gültige Eintrittskarte gefunden werden.</p>
</div>
}
@case ('unpaid') {
@if (performance()) {
<app-performance-info [performance]="performance()!" class="w-full"></app-performance-info>
<div class="h-3"></div>
}
<div class="bg-orange-200 text-orange-600 rounded-md shadow-sm w-full h-full p-6 py-8 items-center justify-center flex flex-col space-y-2">
<mat-icon class="mb-4 material-icons-outlined" style="font-size: 100px; width: 100px; height: 100px;">cancel</mat-icon>
<h1 class="text-2xl font-bold">Ticket nicht bezahlt!</h1>
<p class="text-center">Die Bestellung wurde noch nicht bezahlt und der Sitzplatz befindet sich noch im Status 'reserviert'.</p>
@if (ticket()) {
<app-ticket-small [ticket]="ticket()!" class="w-fit my-5"></app-ticket-small>
}
@if (order()) {
<div [routerLink]="['/checkout/order', order()?.code]" class="cursor-pointer w-fit my-2">
Ticket jetzt bezahlen
</div>
}
</div>
}
@case ('expired') {
@if (performance()) {
<app-performance-info [performance]="performance()!" class="w-full"></app-performance-info>
<div class="h-3"></div>
}
<div class="bg-red-200 text-red-600 rounded-md shadow-sm w-full h-full p-6 py-8 items-center justify-center flex flex-col space-y-2">
<mat-icon class="mb-4 material-icons-outlined" style="font-size: 100px; width: 100px; height: 100px;">cancel</mat-icon>
<h1 class="text-2xl font-bold">Vorstellung beendet!</h1>
<p class="text-center">Die Filmvorführung hat bereits stattgefunden.</p>
@if (ticket()) {
<app-ticket-small [ticket]="ticket()!" class="w-fit my-5"></app-ticket-small>
}
</div>
}
@case ('valid') {
@if (performance()) {
<app-performance-info [performance]="performance()!" class="w-full"></app-performance-info>
<div class="h-3"></div>
}
<div class="bg-green-200 text-green-600 rounded-md shadow-sm w-full h-full p-6 py-8 items-center justify-center flex flex-col space-y-2">
<mat-icon class="mb-4 material-icons-outlined" style="font-size: 100px; width: 100px; height: 100px;">check_circle</mat-icon>
<h1 class="text-2xl font-bold">Ticket gültig!</h1>
@if (ticket()) {
<app-ticket-small [ticket]="ticket()!" class="w-fit my-5"></app-ticket-small>
}
</div>
}
@case ('early') {
@if (performance()) {
<app-performance-info [performance]="performance()!" class="w-full"></app-performance-info>
<div class="h-3"></div>
}
<div class="bg-purple-200 text-purple-600 rounded-md shadow-sm w-full h-full p-6 py-8 items-center justify-center flex flex-col space-y-2">
<mat-icon class="mb-4 material-icons-outlined" style="font-size: 100px; width: 100px; height: 100px;">schedule</mat-icon>
<h1 class="text-2xl font-bold">Noch kein Einlass!</h1>
<p class="text-center">Die Vorstellung beginnt in mehr als zwei Stunden.</p>
@if (ticket()) {
<app-ticket-small [ticket]="ticket()!" class="w-fit my-5"></app-ticket-small>
}
</div>
}
@case ('cancelled') {
@if (performance()) {
<app-performance-info [performance]="performance()!" class="w-full"></app-performance-info>
<div class="h-3"></div>
}
<div class="bg-red-200 text-red-600 rounded-md shadow-sm w-full h-full p-6 py-8 items-center justify-center flex flex-col space-y-2">
<mat-icon class="mb-4 material-icons-outlined" style="font-size: 100px; width: 100px; height: 100px;">cancel</mat-icon>
<h1 class="text-2xl font-bold">Ticket storniert!</h1>
@if (order()) {
<p class="text-center">
Die Bestellung wurde am {{ cancelledDate }} storniert.
</p>
}
@if (ticket()) {
<app-ticket-small [ticket]="ticket()!" class="w-fit my-5"></app-ticket-small>
}
</div>
}
@case ('error') {
<div class="bg-red-200 text-red-600 rounded-md shadow-sm w-full h-full p-6 py-8 items-center justify-center flex flex-col space-y-2">
<mat-icon class="mb-4 material-symbols-outlined" style="font-size: 100px; width: 100px; height: 100px;">error</mat-icon>
<h1 class="text-2xl font-bold">Überprüfung fehlgeschlagen!</h1>
<p class="text-center">Bei der Validierung des Tickets ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.</p>
</div>
}
@default {
<div class="w-full h-full flex items-center justify-center">
<mat-progress-spinner
mode="indeterminate"
diameter="75"
></mat-progress-spinner>
</div>
}
}

View File

@@ -0,0 +1,22 @@
import { Component, input } from '@angular/core';
import { Bestellung, Eintrittskarte, Vorstellung } from '@infinimotion/model-frontend';
export type ValidationResult = 'nothing' | 'invalid' | 'unpaid' | 'expired' | 'valid' | 'early' | 'cancelled' | 'loading' | 'error';
@Component({
selector: 'app-ticket-validation-result',
standalone: false,
templateUrl: './ticket-validation-result.component.html',
styleUrl: './ticket-validation-result.component.css',
})
export class TicketValidationResultComponent {
result = input.required<ValidationResult>();
performance = input<Vorstellung>();
ticket = input<Eintrittskarte>();
order = input<Bestellung>();
get cancelledDate(): string {
if (!this.order()?.cancelled) return '';
return new Date(this.order()?.cancelled!).toLocaleDateString();
}
}

View File

@@ -0,0 +1,7 @@
.middle {
position: relative;
top: 45%;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}

View File

@@ -0,0 +1,61 @@
<app-menu-header label="Einlasskontrolle: Ticketstatus prüfen" icon="security"></app-menu-header>
<div class="flex middle">
<div class="flex-1 flex justify-center my-auto p-20 pl-50">
<div class="w-full max-w-md">
<form class="ticket-search-form w-full" (ngSubmit)="DoSubmit()">
<div class="flex items-center space-x-4">
<mat-form-field class="w-full" subscriptSizing="dynamic">
<mat-label>Ticketnummer eingeben</mat-label>
<input class="w-full" type="text"
matInput
[formControl]="formControl"
(input)="onInput($event)"
[mask]="'TXXXXXXX'"
[patterns]="ticketPattern"
placeholder="TXXXXXXX"
maxlength="8"
autocomplete="off"
/>
<!-- <mat-error>
@if (formControl.hasError('invalid')) {
Ungültiger Ticketcode
}
@else if (formControl.hasError('required')) {
Bitte geben Sie den Ticketcode eingeben
}
@else if (formControl.hasError('severalTickets')) {
Mehrere Tickets gefunden - bitte kontaktieren Sie den Support
}
@else if (formControl.hasError('serverError')) {
Fehler beim Laden des Tickets
}
</mat-error> -->
</mat-form-field>
</div>
<!-- @if (formControl.valid || !formControl.touched) {
<div class="h-6"></div>
} -->
<button
mat-button
class="w-full mt-3"
matButton="filled"
color="accent"
[disabled]="(loadingService.loading$ | async) || (formControl.invalid && !formControl.hasError('serverError'))"
type="submit"
>
Ticket prüfen
</button>
</form>
</div>
</div>
<div class="flex-1 flex justify-center items-center p-20 pr-50">
<div class="w-full max-w-md">
<app-ticket-validation-result class="w-full h-100" [result]="result" [performance]="performance" [ticket]="ticket" [order]="order"></app-ticket-validation-result>
</div>
</div>
</div>

View File

@@ -0,0 +1,147 @@
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { HttpService } from '../http.service';
import { LoadingService } from '../loading.service';
import { catchError, map, Observable, of, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ValidationResult } from '../ticket-validation-result/ticket-validation-result.component';
import { Bestellung, Eintrittskarte, Vorstellung } from '@infinimotion/model-frontend';
@Component({
selector: 'app-ticket-validation',
standalone: false,
templateUrl: './ticket-validation.component.html',
styleUrl: './ticket-validation.component.css',
})
export class TicketValidationComponent implements OnInit {
private httpService = inject(HttpService);
private route = inject(ActivatedRoute);
private destroyRef = inject(DestroyRef);
public loadingService = inject(LoadingService);
queryError?: string;
result: ValidationResult = 'nothing';
performance?: Vorstellung;
ticket?: Eintrittskarte;
order?: Bestellung;
public ticketPattern = {
'X': { pattern: /[A-Za-z0-9]/ },
'T': { pattern: /[Tt]/ },
};
formControl = new FormControl('', {
validators: [
Validators.required,
Validators.minLength(8),
Validators.maxLength(8)
]
});
ngOnInit() {
const error = this.route.snapshot.queryParamMap.get('error');
const code = this.route.snapshot.queryParamMap.get('code');
if (code) {
this.formControl.setValue(code);
}
if (error) {
setTimeout(() => {
this.formControl.clearValidators();
this.formControl.setErrors({ [error]: true });
this.formControl.markAsTouched();
});
this.formControl.valueChanges.pipe(
take(1),
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.formControl.setValidators([
Validators.required,
Validators.minLength(8),
Validators.maxLength(8)
]);
this.formControl.updateValueAndValidity();
});
}
}
onInput(event: Event) {
this.queryError = undefined;
const input = event.target as HTMLInputElement;
const filtered = input.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
this.formControl.setValue(filtered, { emitEvent: false });
}
DoSubmit() {
this.formControl.markAsTouched();
if (this.formControl.invalid) return;
const code = this.formControl.value?.trim();
if (!code || code.length !== 8) return;
this.result = 'loading';
this.loadingService.show();
const ticketFilter = [`eq;code;string;${code}`];
this.httpService.getTicketsByFilter(ticketFilter).pipe(
map(tickets => {
this.loadingService.hide();
if (tickets.length === 0) {
this.result = 'invalid';
this.formControl.setErrors({ invalid: true });
return
}
if (tickets.length > 1) {
throw new Error("Für den Code existieren mehere Tickets");
// this.formControl.setErrors({ severalTickets: true });
return;
}
this.ticket = tickets[0];
this.order = this.ticket.order;
this.performance = this.ticket.show;
const now = new Date;
if (this.ticket.order.cancelled) {
this.result = 'cancelled';
return;
}
const showStart = new Date(this.ticket.show.start)
const showEnd = new Date(showStart.getTime() + this.ticket.show.movie.duration * 60 * 1000);
if (showEnd < now) {
this.result = 'expired';
return;
}
if (this.ticket.order.booked === null) {
this.result = 'unpaid';
return;
}
const twoHoursInMs = 2 * 60 * 60 * 1000;
const twoHoursBeforeShow = new Date(showStart.getTime() - twoHoursInMs);
if (now < twoHoursBeforeShow) {
this.result = 'early';
return;
}
this.result = 'valid';
}),
catchError(err => {
this.result = 'error';
this.loadingService.hide();
this.loadingService.showError(err);
// this.formControl.setErrors({ serverError: true });
console.log(err);
return of(null);
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();
}
}

View File

@@ -8,6 +8,7 @@
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Two+Tone|Material+Icons+Round|Material+Icons+Sharp" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
</head> </head>
<body> <body>