inspection finish and print

This commit is contained in:
Julian Krauser 2025-07-11 14:02:39 +02:00
parent d96c73d5b1
commit 5d26885da3
14 changed files with 367 additions and 29 deletions

View file

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

View file

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

View file

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

View file

@ -11,13 +11,22 @@
Es wird ein PDF ausgedruckt und ist dann zu dieser Prüfung verfügbar. Es wird ein PDF ausgedruckt und ist dann zu dieser Prüfung verfügbar.
</p> </p>
<br /> <br />
<button primary>Prüfung abschließen</button> <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> </div>
<br /> <br />
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal">abbrechen</button> <button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div> </div>
</div> </div>
</div> </div>
@ -28,12 +37,47 @@ import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia"; import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal"; import { useModalStore } from "@/stores/modal";
import { InformationCircleIcon } from "@heroicons/vue/24/outline"; 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>
<script lang="ts"> <script lang="ts">
export default defineComponent({ 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: { methods: {
...mapActions(useModalStore, ["closeModal"]), ...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> </script>

View file

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

View file

@ -1,12 +1,15 @@
<template> <template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md"> <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"> <div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ inspectionPlan.title }}</p> <p>{{ inspectionPlan.title }}</p>
</div> </div>
<div class="p-2"> <div class="p-2">
<p>Interval: {{ inspectionPlan.inspectionInterval }}</p> <p>Interval: {{ inspectionPlan.inspectionInterval }}</p>
</div> </div>
</div> </RouterLink>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View file

@ -102,8 +102,8 @@ export default defineComponent({
default: false, default: false,
}, },
relatedType: { relatedType: {
type: String as PropType<"vehicle" | "equipment" | "wearable">, type: String as PropType<"vehicleType" | "equipmentType" | "wearableType">,
default: "equipment", default: "equipmentType",
}, },
relatedTypeId: { relatedTypeId: {
type: String, type: String,

View file

@ -3,6 +3,7 @@ import { http } from "@/serverCom";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import type { import type {
CreateInspectionViewModel, CreateInspectionViewModel,
CreateOrUpdateInspectionPointResultCommand,
InspectionNextViewModel, InspectionNextViewModel,
InspectionViewModel, InspectionViewModel,
MinifiedInspectionViewModel, MinifiedInspectionViewModel,
@ -89,6 +90,11 @@ export const useInspectionStore = defineStore("inspection", {
fetchInspectionById(id: string) { fetchInspectionById(id: string) {
return http.get(`/admin/inspection/${id}`); return http.get(`/admin/inspection/${id}`);
}, },
fetchInspectionPrintoutById() {
return http.get(`/admin/inspection/${this.activeInspectionObj?.id}/printout`, {
responseType: "blob",
});
},
async createInspection(inspection: CreateInspectionViewModel): Promise<AxiosResponse<any, any>> { async createInspection(inspection: CreateInspectionViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/inspection`, { const result = await http.post(`/admin/inspection`, {
assigned: inspection.assigned, assigned: inspection.assigned,
@ -100,17 +106,41 @@ export const useInspectionStore = defineStore("inspection", {
return result; return result;
}, },
async updateActiveInspection(inspection: UpdateInspectionViewModel): Promise<AxiosResponse<any, any>> { async updateActiveInspection(inspection: UpdateInspectionViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/inspection/${inspection.id}`, { const result = await http.patch(`/admin/inspection/${this.activeInspection}`, {
nextInspection: inspection.nextInspection, nextInspection: inspection.nextInspection,
context: inspection.context, context: inspection.context,
}); });
this.fetchInspectionByActiveId(); this.fetchInspectionByActiveId();
return result; return result;
}, },
async deleteInspection(inspection: number): Promise<AxiosResponse<any, any>> { async updateActiveInspectionResults(
const result = await http.delete(`/admin/inspection/${inspection}`); results: Array<CreateOrUpdateInspectionPointResultCommand>,
files: { [key: string]: File | null }
): Promise<AxiosResponse<any, any>> {
const formData = new FormData();
formData.append("results", JSON.stringify(results));
Object.entries(files).forEach(([key, file]) => {
if (file) {
const extension = file.name.split(".").pop() || "";
formData.append(`files`, new File([file], `${key}.${extension}`, { type: file.type }));
}
});
const result = await http.patch(`/admin/inspection/${this.activeInspection}/results`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
this.fetchInspectionByActiveId(); this.fetchInspectionByActiveId();
return result; return result;
}, },
async finishActiveInspection(): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/inspection/${this.activeInspection}/finish`);
this.fetchInspectionByActiveId();
return result;
},
async deleteInspection(inspection: string): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/inspection/${inspection}`);
return result;
},
}, },
}); });

