maintainance view and wearable inspection integration

This commit is contained in:
Julian Krauser 2025-06-13 12:45:43 +02:00
parent 50fa0128ea
commit 6575948841
26 changed files with 877 additions and 66 deletions

View file

@ -74,9 +74,7 @@ import { useNewsletterRecipientsStore } from "@/stores/admin/club/newsletter/new
import { useAbilityStore } from "@/stores/ability";
import { useQueryStoreStore } from "@/stores/admin/configuration/queryStore";
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
import cloneDeep from "lodash.clonedeep";
import MemberSearchSelectMultiple from "@/components/search/MemberSearchSelectMultiple.vue";
import MemberSearchSelect from "@/components/search/MemberSearchSelect.vue";
import type { FieldType } from "@/types/dynamicQueries";
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
</script>

View file

@ -39,9 +39,6 @@ import { useEquipmentStore } from "@/stores/admin/unit/equipment/equipment";
<script lang="ts">
export default defineComponent({
props: {
equipmentId: String,
},
data() {
return {
tabs: [

View file

@ -80,7 +80,7 @@ export default defineComponent({
openDeleteModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/unit/equipmentType/CreateEquipmentTypeModal.vue"))
defineAsyncComponent(() => import("@/components/admin/unit/equipmentType/DeleteEquipmentTypeModal.vue"))
),
this.equipmentTypeId ?? ""
);

View file

@ -27,7 +27,8 @@
</div>
<EquipmentSearchSelect v-if="active == 'equipment'" title="Gerät" useScanner v-model="related" />
<VehicleSearchSelect v-else title="Fahrzeug" useScanner v-model="related" />
<VehicleSearchSelect v-else-if="active == 'vehicle'" title="Fahrzeug" useScanner v-model="related" />
<WearableSearchSelect v-else title="Kleidung" useScanner v-model="related" />
<InspectionPlanSearchSelect title="Prüfplan" :type="active" v-model="inspectionPlan" />
@ -67,13 +68,14 @@ import ScanInput from "@/components/ScanInput.vue";
import InspectionPlanSearchSelect from "@/components/search/InspectionPlanSearchSelect.vue";
import EquipmentSearchSelect from "@/components/search/EquipmentSearchSelect.vue";
import VehicleSearchSelect from "@/components/search/VehicleSearchSelect.vue";
import WearableSearchSelect from "../../../../components/search/wearableSearchSelect.vue";
</script>
<script lang="ts">
export default defineComponent({
props: {
type: {
type: String as PropType<"vehicle" | "equipment">,
type: String as PropType<"vehicle" | "equipment" | "wearable">,
default: "equipment",
},
relatedId: String,
@ -85,7 +87,7 @@ export default defineComponent({
timeout: null as any,
related: "",
inspectionPlan: "",
active: "equipment" as "equipment" | "vehicle",
active: "equipment" as "equipment" | "vehicle" | "wearable",
tabs: [
{
key: "equipment",
@ -95,11 +97,15 @@ export default defineComponent({
key: "vehicle",
title: "Fahrzeug",
},
] as Array<{ key: "equipment" | "vehicle"; title: string }>,
{
key: "wearable",
title: "Kleidung",
},
] as Array<{ key: "equipment" | "vehicle" | "wearable"; title: string }>,
};
},
mounted() {
if (["vehicle", "equipment"].includes(this.type)) {
if (["vehicle", "equipment", "wearable"].includes(this.type)) {
this.active = this.type;
this.inspectionPlan = this.inspectionPlanId ?? "";
this.related = this.relatedId ?? "";

View file

@ -0,0 +1,46 @@
<template>
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
<Pagination
:items="maintenances"
:totalCount="totalCount"
:indicateLoading="loading == 'loading'"
@load-data="(offset, count, search) => fetchMaintenances(offset, count, search)"
@search="(search) => fetchMaintenances(0, maxEntriesPerPage, search, true)"
>
<template #pageRow="{ row }: { row: MaintenanceViewModel }">
<MaintenanceListItem :maintenance="row" />
</template>
</Pagination>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useAbilityStore } from "@/stores/ability";
import { useMaintenanceStore } from "@/stores/admin/unit/maintenance/maintenance";
import type { MaintenanceViewModel } from "@/viewmodels/admin/unit/maintenance.models";
import Pagination from "@/components/Pagination.vue";
import MaintenanceListItem from "@/components/admin/unit/maintenance/MaintenanceListItem.vue";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
maxEntriesPerPage: 25,
};
},
computed: {
...mapState(useMaintenanceStore, ["maintenances", "totalCount", "loading"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchMaintenances(0, this.maxEntriesPerPage, "", true);
},
methods: {
...mapActions(useMaintenanceStore, ["fetchMaintenances"]),
},
});
</script>

View file

@ -0,0 +1,54 @@
<template>
<MainTemplate title="Wartungen / Reparaturen">
<template #diffMain>
<div class="flex flex-col gap-2 grow px-7 overflow-hidden">
<div class="flex flex-col grow gap-2 overflow-hidden">
<div class="w-full flex flex-row max-lg:flex-wrap justify-center">
<RouterLink
v-for="tab in tabs"
:key="tab.route"
v-slot="{ isExactActive }"
:to="{ name: tab.route }"
class="w-1/2 md:w-1/3 lg:w-full p-0.5 first:pl-0 last:pr-0"
>
<p
:class="[
'w-full rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-hidden',
isExactActive ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200',
]"
>
{{ tab.title }}
</p>
</RouterLink>
</div>
<RouterView />
</div>
</div>
</template>
</MainTemplate>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { RouterLink, RouterView } from "vue-router";
import { useAbilityStore } from "@/stores/ability";
import { useEquipmentStore } from "@/stores/admin/unit/equipment/equipment";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
tabs: [
{ route: "admin-unit-maintenance", title: "offen" },
{ route: "admin-unit-maintenance-done", title: "bearbeitet" },
],
};
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -79,7 +79,7 @@ export default defineComponent({
...mapActions(useModalStore, ["openModal"]),
openDeleteModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/vehicleType/CreateVehicleTypeModal.vue"))),
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/vehicleType/DeleteVehicleTypeModal.vue"))),
this.vehicleTypeId ?? ""
);
},

View file

@ -0,0 +1,69 @@
<template>
<div class="flex flex-col gap-2 h-full w-full">
<Pagination
:items="inspections"
:totalCount="totalCount"
:indicateLoading="false"
@load-data="(offset, count, search) => {}"
@search="(search) => {}"
>
<template #pageRow="{ row }: { row: InspectionViewModel }">
<RouterLink
:to="{ name: 'admin-unit-inspection-execute', params: { inspectionId: row.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 gap-2 items-center">
<PencilSquareIcon v-if="row.isOpen" class="w-5 h-5" />
<p>{{ row.inspectionPlan.title }} - {{ row.finished }}</p>
</div>
<div class="p-2">
<p v-if="row.context">Kontext: {{ row.context }}</p>
<p v-if="row.nextInspection">nächste Inspektion: {{ row.nextInspection }}</p>
</div>
</RouterLink>
</template>
</Pagination>
<div class="flex flex-row gap-4">
<RouterLink
v-if="can('create', 'unit', 'wearable')"
:to="{ name: 'admin-unit-inspection-plan', params: { type: 'wearable', relatedId: wearableId } }"
button
primary
class="w-fit!"
@click=""
>Prüfung durchführen</RouterLink
>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import { useWearableInspectionStore } from "@/stores/admin/unit/wearable/inspection";
import { PencilSquareIcon } from "@heroicons/vue/24/outline";
import Pagination from "@/components/Pagination.vue";
import type { InspectionViewModel } from "@/viewmodels/admin/unit/inspection/inspection.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
wearableId: String,
},
computed: {
...mapState(useAbilityStore, ["can"]),
...mapState(useWearableInspectionStore, ["inspections", "loading", "totalCount"]),
},
mounted() {
this.fetchItem();
},
methods: {
...mapActions(useWearableInspectionStore, ["fetchInspectionForWearable"]),
fetchItem() {
this.fetchInspectionForWearable();
},
},
});
</script>

View file

@ -55,7 +55,8 @@ export default defineComponent({
return {
tabs: [
{ route: "admin-unit-wearable-overview", title: "Übersicht" },
{ route: "admin-unit-wearable-maintenance", title: "Reparaturen" },
{ route: "admin-unit-wearable-maintenance", title: "Wartungen/Reparaturen" },
{ route: "admin-unit-wearable-inspection", title: "Prüfungen" },
{ route: "admin-unit-wearable-damage_report", title: "Schadensmeldungen" },
],
};

View file

@ -0,0 +1,51 @@
<template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<div v-if="inspectionPlans != null" class="flex flex-col gap-2 w-full">
<TypeInspectionPlanListItem v-for="plan in inspectionPlans" :inspection-plan="plan" />
</div>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
</div>
<div class="flex flex-row gap-4">
<RouterLink
v-if="can('create', 'unit', 'vehicle_type')"
:to="{ name: 'admin-unit-inspection_plan-create', query: { type: 'vehicle', id: vehicleTypeId } }"
button
primary
class="w-fit!"
>
Prüfplan erstellen
</RouterLink>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import { useVehicleTypeInspectionPlanStore } from "@/stores/admin/unit/vehicleType/inspectionPlan";
import TypeInspectionPlanListItem from "@/components/admin/unit/inspectionPlan/TypeInspectionPlanListItem.vue";
import Spinner from "@/components/Spinner.vue";
</script>
<script lang="ts">
export default defineComponent({
props: {
vehicleTypeId: String,
},
computed: {
...mapState(useAbilityStore, ["can"]),
...mapState(useVehicleTypeInspectionPlanStore, ["inspectionPlans", "loading"]),
},
mounted() {
this.fetchItem();
},
methods: {
...mapActions(useVehicleTypeInspectionPlanStore, ["fetchInspectionPlanForVehicleType"]),
fetchItem() {
this.fetchInspectionPlanForVehicleType();
},
},
});
</script>

View file

@ -0,0 +1,43 @@
<template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<div v-if="activeWearableTypeObj != null" class="flex flex-col gap-2 w-full">
<div>
<label for="type">Typ</label>
<input type="text" id="type" :value="activeWearableTypeObj.type" readonly />
</div>
<div>
<label for="description">Beschreibung</label>
<textarea id="description" :value="activeWearableTypeObj.description" class="h-18" readonly></textarea>
</div>
</div>
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
<p v-else-if="loadingActive == 'failed'" @click="fetchWearableTypeByActiveId" class="cursor-pointer">
&#8634; laden fehlgeschlagen
</p>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import Spinner from "@/components/Spinner.vue";
import { useWearableTypeStore } from "@/stores/admin/unit/wearableType/wearableType";
</script>
<script lang="ts">
export default defineComponent({
props: {
wearableTypeId: String,
},
computed: {
...mapState(useWearableTypeStore, ["activeWearableTypeObj", "loadingActive"]),
},
mounted() {
this.fetchWearableTypeByActiveId();
},
methods: {
...mapActions(useWearableTypeStore, ["fetchWearableTypeByActiveId"]),
},
});
</script>

View file

@ -1,43 +1,37 @@
<template>
<MainTemplate :title="`Kleidungs-Typ ${origin?.type} - Daten bearbeiten`">
<template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template>
<template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<form
v-else-if="wearableType != null"
class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto"
@submit.prevent="triggerUpdate"
>
<p class="mx-auto">Kleidungstyp bearbeiten</p>
<div>
<label for="type">Bezeichnung</label>
<input type="text" id="type" required v-model="wearableType.type" />
</div>
<div>
<label for="description">Beschreibung (optional)</label>
<input type="text" id="description" v-model="wearableType.description" />
</div>
<div class="flex flex-row justify-end gap-2">
<button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm">
abbrechen
</button>
<button primary type="submit" class="w-fit!" :disabled="status == 'loading'">speichern</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>
</template>
</MainTemplate>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<form
v-else-if="wearableType != null"
class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto"
@submit.prevent="triggerUpdate"
>
<p class="mx-auto">Kleidungstyp bearbeiten</p>
<div>
<label for="type">Bezeichnung</label>
<input type="text" id="type" required v-model="wearableType.type" />
</div>
<div>
<label for="description">Beschreibung (optional)</label>
<input type="text" id="description" v-model="wearableType.description" />
</div>
<div class="flex flex-row justify-end gap-2">
<button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm">
abbrechen
</button>
<button primary type="submit" class="w-fit!" :disabled="status == 'loading'">speichern</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>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useWearableTypeStore } from "@/stores/admin/unit/wearableType/wearableType";
import type {
CreateWearableTypeViewModel,

View file

@ -0,0 +1,88 @@
<template>
<MainTemplate :title="activeWearableTypeObj?.type">
<template #headerInsert>
<RouterLink :to="{ name: 'admin-unit-wearable_type' }" class="text-primary">zurück zur Liste</RouterLink>
</template>
<template #topBar>
<div class="flex flex-row gap-2">
<RouterLink v-if="can('update', 'unit', 'wearable_type')" :to="{ name: 'admin-unit-wearable_type-edit' }">
<PencilIcon class="w-5 h-5" />
</RouterLink>
<TrashIcon
v-if="can('delete', 'unit', 'wearable_type')"
class="w-5 h-5 cursor-pointer"
@click="openDeleteModal"
/>
</div>
</template>
<template #diffMain>
<div class="flex flex-col gap-2 grow px-7 overflow-hidden">
<div class="flex flex-col grow gap-2 overflow-hidden">
<div class="w-full flex flex-row max-lg:flex-wrap justify-center">
<RouterLink
v-for="tab in tabs"
:key="tab.route"
v-slot="{ isActive }"
:to="{ name: tab.route }"
class="w-1/2 md:w-1/3 lg:w-full p-0.5 first:pl-0 last:pr-0"
>
<p
:class="[
'w-full rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-hidden',
isActive ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200',
]"
>
{{ tab.title }}
</p>
</RouterLink>
</div>
<RouterView />
</div>
</div>
</template>
</MainTemplate>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { RouterLink, RouterView } from "vue-router";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
import { useAbilityStore } from "@/stores/ability";
import { useWearableTypeStore } from "@/stores/admin/unit/wearableType/wearableType";
</script>
<script lang="ts">
export default defineComponent({
props: {
wearableTypeId: String,
},
data() {
return {
tabs: [
{ route: "admin-unit-wearable_type-overview", title: "Übersicht" },
{ route: "admin-unit-wearable_type-inspection_plan", title: "Prüfpläne" },
],
};
},
computed: {
...mapState(useWearableTypeStore, ["activeWearableTypeObj"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchWearableTypeByActiveId();
},
methods: {
...mapActions(useWearableTypeStore, ["fetchWearableTypeByActiveId"]),
...mapActions(useModalStore, ["openModal"]),
openDeleteModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/wearableType/DeleteWearableTypeModal.vue"))),
this.wearableTypeId ?? ""
);
},
},
});
</script>