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

@ -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>

View file

@ -1,20 +1,12 @@
<template> <template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md"> <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"> <div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p> <p>
{{ wearableType.type }} {{ wearableType.type }}
</p> </p>
<div class="flex flex-row">
<RouterLink
v-if="can('update', 'unit', 'wearable_type')"
:to="{ name: 'admin-unit-wearable_type-edit', params: { wearableTypeId: wearableType.id } }"
>
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</RouterLink>
<div v-if="can('delete', 'unit', 'wearable_type')" @click="openDeleteModal">
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
</div>
</div> </div>
<div class="flex flex-col p-2"> <div class="flex flex-col p-2">
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
@ -22,7 +14,7 @@
<p class="grow overflow-hidden">{{ wearableType.description }}</p> <p class="grow overflow-hidden">{{ wearableType.description }}</p>
</div> </div>
</div> </div>
</div> </RouterLink>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View file

@ -0,0 +1,196 @@
<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="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="wearable in filtered"
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,
filtered: [] as Array<WearableViewModel>,
chosen: undefined as undefined | WearableViewModel,
};
},
computed: {
selected: {
get() {
return this.modelValue;
},
set(val: string) {
this.chosen = this.getWearableFromSearch(val);
this.$emit("update:model-value", val);
},
},
},
mounted() {
this.loadWearableInitial();
},
methods: {
...mapActions(useWearableStore, ["searchWearables", "fetchWearableById"]),
...mapActions(useModalStore, ["openModal"]),
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.filtered.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/CodeDetector.vue"))),
"codeScanInput",
(result: string) => {
this.getWearableFromSearch(result);
}
);
},
},
});
</script>

View file

@ -19,6 +19,7 @@ import { resetWearableStores, setWearableId } from "./unit/wearable";
import { resetInspectionPlanStores, setInspectionPlanId } from "./unit/inspectionPlan"; import { resetInspectionPlanStores, setInspectionPlanId } from "./unit/inspectionPlan";
import { setVehicleTypeId } from "./unit/vehicleType"; import { setVehicleTypeId } from "./unit/vehicleType";
import { resetInspectionStores, setInspectionId } from "./unit/inspection"; import { resetInspectionStores, setInspectionId } from "./unit/inspection";
import { setWearableTypeId } from "./unit/wearableType";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -498,6 +499,12 @@ const router = createRouter({
component: () => import("@/views/admin/ViewSelect.vue"), component: () => import("@/views/admin/ViewSelect.vue"),
props: true, props: true,
}, },
{
path: "inspection",
name: "admin-unit-wearable-inspection",
component: () => import("@/views/admin/unit/wearable/Inspection.vue"),
props: true,
},
{ {
path: "report", path: "report",
name: "admin-unit-wearable-damage_report", name: "admin-unit-wearable-damage_report",
@ -706,6 +713,25 @@ const router = createRouter({
}, },
], ],
}, },
{
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", path: "equipment-type",
name: "admin-unit-equipment_type-route", name: "admin-unit-equipment_type-route",
@ -806,12 +832,33 @@ const router = createRouter({
component: () => import("@/views/admin/unit/wearableType/WearableType.vue"), component: () => import("@/views/admin/unit/wearableType/WearableType.vue"),
}, },
{ {
path: ":wearableTypeId/edit", path: ":wearableTypeId",
name: "admin-unit-wearable_type-edit", name: "admin-unit-wearable_type-routing",
component: () => import("@/views/admin/unit/wearableType/UpdateWearableType.vue"), component: () => import("@/views/admin/unit/wearableType/WearableTypeRouting.vue"),
meta: { type: "update", section: "unit", module: "wearable_type" }, beforeEnter: [setWearableTypeId],
beforeEnter: [abilityAndNavUpdate],
props: true, 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,
},
],
}, },
], ],
}, },

View 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();
}

View file

