Merge branch 'main' of git.infinimotion.de:infinimotion/frontend

This commit is contained in:
Kevin Szarafin
2025-11-19 16:37:56 +01:00
40 changed files with 828 additions and 341 deletions

View File

@@ -31,9 +31,7 @@
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "css",
"assets": [
@@ -47,23 +45,20 @@
"output": "assets"
}
],
"styles": [
"src/custom-theme.scss",
"src/styles.css"
]
"styles": ["src/custom-theme.scss", "src/styles.css"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
"maximumWarning": "5MB",
"maximumError": "10MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
"maximumWarning": "100kB",
"maximumError": "500kB"
}
],
"outputHashing": "all"
@@ -94,10 +89,7 @@
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "css",
"assets": [
@@ -106,9 +98,7 @@
"input": "public"
}
],
"styles": [
"src/styles.css"
]
"styles": ["src/styles.css"]
}
}
}

284
package-lock.json generated
View File

@@ -16,7 +16,7 @@
"@angular/material": "^20.2.9",
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
"@infinimotion/model-frontend": "^0.0.102",
"@infinimotion/model-frontend": "^0.0.116",
"@tailwindcss/postcss": "^4.1.14",
"angularx-qrcode": "^20.0.0",
"canvas-confetti": "^1.9.4",
@@ -277,13 +277,13 @@
}
},
"node_modules/@angular-devkit/architect": {
"version": "0.2003.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.9.tgz",
"integrity": "sha512-p0GO2H8hiZjRHI9sm4tXTF3OpWaEnkqvB0GBGJfGp8RvpPfDA2t3j2NAUNtd75H+B0xdfyWLmNq9YJGpy6gznA==",
"version": "0.2003.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.10.tgz",
"integrity": "sha512-2SWetxJzS8gRX6OKQstkWx37VRvZVgcEBDLsDSaeTjpnwh81A+niZQjAVRdwL0NEt1Wixk/RxfeUuCmdyyHvhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "20.3.9",
"@angular-devkit/core": "20.3.10",
"rxjs": "7.8.2"
},
"engines": {
@@ -293,9 +293,9 @@
}
},
"node_modules/@angular-devkit/core": {
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.9.tgz",
"integrity": "sha512-bXsAGIUb4p60x548YmvnMvjwd3FwWz6re1uTM7dV0XH8nQn3XMhOQ3Q3sAckzJHxkDuaRhB3K/a4kupoOmVfTQ==",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.10.tgz",
"integrity": "sha512-COOT2eVebDwHhwENk12VR6m0wjL8D7p0dncEHF15zaBt1IXEnVhGESjSrs5klnPnt5T55qCBKyCTaeK7i/cS8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -321,13 +321,13 @@
}
},
"node_modules/@angular-devkit/schematics": {
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.9.tgz",
"integrity": "sha512-oaIjAKPmHMZBTC0met5M7dbXBeZnCNwmHacT/kBHNVBAz/NI95fuAfb2P0Jxt7gWdQXejDSxWp0tL+sZIyO0xw==",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.10.tgz",
"integrity": "sha512-2N2WF9lj+kr3uCG4+vFadYCL5hAT4dxMgzwScSdOqSd0O+GZD0CzKbDzlfvWIWC/ZealC5Sh4dFEQaRfmy72xA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "20.3.9",
"@angular-devkit/core": "20.3.10",
"jsonc-parser": "3.3.1",
"magic-string": "0.30.17",
"ora": "8.2.0",
@@ -340,14 +340,14 @@
}
},
"node_modules/@angular/build": {
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.9.tgz",
"integrity": "sha512-Ulimvg6twPSCraaZECEmENfKBlD4M1yqeHlg6dCzFNM4xcwaGUnuG6O3cIQD59DaEvaG73ceM2y8ftYdxAwFow==",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.10.tgz",
"integrity": "sha512-nQrj1nMNZygYDilThc7hPrD6/NIWF/BOSgMfE4VkXQp8d0QronP3HFJ/h77MeoughMRFRhix0pqQSlXJQ2SGTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "2.3.0",
"@angular-devkit/architect": "0.2003.9",
"@angular-devkit/architect": "0.2003.10",
"@babel/core": "7.28.3",
"@babel/helper-annotate-as-pure": "7.27.3",
"@babel/helper-split-export-declaration": "7.24.7",
@@ -389,7 +389,7 @@
"@angular/platform-browser": "^20.0.0",
"@angular/platform-server": "^20.0.0",
"@angular/service-worker": "^20.0.0",
"@angular/ssr": "^20.3.9",
"@angular/ssr": "^20.3.10",
"karma": "^6.4.0",
"less": "^4.2.0",
"ng-packagr": "^20.0.0",
@@ -439,11 +439,10 @@
}
},
"node_modules/@angular/cdk": {
"version": "20.2.12",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.12.tgz",
"integrity": "sha512-hz8GtiMy3N9/e8407ZfrByHD5GEC4SkWtxyUknWuTM9P88AOie0jDZ6CfQg9gQ0OJX+6BAbJV3RpYZA1uzNUqA==",
"version": "20.2.13",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.13.tgz",
"integrity": "sha512-h1jTkCmJ/rEQQMkxgKFMCBOrMfjZEnppgdekNmSTerwdVp4vdosTDTzFH/kwiOGFeRClffmvqQ2XLG8mQOKOtA==",
"license": "MIT",
"peer": true,
"dependencies": {
"parse5": "^8.0.0",
"tslib": "^2.3.0"
@@ -455,19 +454,19 @@
}
},
"node_modules/@angular/cli": {
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.9.tgz",
"integrity": "sha512-4eKpRDg96B20yrKJqjA24zgxYy1RiRd70FvF/KG1hqSowsWwtzydtEJ3VM6iFWS9t1D8truuVpKjMEnn1Y274A==",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.10.tgz",
"integrity": "sha512-CQzXScurBXSuMMn0jf6UYDItdggaM3bHYERKL4cUG1z5JqSozVFin1+TB1EjWYkddwdgC10R5xQurdMb+ahRNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/architect": "0.2003.9",
"@angular-devkit/core": "20.3.9",
"@angular-devkit/schematics": "20.3.9",
"@angular-devkit/architect": "0.2003.10",
"@angular-devkit/core": "20.3.10",
"@angular-devkit/schematics": "20.3.10",
"@inquirer/prompts": "7.8.2",
"@listr2/prompt-adapter-inquirer": "3.0.1",
"@modelcontextprotocol/sdk": "1.17.3",
"@schematics/angular": "20.3.9",
"@schematics/angular": "20.3.10",
"@yarnpkg/lockfile": "1.1.0",
"algoliasearch": "5.35.0",
"ini": "5.0.0",
@@ -490,11 +489,10 @@
}
},
"node_modules/@angular/common": {
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.10.tgz",
"integrity": "sha512-12fEzvKbEqjqy1fSk9DMYlJz6dF1MJVXuC5BB+oWWJpd+2lfh4xJ62pkvvLGAICI89hfM5n9Cy5kWnXwnqPZsA==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.12.tgz",
"integrity": "sha512-rFcDfe67ffrb435C6t2lc27WGbizeOcgce30tUhH0iezwEvU+kHHWezXXX6Ylx3TFgqGkhcxL0fliuFYrpM1Vw==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -502,16 +500,15 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/core": "20.3.10",
"@angular/core": "20.3.12",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.10.tgz",
"integrity": "sha512-cW939Lr8GZjPSYfbQKIDNrUaHWmn2M+zBbERThfq5skLuY+xM60bJFv4NqBekfX6YqKLCY62ilUZlnImYIXaqA==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.12.tgz",
"integrity": "sha512-bGESKz97nWiEQ/sydTq/Lzv3zlLvDb8t0msLG5Xti7Ch1EdLddXS8d2D/zFsjiGbAUKVsT6RgPCLHYoi4ocbhA==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -520,12 +517,11 @@
}
},
"node_modules/@angular/compiler-cli": {
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.10.tgz",
"integrity": "sha512-9BemvpFxA26yIVdu8ROffadMkEdlk/AQQ2Jb486w7RPkrvUQ0pbEJukhv9aryJvhbMopT66S5H/j4ipOUMzmzQ==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.12.tgz",
"integrity": "sha512-3SJkexqsydYjIs0iLiJr5AdwkvumpzvjJM6s76iaxXHkRll5k/vM0wqkXLlSIwieBrecO9D4J73lDLWDevXl5A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "7.28.3",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -544,7 +540,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "20.3.10",
"@angular/compiler": "20.3.12",
"typescript": ">=5.8 <6.0"
},
"peerDependenciesMeta": {
@@ -554,11 +550,10 @@
}
},
"node_modules/@angular/core": {
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.10.tgz",
"integrity": "sha512-g99Qe+NOVo72OLxowVF9NjCckswWYHmvO7MgeiZTDJbTjF9tXH96dMx7AWq76/GUinV10sNzDysVW16NoAbCRQ==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.12.tgz",
"integrity": "sha512-K7vibMr55a7+EsuDhkg4Pk+ELuMm12olllwqL/CiQUcHXZ9Zgc4KYGTUuxWB69qJCG90gdSZS7tm5Dx0wDcyjg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -566,7 +561,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "20.3.10",
"@angular/compiler": "20.3.12",
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.15.0"
},
@@ -580,11 +575,10 @@
}
},
"node_modules/@angular/forms": {
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.10.tgz",
"integrity": "sha512-9yWr51EUauTEINB745AaHwZNTHLpXIm4uxuykxzOg+g2QskEgVfH26uS8G2ogdNuwYpB8wnsXWr34qhM3qgOWw==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.12.tgz",
"integrity": "sha512-O0Jy8ScaN3qVipDfR4s0SIxGrz/+MbCdmR05ZYVWf1W5P3dvETKt9WNjX9fYYV47GdgSveyFjuCR2NvWlv94zA==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -592,22 +586,22 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "20.3.10",
"@angular/core": "20.3.10",
"@angular/platform-browser": "20.3.10",
"@angular/common": "20.3.12",
"@angular/core": "20.3.12",
"@angular/platform-browser": "20.3.12",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/material": {
"version": "20.2.12",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-20.2.12.tgz",
"integrity": "sha512-DVenIZmV87qhDBlI2Xv3Z+b+IFI1s4wcZsFrzDi1FBMxKLsltJwMHf4SAmuqY0Mm/2Vw7HEZlfE130TuqjG8Ig==",
"version": "20.2.13",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-20.2.13.tgz",
"integrity": "sha512-9pjp2mULOxojYzOO7qdqt/gSVLrpYBwsIM3K0fxp+mNEcJgNjIxvmRKx46LY9+v0yrPY9puoQvP/T2C+o1+xsw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/cdk": "20.2.12",
"@angular/cdk": "20.2.13",
"@angular/common": "^20.0.0 || ^21.0.0",
"@angular/core": "^20.0.0 || ^21.0.0",
"@angular/forms": "^20.0.0 || ^21.0.0",
@@ -616,11 +610,10 @@
}
},
"node_modules/@angular/platform-browser": {
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.10.tgz",
"integrity": "sha512-UV8CGoB5P3FmJciI3/I/n3L7C3NVgGh7bIlZ1BaB/qJDtv0Wq0rRAGwmT/Z3gwmrRtfHZWme7/CeQ2CYJmMyUQ==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.12.tgz",
"integrity": "sha512-14KQsXZyaQhbRwFz1W58CtbXQc9L+mfuHBgwQjQo99422Yk0ye5WVMb6DHH7dH671qFVqL0XL7zdOPBebaAnJQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -628,9 +621,9 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/animations": "20.3.10",
"@angular/common": "20.3.10",
"@angular/core": "20.3.10"
"@angular/animations": "20.3.12",
"@angular/common": "20.3.12",
"@angular/core": "20.3.12"
},
"peerDependenciesMeta": {
"@angular/animations": {
@@ -639,9 +632,9 @@
}
},
"node_modules/@angular/router": {
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.10.tgz",
"integrity": "sha512-Z03cfH1jgQ7XMDJj4R8qAGqivcvhdG3wYBwaiN1K1ODBgPhbFKNeD4stKqYp7xBNtswmM2O2jMxrL/Djwju4Gg==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.12.tgz",
"integrity": "sha512-hUipb9JI/Euy3bdlhzkcWlw3cTyssPTVTDwSvyGxWO4i+UKATQYmxh8EDOrDYzFp6Aexiy0Hff/H8umdsn6ZdA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -650,9 +643,9 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "20.3.10",
"@angular/core": "20.3.10",
"@angular/platform-browser": "20.3.10",
"@angular/common": "20.3.12",
"@angular/core": "20.3.12",
"@angular/platform-browser": "20.3.12",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@@ -687,7 +680,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",
@@ -1403,9 +1395,9 @@
}
},
"node_modules/@infinimotion/model-frontend": {
"version": "0.0.102",
"resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.102/model-frontend-0.0.102.tgz",
"integrity": "sha512-NJV9bSBubdOZ1GBIe9To3o/hh6AZscJcTyaZY2nGmMxH+GhtvO1AHmjhrQeRjwAKFiwZMEwEg4ktFiOAp3MTMQ==",
"version": "0.0.116",
"resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.116/model-frontend-0.0.116.tgz",
"integrity": "sha512-kGnZW1klIHzdL/44fOEUrDTVSkQSxErgHqwuSb6eQDLUW7Q9ZDL389LFR2haJCpqcYWIjsG2HjpvkJwrm2ctpA==",
"license": "ISC"
},
"node_modules/@inquirer/ansi": {
@@ -1419,14 +1411,14 @@
}
},
"node_modules/@inquirer/checkbox": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.1.tgz",
"integrity": "sha512-rOcLotrptYIy59SGQhKlU0xBg1vvcVl2FdPIEclUvKHh0wo12OfGkId/01PIMJ/V+EimJ77t085YabgnQHBa5A==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz",
"integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
@@ -1466,9 +1458,9 @@
}
},
"node_modules/@inquirer/core": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.1.tgz",
"integrity": "sha512-hzGKIkfomGFPgxKmnKEKeA+uCYBqC+TKtRx5LgyHRCrF6S2MliwRIjp3sUaWwVzMp7ZXVs8elB0Tfe682Rpg4w==",
"version": "10.3.2",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
"integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1476,7 +1468,7 @@
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"cli-width": "^4.1.0",
"mute-stream": "^3.0.0",
"mute-stream": "^2.0.0",
"signal-exit": "^4.1.0",
"wrap-ansi": "^6.2.0",
"yoctocolors-cjs": "^2.1.3"
@@ -1494,13 +1486,13 @@
}
},
"node_modules/@inquirer/editor": {
"version": "4.2.22",
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.22.tgz",
"integrity": "sha512-8yYZ9TCbBKoBkzHtVNMF6PV1RJEUvMlhvmS3GxH4UvXMEHlS45jFyqFy0DU+K42jBs5slOaA78xGqqqWAx3u6A==",
"version": "4.2.23",
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz",
"integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/external-editor": "^1.0.3",
"@inquirer/type": "^3.0.10"
},
@@ -1517,13 +1509,13 @@
}
},
"node_modules/@inquirer/expand": {
"version": "4.0.22",
"resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.22.tgz",
"integrity": "sha512-9XOjCjvioLjwlq4S4yXzhvBmAXj5tG+jvva0uqedEsQ9VD8kZ+YT7ap23i0bIXOtow+di4+u3i6u26nDqEfY4Q==",
"version": "4.0.23",
"resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz",
"integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
},
@@ -1572,13 +1564,13 @@
}
},
"node_modules/@inquirer/input": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.0.tgz",
"integrity": "sha512-h4fgse5zeGsBSW3cRQqu9a99OXRdRsNCvHoBqVmz40cjYjYFzcfwD0KA96BHIPlT7rZw0IpiefQIqXrjbzjS4Q==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz",
"integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
@@ -1594,13 +1586,13 @@
}
},
"node_modules/@inquirer/number": {
"version": "3.0.22",
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.22.tgz",
"integrity": "sha512-oAdMJXz++fX58HsIEYmvuf5EdE8CfBHHXjoi9cTcQzgFoHGZE+8+Y3P38MlaRMeBvAVnkWtAxMUF6urL2zYsbg==",
"version": "3.0.23",
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz",
"integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
@@ -1616,14 +1608,14 @@
}
},
"node_modules/@inquirer/password": {
"version": "4.0.22",
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.22.tgz",
"integrity": "sha512-CbdqK1ioIr0Y3akx03k/+Twf+KSlHjn05hBL+rmubMll7PsDTGH0R4vfFkr+XrkB0FOHrjIwVP9crt49dgt+1g==",
"version": "4.0.23",
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz",
"integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
@@ -1644,7 +1636,6 @@
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@inquirer/checkbox": "^4.2.1",
"@inquirer/confirm": "^5.1.14",
@@ -1670,13 +1661,13 @@
}
},
"node_modules/@inquirer/rawlist": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.10.tgz",
"integrity": "sha512-Du4uidsgTMkoH5izgpfyauTL/ItVHOLsVdcY+wGeoGaG56BV+/JfmyoQGniyhegrDzXpfn3D+LFHaxMDRygcAw==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz",
"integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
},
@@ -1693,13 +1684,13 @@
}
},
"node_modules/@inquirer/search": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.1.tgz",
"integrity": "sha512-cKiuUvETublmTmaOneEermfG2tI9ABpb7fW/LqzZAnSv4ZaJnbEis05lOkiBuYX5hNdnX0Q9ryOQyrNidb55WA==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz",
"integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
@@ -1717,14 +1708,14 @@
}
},
"node_modules/@inquirer/select": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.1.tgz",
"integrity": "sha512-E9hbLU4XsNe2SAOSsFrtYtYQDVi1mfbqJrPDvXKnGlnRiApBdWMJz7r3J2Ff38AqULkPUD3XjQMD4492TymD7Q==",
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz",
"integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
@@ -3429,14 +3420,14 @@
]
},
"node_modules/@schematics/angular": {
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.9.tgz",
"integrity": "sha512-XkgTwGhhrx+MVi2+TFO32d6Es5Uezzx7Y7B/e2ulDlj08bizxQj+9wkeLt5+bR8JWODHpEntZn/Xd5WvXnODGA==",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.10.tgz",
"integrity": "sha512-F9ntS2CElpoWlENf4b03nwdTcN9Ri0Nb4SAE/pfRw3In09h2UHxYyf1ex9jqQt70xltDg4wvyuc3mMs+JlSx9A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "20.3.9",
"@angular-devkit/schematics": "20.3.9",
"@angular-devkit/core": "20.3.10",
"@angular-devkit/schematics": "20.3.10",
"jsonc-parser": "3.3.1"
},
"engines": {
@@ -3865,9 +3856,9 @@
"license": "MIT"
},
"node_modules/@types/jasmine": {
"version": "5.1.12",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.12.tgz",
"integrity": "sha512-1BzPxNsFDLDfj9InVR3IeY0ZVf4o9XV+4mDqoCfyPkbsA7dYyKAPAb2co6wLFlHcvxPlt1wShm7zQdV7uTfLGA==",
"version": "5.1.13",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.13.tgz",
"integrity": "sha512-MYCcDkruFc92LeYZux5BC0dmqo2jk+M5UIZ4/oFnAPCXN9mCcQhLyj7F3/Za7rocVyt5YRr1MmqJqFlvQ9LVcg==",
"dev": true,
"license": "MIT"
},
@@ -3877,7 +3868,6 @@
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -4097,9 +4087,9 @@
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.26",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.26.tgz",
"integrity": "sha512-73lC1ugzwoaWCLJ1LvOgrR5xsMLTqSKIEoMHVtL9E/HNk0PXtTM76ZIm84856/SF7Nv8mPZxKoBsgpm0tR1u1Q==",
"version": "2.8.29",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz",
"integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4224,7 +4214,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -4412,9 +4401,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001754",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
"integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
"version": "1.0.30001755",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz",
"integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==",
"dev": true,
"funding": [
{
@@ -4992,9 +4981,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.250",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz",
"integrity": "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==",
"version": "1.5.254",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz",
"integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==",
"dev": true,
"license": "ISC"
},
@@ -5353,7 +5342,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",
@@ -6317,8 +6305,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",
@@ -6412,7 +6399,6 @@
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@colors/colors": "1.5.0",
"body-parser": "^1.19.0",
@@ -7129,7 +7115,6 @@
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cli-truncate": "^4.0.0",
"colorette": "^2.0.20",
@@ -7743,13 +7728,13 @@
}
},
"node_modules/mute-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
"integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/nanoid": {
@@ -8991,7 +8976,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"
}
@@ -9048,7 +9032,6 @@
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -9903,8 +9886,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",
@@ -9942,7 +9924,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10129,7 +10110,6 @@
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -10532,7 +10512,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -10551,8 +10530,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"
}
}
}

