Compare commits

..

90 commits

Author SHA1 Message Date
1570c174cc 1.8.0-beta1 2025-07-25 13:11:41 +02:00
9b29cd7ba3 remove faulty imports 2025-07-25 13:11:36 +02:00
2c541eb5aa optimize UI
hide maintenance
2025-07-25 12:45:59 +02:00
8e64b549d7 improve UI loading vis 2025-07-25 12:36:45 +02:00
583233b95e extend inspection_plan by wearable and enable optional interval 2025-07-25 12:21:56 +02:00
91ad11e20c adapt to schema update 2025-07-25 11:05:01 +02:00
0b16599d2a Merge branch 'develop' into milestone/ff-admin-unit
# Conflicts:
#	package-lock.json
#	package.json
2025-07-25 09:29:01 +02:00
fed08e0232 Merge branch 'develop' into milestone/ff-admin-unit
# Conflicts:
#	src/components/Modal.vue
#	src/components/TextCopy.vue
2025-07-23 10:45:04 +02:00
4da2e9c792 update components 2025-07-23 10:41:46 +02:00
8c4a53bc17 update permissions 2025-07-23 10:28:37 +02:00
6c1140f7d2 Merge pull request 'unit/#116-repairs' (#119) from unit/#116-repairs into milestone/ff-admin-unit
Reviewed-on: #119
2025-07-23 07:30:04 +00:00
789272dc37 edit repairs 2025-07-22 13:10:04 +02:00
c79d5bb1cd repair create and view 2025-07-21 12:58:32 +02:00
b5a3ff4dc6 extend report by title 2025-07-21 11:03:49 +02:00
b56347c172 base views and store 2025-07-19 11:02:03 +02:00
b9b0381356 Merge branch 'develop' into milestone/ff-admin-unit 2025-07-19 08:43:51 +02:00
c5687729a9 Merge pull request 'unit/#99-damage-reports' (#113) from unit/#99-damage-reports into milestone/ff-admin-unit
Reviewed-on: #113
2025-07-17 08:50:13 +00:00
c29da7ca1d better readable date format 2025-07-17 10:42:48 +02:00
31b7ec9c3e Enable Edit and View of Reports 2025-07-17 10:37:40 +02:00
5ea5a0160a show details to damage 2025-07-17 09:32:18 +02:00
93a04abee1 show reports inside admin ui 2025-07-16 12:43:03 +02:00
6aae09cd03 enable public report 2025-07-16 12:24:48 +02:00
766114bf53 enhance send result button 2025-07-16 07:47:29 +02:00
3ec74e751a Merge pull request 'unit/#95-use-smartphone-as-barcode-scanner' (#111) from unit/#95-use-smartphone-as-barcode-scanner into milestone/ff-admin-unit
Reviewed-on: #111
2025-07-15 15:00:10 +00:00
304ae1147e send external scans to app 2025-07-15 16:58:49 +02:00
3e47d3ebf6 admin side scan connection 2025-07-15 15:16:18 +02:00
ed947e5777 remodel scan process and visualize external 2025-07-15 13:19:59 +02:00
9ef76a7c26 add socketio to app 2025-07-15 11:52:44 +02:00
a6892bac85 Merge branch 'develop' into milestone/ff-admin-unit
# Conflicts:
#	package-lock.json
#	package.json
2025-07-14 15:39:44 +02:00
6fa1c67574 Merge pull request 'unit/#103-base-management' (#110) from unit/#103-base-management into milestone/ff-admin-unit
Reviewed-on: #110
2025-07-14 13:36:47 +00:00
6c8c0939b4 refactor 2025-07-12 17:13:32 +02:00
98bf4532aa enable viewing of uploaded files to inspection points 2025-07-12 17:04:27 +02:00
5d26885da3 inspection finish and print 2025-07-11 14:02:39 +02:00
d96c73d5b1 correct type inspectionPlans request 2025-07-11 09:42:07 +02:00
adfe64d1f2 show instances to type 2025-07-11 09:33:34 +02:00
74b05ee97f next and running inspections 2025-07-10 13:22:38 +02:00
1409cf8045 fill out inspection and inspection plan 2025-07-10 10:49:44 +02:00
23bdde5fc2 save inspectionPoints 2025-07-09 16:01:15 +02:00
eb4d338583 inspection Point base form 2025-07-09 12:58:12 +02:00
e7078960ba search select component improvements 2025-06-15 22:04:41 +02:00
fe0f31ce6b search z-index 2025-06-14 13:39:34 +02:00
6575948841 maintainance view and wearable inspection integration 2025-06-13 12:45:43 +02:00
50fa0128ea Merge branch 'milestone/ff-admin-unit' into unit/#103-base-management 2025-06-08 08:02:38 +02:00
b7f22357ec Merge branch 'develop' into milestone/ff-admin-unit
# Conflicts:
#	package-lock.json
#	package.json
#	src/views/admin/club/newsletter/NewsletterRecipients.vue
2025-06-08 08:02:16 +02:00
ddeac1aa26 connect to backend 2025-06-04 14:30:41 +02:00
6c8d57a7e5 update imports 2025-06-04 12:49:42 +02:00
b280654f92 change icon sizing 2025-06-02 13:14:24 +02:00
bd1fdaa234 update models to backend changes 2025-05-29 10:57:24 +02:00
ab3083c18d Merge pull request 'unit/#70-build-ui-demo' (#102) from unit/#70-build-ui-demo into milestone/ff-admin-unit
Reviewed-on: #102
2025-05-28 14:28:31 +00:00
98fd7b64d2 adapt viewmodel changes 2025-05-24 12:55:24 +02:00
30baca2567 type safetiy 2025-05-24 11:40:10 +02:00
9b2ab1923e adapt models to backend changes 2025-05-24 11:30:23 +02:00
d70edd0086 respiratory create forms 2025-05-21 09:38:16 +02:00
def32b786c adapt unified design in template 2025-05-21 09:20:22 +02:00
b83b22d806 add additional fields 2025-05-21 09:10:41 +02:00
43f46c0fad Merge branch 'milestone/ff-admin-unit' into unit/#70-build-ui-demo 2025-05-20 18:55:35 +02:00
b6e80b358a Merge branch 'develop' into milestone/ff-admin-unit
# Conflicts:
#	src/stores/admin/navigation.ts
2025-05-20 18:55:02 +02:00
dd93f7a7b8 order of permission and sidebar 2025-05-16 10:58:31 +02:00
9ee1cca46d vehicle update screen 2025-05-16 10:55:23 +02:00
70e9b47483 inspection plan execute 2025-05-16 10:27:08 +02:00
b359044cb5 inspection create 2025-05-15 14:11:33 +02:00
ee700d9e02 change of models 2025-05-14 14:42:00 +02:00
a49babe48d damage report and inspections 2025-05-14 09:13:47 +02:00
0ea9601ea3 vehicle update and code 2025-05-13 12:30:57 +02:00
8766bbce08 update inspection schema 2025-05-13 12:16:39 +02:00
05555425ce change models 2025-05-13 10:11:26 +02:00
d5c33d899f correct type errors 2025-05-11 16:44:16 +02:00
bdc139f37f Merge branch 'milestone/ff-admin-unit' into unit/#70-build-ui-demo
# Conflicts:
#	package-lock.json
#	package.json
#	src/router/club/newsletterGuard.ts
#	src/router/club/protocolGuard.ts
#	src/router/index.ts
#	src/types/permissionTypes.ts
#	src/views/admin/club/newsletter/NewsletterRecipients.vue
2025-05-09 12:29:30 +02:00
4ebacc5f52 Merge branch 'develop' into milestone/ff-admin-unit 2025-05-09 11:25:31 +02:00
6ad2da1c16 data creation forms and centralization 2025-04-28 12:29:44 +02:00
835e6ef8db update to tailwind 2025-04-13 16:31:23 +02:00
c4a67fd11a Merge branch 'milestone/ff-admin-unit' into unit/#70-build-ui-demo
# Conflicts:
#	package-lock.json
2025-04-13 16:29:59 +02:00
46432fbf7d Merge branch 'version-update' into milestone/ff-admin-unit 2025-04-13 16:29:20 +02:00
e25d91802c inspection plans and vehicle types 2025-04-11 14:14:11 +02:00
00fad29b25 Merge branch 'milestone/ff-admin-unit' into unit/#70-build-ui-demo 2025-04-11 11:14:18 +02:00
b6c68d2205 Merge branch 'develop' into milestone/ff-admin-unit 2025-04-11 11:13:56 +02:00
553eeb7bfb wearable 2025-04-01 16:11:39 +02:00
716823f536 equipment form 2025-03-31 15:35:21 +02:00
5641dbb57f equipmenttype form 2025-03-31 10:59:41 +02:00
8be88a5245 navigation 2025-03-30 17:24:17 +02:00
f951a1cd4c Tabs 2025-03-28 10:02:13 +01:00
4faf93c3ce damage report routing 2025-03-27 17:02:21 +01:00
36ca3d90a7 demo data 2025-03-26 16:56:07 +01:00
5faa4b7906 view models and data structure 2025-03-26 12:45:14 +01:00
45fe7b34c3 collection details 2025-03-26 12:20:06 +01:00
3e87bbc267 base components on collections 2025-03-25 10:42:40 +01:00
b6d6dd0796 unit data collections 2025-03-24 17:16:07 +01:00
2a77a950f5 code scanner 2025-03-24 16:18:23 +01:00
2b3231e26c vehicle and equipment base 2025-03-24 15:12:03 +01:00
4338f58dea unit title link 2025-03-22 08:18:36 +01:00
220 changed files with 16046 additions and 134 deletions

68
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "ff-admin", "name": "ff-admin",
"version": "1.7.5", "version": "1.8.0-beta1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ff-admin", "name": "ff-admin",
"version": "1.7.5", "version": "1.8.0-beta1",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@fullcalendar/core": "^6.1.18", "@fullcalendar/core": "^6.1.18",
@ -43,6 +43,7 @@
"unplugin-vue-markdown": "^29.1.0", "unplugin-vue-markdown": "^29.1.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-qrcode-reader": "^5.7.1",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
@ -3792,6 +3793,18 @@
"@types/underscore": "*" "@types/underscore": "*"
} }
}, },
"node_modules/@types/dom-webcodecs": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.14.tgz",
"integrity": "sha512-ba9aF0qARLLQpLihONIRbj8VvAdUxO+5jIxlscVcDAQTcJmq5qVr781+ino5qbQUJUmO21cLP2eLeXYWzao5Vg==",
"license": "MIT"
},
"node_modules/@types/emscripten": {
"version": "1.40.1",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.40.1.tgz",
"integrity": "sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==",
"license": "MIT"
},
"node_modules/@types/eslint": { "node_modules/@types/eslint": {
"version": "9.6.1", "version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@ -4912,6 +4925,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/barcode-detector": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.2.2.tgz",
"integrity": "sha512-JcSekql+EV93evfzF9zBr+Y6aRfkR+QFvgyzbwQ0dbymZXoAI9+WgT7H1E429f+3RKNncHz2CW98VQtaaKpmfQ==",
"license": "MIT",
"dependencies": {
"@types/dom-webcodecs": "^0.1.11",
"zxing-wasm": "1.1.3"
}
},
"node_modules/birpc": { "node_modules/birpc": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz",
@ -9443,6 +9466,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/sdp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
"license": "MIT"
},
"node_modules/section-matter": { "node_modules/section-matter": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
@ -11009,6 +11038,19 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/vue-qrcode-reader": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.7.2.tgz",
"integrity": "sha512-MRwo8IWM+1UzvfRhOQQBqEap06nl0E8QFIb+/HxS1KiH8BqL2qhlzMVvJgMUti4m5x+XH2YlGS0v1Qshpg+Hbw==",
"license": "MIT",
"dependencies": {
"barcode-detector": "2.2.2",
"webrtc-adapter": "8.2.3"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.5.1", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
@ -11060,6 +11102,19 @@
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/webrtc-adapter": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz",
"integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==",
"license": "BSD-3-Clause",
"dependencies": {
"sdp": "^3.2.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/whatwg-url": { "node_modules/whatwg-url": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
@ -11752,6 +11807,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zxing-wasm": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.1.3.tgz",
"integrity": "sha512-MYm9k/5YVs4ZOTIFwlRjfFKD0crhefgbnt1+6TEpmKUDFp3E2uwqGSKwQOd2hOIsta/7Usq4hnpNRYTLoljnfA==",
"license": "MIT",
"dependencies": {
"@types/emscripten": "^1.39.10"
}
} }
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "ff-admin", "name": "ff-admin",
"version": "1.7.5", "version": "1.8.0-beta1",
"description": "Feuerwehr/Verein Mitgliederverwaltung UI", "description": "Feuerwehr/Verein Mitgliederverwaltung UI",
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -58,6 +58,7 @@
"unplugin-vue-markdown": "^29.1.0", "unplugin-vue-markdown": "^29.1.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-qrcode-reader": "^5.7.1",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -36,12 +36,9 @@ export default defineComponent({
document.getSelection()?.toString() || this.clickedOnEl.value || this.clickedOnEl.innerText || ""; document.getSelection()?.toString() || this.clickedOnEl.value || this.clickedOnEl.innerText || "";
let selection = document.getSelection()?.toString(); let selection = document.getSelection()?.toString();
console.log(selection);
if (selection == "") { if (selection == "") {
console.log("jo");
const range = document.createRange(); const range = document.createRange();
range.selectNode(this.clickedOnEl); range.selectNode(this.clickedOnEl);
console.log(range);
window.getSelection()?.removeAllRanges(); window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range); window.getSelection()?.addRange(range);
} }

View file

@ -4,24 +4,27 @@
class="absolute inset-0 w-full h-full flex justify-center items-center bg-black/50 select-none z-50 p-2" class="absolute inset-0 w-full h-full flex justify-center items-center bg-black/50 select-none z-50 p-2"
v-show="show" v-show="show"
> >
<component :is="component_ref" :data="data" class="p-4 bg-white rounded-lg max-h-[95%] overflow-y-auto" /> <component
:is="component_ref"
:data="data"
:callback="callback"
class="p-4 bg-white rounded-lg max-h-[95%] overflow-y-auto"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { mapState, mapActions } from "pinia"; import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal"; import { useModalStore } from "@/stores/modal";
import { useContextMenuStore } from "@/stores/context-menu";
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
computed: { computed: {
...mapState(useModalStore, ["show", "component_ref", "data"]), ...mapState(useModalStore, ["show", "component_ref", "data", "callback"]),
}, },
methods: { methods: {
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useContextMenuStore, ["closeContextMenu"]),
}, },
}; };
</script> </script>

View file

@ -20,6 +20,7 @@
notification.type == 'error' ? 'border border-red-400' : '', notification.type == 'error' ? 'border border-red-400' : '',
notification.type == 'warning' ? 'border border-red-400' : '', notification.type == 'warning' ? 'border border-red-400' : '',
notification.type == 'info' ? 'border border-gray-400' : '', notification.type == 'info' ? 'border border-gray-400' : '',
notification.type == 'success' ? 'border border-green-400' : '',
]" ]"
> >
<!-- @mouseover="hovering(notification.id, true)" <!-- @mouseover="hovering(notification.id, true)"
@ -36,6 +37,10 @@
v-if="notification.type == 'info'" v-if="notification.type == 'info'"
class="flex items-center justify-center min-w-12 w-12 h-12 bg-gray-500 rounded-lg text-white p-1" class="flex items-center justify-center min-w-12 w-12 h-12 bg-gray-500 rounded-lg text-white p-1"
/> />
<HandThumbUpIcon
v-if="notification.type == 'success'"
class="flex items-center justify-center min-w-12 w-12 h-12 bg-green-500 rounded-lg text-white p-1"
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span <span
@ -44,6 +49,7 @@
notification.type == 'error' ? 'text-red-500' : '', notification.type == 'error' ? 'text-red-500' : '',
notification.type == 'warning' ? 'text-red-500' : '', notification.type == 'warning' ? 'text-red-500' : '',
notification.type == 'info' ? 'text-gray-700' : '', notification.type == 'info' ? 'text-gray-700' : '',
notification.type == 'success' ? 'text-green-700' : '',
]" ]"
>{{ notification.title }}</span >{{ notification.title }}</span
> >
@ -71,6 +77,7 @@ import {
ExclamationCircleIcon, ExclamationCircleIcon,
InformationCircleIcon, InformationCircleIcon,
XMarkIcon, XMarkIcon,
HandThumbUpIcon,
} from "@heroicons/vue/24/outline"; } from "@heroicons/vue/24/outline";
export interface Props { export interface Props {

View file

@ -2,6 +2,7 @@
<div class="grow flex flex-col gap-2 overflow-hidden"> <div class="grow flex flex-col gap-2 overflow-hidden">
<div v-if="useSearch" class="relative self-end flex flex-row items-center gap-2"> <div v-if="useSearch" class="relative self-end flex flex-row items-center gap-2">
<Spinner v-if="deferingSearch" /> <Spinner v-if="deferingSearch" />
<QrCodeIcon v-if="useScanner" class="h-7 cursor-pointer" @click="scanCode" />
<input <input
type="text" type="text"
class="max-w-64! w-64! rounded-md shadow-xs relative block px-3 py-2 pr-5 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" class="max-w-64! w-64! rounded-md shadow-xs relative block px-3 py-2 pr-5 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
@ -66,10 +67,12 @@
</template> </template>
<script setup lang="ts" generic="T extends { id: FieldType }"> <script setup lang="ts" generic="T extends { id: FieldType }">
import { computed, ref, watch } from "vue"; import { computed, defineAsyncComponent, markRaw, ref, watch } from "vue";
import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid"; import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import Spinner from "./Spinner.vue"; import Spinner from "./Spinner.vue";
import type { FieldType } from "@/types/dynamicQueries"; import type { FieldType } from "@/types/dynamicQueries";
import { QrCodeIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
const props = defineProps({ const props = defineProps({
items: { type: Array<T>, default: [] }, items: { type: Array<T>, default: [] },
@ -79,6 +82,7 @@ const props = defineProps({
useSearch: { type: Boolean, default: false }, useSearch: { type: Boolean, default: false },
enablePreSearch: { type: Boolean, default: false }, enablePreSearch: { type: Boolean, default: false },
indicateLoading: { type: Boolean, default: false }, indicateLoading: { type: Boolean, default: false },
useScanner: { type: Boolean, default: false },
}); });
const slots = defineSlots<{ const slots = defineSlots<{
@ -183,84 +187,14 @@ const filterData = (array: Array<any>, searchString: string, start: number, end:
) )
.filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end); .filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end);
}; };
</script>
<!-- function scanCode() {
<script lang="ts"> useModalStore().openModal(
export default defineComponent({ markRaw(defineAsyncComponent(() => import("@/components/scanner/ManageScanModal.vue"))),
computed: { "pagination",
entryCount() { (result: string) => {
return this.totalCount ?? this.items.length; searchString.value = result;
},
showingStart() {
return this.currentPage * this.maxEntriesPerPage;
},
showingEnd() {
let max = this.currentPage * this.maxEntriesPerPage + this.maxEntriesPerPage;
if (max > this.entryCount) max = this.entryCount;
return max;
},
showingText() {
return `${this.entryCount != 0 ? this.showingStart + 1 : 0} - ${this.showingEnd}`;
},
countOfPages() {
return Math.ceil(this.entryCount / this.maxEntriesPerPage);
},
displayedPagesNumbers(): Array<number | "."> {
//indicate if "." or page number gets pushed
let stateOfPush = false;
return [...new Array(this.countOfPages)].reduce((acc, curr, index) => {
if (
// always display first 2 pages
index <= 1 ||
// always display last 2 pages
index >= this.countOfPages - 2 ||
// always display 1 pages around current page
(this.currentPage - 1 <= index && index <= this.currentPage + 1)
) {
acc.push(index);
stateOfPush = false;
return acc;
} }
// abort if placeholder already added to array );
if (stateOfPush == true) return acc; }
// show placeholder if pagenumber is not actively rendered </script>
acc.push(".");
stateOfPush = true;
return acc;
}, []);
},
visibleRows() {
return this.filterData(this.items, this.searchString, this.showingStart, this.showingEnd);
},
},
methods: {
loadPage(newPage: number | ".") {
if (newPage == ".") return;
if (newPage < 0 || newPage >= this.countOfPages) return;
let pageStart = newPage * this.maxEntriesPerPage;
let pageEnd = newPage * this.maxEntriesPerPage + this.maxEntriesPerPage;
if (pageEnd > this.entryCount) pageEnd = this.entryCount;
let loadedElementCount = this.filterData(this.items, this.searchString, pageStart, pageEnd).length;
if (loadedElementCount < this.maxEntriesPerPage)
this.$emit("loadData", { offset: pageStart, count: this.maxEntriesPerPage, search: this.searchString });
this.currentPage = newPage;
},
filterData(array: Array<any>, searchString: string, start: number, end: number): Array<any> {
return array
.filter(
(elem) =>
!this.enablePreSearch ||
searchString.trim() == "" ||
this.config.some((col) => typeof elem?.[col.key] == "string" && elem[col.key].includes(searchString.trim()))
)
.filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end);
},
},
});
</script> -->

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="flex relative"> <div class="flex w-full relative">
<input type="text" readonly :value="copyText" /> <input type="text" readonly :value="copyText" />
<ClipboardIcon <ClipboardIcon
class="w-5 h-5 p-2 box-content absolute right-1 top-1/2 -translate-y-1/2 bg-white cursor-pointer" class="w-5 h-5 p-2 box-content absolute right-1 top-1/2 -translate-y-1/2 bg-white cursor-pointer"

View file

@ -18,7 +18,23 @@
<CheckIcon v-if="appSettings['app.show_link_to_calendar']" class="h-2.5 w-2.5 stroke-4 text-white" /> <CheckIcon v-if="appSettings['app.show_link_to_calendar']" class="h-2.5 w-2.5 stroke-4 text-white" />
</div> </div>
<input v-else id="show_link_to_calendar" type="checkbox" :checked="appSettings['app.show_link_to_calendar']" /> <input v-else id="show_link_to_calendar" type="checkbox" :checked="appSettings['app.show_link_to_calendar']" />
<label for="show_link_to_calendar">Kalender-Link anzeigen</label> <label for="show_link_to_calendar"><i>Kalender</i> Link anzeigen</label>
</div>
<div class="w-full flex flex-row items-center gap-2">
<div
v-if="!enableEdit"
class="border-2 border-gray-500 rounded-sm"
:class="appSettings['app.show_link_to_damagereport'] ? 'bg-gray-500' : 'h-3.5 w-3.5'"
>
<CheckIcon v-if="appSettings['app.show_link_to_damagereport']" class="h-2.5 w-2.5 stroke-4 text-white" />
</div>
<input
v-else
id="show_link_to_damagereport"
type="checkbox"
:checked="appSettings['app.show_link_to_damagereport']"
/>
<label for="show_link_to_damagereport"><i>Schaden melden</i> Link anzeigen</label>
</div> </div>
</BaseSetting> </BaseSetting>
</template> </template>
@ -60,6 +76,10 @@ export default defineComponent({
key: "app.show_link_to_calendar", key: "app.show_link_to_calendar",
value: formData.show_link_to_calendar.checked, value: formData.show_link_to_calendar.checked,
}, },
{
key: "app.show_link_to_damagereport",
value: formData.show_link_to_damagereport.checked,
},
]); ]);
}, },
}, },

