unit/#103-base-management #110

Merged
jkeffects merged 17 commits from unit/#103-base-management into milestone/ff-admin-unit 2025-07-14 13:36:48 +00:00
12 changed files with 374 additions and 77 deletions
Showing only changes of commit 1409cf8045 - Show all commits

View file

@ -0,0 +1,76 @@
<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">{{ 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()"
>
<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 { defineComponent, type PropType } from "vue";
import type { InspectionPointViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
import { CheckIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
props: {
inspectionPoint: {
type: Object as PropType<InspectionPointViewModel>,
required: true,
},
modelValue: {
type: String,
default: "",
},
},
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: {
selectFile(e: Event) {
this.$emit("update:upload", (e.target as HTMLInputElement).files?.[0] ?? null);
this.value = "set";
},
},
});
</script>

View file

@ -0,0 +1,39 @@
<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 />
<button primary>Prüfung abschließen</button>
</div>
<br />
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal">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";
</script>
<script lang="ts">
export default defineComponent({
methods: {
...mapActions(useModalStore, ["closeModal"]),
},
});
</script>

View file

@ -0,0 +1,75 @@
<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 }"
/>
</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: "",
},
},
emits: ["update:model-value"],
computed: {
value: {
get() {
return this.modelValue;
},
set(val: string | number) {
this.$emit("update:model-value", val.toString());
},
},
restrictedRange() {
let range = "";
if (this.inspectionPoint.min != null) range += `min. ${this.inspectionPoint.min}`;
if (this.inspectionPoint.max != null) range += ` bis max. ${this.inspectionPoint.max}`;
return range;
},
isInRange() {
if (this.inspectionPoint.min != null && this.inspectionPoint.max != null)
return Number(this.value) < this.inspectionPoint.min || this.inspectionPoint.max < Number(this.value);
if (this.inspectionPoint.min != null) return Number(this.value) < this.inspectionPoint.min;
if (this.inspectionPoint.max != null) return this.inspectionPoint.max < Number(this.value);
return false;
},
},
mounted() {
if (!this.value) {
this.value = String(this.inspectionPoint.min ?? 0);
}
},
});
</script>

View file

@ -36,8 +36,8 @@ export default defineComponent({
required: true,
},
modelValue: {
type: String as PropType<"true" | "false">,
default: "false",
type: String as PropType<"true" | "false" | "">,
default: "",
},
},
emits: ["update:model-value"],
@ -59,5 +59,8 @@ export default defineComponent({
},
},
},
mounted() {
if (this.value == "") this.value = "false";
},
});
</script>

View file

@ -6,18 +6,14 @@
<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.type == "number" ? "Zahl" : "Freitext" }}</label>
<input
v-if="inspectionPoint.type == 'number'"
<label :for="inspectionPoint.id">Freitext</label>
<textarea
:id="inspectionPoint.id"
:name="inspectionPoint.id"
type="number"
required
class="h-18"
:class="{ 'ring-red-500! ring-1!': value == '' }"
v-model="value"
:min="inspectionPoint.min"
:max="inspectionPoint.max"
/>
<textarea v-else :id="inspectionPoint.id" :name="inspectionPoint.id" required class="h-18" v-model="value" />
></textarea>
</div>
</div>
</template>

View file

@ -6,7 +6,7 @@
<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" />
<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')" />
@ -26,13 +26,17 @@
</div>
<div v-if="modelValue.type == InspectionPointEnum.number">
<label for="max">Maximal</label>
<input type="number" v-model="max" min="0" />
<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'" @click="others = 'img'">Bild</button>
<button :primary="others == 'pdf'" :primary-outline="others != 'pdf'" @click="others = 'pdf'">PDF</button>
<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>
@ -87,7 +91,7 @@ export default defineComponent({
return this.modelValue.min;
},
set(val: string) {
this.$emit("update:model-value", { ...this.modelValue, min: val == "" ? null : val });
this.$emit("update:model-value", { ...this.modelValue, min: String(val) == "" ? null : String(val) });
},
},
max: {
@ -95,7 +99,7 @@ export default defineComponent({
return this.modelValue.max;
},
set(val: string) {
this.$emit("update:model-value", { ...this.modelValue, max: val == "" ? null : val });
this.$emit("update:model-value", { ...this.modelValue, max: String(val) == "" ? null : String(val) });
},
},
others: {
@ -107,5 +111,10 @@ export default defineComponent({
},
},
},
mounted() {
if (this.modelValue.type == InspectionPointEnum.file && !this.others) {
this.others = "img";
}
},
});
</script>

View file

@ -1,7 +1,11 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
import type { InspectionViewModel } from "@/viewmodels/admin/unit/inspection/inspection.models";
import type {
CreateInspectionViewModel,
InspectionViewModel,
UpdateInspectionViewModel,
} from "@/viewmodels/admin/unit/inspection/inspection.models";
export const useInspectionStore = defineStore("inspection", {
state: () => {
@ -27,19 +31,20 @@ export const useInspectionStore = defineStore("inspection", {
fetchInspectionById(id: string) {
return http.get(`/admin/inspection/${id}`);
},
async createInspection(inspection: any): Promise<AxiosResponse<any, any>> {
async createInspection(inspection: CreateInspectionViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/inspection`, {
context: "",
inspectionPlanId: "",
relatedId: "",
assigned: "equipment|vehicle",
assigned: inspection.assigned,
relatedId: inspection.relatedId,
inspectionPlanId: inspection.inspectionPlanId,
nextInspection: inspection.nextInspection,
context: inspection.context,
});
this.fetchInspectionByActiveId();
return result;
},
async updateActiveInspection(inspection: any): Promise<AxiosResponse<any, any>> {
async updateActiveInspection(inspection: UpdateInspectionViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/inspection/${inspection.id}`, {
context: "",
nextInspection: inspection.nextInspection,
context: inspection.context,
});
this.fetchInspectionByActiveId();
return result;

View file

@ -37,8 +37,21 @@ export type InspectionViewModel = {
export interface InspectionPointResultViewModel {
inspectionId: string;
inspectionVersionedPlanId: string;
inspectionPointId: string;
inspectionPoint?: InspectionPointViewModel;
value: string;
}
export type CreateInspectionViewModel = {
assigned: "equipment" | "vehicle" | "wearable";
relatedId: string;
inspectionPlanId: string;
nextInspection?: Date;
context?: string;
};
export type UpdateInspectionViewModel = {
id: string;
nextInspection?: Date;
context?: string;
};

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">
<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)"
/>
<ResultInput
v-else
<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 class="flex flex-row justify-end gap-2">
</div>
<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>,