View File

@@ -30,7 +30,7 @@
"@angular/material": "^20.2.9",
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
"@infinimotion/model-frontend": "^0.0.102",
"@infinimotion/model-frontend": "^0.0.116",
"@tailwindcss/postcss": "^4.1.14",
"angularx-qrcode": "^20.0.0",
"canvas-confetti": "^1.9.4",

View File

@@ -23,6 +23,10 @@ import { MatDividerModule } from '@angular/material/divider';
import { MatDialogClose, MatDialogTitle, MatDialogContent, MatDialogActions } from "@angular/material/dialog";
import { MatStepperModule } from '@angular/material/stepper';
import { MatCheckboxModule } from '@angular/material/checkbox';
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 { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
@@ -62,6 +66,8 @@ import { PurchaseSuccessComponent } from './purchase-success/purchase-success.co
import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.component';
import { TicketSmallComponent } from './ticket-small/ticket-small.component';
import { TicketListComponent } from './ticket-list/ticket-list.component';
import { StatisticsComponent } from './statistics/statistics.component';
import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component';
import { PricelistComponent } from './pricelist/pricelist.component';
@@ -106,6 +112,8 @@ import { PricelistComponent } from './pricelist/pricelist.component';
PurchaseFailedComponent,
TicketSmallComponent,
TicketListComponent,
StatisticsComponent,
ZoomWarningComponent,
PricelistComponent,
],
imports: [
@@ -135,6 +143,10 @@ import { PricelistComponent } from './pricelist/pricelist.component';
NgxMaskDirective,
NgxMaskPipe,
QRCodeComponent,
MatBadgeModule,
MatTooltipModule,
MatPaginatorModule,
MatTableModule,
],
providers: [
provideBrowserGlobalErrorListeners(),

View File

@@ -8,12 +8,13 @@ import { ScheduleComponent } from './schedule/schedule.component';
import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component';
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
import { AuthGuard } from './auth.guard';
import {StatisticsComponent} from './statistics/statistics.component';
import { PricelistComponent } from './pricelist/pricelist.component';
const routes: Routes = [
// Seiten ohne Layout
{ path: 'landing', component: HomeComponent },
{ path: 'poc-model', component: PocModelComponent },
{ path: 'poc-model', component: PocModelComponent, data: { allowMobile: true } },
// Seiten mit MainLayout
{
@@ -28,7 +29,13 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
},
{ path: 'selection/performance/:id', component: TheaterOverlayComponent},
{ path: 'performance/:performanceId/checkout', component: TheaterOverlayComponent},
{
path: 'admin/statistics',
component: StatisticsComponent,
canActivate: [AuthGuard],
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
},
{ path: 'prices', component: PricelistComponent },
],
},