View file

@ -0,0 +1,25 @@
<template>
<div @click="showInfo" class="cursor-pointer">
<InformationCircleIcon class="w-5 h-5" />
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import { InformationCircleIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
methods: {
...mapActions(useModalStore, ["openModal"]),
showInfo() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/InspectionTimeFormatExplainModal.vue")))
);
},
},
});
</script>

View file

@ -0,0 +1,55 @@
<template>
<div class="relative w-full md:max-w-md">
<div class="flex flex-row gap-2 items-center justify-center">
<InformationCircleIcon class="text-gray-500 h-5 w-5" />
<p class="text-xl font-medium">Zeit Format für Erinnerung und Intervall</p>
</div>
<br />
<table class="min-w-full text-sm border border-gray-200 rounded">
<tbody>
<tr>
<td class="px-3 py-2 font-mono text-gray-700 border-b border-gray-100">&lt;zahl&gt;-(d|m|y)</td>
<td class="px-3 py-2 text-gray-600 border-b border-gray-100">
Ein Intervall, z.B. <span class="font-mono">7-d</span> für alle 7 Tage,
<span class="font-mono">1-m</span> für jeden Monat.
</td>
</tr>
<tr>
<td class="px-3 py-2 font-mono text-gray-700 border-b border-gray-100">DD/MM</td>
<td class="px-3 py-2 text-gray-600 border-b border-gray-100">
Ein bestimmtes Datum, z.B. <span class="font-mono">15/06</span> für den 15. Juni.
</td>
</tr>
<tr>
<td class="px-3 py-2 font-mono text-gray-700">DD/*</td>
<td class="px-3 py-2 text-gray-600">
Ein Tag jeden Monats, z.B. <span class="font-mono">01/*</span> für den ersten Tag jedes Monats.
</td>
</tr>
</tbody>
</table>
<p>Im Fall von Erinnerungen wird das Format als zeitliche Angabe vor einem Datum verwendet.</p>
<br />
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal">schließen</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import { InformationCircleIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
methods: {
...mapActions(useModalStore, ["closeModal"]),
},
});
</script>

View file

@ -0,0 +1,52 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-damage_report-overview', params: { damageReportId: damageReport.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ damageReport.title }} -
{{ damageReport?.related?.name ?? "Ohne Zuordnung" }}
<small v-if="damageReport?.related">({{ damageReport.related.code }})</small>
</p>
<div class="flex flex-row gap-2">
<div v-if="damageReport.images.length != 0" class="cursor-pointer">
<PhotoIcon class="w-5 h-5" />
</div>
<div v-if="damageReport.location" class="cursor-pointer">
<MapPinIcon class="w-5 h-5" />
</div>
<div v-if="damageReport.reportedBy" class="cursor-pointer">
<UserIcon class="w-5 h-5" />
</div>
<div v-if="damageReport.repair" class="cursor-pointer">
<WrenchScrewdriverIcon class="w-5 h-5" />
</div>
</div>
</div>
<div class="p-2">
<p>gemeldet: {{ new Date(damageReport.reportedAt).toLocaleString("de") }}</p>
<p>Status: {{ damageReport.status }}</p>
<p v-if="damageReport.description">Beschreibung: {{ damageReport.description }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { DamageReportViewModel } from "@/viewmodels/admin/unit/damageReport.models";
import { MapPinIcon, PhotoIcon, UserIcon, WrenchScrewdriverIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
props: {
damageReport: { type: Object as PropType<DamageReportViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,33 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-equipment-overview', params: { equipmentId: equipment.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ equipment.name }}
</p>
</div>
<div class="p-2">
<p v-if="equipment.code">Code: {{ equipment.code }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { EquipmentViewModel } from "@/viewmodels/admin/unit/equipment/equipment.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
equipment: { type: Object as PropType<EquipmentViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,82 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Typ erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<label for="type">Typ</label>
<input type="text" id="type" required />
</div>
<div>
<label for="description">Beschreibung (optional)</label>
<textarea id="description" class="h-18"></textarea>
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</form>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useEquipmentTypeStore } from "@/stores/admin/unit/equipmentType/equipmentType";
import type { CreateEquipmentTypeViewModel } from "@/viewmodels/admin/unit/equipment/equipmentType.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEquipmentTypeStore, ["createEquipmentType"]),
triggerCreate(e: any) {
let formData = e.target.elements;
let createEquipmentType: CreateEquipmentTypeViewModel = {
type: formData.type.value,
description: formData.description.value,
};
this.status = "loading";
this.createEquipmentType(createEquipmentType)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,84 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Ausrüstung-Type löschen</p>
</div>
<br />
<p class="text-center">Type {{ equipmentType?.type }} löschen?</p>
<br />
<div class="flex flex-row gap-2">
<button
primary
type="submit"
:disabled="status == 'loading' || status?.status == 'success'"
@click="triggerDelete"
>
löschen
</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useEquipmentTypeStore } from "@/stores/admin/unit/equipmentType/equipmentType";
import type { CreateEquipmentTypeViewModel } from "@/viewmodels/admin/unit/equipment/equipmentType.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useEquipmentTypeStore, ["equipmentTypes"]),
equipmentType() {
return this.equipmentTypes.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEquipmentTypeStore, ["deleteEquipmentType"]),
triggerDelete() {
this.status = "loading";
this.deleteEquipmentType(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.$router.push({ name: "admin-unit-equipment_type" });
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,40 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-equipment_type-overview', params: { equipmentTypeId: equipmentType.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ equipmentType.type }}
</p>
</div>
<div class="flex flex-col p-2">
<div class="flex flex-row gap-2">
<p class="min-w-16">Anzahl angelegter Geräte:</p>
<p class="grow overflow-hidden">{{ equipmentType.equipmentCount }}</p>
</div>
<div v-if="equipmentType.description" class="flex flex-row gap-2">
<p class="min-w-16">Beschreibung:</p>
<p class="grow overflow-hidden">{{ equipmentType.description }}</p>
</div>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { EquipmentTypeViewModel } from "@/viewmodels/admin/unit/equipment/equipmentType.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
equipmentType: { type: Object as PropType<EquipmentTypeViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,85 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">angefangene Prüfung löschen</p>
</div>
<br />
<p class="text-center">
{{ activeInspectionObj?.inspectionPlan.title }} zu {{ activeInspectionObj?.related.name }}
<small v-if="activeInspectionObj?.related.code">({{ activeInspectionObj?.related.code }})</small> begonnen am
{{ new Date(activeInspectionObj?.created ?? "").toLocaleDateString("de-de") }} löschen?
</p>
<br />
<div class="flex flex-row gap-2">
<button
primary
type="submit"
:disabled="status == 'loading' || status?.status == 'success'"
@click="triggerDelete"
>
löschen
</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useInspectionStore } from "@/stores/admin/unit/inspection/inspection";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
computed: {
...mapState(useInspectionStore, ["activeInspectionObj"]),
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useInspectionStore, ["deleteInspection"]),
triggerDelete() {
if (!this.activeInspectionObj) return;
this.status = "loading";
this.deleteInspection(this.activeInspectionObj.id)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.$router.push({ name: "admin-unit-inspection" });
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,91 @@
<template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ inspectionPoint.title }}</p>
<DocumentCheckIcon class="w-5 h-5 cursor-pointer" @click="openDetailView" />
</div>
<div class="p-2">
<p v-if="inspectionPoint.description" class="pb-2">Beschreibung: {{ inspectionPoint.description }}</p>
<hr v-if="inspectionPoint.description" />
<label :for="inspectionPoint.id">{{ inspectionPoint.others == "pdf" ? "PDF" : "Bild" }}-Datei</label>
<button
:primary="value != ''"
:primary-outline="value == ''"
type="button"
class="flex flex-row gap-2"
@click="($refs.fileInput as HTMLInputElement).click()"
:disabled="!editable"
>
<span v-if="value == ''">Datei wählen</span><span v-else>Datei gewählt</span>
<CheckIcon v-if="value != ''" class="h-5 w-5" />
</button>
<input
ref="fileInput"
:id="inspectionPoint.id"
:name="inspectionPoint.id"
type="file"
:accept="inspectionPoint.others == 'pdf' ? 'application/pdf' : 'image/*'"
hidden
@change="selectFile"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import type { InspectionPointViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
import { CheckIcon, DocumentCheckIcon } from "@heroicons/vue/24/outline";
import { mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
props: {
inspectionPoint: {
type: Object as PropType<InspectionPointViewModel>,
required: true,
},
modelValue: {
type: String,
default: "",
},
editable: {
type: Boolean,
default: true,
},
},
emits: {
"update:model-value": (p: string) => {
return true;
},
"update:upload": (p: File | null) => {
return true;
},
},
computed: {
value: {
get() {
return this.modelValue;
},
set(val: string | number) {
this.$emit("update:model-value", val.toString());
},
},
},
methods: {
...mapActions(useModalStore, ["openModal"]),
selectFile(e: Event) {
this.$emit("update:upload", (e.target as HTMLInputElement).files?.[0] ?? null);
this.value = "set";
},
openDetailView() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/inspection/UploadedFileViewModal.vue"))),
this.inspectionPoint
);
},
},
});
</script>

View file

@ -0,0 +1,83 @@
<template>
<div class="relative w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Prüfung abschließen</p>
</div>
<br />
<div class="flex flex-col gap-2">
<p class="flex flex-row text-sm">
<InformationCircleIcon class="text-gray-500 h-5 w-5" /> Nach abschluss der Prüfung können keine Änderung mehr an
dieser vorgenommen werden. <br />
Es wird ein PDF ausgedruckt und ist dann zu dieser Prüfung verfügbar.
</p>
<br />
<div class="flex flex-row gap-2">
<button :disabled="status == 'loading' || status?.status == 'success'" primary @click="finishInspection">
Prüfung abschließen
</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</div>
<br />
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import { InformationCircleIcon } from "@heroicons/vue/24/outline";
import { useInspectionStore } from "@/stores/admin/unit/inspection/inspection";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: null as any,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useInspectionStore, ["finishActiveInspection"]),
finishInspection(e: any) {
this.status = "loading";
this.finishActiveInspection()
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 2100);
})
.catch((err) => {
this.status = { status: "failed" };
})
.finally(() => {
this.timeout = setTimeout(() => {
this.status = null;
}, 2000);
});
},
},
});
</script>

View file

@ -0,0 +1,62 @@
<template>
<div class="w-full h-full flex flex-col gap-2">
<Spinner v-if="status == 'loading'" />
<div class="grow">
<iframe ref="viewer" class="w-full h-full" />
</div>
<div class="flex flex-row gap-2 justify-end">
<a ref="download" button primary class="w-fit!">download</a>
<button primary-outline class="w-fit!" @click="closeModal">schließen</button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import { useInspectionStore } from "@/stores/admin/unit/inspection/inspection";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
};
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useInspectionStore, ["activeInspectionObj"]),
},
mounted() {
this.fetchItem();
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useInspectionStore, ["fetchInspectionPrintoutById"]),
fetchItem() {
this.status = "loading";
this.fetchInspectionPrintoutById()
.then((response) => {
this.status = { status: "success" };
const blob = new Blob([response.data], { type: "application/pdf" });
(this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob);
const fileURL = window.URL.createObjectURL(new Blob([response.data]));
const fileLink = this.$refs.download as HTMLAnchorElement;
fileLink.href = fileURL;
fileLink.setAttribute(
"download",
`Prüf-Ausdruck_${[this.activeInspectionObj?.related.code ?? "", this.activeInspectionObj?.related.name].join("_")}_${this.activeInspectionObj?.inspectionPlan.title}_${new Date(this.activeInspectionObj?.created ?? "").toLocaleDateString("de-de")}.pdf`
);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,80 @@
<template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ inspectionPoint.title }}</p>
</div>
<div class="p-2">
<p v-if="inspectionPoint.description" class="pb-2">Beschreibung: {{ inspectionPoint.description }}</p>
<hr v-if="inspectionPoint.description" />
<label :for="inspectionPoint.id">
Zahl <small>{{ restrictedRange }}</small>
</label>
<input
:id="inspectionPoint.id"
:name="inspectionPoint.id"
type="number"
v-model="value"
:min="inspectionPoint.min"
:max="inspectionPoint.max"
:class="{ 'ring-red-500! ring-1!': isInRange && editable }"
:disabled="!editable"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import type { InspectionPointViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
inspectionPoint: {
type: Object as PropType<InspectionPointViewModel>,
required: true,
},
modelValue: {
type: String,
default: "",
},
editable: {
type: Boolean,
default: true,
},
},
emits: ["update:model-value"],
computed: {
value: {
get() {
return this.modelValue;
},
set(val: string | number) {
this.$emit("update:model-value", val.toString());
},
},
restrictedRange() {
let range = "";
if (this.inspectionPoint.min != null) range += `min. ${this.inspectionPoint.min}`;
if (this.inspectionPoint.max != null) range += ` bis max. ${this.inspectionPoint.max}`;
return range;
},
isInRange() {
if (this.inspectionPoint.min != null && this.inspectionPoint.max != null)
return Number(this.value) < this.inspectionPoint.min || this.inspectionPoint.max < Number(this.value);
if (this.inspectionPoint.min != null) return Number(this.value) < this.inspectionPoint.min;
if (this.inspectionPoint.max != null) return this.inspectionPoint.max < Number(this.value);
return false;
},
},
mounted() {
if (!this.value) {
this.value = String(this.inspectionPoint.min ?? 0);
}
},
});
</script>

View file

@ -0,0 +1,71 @@
<template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ inspectionPoint.title }}</p>
</div>
<div class="p-2">
<p v-if="inspectionPoint.description" class="pb-2">Beschreibung: {{ inspectionPoint.description }}</p>
<hr v-if="inspectionPoint.description" />
<div class="flex flex-row gap-2">
<button
v-for="option in options"
:key="option.key"
:primary="value == option.key"
:primary-outline="value != option.key"
:disabled="!editable"
:value="option.key"
@click="value = option.key"
>
{{ option.title }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { RadioGroup, RadioGroupOption } from "@headlessui/vue";
import type { InspectionPointViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
inspectionPoint: {
type: Object as PropType<InspectionPointViewModel>,
required: true,
},
modelValue: {
type: String as PropType<"true" | "false" | "">,
default: "",
},
editable: {
type: Boolean,
default: true,
},
},
emits: ["update:model-value"],
data() {
return {
options: [
{ key: "true", title: "OK" },
{ key: "false", title: "nicht OK" },
] as Array<{ key: "true" | "false"; title: string }>,
};
},
computed: {
value: {
get() {
return this.modelValue;
},
set(val: string) {
this.$emit("update:model-value", val);
},
},
},
mounted() {
if (this.value == "") this.value = "false";
},
});
</script>

View file

@ -0,0 +1,55 @@
<template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ inspectionPoint.title }}</p>
</div>
<div class="p-2">
<p v-if="inspectionPoint.description" class="pb-2">Beschreibung: {{ inspectionPoint.description }}</p>
<hr v-if="inspectionPoint.description" />
<label :for="inspectionPoint.id">Freitext</label>
<textarea
:id="inspectionPoint.id"
:name="inspectionPoint.id"
class="h-18"
:class="{ 'ring-red-500! ring-1!': value == '' && editable }"
v-model="value"
:disabled="!editable"
></textarea>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import type { InspectionPointViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
inspectionPoint: {
type: Object as PropType<InspectionPointViewModel>,
required: true,
},
modelValue: {
type: String,
default: "",
},
editable: {
type: Boolean,
default: true,
},
},
emits: ["update:model-value"],
computed: {
value: {
get() {
return this.modelValue;
},
set(val: string | number) {
this.$emit("update:model-value", val.toString());
},
},
},
});
</script>

View file

@ -0,0 +1,63 @@
<template>
<div class="w-full h-full flex flex-col gap-2">
<Spinner v-if="status == 'loading'" />
<div class="grow">
<iframe v-if="data.others == 'pdf'" ref="viewer" class="w-full h-full" />
<img v-else ref="viewer" class="w-full h-full object-contain" />
</div>
<div class="flex flex-row gap-2 justify-end">
<a ref="download" button primary class="w-fit!">download</a>
<button primary-outline class="w-fit!" @click="closeModal">schließen</button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import { useInspectionStore } from "@/stores/admin/unit/inspection/inspection";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
};
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useInspectionStore, ["activeInspectionObj"]),
},
mounted() {
this.fetchItem();
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useInspectionStore, ["fetchUploadedFileByPointId"]),
fetchItem() {
this.status = "loading";
this.fetchUploadedFileByPointId(this.data.id)
.then((response) => {
this.status = { status: "success" };
const blob = new Blob([response.data], { type: this.data.others == "pdf" ? "application/pdf" : "image/png" });
(this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob);
const fileURL = window.URL.createObjectURL(new Blob([response.data]));
const fileLink = this.$refs.download as HTMLAnchorElement;
fileLink.href = fileURL;
fileLink.setAttribute(
"download",
this.data.others == "pdf" ? `${this.data.title}.pdf` : `${this.data.title}.png`
);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,37 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-inspection_plan-overview', params: { inspectionPlanId: inspectionPlan.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ inspectionPlan.title }} - {{ inspectionPlan.related.type }}</p>
</div>
<div class="p-2">
<p v-if="inspectionPlan.inspectionInterval">Prüfinterval: {{ inspectionPlan.inspectionInterval }}</p>
<p v-if="inspectionPlan.remindTime">Erinnerung: {{ inspectionPlan.remindTime }}</p>
<div v-if="inspectionPlan.inspectionPoints.length == 0" class="flex flex-row gap-2 items-center">
<ExclamationTriangleIcon class="h-5 w-5 text-error" />
<p>Prüfplan noch nicht fertig gestellt. Es fehlen Prüfpunkte!</p>
</div>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { InspectionPlanViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
import { ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
props: {
inspectionPlan: { type: Object as PropType<InspectionPlanViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,120 @@
<template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center gap-2">
<ClipboardDocumentCheckIcon v-if="modelValue.type == InspectionPointEnum.oknok" class="w-6 h-6" />
<CalculatorIcon v-else-if="modelValue.type == InspectionPointEnum.number" class="w-6 h-6" />
<BoldIcon v-else-if="modelValue.type == InspectionPointEnum.text" class="w-6 h-6" />
<DocumentIcon v-else-if="modelValue.type == InspectionPointEnum.file" class="w-6 h-6" />
<input type="text" placeholder="Titel" class="grow !w-fit" v-model="title" required />
<div class="flex flex-col">
<ChevronUpIcon v-if="index != 0" class="text-white w-4 h-4 stroke-2 cursor-pointer" @click="$emit('up')" />
<ChevronDownIcon
v-if="index != totalCount - 1"
class="text-white w-4 h-4 stroke-2 cursor-pointer"
@click="$emit('down')"
/>
</div>
<TrashIcon class="h-5 w-5 cursor-pointer" @click="$emit('remove')" />
</div>
<div class="p-2">
<textarea name="description" placeholder="Beschreibung" v-model="description"></textarea>
<div v-if="modelValue.type == InspectionPointEnum.number">
<label for="min">Mindestens</label>
<input type="number" v-model="min" min="0" />
</div>
<div v-if="modelValue.type == InspectionPointEnum.number">
<label for="max">Maximal</label>
<input type="number" v-model="max" :min="Number(min ?? 0) + 1" />
</div>
<div v-if="modelValue.type == InspectionPointEnum.file">
<p>Dateiart</p>
<div class="flex flex-row gap-2">
<button :primary="others == 'img'" :primary-outline="others != 'img'" type="button" @click="others = 'img'">
Bild
</button>
<button :primary="others == 'pdf'" :primary-outline="others != 'pdf'" type="button" @click="others = 'pdf'">
PDF
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import type { InspectionPointViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
import { InspectionPointEnum } from "@/enums/inspectionEnum";
import {
BoldIcon,
CalculatorIcon,
ClipboardDocumentCheckIcon,
TrashIcon,
ChevronUpIcon,
ChevronDownIcon,
DocumentIcon,
} from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
props: {
index: { type: Number, default: 0 },
totalCount: { type: Number, default: 0 },
modelValue: {
type: Object as PropType<InspectionPointViewModel>,
required: true,
},
},
emits: ["up", "down", "remove", "update:model-value"],
computed: {
title: {
get() {
return this.modelValue.title;
},
set(val: string) {
this.$emit("update:model-value", { ...this.modelValue, title: val });
},
},
description: {
get() {
return this.modelValue.description;
},
set(val: string) {
this.$emit("update:model-value", { ...this.modelValue, description: val });
},
},
min: {
get() {
return this.modelValue.min;
},
set(val: string) {
this.$emit("update:model-value", { ...this.modelValue, min: String(val) == "" ? null : String(val) });
},
},
max: {
get() {
return this.modelValue.max;
},
set(val: string) {
this.$emit("update:model-value", { ...this.modelValue, max: String(val) == "" ? null : String(val) });
},
},
others: {
get() {
return this.modelValue.others;
},
set(val: string) {
this.$emit("update:model-value", { ...this.modelValue, others: val == "" ? null : val });
},
},
},
mounted() {
if (this.modelValue.type == InspectionPointEnum.file && !this.others) {
this.others = "img";
}
},
});
</script>

View file

@ -0,0 +1,35 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-inspection_plan-overview', params: { inspectionPlanId: inspectionPlan.id } }"
:disabled="!can('create', 'unit', 'inspection_plan')"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ inspectionPlan.title }}</p>
</div>
<div class="p-2">
<p>Interval: {{ inspectionPlan.inspectionInterval }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import type { InspectionPlanViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
import { mapState } from "pinia";
import { useAbilityStore } from "@/stores/ability";
</script>
<script lang="ts">
export default defineComponent({
props: {
inspectionPlan: {
type: Object as PropType<InspectionPlanViewModel>,
default: {},
},
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,31 @@
<template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ damageReport.related.name }}
</p>
</div>
<div class="p-2">
<p v-if="damageReport.related">Code: {{ damageReport.related.code }}</p>
<p v-if="damageReport.description">Beschreibung: {{ damageReport.description }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { DamageReportViewModel } from "@/viewmodels/admin/unit/damageReport.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
damageReport: { type: Object as PropType<DamageReportViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,39 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-repair-overview', params: { repairId: repair.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ repair.title }} -
{{ repair?.related?.name ?? "Ohne Zuordnung" }}
<small v-if="repair?.related">({{ repair.related.code }})</small>
</p>
</div>
<div class="p-2">
<p>begonnen: {{ new Date(repair.createdAt).toLocaleString("de") }}</p>
<p>Status: {{ repair.status }}</p>
<p v-if="repair.responsible">Verantwortlich: {{ repair.responsible }}</p>
<p v-if="repair.description">Beschreibung: {{ repair.description }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { RepairViewModel } from "@/viewmodels/admin/unit/repair.models";
import { MapPinIcon, PhotoIcon, UserIcon, WrenchScrewdriverIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
props: {
repair: { type: Object as PropType<RepairViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,156 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="selectedSalutation" name="salutation" by="id">
<ListboxLabel>Anrede</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start"> {{ selectedSalutation?.salutation }}</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>
<transition
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption
v-slot="{ active, selected }"
v-for="salutation in salutations"
:key="salutation.id"
:value="salutation"
as="template"
>
<li
:class="[
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
'relative cursor-default select-none py-2 pl-10 pr-4',
]"
>
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">{{
salutation.salutation
}}</span>
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</div>
<div>
<label for="firstname">Vorname</label>
<input type="text" id="firstname" required />
</div>
<div>
<label for="lastname">Nachname</label>
<input type="text" id="lastname" required />
</div>
<div>
<label for="nameaffix">Nameaffix (optional)</label>
<input type="text" id="nameaffix" />
</div>
<div>
<label for="birthdate">Geburtsdatum</label>
<input type="date" id="birthdate" required />
</div>
<div>
<label for="internalId">Interne ID (optional)</label>
<input type="text" id="internalId" />
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</form>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useEquipmentStore } from "@/stores/admin/unit/equipment/equipment";
import type { CreateEquipmentViewModel } from "@/viewmodels/admin/unit/equipment/equipment.models";
import { useSalutationStore } from "@/stores/admin/configuration/salutation";
import type { SalutationViewModel } from "@/viewmodels/admin/configuration/salutation.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedSalutation: null as null | SalutationViewModel,
};
},
computed: {
...mapState(useSalutationStore, ["salutations"]),
},
mounted() {
this.fetchSalutations();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEquipmentStore, ["createEquipment"]),
...mapActions(useSalutationStore, ["fetchSalutations"]),
triggerCreate(e: any) {
if (!this.selectedSalutation) return;
let formData = e.target.elements;
let createEquipment: CreateEquipmentViewModel = {
code: "",
name: "",
location: "",
equipmentTypeId: "",
commissioned: new Date(),
};
this.status = "loading";
this.createEquipment(createEquipment)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,33 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-respiratory_gear-overview', params: { respiratoryGearId: respiratoryGear.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ respiratoryGear.equipment.name }}
</p>
</div>
<div class="p-2">
<p v-if="respiratoryGear.equipment">Code: {{ respiratoryGear.equipment.code }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { RespiratoryGearViewModel } from "@/viewmodels/admin/unit/respiratory/respiratoryGear.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
respiratoryGear: { type: Object as PropType<RespiratoryGearViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,156 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="selectedSalutation" name="salutation" by="id">
<ListboxLabel>Anrede</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start"> {{ selectedSalutation?.salutation }}</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>
<transition
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption
v-slot="{ active, selected }"
v-for="salutation in salutations"
:key="salutation.id"
:value="salutation"
as="template"
>
<li
:class="[
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
'relative cursor-default select-none py-2 pl-10 pr-4',
]"
>
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">{{
salutation.salutation
}}</span>
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</div>
<div>
<label for="firstname">Vorname</label>
<input type="text" id="firstname" required />
</div>
<div>
<label for="lastname">Nachname</label>
<input type="text" id="lastname" required />
</div>
<div>
<label for="nameaffix">Nameaffix (optional)</label>
<input type="text" id="nameaffix" />
</div>
<div>
<label for="birthdate">Geburtsdatum</label>
<input type="date" id="birthdate" required />
</div>
<div>
<label for="internalId">Interne ID (optional)</label>
<input type="text" id="internalId" />
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</form>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useEquipmentStore } from "@/stores/admin/unit/equipment/equipment";
import type { CreateEquipmentViewModel } from "@/viewmodels/admin/unit/equipment/equipment.models";
import { useSalutationStore } from "@/stores/admin/configuration/salutation";
import type { SalutationViewModel } from "@/viewmodels/admin/configuration/salutation.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedSalutation: null as null | SalutationViewModel,
};
},
computed: {
...mapState(useSalutationStore, ["salutations"]),
},
mounted() {
this.fetchSalutations();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEquipmentStore, ["createEquipment"]),
...mapActions(useSalutationStore, ["fetchSalutations"]),
triggerCreate(e: any) {
if (!this.selectedSalutation) return;
let formData = e.target.elements;
let createEquipment: CreateEquipmentViewModel = {
code: "",
name: "",
location: "",
equipmentTypeId: "",
commissioned: new Date(),
};
this.status = "loading";
this.createEquipment(createEquipment)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,31 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-respiratory_mission-overview', params: { respiratoryMissionId: respiratoryMission.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ respiratoryMission.title }} - {{ respiratoryMission.date }}</p>
</div>
<div class="p-2">
<p v-if="respiratoryMission.description">{{ respiratoryMission.description }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { RespiratoryMissionViewModel } from "@/viewmodels/admin/unit/respiratory/respiratoryMission.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
respiratoryMission: { type: Object as PropType<RespiratoryMissionViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,156 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="selectedSalutation" name="salutation" by="id">
<ListboxLabel>Anrede</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start"> {{ selectedSalutation?.salutation }}</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>
<transition
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption
v-slot="{ active, selected }"
v-for="salutation in salutations"
:key="salutation.id"
:value="salutation"
as="template"
>
<li
:class="[
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
'relative cursor-default select-none py-2 pl-10 pr-4',
]"
>
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">{{
salutation.salutation
}}</span>
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</div>
<div>
<label for="firstname">Vorname</label>
<input type="text" id="firstname" required />
</div>
<div>
<label for="lastname">Nachname</label>
<input type="text" id="lastname" required />
</div>
<div>
<label for="nameaffix">Nameaffix (optional)</label>
<input type="text" id="nameaffix" />
</div>
<div>
<label for="birthdate">Geburtsdatum</label>
<input type="date" id="birthdate" required />
</div>
<div>
<label for="internalId">Interne ID (optional)</label>
<input type="text" id="internalId" />
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</form>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useEquipmentStore } from "@/stores/admin/unit/equipment/equipment";
import type { CreateEquipmentViewModel } from "@/viewmodels/admin/unit/equipment/equipment.models";
import { useSalutationStore } from "@/stores/admin/configuration/salutation";
import type { SalutationViewModel } from "@/viewmodels/admin/configuration/salutation.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedSalutation: null as null | SalutationViewModel,
};
},
computed: {
...mapState(useSalutationStore, ["salutations"]),
},
mounted() {
this.fetchSalutations();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEquipmentStore, ["createEquipment"]),
...mapActions(useSalutationStore, ["fetchSalutations"]),
triggerCreate(e: any) {
if (!this.selectedSalutation) return;
let formData = e.target.elements;
let createEquipment: CreateEquipmentViewModel = {
code: "",
name: "",
location: "",
equipmentTypeId: "",
commissioned: new Date(),
};
this.status = "loading";
this.createEquipment(createEquipment)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,34 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-respiratory_wearer-overview', params: { respiratoryWearerId: respiratoryWearer.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ respiratoryWearer.member.lastname }}, {{ respiratoryWearer.member.firstname }}
{{ respiratoryWearer.member.nameaffix ? `- ${respiratoryWearer.member.nameaffix}` : "" }}
</p>
</div>
<div class="p-2">
<p v-if="respiratoryWearer.member.internalId">ID: {{ respiratoryWearer.member.internalId }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { RespiratoryWearerViewModel } from "@/viewmodels/admin/unit/respiratory/respiratoryWearer.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
respiratoryWearer: { type: Object as PropType<RespiratoryWearerViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,31 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-vehicle-overview', params: { vehicleId: vehicle.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ vehicle.name }}</p>
</div>
<div class="p-2">
<p v-if="vehicle.code">Code: {{ vehicle.code }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { VehicleViewModel } from "@/viewmodels/admin/unit/vehicle/vehicle.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
vehicle: { type: Object as PropType<VehicleViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,82 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Typ erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<label for="type">Typ</label>
<input type="text" id="type" required />
</div>
<div>
<label for="description">Beschreibung (optional)</label>
<textarea id="description" class="h-18"></textarea>
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</form>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useVehicleTypeStore } from "@/stores/admin/unit/vehicleType/vehicleType";
import type { CreateVehicleTypeViewModel } from "@/viewmodels/admin/unit/vehicle/vehicleType.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useVehicleTypeStore, ["createVehicleType"]),
triggerCreate(e: any) {
let formData = e.target.elements;
let createVehicleType: CreateVehicleTypeViewModel = {
type: formData.type.value,
description: formData.description.value,
};
this.status = "loading";
this.createVehicleType(createVehicleType)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,83 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Ausrüstung-Type löschen</p>
</div>
<br />
<p class="text-center">Type {{ vehicleType?.type }} löschen?</p>
<br />
<div class="flex flex-row gap-2">
<button
primary
type="submit"
:disabled="status == 'loading' || status?.status == 'success'"
@click="triggerDelete"
>
löschen
</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useVehicleTypeStore } from "@/stores/admin/unit/vehicleType/vehicleType";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useVehicleTypeStore, ["vehicleTypes"]),
vehicleType() {
return this.vehicleTypes.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useVehicleTypeStore, ["deleteVehicleType"]),
triggerDelete() {
this.status = "loading";
this.deleteVehicleType(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.$router.push({ name: "admin-unit-vehicle_type" });
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,40 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-vehicle_type-overview', params: { vehicleTypeId: vehicleType.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ vehicleType.type }}
</p>
</div>
<div class="flex flex-col p-2">
<div class="flex flex-row gap-2">
<p class="min-w-16">Anzahl angelegter Fahrzeuge:</p>
<p class="grow overflow-hidden">{{ vehicleType.vehicleCount }}</p>
</div>
<div v-if="vehicleType.description" class="flex flex-row gap-2">
<p class="min-w-16">Beschreibung:</p>
<p class="grow overflow-hidden">{{ vehicleType.description }}</p>
</div>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { VehicleTypeViewModel } from "@/viewmodels/admin/unit/vehicle/vehicleType.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
vehicleType: { type: Object as PropType<VehicleTypeViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,33 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-wearable-overview', params: { wearableId: wearable.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ wearable.name }}
</p>
</div>
<div class="p-2">
<p v-if="wearable.code">Code: {{ wearable.code }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { WearableViewModel } from "@/viewmodels/admin/unit/wearable/wearable.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
wearable: { type: Object as PropType<WearableViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,82 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Typ erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<label for="type">Typ</label>
<input type="text" id="type" required />
</div>
<div>
<label for="description">Beschreibung (optional)</label>
<textarea id="description" class="h-18"></textarea>
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</form>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useWearableTypeStore } from "@/stores/admin/unit/wearableType/wearableType";
import type { CreateWearableTypeViewModel } from "@/viewmodels/admin/unit/wearable/wearableType.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useWearableTypeStore, ["createWearableType"]),
triggerCreate(e: any) {
let formData = e.target.elements;
let createWearableType: CreateWearableTypeViewModel = {
type: formData.type.value,
description: formData.description.value,
};
this.status = "loading";
this.createWearableType(createWearableType)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,84 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Ausrüstung-Type löschen</p>
</div>
<br />
<p class="text-center">Type {{ wearableType?.type }} löschen?</p>
<br />
<div class="flex flex-row gap-2">
<button
primary
type="submit"
:disabled="status == 'loading' || status?.status == 'success'"
@click="triggerDelete"
>
löschen
</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useWearableTypeStore } from "@/stores/admin/unit/wearableType/wearableType";
import type { CreateWearableTypeViewModel } from "@/viewmodels/admin/unit/wearable/wearableType.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useWearableTypeStore, ["wearableTypes"]),
wearableType() {
return this.wearableTypes.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useWearableTypeStore, ["deleteWearableType"]),
triggerDelete() {
this.status = "loading";
this.deleteWearableType(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.$router.push({ name: "admin-unit-wearable_type" });
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,51 @@
<template>
<RouterLink
:to="{ name: 'admin-unit-wearable_type-overview', params: { wearableTypeId: wearableType.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
{{ wearableType.type }}
</p>
</div>
<div class="flex flex-col p-2">
<div class="flex flex-row gap-2">
<p class="min-w-16">Anzahl angelegter Kleidung:</p>
<p class="grow overflow-hidden">{{ wearableType.wearableCount }}</p>
</div>
<div v-if="wearableType.description" class="flex flex-row gap-2">
<p class="min-w-16">Beschreibung:</p>
<p class="grow overflow-hidden">{{ wearableType.description }}</p>
</div>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useAbilityStore } from "@/stores/ability";
import type { WearableTypeViewModel } from "@/viewmodels/admin/unit/wearable/wearableType.models";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
props: {
wearableType: { type: Object as PropType<WearableTypeViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openDeleteModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/wearableType/DeleteWearableTypeModal.vue"))),
this.wearableType.id
);
},
},
});
</script>

View file

@ -0,0 +1,132 @@
<template>
<div class="flex flex-col gap-2">
<p class="text-primary cursor-pointer" @click="$emit('stepBack')">zurück</p>
<div class="flex flex-col gap-2">
<p class="mx-auto">Eingaben prüfen</p>
<div v-if="check.gear" class="rounded-md p-2 box-border border-2 font-medium border-primary text-black">
<p>
{{ check.gear.name }} <small>({{ check.gear.code }})</small>
</p>
<p>Typ: {{ check.gear.type }}</p>
</div>
<div>
<label for="title">Kurzbeschreibung (Titel)</label>
<input id="title" type="text" readonly :value="check.title" />
</div>
<div>
<label for="description">Beschreibung des Schadens</label>
<textarea id="description" readonly :value="check.description"></textarea>
</div>
<div>
<label for="location">Fundort (optional)</label>
<textarea id="location" readonly :value="check.location"></textarea>
</div>
<div>
<label for="note">Anmerkung für Bearbeiter (optional)</label>
<textarea id="note" readonly :value="check.note"></textarea>
</div>
<div>
<label for="reportedBy">Gemeldet von (optional)</label>
<input id="reportedBy" type="text" readonly :value="check.reportedBy" />
</div>
<div>
<label for="image">Bild (optional)</label>
<img ref="vis" class="mt-2 max-h-36 mx-auto" />
</div>
</div>
<div class="flex flex-row gap-2">
<button primary :disabled="status == 'loading' || status == 'success'" @click="submit">
Schadensmeldung absenden
</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status == 'success'" />
<FailureXMark v-else-if="status == 'failed'" />
</div>
<p v-if="message" class="text-center">{{ message }}</p>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import type { MinifiedEquipmentViewModel } from "@/viewmodels/admin/unit/equipment/equipment.models";
import type { MinifiedVehicleViewModel } from "@/viewmodels/admin/unit/vehicle/vehicle.models";
import type { MinifiedWearableViewModel } from "@/viewmodels/admin/unit/wearable/wearable.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
check: {
type: Object as PropType<{
gear: undefined | MinifiedEquipmentViewModel | MinifiedVehicleViewModel | MinifiedWearableViewModel;
title: string;
description: string;
location: string;
note: string;
reportedBy: string;
image: undefined | File;
}>,
required: true,
},
},
emits: {
nextStep: (s: string) => true,
stepBack: () => true,
},
data() {
return {
status: undefined as undefined | "loading" | "success" | "failed",
message: "" as string,
};
},
mounted() {
if (this.check.image) {
this.setImage(this.check.image);
}
},
methods: {
setImage(img: File) {
let image = this.$refs.vis as HTMLImageElement;
const reader = new FileReader();
reader.onload = function (e) {
image.src = e.target?.result as string;
};
reader.readAsDataURL(img);
},
submit() {
this.status = "loading";
this.message = "";
const formData = new FormData();
if (this.check.gear) formData.append("related", JSON.stringify(this.check.gear));
formData.append("title", this.check.title);
formData.append("description", this.check.description);
formData.append("location", this.check.location);
formData.append("note", this.check.note);
formData.append("reportedBy", this.check.reportedBy);
if (this.check.image) formData.append("images", this.check.image);
this.$http
.post("/public/reportdamage", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((res) => {
this.status = "success";
setTimeout(() => {
this.$emit("nextStep", "result");
}, 2000);
})
.catch((err) => {
this.status = "failed";
this.message = err.message;
});
},
},
});
</script>

View file

@ -0,0 +1,103 @@
<template>
<form class="flex flex-col gap-2" @submit.prevent="setup">
<p class="text-primary cursor-pointer" @click="$emit('stepBack')">zurück</p>
<div class="flex flex-col gap-2">
<div>
<label for="title">Kurzbeschreibung (Titel)</label>
<input id="title" type="text" required :value="data.title" />
</div>
<div>
<label for="description">Beschreibung des Schadens</label>
<textarea id="description" required :value="data.description"></textarea>
</div>
<div>
<label for="location">Fundort (optional)</label>
<textarea id="location" :value="data.location"></textarea>
</div>
<div>
<label for="note">Anmerkung für Bearbeiter (optional)</label>
<textarea id="note" :value="data.note"></textarea>
</div>
<div>
<label for="reportedBy">Gemeldet von (optional)</label>
<input id="reportedBy" type="text" placeholder="dein Name" :value="data.reportedBy" />
</div>
<div>
<label for="image">Bild (optional)</label>
<input id="image" type="file" accept="image/*" @change="(e: any) => setImage(e.target.files[0])" />
<img ref="vis" class="mt-2 max-h-36 mx-auto" />
</div>
</div>
<button type="submit" primary>weiter</button>
</form>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import ScanInput from "@/components/scanner/ScanInput.vue";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import type { MinifiedEquipmentViewModel } from "@/viewmodels/admin/unit/equipment/equipment.models";
import type { MinifiedVehicleViewModel } from "@/viewmodels/admin/unit/vehicle/vehicle.models";
import type { MinifiedWearableViewModel } from "@/viewmodels/admin/unit/wearable/wearable.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
data: {
type: Object as PropType<{
gear: undefined | MinifiedEquipmentViewModel | MinifiedVehicleViewModel | MinifiedWearableViewModel;
title: string;
description: string;
location: string;
note: string;
reportedBy: string;
image: undefined | File;
}>,
required: true,
},
},
emits: {
nextStep: (s: string) => true,
stepBack: () => true,
data: (d: {
title: string;
description: string;
location: string;
note: string;
reportedBy: string;
image?: File;
}) => true,
},
mounted() {
if (this.data.image) {
this.setImage(this.data.image);
}
},
methods: {
setup(e: any) {
let formData = e.target.elements;
this.$emit("data", {
title: formData.title.value,
description: formData.description.value,
location: formData.location.value,
note: formData.note.value,
reportedBy: formData.reportedBy.value,
image: formData.image.files[0],
});
this.$emit("nextStep", "check");
},
setImage(img: File) {
let image = this.$refs.vis as HTMLImageElement;
const reader = new FileReader();
reader.onload = function (e) {
image.src = e.target?.result as string;
};
reader.readAsDataURL(img);
},
},
});
</script>

View file

@ -0,0 +1,28 @@
<template>
<div class="flex flex-col gap-2">
<h1 class="font-medium text-center">Schadensmeldung eingereicht</h1>
<br />
<p>
Deine Schadensmeldung wurde erfolgreich übermittelt.
<br />
Die Verwantwortlichen werden benachtichtigt.
<br />
Du kannst diese Seite jetzt schließen.
</p>
<br />
<p class="text-primary cursor-pointer self-end" @click="$emit('start')">neue Meldung einreichen</p>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
</script>
<script lang="ts">
export default defineComponent({
emits: {
start: () => true,
},
});
</script>

View file

@ -0,0 +1,116 @@
<template>
<div class="flex flex-col gap-2">
<p class="text-primary cursor-pointer" @click="$emit('stepBack')">zurück</p>
<Scanner v-if="!scanned" useInput @code="checkCode" />
<div v-if="scanned" class="contents">
<div class="flex flex-col gap-2 h-80 overflow-y-scroll pr-2">
<div v-if="status != undefined" class="flex flex-row gap-2 h-7 w-7 mx-auto">
<p>Suche</p>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status == 'success'" />
<FailureXMark v-else-if="status == 'failed'" />
</div>
<p v-else-if="available.length == 0" class="mx-auto">nichts gefunden</p>
<div
v-for="item in available"
:key="item.id"
class="rounded-md p-2 cursor-pointer box-border border-2 font-medium"
:class="
item.type == selected?.type && item.id == selected.id
? 'bg-primary text-white border-transparent hover:bg-accent'
: 'border-primary text-black hover:bg-primary hover:text-white'
"
@click="selected = item"
>
<p>
{{ item.name }} <small>({{ item.code }})</small>
</p>
<p>Typ: {{ item.type }}</p>
</div>
</div>
<button
v-if="selected != undefined"
primary
:disabled="status == 'loading'"
@click="
$emit('data', selected);
$emit('nextStep', 'input');
"
>
mit ausgewähltem fortfahren
</button>
<button
primary-outline
:disabled="status == 'loading'"
@click="
scanned = false;
code = '';
"
>
neu Scannen
</button>
</div>
<p v-if="message" class="text-center">{{ message }}</p>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import ScanInput from "@/components/scanner/ScanInput.vue";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import Scanner from "@/components/scanner/Scanner.vue";
import type { MinifiedEquipmentViewModel } from "@/viewmodels/admin/unit/equipment/equipment.models";
import type { MinifiedVehicleViewModel } from "@/viewmodels/admin/unit/vehicle/vehicle.models";
import type { MinifiedWearableViewModel } from "@/viewmodels/admin/unit/wearable/wearable.models";
</script>
<script lang="ts">
export default defineComponent({
emits: {
nextStep: (s: string) => true,
stepBack: () => true,
data: (gear: MinifiedEquipmentViewModel | MinifiedVehicleViewModel | MinifiedWearableViewModel) => true,
},
data() {
return {
code: "" as string,
status: undefined as undefined | "loading" | "success" | "failed",
message: "" as string,
available: [] as Array<MinifiedEquipmentViewModel | MinifiedVehicleViewModel | MinifiedWearableViewModel>,
selected: undefined as
| undefined
| MinifiedEquipmentViewModel
| MinifiedVehicleViewModel
| MinifiedWearableViewModel,
scanned: false as boolean,
};
},
methods: {
checkCode(c: string) {
this.available = [];
this.selected = undefined;
this.scanned = true;
this.status = "loading";
this.code = c;
this.$http
.get(`/public/reportdamage?code=${this.code}`)
.then((res) => {
this.available = res.data;
this.status = "success";
})
.catch((err) => {
this.status = "failed";
})
.finally(() => {
setTimeout(() => {
this.status = undefined;
}, 1500);
});
},
},
});
</script>

View file

@ -0,0 +1,19 @@
<template>
<div class="flex flex-col gap-2">
<br />
<button primary @click="$emit('nextStep', 'select')">Barcode verwenden</button>
<button primary-outline @click="$emit('nextStep', 'input')">ohne Barcode fortfahren</button>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
</script>
<script lang="ts">
export default defineComponent({
emits: {
nextStep: (s: string) => true,
},
});
</script>

View file

@ -0,0 +1,64 @@
<template>
<div class="w-full md:max-w-md">
<XMarkIcon class="ml-auto mb-2 w-5 h-5 cursor-pointer" @click="closeModal" />
<div class="w-full flex flex-row justify-center items-stretch" :class="{ 'max-md:hidden': activeTab == 'self' }">
<div v-for="tab in tabs" :key="tab.type" class="w-1/2 p-0.5 first:pl-0 last:pr-0" @click="activeTab = tab.type">
<p
:class="[
'flex w-full h-full items-center justify-center rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-hidden',
activeTab == tab.type
? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none'
: ' hover:bg-red-200',
]"
>
{{ tab.title }}
</p>
</div>
</div>
<Scanner v-if="activeTab == 'self'" @code="(c) => commit(c)" />
<Phone v-else @code="(c) => commit(c)" />
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import { XMarkIcon } from "@heroicons/vue/24/outline";
import Scanner from "./Scanner.vue";
import Phone from "./Phone.vue";
import { useScannerStore } from "@/stores/admin/scanner";
</script>
<script lang="ts">
export default defineComponent({
props: {
callback: {
type: Function,
default: (result: string) => {},
},
},
data() {
return {
activeTab: "self" as "self" | "phone",
tabs: [
{ type: "self", title: "Scanner" },
{ type: "phone", title: "Smartphone" },
] as Array<{ type: "self" | "phone"; title: string }>,
};
},
computed: {
...mapState(useScannerStore, ["inUse"]),
},
mounted() {
if (this.inUse) this.activeTab = "phone";
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
commit(code: string) {
this.callback(code);
this.closeModal();
},
},
});
</script>

View file

@ -0,0 +1,91 @@
<template>
<div v-if="!inUse" class="relative flex flex-col w-full h-[455px] overflow-hidden">
<br />
<button primary @click="startSession">Smartphone als Scanner verwenden</button>
</div>
<div v-else class="relative flex flex-col w-full h-[455px] overflow-hidden">
<br />
<div class="grow flex flex-col gap-2 overflow-y-scroll pr-2">
<div
v-for="i in results"
:key="i"
class="h-10 w-full flex justify-center items-center gap-2 py-2 px-4 text-sm font-medium rounded-md text-white bg-primary"
>
<p class="grow">{{ i }}</p>
<TrashIcon class="w-5 h-5 cursor-pointer" @click="() => removeElementFromResults(i)" />
<ArrowRightIcon class="w-5 h-5 cursor-pointer" @click="commit(i)" />
</div>
<p v-if="results.length == 0">Bisher keine Scan-Ergebnisse vorhanden.</p>
</div>
<br />
<RouterLink :to="{ name: 'public-scanner-select' }" target="_blank" class="text-primary"
>Link zur Scanoberfläche:</RouterLink
>
<div class="flex flex-row gap-4 items-center">
<TextCopy :copyText="roomId" />
<QrCodeIcon class="h-7 w-7 cursor-pointer" @click="showQRCode = true" />
</div>
<div v-show="showQRCode" class="absolute w-full h-full flex items-center justify-center bg-white/95 p-2">
<img ref="qr" />
<QrCodeIcon class="absolute bottom-1 right-0 h-7 w-7 cursor-pointer" @click="showQRCode = false" />
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import { useScannerStore } from "@/stores/admin/scanner";
import { ArrowRightIcon, QrCodeIcon, TrashIcon } from "@heroicons/vue/24/outline";
import TextCopy from "../TextCopy.vue";
import QRCode from "qrcode";
import { RouterLink } from "vue-router";
</script>
<script lang="ts">
export default defineComponent({
emits: ["code"],
data() {
return {
showQRCode: false as boolean,
};
},
watch: {
linkToScan() {
this.renderQRCode();
},
},
computed: {
...mapState(useScannerStore, ["inUse", "roomId", "results"]),
linkToScan() {
return `${window.location.origin}/public/scanner/${this.roomId}`;
},
},
mounted() {
this.renderQRCode();
},
methods: {
...mapActions(useScannerStore, ["startSession", "endSession", "removeElementFromResults"]),
commit(c: string) {
this.$emit("code", c);
},
renderQRCode() {
QRCode.toDataURL(this.linkToScan, {
width: 300,
margin: 2,
color: {
dark: "#000000",
light: "#FFFFFF",
},
errorCorrectionLevel: "M",
})
.then((res) => {
(this.$refs.qr as HTMLImageElement).src = res;
})
.catch((err) => {});
},
},
});
</script>

View file

@ -0,0 +1,57 @@
<template>
<div>
<label :for="name">{{ label }}{{ required ? "" : " (optional)" }}</label>
<div class="relative flex flex-row items-center gap-2">
<input ref="resultInput" class="pl-9!" :id="name" type="text" v-model="value" :required="required" />
<QrCodeIcon class="absolute h-6 stroke-1 left-2 top-1/2 -translate-y-1/2 cursor-pointer z-10" @click="scanCode" />
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, markRaw, defineAsyncComponent } from "vue";
import { QrCodeIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
import { mapActions } from "pinia";
</script>
<script lang="ts">
export default defineComponent({
props: {
label: String,
name: String,
required: {
type: Boolean,
default: true,
},
modelValue: {
type: String,
default: "",
required: false,
},
},
emits: ["update:model-value"],
computed: {
value: {
get() {
return this.modelValue;
},
set(val: String) {
this.$emit("update:model-value", val);
},
},
},
methods: {
...mapActions(useModalStore, ["openModal"]),
scanCode() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/scanner/ManageScanModal.vue"))),
"codeScanInput",
(result: string) => {
(this.$refs.resultInput as HTMLInputElement).value = result;
}
);
},
},
});
</script>

View file

@ -0,0 +1,51 @@
<template>
<div class="flex flex-col items-center w-full md:max-w-md">
<div class="w-full flex flex-row items-center justify-between">
<p>Link zur Scanoberfläche</p>
<XMarkIcon class="w-5 h-5 cursor-pointer" @click="closeModal" />
</div>
<br />
<img ref="qr" />
<TextCopy :copyText="roomId" />
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import { useScannerStore } from "@/stores/admin/scanner";
import { XMarkIcon } from "@heroicons/vue/24/outline";
import TextCopy from "../TextCopy.vue";
import QRCode from "qrcode";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
computed: {
...mapState(useScannerStore, ["roomId"]),
linkToScan() {
return `${window.location.origin}/public/scanner/${this.roomId}`;
},
},
mounted() {
QRCode.toDataURL(this.linkToScan, {
width: 300,
margin: 2,
color: {
dark: "#000000",
light: "#FFFFFF",
},
errorCorrectionLevel: "M",
})
.then((res) => {
(this.$refs.qr as HTMLImageElement).src = res;
})
.catch(() => {});
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
},
});
</script>

View file

@ -0,0 +1,41 @@
<template>
<div class="flex flex-col items-center w-full md:max-w-md h-[455px] overflow-hidden">
<div class="w-full flex flex-row items-center justify-between">
<p>Scan-Ergebnisse</p>
<XMarkIcon class="w-5 h-5 cursor-pointer" @click="closeModal" />
</div>
<br />
<div class="w-full grow flex flex-col gap-2 overflow-y-scroll pr-2">
<div
v-for="i in results"
:key="i"
class="h-10 w-full flex justify-center items-center gap-2 py-2 px-4 text-sm font-medium rounded-md text-white bg-primary"
>
<p class="grow select-text">{{ i }}</p>
<TrashIcon class="w-5 h-5 cursor-pointer" @click="() => removeElementFromResults(i)" />
</div>
<p v-if="results.length == 0">Bisher keine Scan-Ergebnisse vorhanden.</p>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import { useScannerStore } from "@/stores/admin/scanner";
import { TrashIcon, XMarkIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
computed: {
...mapState(useScannerStore, ["results"]),
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useScannerStore, ["removeElementFromResults"]),
},
});
</script>

View file

@ -0,0 +1,42 @@
<template>
<div class="w-full h-fit min-h-fit flex flex-row gap-2 bg-white rounded-lg items-center overflow-hidden p-4">
<LinkSlashIcon v-if="connectedDevices == 0" class="w-5 h-5" />
<LinkIcon v-else class="w-5 h-5" />
<p class="grow">Externer Scanner aktiviert</p>
<div title="Link zur Scan-Ansicht" @click="showQRCode">
<QrCodeIcon class="w-6 h-6 cursor-pointer" />
</div>
<div title="Scan-Ergebnisse anzeigen" @click="showResults">
<RectangleStackIcon class="w-6 h-6 cursor-pointer" />
</div>
<div title="Scan-Verbindung beenden" @click="endSession">
<NoSymbolIcon class="w-6 h-6 cursor-pointer" />
</div>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapState, mapActions } from "pinia";
import { useScannerStore } from "@/stores/admin/scanner";
import { LinkIcon, LinkSlashIcon, NoSymbolIcon, QrCodeIcon, RectangleStackIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
computed: {
...mapState(useScannerStore, ["inUse", "results", "roomId", "connectedDevices"]),
},
methods: {
...mapActions(useScannerStore, ["endSession"]),
...mapActions(useModalStore, ["openModal"]),
showQRCode() {
this.openModal(markRaw(defineAsyncComponent(() => import("@/components/scanner/ScanQRCodeModal.vue"))));
},
showResults() {
this.openModal(markRaw(defineAsyncComponent(() => import("@/components/scanner/ScanResultsModal.vue"))));
},
},
});
</script>

View file

@ -0,0 +1,77 @@
<template>
<div class="flex flex-col gap-2 w-full min-h-[455px]">
<qrcode-stream
class="grow"
:constraints="selectedCamera?.constraints"
:track="trackFunctionOptions[4].value"
:formats="barcodeFormats"
:paused="paused"
@error="onError"
@detect="onDetect"
@camera-on="onCameraReady"
/>
<select v-model="selectedCamera">
<option v-for="c in selecteableCameras" :value="c">{{ c.label }}</option>
</select>
<div>
<label for="manual">Code eingeben</label>
<input v-if="useInput" id="manual" type="text" v-model="detected" />
</div>
<div class="flex flex-row justify-end gap-4 py-2">
<button primary-outline @click="paused = false" :disabled="!paused">weiter scannen</button>
<button primary-outline @click="commit" :disabled="detected == ''">bestätigen</button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import {
barcodeFormats,
defaultConstraintOptions,
getAvailableCameras,
handleScannerError,
trackFunctionOptions,
type Camera,
} from "@/helpers/scanner";
import { QrcodeStream, type DetectedBarcode } from "vue-qrcode-reader";
</script>
<script lang="ts">
export default defineComponent({
props: {
useInput: {
type: Boolean,
default: false,
},
},
emits: ["code", "ready"],
data() {
return {
selecteableCameras: defaultConstraintOptions,
selectedCamera: undefined as undefined | Camera,
paused: false,
detected: "",
};
},
methods: {
async onCameraReady() {
this.selecteableCameras = await getAvailableCameras();
if (!this.selectedCamera) {
this.selectedCamera = this.selecteableCameras[0];
}
this.$emit("ready");
},
onDetect(result: Array<DetectedBarcode>) {
this.paused = true;
this.detected = result.map((r) => r.rawValue)[0];
},
onError(err: Error) {
console.log(handleScannerError(err));
},
commit() {
this.$emit("code", this.detected);
},
},
});
</script>

View file

@ -0,0 +1,233 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled" multiple>
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:displayValue="(e) => chosen.map((c) => c.title).join(', ')"
@input="query = $event.target.value"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="damageReport in available"
as="template"
:key="damageReport.id"
:value="damageReport.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ damageReport.title }} <span v-if="damageReport.reportedBy">von {{ damageReport.reportedBy }}</span>
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useDamageReportStore } from "@/stores/admin/unit/damageReport";
import type { DamageReportViewModel } from "@/viewmodels/admin/unit/damageReport.models";
import difference from "lodash.difference";
import Spinner from "@/components/Spinner.vue";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: Array as PropType<Array<string>>,
default: [],
},
title: String,
disabled: {
type: Boolean,
default: false,
},
related: {
type: String as PropType<"vehicle" | "equipment" | "wearable">,
default: "equipment",
},
relatedId: {
type: String,
required: true,
},
},
emits: ["update:model-value", "add:difference", "remove:difference", "add:damageReport"],
watch: {
modelValue() {
// if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadDamageReportsInitial();
},
related() {
this.reload();
},
relatedId() {
this.reload();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<DamageReportViewModel>,
filtered: [] as Array<DamageReportViewModel>,
chosen: [] as Array<DamageReportViewModel>,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: Array<string>) {
this.$emit("update:model-value", val);
if (this.modelValue.length < val.length) {
let diff = difference(val, this.modelValue);
if (diff.length != 1) return;
let diffObj = this.getDamageReportFromSearch(diff[0]);
if (!diffObj) return;
this.$emit("add:difference", diff[0]);
this.$emit("add:damageReport", diffObj);
} else {
let diff = difference(this.modelValue, val);
if (diff.length != 1) return;
this.$emit("remove:difference", diff[0]);
}
},
},
},
mounted() {
this.reload();
},
methods: {
...mapActions(useDamageReportStore, [
"searchDamageReports",
"getDamageReportsByIds",
"getAllDamageReportsWithRelated",
"searchDamageReportsWithRelated",
]),
reload() {
this.chosen = [];
this.filtered = [];
this.preloadAll();
this.loadDamageReportsInitial();
},
preloadAll() {
this.all = [];
if (this.relatedId == "") return;
this.loading = true;
this.getAllDamageReportsWithRelated(this.related, this.relatedId)
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchDamageReportsWithRelated(this.related, this.relatedId, this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getDamageReportFromSearch(id: string) {
return this.available.find((f) => f.id == id);
},
loadDamageReportsInitial() {
if (this.modelValue.length == 0) {
this.chosen = [];
} else {
this.getDamageReportsByIds(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
}
},
},
});
</script>

View file

@ -0,0 +1,216 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:display-value="() => chosen?.title ?? ''"
@input="query = $event.target.value"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && query == ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="damageReport in available"
as="template"
:key="damageReport.id"
:value="damageReport.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ damageReport.title }} <span v-if="damageReport.reportedBy">von {{ damageReport.reportedBy }}</span>
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useDamageReportStore } from "@/stores/admin/unit/damageReport";
import type { DamageReportViewModel } from "@/viewmodels/admin/unit/damageReport.models";
import Spinner from "../Spinner.vue";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
related: {
type: String as PropType<"vehicle" | "equipment" | "wearable">,
default: "equipment",
},
relatedId: {
type: String,
required: true,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
//if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadDamageReportInitial();
},
related() {
this.reload();
},
relatedId() {
this.reload();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<DamageReportViewModel>,
filtered: [] as Array<DamageReportViewModel>,
chosen: undefined as undefined | DamageReportViewModel,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getDamageReportFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.reload();
},
methods: {
...mapActions(useDamageReportStore, [
"searchDamageReports",
"fetchDamageReportById",
"getAllDamageReportsWithRelated",
"searchDamageReportsWithRelated",
]),
reload() {
this.chosen = undefined;
this.filtered = [];
this.preloadAll();
this.loadDamageReportInitial();
},
preloadAll() {
this.all = [];
if (this.relatedId == "") return;
this.loading = true;
this.getAllDamageReportsWithRelated(this.related, this.relatedId)
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchDamageReportsWithRelated(this.related, this.relatedId, this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getDamageReportFromSearch(id: string) {
return this.available.find((f) => f.id == id);
},
loadDamageReportInitial() {
if (this.modelValue == "" || this.modelValue == null) return;
this.fetchDamageReportById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
},
});
</script>

View file

@ -0,0 +1,212 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:class="useScanner ? 'pl-9!' : ''"
:displayValue="() => chosen?.name ?? ''"
@input="query = $event.target.value"
/>
<QrCodeIcon
v-if="useScanner"
class="absolute h-6 stroke-1 left-2 top-1/2 -translate-y-1/2 cursor-pointer z-10"
@click="scanCode"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && query != ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="equipment in available"
as="template"
:key="equipment.id"
:value="equipment.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ equipment.name }}<span v-if="equipment.code"> - Code: {{ equipment.code }}</span>
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw, type Prop } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import Spinner from "../Spinner.vue";
import { useEquipmentStore } from "@/stores/admin/unit/equipment/equipment";
import type { EquipmentViewModel } from "@/viewmodels/admin/unit/equipment/equipment.models";
import { QrCodeIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
useScanner: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadEquipmentInitial();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<EquipmentViewModel>,
filtered: [] as Array<EquipmentViewModel>,
chosen: undefined as undefined | EquipmentViewModel,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getEquipmentFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.loadEquipmentInitial();
this.preloadAll();
},
methods: {
...mapActions(useEquipmentStore, ["searchEquipments", "fetchEquipmentById", "getAllEquipments"]),
...mapActions(useModalStore, ["openModal"]),
preloadAll() {
this.loading = true;
this.getAllEquipments()
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchEquipments(this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getEquipmentFromSearch(id: string) {
return this.available.find((f) => f.id == id);
},
loadEquipmentInitial() {
if (this.modelValue == "") return;
this.fetchEquipmentById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
scanCode() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/scanner/ManageScanModal.vue"))),
"codeScanInput",
(result: string) => {
this.getEquipmentFromSearch(result);
}
);
},
},
});
</script>

View file

@ -0,0 +1,190 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:displayValue="() => chosen?.type ?? ''"
@input="query = $event.target.value"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && query != ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="equipmentType in available"
as="template"
:key="equipmentType.id"
:value="equipmentType.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ equipmentType.type }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import Spinner from "../Spinner.vue";
import { useEquipmentTypeStore } from "@/stores/admin/unit/equipmentType/equipmentType";
import type { EquipmentTypeViewModel } from "@/viewmodels/admin/unit/equipment/equipmentType.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadEquipmentTypeInitial();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<EquipmentTypeViewModel>,
filtered: [] as Array<EquipmentTypeViewModel>,
chosen: undefined as undefined | EquipmentTypeViewModel,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getEquipmentTypeFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.loadEquipmentTypeInitial();
this.preloadAll();
},
methods: {
...mapActions(useEquipmentTypeStore, ["searchEquipmentTypes", "fetchEquipmentTypeById", "getAllEquipmentTypes"]),
preloadAll() {
this.loading = true;
this.getAllEquipmentTypes()
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchEquipmentTypes(this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getEquipmentTypeFromSearch(id: string) {
return this.available.find((f) => f.id == id);
},
loadEquipmentTypeInitial() {
if (this.modelValue == "") return;
this.fetchEquipmentTypeById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
},
});
</script>

View file

@ -0,0 +1,194 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:displayValue="() => chosen?.title ?? ''"
@input="query = $event.target.value"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && query != ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="inspectionPlan in available"
as="template"
:key="inspectionPlan.id"
:value="inspectionPlan.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ inspectionPlan.title }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import Spinner from "../Spinner.vue";
import { useInspectionPlanStore } from "@/stores/admin/unit/inspectionPlan/inspectionPlan";
import type { InspectionPlanViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadInspectionPlanInitial();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<InspectionPlanViewModel>,
filtered: [] as Array<InspectionPlanViewModel>,
chosen: undefined as undefined | InspectionPlanViewModel,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getInspectionPlanFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.loadInspectionPlanInitial();
this.preloadAll();
},
methods: {
...mapActions(useInspectionPlanStore, [
"searchInspectionPlans",
"fetchInspectionPlanById",
"getAllInspectionPlans",
]),
preloadAll() {
this.loading = true;
this.getAllInspectionPlans()
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchInspectionPlans(this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getInspectionPlanFromSearch(id: string) {
return this.available.find((f) => f.id == id);
},
loadInspectionPlanInitial() {
if (this.modelValue == "") return;
this.fetchInspectionPlanById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
},
});
</script>

View file

@ -0,0 +1,215 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:displayValue="() => chosen?.title ?? ''"
@input="query = $event.target.value"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && query != ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="inspectionPlan in available"
as="template"
:key="inspectionPlan.id"
:value="inspectionPlan.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ inspectionPlan.title }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import Spinner from "../Spinner.vue";
import { useInspectionPlanStore } from "@/stores/admin/unit/inspectionPlan/inspectionPlan";
import type { InspectionPlanViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
relatedType: {
type: String as PropType<"vehicleType" | "equipmentType" | "wearableType">,
default: "equipmentType",
},
relatedTypeId: {
type: String,
required: true,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadInspectionPlanInitial();
},
relatedType() {
this.reload();
},
relatedTypeId() {
this.reload();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<InspectionPlanViewModel>,
filtered: [] as Array<InspectionPlanViewModel>,
chosen: undefined as undefined | InspectionPlanViewModel,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getInspectionPlanFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.reload();
},
methods: {
...mapActions(useInspectionPlanStore, [
"fetchInspectionPlanById",
"getAllInspectionPlansWithRelated",
"searchInspectionPlansWithRelated",
]),
reload() {
this.chosen = undefined;
this.filtered = [];
this.preloadAll();
this.loadInspectionPlanInitial();
},
preloadAll() {
this.all = [];
if (this.relatedTypeId == "") return;
this.loading = true;
this.getAllInspectionPlansWithRelated(this.relatedType, this.relatedTypeId)
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "" || this.relatedTypeId == "") return;
this.loading = true;
this.searchInspectionPlansWithRelated(this.relatedType, this.relatedTypeId, this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getInspectionPlanFromSearch(id: string) {
return this.available.find((f) => f.id == id);
},
loadInspectionPlanInitial() {
if (this.modelValue == "") return;
this.fetchInspectionPlanById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
},
});
</script>

View file

@ -18,7 +18,7 @@
@after-leave="query = ''" @after-leave="query = ''"
> >
<ComboboxOptions <ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm" class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
> >
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled> <ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4"> <li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">

View file

@ -0,0 +1,174 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:display-value="() => (chosen?.firstname ?? '') + ' ' + (chosen?.lastname ?? '')"
@input="query = $event.target.value"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="filtered.length === 0 && query == ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="filtered.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="member in filtered"
as="template"
:key="member.id"
:value="member.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ member.firstname }} {{ member.lastname }} {{ member.nameaffix }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useMemberStore } from "@/stores/admin/club/member/member";
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
import Spinner from "../Spinner.vue";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
//if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadMemberInitial();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
filtered: [] as Array<MemberViewModel>,
chosen: undefined as undefined | MemberViewModel,
};
},
computed: {
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getMemberFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.loadMemberInitial();
},
methods: {
...mapActions(useMemberStore, ["searchMembers", "fetchMemberById"]),
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchMembers(this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getMemberFromSearch(id: string) {
return this.filtered.find((f) => f.id == id);
},
loadMemberInitial() {
if (this.modelValue == "" || this.modelValue == null) return;
this.fetchMemberById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
},
});
</script>

View file

@ -0,0 +1,212 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:class="useScanner ? 'pl-9!' : ''"
:displayValue="() => chosen?.name ?? ''"
@input="query = $event.target.value"
/>
<QrCodeIcon
v-if="useScanner"
class="absolute h-6 stroke-1 left-2 top-1/2 -translate-y-1/2 cursor-pointer z-10"
@click="scanCode"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && query != ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="vehicle in available"
as="template"
:key="vehicle.id"
:value="vehicle.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ vehicle.name }}<span v-if="vehicle.code"> - Code: {{ vehicle.code }}</span>
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw, type Prop } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import Spinner from "../Spinner.vue";
import { useVehicleStore } from "@/stores/admin/unit/vehicle/vehicle";
import type { VehicleViewModel } from "@/viewmodels/admin/unit/vehicle/vehicle.models";
import { QrCodeIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
useScanner: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadVehicleInitial();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<VehicleViewModel>,
filtered: [] as Array<VehicleViewModel>,
chosen: undefined as undefined | VehicleViewModel,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getVehicleFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.loadVehicleInitial();
this.preloadAll();
},
methods: {
...mapActions(useVehicleStore, ["searchVehicles", "fetchVehicleById", "getAllVehicles"]),
...mapActions(useModalStore, ["openModal"]),
preloadAll() {
this.loading = true;
this.getAllVehicles()
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchVehicles(this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getVehicleFromSearch(id: string) {
return this.available.find((f) => f.id == id);
},
loadVehicleInitial() {
if (this.modelValue == "") return;
this.fetchVehicleById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
scanCode() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/scanner/ManageScanModal.vue"))),
"codeScanInput",
(result: string) => {
this.getVehicleFromSearch(result);
}
);
},
},
});
</script>

View file

@ -0,0 +1,190 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:displayValue="() => chosen?.type ?? ''"
@input="query = $event.target.value"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && query != ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="vehicleType in available"
as="template"
:key="vehicleType.id"
:value="vehicleType.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ vehicleType.type }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import Spinner from "../Spinner.vue";
import { useVehicleTypeStore } from "@/stores/admin/unit/vehicleType/vehicleType";
import type { VehicleTypeViewModel } from "@/viewmodels/admin/unit/vehicle/vehicleType.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadVehicleTypeInitial();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<VehicleTypeViewModel>,
filtered: [] as Array<VehicleTypeViewModel>,
chosen: undefined as undefined | VehicleTypeViewModel,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getVehicleTypeFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.loadVehicleTypeInitial();
this.preloadAll();
},
methods: {
...mapActions(useVehicleTypeStore, ["searchVehicleTypes", "fetchVehicleTypeById", "getAllVehicleTypes"]),
preloadAll() {
this.loading = true;
this.getAllVehicleTypes()
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchVehicleTypes(this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getVehicleTypeFromSearch(id: string) {
return this.available.find((f) => f.id == id);
},
loadVehicleTypeInitial() {
if (this.modelValue == "") return;
this.fetchVehicleTypeById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
},
});
</script>

View file

@ -0,0 +1,212 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:class="useScanner ? 'pl-9!' : ''"
:displayValue="() => chosen?.name ?? ''"
@input="query = $event.target.value"
/>
<QrCodeIcon
v-if="useScanner"
class="absolute h-6 stroke-1 left-2 top-1/2 -translate-y-1/2 cursor-pointer z-10"
@click="scanCode"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && all.length == 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && query != ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="wearable in available"
as="template"
:key="wearable.id"
:value="wearable.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ wearable.name }}<span v-if="wearable.code"> - Code: {{ wearable.code }}</span>
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw, type Prop } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import Spinner from "../Spinner.vue";
import { useWearableStore } from "@/stores/admin/unit/wearable/wearable";
import type { WearableViewModel } from "@/viewmodels/admin/unit/wearable/wearable.models";
import { QrCodeIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
useScanner: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadWearableInitial();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<WearableViewModel>,
filtered: [] as Array<WearableViewModel>,
chosen: undefined as undefined | WearableViewModel,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getWearableFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.loadWearableInitial();
this.preloadAll();
},
methods: {
...mapActions(useWearableStore, ["searchWearables", "fetchWearableById", "getAllWearables"]),
...mapActions(useModalStore, ["openModal"]),
preloadAll() {
this.loading = true;
this.getAllWearables()
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchWearables(this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getWearableFromSearch(id: string) {
return this.all.find((f) => f.id == id);
},
loadWearableInitial() {
if (this.modelValue == "") return;
this.fetchWearableById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
scanCode() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/scanner/ManageScanModal.vue"))),
"codeScanInput",
(result: string) => {
this.getWearableFromSearch(result);
}
);
},
},
});
</script>

View file

@ -0,0 +1,190 @@
<template>
<div class="w-full">
<Combobox v-model="selected" :disabled="disabled">
<ComboboxLabel>{{ title }}</ComboboxLabel>
<div class="relative mt-1">
<ComboboxInput
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
:displayValue="() => chosen?.type ?? ''"
@input="query = $event.target.value"
/>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
<TransitionRoot
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
@after-leave="query = ''"
>
<ComboboxOptions
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm z-20"
>
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
<Spinner />
<span class="font-normal block truncate">suche</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && all.length == 0" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">tippe, um zu suchen...</span>
</li>
</ComboboxOption>
<ComboboxOption v-else-if="available.length === 0 && query != ''" as="template" disabled>
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
</li>
</ComboboxOption>
<ComboboxOption
v-if="!(loading || deferingSearch)"
v-for="wearableType in available"
as="template"
:key="wearableType.id"
:value="wearableType.id"
v-slot="{ selected, active }"
>
<li
class="relative cursor-default select-none py-2 pl-10 pr-4"
:class="{
'bg-primary text-white': active,
'text-gray-900': !active,
}"
>
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ wearableType.type }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3"
:class="{ 'text-white': active, 'text-primary': !active }"
>
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</div>
</Combobox>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import Spinner from "../Spinner.vue";
import { useWearableTypeStore } from "@/stores/admin/unit/wearableType/wearableType";
import type { WearableTypeViewModel } from "@/viewmodels/admin/unit/wearable/wearableType.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: String,
disabled: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
watch: {
modelValue() {
if (this.initialLoaded) return;
this.initialLoaded = true;
this.loadWearableTypeInitial();
},
query() {
this.deferingSearch = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.deferingSearch = false;
this.search();
}, 600);
},
},
data() {
return {
initialLoaded: false as boolean,
loading: false as boolean,
deferingSearch: false as boolean,
timer: undefined as any,
query: "" as string,
all: [] as Array<WearableTypeViewModel>,
filtered: [] as Array<WearableTypeViewModel>,
chosen: undefined as undefined | WearableTypeViewModel,
};
},
computed: {
available() {
return this.query == "" ? this.all : this.filtered;
},
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getWearableTypeFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.loadWearableTypeInitial();
this.preloadAll();
},
methods: {
...mapActions(useWearableTypeStore, ["searchWearableTypes", "fetchWearableTypeById", "getAllWearableTypes"]),
preloadAll() {
this.loading = true;
this.getAllWearableTypes()
.then((res) => {
this.all = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
search() {
this.filtered = [];
if (this.query == "") return;
this.loading = true;
this.searchWearableTypes(this.query)
.then((res) => {
this.filtered = res.data;
})
.catch((err) => {})
.finally(() => {
this.loading = false;
});
},
getWearableTypeFromSearch(id: string) {
return this.all.find((f) => f.id == id);
},
loadWearableTypeInitial() {
if (this.modelValue == "") return;
this.fetchWearableTypeById(this.modelValue)
.then((res) => {
this.chosen = res.data;
})
.catch(() => {});
},
},
});
</script>

View file

@ -0,0 +1,6 @@
export enum InspectionPointEnum {
oknok = "oknok",
text = "text",
number = "number",
file = "file",
}

4
src/enums/socketEnum.ts Normal file
View file

@ -0,0 +1,4 @@
export enum SocketConnectionTypes {
scanner = "/scanner",
pscanner = "/public_scanner",
}

4
src/global.ts Normal file
View file

@ -0,0 +1,4 @@
declare global {
//type Optional<T> = T | { [K in keyof T]?: never };
type Optional<T> = T | never;
}

143
src/helpers/scanner.ts Normal file
View file

@ -0,0 +1,143 @@
import type { BarcodeFormat, DetectedBarcode } from "barcode-detector/pure";
/*** select camera ***/
export interface Camera {
label: string;
constraints: {
deviceId?: string;
facingMode: string;
};
}
export const defaultConstraintOptions: Array<Camera> = [
{ label: "rear camera", constraints: { facingMode: "environment" } },
{ label: "front camera", constraints: { facingMode: "user" } },
];
export async function getAvailableCameras(useDefault: boolean = false): Promise<Array<Camera>> {
// NOTE: on iOS we can't invoke `enumerateDevices` before the user has given
// camera access permission. `QrcodeStream` internally takes care of
// requesting the permissions. The `camera-on` event should guarantee that this
// has happened.
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(({ kind }) => kind === "videoinput");
return [
...(useDefault ? defaultConstraintOptions : []),
...videoDevices.map(({ deviceId, label }) => ({
label: `${label}`, //(ID: ${deviceId})
constraints: { deviceId, facingMode: "custom" },
})),
];
}
/*** track functons ***/
export function paintOutline(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) {
for (const detectedCode of detectedCodes) {
const [firstPoint, ...otherPoints] = detectedCode.cornerPoints;
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.moveTo(firstPoint.x, firstPoint.y);
for (const { x, y } of otherPoints) {
ctx.lineTo(x, y);
}
ctx.lineTo(firstPoint.x, firstPoint.y);
ctx.closePath();
ctx.stroke();
}
}
export function paintBoundingBox(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) {
for (const detectedCode of detectedCodes) {
const {
boundingBox: { x, y, width, height },
} = detectedCode;
ctx.lineWidth = 2;
ctx.strokeStyle = "#007bff";
ctx.strokeRect(x, y, width, height);
}
}
export function paintCenterText(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) {
for (const detectedCode of detectedCodes) {
const { boundingBox, rawValue } = detectedCode;
const centerX = boundingBox.x + boundingBox.width / 2;
const centerY = boundingBox.y + boundingBox.height / 2;
const fontSize = Math.max(12, (50 * boundingBox.width) / ctx.canvas.width);
ctx.font = `bold ${fontSize}px sans-serif`;
ctx.textAlign = "center";
ctx.lineWidth = 3;
ctx.strokeStyle = "#35495e";
ctx.strokeText(detectedCode.rawValue, centerX, centerY);
ctx.fillStyle = "#5cb984";
ctx.fillText(rawValue, centerX, centerY);
}
}
export const trackFunctionOptions = [
{ text: "nothing (default)", value: undefined },
{ text: "outline", value: paintOutline },
{ text: "centered text", value: paintCenterText },
{ text: "bounding box", value: paintBoundingBox },
{
text: "mixed",
value: (detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) => {
paintOutline(detectedCodes, ctx);
paintCenterText(detectedCodes, ctx);
},
},
];
/*** barcode formats ***/
export const barcodeFormats: Array<BarcodeFormat> = [
"aztec",
"code_128",
"code_39",
"code_93",
"codabar",
"databar",
"databar_expanded",
"data_matrix",
"dx_film_edge",
"ean_13",
"ean_8",
"itf",
"maxi_code",
"micro_qr_code",
"pdf417",
"qr_code",
"rm_qr_code",
"upc_a",
"upc_e",
"linear_codes",
"matrix_codes",
];
/*** error handling ***/
export function handleScannerError(err: Error) {
let error = `[${err.name}]: `;
if (err.name === "NotAllowedError") {
error += "you need to grant camera access permission";
} else if (err.name === "NotFoundError") {
error += "no camera on this device";
} else if (err.name === "NotSupportedError") {
error += "secure context required (HTTPS, localhost)";
} else if (err.name === "NotReadableError") {
error += "is the camera already in use?";
} else if (err.name === "OverconstrainedError") {
error += "installed cameras are not suitable";
} else if (err.name === "StreamApiNotSupportedError") {
error += "Stream API is not supported in this browser";
} else if (err.name === "InsecureContextError") {
error += "Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.";
} else {
error += err.message;
}
return error;
}

