Add PDF ticket generation and download feature
Introduces PDF ticket generation using html2canvas and jsPDF, including a new PdfTicketComponent for ticket rendering and a PdfService for PDF creation. Updates purchase success flow to allow users to download tickets as PDFs, adds progress feedback, and includes a test route and component for development. Also refactors order form with a fake fill helper and improves UI details.
This commit is contained in:
250
package-lock.json
generated
250
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,6 +167,7 @@ import { TheaterLayoutDesignerComponent } from './theater-layout-designer/theate
|
||||
MatTableModule,
|
||||
MatSelectModule,
|
||||
MatSortModule,
|
||||
PdfTicketComponent
|
||||
],
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -158,7 +158,10 @@
|
||||
<!-- Card Name -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Name des Besitzers</mat-label>
|
||||
<input matInput formControlName="cardName" />
|
||||
<input
|
||||
matInput
|
||||
formControlName="cardName"
|
||||
/>
|
||||
@if (fPayment['cardName'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
@@ -178,14 +181,19 @@
|
||||
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>CVV</mat-label>
|
||||
<input matInput type="password" maxlength="4" formControlName="cvv" />
|
||||
<input
|
||||
matInput
|
||||
type="password"
|
||||
maxlength="4"
|
||||
formControlName="cvv"
|
||||
/>
|
||||
@if (fPayment['cvv'].hasError('pattern')) { <mat-error>3–4 Ziffernt</mat-error> }
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex w-full space-x-2 mt-2 items-center">
|
||||
<mat-icon class="material-symbols-outlined opacity-50" style="font-size: 32px; width: 32px; height: 32px">
|
||||
<mat-icon class="material-symbols-outlined opacity-50" style="font-size: 32px; width: 32px; height: 32px" (click)="fakeFormFill()">
|
||||
encrypted
|
||||
</mat-icon>
|
||||
<p class="text-sm opacity-75">
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
117
src/app/pdf-ticket/pdf-ticket.component.css
Normal file
117
src/app/pdf-ticket/pdf-ticket.component.css
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
63
src/app/pdf-ticket/pdf-ticket.component.html
Normal file
63
src/app/pdf-ticket/pdf-ticket.component.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<div class="ticket">
|
||||
|
||||
<div class="ticket-header">
|
||||
<h1>INFINIMOTION EINTRITTSKARTE</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="ticket-body">
|
||||
|
||||
<div class="movie-title">
|
||||
{{ ticket.show.movie.title }}
|
||||
<div class="title-underline"></div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-flex">
|
||||
|
||||
<div class="ticket-info">
|
||||
<div class="info-section">
|
||||
<div class="info-row">
|
||||
<span class="label">Datum:</span>
|
||||
<span class="value">{{ formatDate(ticket.show.start) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Uhrzeit:</span>
|
||||
<span class="value">{{ formatTime(ticket.show.start) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Preis:</span>
|
||||
<span class="value price">{{ formatPrice(ticket.seat.row.category.price) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="info-row">
|
||||
<span class="label">Saal:</span>
|
||||
<span class="value">{{ ticket.show.hall.name }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Sitz:</span>
|
||||
<span class="value">{{ formatSeat(ticket.seat.row.position, ticket.seat.position) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Tarif:</span>
|
||||
<span class="value">{{ ticket.seat.row.category.name }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<qrcode class="mt-2" qrdata="{{ ticket.code }}" [errorCorrectionLevel]="'M'"></qrcode>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="ticket-footer">
|
||||
<div class="ticket-number">{{ formatTicketNr(ticket.code) }}</div>
|
||||
<div class="ticket-number">{{ formatOrderNr(ticket.order.code) }}</div>
|
||||
<div class="ticket-number">{{ formatPerformanceNr(ticket.show.id) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="perforation"></div>
|
||||
</div>
|
||||
62
src/app/pdf-ticket/pdf-ticket.component.ts
Normal file
62
src/app/pdf-ticket/pdf-ticket.component.ts
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
111
src/app/pdf.service.ts
Normal file
111
src/app/pdf.service.ts
Normal file
@@ -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<void> {
|
||||
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<any>[] = [];
|
||||
|
||||
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`;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<app-ticket-list [tickets]="tickets()" class="w-8/10 my-4"></app-ticket-list>
|
||||
|
||||
<button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-4">{{ buttonText }}</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>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
0
src/app/test/test.component.css
Normal file
0
src/app/test/test.component.css
Normal file
0
src/app/test/test.component.html
Normal file
0
src/app/test/test.component.html
Normal file
11
src/app/test/test.component.ts
Normal file
11
src/app/test/test.component.ts
Normal file
@@ -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 {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user