diff --git a/package-lock.json b/package-lock.json index c310f8b..8cc2069 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,8 @@ "@tailwindcss/postcss": "^4.1.14", "angularx-qrcode": "^20.0.0", "canvas-confetti": "^1.9.4", + "html2canvas": "^1.4.1", + "jspdf": "^3.0.4", "ngx-mask": "^20.0.3", "postcss": "^8.5.6", "rxjs": "~7.8.0", @@ -31,6 +33,7 @@ "@angular/build": "^20.3.5", "@angular/cli": "^20.3.5", "@angular/compiler-cli": "^20.3.0", + "@types/html2canvas": "^0.5.35", "@types/jasmine": "~5.1.0", "jasmine-core": "~5.9.0", "karma": "~6.4.0", @@ -902,6 +905,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -3864,6 +3876,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/html2canvas": { + "version": "0.5.35", + "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-0.5.35.tgz", + "integrity": "sha512-1A2dtWZbOIZ+rUK8jmAx1We/EiNV+5vScpphU3AF14Vby6COIazi/9StosrvlVCqlQegRhsEgZf7QYOuWbwuuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jquery": "*" + } + }, "node_modules/@types/jasmine": { "version": "5.1.13", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.13.tgz", @@ -3871,6 +3893,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jquery": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz", + "integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sizzle": "*" + } + }, "node_modules/@types/node": { "version": "24.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", @@ -3882,6 +3914,33 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", @@ -4086,6 +4145,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -4442,6 +4510,26 @@ "url": "https://www.paypal.me/kirilvatev" } }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -4745,6 +4833,18 @@ "node": ">=6.6.0" } }, + "node_modules/core-js": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -4774,6 +4874,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-select": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", @@ -4948,6 +5057,16 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -5429,6 +5548,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -5464,6 +5594,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5847,6 +5983,19 @@ "dev": true, "license": "MIT" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", @@ -6049,6 +6198,12 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -6407,6 +6562,23 @@ ], "license": "MIT" }, + "node_modules/jspdf": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", + "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -8359,6 +8531,12 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -8505,6 +8683,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8781,6 +8966,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8828,6 +9023,13 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8915,6 +9117,16 @@ "dev": true, "license": "MIT" }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -9535,6 +9747,16 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -9713,6 +9935,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwindcss": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", @@ -9833,6 +10065,15 @@ "dev": true, "license": "ISC" }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -10076,6 +10317,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 594376e..87d9144 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "@tailwindcss/postcss": "^4.1.14", "angularx-qrcode": "^20.0.0", "canvas-confetti": "^1.9.4", + "html2canvas": "^1.4.1", + "jspdf": "^3.0.4", "ngx-mask": "^20.0.3", "postcss": "^8.5.6", "rxjs": "~7.8.0", @@ -45,6 +47,7 @@ "@angular/build": "^20.3.5", "@angular/cli": "^20.3.5", "@angular/compiler-cli": "^20.3.0", + "@types/html2canvas": "^0.5.35", "@types/jasmine": "~5.1.0", "jasmine-core": "~5.9.0", "karma": "~6.4.0", diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 4003cf2..6363aa9 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -77,7 +77,8 @@ import { PayForOrderComponent } from './pay-for-order/pay-for-order.component'; import { CancelOrderDialog } from './cancel-order/cancel-order.dialog'; 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'; @@ -131,6 +132,7 @@ import { TheaterLayoutDesignerComponent } from './theater-layout-designer/theate CancelOrderDialog, PricelistComponent, TheaterLayoutDesignerComponent, + TestComponent, ], imports: [ AppRoutingModule, @@ -165,7 +167,8 @@ import { TheaterLayoutDesignerComponent } from './theater-layout-designer/theate MatTableModule, MatSelectModule, MatSortModule, - ], + PdfTicketComponent +], providers: [ provideBrowserGlobalErrorListeners(), provideHttpClient( diff --git a/src/app/app-routing-module.ts b/src/app/app-routing-module.ts index f775818..6c32e8a 100644 --- a/src/app/app-routing-module.ts +++ b/src/app/app-routing-module.ts @@ -12,11 +12,13 @@ import { PayForOrderComponent } from './pay-for-order/pay-for-order.component'; import { StatisticsComponent } from './statistics/statistics.component'; import { PricelistComponent } from './pricelist/pricelist.component'; import { TheaterLayoutDesignerComponent } from './theater-layout-designer/theater-layout-designer.component'; +import { TestComponent } from './test/test.component'; const routes: Routes = [ // Seiten ohne Layout { path: 'landing', component: HomeComponent }, - { path: 'poc-model', component: PocModelComponent, data: { allowMobile: true } }, + { path: 'poc-model', component: PocModelComponent, data: { allowMobile: true, roles: ['employee', 'admin'] }, canActivate: [AuthGuard] }, + { path: 'test', component: TestComponent, data: { allowMobile: true, roles: ['employee', 'admin'] }, canActivate: [AuthGuard] }, // Seiten mit MainLayout { diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index ab9e36b..f2a5d1f 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -158,7 +158,10 @@ Name des Besitzers - + @if (fPayment['cardName'].hasError('minlength')) { Mindestens 3 Zeichen } @@ -178,14 +181,19 @@ CVV - + @if (fPayment['cvv'].hasError('pattern')) { 3–4 Ziffernt }
- + encrypted

diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index 5ec96e5..a35ad2c 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -412,4 +412,13 @@ export class OrderComponent { this.orderState.set({ status: 'idle' }); this.cancelReservation(); } + + fakeFormFill() { + this.paymentForm.patchValue({ + cardNumber: '4111 1111 1111 1111', + cardName: 'Max Mustermann', + expiry: '12/30', + cvv: '123' + }); + } } diff --git a/src/app/pdf-ticket/pdf-ticket.component.css b/src/app/pdf-ticket/pdf-ticket.component.css new file mode 100644 index 0000000..2134760 --- /dev/null +++ b/src/app/pdf-ticket/pdf-ticket.component.css @@ -0,0 +1,117 @@ +.ticket { + width: 210mm; + height: 99mm; + background: white; + overflow: hidden; + font-family: 'Arial', sans-serif; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + margin-bottom: 20px; + position: relative; +} + +.ticket-header { + height: 15mm; + background: linear-gradient(to right, #6366f1, #db2777); + display: flex; + justify-content: center; + align-items: flex-start; + text-align: center; +} + +.ticket-header h1 { + margin: 0; + color: white; + font-size: 28px; + font-weight: bold; + letter-spacing: 2px; +} + +.ticket-body { + padding: 25px; +} + +.movie-title { + font-size: 26px; + font-weight: bold; + margin-bottom: 10px; + position: relative; + padding-bottom: 6px; +} + +.title-underline { + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 3px; + background: linear-gradient(to right, #6366f1, #db2777); + border-radius: 2px; +} + + +.ticket-flex { + padding-left: 1mm; + display: flex; + justify-content: space-between; +} + +.ticket-info { + display: flex; + justify-content: space-between; + gap: 30px; + margin-top: 15px; + margin-bottom: 5px; + width: 100%; +} + +.info-section { + flex: 1; +} + +.info-row { + display: flex; + margin-bottom: 5mm; + font-size: 14px; +} + +.info-row .label { + font-weight: bold; + color: #555; + min-width: 90px; +} + +.info-row .value { + color: #222; +} + +.info-row .value.price { + color: #6366f1; +} + + +.ticket-footer { + display: flex; + justify-content: space-between; + padding: 20px 25px; + align-items: center; + border-top: 1px dashed #ccc; +} + +.ticket-number { + font-size: 12px; + color: #666; + font-family: 'Courier New', monospace; +} + + +.perforation { + position: absolute; + right: 20%; + top: 0; + bottom: 0; + width: 1px; + background-image: linear-gradient(to bottom, #ccc 50%, transparent 50%); + background-size: 1px 8px; +} + + diff --git a/src/app/pdf-ticket/pdf-ticket.component.html b/src/app/pdf-ticket/pdf-ticket.component.html new file mode 100644 index 0000000..504295d --- /dev/null +++ b/src/app/pdf-ticket/pdf-ticket.component.html @@ -0,0 +1,63 @@ +

+ +
+

INFINIMOTION EINTRITTSKARTE

+
+ + +
+ +
+ {{ ticket.show.movie.title }} +
+
+ +
+ +
+
+
+ Datum: + {{ formatDate(ticket.show.start) }} +
+
+ Uhrzeit: + {{ formatTime(ticket.show.start) }} +
+
+ Preis: + {{ formatPrice(ticket.seat.row.category.price) }} +
+
+ +
+
+ Saal: + {{ ticket.show.hall.name }} +
+
+ Sitz: + {{ formatSeat(ticket.seat.row.position, ticket.seat.position) }} +
+
+ Tarif: + {{ ticket.seat.row.category.name }} +
+ +
+
+ + +
+ + +
+ + + +
+
diff --git a/src/app/pdf-ticket/pdf-ticket.component.ts b/src/app/pdf-ticket/pdf-ticket.component.ts new file mode 100644 index 0000000..62690b3 --- /dev/null +++ b/src/app/pdf-ticket/pdf-ticket.component.ts @@ -0,0 +1,62 @@ +import { Component, Input } from '@angular/core'; +import { Eintrittskarte } from '@infinimotion/model-frontend'; +import { QRCodeComponent } from "angularx-qrcode"; + +@Component({ + selector: 'app-pdf-ticket', + standalone: true, + templateUrl: './pdf-ticket.component.html', + styleUrl: './pdf-ticket.component.css', + imports: [QRCodeComponent], +}) +export class PdfTicketComponent { + @Input() ticket!: Eintrittskarte; + + formatDate(date: Date | string | number): string { + if (!date) return 'N/A'; + const d = new Date(date); + if (isNaN(d.getTime())) return 'N/A'; + + return new Intl.DateTimeFormat('de-DE', { + day: '2-digit', + month: 'long', + year: 'numeric' + }).format(d); + } + + formatTime(date: Date | string | number): string { + if (!date) return 'N/A'; + const d = new Date(date); + if (isNaN(d.getTime())) return 'N/A'; + + return new Intl.DateTimeFormat('de-DE', { + hour: '2-digit', + minute: '2-digit' + }).format(d) + ' Uhr'; + } + + formatPrice(price: number): string { + return `${(price / 100).toFixed(2)} €`; + } + + formatSeat(row: number, seat: number): string { + return "Reihe " + this.convertIntoRowName(row) + ", Platz " + seat; + } + + private convertIntoRowName(n: number): string { + return String.fromCharCode(64 + n); + } + + formatTicketNr(code: string): string { + return "Ticket-Nr: " + code; + } + + formatOrderNr(code: string): string { + return "Bestellungs-Nr: " + code; + } + + formatPerformanceNr(id: number): string { + return "Vorstellungs-Nr: " + id; + } + +} diff --git a/src/app/pdf.service.ts b/src/app/pdf.service.ts new file mode 100644 index 0000000..029540c --- /dev/null +++ b/src/app/pdf.service.ts @@ -0,0 +1,111 @@ +import { Injectable, ComponentRef, ViewContainerRef, ApplicationRef, createComponent, EnvironmentInjector, inject, signal } from '@angular/core'; +import html2canvas from 'html2canvas'; +import jsPDF from 'jspdf'; +import { Eintrittskarte } from '@infinimotion/model-frontend'; + +@Injectable({ + providedIn: 'root', +}) +export class PdfService { + + private ticketsGreatedSignal = signal(0); + private totalTicketsSignal = signal(0); + + readonly ticketsGreated = this.ticketsGreatedSignal.asReadonly(); + readonly totalTickets = this.totalTicketsSignal.asReadonly(); + + appRef = inject(ApplicationRef); + injector = inject(EnvironmentInjector); + + async genTicket(tickets: Eintrittskarte[], ticketComponent: any): Promise { + if (tickets.length === 0) { + throw new Error('Keine Tickets zum Generieren vorhanden'); + } + + this.ticketsGreatedSignal.set(0) + this.totalTicketsSignal.set(tickets.length) + + // Container für temporäres Rendering erstellen + const container = document.createElement('div'); + container.style.position = 'fixed'; + container.style.left = '-9999px'; + container.style.top = '0'; + document.body.appendChild(container); + + const componentRefs: ComponentRef[] = []; + + try { + // Ticket-Format: 210mm x 99mm + const ticketWidthMM = 210; + const ticketHeightMM = 99; + + const pdf = new jsPDF({ + orientation: 'landscape', + unit: 'mm', + format: [ticketWidthMM, ticketHeightMM] + }); + + for (let i = 0; i < tickets.length; i++) { + const ticket = tickets[i]; + + // Komponente dynamisch erstellen + const componentRef = createComponent(ticketComponent, { + environmentInjector: this.injector + }); + + // Ticket-Daten an die Komponente übergeben + (componentRef.instance as any).ticket = ticket; + + // Komponente ins DOM einfügen + this.appRef.attachView(componentRef.hostView); + container.appendChild(componentRef.location.nativeElement); + componentRefs.push(componentRef); + + // Change Detection triggern + componentRef.changeDetectorRef.detectChanges(); + + // Kurz warten, damit alles gerendert ist + await new Promise(requestAnimationFrame); + + // HTML zu Canvas konvertieren + const canvas = await html2canvas(componentRef.location.nativeElement, { + scale: 2, + backgroundColor: '#ffffff', + logging: false, + useCORS: true + }); + + const imgData = canvas.toDataURL('image/png'); + + if (i > 0) { + pdf.addPage([ticketWidthMM, ticketHeightMM], 'landscape'); + } + + // Bild ins PDF einfügen + pdf.addImage(imgData, 'PNG', 0, 0, ticketWidthMM, ticketHeightMM); + + this.ticketsGreatedSignal.set(this.ticketsGreatedSignal() + 1) + } + + const fileName = this.generateFileName(tickets); + pdf.save(fileName); + + } catch (error) { + console.error('Fehler beim Generieren des PDFs:', error); + throw new Error('Das PDF konnte nicht erstellt werden. Bitte versuche es erneut.'); + } finally { + componentRefs.forEach(ref => { + this.appRef.detachView(ref.hostView); + ref.destroy(); + }); + document.body.removeChild(container); + } + } + + private generateFileName(tickets: Eintrittskarte[]): string { + const orderCode = tickets[0].order.code; + const timestamp = new Date().getTime(); + + return `Ticket_${orderCode}_${timestamp}.pdf`; + } +} diff --git a/src/app/purchase-success/purchase-success.component.html b/src/app/purchase-success/purchase-success.component.html index 68777c2..8e986e3 100644 --- a/src/app/purchase-success/purchase-success.component.html +++ b/src/app/purchase-success/purchase-success.component.html @@ -4,7 +4,7 @@ - +
diff --git a/src/app/purchase-success/purchase-success.component.ts b/src/app/purchase-success/purchase-success.component.ts index 3805ad4..d1e778d 100644 --- a/src/app/purchase-success/purchase-success.component.ts +++ b/src/app/purchase-success/purchase-success.component.ts @@ -1,5 +1,9 @@ import { Eintrittskarte } from '@infinimotion/model-frontend'; -import { Component, input } from '@angular/core'; +import { ChangeDetectorRef, Component, inject, input } from '@angular/core'; +import { PdfService } from '../pdf.service'; +import { PdfTicketComponent } from '../pdf-ticket/pdf-ticket.component'; +import { LoadingService } from '../loading.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-purchase-success', @@ -14,13 +18,51 @@ export class PurchaseSuccessComponent { infoText!: string; buttonText!: string; - ngOnInit(): void { - this.infoText = this.moreThanOne()? - 'Ihre Sitzplätze wurden erfolgreich gebucht.' : - 'Ihr Sitzplatz wurden erfolgreich gebucht.'; + isGenerating = false; - this.buttonText = this.moreThanOne()? - 'Tickets herunterladen' : - 'Ticket herunterladen'; + public pdfService = inject(PdfService); + private loadingService = inject(LoadingService); + + constructor(private snackBar: MatSnackBar) {} + + ngOnInit(): void { + this.infoText = this.moreThanOne() + ? 'Ihre Sitzplätze wurden erfolgreich gebucht.' + : 'Ihr Sitzplatz wurde erfolgreich gebucht.'; + this.buttonText = this.moreThanOne() + ? 'Tickets herunterladen' + : 'Ticket herunterladen'; + } + + async downloadTickets() { + this.loadingService.show(); + this.isGenerating = true; + + const message = "PDF wird erstellt. Dieser Prozess kann einige Sekunden dauern."; + this.snackBar.open(message, 'Schließen', { + duration: 10000, + horizontalPosition: 'right', + verticalPosition: 'top' + }); + + try { + await this.pdfService.genTicket(this.tickets(), PdfTicketComponent) + this.loadingService.hide(); + } catch (error) { + console.error('Fehler beim PDF erstellen:', error); + this.loadingService.showError('Es gab einen Fehler beim Erstellen des PDFs. Bitte versuche es erneut.'); + } finally { + this.isGenerating = false; + } + } + + getButtonText() { + if (this.isGenerating ) { + if (this.moreThanOne()) { + return "Tickets werden erstellt... " + this.pdfService.ticketsGreated() + "/" + this.pdfService.totalTickets(); + } + return "Ticket wird erstellt..."; + } + return this.buttonText; } } diff --git a/src/app/test/test.component.css b/src/app/test/test.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/test/test.component.html b/src/app/test/test.component.html new file mode 100644 index 0000000..e69de29 diff --git a/src/app/test/test.component.ts b/src/app/test/test.component.ts new file mode 100644 index 0000000..e1c512d --- /dev/null +++ b/src/app/test/test.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-test', + standalone: false, + templateUrl: './test.component.html', + styleUrl: './test.component.css', +}) +export class TestComponent { + +}