View file

@ -68,26 +68,33 @@ body {
/*:not([headlessui]):not([id*="headlessui"]):not([class*="headlessui"])*/ /*:not([headlessui]):not([id*="headlessui"]):not([class*="headlessui"])*/
button:not([class*="ql"] *):not([class*="fc"]):not([id*="headlessui-combobox"]), button:not([class*="ql"] *):not([class*="fc"]):not([id*="headlessui-combobox"]),
a[button] { [button] {
@apply cursor-pointer relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-hidden focus:ring-0; @apply cursor-pointer relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-hidden focus:ring-0;
} }
button[primary]:not([primary="false"]), button[primary]:not([primary="false"]),
a[button][primary]:not([primary="false"]) { [button][primary]:not([primary="false"]) {
@apply border-2 border-transparent text-white bg-primary hover:bg-primary; @apply border-2 border-transparent text-white bg-primary hover:bg-accent;
} }
button[primary-outline]:not([primary-outline="false"]), button[primary-outline]:not([primary-outline="false"]),
a[button][primary-outline]:not([primary-outline="false"]) { [button][primary-outline]:not([primary-outline="false"]) {
@apply border-2 border-primary text-black hover:bg-primary; @apply border-2 border-primary text-black hover:bg-primary hover:text-white;
} }
button:disabled, button:disabled,
a[button]:disabled, [button]:disabled,
a[button].disabled { [button].disabled,
[button][disabled="true"] {
@apply opacity-75 pointer-events-none; @apply opacity-75 pointer-events-none;
} }
a:disabled,
a.disabled,
a[disabled="true"] {
@apply cursor-default pointer-events-none;
}
input:not([type="checkbox"]), input:not([type="checkbox"]),
textarea, textarea,
select { select {
@ -97,12 +104,14 @@ select {
input[readonly], input[readonly],
textarea[readonly], textarea[readonly],
select[readonly] { select[readonly] {
@apply select-none; @apply select-none focus:border-gray-300 cursor-default;
/* pointer-events-none; */ /* pointer-events-none; */
} }
input[disabled], input[disabled],
textarea[disabled], textarea[disabled] {
@apply opacity-75;
}
select[disabled] { select[disabled] {
@apply opacity-75 pointer-events-none; @apply opacity-75 pointer-events-none;
} }

View file

@ -4,7 +4,7 @@ import { useMemberAwardStore } from "@/stores/admin/club/member/memberAward";
import { useMemberExecutivePositionStore } from "@/stores/admin/club/member/memberExecutivePosition"; import { useMemberExecutivePositionStore } from "@/stores/admin/club/member/memberExecutivePosition";
import { useMemberQualificationStore } from "@/stores/admin/club/member/memberQualification"; import { useMemberQualificationStore } from "@/stores/admin/club/member/memberQualification";
import { useMembershipStore } from "@/stores/admin/club/member/membership"; import { useMembershipStore } from "@/stores/admin/club/member/membership";
import { useMemberEducationStore } from "../stores/admin/club/member/memberEducation"; import { useMemberEducationStore } from "@/stores/admin/club/member/memberEducation";
export async function setMemberId(to: any, from: any, next: any) { export async function setMemberId(to: any, from: any, next: any) {
const member = useMemberStore(); const member = useMemberStore();

View file

@ -5,10 +5,23 @@ import { isAuthenticated } from "./authGuard";
import { isSetup } from "./setupGuard"; import { isSetup } from "./setupGuard";
import { abilityAndNavUpdate } from "./adminGuard"; import { abilityAndNavUpdate } from "./adminGuard";
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes"; import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
import { resetMemberStores, setMemberId } from "./memberGuard"; import { resetMemberStores, setMemberId } from "./club/memberGuard";
import { resetProtocolStores, setProtocolId } from "./protocolGuard"; import { resetProtocolStores, setProtocolId } from "./club/protocolGuard";
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard"; import { resetNewsletterStores, setNewsletterId } from "./club/newsletterGuard";
import { setBackupPage } from "./backupGuard"; import { setBackupPage } from "./management/backupGuard";
import { resetEquipmentTypeStores, setEquipmentTypeId } from "./unit/equipmentType";
import { resetEquipmentStores, setEquipmentId } from "./unit/equipment";
import { resetVehicleStores, setVehicleId } from "./unit/vehicle";
import { resetRespiratoryGearStores, setRespiratoryGearId } from "./unit/respiratoryGear";
import { resetRespiratoryWearerStores, setRespiratoryWearerId } from "./unit/respiratoryWearer";
import { resetRespiratoryMissionStores, setRespiratoryMissionId } from "./unit/respiratoryMission";
import { resetWearableStores, setWearableId } from "./unit/wearable";
import { resetInspectionPlanStores, setInspectionPlanId } from "./unit/inspectionPlan";
import { setVehicleTypeId } from "./unit/vehicleType";
import { resetInspectionStores, setInspectionId } from "./unit/inspection";
import { setWearableTypeId } from "./unit/wearableType";
import { resetDamageReportStores, setDamageReportId } from "./unit/damageReport";
import { resetRepairStores, setRepairId } from "./unit/repair";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -309,6 +322,761 @@ const router = createRouter({
}, },
], ],
}, },
{
path: "unit",
name: "admin-unit",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-default",
component: () => import("@/views/admin/ViewSelect.vue"),
meta: { type: "read", section: "unit" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: "equipment",
name: "admin-unit-equipment-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "equipment" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-equipment",
component: () => import("@/views/admin/unit/equipment/Equipment.vue"),
beforeEnter: [resetEquipmentStores],
},
{
path: "create",
name: "admin-unit-equipment-create",
component: () => import("@/views/admin/unit/equipment/CreateEquipment.vue"),
meta: { type: "create", section: "unit", module: "equipment" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: ":equipmentId",
name: "admin-unit-equipment-routing",
component: () => import("@/views/admin/unit/equipment/EquipmentRouting.vue"),
beforeEnter: [setEquipmentId],
props: true,
children: [
{
path: "overview",
name: "admin-unit-equipment-overview",
component: () => import("@/views/admin/unit/equipment/Overview.vue"),
props: true,
},
{
path: "maintenance",
name: "admin-unit-equipment-maintenance",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "repair",
name: "admin-unit-equipment-repair",
component: () => import("@/views/admin/unit/equipment/Repair.vue"),
props: true,
},
{
path: "inspection",
name: "admin-unit-equipment-inspection",
component: () => import("@/views/admin/unit/equipment/Inspection.vue"),
props: true,
},
{
path: "report",
name: "admin-unit-equipment-damage_report",
component: () => import("@/views/admin/unit/equipment/DamageReport.vue"),
props: true,
},
{
path: "edit",
name: "admin-unit-equipment-edit",
component: () => import("@/views/admin/unit/equipment/UpdateEquipment.vue"),
meta: { type: "update", section: "unit", module: "equipment" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
],
},
],
},
{
path: "vehicle",
name: "admin-unit-vehicle-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "vehicle" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-vehicle",
component: () => import("@/views/admin/unit/vehicle/Vehicle.vue"),
beforeEnter: [resetVehicleStores],
},
{
path: "create",
name: "admin-unit-vehicle-create",
component: () => import("@/views/admin/unit/vehicle/CreateVehicle.vue"),
meta: { type: "create", section: "unit", module: "vehicle" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: ":vehicleId",
name: "admin-unit-vehicle-routing",
component: () => import("@/views/admin/unit/vehicle/VehicleRouting.vue"),
beforeEnter: [setVehicleId],
props: true,
children: [
{
path: "overview",
name: "admin-unit-vehicle-overview",
component: () => import("@/views/admin/unit/vehicle/Overview.vue"),
props: true,
},
{
path: "maintenance",
name: "admin-unit-vehicle-maintenance",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "repair",
name: "admin-unit-vehicle-repair",
component: () => import("@/views/admin/unit/vehicle/Repair.vue"),
props: true,
},
{
path: "inspection",
name: "admin-unit-vehicle-inspection",
component: () => import("@/views/admin/unit/vehicle/Inspection.vue"),
props: true,
},
{
path: "report",
name: "admin-unit-vehicle-damage_report",
component: () => import("@/views/admin/unit/vehicle/DamageReport.vue"),
props: true,
},
{
path: "edit",
name: "admin-unit-vehicle-edit",
component: () => import("@/views/admin/unit/vehicle/UpdateVehicle.vue"),
meta: { type: "update", section: "unit", module: "vehicle" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
],
},
],
},
{
path: "wearable",
name: "admin-unit-wearable-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "wearable" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-wearable",
component: () => import("@/views/admin/unit/wearable/Wearable.vue"),
beforeEnter: [resetWearableStores],
},
{
path: "create",
name: "admin-unit-wearable-create",
component: () => import("@/views/admin/unit/wearable/CreateWearable.vue"),
meta: { type: "create", section: "unit", module: "wearable" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: ":wearableId",
name: "admin-unit-wearable-routing",
component: () => import("@/views/admin/unit/wearable/WearableRouting.vue"),
beforeEnter: [setWearableId],
props: true,
children: [
{
path: "overview",
name: "admin-unit-wearable-overview",
component: () => import("@/views/admin/unit/wearable/Overview.vue"),
props: true,
},
{
path: "maintenance",
name: "admin-unit-wearable-maintenance",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "repair",
name: "admin-unit-wearable-repair",
component: () => import("@/views/admin/unit/wearable/Repair.vue"),
props: true,
},
{
path: "inspection",
name: "admin-unit-wearable-inspection",
component: () => import("@/views/admin/unit/wearable/Inspection.vue"),
props: true,
},
{
path: "report",
name: "admin-unit-wearable-damage_report",
component: () => import("@/views/admin/unit/wearable/DamageReport.vue"),
props: true,
},
{
path: "edit",
name: "admin-unit-wearable-edit",
component: () => import("@/views/admin/unit/wearable/UpdateWearable.vue"),
meta: { type: "update", section: "unit", module: "wearable" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
],
},
],
},
{
path: "respiratory-gear",
name: "admin-unit-respiratory_gear-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "respiratory_gear" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-respiratory_gear",
component: () => import("@/views/admin/unit/respiratoryGear/RespiratoryGear.vue"),
beforeEnter: [resetRespiratoryGearStores],
},
{
path: "create",
name: "admin-unit-respiratory_gear-create",
component: () => import("@/views/admin/unit/respiratoryGear/CreateRespiratoryGear.vue"),
meta: { type: "create", section: "unit", module: "respiratory_gear" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: ":respiratoryGearId",
name: "admin-unit-respiratory_gear-routing",
component: () => import("@/views/admin/unit/respiratoryGear/RespiratoryGearRouting.vue"),
beforeEnter: [setRespiratoryGearId],
props: true,
children: [
{
path: "overview",
name: "admin-unit-respiratory_gear-overview",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "maintenance",
name: "admin-unit-respiratory_gear-maintenance",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "inspection",
name: "admin-unit-respiratory_gear-inspection",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "mission",
name: "admin-unit-respiratory_gear-mission",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
],
},
],
},
{
path: "respiratory-wearer",
name: "admin-unit-respiratory_wearer-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "respiratory_wearer" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-respiratory_wearer",
component: () => import("@/views/admin/unit/respiratoryWearer/RespiratoryWearer.vue"),
beforeEnter: [resetRespiratoryWearerStores],
},
{
path: "create",
name: "admin-unit-respiratory_wearer-create",
component: () => import("@/views/admin/unit/respiratoryWearer/CreateRespiratoryWearer.vue"),
meta: { type: "create", section: "unit", module: "respiratory_wearer" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: ":respiratoryWearerId",
name: "admin-unit-respiratory_wearer-routing",
component: () => import("@/views/admin/unit/respiratoryWearer/RespiratoryWearerRouting.vue"),
beforeEnter: [setRespiratoryWearerId],
props: true,
children: [
{
path: "overview",
name: "admin-unit-respiratory_wearer-overview",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "mission",
name: "admin-unit-respiratory_wearer-mission",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "education",
name: "admin-unit-respiratory_wearer-education",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "instruction",
name: "admin-unit-respiratory_wearer-instruction",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "screening",
name: "admin-unit-respiratory_wearer-screening",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "strain",
name: "admin-unit-respiratory_wearer-strain",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
],
},
],
},
{
path: "respiratory-mission",
name: "admin-unit-respiratory_mission-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "respiratory_mission" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-respiratory_mission",
component: () => import("@/views/admin/unit/respiratoryMission/RespiratoryMission.vue"),
beforeEnter: [resetRespiratoryMissionStores],
},
{
path: "create",
name: "admin-unit-respiratory_mission-create",
component: () => import("@/views/admin/unit/respiratoryMission/CreateRespiratoryMission.vue"),
meta: { type: "create", section: "unit", module: "respiratory_mission" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: ":respiratoryMissionId",
name: "admin-unit-respiratory_mission-routing",
component: () => import("@/views/admin/unit/respiratoryMission/RespiratoryMissionRouting.vue"),
beforeEnter: [setRespiratoryMissionId],
props: true,
children: [
{
path: "overview",
name: "admin-unit-respiratory_mission-overview",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "wearer",
name: "admin-unit-respiratory_mission-wearer",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
{
path: "gear",
name: "admin-unit-respiratory_mission-gear",
component: () => import("@/views/admin/ViewSelect.vue"),
props: true,
},
],
},
],
},
{
path: "damage-report",
name: "admin-unit-damage_report-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "damage_report" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-damage_report",
redirect: { name: "admin-unit-damage_report-open" },
},
{
path: "status",
name: "admin-unit-damage_report-statusrouting",
component: () => import("@/views/admin/unit/damageReport/DamageReportStatusRouting.vue"),
beforeEnter: [resetDamageReportStores],
children: [
{
path: "",
name: "admin-unit-damage_report-status",
redirect: { name: "admin-unit-damage_report-open" },
},
{
path: "open",
name: "admin-unit-damage_report-open",
component: () => import("@/views/admin/unit/damageReport/DamageReport.vue"),
},
{
path: "done",
name: "admin-unit-damage_report-done",
component: () => import("@/views/admin/unit/damageReport/DamageReportClosed.vue"),
},
],
},
{
path: ":damageReportId",
name: "admin-unit-damage_report-routing",
component: () => import("@/views/admin/unit/damageReport/DamageReportRouting.vue"),
beforeEnter: [setDamageReportId],
props: true,
children: [
{
path: "",
name: "admin-unit-damage_report-overview",
component: () => import("@/views/admin/unit/damageReport/Overview.vue"),
props: true,
},
],
},
],
},
{
path: "repair",
name: "admin-unit-repair-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "repair" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-repair",
redirect: { name: "admin-unit-repair-open" },
},
{
path: "status",
name: "admin-unit-repair-statusrouting",
component: () => import("@/views/admin/unit/repair/RepairStatusRouting.vue"),
beforeEnter: [resetRepairStores],
children: [
{
path: "",
name: "admin-unit-repair-status",
redirect: { name: "admin-unit-repair-open" },
},
{
path: "open",
name: "admin-unit-repair-open",
component: () => import("@/views/admin/unit/repair/RepairOpen.vue"),
},
{
path: "done",
name: "admin-unit-repair-done",
component: () => import("@/views/admin/unit/repair/RepairClosed.vue"),
},
],
},
{
path: "create/:type?/:relatedId?",
name: "admin-unit-repair-create",
component: () => import("@/views/admin/unit/repair/RepairCreate.vue"),
meta: { type: "create", section: "unit", module: "repair" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
{
path: "execute/:repairId",
name: "admin-unit-repair-routing",
component: () => import("@/views/admin/unit/repair/RepairRouting.vue"),
beforeEnter: [setRepairId],
props: true,
children: [
{
path: "",
name: "admin-unit-repair-overview",
component: () => import("@/views/admin/unit/repair/Overview.vue"),
props: true,
},
{
path: "reports",
name: "admin-unit-repair-reports",
component: () => import("@/views/admin/unit/repair/DamageReports.vue"),
props: true,
},
],
},
],
},
{
path: "maintenance",
name: "admin-unit-maintenance-route",
component: () => import("@/views/admin/unit/maintenance/MaintenanceRouting.vue"),
meta: { type: "read", section: "unit", module: "maintenance" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-maintenance",
component: () => import("@/views/admin/unit/maintenance/Maintenance.vue"),
},
{
path: "done",
name: "admin-unit-maintenance-done",
component: () => import("@/views/admin/unit/maintenance/Maintenance.vue"),
},
],
},
{
path: "equipment-type",
name: "admin-unit-equipment_type-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "equipment_type" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-equipment_type",
component: () => import("@/views/admin/unit/equipmentType/EquipmentType.vue"),
beforeEnter: [resetEquipmentTypeStores],
},
{
path: ":equipmentTypeId",
name: "admin-unit-equipment_type-routing",
component: () => import("@/views/admin/unit/equipmentType/EquipmentTypeRouting.vue"),
beforeEnter: [setEquipmentTypeId],
props: true,
children: [
{
path: "overview",
name: "admin-unit-equipment_type-overview",
component: () => import("@/views/admin/unit/equipmentType/Overview.vue"),
props: true,
},
{
path: "inspection-plan",
name: "admin-unit-equipment_type-inspection_plan",
component: () => import("@/views/admin/unit/equipmentType/InspectionPlans.vue"),
props: true,
},
{
path: "edit",
name: "admin-unit-equipment_type-edit",
component: () => import("@/views/admin/unit/equipmentType/UpdateEquipmentType.vue"),
meta: { type: "update", section: "unit", module: "equipment_type" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
],
},
],
},
{
path: "vehicle-type",
name: "admin-unit-vehicle_type-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "vehicle_type" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-vehicle_type",
component: () => import("@/views/admin/unit/vehicleType/VehicleType.vue"),
},
{
path: ":vehicleTypeId",
name: "admin-unit-vehicle_type-routing",
component: () => import("@/views/admin/unit/vehicleType/VehicleTypeRouting.vue"),
beforeEnter: [setVehicleTypeId],
props: true,
children: [
{
path: "overview",
name: "admin-unit-vehicle_type-overview",
component: () => import("@/views/admin/unit/vehicleType/Overview.vue"),
props: true,
},
{
path: "inspection-plan",
name: "admin-unit-vehicle_type-inspection_plan",
component: () => import("@/views/admin/unit/vehicleType/InspectionPlans.vue"),
props: true,
},
{
path: "edit",
name: "admin-unit-vehicle_type-edit",
component: () => import("@/views/admin/unit/vehicleType/UpdateVehicleType.vue"),
meta: { type: "update", section: "unit", module: "vehicle_type" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
],
},
],
},
{
path: "wearable-type",
name: "admin-unit-wearable_type-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "wearable_type" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-wearable_type",
component: () => import("@/views/admin/unit/wearableType/WearableType.vue"),
},
{
path: ":wearableTypeId",
name: "admin-unit-wearable_type-routing",
component: () => import("@/views/admin/unit/wearableType/WearableTypeRouting.vue"),
beforeEnter: [setWearableTypeId],
props: true,
children: [
{
path: "overview",
name: "admin-unit-wearable_type-overview",
component: () => import("@/views/admin/unit/wearableType/Overview.vue"),
props: true,
},
{
path: "inspection-plan",
name: "admin-unit-wearable_type-inspection_plan",
component: () => import("@/views/admin/unit/wearableType/InspectionPlans.vue"),
props: true,
},
{
path: "edit",
name: "admin-unit-wearable_type-edit",
component: () => import("@/views/admin/unit/wearableType/UpdateWearableType.vue"),
meta: { type: "update", section: "unit", module: "wearable_type" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
],
},
],
},
{
path: "inspection-plan",
name: "admin-unit-inspection_plan-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "inspection_plan" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-inspection_plan",
component: () => import("@/views/admin/unit/inspectionPlan/InspectionPlan.vue"),
beforeEnter: [resetInspectionPlanStores],
},
{
path: "create",
name: "admin-unit-inspection_plan-create",
component: () => import("@/views/admin/unit/inspectionPlan/CreateInspectionPlan.vue"),
meta: { type: "create", section: "unit", module: "inspection_plan" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: ":inspectionPlanId",
name: "admin-unit-inspection_plan-routing",
component: () => import("@/views/admin/unit/inspectionPlan/InspectionPlanRouting.vue"),
beforeEnter: [setInspectionPlanId],
props: true,
children: [
{
path: "",
name: "admin-unit-inspection_plan-overview",
component: () => import("@/views/admin/unit/inspectionPlan/Overview.vue"),
props: true,
},
{
path: "edit",
name: "admin-unit-inspection_plan-edit",
component: () => import("@/views/admin/unit/inspectionPlan/UpdateInspectionPlan.vue"),
meta: { type: "update", section: "unit", module: "inspection_plan" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
{
path: "pointedit",
name: "admin-unit-inspection_plan-pointedit",
component: () => import("@/views/admin/unit/inspectionPlan/UpdateInspectionPlanPoints.vue"),
meta: { type: "update", section: "unit", module: "inspection_plan" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
],
},
],
},
{
path: "inspection",
name: "admin-unit-inspection-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "create", section: "unit", module: "inspection" },
beforeEnter: [abilityAndNavUpdate, resetInspectionStores],
children: [
{
path: "",
name: "admin-unit-inspection-routing",
component: () => import("@/views/admin/unit/inspection/InspectionRouting.vue"),
children: [
{
path: "next",
name: "admin-unit-inspection",
component: () => import("@/views/admin/unit/inspection/NextInspections.vue"),
},
{
path: "running",
name: "admin-unit-inspection-running",
component: () => import("@/views/admin/unit/inspection/RunningInspections.vue"),
},
],
},
{
path: "start/:type?/:relatedId?/:inspectionPlanId?",
name: "admin-unit-inspection-start",
component: () => import("@/views/admin/unit/inspection/InspectionStart.vue"),
meta: { type: "create", section: "unit", module: "inspection_plan" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
{
path: "execute/:inspectionId",
name: "admin-unit-inspection-execute",
component: () => import("@/views/admin/unit/inspection/InspectionExecute.vue"),
beforeEnter: [setInspectionId],
props: true,
},
],
},
],
},
{ {
path: "configuration", path: "configuration",
name: "admin-configuration", name: "admin-configuration",
@ -778,6 +1546,29 @@ const router = createRouter({
name: "public-calendar-explain", name: "public-calendar-explain",
component: () => import("@/views/public/calendar/CalendarExplain.vue"), component: () => import("@/views/public/calendar/CalendarExplain.vue"),
}, },
{
path: "scanner",
name: "public-scanner-routing",
component: () => import("@/views/public/scanner/ScannerRouting.vue"),
children: [
{
path: "",
name: "public-scanner-select",
component: () => import("@/views/public/scanner/RoomSelect.vue"),
},
{
path: ":room",
name: "public-scanner-room",
component: () => import("@/views/public/scanner/Scanner.vue"),
props: true,
},
],
},
{
path: "damagereport",
name: "public-damage_report",
component: () => import("@/views/public/damageReport/Report.vue"),
},
], ],
}, },
{ {

View file

@ -0,0 +1,20 @@
import { useDamageReportStore } from "@/stores/admin/unit/damageReport";
export async function setDamageReportId(to: any, from: any, next: any) {
const damageReportStore = useDamageReportStore();
damageReportStore.activeDamageReport = to.params?.damageReportId ?? null;
//xystore().$reset();
next();
}
export async function resetDamageReportStores(to: any, from: any, next: any) {
const damageReportStore = useDamageReportStore();
damageReportStore.activeDamageReport = null;
damageReportStore.activeDamageReportObj = null;
//xystore().$reset();
next();
}

View file

@ -0,0 +1,27 @@
import { useEquipmentStore } from "@/stores/admin/unit/equipment/equipment";
import { useEquipmentDamageReportStore } from "@/stores/admin/unit/equipment/damageReport";
import { useEquipmentInspectionStore } from "@/stores/admin/unit/equipment/inspection";
import { useEquipmentRepairStore } from "@/stores/admin/unit/equipment/repair";
export async function setEquipmentId(to: any, from: any, next: any) {
const equipmentStore = useEquipmentStore();
equipmentStore.activeEquipment = to.params?.equipmentId ?? null;
useEquipmentDamageReportStore().$reset();
useEquipmentInspectionStore().$reset();
useEquipmentRepairStore().$reset();
next();
}
export async function resetEquipmentStores(to: any, from: any, next: any) {
const equipmentStore = useEquipmentStore();
equipmentStore.activeEquipment = null;
equipmentStore.activeEquipmentObj = null;
useEquipmentDamageReportStore().$reset();
useEquipmentInspectionStore().$reset();
useEquipmentRepairStore().$reset();
next();
}

View file

@ -0,0 +1,21 @@
import { useEquipmentTypeStore } from "@/stores/admin/unit/equipmentType/equipmentType";
import { useEquipmentTypeInspectionPlanStore } from "@/stores/admin/unit/equipmentType/inspectionPlan";
export async function setEquipmentTypeId(to: any, from: any, next: any) {
const equipmentTypeStore = useEquipmentTypeStore();
equipmentTypeStore.activeEquipmentType = to.params?.equipmentTypeId ?? null;
useEquipmentTypeInspectionPlanStore().$reset();
next();
}
export async function resetEquipmentTypeStores(to: any, from: any, next: any) {
const equipmentTypeStore = useEquipmentTypeStore();
equipmentTypeStore.activeEquipmentType = null;
equipmentTypeStore.activeEquipmentTypeObj = null;
useEquipmentTypeInspectionPlanStore().$reset();
next();
}

View file

@ -0,0 +1,20 @@
import { useInspectionStore } from "@/stores/admin/unit/inspection/inspection";
export async function setInspectionId(to: any, from: any, next: any) {
const inspectionStore = useInspectionStore();
inspectionStore.activeInspection = to.params?.inspectionId ?? null;
//useXYStore().$reset();
next();
}
export async function resetInspectionStores(to: any, from: any, next: any) {
const inspectionStore = useInspectionStore();
inspectionStore.activeInspection = null;
inspectionStore.activeInspectionObj = null;
//useXYStore().$reset();
next();
}

View file

@ -0,0 +1,21 @@
import { useInspectionPlanStore } from "@/stores/admin/unit/inspectionPlan/inspectionPlan";
import { useInspectionPointStore } from "@/stores/admin/unit/inspectionPlan/inspectionPoint";
export async function setInspectionPlanId(to: any, from: any, next: any) {
const inspectionPlanStore = useInspectionPlanStore();
inspectionPlanStore.activeInspectionPlan = to.params?.inspectionPlanId ?? null;
useInspectionPointStore().$reset();
next();
}
export async function resetInspectionPlanStores(to: any, from: any, next: any) {
const inspectionPlanStore = useInspectionPlanStore();
inspectionPlanStore.activeInspectionPlan = null;
inspectionPlanStore.activeInspectionPlanObj = null;
useInspectionPointStore().$reset();
next();
}

20
src/router/unit/repair.ts Normal file
View file

@ -0,0 +1,20 @@
import { useRepairStore } from "@/stores/admin/unit/repair";
export async function setRepairId(to: any, from: any, next: any) {
const repairStore = useRepairStore();
repairStore.activeRepair = to.params?.repairId ?? null;
//xystore().$reset();
next();
}
export async function resetRepairStores(to: any, from: any, next: any) {
const repairStore = useRepairStore();
repairStore.activeRepair = null;
repairStore.activeRepairObj = null;
//xystore().$reset();
next();
}

View file

@ -0,0 +1,20 @@
import { useRespiratoryGearStore } from "@/stores/admin/unit/respiratoryGear/respiratoryGear";
export async function setRespiratoryGearId(to: any, from: any, next: any) {
const respiratoryGearStore = useRespiratoryGearStore();
respiratoryGearStore.activeRespiratoryGear = to.params?.respiratoryGearId ?? null;
//useXYStore().$reset();
next();
}
export async function resetRespiratoryGearStores(to: any, from: any, next: any) {
const respiratoryGearStore = useRespiratoryGearStore();
respiratoryGearStore.activeRespiratoryGear = null;
respiratoryGearStore.activeRespiratoryGearObj = null;
//useXYStore().$reset();
next();
}

View file

@ -0,0 +1,20 @@
import { useRespiratoryMissionStore } from "@/stores/admin/unit/respiratoryMission/respiratoryMission";
export async function setRespiratoryMissionId(to: any, from: any, next: any) {
const respiratoryMissionStore = useRespiratoryMissionStore();
respiratoryMissionStore.activeRespiratoryMission = to.params?.respiratoryMissionId ?? null;
//useXYStore().$reset();
next();
}
export async function resetRespiratoryMissionStores(to: any, from: any, next: any) {
const respiratoryMissionStore = useRespiratoryMissionStore();
respiratoryMissionStore.activeRespiratoryMission = null;
respiratoryMissionStore.activeRespiratoryMissionObj = null;
//useXYStore().$reset();
next();
}

View file

@ -0,0 +1,20 @@
import { useRespiratoryWearerStore } from "@/stores/admin/unit/respiratoryWearer/respiratoryWearer";
export async function setRespiratoryWearerId(to: any, from: any, next: any) {
const respiratoryWearerStore = useRespiratoryWearerStore();
respiratoryWearerStore.activeRespiratoryWearer = to.params?.respiratoryWearerId ?? null;
//useXYStore().$reset();
next();
}
export async function resetRespiratoryWearerStores(to: any, from: any, next: any) {
const respiratoryWearerStore = useRespiratoryWearerStore();
respiratoryWearerStore.activeRespiratoryWearer = null;
respiratoryWearerStore.activeRespiratoryWearerObj = null;
//useXYStore().$reset();
next();
}

View file

@ -0,0 +1,27 @@
import { useVehicleStore } from "@/stores/admin/unit/vehicle/vehicle";
import { useVehicleDamageReportStore } from "@/stores/admin/unit/vehicle/damageReport";
import { useVehicleInspectionStore } from "@/stores/admin/unit/vehicle/inspection";
import { useVehicleRepairStore } from "@/stores/admin/unit/vehicle/repair";
export async function setVehicleId(to: any, from: any, next: any) {
const vehicleStore = useVehicleStore();
vehicleStore.activeVehicle = to.params?.vehicleId ?? null;
useVehicleDamageReportStore().$reset();
useVehicleInspectionStore().$reset();
useVehicleRepairStore().$reset();
next();
}
export async function resetVehicleStores(to: any, from: any, next: any) {
const vehicleStore = useVehicleStore();
vehicleStore.activeVehicle = null;
vehicleStore.activeVehicleObj = null;
useVehicleDamageReportStore().$reset();
useVehicleInspectionStore().$reset();
useVehicleRepairStore().$reset();
next();
}

View file

@ -0,0 +1,21 @@
import { useVehicleTypeStore } from "@/stores/admin/unit/vehicleType/vehicleType";
import { useVehicleTypeInspectionPlanStore } from "@/stores/admin/unit/vehicleType/inspectionPlan";
export async function setVehicleTypeId(to: any, from: any, next: any) {
const vehicleTypeStore = useVehicleTypeStore();
vehicleTypeStore.activeVehicleType = to.params?.vehicleTypeId ?? null;
useVehicleTypeInspectionPlanStore().$reset();
next();
}
export async function resetVehicleTypeStores(to: any, from: any, next: any) {
const vehicleTypeStore = useVehicleTypeStore();
vehicleTypeStore.activeVehicleType = null;
vehicleTypeStore.activeVehicleTypeObj = null;
useVehicleTypeInspectionPlanStore().$reset();
next();
}

View file

@ -0,0 +1,27 @@
import { useWearableStore } from "@/stores/admin/unit/wearable/wearable";
import { useWearableDamageReportStore } from "@/stores/admin/unit/wearable/damageReport";
import { useWearableRepairStore } from "@/stores/admin/unit/wearable/repair";
import { useWearableInspectionStore } from "@/stores/admin/unit/wearable/inspection";
export async function setWearableId(to: any, from: any, next: any) {
const wearableStore = useWearableStore();
wearableStore.activeWearable = to.params?.wearableId ?? null;
useWearableDamageReportStore().$reset();
useWearableInspectionStore().$reset();
useWearableRepairStore().$reset();
next();
}
export async function resetWearableStores(to: any, from: any, next: any) {
const wearableStore = useWearableStore();
wearableStore.activeWearable = null;
wearableStore.activeWearableObj = null;
useWearableDamageReportStore().$reset();
useWearableInspectionStore().$reset();
useWearableRepairStore().$reset();
next();
}

View file

@ -0,0 +1,21 @@
import { useWearableTypeStore } from "@/stores/admin/unit/wearableType/wearableType";
import { useWearableTypeInspectionPlanStore } from "@/stores/admin/unit/wearableType/inspectionPlan";
export async function setWearableTypeId(to: any, from: any, next: any) {
const wearableTypeStore = useWearableTypeStore();
wearableTypeStore.activeWearableType = to.params?.wearableTypeId ?? null;
useWearableTypeInspectionPlanStore().$reset();
next();
}
export async function resetWearableTypeStores(to: any, from: any, next: any) {
const wearableTypeStore = useWearableTypeStore();
wearableTypeStore.activeWearableType = null;
wearableTypeStore.activeWearableTypeObj = null;
useWearableTypeInspectionPlanStore().$reset();
next();
}

View file

@ -78,7 +78,7 @@ http.interceptors.response.use(
} }
); );
export async function refreshToken(): Promise<void> { async function refreshToken(): Promise<void> {
return new Promise<void>(async (resolve, reject) => { return new Promise<void>(async (resolve, reject) => {
await http await http
.post(`/auth/refresh`, { .post(`/auth/refresh`, {
@ -135,4 +135,4 @@ async function* streamingFetch(path: string, abort?: AbortController) {
} }
} }
export { http, newEventSource, streamingFetch, host, url }; export { http, newEventSource, streamingFetch, host, url, refreshToken };

87
src/socketManager.ts Normal file
View file

@ -0,0 +1,87 @@
import { Manager, Socket } from "socket.io-client";
import { refreshToken, url } from "./serverCom";
import { useNotificationStore } from "./stores/notification";
import { SocketConnectionTypes } from "./enums/socketEnum";
export abstract class SocketManager {
private static readonly manager = new Manager(url, {
reconnection: true,
reconnectionDelayMax: 10000,
});
private static readonly connections = new Map<SocketConnectionTypes, Socket>();
public static establishConnection(
connection: SocketConnectionTypes,
restoreAfterDisconnect: boolean = false
): Socket {
console.log("establish");
const existingSocket = this.connections.get(connection);
if (existingSocket !== undefined && existingSocket.connected) return existingSocket!;
console.log("create");
existingSocket?.removeAllListeners();
const notificationStore = useNotificationStore();
let socket = this.manager.socket(connection, {
auth: (cb) => {
cb({ token: localStorage.getItem("accessToken") });
},
});
socket.on("connect", () => {
notificationStore.push("Socket-Erfolg", `Verbindung aufgebaut`, "success");
});
socket.on("connect_error", (err) => {
this.socketHandleError(connection, err, true);
});
socket.on("disconnect", () => {
if (restoreAfterDisconnect) this.establishConnection(connection, restoreAfterDisconnect);
else notificationStore.push("Socket", `Verbindung getrennt`, "info");
});
socket.on("warning", (msg: string) => {
notificationStore.push("Socket-Warnung", msg, "warning");
});
socket.on("error", (msg: string) => {
this.socketHandleError(connection, {
name: "Error",
message: msg,
});
});
this.connections.set(connection, socket);
return socket;
}
public static getConnection(connection: SocketConnectionTypes) {
return this.connections.get(connection);
}
public static closeConnection(connection: SocketConnectionTypes) {
let socket = this.connections.get(connection);
if (socket) {
socket.removeAllListeners();
socket.disconnect();
this.connections.delete(connection);
}
}
private static socketHandleError(connection: SocketConnectionTypes, err: Error, onConnect = false) {
const notificationStore = useNotificationStore();
if (err.message == "xhr poll error") {
notificationStore.push("Socket-Netzwerk-Fehler", "Reconnect Versuch in 10s", "error");
} else if (err.message == "Token expired") {
notificationStore.push("Session", "Session wird verlängert", "info");
refreshToken()
.then(() => {
notificationStore.push("Session", "Session erfolgreich verlängert", "success");
this.closeConnection(connection);
})
.catch(() => {
notificationStore.push("Session-Fehler", "Anmeldung wurde nicht verlängert", "error");
});
} else if (onConnect) {
notificationStore.push("Socket-Fehler", `Verbindung fehlgeschlagen`, "error");
} else {
notificationStore.push("Socket-Fehler", err.message, "error");
}
}
}

View file

@ -62,6 +62,15 @@ export const useNavigationStore = defineStore("navigation", {
} as topLevelNavigationModel, } as topLevelNavigationModel,
] ]
: []), : []),
...(abilityStore.canAccessSection("unit")
? [
{
key: "unit",
title: "Wehr",
levelDefault: "equipment",
} as topLevelNavigationModel,
]
: []),
...(abilityStore.canAccessSection("configuration") ...(abilityStore.canAccessSection("configuration")
? [ ? [
{ {
@ -100,8 +109,46 @@ export const useNavigationStore = defineStore("navigation", {
...(abilityStore.can("read", "club", "listprint") ? [{ key: "listprint", title: "Liste Drucken" }] : []), ...(abilityStore.can("read", "club", "listprint") ? [{ key: "listprint", title: "Liste Drucken" }] : []),
], ],
}, },
unit: {
mainTitle: "Wehr",
main: [
...(abilityStore.can("read", "unit", "equipment") ? [{ key: "equipment", title: "Gerätschaften" }] : []),
...(abilityStore.can("read", "unit", "vehicle") ? [{ key: "vehicle", title: "Fahrzeuge" }] : []),
...(abilityStore.can("read", "unit", "wearable") ? [{ key: "wearable", title: "Kleidung" }] : []),
...(false && abilityStore.can("read", "unit", "respiratory_gear")
? [{ key: "respiratory_gear", title: "Atemschutz-Geräte" }]
: []),
...(false && abilityStore.can("read", "unit", "respiratory_wearer")
? [{ key: "respiratory_wearer", title: "Atemschutz-Träger" }]
: []),
...(false && abilityStore.can("read", "unit", "respiratory_mission")
? [{ key: "respiratory_mission", title: "Atemschutz-Einsätze" }]
: []),
...(abilityStore.can("create", "unit", "inspection") ? [{ key: "inspection", title: "Prüfungen" }] : []),
...(false && abilityStore.can("read", "unit", "maintenance")
? [{ key: "maintenance", title: "Wartungen" }]
: []),
...(abilityStore.can("read", "unit", "damage_report")
? [{ key: "damage_report", title: "Schadensmeldungen" }]
: []),
...(abilityStore.can("read", "unit", "repair") ? [{ key: "repair", title: "Reparaturen" }] : []),
{ key: "divider1", title: "Basisdaten" },
...(abilityStore.can("read", "unit", "equipment_type")
? [{ key: "equipment_type", title: "Geräte-Typen" }]
: []),
...(abilityStore.can("read", "unit", "vehicle_type")
? [{ key: "vehicle_type", title: "Fahrzeug-Arten" }]
: []),
...(abilityStore.can("read", "unit", "wearable_type")
? [{ key: "wearable_type", title: "Kleidungs-Arten" }]
: []),
...(abilityStore.can("read", "unit", "inspection_plan")
? [{ key: "inspection_plan", title: "Prüfpläne" }]
: []),
],
},
configuration: { configuration: {
mainTitle: "Einstellungen", mainTitle: "Konfiguration",
main: [ main: [
{ key: "divider1", title: "Mitgliederdaten" }, { key: "divider1", title: "Mitgliederdaten" },
...(abilityStore.can("read", "configuration", "salutation") ...(abilityStore.can("read", "configuration", "salutation")

View file

@ -0,0 +1,58 @@
import { defineStore } from "pinia";
import { v4 as uuid } from "uuid";
import { SocketManager } from "@/socketManager";
import { SocketConnectionTypes } from "@/enums/socketEnum";
import { useNotificationStore } from "../notification";
export const useScannerStore = defineStore("scanner", {
state: () => {
return {
inUse: false as boolean,
roomId: undefined as undefined | string,
results: [] as Array<string>,
connectedDevices: 0 as number,
};
},
actions: {
startSession() {
if (this.inUse) return;
const notificationStore = useNotificationStore();
this.roomId = uuid();
this.inUse = true;
let connection = SocketManager.establishConnection(SocketConnectionTypes.scanner);
connection.on("connect", () => {
SocketManager.getConnection(SocketConnectionTypes.scanner)?.emit("session:create", this.roomId);
});
connection.on("status-session:create", () => {
notificationStore.push("Socket-Erfolg", `Scan-Session gestartet`, "success");
});
connection.on("status-session:close", () => {
notificationStore.push("Socket", `Scan-Session beendet`, "info");
SocketManager.getConnection(SocketConnectionTypes.scanner)?.disconnect();
});
connection.on("package-scanner_join", (socketId: string) => {
this.connectedDevices++;
notificationStore.push("Scan-Verbindung", `Neuer Scanner verbunden`, "info");
});
connection.on("package-scanner_leave", (socketId: string) => {
this.connectedDevices--;
notificationStore.push("Scan-Verbindung", `Scanner getrennt`, "info");
});
connection.on("package-scan_receive", (result: string) => {
this.results.push(result);
notificationStore.push("Scan", `Neuen Scan erhalten`, "info");
});
},
endSession() {
this.inUse = false;
this.roomId = undefined;
this.results = [];
SocketManager.getConnection(SocketConnectionTypes.scanner)?.emit("session:close");
},
removeElementFromResults(el: string) {
this.results = this.results.filter((result) => result !== el);
},
},
});

View file

@ -0,0 +1,123 @@
import { defineStore } from "pinia";
import type { DamageReportViewModel, UpdateDamageReportViewModel } from "@/viewmodels/admin/unit/damageReport.models";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
export const useDamageReportStore = defineStore("damageReport", {
state: () => {
return {
damageReports: [] as Array<DamageReportViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
activeDamageReport: null as string | null,
activeDamageReportObj: null as DamageReportViewModel | null,
loadingActive: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
formatQueryReturnToPagination(result: AxiosResponse<any, any>, offset: number) {
this.totalCount = result.data.total;
result.data.damageReports
.filter((elem: DamageReportViewModel) => this.damageReports.findIndex((m) => m.id == elem.id) == -1)
.map((elem: DamageReportViewModel, index: number): DamageReportViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: DamageReportViewModel & { tab_pos: number }) => {
this.damageReports.push(elem);
});
},
fetchOpenDamageReports(offset = 0, count = 25, search = "", clear = false) {
if (clear) this.damageReports = [];
this.loading = "loading";
http
.get(`/admin/damageReport?done=false&offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.then((result) => {
this.formatQueryReturnToPagination(result, offset);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
fetchDoneDamageReports(offset = 0, count = 25, search = "", clear = false) {
if (clear) this.damageReports = [];
this.loading = "loading";
http
.get(`/admin/damageReport?done=true&offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.then((result) => {
this.formatQueryReturnToPagination(result, offset);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
async getAllDamageReports(): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/damageReport?noLimit=true`).then((res) => {
return { ...res, data: res.data.damageReports };
});
},
async getDamageReportsByIds(ids: Array<string>): Promise<AxiosResponse<any, any>> {
return await http
.post(`/admin/damageReport/ids`, {
ids,
})
.then((res) => {
return { ...res, data: res.data.damageReports };
});
},
async searchDamageReports(search: string): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/damageReport?search=${search}&noLimit=true`).then((res) => {
return { ...res, data: res.data.damageReports };
});
},
async getAllDamageReportsWithRelated(
related: "vehicle" | "equipment" | "wearable",
relatedId: string
): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/damageReport/${related}/${relatedId}?noLimit=true`).then((res) => {
return { ...res, data: res.data.damageReports };
});
},
async searchDamageReportsWithRelated(
related: "vehicle" | "equipment" | "wearable",
relatedId: string,
search: string
): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/damageReport/${related}/${relatedId}?search=${search}&noLimit=true`).then((res) => {
return { ...res, data: res.data.damageReports };
});
},
fetchDamageReportByActiveId() {
this.loadingActive = "loading";
http
.get(`/admin/damageReport/${this.activeDamageReport}`)
.then((res) => {
this.activeDamageReportObj = res.data;
this.loadingActive = "fetched";
})
.catch((err) => {
this.loadingActive = "failed";
});
},
fetchDamageReportById(id: string) {
return http.get(`/admin/damageReport/${id}`);
},
loadDamageReportImage(url: string) {
return http.get(`/admin/damageReport/${this.activeDamageReportObj?.id}/${url}`, {
responseType: "blob",
});
},
async updateDamageReport(damageReport: UpdateDamageReportViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/damageReport/${damageReport.id}`, {
status: damageReport.status,
noteByWorker: damageReport.noteByWorker,
done: damageReport.done,
});
return result;
},
},
});

View file

@ -0,0 +1,43 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import { useEquipmentStore } from "./equipment";
import type { DamageReportViewModel } from "@/viewmodels/admin/unit/damageReport.models";
export const useEquipmentDamageReportStore = defineStore("equipmentDamageReport", {
state: () => {
return {
damageReports: [] as Array<DamageReportViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchDamageReportForEquipment(offset = 0, count = 25, search = "", clear = false) {
const equipmentId = useEquipmentStore().activeEquipment;
if (clear) this.damageReports = [];
this.loading = "loading";
http
.get(
`/admin/damagereport/equipment/${equipmentId}?offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`
)
.then((result) => {
this.totalCount = result.data.total;
result.data.damageReports
.filter((elem: DamageReportViewModel) => this.damageReports.findIndex((m) => m.id == elem.id) == -1)
.map((elem: DamageReportViewModel, index: number): DamageReportViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: DamageReportViewModel & { tab_pos: number }) => {
this.damageReports.push(elem);
});
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
},
});

View file

@ -0,0 +1,108 @@
import { defineStore } from "pinia";
import type {
EquipmentViewModel,
CreateEquipmentViewModel,
UpdateEquipmentViewModel,
} from "@/viewmodels/admin/unit/equipment/equipment.models";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
export const useEquipmentStore = defineStore("equipment", {
state: () => {
return {
equipments: [] as Array<EquipmentViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
activeEquipment: null as string | null,
activeEquipmentObj: null as EquipmentViewModel | null,
loadingActive: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchEquipments(offset = 0, count = 25, search = "", clear = false) {
if (clear) this.equipments = [];
this.loading = "loading";
http
.get(`/admin/equipment?offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.then((result) => {
this.totalCount = result.data.total;
result.data.equipments
.filter((elem: EquipmentViewModel) => this.equipments.findIndex((m) => m.id == elem.id) == -1)
.map((elem: EquipmentViewModel, index: number): EquipmentViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: EquipmentViewModel & { tab_pos: number }) => {
this.equipments.push(elem);
});
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
async getAllEquipments(): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/equipment?noLimit=true`).then((res) => {
return { ...res, data: res.data.equipments };
});
},
async getEquipmentsByIds(ids: Array<string>): Promise<AxiosResponse<any, any>> {
return await http
.post(`/admin/equipment/ids`, {
ids,
})
.then((res) => {
return { ...res, data: res.data.equipments };
});
},
async searchEquipments(search: string): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/equipment?search=${search}&noLimit=true`).then((res) => {
return { ...res, data: res.data.equipments };
});
},
fetchEquipmentByActiveId() {
this.loadingActive = "loading";
http
.get(`/admin/equipment/${this.activeEquipment}`)
.then((res) => {
this.activeEquipmentObj = res.data;
this.loadingActive = "fetched";
})
.catch((err) => {
this.loadingActive = "failed";
});
},
fetchEquipmentById(id: string) {
return http.get(`/admin/equipment/${id}`);
},
async createEquipment(equipment: CreateEquipmentViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/equipment`, {
equipmentTypeId: equipment.equipmentTypeId,
name: equipment.name,
code: equipment.code,
location: equipment.location,
commissioned: equipment.commissioned,
});
this.fetchEquipments();
return result;
},
async updateActiveEquipment(equipment: UpdateEquipmentViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/equipment/${equipment.id}`, {
name: equipment.name,
code: equipment.code,
location: equipment.location,
commissioned: equipment.commissioned,
decommissioned: equipment.decommissioned,
});
this.fetchEquipments();
return result;
},
async deleteEquipment(equipment: number): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/equipment/${equipment}`);
this.fetchEquipments();
return result;
},
},
});

View file

@ -0,0 +1,41 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import type { InspectionViewModel } from "@/viewmodels/admin/unit/inspection/inspection.models";
import { useEquipmentStore } from "./equipment";
export const useEquipmentInspectionStore = defineStore("equipmentInspection", {
state: () => {
return {
inspections: [] as Array<InspectionViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchInspectionForEquipment(offset = 0, count = 25, clear = false) {
const equipmentId = useEquipmentStore().activeEquipment;
if (clear) this.inspections = [];
this.loading = "loading";
http
.get(`/admin/inspection/equipment/${equipmentId}?offset=${offset}&count=${count}`)
.then((result) => {
this.totalCount = result.data.total;
result.data.inspections
.filter((elem: InspectionViewModel) => this.inspections.findIndex((m) => m.id == elem.id) == -1)
.map((elem: InspectionViewModel, index: number): InspectionViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: InspectionViewModel & { tab_pos: number }) => {
this.inspections.push(elem);
});
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
},
});

View file

@ -0,0 +1,43 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import { useEquipmentStore } from "./equipment";
import type { RepairViewModel } from "@/viewmodels/admin/unit/repair.models";
export const useEquipmentRepairStore = defineStore("equipmentRepair", {
state: () => {
return {
repairs: [] as Array<RepairViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchRepairForEquipment(offset = 0, count = 25, search = "", clear = false) {
const equipmentId = useEquipmentStore().activeEquipment;
if (clear) this.repairs = [];
this.loading = "loading";
http
.get(
`/admin/repair/equipment/${equipmentId}?offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`
)
.then((result) => {
this.totalCount = result.data.total;
result.data.repairs
.filter((elem: RepairViewModel) => this.repairs.findIndex((m) => m.id == elem.id) == -1)
.map((elem: RepairViewModel, index: number): RepairViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: RepairViewModel & { tab_pos: number }) => {
this.repairs.push(elem);
});
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
},
});

View file

@ -0,0 +1,93 @@
import { defineStore } from "pinia";
import type {
EquipmentTypeViewModel,
CreateEquipmentTypeViewModel,
UpdateEquipmentTypeViewModel,
} from "@/viewmodels/admin/unit/equipment/equipmentType.models";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
export const useEquipmentTypeStore = defineStore("equipmentType", {
state: () => {
return {
equipmentTypes: [] as Array<EquipmentTypeViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
activeEquipmentType: null as string | null,
activeEquipmentTypeObj: null as EquipmentTypeViewModel | null,
loadingActive: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchEquipmentTypes(offset = 0, count = 25, search = "", clear = false) {
if (clear) this.equipmentTypes = [];
this.loading = "loading";
http
.get(`/admin/equipmentType?offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.then((result) => {
this.totalCount = result.data.total;
result.data.equipmentTypes
.filter((elem: EquipmentTypeViewModel) => this.equipmentTypes.findIndex((m) => m.id == elem.id) == -1)
.map((elem: EquipmentTypeViewModel, index: number): EquipmentTypeViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: EquipmentTypeViewModel & { tab_pos: number }) => {
this.equipmentTypes.push(elem);
});
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
async getAllEquipmentTypes(): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/equipmentType?noLimit=true`).then((res) => {
return { ...res, data: res.data.equipmentTypes };
});
},
async searchEquipmentTypes(search: string): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/equipmentType?search=${search}&noLimit=true`).then((res) => {
return { ...res, data: res.data.equipmentTypes };
});
},
fetchEquipmentTypeByActiveId() {
this.loadingActive = "loading";
http
.get(`/admin/equipmentType/${this.activeEquipmentType}`)
.then((res) => {
this.activeEquipmentTypeObj = res.data;
this.loadingActive = "fetched";
})
.catch((err) => {
this.loadingActive = "failed";
});
},
fetchEquipmentTypeById(id: string) {
return http.get(`/admin/equipmentType/${id}`);
},
async createEquipmentType(equipmentType: CreateEquipmentTypeViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/equipmentType`, {
type: equipmentType.type,
description: equipmentType.description,
});
this.fetchEquipmentTypes();
return result;
},
async updateActiveEquipmentType(equipmentType: UpdateEquipmentTypeViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/equipmentType/${equipmentType.id}`, {
type: equipmentType.type,
description: equipmentType.description,
});
this.fetchEquipmentTypes();
return result;
},
async deleteEquipmentType(equipmentType: number): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/equipmentType/${equipmentType}`);
this.fetchEquipmentTypes();
return result;
},
},
});

View file

@ -0,0 +1,28 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import type { InspectionPlanViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
import { useEquipmentTypeStore } from "./equipmentType";
export const useEquipmentTypeInspectionPlanStore = defineStore("equipmentTypeInspectionPlan", {
state: () => {
return {
inspectionPlans: [] as Array<InspectionPlanViewModel>,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchInspectionPlanForEquipmentType() {
const equipmentTypeId = useEquipmentTypeStore().activeEquipmentType;
this.loading = "loading";
http
.get(`/admin/inspectionPlan/equipmentType/${equipmentTypeId}?noLimit=true`)
.then((result) => {
this.inspectionPlans = result.data.inspectionPlans;
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
},
});

Some files were not shown because too many files have changed in this diff Show more