@ -123,6 +123,9 @@ export const useNavigationStore = defineStore("navigation", {
...(abilityStore.can("read", "unit", "damage_report") ...(abilityStore.can("read", "unit", "damage_report")
? [{ key: "damage_report", title: "Schadensmeldungen" }] ? [{ key: "damage_report", title: "Schadensmeldungen" }]
: []), : []),
...(abilityStore.can("read", "unit", "maintenance")
? [{ key: "maintenance", title: "Wartungen / Reparaturen" }]
: []),
{ key: "divider1", title: "Basisdaten" }, { key: "divider1", title: "Basisdaten" },
...(abilityStore.can("read", "unit", "equipment_type") ...(abilityStore.can("read", "unit", "equipment_type")
? [{ key: "equipment_type", title: "Geräte-Typen" }] ? [{ key: "equipment_type", title: "Geräte-Typen" }]

View file

@ -0,0 +1,74 @@
import { defineStore } from "pinia";
import type {
MaintenanceViewModel,
CreateMaintenanceViewModel,
UpdateMaintenanceViewModel,
} from "@/viewmodels/admin/unit/maintenance.models";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
export const useMaintenanceStore = defineStore("maintenance", {
state: () => {
return {
maintenances: [] as Array<MaintenanceViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchMaintenances(offset = 0, count = 25, search = "", clear = false) {
if (clear) this.maintenances = [];
this.loading = "loading";
//TODO enable fetch of done reports
http
.get(`/admin/maintenance?offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.then((result) => {
this.totalCount = result.data.total;
result.data.maintenances
.filter((elem: MaintenanceViewModel) => this.maintenances.findIndex((m) => m.id == elem.id) == -1)
.map((elem: MaintenanceViewModel, index: number): MaintenanceViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: MaintenanceViewModel & { tab_pos: number }) => {
this.maintenances.push(elem);
});
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
async getAllMaintenances(): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/maintenance?noLimit=true`).then((res) => {
return { ...res, data: res.data.maintenances };
});
},
async getMaintenancesByIds(ids: Array<string>): Promise<AxiosResponse<any, any>> {
return await http
.post(`/admin/maintenance/ids`, {
ids,
})
.then((res) => {
return { ...res, data: res.data.maintenances };
});
},
async searchMaintenances(search: string): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/maintenance?search=${search}&noLimit=true`).then((res) => {
return { ...res, data: res.data.maintenances };
});
},
fetchMaintenanceById(id: string) {
return http.get(`/admin/maintenance/${id}`);
},
async updateMaintenance(maintenance: UpdateMaintenanceViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/maintenance/${maintenance.id}`, {
// TODO: data
});
this.fetchMaintenances();
return result;
},
},
});

View file

@ -0,0 +1,43 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import type { InspectionViewModel } from "@/viewmodels/admin/unit/inspection/inspection.models";
import { useWearableStore } from "./wearable";
export const useWearableInspectionStore = defineStore("wearableInspection", {
state: () => {
return {
inspections: [] as Array<InspectionViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchInspectionForWearable(offset = 0, count = 25, search = "", clear = false) {
const wearableId = useWearableStore().activeWearable;
if (clear) this.inspections = [];
this.loading = "loading";
http
.get(
`/admin/inspection/wearable/${wearableId}?offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`
)
.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";
});
},
},
});

View file

@ -0,0 +1,29 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import type { InspectionPlanViewModel } from "@/viewmodels/admin/unit/inspection/inspectionPlan.models";
import { useWearableTypeStore } from "./wearableType";
export const useWearableTypeInspectionPlanStore = defineStore("wearableTypeInspectionPlan", {
state: () => {
return {
inspectionPlans: [] as Array<InspectionPlanViewModel>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchInspectionPlanForWearableType() {
const wearableTypeId = useWearableTypeStore().activeWearableType;
this.loading = "loading";
http
.get(`/admin/inspectionPlan/wearableType/${wearableTypeId}`)
.then((result) => {
this.inspectionPlans = result.data;
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
},
});

View file

@ -13,6 +13,9 @@ export const useWearableTypeStore = defineStore("wearableType", {
wearableTypes: [] as Array<WearableTypeViewModel & { tab_pos: number }>, wearableTypes: [] as Array<WearableTypeViewModel & { tab_pos: number }>,
totalCount: 0 as number, totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed", loading: "loading" as "loading" | "fetched" | "failed",
activeWearableType: null as string | null,
activeWearableTypeObj: null as WearableTypeViewModel | null,
loadingActive: "loading" as "loading" | "fetched" | "failed",
}; };
}, },
actions: { actions: {
@ -50,6 +53,18 @@ export const useWearableTypeStore = defineStore("wearableType", {
return { ...res, data: res.data.wearableTypes }; return { ...res, data: res.data.wearableTypes };
}); });
}, },
fetchWearableTypeByActiveId() {
this.loadingActive = "loading";
http
.get(`/admin/wearableType/${this.activeWearableType}`)
.then((res) => {
this.activeWearableTypeObj = res.data;
this.loadingActive = "fetched";
})
.catch((err) => {
this.loadingActive = "failed";
});
},
fetchWearableTypeById(id: string) { fetchWearableTypeById(id: string) {
return http.get(`/admin/wearableType/${id}`); return http.get(`/admin/wearableType/${id}`);
}, },

View file

@ -21,6 +21,7 @@ export type PermissionModule =
| "respiratory_wearer" | "respiratory_wearer"
| "respiratory_mission" | "respiratory_mission"
| "damage_report" | "damage_report"
| "maintenance"
// configuration // configuration
| "qualification" | "qualification"
| "award" | "award"
@ -95,6 +96,7 @@ export const permissionModules: Array<PermissionModule> = [
"respiratory_wearer", "respiratory_wearer",
"respiratory_mission", "respiratory_mission",
"damage_report", "damage_report",
"maintenance",
// configuration // configuration
"qualification", "qualification",
"award", "award",
@ -131,6 +133,7 @@ export const sectionsAndModules: SectionsAndModulesObject = {
"respiratory_wearer", "respiratory_wearer",
"respiratory_mission", "respiratory_mission",
"damage_report", "damage_report",
"maintenance",
], ],
configuration: [ configuration: [
"qualification", "qualification",

View file

@ -5,6 +5,7 @@ import type {
InspectionVersionedPlanViewModel, InspectionVersionedPlanViewModel,
} from "./inspectionPlan.models"; } from "./inspectionPlan.models";
import type { VehicleViewModel } from "../vehicle/vehicle.models"; import type { VehicleViewModel } from "../vehicle/vehicle.models";
import type { WearableViewModel } from "../wearable/wearable.models";
export type InspectionViewModel = { export type InspectionViewModel = {
id: string; id: string;
@ -28,6 +29,10 @@ export type InspectionViewModel = {
assigned: "vehicle"; assigned: "vehicle";
related: VehicleViewModel; related: VehicleViewModel;
} }
| {
assigned: "wearable";
related: WearableViewModel;
}
); );
export interface InspectionPointResultViewModel { export interface InspectionPointResultViewModel {

View file

@ -3,6 +3,7 @@ import type { EquipmentViewModel } from "../equipment/equipment.models";
import type { VehicleViewModel } from "../vehicle/vehicle.models"; import type { VehicleViewModel } from "../vehicle/vehicle.models";
import type { EquipmentTypeViewModel } from "../equipment/equipmentType.models"; import type { EquipmentTypeViewModel } from "../equipment/equipmentType.models";
import type { VehicleTypeViewModel } from "../vehicle/vehicleType.models"; import type { VehicleTypeViewModel } from "../vehicle/vehicleType.models";
import type { WearableTypeViewModel } from "../wearable/wearableType.models";
export type PlanTimeDefinition = `${number}-${"d" | "m" | "y"}` | `${number}/${number | "*"}`; export type PlanTimeDefinition = `${number}-${"d" | "m" | "y"}` | `${number}/${number | "*"}`;
@ -24,6 +25,10 @@ export type InspectionPlanViewModel = {
assigned: "vehicle"; assigned: "vehicle";
related: VehicleTypeViewModel; related: VehicleTypeViewModel;
} }
| {
assigned: "wearable";
related: WearableTypeViewModel;
}
); );
export interface InspectionVersionedPlanViewModel { export interface InspectionVersionedPlanViewModel {
@ -48,7 +53,7 @@ export interface CreateInspectionPlanViewModel {
inspectionInterval: PlanTimeDefinition; inspectionInterval: PlanTimeDefinition;
remindTime: PlanTimeDefinition; remindTime: PlanTimeDefinition;
relatedId: string; relatedId: string;
assigned: "vehicle" | "equipment"; assigned: "vehicle" | "equipment" | "wearable";
} }
export interface UpdateInspectionPlanViewModel { export interface UpdateInspectionPlanViewModel {

View file

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

View file

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

View file

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

View file

@ -27,7 +27,8 @@
</div> </div>
<EquipmentSearchSelect v-if="active == 'equipment'" title="Gerät" useScanner v-model="related" /> <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" /> <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 InspectionPlanSearchSelect from "@/components/search/InspectionPlanSearchSelect.vue";
import EquipmentSearchSelect from "@/components/search/EquipmentSearchSelect.vue"; import EquipmentSearchSelect from "@/components/search/EquipmentSearchSelect.vue";
import VehicleSearchSelect from "@/components/search/VehicleSearchSelect.vue"; import VehicleSearchSelect from "@/components/search/VehicleSearchSelect.vue";
import WearableSearchSelect from "../../../../components/search/wearableSearchSelect.vue";
</script> </script>
<script lang="ts"> <script lang="ts">
export default defineComponent({ export default defineComponent({
props: { props: {
type: { type: {
type: String as PropType<"vehicle" | "equipment">, type: String as PropType<"vehicle" | "equipment" | "wearable">,
default: "equipment", default: "equipment",
}, },
relatedId: String, relatedId: String,
@ -85,7 +87,7 @@ export default defineComponent({
timeout: null as any, timeout: null as any,
related: "", related: "",
inspectionPlan: "", inspectionPlan: "",
active: "equipment" as "equipment" | "vehicle", active: "equipment" as "equipment" | "vehicle" | "wearable",
tabs: [ tabs: [
{ {
key: "equipment", key: "equipment",
@ -95,11 +97,15 @@ export default defineComponent({
key: "vehicle", key: "vehicle",
title: "Fahrzeug", title: "Fahrzeug",
}, },
] as Array<{ key: "equipment" | "vehicle"; title: string }>, {
key: "wearable",
title: "Kleidung",
},
] as Array<{ key: "equipment" | "vehicle" | "wearable"; title: string }>,
}; };
}, },
mounted() { mounted() {
if (["vehicle", "equipment"].includes(this.type)) { if (["vehicle", "equipment", "wearable"].includes(this.type)) {
this.active = this.type; this.active = this.type;
this.inspectionPlan = this.inspectionPlanId ?? ""; this.inspectionPlan = this.inspectionPlanId ?? "";
this.related = this.relatedId ?? ""; 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"]), ...mapActions(useModalStore, ["openModal"]),
openDeleteModal() { openDeleteModal() {
this.openModal( this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/unit/vehicleType/CreateVehicleTypeModal.vue"))), markRaw(defineAsyncComponent(() => import("@/components/admin/unit/vehicleType/DeleteVehicleTypeModal.vue"))),
this.vehicleTypeId ?? "" 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 { return {
tabs: [ tabs: [
{ route: "admin-unit-wearable-overview", title: "Übersicht" }, { 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" }, { 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> <template>
<MainTemplate :title="`Kleidungs-Typ ${origin?.type} - Daten bearbeiten`"> <div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<template #headerInsert> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
</template> <form
<template #main> v-else-if="wearableType != null"
<Spinner v-if="loading == 'loading'" class="mx-auto" /> class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto"
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> @submit.prevent="triggerUpdate"
<form >
v-else-if="wearableType != null" <p class="mx-auto">Kleidungstyp bearbeiten</p>
class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto" <div>
@submit.prevent="triggerUpdate" <label for="type">Bezeichnung</label>
> <input type="text" id="type" required v-model="wearableType.type" />
<p class="mx-auto">Kleidungstyp bearbeiten</p> </div>
<div> <div>
<label for="type">Bezeichnung</label> <label for="description">Beschreibung (optional)</label>
<input type="text" id="type" required v-model="wearableType.type" /> <input type="text" id="description" v-model="wearableType.description" />
</div> </div>
<div> <div class="flex flex-row justify-end gap-2">
<label for="description">Beschreibung (optional)</label> <button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm">
<input type="text" id="description" v-model="wearableType.description" /> abbrechen
</div> </button>
<div class="flex flex-row justify-end gap-2"> <button primary type="submit" class="w-fit!" :disabled="status == 'loading'">speichern</button>
<button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm"> <Spinner v-if="status == 'loading'" class="my-auto" />
abbrechen <SuccessCheckmark v-else-if="status?.status == 'success'" />
</button> <FailureXMark v-else-if="status?.status == 'failed'" />
<button primary type="submit" class="w-fit!" :disabled="status == 'loading'">speichern</button> </div>
<Spinner v-if="status == 'loading'" class="my-auto" /> </form>
<SuccessCheckmark v-else-if="status?.status == 'success'" /> </div>
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</form>
</template>
</MainTemplate>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia"; import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useWearableTypeStore } from "@/stores/admin/unit/wearableType/wearableType"; import { useWearableTypeStore } from "@/stores/admin/unit/wearableType/wearableType";
import type { import type {
CreateWearableTypeViewModel, 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>