From 1f9c84ea365311f3e8e613b6a03549e2b415c517 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Fri, 28 Nov 2025 01:38:12 +0100 Subject: [PATCH] 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. --- package-lock.json | 71 ++++----- package.json | 1 + src/app/app-module.ts | 12 +- src/app/app-routing-module.ts | 2 +- src/app/auth.guard.ts | 2 +- src/app/navbar/navbar.component.ts | 4 +- .../pay-for-order/pay-for-order.component.ts | 8 +- .../purchase-success.component.html | 2 +- .../reservation-success.component.html | 2 +- .../ticket-small/ticket-small.component.html | 7 +- .../ticket-validation-result.component.css | 0 .../ticket-validation-result.component.html | 118 ++++++++++++++ .../ticket-validation-result.component.ts | 22 +++ .../ticket-validation.component.css | 7 + .../ticket-validation.component.html | 61 ++++++++ .../ticket-validation.component.ts | 147 ++++++++++++++++++ src/index.html | 1 + 17 files changed, 403 insertions(+), 64 deletions(-) create mode 100644 src/app/ticket-validation-result/ticket-validation-result.component.css create mode 100644 src/app/ticket-validation-result/ticket-validation-result.component.html create mode 100644 src/app/ticket-validation-result/ticket-validation-result.component.ts create mode 100644 src/app/ticket-validation/ticket-validation.component.css create mode 100644 src/app/ticket-validation/ticket-validation.component.html create mode 100644 src/app/ticket-validation/ticket-validation.component.ts diff --git a/package-lock.json b/package-lock.json index 8cc2069..eba7cf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "html2canvas": "^1.4.1", "jspdf": "^3.0.4", "ngx-mask": "^20.0.3", + "ngx-scanner-qrcode": "^1.7.6", "postcss": "^8.5.6", "rxjs": "~7.8.0", "tailwindcss": "^4.1.14", @@ -446,7 +447,6 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.14.tgz", "integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==", "license": "MIT", - "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -497,7 +497,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.13.tgz", "integrity": "sha512-Jy+Qu6760TZyiDJX0+fNzkc70+lwF9ojdkIyCso/Lvbx1v3Fki0+9Wui7Vge56hknkr05xXg1aEUeqMN0966Lg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -514,7 +513,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.13.tgz", "integrity": "sha512-YEjzHxz9laEcC2YPBA7L09Ys8UIuPrRiBZcGCrOXzXmPATHGYuxqYuhZ8iKmKV0PG/4pP2fxD3Mv5wN0cBaOWg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -528,7 +526,6 @@ "integrity": "sha512-Cou3G8C60eKpD93SKBJRG5pa/xpmMHe6sc2aanWjneGWjZq1kR4v5eQwwr8LUByIsafcqxHGT7+q1bYXT2p2DQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -561,7 +558,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.13.tgz", "integrity": "sha512-12Kou+WAIjAUSG5TkDbypV2kreJ105VylAjlQ09bCvsGNTHjezGgahFa/tLz7iyrozhuivtGiQtiDaYsc79ysw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -587,7 +583,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.13.tgz", "integrity": "sha512-9vu9MCHJtgXvgPH+ZgXN46N3gpBBAckcmG62P7U+9BKivWvv3rEvkgX+4HvO+Pm2D6x/Jy1xbiQuVq9EDGPSNA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -623,7 +618,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.13.tgz", "integrity": "sha512-KyJzzpD4jMPGotDgVHF0cz9psjlVg6wYQrhuWcLeE97VUvp+CdwdOJ9tlxDlGE5tYZ0JrQxAT0l5qdcr6K9iNQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -690,7 +684,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1656,7 +1649,6 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -3909,7 +3901,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4208,37 +4199,28 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "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": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/boolbase": { @@ -4292,7 +4274,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5473,7 +5454,6 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6473,8 +6453,7 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jiti": { "version": "2.6.1", @@ -6585,7 +6564,6 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -7302,7 +7280,6 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -7971,6 +7948,19 @@ "@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": { "version": "6.1.0", "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", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -9244,7 +9233,6 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -10128,8 +10116,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "3.1.0", @@ -10167,7 +10154,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10363,7 +10349,6 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -10766,7 +10751,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -10785,8 +10769,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT", - "peer": true + "license": "MIT" } } } diff --git a/package.json b/package.json index 87d9144..a5ba11d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "html2canvas": "^1.4.1", "jspdf": "^3.0.4", "ngx-mask": "^20.0.3", + "ngx-scanner-qrcode": "^1.7.6", "postcss": "^8.5.6", "rxjs": "~7.8.0", "tailwindcss": "^4.1.14", diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 6363aa9..e803c3e 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -27,7 +27,7 @@ import { MatBadgeModule } from '@angular/material/badge'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatPaginatorModule } from '@angular/material/paginator'; 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 { 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 { PdfTicketComponent } from './pdf-ticket/pdf-ticket.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({ @@ -133,6 +134,8 @@ import { TestComponent } from './test/test.component'; PricelistComponent, TheaterLayoutDesignerComponent, TestComponent, + TicketValidationComponent, + TicketValidationResultComponent, ], imports: [ AppRoutingModule, @@ -167,7 +170,7 @@ import { TestComponent } from './test/test.component'; MatTableModule, MatSelectModule, MatSortModule, - PdfTicketComponent + PdfTicketComponent, ], providers: [ provideBrowserGlobalErrorListeners(), @@ -178,4 +181,5 @@ import { TestComponent } from './test/test.component'; ], bootstrap: [App] }) -export class AppModule { } +export class AppModule { +} diff --git a/src/app/app-routing-module.ts b/src/app/app-routing-module.ts index 303391c..0ce4a72 100644 --- a/src/app/app-routing-module.ts +++ b/src/app/app-routing-module.ts @@ -60,7 +60,7 @@ const routes: Routes = [ path: 'employee/validation/ticket', component: TicketValidationComponent, canActivate: [AuthGuard], - data: { roles: ['employee'] }, + data: { roles: ['employee'], allowMobile: true }, }, { path: 'employee/validation/ticket/:ticketId', diff --git a/src/app/auth.guard.ts b/src/app/auth.guard.ts index 17f490b..c2af3a8 100644 --- a/src/app/auth.guard.ts +++ b/src/app/auth.guard.ts @@ -33,7 +33,7 @@ export class AuthGuard implements CanActivate { const dialogRef = this.dialog.open(LoginDialog, { disableClose: true, backdropClass: 'backdropBackground', - data: { user }, + data: { user, allowMobile: true }, }); const result = await firstValueFrom(dialogRef.afterClosed()); diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index fd0cffb..317c914 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -1,4 +1,4 @@ -import { AuthService, User, UserRole } from './../auth.service'; +import { AuthService, UserRole } from './../auth.service'; import { Component, inject, computed } from '@angular/core'; @Component({ @@ -13,9 +13,9 @@ export class NavbarComponent { { label: 'Preise', path: '/prices', auth: null }, { label: 'Bezahlen', path: '/checkout/order', auth: null }, { 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: 'Saal-Designer', path: '/admin/designer', auth: ['admin'] }, + { label: 'Film-Importer', path: '/admin/movie-importer', auth: ['admin']}, ]; private auth = inject(AuthService); diff --git a/src/app/pay-for-order/pay-for-order.component.ts b/src/app/pay-for-order/pay-for-order.component.ts index 9ae834f..0daaf82 100644 --- a/src/app/pay-for-order/pay-for-order.component.ts +++ b/src/app/pay-for-order/pay-for-order.component.ts @@ -88,14 +88,14 @@ export class PayForOrderComponent implements OnInit { return; } const order = orders[0]; - if (order.booked) { - this.formControl.setErrors({ alreadyBooked: true }); - return; - } if (order.cancelled) { this.formControl.setErrors({ cancelled: true }); return; } + if (order.booked) { + this.formControl.setErrors({ alreadyBooked: true }); + return; + } this.router.navigate(['/checkout/order', order.code]); }), catchError(err => { diff --git a/src/app/purchase-success/purchase-success.component.html b/src/app/purchase-success/purchase-success.component.html index 8e986e3..2423084 100644 --- a/src/app/purchase-success/purchase-success.component.html +++ b/src/app/purchase-success/purchase-success.component.html @@ -2,7 +2,7 @@

Vielen Dank für Ihren Einkauf!

{{ infoText }}

- + diff --git a/src/app/reservation-success/reservation-success.component.html b/src/app/reservation-success/reservation-success.component.html index 590241e..d84264d 100644 --- a/src/app/reservation-success/reservation-success.component.html +++ b/src/app/reservation-success/reservation-success.component.html @@ -12,7 +12,7 @@ -
+
Reservierung stornieren
diff --git a/src/app/ticket-small/ticket-small.component.html b/src/app/ticket-small/ticket-small.component.html index 8d9d732..8ad4428 100644 --- a/src/app/ticket-small/ticket-small.component.html +++ b/src/app/ticket-small/ticket-small.component.html @@ -6,12 +6,7 @@

{{ ticket().seat.row.category.name }} - @if (ticket().seat.row.category.name.length > 10) { -
- } - @else { - • - } Reihe + • Reihe {{ convertIntoRowName(ticket().seat.row.position) }} Platz {{ ticket().seat.position }} diff --git a/src/app/ticket-validation-result/ticket-validation-result.component.css b/src/app/ticket-validation-result/ticket-validation-result.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/ticket-validation-result/ticket-validation-result.component.html b/src/app/ticket-validation-result/ticket-validation-result.component.html new file mode 100644 index 0000000..67e8981 --- /dev/null +++ b/src/app/ticket-validation-result/ticket-validation-result.component.html @@ -0,0 +1,118 @@ +@switch (result()) { + + @case ('nothing') { +

+ qr_code_scanner +

Bitte Ticket scannen
oder Ticketcode eingeben

+
+ } + + @case ('invalid') { +
+ cancel +

Ticket ungültig!

+

Unter der angegebenen Ticketnummer konnte keine gültige Eintrittskarte gefunden werden.

+
+ } + + @case ('unpaid') { + @if (performance()) { + +
+ } +
+ cancel +

Ticket nicht bezahlt!

+

Die Bestellung wurde noch nicht bezahlt und der Sitzplatz befindet sich noch im Status 'reserviert'.

+ @if (ticket()) { + + } + @if (order()) { +
+ Ticket jetzt bezahlen +
+ } +
+ } + + @case ('expired') { + @if (performance()) { + +
+ } +
+ cancel +

Vorstellung beendet!

+

Die Filmvorführung hat bereits stattgefunden.

+ @if (ticket()) { + + } +
+ } + + @case ('valid') { + @if (performance()) { + +
+ } +
+ check_circle +

Ticket gültig!

+ @if (ticket()) { + + } +
+ } + + @case ('early') { + @if (performance()) { + +
+ } +
+ schedule +

Noch kein Einlass!

+

Die Vorstellung beginnt in mehr als zwei Stunden.

+ @if (ticket()) { + + } +
+ } + + @case ('cancelled') { + @if (performance()) { + +
+ } +
+ cancel +

Ticket storniert!

+ @if (order()) { +

+ Die Bestellung wurde am {{ cancelledDate }} storniert. +

+ } + @if (ticket()) { + + } +
+ } + + @case ('error') { +
+ error +

Überprüfung fehlgeschlagen!

+

Bei der Validierung des Tickets ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.

+
+ } + + @default { +
+ +
+ } +} + diff --git a/src/app/ticket-validation-result/ticket-validation-result.component.ts b/src/app/ticket-validation-result/ticket-validation-result.component.ts new file mode 100644 index 0000000..cd4074e --- /dev/null +++ b/src/app/ticket-validation-result/ticket-validation-result.component.ts @@ -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(); + performance = input(); + ticket = input(); + order = input(); + + get cancelledDate(): string { + if (!this.order()?.cancelled) return ''; + return new Date(this.order()?.cancelled!).toLocaleDateString(); + } +} diff --git a/src/app/ticket-validation/ticket-validation.component.css b/src/app/ticket-validation/ticket-validation.component.css new file mode 100644 index 0000000..dd13be1 --- /dev/null +++ b/src/app/ticket-validation/ticket-validation.component.css @@ -0,0 +1,7 @@ +.middle { + position: relative; + top: 45%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} diff --git a/src/app/ticket-validation/ticket-validation.component.html b/src/app/ticket-validation/ticket-validation.component.html new file mode 100644 index 0000000..62d71dd --- /dev/null +++ b/src/app/ticket-validation/ticket-validation.component.html @@ -0,0 +1,61 @@ + + +
+
+
+
+
+ + Ticketnummer eingeben + + + +
+ + + + +
+ +
+
+ +
+
+ +
+
+
diff --git a/src/app/ticket-validation/ticket-validation.component.ts b/src/app/ticket-validation/ticket-validation.component.ts new file mode 100644 index 0000000..76016df --- /dev/null +++ b/src/app/ticket-validation/ticket-validation.component.ts @@ -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(); + } +} diff --git a/src/index.html b/src/index.html index 2df19e0..6f0646b 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,7 @@ +