fill out inspection and inspection plan

This commit is contained in:
Julian Krauser 2025-07-10 10:49:44 +02:00
parent 23bdde5fc2
commit 1409cf8045
12 changed files with 374 additions and 77 deletions

View file

@ -2,49 +2,80 @@
<MainTemplate>
<template #topBar>
<h1 class="font-bold text-xl min-h-8">
Prüfung durchführen: {{ activeInspectionObj?.related.name }} - {{ activeInspectionObj?.inspectionPlan.title }}
Prüfung durchführen: {{ activeInspectionObj?.related.name }}
<small v-if="activeInspectionObj?.related.code">({{ activeInspectionObj?.related.code }})</small> -
{{ activeInspectionObj?.inspectionPlan.title }}
</h1>
</template>
<template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<form
<div
v-else-if="activeInspectionObj != null"
class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto"
@submit.prevent="triggerUpdate"
class="flex flex-col gap-4 py-2 grow w-full max-w-xl mx-auto overflow-hidden"
>
<div v-for="point in points" :key="point.title">
<OkNotOk
v-if="point.type == InspectionPointEnum.oknok"
:inspectionPoint="point"
:modelValue="boolPointResult(point.id)"
@update:model-value="(val) => updateCheckResult(point.id, val)"
/>
<ResultInput
v-else
:inspectionPoint="point"
:modelValue="pointResult(point.id)"
@update:model-value="(val) => updateCheckResult(point.id, val)"
/>
<div class="flex flex-col gap-2 grow overflow-y-scroll">
<div v-for="point in points" :key="point.title" class="contents">
<OkNotOk
v-if="point.type == InspectionPointEnum.oknok"
:inspectionPoint="point"
:modelValue="boolPointResult(point.id)"
@update:model-value="(val) => updateCheckResult(point.id, val)"
/>
<NumberInput
v-else-if="point.type == InspectionPointEnum.number"
:inspectionPoint="point"
:modelValue="pointResult(point.id)"
@update:model-value="(val) => updateCheckResult(point.id, val)"
/>
<TextInput
v-else-if="point.type == InspectionPointEnum.text"
:inspectionPoint="point"
:modelValue="pointResult(point.id)"
@update:model-value="(val) => updateCheckResult(point.id, val)"
/>
<FileInput
v-else-if="point.type == InspectionPointEnum.file"
:inspectionPoint="point"
:modelValue="pointResult(point.id)"
@update:model-value="(val) => updateCheckResult(point.id, val)"
@update:upload="(val) => (fileStore[point.id] = val)"
/>
</div>
</div>
<div class="flex flex-row justify-end gap-2">
<div class="flex flex-row justify-end flex-wrap min-h-fit gap-2">
<button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm">
verwerfen
</button>
<button primary type="submit" class="w-fit!" :disabled="status == 'loading' || canSaveOrReset">
speichern
<button
primary
type="submit"
class="w-fit!"
:disabled="status == 'loading' || canSaveOrReset"
@click="triggerUpdate"
>
zwischenspeichern
</button>
<button
primary
type="button"
class="w-fit!"
:disabled="status == 'loading' || !canSaveOrReset || !isEverythingFilled"
@click.prevent="finishInspection"
>
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>
</form>
</div>
</template>
</MainTemplate>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import Spinner from "@/components/Spinner.vue";
@ -52,11 +83,14 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useInspectionStore } from "@/stores/admin/unit/inspection/inspection";
import OkNotOk from "@/components/admin/unit/inspection/OkNotOk.vue";
import ResultInput from "@/components/admin/unit/inspection/ResultInput.vue";
import type { InspectionPointResultViewModel } from "@/viewmodels/admin/unit/inspection/inspection.models";
import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isequal";
import { InspectionPointEnum } from "@/enums/inspectionEnum";
import { useModalStore } from "@/stores/modal";
import NumberInput from "@/components/admin/unit/inspection/NumberInput.vue";
import TextInput from "@/components/admin/unit/inspection/TextInput.vue";
import FileInput from "@/components/admin/unit/inspection/FileInput.vue";
</script>
<script lang="ts">
@ -80,6 +114,7 @@ export default defineComponent({
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: null as any,
checks: [] as Array<InspectionPointResultViewModel>,
fileStore: {} as { [key: string]: File | null },
};
},
computed: {
@ -99,13 +134,25 @@ export default defineComponent({
};
},
boolPointResult() {
return (pointId: string): "true" | "false" => {
return this.pointResult(pointId) == "true" ? "true" : "false";
return (pointId: string): "true" | "false" | "" => {
let value = this.pointResult(pointId);
return ["true", "false"].includes(value) ? (value as "true" | "false") : "";
};
},
isEverythingFilled() {
return this.points.every((p) => {
if (p.type == InspectionPointEnum.file) {
return this.pointResult(p.id) == "set" ? !!this.fileStore[p.id] : !!this.pointResult(p.id);
} else if (p.type == InspectionPointEnum.oknok) {
return this.boolPointResult(p.id) != "";
} else {
return !!this.pointResult(p.id);
}
});
},
},
mounted() {
this.fetchItem();
this.fetchInspectionByActiveId();
},
beforeUnmount() {
try {
@ -114,12 +161,10 @@ export default defineComponent({
},
methods: {
...mapActions(useInspectionStore, ["fetchInspectionByActiveId"]),
...mapActions(useModalStore, ["openModal"]),
resetForm() {
this.checks = cloneDeep(this.activeInspectionObj?.checks ?? []);
},
fetchItem() {
this.fetchInspectionByActiveId();
},
updateCheckResult(id: string, value: string) {
if (this.activeInspectionObj == null) return;
@ -127,7 +172,6 @@ export default defineComponent({
if (index == -1) {
this.checks.push({
inspectionId: this.activeInspectionObj.id,
inspectionVersionedPlanId: this.activeInspectionObj.inspectionVersionedPlanId,
inspectionPointId: id,
value: value,
});
@ -142,7 +186,6 @@ export default defineComponent({
resolve();
})
.then(() => {
this.fetchItem();
this.status = { status: "success" };
})
.catch((err) => {
@ -154,6 +197,11 @@ export default defineComponent({
}, 2000);
});
},
finishInspection() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/inspection/InspectionFinishModal.vue")))
);
},
},
});
</script>

View file

@ -1,7 +1,7 @@
<template>
<MainTemplate title="Prüfung starten">
<template #main>
<form class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto" @submit.prevent="">
<form class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto" @submit.prevent="createNewInspection">
<div class="flex flex-row">
<div
v-for="tab in tabs"
@ -44,6 +44,11 @@
<input id="nextInspection" type="date" />
</div>
<div>
<label for="context"> Kontext </label>
<textarea id="context" class="h-24"></textarea>
</div>
<div class="flex flex-row justify-end gap-2">
<RouterLink
:to="{ name: 'admin-unit-inspection_plan' }"
@ -80,6 +85,8 @@ import InspectionPlanSearchSelectWithRelated from "@/components/search/Inspectio
import { useEquipmentStore } from "@/stores/admin/unit/equipment/equipment";
import { useVehicleStore } from "@/stores/admin/unit/vehicle/vehicle";
import { useWearableStore } from "@/stores/admin/unit/wearable/wearable";
import type { CreateInspectionViewModel } from "@/viewmodels/admin/unit/inspection/inspection.models";
import { useInspectionStore } from "@/stores/admin/unit/inspection/inspection";
</script>
<script lang="ts">
@ -94,6 +101,7 @@ export default defineComponent({
},
watch: {
inspectionPlan() {
this.selectedInspectionPlan = undefined;
if (this.inspectionPlan != "") this.getInspectionPlanData();
},
related() {
@ -133,6 +141,7 @@ export default defineComponent({
}
},
methods: {
...mapActions(useInspectionStore, ["createInspection"]),
...mapActions(useInspectionPlanStore, ["fetchInspectionPlanById"]),
...mapActions(useEquipmentStore, ["fetchEquipmentById"]),
...mapActions(useVehicleStore, ["fetchVehicleById"]),
@ -175,6 +184,34 @@ export default defineComponent({
});
}
},
createNewInspection(e: any) {
if (this.related == "" || this.inspectionPlan == "") return;
let formData = e.target.elements;
let createInspection: CreateInspectionViewModel = {
assigned: this.active,
relatedId: this.related,
inspectionPlanId: this.inspectionPlan,
nextInspection: formData.nextInspection.value,
context: formData.context.value,
};
this.status = "loading";
this.createInspection(createInspection)
.then((res) => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.$router.push({
name: "admin-unit-inspection-execute",
params: {
inspectionId: res.data,
},
});
}, 1500);
})
.catch((err) => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -83,20 +83,8 @@ import MainTemplate from "@/templates/Main.vue";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useInspectionPlanStore } from "@/stores/admin/unit/inspectionPlan/inspectionPlan";
import type { CreateInspectionPlanViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
import ScanInput from "@/components/ScanInput.vue";
import type { EquipmentTypeViewModel } from "@/viewmodels/admin/unit/equipment/equipmentType.models";
import { useEquipmentTypeStore } from "@/stores/admin/unit/equipmentType/equipmentType";
import EquipmentTypeSearchSelect from "@/components/search/EquipmentTypeSearchSelect.vue";
import VehicleTypeSearchSelect from "@/components/search/VehicleTypeSearchSelect.vue";

View file

@ -1,9 +1,13 @@
<template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<RouterLink to="./" class="text-primary">zurück zur Übersicht</RouterLink>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<Spinner v-if="loading == 'loading' && showLoading" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<div v-else-if="localPoints != null" class="flex flex-col grow gap-4 py-2 w-full max-w-xl mx-auto overflow-hidden">
<form
v-else-if="localPoints != null"
class="flex flex-col grow gap-4 py-2 w-full max-w-xl mx-auto overflow-hidden"
@submit.prevent="triggerUpdate"
>
<p class="mx-auto">Prüfplan-Punkte bearbeiten</p>
<div class="flex flex-row justify-center gap-4">
@ -38,6 +42,7 @@
</div>
<div class="grow flex flex-col gap-2 overflow-y-scroll">
<p v-if="sortedPoints.length == 0" class="m-auto">Wähle mindestens 1 Eingabefeld</p>
<InspectionPointListItem
v-for="(point, index) in sortedPoints"
:key="index"
@ -59,8 +64,7 @@
primary
type="submit"
class="w-fit!"
:disabled="status == 'loading' || canSaveOrReset"
@click="triggerUpdate"
:disabled="status == 'loading' || canSaveOrReset || sortedPoints.length == 0"
>
speichern
</button>
@ -68,7 +72,7 @@
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</div>
</form>
</div>
</template>
@ -95,11 +99,15 @@ export default defineComponent({
},
watch: {
loading() {
if (this.loading == "fetched") this.localPoints = cloneDeep(this.inspectionPoints);
if (this.loading == "fetched") {
this.localPoints = cloneDeep(this.inspectionPoints);
this.showLoading = false;
}
},
},
data() {
return {
showLoading: true as boolean,
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: null as any,
localPoints: [] as Array<InspectionPointViewModel>,