Compare commits
90 commits
main
...
v1.8.0-bet
Author | SHA1 | Date | |
---|---|---|---|
1570c174cc | |||
9b29cd7ba3 | |||
2c541eb5aa | |||
8e64b549d7 | |||
583233b95e | |||
91ad11e20c | |||
0b16599d2a | |||
fed08e0232 | |||
4da2e9c792 | |||
8c4a53bc17 | |||
6c1140f7d2 | |||
789272dc37 | |||
c79d5bb1cd | |||
b5a3ff4dc6 | |||
b56347c172 | |||
b9b0381356 | |||
c5687729a9 | |||
c29da7ca1d | |||
31b7ec9c3e | |||
5ea5a0160a | |||
93a04abee1 | |||
6aae09cd03 | |||
766114bf53 | |||
3ec74e751a | |||
304ae1147e | |||
3e47d3ebf6 | |||
ed947e5777 | |||
9ef76a7c26 | |||
a6892bac85 | |||
6fa1c67574 | |||
6c8c0939b4 | |||
98bf4532aa | |||
5d26885da3 | |||
d96c73d5b1 | |||
adfe64d1f2 | |||
74b05ee97f | |||
1409cf8045 | |||
23bdde5fc2 | |||
eb4d338583 | |||
e7078960ba | |||
fe0f31ce6b | |||
6575948841 | |||
50fa0128ea | |||
b7f22357ec | |||
ddeac1aa26 | |||
6c8d57a7e5 | |||
b280654f92 | |||
bd1fdaa234 | |||
ab3083c18d | |||
98fd7b64d2 | |||
30baca2567 | |||
9b2ab1923e | |||
d70edd0086 | |||
def32b786c | |||
b83b22d806 | |||
43f46c0fad | |||
b6e80b358a | |||
dd93f7a7b8 | |||
9ee1cca46d | |||
70e9b47483 | |||
b359044cb5 | |||
ee700d9e02 | |||
a49babe48d | |||
0ea9601ea3 | |||
8766bbce08 | |||
05555425ce | |||
d5c33d899f | |||
bdc139f37f | |||
4ebacc5f52 | |||
6ad2da1c16 | |||
835e6ef8db | |||
c4a67fd11a | |||
46432fbf7d | |||
e25d91802c | |||
00fad29b25 | |||
b6c68d2205 | |||
553eeb7bfb | |||
716823f536 | |||
5641dbb57f | |||
8be88a5245 | |||
f951a1cd4c | |||
4faf93c3ce | |||
36ca3d90a7 | |||
5faa4b7906 | |||
45fe7b34c3 | |||
3e87bbc267 | |||
b6d6dd0796 | |||
2a77a950f5 | |||
2b3231e26c | |||
4338f58dea |
220 changed files with 16046 additions and 134 deletions
68
package-lock.json
generated
68
package-lock.json
generated
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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> -->
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
|
@ -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"><zahl>-(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>
|
|
@ -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>
|
33
src/components/admin/unit/equipment/EquipmentListItem.vue
Normal file
33
src/components/admin/unit/equipment/EquipmentListItem.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
91
src/components/admin/unit/inspection/FileInput.vue
Normal file
91
src/components/admin/unit/inspection/FileInput.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
80
src/components/admin/unit/inspection/NumberInput.vue
Normal file
80
src/components/admin/unit/inspection/NumberInput.vue
Normal 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>
|
71
src/components/admin/unit/inspection/OkNotOk.vue
Normal file
71
src/components/admin/unit/inspection/OkNotOk.vue
Normal 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>
|
55
src/components/admin/unit/inspection/TextInput.vue
Normal file
55
src/components/admin/unit/inspection/TextInput.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
39
src/components/admin/unit/repair/RepairListItem.vue
Normal file
39
src/components/admin/unit/repair/RepairListItem.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
31
src/components/admin/unit/vehicle/VehicleListItem.vue
Normal file
31
src/components/admin/unit/vehicle/VehicleListItem.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
33
src/components/admin/unit/wearable/WearableListItem.vue
Normal file
33
src/components/admin/unit/wearable/WearableListItem.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
132
src/components/public/damageReport/CheckEntry.vue
Normal file
132
src/components/public/damageReport/CheckEntry.vue
Normal 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>
|
103
src/components/public/damageReport/InputData.vue
Normal file
103
src/components/public/damageReport/InputData.vue
Normal 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>
|
28
src/components/public/damageReport/Result.vue
Normal file
28
src/components/public/damageReport/Result.vue
Normal 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>
|
116
src/components/public/damageReport/SelectGear.vue
Normal file
116
src/components/public/damageReport/SelectGear.vue
Normal 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>
|
19
src/components/public/damageReport/Start.vue
Normal file
19
src/components/public/damageReport/Start.vue
Normal 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>
|
64
src/components/scanner/ManageScanModal.vue
Normal file
64
src/components/scanner/ManageScanModal.vue
Normal 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>
|
91
src/components/scanner/Phone.vue
Normal file
91
src/components/scanner/Phone.vue
Normal 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>
|
57
src/components/scanner/ScanInput.vue
Normal file
57
src/components/scanner/ScanInput.vue
Normal 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>
|
51
src/components/scanner/ScanQRCodeModal.vue
Normal file
51
src/components/scanner/ScanQRCodeModal.vue
Normal 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>
|
41
src/components/scanner/ScanResultsModal.vue
Normal file
41
src/components/scanner/ScanResultsModal.vue
Normal 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>
|
42
src/components/scanner/ScanSidebarInfo.vue
Normal file
42
src/components/scanner/ScanSidebarInfo.vue
Normal 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>
|
77
src/components/scanner/Scanner.vue
Normal file
77
src/components/scanner/Scanner.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
212
src/components/search/EquipmentSearchSelect.vue
Normal file
212
src/components/search/EquipmentSearchSelect.vue
Normal 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>
|
190
src/components/search/EquipmentTypeSearchSelect.vue
Normal file
190
src/components/search/EquipmentTypeSearchSelect.vue
Normal 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>
|
194
src/components/search/InspectionPlanSearchSelect.vue
Normal file
194
src/components/search/InspectionPlanSearchSelect.vue
Normal 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>
|
215
src/components/search/InspectionPlanSearchSelectWithRelated.vue
Normal file
215
src/components/search/InspectionPlanSearchSelectWithRelated.vue
Normal 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>
|
|
@ -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">
|
174
src/components/search/MemberSearchSelectSingle.vue
Normal file
174
src/components/search/MemberSearchSelectSingle.vue
Normal 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>
|
212
src/components/search/VehicleSearchSelect.vue
Normal file
212
src/components/search/VehicleSearchSelect.vue
Normal 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>
|
190
src/components/search/VehicleTypeSearchSelect.vue
Normal file
190
src/components/search/VehicleTypeSearchSelect.vue
Normal 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>
|
212
src/components/search/WearableSearchSelect.vue
Normal file
212
src/components/search/WearableSearchSelect.vue
Normal 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>
|
190
src/components/search/WearableTypeSearchSelect.vue
Normal file
190
src/components/search/WearableTypeSearchSelect.vue
Normal 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>
|
6
src/enums/inspectionEnum.ts
Normal file
6
src/enums/inspectionEnum.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export enum InspectionPointEnum {
|
||||||
|
oknok = "oknok",
|
||||||
|
text = "text",
|
||||||
|
number = "number",
|
||||||
|
file = "file",
|
||||||
|
}
|
4
src/enums/socketEnum.ts
Normal file
4
src/enums/socketEnum.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export enum SocketConnectionTypes {
|
||||||
|
scanner = "/scanner",
|
||||||
|
pscanner = "/public_scanner",
|
||||||
|
}
|
4
src/global.ts
Normal file
4
src/global.ts
Normal 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
143
src/helpers/scanner.ts
Normal 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;
|
||||||
|
}
|
27
src/main.css
27
src/main.css
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
@ -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"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
20
src/router/unit/damageReport.ts
Normal file
20
src/router/unit/damageReport.ts
Normal 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();
|
||||||
|
}
|
27
src/router/unit/equipment.ts
Normal file
27
src/router/unit/equipment.ts
Normal 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();
|
||||||
|
}
|
21
src/router/unit/equipmentType.ts
Normal file
21
src/router/unit/equipmentType.ts
Normal 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();
|
||||||
|
}
|
20
src/router/unit/inspection.ts
Normal file
20
src/router/unit/inspection.ts
Normal 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();
|
||||||
|
}
|
21
src/router/unit/inspectionPlan.ts
Normal file
21
src/router/unit/inspectionPlan.ts
Normal 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
20
src/router/unit/repair.ts
Normal 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();
|
||||||
|
}
|
20
src/router/unit/respiratoryGear.ts
Normal file
20
src/router/unit/respiratoryGear.ts
Normal 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();
|
||||||
|
}
|
20
src/router/unit/respiratoryMission.ts
Normal file
20
src/router/unit/respiratoryMission.ts
Normal 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();
|
||||||
|
}
|
20
src/router/unit/respiratoryWearer.ts
Normal file
20
src/router/unit/respiratoryWearer.ts
Normal 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();
|
||||||
|
}
|
27
src/router/unit/vehicle.ts
Normal file
27
src/router/unit/vehicle.ts
Normal 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();
|
||||||
|
}
|
21
src/router/unit/vehicleType.ts
Normal file
21
src/router/unit/vehicleType.ts
Normal 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();
|
||||||
|
}
|
27
src/router/unit/wearable.ts
Normal file
27
src/router/unit/wearable.ts
Normal 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();
|
||||||
|
}
|
21
src/router/unit/wearableType.ts
Normal file
21
src/router/unit/wearableType.ts
Normal 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();
|
||||||
|
}
|
|
@ -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
87
src/socketManager.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
58
src/stores/admin/scanner.ts
Normal file
58
src/stores/admin/scanner.ts
Normal 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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
123
src/stores/admin/unit/damageReport.ts
Normal file
123
src/stores/admin/unit/damageReport.ts
Normal 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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
43
src/stores/admin/unit/equipment/damageReport.ts
Normal file
43
src/stores/admin/unit/equipment/damageReport.ts
Normal 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";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
108
src/stores/admin/unit/equipment/equipment.ts
Normal file
108
src/stores/admin/unit/equipment/equipment.ts
Normal 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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
41
src/stores/admin/unit/equipment/inspection.ts
Normal file
41
src/stores/admin/unit/equipment/inspection.ts
Normal 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";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
43
src/stores/admin/unit/equipment/repair.ts
Normal file
43
src/stores/admin/unit/equipment/repair.ts
Normal 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";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
93
src/stores/admin/unit/equipmentType/equipmentType.ts
Normal file
93
src/stores/admin/unit/equipmentType/equipmentType.ts
Normal 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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
28
src/stores/admin/unit/equipmentType/inspectionPlan.ts
Normal file
28
src/stores/admin/unit/equipmentType/inspectionPlan.ts
Normal 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
Loading…
Add table
Add a link
Reference in a new issue