View File

@@ -1 +1,2 @@
<app-zoom-warning></app-zoom-warning>
<router-outlet />

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DeviceDetectionService {
private _isMobile: boolean;
constructor() {
this._isMobile = this.checkIfMobile();
}
isMobile(): boolean {
return this._isMobile;
}
private checkIfMobile(): boolean {
const userAgent = navigator.userAgent.toLowerCase();
const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isSmallScreen = window.innerWidth < 768;
return isMobileUA || (isTouchDevice && isSmallScreen);
}
recheckDevice(): void {
this._isMobile = this.checkIfMobile();
}
}

View File

@@ -1,4 +1,13 @@
import { Kinosaal, Sitzplatz, Vorstellung, Film, OmdbSearch, Bestellung, Eintrittskarte } from '@infinimotion/model-frontend';
import {
Kinosaal,
Sitzplatz,
Vorstellung,
Film,
OmdbSearch,
Bestellung,
Eintrittskarte,
StatisticsFilm, StatisticsVorstellung
} from '@infinimotion/model-frontend';
import { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
import { Observable } from "rxjs";
@@ -44,6 +53,12 @@ export class HttpService {
}
/* POST /api/order-transaction/create */
saveAddOrder(req: {order:Bestellung, tickets:Eintrittskarte[]}): Observable<{order:Bestellung, tickets:Eintrittskarte[]}> {
return this.http.post<{order: Bestellung, tickets: Eintrittskarte[]}>(`${this.baseUrl}order-transaction/create`, req);
}
/* Eintrittskarte APIs */
/* GET /api/eintrittskarte/{id} */
@@ -154,8 +169,12 @@ export class HttpService {
/* Show-Seats APIs */
/* GET /api/show-seats/{show} */
getSeatsByShowId(show: number): Observable<{seats:Sitzplatz[], reserved:Sitzplatz[], booked:Sitzplatz[]}> {
return this.http.get<{seats:Sitzplatz[], reserved:Sitzplatz[], booked:Sitzplatz[]}>(`${this.baseUrl}show-seats/${show}`);
getSeatsByShowId(show: number): Observable<{ seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }> {
return this.http.get<{
seats: Sitzplatz[],
reserved: Sitzplatz[],
booked: Sitzplatz[]
}>(`${this.baseUrl}show-seats/${show}`);
}
@@ -164,7 +183,7 @@ export class HttpService {
/* GET /api/importer/search */
searchMovie(query: string): Observable<OmdbSearch> {
return this.http.get<OmdbSearch>(`${this.baseUrl}importer/search`, {
params: { title: query }
params: {title: query}
});
}
@@ -172,4 +191,17 @@ export class HttpService {
importMovie(imdbId: string): Observable<Film> {
return this.http.post<Film>(`${this.baseUrl}importer/import?id=${imdbId}`, {})
}
/* Statistics APIs */
/* GET /api/statistics/movies */
getMovieStatistics(): Observable<StatisticsFilm[]> {
return this.http.get<StatisticsFilm[]>(`${this.baseUrl}statistics/movies`)
}
/* GET /api/statistics/shows */
getShowStatistics(): Observable<StatisticsVorstellung[]> {
return this.http.get<StatisticsVorstellung[]>(`${this.baseUrl}statistics/shows`)
}
}

View File

@@ -1,11 +1,11 @@
<div class="flex flex-row items-center justify-center gap-8 h-1/1">
<div class="max-w-xl m-auto">
<div class="flex items-center h-1/1 justify-center space-x-30">
<div class="max-w-xl">
<section class="felx flex-row">
<div class="flex items-center">
<h1 class="text-3xl font-bold">
Willkommen bei
</h1>
<div class="bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent text-3xl font-bold">
<div class="bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent text-3xl font-bold">
&nbsp;InfiniMotion
</div>
<h1 class="text-3xl font-bold">! 🎬</h1>
@@ -41,7 +41,7 @@
Wir haben uns bei Gestaltung und Stil bewusst an bestehenden Kinowebsites orientiert.
Dabei handelt es sich um eine rein stilistische Anlehnung; diese Seite verfolgt keinerlei kommerzielle Zwecke und dient ausschließlich universitären Zwecken.
Marken, Designs oder Funktionalitäten, die bekannten Anbietern ähneln, sind nicht als Kopie zum Wettbewerb gedacht, sondern als pragmatische Inspirationsquelle im Rahmen der Praxisarbeit.
<a href="https://infinimotion.de" target="_blank" class="bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
<a href="https://infinimotion.de" target="_blank" class="bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
https://infinimotion.de
</a>
wird zum Projektende offline genommen.
@@ -52,7 +52,7 @@
</section>
</div>
<div class="max-w-md mr-60 mt-10">
<div class="mt-10">
<mat-vertical-stepper [linear]="false" [selectedIndex]="5" class="always-open-stepper">
<mat-step
@@ -81,7 +81,7 @@
<mat-step
[completed]="isCompleted(3)"
[editable]="isEditable(3)">
[editable]="true">
<ng-template matStepLabel>
<span>Sprint #3:&nbsp; &nbsp; Vorstellungstickets reservieren und buchen</span>
</ng-template>
@@ -104,5 +104,5 @@
</mat-step>
</mat-vertical-stepper>
</div>
</div>

View File

@@ -7,7 +7,7 @@ import { Component } from '@angular/core';
styleUrl: './main.component.css'
})
export class MainComponent {
currentSprint = 3;
currentSprint = 4;
isCompleted(index: number): boolean {
return index <= this.currentSprint;

View File

@@ -3,8 +3,8 @@
@if ( icon() ) {
<mat-icon style="font-size: 35px; width: 35px; height: 35px; opacity: 50%;">{{ icon() }}</mat-icon>
}
<p class="text-2xl font-medium pl-2 bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
{{ title() }}
<p class="text-2xl font-medium pl-2 bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
{{ label() }}
</p>
</div>
@if ( searchBar() ) {

View File

@@ -7,7 +7,7 @@ import { Component, input, output } from '@angular/core';
styleUrl: './menu-header.component.css'
})
export class MenuHeaderComponent {
title = input.required<string>();
label = input.required<string>();
icon = input<string>();
searchBar = input<boolean>(false);

View File

@@ -1,4 +1,4 @@
<app-menu-header title="Film aus IMDb importieren" icon="cloud_download"></app-menu-header>
<app-menu-header label="Film aus IMDb importieren" icon="cloud_download"></app-menu-header>
<div class="w-6/10 m-auto my-20">
<form class="movie-search-form w-full" (ngSubmit)="DoSubmit()">

View File

@@ -1,6 +1,6 @@
<a [routerLink]="route" class="bg-gray-200 m-2 flex flex-col items-center justify-between rounded-md overflow-hidden text-xl shadow-lg transform transition-all duration-300 hover:scale-105">
<div class="bg-gradient-to-r from-indigo-500 to-pink-600 w-full text-center text-white font-medium rounded-t-md py-0.5 px-2">
<div class="bg-linear-to-r from-indigo-500 to-pink-600 w-full text-center text-white font-medium rounded-t-md py-0.5 px-2">
<p>{{ hall() }}</p>
</div>

View File

@@ -15,7 +15,7 @@ export class MoviePerformanceComponent implements OnInit {
route: string = '';
ngOnInit() {
this.route = `../selection/performance/${this.id()}`;
this.route = `../performance/${this.id()}/checkout`;
}
startTime = computed(() =>

View File

@@ -1,11 +1,15 @@
<form class="movie-search-form w-88">
<div class="flex items-center space-x-4">
@if (searchControl.value && searchControl.value.length > 0) {
<button mat-icon-button #tooltip="matTooltip" matTooltip="Filter löschen" matTooltipPosition="above" class="w-11! h-11! opacity-50" (click)="searchControl.setValue('')">
<mat-icon style="font-size: 25px; width: 25px; height: 25px;">filter_alt_off</mat-icon>
</button>
}
<form class="movie-search-form w-88">
<mat-form-field class="w-full" subscriptSizing="dynamic">
<mat-label>Film suchen</mat-label>
<input class="w-full" type="text" matInput [formControl]="searchControl" [matAutocomplete]="auto" (click)="searchControl.setValue('')">
<!-- @if (searchControl.hasError('filmNotFound')) { -->
<!-- <mat-error>Film existiert nicht</mat-error> -->
<!-- } -->
<input class="w-full" type="text" matInput [formControl]="searchControl" [matAutocomplete]="auto">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
@for (option of filteredOptions | async; track option) {
@@ -13,4 +17,6 @@
}
</mat-autocomplete>
</mat-form-field>
</form>
</form>
</div>

View File

@@ -12,6 +12,7 @@ export class NavbarComponent {
{label: 'Programm', path: '/schedule'},
{label: 'Preise', path: '/prices'},
{label: 'Film importieren', path: '/admin/movie-importer'},
{label: 'Statistiken', path: '/admin/statistics'},
]
private auth = inject(AuthService)

View File

@@ -161,37 +161,31 @@ export class OrderComponent {
submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) {
this.httpService.addOrder(order).pipe(
// Order erstellen
switchMap(createdOrder => {
// Tickets parallel erstellen
const ticketObservables = seats.map(seat => {
const ticket = this.generateNewTicketObject(performance, seat, createdOrder);
return this.httpService.addTicket(ticket);
// Tickets anlegen
const tickets = seats.map(seat => {
return this.generateNewTicketObject(performance, seat, order);
});
// Warten bis alles fertig sind
return forkJoin(ticketObservables).pipe(
tap(createdTickets => {
// Transaktionssicher Sitzplatzbuchung
this.httpService.saveAddOrder({order, tickets}).pipe(
tap(createdOrderAndTickets => {
// Success Handling
if (mode === 'reservation') {
this.orderState.set({
status: 'reservation-success',
order: createdOrder
order: createdOrderAndTickets.order
});
} else {
this.orderState.set({
status: 'purchase-success',
tickets: createdTickets
tickets: createdOrderAndTickets.tickets
});
}
this.selectedSeatsService.commit();
this.loadingService.hide();
this.showConfetti();
})
);
}),
catchError(err => {
// Error handling

View File

@@ -13,7 +13,7 @@
<h3 class="opacity-75">{{ getStartTimeString() }} • {{ performance().hall.name }}</h3>
<h1 class="font-semibold mb-0.5">{{ movie().title }}</h1>
<div class="flex items-center">
<app-movie-rating [rating]="movie().rating" class="rounded-sm shadow-xs px-1 py-0.25 text-sm"></app-movie-rating>
<app-movie-rating [rating]="movie().rating" class="rounded-sm shadow-xs px-1 py-px text-sm"></app-movie-rating>
<app-movie-duration [duration]="movie().duration" [showIcon]="false" class="ml-1.5 opacity-75"></app-movie-duration>
</div>
</div>

View File

@@ -6,6 +6,6 @@
<p class="text-center">Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.</p>
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80">Zurück zur Programmauswahl</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Programmauswahl</button>
</div>

View File

@@ -2,10 +2,10 @@
<h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1>
<p class="text-center">Ihre Sitzplätze wurden erfolgreich gebucht.</p>
<app-ticket-list [tickets]="tickets()" class="w-8/10"></app-ticket-list>
<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-2">Tickets herunterladen</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="success-button w-80">Zurück zur Programmauswahl</button>
<button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-4">Tickets herunterladen</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zurück zur Programmauswahl</button>
</div>

View File

@@ -6,6 +6,6 @@
<p class="text-center">Leider konnten Ihre Sitzplätze nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.</p>
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80">Zurück zur Programmauswahl</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Programmauswahl</button>
</div>

View File

@@ -1,17 +1,15 @@
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
<h1 class="text-xl font-bold">Reservierung erfolgreich!</h1>
<!-- <p class="text-center">Ihre Sitzplätze wurden reserviert. </p> -->
<p class="text-center">Ihre Sitzplätze wurden erfolgreich reserviert. Bitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln.</p>
<div class="bg-white text-5xl font-mono rounded-md shadow-sm w-fit h-fit p-4 py-2 my-4">
<strong>{{ order().code }}</strong>
</div>
<button routerLink="/schedule" mat-button matButton="filled" class="success-button mt-4 w-80">Zurück zur Programmauswahl</button>
<button [disabled]="true" mat-button matButton="outlined" color="accent" class="success-button w-80">Tickets jetzt online bezahlen</button>
<div class="text-green-500 cursor-pointer w-fit mt-1" (click)="cancelReservation()">
<button [disabled]="true" mat-button matButton="filled" color="accent" class="success-button mt-2 w-80">Tickets jetzt online bezahlen</button>
<button routerLink="/schedule" mat-button matButton="outlined" class="success-button mb-4 w-80 mt-1">Zurück zur Programmauswahl</button>
<div class="text-green-500 cursor-pointer w-fit mt-2" (click)="cancelReservation()">
Reservierung stornieren
</div>
</div>

View File

@@ -5,3 +5,7 @@
::ng-deep .mat-mdc-tab .mdc-tab-indicator__content--underline {
border-color: #6366f1 !important; /* indigo-500 */
}
.mat-badge-content {
background: #dd2979;
}

View File

@@ -1,21 +1,26 @@
<app-menu-header title="Programmübersicht" icon="event" [searchBar]="true" (movieSearchResult)="movieSearchResult = $event"></app-menu-header>
<app-menu-header label="Programmübersicht" icon="event" [searchBar]="true" (movieSearchResult)="movieSearchResult = $event"></app-menu-header>
<mat-tab-group mat-stretch-tabs>
@for (dateInfo of dates; track dateInfo.date; let i = $index) {
<mat-tab [label]="dateInfo.label">
<ng-template mat-tab-label>
<span [matBadge]="getMovieCount(i)" matBadgeOverlap="false" [matBadgeHidden]="!isSearch() || getMovieCount(i) === 0" [class]="(isSearch() && getMovieCount(i) === 0)? 'text-gray-300' : ''">
{{ dateInfo.label }}
</span>
</ng-template>
@if (getMovieCount(i) > 0) {
@if (hasSearchResults(i)) {
@for (group of dateInfo.performances; track group.movie.id) {
@if (group.movie.title.toLowerCase().includes(movieSearchResult.toLowerCase())) {
<app-movie-schedule-info [movieGroup]="group"></app-movie-schedule-info>
}
}
} @else {
<app-movie-schedule-no-search-result [search]="movieSearchResult" [date]="dates[i].date" ></app-movie-schedule-no-search-result>
}
@if (isSearch()) {
<app-movie-schedule-no-search-result [search]="movieSearchResult" [date]="dates[i].date"></app-movie-schedule-no-search-result>
} @else {
<app-movie-schedule-empty></app-movie-schedule-empty>
}
}
</mat-tab>
}
</mat-tab-group>

View File

@@ -31,15 +31,7 @@ export class ScheduleComponent implements OnInit {
this.loadPerformances(this.bookableDays);
}
hasSearchResults(dateIndex: number): boolean {
if (!this.movieSearchResult) return true;
return this.dates[dateIndex].performances.some(group =>
group.movie.title.toLowerCase().includes(this.movieSearchResult.toLowerCase())
);
}
generateDates(bookableDays: number) {
private generateDates(bookableDays: number) {
const today = new Date();
for (let i = 0; i < bookableDays; i++) {
const date = new Date(today);
@@ -58,7 +50,7 @@ export class ScheduleComponent implements OnInit {
}
}
loadPerformances(bookableDays: number) {
private loadPerformances(bookableDays: number) {
this.loading.show();
const filter = this.generateDateFilter(bookableDays);
this.http.getPerformacesByFilter(filter).pipe(
@@ -75,7 +67,7 @@ export class ScheduleComponent implements OnInit {
).subscribe();
}
private generateDateFilter(bookableDays: number): string[] {
private generateDateFilter(bookableDays: number): string[] {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + bookableDays - 1);
@@ -87,10 +79,9 @@ private generateDateFilter(bookableDays: number): string[] {
`ge;start;date;${startStr}`,
`le;start;date;${endStr}`,
];
}
}
assignPerformancesToDates() {
private assignPerformancesToDates() {
// Gruppieren nach Datum
const groupedByDate: { [key: string]: Vorstellung[] } = {};
@@ -133,6 +124,22 @@ private generateDateFilter(bookableDays: number): string[] {
}
getMovieCount(index: number): number {
return this.dates[index].performances.length;
if (!this.dates[index]?.performances) {
return 0;
}
const performances = this.dates[index].performances;
if (!this.isSearch()) {
return performances.length;
}
return performances.filter(group =>
group.movie.title.toLowerCase().includes(this.movieSearchResult.toLowerCase())
).length;
}
isSearch(): boolean {
return !!this.movieSearchResult && this.movieSearchResult.trim() !== '';
}
}

View File

@@ -1 +1,6 @@
.empty-seat-space {
width: 30px;
/* Keine Ahnung, wo die zusätzlichen 6.5px herkommen müssen. Wir sonst dünner angezeigt */
height: 36.5px;
/* height: 30px; */
}

View File

@@ -1,3 +1,8 @@
@for (entry of rowSeatList(); track $index) {
<app-seat class="my-1" [state]="entry.state" [seat]="entry.seat"></app-seat>
@if (entry.seat != null && entry.state != null) {
<app-seat class="my-1" [seat]="entry.seat" [state]="entry.state" ></app-seat>
} @else {
<div class="empty-seat-space my-1 mx-0.5"></div>
}
}

View File

@@ -9,5 +9,5 @@ import {TheaterSeatState} from '../model/theater-seat-state.model';
styleUrl: './seat-row.component.css'
})
export class SeatRowComponent {
rowSeatList = input.required<{ seat: Sitzplatz, state: TheaterSeatState }[]>();
rowSeatList = input.required<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[]>();
}

View File

@@ -0,0 +1,85 @@
<app-menu-header label="Statistiken"></app-menu-header>
<div class="table-table-container">
<table mat-table [dataSource]="movies" class="example-table">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let row">{{row.movieId}}</td>
</ng-container>
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef>Titel</th>
<td mat-cell *matCellDef="let row">{{row.movieTitle}}</td>
</ng-container>
<ng-container matColumnDef="earnings">
<th mat-header-cell *matHeaderCellDef>
Umsatz
</th>
<td mat-cell *matCellDef="let row">{{(row.earnings/100).toFixed(2)}} €</td>
</ng-container>
<ng-container matColumnDef="tickets">
<th mat-header-cell *matHeaderCellDef>
Gebuchte Tickets
</th>
<td mat-cell *matCellDef="let row">{{row.tickets}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="moviesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: moviesDisplayedColumns;"></tr>
</table>
</div>
<mat-paginator [length]="movieResultsLength" [pageSize]="30" aria-label="Select page of GitHub search results"></mat-paginator>
<div class="show-table-container">
<table mat-table [dataSource]="shows" class="example-table">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let row">{{row.showId}}</td>
</ng-container>
<ng-container matColumnDef="hall">
<th mat-header-cell *matHeaderCellDef>Kinosaal</th>
<td mat-cell *matCellDef="let row">{{row.showHallName}}</td>
</ng-container>
<ng-container matColumnDef="movie_title">
<th mat-header-cell *matHeaderCellDef>Film Name</th>
<td mat-cell *matCellDef="let row">{{row.movieTitle}}</td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef>Datum</th>
<td mat-cell *matCellDef="let row">{{formatDate(row.showStart)}}</td>
</ng-container>
<ng-container matColumnDef="earnings">
<th mat-header-cell *matHeaderCellDef>
Umsatz
</th>
<td mat-cell *matCellDef="let row">{{(row.earnings/100).toFixed(2)}} €</td>
</ng-container>
<ng-container matColumnDef="tickets">
<th mat-header-cell *matHeaderCellDef>
Gebuchte Tickets
</th>
<td mat-cell *matCellDef="let row">{{row.tickets}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="showsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: showsDisplayedColumns;"></tr>
</table>
</div>
<mat-paginator [length]="showsResultLength" [pageSize]="30" aria-label="Select page of GitHub search results"></mat-paginator>

View File

@@ -0,0 +1,56 @@
import {Component, inject} from '@angular/core';
import {HttpService} from '../http.service';
import {
StatisticsFilm,
StatisticsVorstellung,
} from '@infinimotion/model-frontend';
import {LoadingService} from '../loading.service';
import {firstValueFrom, forkJoin} from 'rxjs';
@Component({
selector: 'app-statistics',
standalone: false,
templateUrl: './statistics.component.html',
styleUrl: './statistics.component.css',
})
export class StatisticsComponent {
private http = inject(HttpService);
protected movies: StatisticsFilm[] = [];
protected shows: StatisticsVorstellung[] = [];
protected moviesDisplayedColumns: string[] = ['id', 'title', 'earnings', 'tickets'];
protected showsDisplayedColumns: string[] = ['id', 'hall', 'movie_title', 'date', 'earnings', 'tickets'];
protected movieResultsLength: number = 0;
protected showsResultLength: number = 0;
private loading = inject(LoadingService);
ngOnInit(): void {
this.loading.show()
this.loadData().then();
}
async loadData() {
let movieRequest = this.http.getMovieStatistics();
let showRequest = this.http.getShowStatistics();
let movieResponse = await firstValueFrom(movieRequest);
let showResponse = await firstValueFrom(showRequest);
this.movies = movieResponse
this.shows = showResponse
if (this.movies.length / 30 < 1) {
this.movieResultsLength = 1;
} else {
this.movieResultsLength = Math.ceil(this.movies.length / 30);
}
if (this.shows.length / 30 < 1) {
this.showsResultLength = 1;
} else {
this.showsResultLength = Math.ceil(this.shows.length / 30);
}
this.loading.hide();
}
formatDate(date: Date) {
return new Date(date).toLocaleString("de");
}
}

View File

@@ -3,12 +3,12 @@
Leinwand
</p>
</div>
<div>
<div class="mb-5">
@for (row of seatsPerRow(); track $index) {
<div class="flex items-center justify-between">
<!-- Speaker -->
<div class="shrink-0 pl-25">
<div class="shrink-0 pl-20">
@if ($index % 4 === 0) {
<mat-icon class="material-symbols-outlined opacity-25" style="font-size: 30px; width: 30px; height: 30px">
speaker
@@ -25,7 +25,7 @@
<app-seat-row class="flex justify-center" [rowSeatList]="row"></app-seat-row>
<!-- Speaker -->
<div class="shrink-0 pr-25">
<div class="shrink-0 pr-20">
@if ($index % 4 === 0) {
<mat-icon class="material-symbols-outlined opacity-25 mirrored" style="font-size: 30px; width: 30px; height: 30px">
speaker

View File

@@ -10,7 +10,7 @@ import {TheaterSeatState} from '../model/theater-seat-state.model';
styleUrl: './theater-layout.component.css'
})
export class TheaterLayoutComponent {
seatsPerRow = input.required<{ seat: Sitzplatz, state: TheaterSeatState }[][]>();
seatsPerRow = input.required<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>();
protected selectedSeatsService = inject(SelectedSeatsService);
}

View File

@@ -1,8 +1,8 @@
<app-menu-header title="Vorstellungstickets kaufen" icon="local_activity" [backToSchedule]="true"></app-menu-header>
<app-menu-header label="Vorstellungstickets kaufen" icon="local_activity" [backToSchedule]="true"></app-menu-header>
<div class="flex justify-between h-100">
<div class="flex h-fit">
<div class="w-7/10 p-10 h-188">
<div class="w-7/10 p-10 h-fit">
<div>
@if (!performance && (loading.loading$ | async)){
<div class="w-full h-full flex items-center justify-center mt-70">
@@ -18,6 +18,6 @@
</div>
</div>
<app-order class="m-10 mr-20 w-3/10" [performance]="performance" [seatCategories]="seatCategories"></app-order>
<app-order class="mt-10 mr-30 w-3/10" [performance]="performance" [seatCategories]="seatCategories"></app-order>
</div>

View File

@@ -25,7 +25,8 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
readonly loading = inject(LoadingService);
showId!: number;
seatsPerRow = signal<{ seat: Sitzplatz, state: TheaterSeatState }[][]>([]);
orderId?: string;
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
performance: Vorstellung | undefined;
seatCategories: Sitzkategorie[] = [];
@@ -33,7 +34,8 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
private isInitialLoad = signal(true);
ngOnInit() {
this.showId = Number(this.route.snapshot.paramMap.get('id')!);
this.showId = Number(this.route.snapshot.paramMap.get('performanceId')!);
this.orderId = this.route.snapshot.queryParams['paramName'];
this.selectedSeatService.clearSelection();
this.selectedSeatService.setSeatSelectable(true);
@@ -90,12 +92,13 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
}
converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): {
seat: Sitzplatz,
state: TheaterSeatState
seat: Sitzplatz | null,
state: TheaterSeatState | null
}[][] {
let rows: { seat: Sitzplatz, state: TheaterSeatState }[][] = [];
let rows: { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] = [];
const categoryMap = new Map<number, Sitzkategorie>();
// Sitzplätze sammeln
resp.seats.forEach(seat => {
if (!rows[seat.row.position]) {
rows[seat.row.position] = [];
@@ -114,9 +117,50 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
this.seatCategories = Array.from(categoryMap.values()).sort((a, b) => a.id - b.id);
rows = rows.filter(row => row.length > 0).sort((a, b) => a[0].seat.row.position - b[0].seat.row.position);
rows.forEach(row => row.sort((a, b) => a.seat.position - b.seat.position));
return rows;
rows = rows.filter(row => row && row.length > 0).sort((a, b) => a[0].seat!.row.position - b[0].seat!.row.position);
if (rows.length === 0) {
return [];
}
// Leere Plätze auffüllen
const filledSeats: { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] = [];
rows.forEach(row => {
row.sort((a, b) => a.seat!.position - b.seat!.position)
const minPos = row[0].seat!.position;
const maxPos = row[row.length - 1].seat!.position;
const filledRow: { seat: Sitzplatz | null, state: TheaterSeatState | null }[] = [];
for (let pos = minPos; pos <= maxPos; pos++) {
const existingSeat = row.find(s => s.seat!.position === pos);
if (existingSeat) {
filledRow.push(existingSeat);
} else {
filledRow.push({ seat: null, state: null });
}
}
filledSeats.push(filledRow);
});
// Leere Reihen auffüllen
const minRowPos = rows[0][0].seat!.row.position;
const maxRowPos = rows[rows.length - 1][0].seat!.row.position;
const filledRows: { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] = [];
let processedIndex = 0;
for (let rowPos = minRowPos; rowPos <= maxRowPos; rowPos++) {
if (processedIndex < filledSeats.length && filledSeats[processedIndex][0].seat!.row.position === rowPos) {
filledRows.push(filledSeats[processedIndex]);
processedIndex++;
} else {
filledRows.push([{ seat: null, state: null }]);
}
}
return filledRows;
}
refreshSeats(): void {

View File

@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ZoomDetectionService {
private zoomLevel$ = new BehaviorSubject<number>(this.getZoomLevel());
constructor() {
// Zoom-Änderungen überwachen
fromEvent(window, 'resize')
.pipe(debounceTime(200))
.subscribe(() => {
this.zoomLevel$.next(this.getZoomLevel());
});
}
getZoomLevel(): number {
const devicePixelRatio = window.devicePixelRatio || 1;
return devicePixelRatio;
}
getZoomLevel$() {
return this.zoomLevel$.asObservable();
}
isZoomOutOfRange(minZoom: number = 0.95, maxZoom: number = 1.05): boolean {
const currentZoom = this.getZoomLevel();
return currentZoom < minZoom || currentZoom > maxZoom;
}
}

View File

@@ -0,0 +1,14 @@
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.zoom-info-box {
animation: slideIn 0.3s ease-out;
}

View File

@@ -0,0 +1,81 @@
@if (isOutOfRange && !isDismissed && !isMobile) {
<div class="zoom-info-box fixed bottom-5 right-5 z-9999 w-[500px] bg-amber-300 border-4 border-dashed rounded-md shadow-lg p-6 py-8 items-center justify-center flex flex-col space-y-2 origin-bottom-right transition-all duration-300" [style.transform]="getCompensationTransform()">
<button
(click)="dismissWarning()"
class="absolute top-2 right-2 w-10 h-10 flex items-center justify-center rounded-full hover:bg-amber-400 transition-colors"
aria-label="Warnung schließen">
<span class="text-3xl leading-none">×</span>
</button>
<div class="relative mb-5">
<mat-icon class="material-symbols-outlined" style="font-size: 100px; width: 100px; height: 100px">
screenshot_monitor
</mat-icon>
<mat-icon class="material-symbols-outlined absolute top-[calc(50%-9px)] left-1/2 -translate-x-1/2 -translate-y-1/2" style="font-size: 30px; width: 30px; height: 30px">
warning
</mat-icon>
</div>
<h1 class="text-xl font-bold">Browser-Zoom nicht optimal!</h1>
<p class="text-center">Ihr Browser-Zoom ist auf <strong>{{ currentZoomPercentage }}%</strong> eingestellt.<br>Für die beste Darstellung empfehlen wir <strong>100%</strong>.</p>
<div class="mt-4 text-sm space-y-3 w-full">
<div class="bg-white/50 rounded p-3">
<p class="font-semibold mb-2">Browser-Zoom zurücksetzen:</p>
<div class="space-y-1 ml-1">
<p class="flex items-center gap-1.5">
<kbd class="px-2 py-1 bg-white rounded shadow border border-gray-500 font-mono text-xs">Strg</kbd>
<span>/</span>
<kbd class="px-2 py-1 bg-white rounded shadow border border-gray-500 font-mono text-xs"></kbd>
<span>+</span>
<kbd class="px-2 py-1 bg-white rounded shadow border border-gray-500 font-mono text-xs">0</kbd>
<span class="px-1">(Windows, Linux / Mac)</span>
</p>
</div>
</div>
<div class="bg-white/50 rounded p-3">
<p class="font-semibold mb-2">Windows-Skalierung prüfen:</p>
<ol class="list-decimal ml-6 space-y-1">
<li>Öffnen Sie die <strong>Windows-Einstellungen</strong></li>
<li>Navigieren Sie zu <strong>System</strong><strong>Bildschirm</strong></li>
<li>Setzen Sie unter <strong>"Skalierung"</strong> den Wert auf <strong>100%</strong></li>
</ol>
</div>
</div>
</div>
}
@if (showMobileWarning) {
<div class="header z-99999 px-8 pt-4 pb-3 relative bg-white text-center">
<div class="flex items-center justify-center space-x-4 transition m-auto">
<img src="assets/logo.png" class="h-10 w-10 transform scale-175 translate-y-px" />
<h1 class="text-3xl font-semibold tracking-wide">InfiniMotion</h1>
</div>
</div>
<div class="fixed inset-0 z-99998 bg-amber-300 flex items-center justify-center p-6">
<div class="max-w-md w-full text-center space-y-6 mt-10">
<div class="relative inline-block">
<mat-icon class="material-symbols-outlined" style="font-size: 120px; width: 120px; height: 120px">
screenshot_monitor
</mat-icon>
<mat-icon class="material-symbols-outlined absolute top-[calc(50%-11px)] left-1/2 -translate-x-1/2 -translate-y-1/2" style="font-size: 35px; width: 35px; height: 35px">
warning
</mat-icon>
</div>
<h1 class="text-2xl font-bold text-gray-800">Nur am PC verfügbar</h1>
<p class="text-base text-gray-700">
Diese Anwendung ist für die Nutzung am <strong>Desktop-PC</strong> optimiert und kann auf mobilen Geräten nicht verwendet werden.
</p>
<div class="text-xs text-gray-600 mt-12">
<p>Deine derzeitige URL:</p>
<p class="font-mono bg-white/70 px-3 py-2 rounded mt-1 break-all">
{{ currentUrl }}
</p>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,100 @@
import { DeviceDetectionService } from './../device-detection.service';
import { Component, HostListener, inject, OnDestroy, OnInit } from '@angular/core';
import { filter, Subject, takeUntil } from 'rxjs';
import { ZoomDetectionService } from '../zoom-detection.service';
import { NavigationEnd, Router } from '@angular/router';
@Component({
selector: 'app-zoom-warning',
standalone: false,
templateUrl: './zoom-warning.component.html',
styleUrl: './zoom-warning.component.css',
})
export class ZoomWarningComponent implements OnInit, OnDestroy {
currentZoomPercentage = 100;
isOutOfRange = false;
isDismissed = false;
isMobile = false;
showMobileWarning = false;
currentUrl = '';
private destroy$ = new Subject<void>();
private currentZoomLevel = 1;
private lastZoomLevel = 0;
zoomDetectionService = inject(ZoomDetectionService);
deviceDetectionService = inject(DeviceDetectionService)
constructor(private router: Router) {
this.isMobile = this.deviceDetectionService.isMobile();
this.currentUrl = window.location.href;
this.checkIfShouldShowMobileWarning();
}
ngOnInit() {
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.destroy$)
)
.subscribe(() => {
this.checkIfShouldShowMobileWarning();
});
if (!this.isMobile) {
this.updateZoomInfo();
this.zoomDetectionService.getZoomLevel$()
.pipe(takeUntil(this.destroy$))
.subscribe((newZoomLevel) => {
if (Math.abs(newZoomLevel - this.lastZoomLevel) > 0.01) {
this.lastZoomLevel = newZoomLevel;
this.isDismissed = false;
this.updateZoomInfo();
}
});
}
}
private checkIfShouldShowMobileWarning() {
if (!this.isMobile) {
this.showMobileWarning = false;
return;
}
const currentRoute = this.router.routerState.root;
const allowMobile = this.getRouteData(currentRoute, 'allowMobile');
this.showMobileWarning = !allowMobile;
}
private getRouteData(route: any, key: string): any {
while (route) {
if (route.snapshot?.data?.[key] !== undefined) {
return route.snapshot.data[key];
}
route = route.firstChild;
}
return false;
}
private updateZoomInfo() {
this.currentZoomLevel = this.zoomDetectionService.getZoomLevel();
this.currentZoomPercentage = Math.round(this.currentZoomLevel * 100);
this.isOutOfRange = this.zoomDetectionService.isZoomOutOfRange();
}
getCompensationTransform(): string {
const scale = 1 / this.currentZoomLevel;
return `scale(${scale})`;
}
dismissWarning() {
this.isDismissed = true;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}