View file

@ -54,7 +54,7 @@ export const useInspectionPlanStore = defineStore("inspectionPlan", {
}); });
}, },
async getAllInspectionPlansWithRelated( async getAllInspectionPlansWithRelated(
related: "vehicle" | "equipment" | "wearable", related: "vehicleType" | "equipmentType" | "wearableType",
relatedId: string relatedId: string
): Promise<AxiosResponse<any, any>> { ): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/inspectionPlan/${related}/${relatedId}?noLimit=true`).then((res) => { return await http.get(`/admin/inspectionPlan/${related}/${relatedId}?noLimit=true`).then((res) => {
@ -62,7 +62,7 @@ export const useInspectionPlanStore = defineStore("inspectionPlan", {
}); });
}, },
async searchInspectionPlansWithRelated( async searchInspectionPlansWithRelated(
related: "vehicle" | "equipment" | "wearable", related: "vehicleType" | "equipmentType" | "wearableType",
relatedId: string, relatedId: string,
search: string search: string
): Promise<AxiosResponse<any, any>> { ): Promise<AxiosResponse<any, any>> {

View file

@ -79,3 +79,8 @@ export type UpdateInspectionViewModel = {
nextInspection?: Date; nextInspection?: Date;
context?: string; context?: string;
}; };
export interface CreateOrUpdateInspectionPointResultCommand {
inspectionPointId: string;
value: string;
}

View file

@ -4,8 +4,7 @@
:items="inspections" :items="inspections"
:totalCount="totalCount" :totalCount="totalCount"
:indicateLoading="false" :indicateLoading="false"
@load-data="(offset, count, search) => {}" @load-data="(offset, count, search) => fetchInspectionForEquipment(offset, count, false)"
@search="(search) => {}"
> >
<template #pageRow="{ row }: { row: InspectionViewModel }"> <template #pageRow="{ row }: { row: InspectionViewModel }">
<RouterLink <RouterLink

View file

@ -6,6 +6,13 @@
<small v-if="activeInspectionObj?.related.code">({{ activeInspectionObj?.related.code }})</small> - <small v-if="activeInspectionObj?.related.code">({{ activeInspectionObj?.related.code }})</small> -
{{ activeInspectionObj?.inspectionPlan.title }} {{ activeInspectionObj?.inspectionPlan.title }}
</h1> </h1>
<div class="flex flex-row gap-2">
<TrashIcon
v-if="activeInspectionObj?.isOpen && can('delete', 'unit', 'inspection')"
class="w-5 h-5 cursor-pointer"
@click="openDeleteModal"
/>
</div>
</template> </template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
@ -79,7 +86,7 @@
<FailureXMark v-else-if="status?.status == 'failed'" /> <FailureXMark v-else-if="status?.status == 'failed'" />
</div> </div>
<div v-else> <div v-else>
<button primary type="button" @click="">Bericht anzeigen</button> <button primary type="button" @click="openPrintModal">Bericht anzeigen</button>
</div> </div>
</div> </div>
</template> </template>
@ -103,6 +110,8 @@ import { useModalStore } from "@/stores/modal";
import NumberInput from "@/components/admin/unit/inspection/NumberInput.vue"; import NumberInput from "@/components/admin/unit/inspection/NumberInput.vue";
import TextInput from "@/components/admin/unit/inspection/TextInput.vue"; import TextInput from "@/components/admin/unit/inspection/TextInput.vue";
import FileInput from "@/components/admin/unit/inspection/FileInput.vue"; import FileInput from "@/components/admin/unit/inspection/FileInput.vue";
import { useAbilityStore } from "@/stores/ability";
import { DocumentTextIcon, TrashIcon } from "@heroicons/vue/24/outline";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -137,8 +146,11 @@ export default defineComponent({
); );
}, },
...mapState(useInspectionStore, ["activeInspectionObj", "loadingActive"]), ...mapState(useInspectionStore, ["activeInspectionObj", "loadingActive"]),
...mapState(useAbilityStore, ["can"]),
points() { points() {
return this.activeInspectionObj?.inspectionVersionedPlan.inspectionPoints ?? []; return (this.activeInspectionObj?.inspectionVersionedPlan.inspectionPoints ?? [])
.slice()
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
}, },
pointResult() { pointResult() {
return (pointId: string) => { return (pointId: string) => {
@ -172,8 +184,24 @@ export default defineComponent({
} catch (error) {} } catch (error) {}
}, },
methods: { methods: {
...mapActions(useInspectionStore, ["fetchInspectionByActiveId"]), ...mapActions(useInspectionStore, ["fetchInspectionByActiveId", "updateActiveInspectionResults"]),
...mapActions(useModalStore, ["openModal"]), ...mapActions(useModalStore, ["openModal"]),
openDeleteModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/inspection/DeleteInspectionModal.vue"))),
parseInt(this.inspectionId ?? "")
);
},
openPrintModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/inspection/InspectionPrintModal.vue")))
);
},
finishInspection() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/inspection/InspectionFinishModal.vue")))
);
},
resetForm() { resetForm() {
this.checks = cloneDeep(this.activeInspectionObj?.checks ?? []); this.checks = cloneDeep(this.activeInspectionObj?.checks ?? []);
}, },
@ -194,9 +222,7 @@ export default defineComponent({
triggerUpdate(e: any) { triggerUpdate(e: any) {
if (this.activeInspectionObj == null) return; if (this.activeInspectionObj == null) return;
this.status = "loading"; this.status = "loading";
new Promise<void>((resolve, reject) => { this.updateActiveInspectionResults(this.checks, this.fileStore)
resolve();
})
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };
}) })
@ -209,11 +235,6 @@ export default defineComponent({
}, 2000); }, 2000);
}); });
}, },
finishInspection() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/inspection/InspectionFinishModal.vue")))
);
},
}, },
}); });
</script> </script>

View file

@ -32,14 +32,15 @@
<InspectionPlanSearchSelectWithRelated <InspectionPlanSearchSelectWithRelated
title="Prüfplan" title="Prüfplan"
:relatedType="active" :relatedType="`${active}Type`"
:relatedTypeId="relatedType" :relatedTypeId="relatedType"
v-model="inspectionPlan" v-model="inspectionPlan"
/> />
<div> <div>
<label for="nextInspection"> <label for="nextInspection" class="flex flex-row justify-between">
Nächste Prüfung (optional) - Intervall: {{ selectedInspectionPlan?.inspectionInterval }} Nächste Prüfung (optional) - Intervall: {{ selectedInspectionPlan?.inspectionInterval ?? "xx" }}
<InspectionTimeFormatExplainIcon />
</label> </label>
<input id="nextInspection" type="date" /> <input id="nextInspection" type="date" />
</div> </div>
@ -87,6 +88,7 @@ import { useVehicleStore } from "@/stores/admin/unit/vehicle/vehicle";
import { useWearableStore } from "@/stores/admin/unit/wearable/wearable"; import { useWearableStore } from "@/stores/admin/unit/wearable/wearable";
import type { CreateInspectionViewModel } from "@/viewmodels/admin/unit/inspection/inspection.models"; import type { CreateInspectionViewModel } from "@/viewmodels/admin/unit/inspection/inspection.models";
import { useInspectionStore } from "@/stores/admin/unit/inspection/inspection"; import { useInspectionStore } from "@/stores/admin/unit/inspection/inspection";
import InspectionTimeFormatExplainIcon from "@/components/admin/unit/InspectionTimeFormatExplainIcon.vue";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -34,7 +34,10 @@
<input type="text" id="name" required /> <input type="text" id="name" required />
</div> </div>
<div> <div>
<label for="interval">Intervall</label> <label for="interval" class="flex flex-row justify-between">
Intervall
<InspectionTimeFormatExplainIcon />
</label>
<input <input
type="text" type="text"
id="interval" id="interval"
@ -45,7 +48,10 @@
/> />
</div> </div>
<div> <div>
<label for="remind">Erinnerung vor Fälligkeit</label> <label for="remind" class="flex flex-row justify-between">
Erinnerung vor Fälligkeit
<InspectionTimeFormatExplainIcon />
</label>
<input <input
type="text" type="text"
id="remind" id="remind"
@ -88,6 +94,7 @@ import type { CreateInspectionPlanViewModel } from "@/viewmodels/admin/unit/insp
import { useEquipmentTypeStore } from "@/stores/admin/unit/equipmentType/equipmentType"; import { useEquipmentTypeStore } from "@/stores/admin/unit/equipmentType/equipmentType";
import EquipmentTypeSearchSelect from "@/components/search/EquipmentTypeSearchSelect.vue"; import EquipmentTypeSearchSelect from "@/components/search/EquipmentTypeSearchSelect.vue";
import VehicleTypeSearchSelect from "@/components/search/VehicleTypeSearchSelect.vue"; import VehicleTypeSearchSelect from "@/components/search/VehicleTypeSearchSelect.vue";
import InspectionTimeFormatExplainIcon from "@/components/admin/unit/InspectionTimeFormatExplainIcon.vue";
</script> </script>
<script lang="ts"> <script lang="ts">