show details to damage

This commit is contained in:
Julian Krauser 2025-07-17 09:32:18 +02:00
parent 93a04abee1
commit 5ea5a0160a
10 changed files with 332 additions and 41 deletions

View file

@ -1,5 +1,8 @@
<template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
<RouterLink
:to="{ name: 'admin-unit-damage_report-overview', params: { damageReportId: damageReport.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">
<p>
{{ damageReport?.related?.name ?? "Ohne Zuordnung" }}
@ -15,12 +18,15 @@
<div v-if="damageReport.reportedBy" class="cursor-pointer" title="hi">
<UserIcon class="w-5 h-5" />
</div>
<div v-if="damageReport.maintenance" class="cursor-pointer" title="hi">
<WrenchScrewdriverIcon class="w-5 h-5" />
</div>
</div>
</div>
<div class="p-2">
<p v-if="damageReport.description">Beschreibung: {{ damageReport.description }}</p>
</div>
</div>
</RouterLink>
</template>
<script setup lang="ts">
@ -28,7 +34,7 @@ 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";
import { MapPinIcon, PhotoIcon, UserIcon } from "@heroicons/vue/24/outline";
import { MapPinIcon, PhotoIcon, UserIcon, WrenchScrewdriverIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">

View file

@ -1,3 +1,4 @@
declare global {
type Optional<T> = T | { [K in keyof T]?: never };
//type Optional<T> = T | { [K in keyof T]?: never };
type Optional<T> = T | never;
}

View file

@ -20,6 +20,7 @@ import { resetInspectionPlanStores, setInspectionPlanId } from "./unit/inspectio
import { setVehicleTypeId } from "./unit/vehicleType";
import { resetInspectionStores, setInspectionId } from "./unit/inspection";
import { setWearableTypeId } from "./unit/wearableType";
import { resetDamageReportStores, setDamageReportId } from "./unit/damageReport";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -697,19 +698,47 @@ const router = createRouter({
{
path: "damage-report",
name: "admin-unit-damage_report-route",
component: () => import("@/views/admin/unit/damageReport/DamageReportRouting.vue"),
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "unit", module: "damage_report" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-unit-damage_report",
component: () => import("@/views/admin/unit/damageReport/DamageReport.vue"),
redirect: { name: "admin-unit-damage_report-open" },
},
{
path: "done",
name: "admin-unit-damage_report-done",
component: () => import("@/views/admin/unit/damageReport/DamageReport.vue"),
path: "status",
name: "admin-unit-damage_report-statusrouting",
component: () => import("@/views/admin/unit/damageReport/DamageReportStatusRouting.vue"),
beforeEnter: [resetDamageReportStores],
children: [
{
path: "open",
name: "admin-unit-damage_report-open",
component: () => import("@/views/admin/unit/damageReport/DamageReport.vue"),
},
{
path: "done",
name: "admin-unit-damage_report-done",
component: () => import("@/views/admin/unit/damageReport/DamageReportClosed.vue"),
},
],
},
{
path: ":damageReportId",
name: "admin-unit-damage_report-routing",
component: () => import("@/views/admin/unit/damageReport/DamageReportRouting.vue"),
beforeEnter: [setDamageReportId],
props: true,
children: [
{
path: "",
name: "admin-unit-damage_report-overview",
component: () => import("@/views/admin/unit/damageReport/Overview.vue"),
props: true,
},
],
},
],
},

View file

@ -0,0 +1,20 @@
import { useDamageReportStore } from "@/stores/admin/unit/damageReport/damageReport";
export async function setDamageReportId(to: any, from: any, next: any) {
const damageReportStore = useDamageReportStore();
damageReportStore.activeDamageReport = to.params?.damageReportId ?? null;
//xystore().$reset();
next();
}
export async function resetDamageReportStores(to: any, from: any, next: any) {
const damageReportStore = useDamageReportStore();
damageReportStore.activeDamageReport = null;
damageReportStore.activeDamageReportObj = null;
//xystore().$reset();
next();
}

View file

@ -1,9 +1,5 @@
import { defineStore } from "pinia";
import type {
DamageReportViewModel,
CreateDamageReportViewModel,
UpdateDamageReportViewModel,
} from "@/viewmodels/admin/unit/damageReport.models";
import type { DamageReportViewModel, UpdateDamageReportViewModel } from "@/viewmodels/admin/unit/damageReport.models";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
@ -13,28 +9,46 @@ export const useDamageReportStore = defineStore("damageReport", {
damageReports: [] as Array<DamageReportViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
activeDamageReport: null as string | null,
activeDamageReportObj: null as DamageReportViewModel | null,
loadingActive: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchDamageReports(offset = 0, count = 25, search = "", clear = false) {
formatQueryReturnToPagination(result: AxiosResponse<any, any>, offset: number) {
this.totalCount = result.data.total;
result.data.damageReports
.filter((elem: DamageReportViewModel) => this.damageReports.findIndex((m) => m.id == elem.id) == -1)
.map((elem: DamageReportViewModel, index: number): DamageReportViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: DamageReportViewModel & { tab_pos: number }) => {
this.damageReports.push(elem);
});
},
fetchOpenDamageReports(offset = 0, count = 25, search = "", clear = false) {
if (clear) this.damageReports = [];
this.loading = "loading";
//TODO enable fetch of done reports
http
.get(`/admin/damageReport?offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.get(`/admin/damageReport?done=false&offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.then((result) => {
this.totalCount = result.data.total;
result.data.damageReports
.filter((elem: DamageReportViewModel) => this.damageReports.findIndex((m) => m.id == elem.id) == -1)
.map((elem: DamageReportViewModel, index: number): DamageReportViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: DamageReportViewModel & { tab_pos: number }) => {
this.damageReports.push(elem);
});
this.formatQueryReturnToPagination(result, offset);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
fetchDoneDamageReports(offset = 0, count = 25, search = "", clear = false) {
if (clear) this.damageReports = [];
this.loading = "loading";
http
.get(`/admin/damageReport?done=true&offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.then((result) => {
this.formatQueryReturnToPagination(result, offset);
this.loading = "fetched";
})
.catch((err) => {
@ -60,14 +74,30 @@ export const useDamageReportStore = defineStore("damageReport", {
return { ...res, data: res.data.damageReports };
});
},
fetchDamageReportByActiveId() {
this.loadingActive = "loading";
http
.get(`/admin/damageReport/${this.activeDamageReport}`)
.then((res) => {
this.activeDamageReportObj = res.data;
this.loadingActive = "fetched";
})
.catch((err) => {
this.loadingActive = "failed";
});
},
fetchDamageReportById(id: string) {
return http.get(`/admin/damageReport/${id}`);
},
loadDamageReportImage(url: string) {
return http.get(`/admin/damageReport/${this.activeDamageReportObj?.id}/${url}`, {
responseType: "blob",
});
},
async updateDamageReport(damageReport: UpdateDamageReportViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/damageReport/${damageReport.id}`, {
// TODO: data
});
this.fetchDamageReports();
return result;
},
},

View file

@ -4,8 +4,8 @@
:items="damageReports"
:totalCount="totalCount"
:indicateLoading="loading == 'loading'"
@load-data="(offset, count, search) => fetchDamageReports(offset, count, search)"
@search="(search) => fetchDamageReports(0, maxEntriesPerPage, search, true)"
@load-data="(offset, count, search) => fetchOpenDamageReports(offset, count, search)"
@search="(search) => fetchOpenDamageReports(0, maxEntriesPerPage, search, true)"
>
<template #pageRow="{ row }: { row: DamageReportViewModel }">
<DamageReportListItem :damageReport="row" />
@ -37,10 +37,10 @@ export default defineComponent({
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchDamageReports(0, this.maxEntriesPerPage, "", true);
this.fetchOpenDamageReports(0, this.maxEntriesPerPage, "", true);
},
methods: {
...mapActions(useDamageReportStore, ["fetchDamageReports"]),
...mapActions(useDamageReportStore, ["fetchOpenDamageReports"]),
},
});
</script>

View file

@ -0,0 +1,46 @@
<template>
<div class="flex flex-col w-full h-full gap-2 justify-center">
<Pagination
:items="damageReports"
:totalCount="totalCount"
:indicateLoading="loading == 'loading'"
@load-data="(offset, count, search) => fetchDoneDamageReports(offset, count, search)"
@search="(search) => fetchDoneDamageReports(0, maxEntriesPerPage, search, true)"
>
<template #pageRow="{ row }: { row: DamageReportViewModel }">
<DamageReportListItem :damageReport="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 { useDamageReportStore } from "@/stores/admin/unit/damageReport/damageReport";
import type { DamageReportViewModel } from "@/viewmodels/admin/unit/damageReport.models";
import Pagination from "@/components/Pagination.vue";
import DamageReportListItem from "@/components/admin/unit/damageReport/DamageReportListItem.vue";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
maxEntriesPerPage: 25,
};
},
computed: {
...mapState(useDamageReportStore, ["damageReports", "totalCount", "loading"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchDoneDamageReports(0, this.maxEntriesPerPage, "", true);
},
methods: {
...mapActions(useDamageReportStore, ["fetchDoneDamageReports"]),
},
});
</script>

View file

@ -1,9 +1,29 @@
<template>
<MainTemplate title="Schadensmeldungen">
<MainTemplate>
<template #headerInsert>
<RouterLink :to="{ name: 'admin-unit-damage_report-open' }" class="text-primary">zurück zur Liste</RouterLink>
</template>
<template #topBar>
<h1 class="font-bold text-xl h-8 min-h-fit">
Schadensmeldung:
{{ activeDamageReportObj?.related?.name ?? "Ohne Zuordnung" }}
<small v-if="activeDamageReportObj?.related">({{ activeDamageReportObj.related.code }})</small>
</h1>
<RouterLink
v-if="activeDamageReportObj?.related && can('read', 'unit', 'equipment')"
:to="{
name: `admin-unit-${activeDamageReportObj.assigned}-overview`,
params: { [`${activeDamageReportObj.assigned}Id`]: activeDamageReportObj.related.id ?? '_' },
}"
>
<ArrowTopRightOnSquareIcon class="w-5 h-5" />
</RouterLink>
</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">
<div hidden class="w-full flex flex-row max-lg:flex-wrap justify-center">
<RouterLink
v-for="tab in tabs"
:key="tab.route"
@ -34,21 +54,29 @@ 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";
import { useDamageReportStore } from "@/stores/admin/unit/damageReport/damageReport";
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
props: {
damageReportId: String,
},
data() {
return {
tabs: [
{ route: "admin-unit-damage_report", title: "offen" },
{ route: "admin-unit-damage_report-done", title: "bearbeitet" },
],
tabs: [{ route: "admin-unit-damage_report-overview", title: "Übersicht" }],
};
},
computed: {
...mapState(useDamageReportStore, ["activeDamageReportObj"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchDamageReportByActiveId();
},
methods: {
...mapActions(useDamageReportStore, ["fetchDamageReportByActiveId"]),
},
});
</script>

View file

@ -0,0 +1,54 @@
<template>
<MainTemplate title="Schadensmeldungen">
<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-damage_report-open", title: "offen" },
{ route: "admin-unit-damage_report-done", title: "bearbeitet" },
],
};
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -0,0 +1,77 @@
<template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<div v-if="activeDamageReportObj != null" class="flex flex-col gap-2 w-full">
<div>
<label for="status">Status</label>
<input id="status" type="text" readonly :value="activeDamageReportObj.status" />
</div>
<br />
<div>
<label for="description">Beschreibung des Schadens</label>
<textarea id="description" readonly :value="activeDamageReportObj.description"></textarea>
</div>
<div>
<label for="location">Fundort</label>
<textarea id="location" readonly :value="activeDamageReportObj.location || '---'"></textarea>
</div>
<div>
<label for="note">Anmerkung für Bearbeiter</label>
<textarea id="note" readonly :value="activeDamageReportObj.note || '---'"></textarea>
</div>
<div>
<label for="reportedBy">Gemeldet von</label>
<input id="reportedBy" type="text" readonly :value="activeDamageReportObj.reportedBy || '---'" />
</div>
<div>
<label>Bild</label>
<div ref="imgs"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import { useDamageReportStore } from "@/stores/admin/unit/damageReport/damageReport";
</script>
<script lang="ts">
export default defineComponent({
props: {
damageReportId: String,
},
data() {
return {};
},
computed: {
...mapState(useDamageReportStore, ["activeDamageReportObj"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.loadImages();
},
methods: {
...mapActions(useDamageReportStore, ["loadDamageReportImage"]),
loadImages() {
for (const i of this.activeDamageReportObj?.images ?? []) {
this.loadDamageReportImage(i)
.then((response) => {
const contentType =
response.headers && response.headers["content-type"] ? response.headers["content-type"] : "image/*";
const blob = new Blob([response.data], { type: contentType });
const img = document.createElement("img");
img.src = window.URL.createObjectURL(blob);
img.alt = "Schadensbild";
img.classList = "h-35 w-auto";
(this.$refs.imgs as HTMLElement).appendChild(img);
})
.catch((err) => {
console.log(err);
});
}
},
},
});
</script>