unit/#116-repairs #119

Merged
jkeffects merged 4 commits from unit/#116-repairs into milestone/ff-admin-unit 2025-07-23 07:30:04 +00:00
9 changed files with 201 additions and 74 deletions
Showing only changes of commit 789272dc37 - Show all commits

View file

@ -111,7 +111,7 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
emits: ["update:model-value", "add:difference", "remove:difference", "add:damageReport", "add:damageReportByArray"], emits: ["update:model-value", "add:difference", "remove:difference", "add:damageReport"],
watch: { watch: {
modelValue() { modelValue() {
// if (this.initialLoaded) return; // if (this.initialLoaded) return;
@ -215,15 +215,18 @@ export default defineComponent({
}); });
}, },
getDamageReportFromSearch(id: string) { getDamageReportFromSearch(id: string) {
return this.filtered.find((f) => f.id == id); return this.available.find((f) => f.id == id);
}, },
loadDamageReportsInitial() { loadDamageReportsInitial() {
if (this.modelValue.length == 0) return; if (this.modelValue.length == 0) {
this.chosen = [];
} else {
this.getDamageReportsByIds(this.modelValue) this.getDamageReportsByIds(this.modelValue)
.then((res) => { .then((res) => {
this.chosen = res.data; this.chosen = res.data;
}) })
.catch(() => {}); .catch(() => {});
}
}, },
}, },
}); });

View file

@ -201,7 +201,7 @@ export default defineComponent({
}); });
}, },
getDamageReportFromSearch(id: string) { getDamageReportFromSearch(id: string) {
return this.filtered.find((f) => f.id == id); return this.available.find((f) => f.id == id);
}, },
loadDamageReportInitial() { loadDamageReportInitial() {
if (this.modelValue == "" || this.modelValue == null) return; if (this.modelValue == "" || this.modelValue == null) return;

View file

@ -821,6 +821,12 @@ const router = createRouter({
component: () => import("@/views/admin/unit/repair/Overview.vue"), component: () => import("@/views/admin/unit/repair/Overview.vue"),
props: true, props: true,
}, },
{
path: "reports",
name: "admin-unit-repair-reports",
component: () => import("@/views/admin/unit/repair/DamageReports.vue"),
props: true,
},
], ],
}, },
], ],

View file

@ -2,6 +2,7 @@ import { defineStore } from "pinia";
import type { import type {
CreateRepairViewModel, CreateRepairViewModel,
RepairViewModel, RepairViewModel,
UpdateRepairStatusViewModel,
UpdateRepairViewModel, UpdateRepairViewModel,
} from "@/viewmodels/admin/unit/repair.models"; } from "@/viewmodels/admin/unit/repair.models";
import { http } from "@/serverCom"; import { http } from "@/serverCom";
@ -110,9 +111,22 @@ export const useRepairStore = defineStore("repair", {
return result; return result;
}, },
async updateRepair(repair: UpdateRepairViewModel): Promise<AxiosResponse<any, any>> { async updateRepair(repair: UpdateRepairViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/repair/${repair.id}`, { const result = await http.patch(`/admin/repair/${this.activeRepairObj?.id}`, {
title: repair.title,
description: repair.description,
responsible: repair.responsible,
});
return result;
},
async updateRepairReports(reports: Array<string>): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/repair/${this.activeRepairObj?.id}/reports`, {
reports,
});
return result;
},
async updateRepairStatus(repair: UpdateRepairStatusViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/repair/${this.activeRepairObj?.id}/status`, {
status: repair.status, status: repair.status,
noteByWorker: repair.noteByWorker,
done: repair.done, done: repair.done,
}); });
return result; return result;

View file

@ -43,9 +43,15 @@ export interface CreateRepairViewModel {
reports: string[]; reports: string[];
} }
export interface UpdateRepairViewModel { export interface UpdateRepairStatusViewModel {
id: string; id: string;
status: string; status: string;
noteByWorker: string;
done: boolean; done: boolean;
} }
export interface UpdateRepairViewModel {
id: string;
title: string;
description: string;
responsible: string;
}

View file

@ -0,0 +1,92 @@
<template>
<div v-if="activeRepairObj != null" class="flex flex-col gap-2 h-full w-full">
<DamageReportSearchSelectMultipleWithRelated
title="verbundene Schadensmeldungen"
:related="activeRepairObj.assigned"
:relatedId="activeRepairObj.relatedId"
:model-value="activeRepairObj.reports.map((r) => r.id)"
:disabled="!!activeRepairObj.finishedAt"
@add:damage-report="handleReportAdd"
@remove:difference="handleReportRemove"
/>
<div class="flex flex-col gap-2 overflow-y-scroll h-full">
<div
v-for="damageReport in activeRepairObj.reports"
:key="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.title }}</p>
<TrashIcon
v-if="!activeRepairObj.finishedAt"
class="w-5 h-5 cursor-pointer"
@click="handleReportRemove(damageReport.id)"
/>
</div>
<div class="p-2">
<p v-if="damageReport.description">Beschreibung: {{ damageReport.description }}</p>
</div>
</div>
</div>
<button primary class="w-fit! self-end" :disabled="!!activeRepairObj.finishedAt" @click="saveReports">
Änderungen speichern
</button>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import { useRepairStore } from "@/stores/admin/unit/repair";
import type { RepairViewModel, UpdateRepairViewModel } from "@/viewmodels/admin/unit/repair.models";
import DamageReportSearchSelectMultipleWithRelated from "@/components/search/DamageReportSearchSelectMultipleWithRelated.vue";
import { aC } from "node_modules/@fullcalendar/core/internal-common";
import type { DamageReportViewModel } from "@/viewmodels/admin/unit/damageReport.models";
import { TrashIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
props: {
repairId: String,
},
watch: {},
data() {
return {
loading: undefined as undefined | "loading" | "success" | "failed",
};
},
computed: {
...mapWritableState(useRepairStore, ["activeRepairObj"]),
...mapState(useAbilityStore, ["can"]),
},
methods: {
...mapActions(useRepairStore, ["loadRepairImage", "updateRepairReports"]),
handleReportAdd(report: DamageReportViewModel) {
if (!this.activeRepairObj) return;
this.activeRepairObj.reports.push(report);
},
handleReportRemove(reportId: string) {
if (!this.activeRepairObj) return;
this.activeRepairObj.reports = this.activeRepairObj.reports.filter((r) => r.id != reportId);
},
saveReports() {
if (this.activeRepairObj == null) return;
this.loading = "loading";
this.updateRepairReports(this.activeRepairObj.reports.map((r) => r.id))
.then((res) => {
this.loading = "success";
})
.catch((err) => {
this.loading = "failed";
})
.finally(() => {
setTimeout(() => {
this.loading = undefined;
}, 2000);
});
},
},
});
</script>

View file

@ -1,6 +1,5 @@
<template> <template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto"> <div v-if="activeRepairObj != null" class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<div v-if="activeRepairObj != null" class="flex flex-col gap-2 w-full">
<div> <div>
<label for="status">Status</label> <label for="status">Status</label>
<input id="status" ref="status" type="text" :readonly="!editStatus" :value="activeRepairObj.status" /> <input id="status" ref="status" type="text" :readonly="!editStatus" :value="activeRepairObj.status" />
@ -18,21 +17,23 @@
<button primary class="w-fit!" @click="saveStatus(false)">Status speichern</button> <button primary class="w-fit!" @click="saveStatus(false)">Status speichern</button>
</div> </div>
<br /> <br />
<form class="flex flex-col gap-2" @submit.prevent="saveData">
<div> <div>
<label for="description">Beschreibung des Schadens</label> <label for="title">Kurzbeschreibung</label>
<textarea id="description" readonly :value="activeRepairObj.description"></textarea> <input id="title" type="text" placeholder="---" :value="activeRepairObj.title" />
</div>
<div>
<label for="description">Beschreibung der Reparatur</label>
<textarea id="description" placeholder="---" :value="activeRepairObj.description"></textarea>
</div> </div>
<div> <div>
<label for="responsible">Verantwortlich</label> <label for="responsible">Verantwortlich</label>
<input id="responsible" type="text" readonly placeholder="---" :value="activeRepairObj.responsible" /> <input id="responsible" type="text" placeholder="---" :value="activeRepairObj.responsible" />
</div>
<div>
<label>Bild</label>
<div ref="imgs">
<small v-if="activeRepairObj.images.length == 0">Keine Bilder hochgeladen</small>
</div>
</div>
</div> </div>
<button primary type="submit" class="w-fit! self-end" :disabled="!!activeRepairObj.finishedAt">
Änderungen speichern
</button>
</form>
</div> </div>
</template> </template>
@ -41,7 +42,7 @@ import { defineComponent } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia"; import { mapActions, mapState, mapWritableState } from "pinia";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { useRepairStore } from "@/stores/admin/unit/repair"; import { useRepairStore } from "@/stores/admin/unit/repair";
import type { RepairViewModel, UpdateRepairViewModel } from "@/viewmodels/admin/unit/repair.models"; import type { UpdateRepairStatusViewModel, UpdateRepairViewModel } from "@/viewmodels/admin/unit/repair.models";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -49,11 +50,6 @@ export default defineComponent({
props: { props: {
repairId: String, repairId: String,
}, },
watch: {
activeRepairObj(val: RepairViewModel, oldVal: RepairViewModel | null) {
if (val && oldVal == null) this.loadImages();
},
},
data() { data() {
return { return {
editStatus: false as boolean, editStatus: false as boolean,
@ -64,41 +60,47 @@ export default defineComponent({
...mapWritableState(useRepairStore, ["activeRepairObj"]), ...mapWritableState(useRepairStore, ["activeRepairObj"]),
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),
}, },
mounted() {
this.loadImages();
},
methods: { methods: {
...mapActions(useRepairStore, ["loadRepairImage", "updateRepair"]), ...mapActions(useRepairStore, ["loadRepairImage", "updateRepairStatus", "updateRepair"]),
loadImages() {
for (const i of this.activeRepairObj?.images ?? []) {
this.loadRepairImage(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);
});
}
},
saveStatus(finish: boolean) { saveStatus(finish: boolean) {
if (this.activeRepairObj == null) return; if (this.activeRepairObj == null) return;
this.loading = "loading"; this.loading = "loading";
let update: UpdateRepairViewModel = { let update: UpdateRepairStatusViewModel = {
id: this.activeRepairObj.id, id: this.activeRepairObj.id,
status: (this.$refs.status as HTMLInputElement).value, status: (this.$refs.status as HTMLInputElement).value,
noteByWorker: (this.$refs.noteByWorker as HTMLInputElement).value,
done: finish, done: finish,
}; };
this.updateRepairStatus(update)
.then((res) => {
this.activeRepairObj!.status = update.status;
this.activeRepairObj!.finishedAt = update.done ? new Date() : undefined;
this.loading = "success";
this.editStatus = false;
})
.catch((err) => {
this.loading = "failed";
})
.finally(() => {
setTimeout(() => {
this.loading = undefined;
}, 2000);
});
},
saveData(e: any) {
if (this.activeRepairObj == null) return;
this.loading = "loading";
const formData = e.target.elements;
let update: UpdateRepairViewModel = {
id: this.activeRepairObj.id,
title: formData.title.value,
description: formData.description.value,
responsible: formData.responsible.value,
};
this.updateRepair(update) this.updateRepair(update)
.then((res) => { .then((res) => {
this.activeRepairObj!.status = update.status; this.activeRepairObj!.title = update.title;
this.activeRepairObj!.description = update.description;
this.activeRepairObj!.responsible = update.responsible;
this.loading = "success"; this.loading = "success";
this.editStatus = false; this.editStatus = false;
}) })

View file

@ -23,7 +23,7 @@
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-2 grow px-7 overflow-hidden"> <div class="flex flex-col gap-2 grow px-7 overflow-hidden">
<div class="flex flex-col grow gap-2 overflow-hidden"> <div class="flex flex-col grow gap-2 overflow-hidden">
<div hidden class="w-full flex flex-row max-lg:flex-wrap justify-center"> <div class="w-full flex flex-row max-lg:flex-wrap justify-center">
<RouterLink <RouterLink
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.route" :key="tab.route"
@ -65,7 +65,11 @@ export default defineComponent({
}, },
data() { data() {
return { return {
tabs: [{ route: "admin-unit-repair-overview", title: "Übersicht" }], tabs: [
{ route: "admin-unit-repair-overview", title: "Übersicht" },
// { route: "admin-unit-repair-overview", title: "Bilder & Bericht" },
{ route: "admin-unit-repair-reports", title: "Schadensmeldungen" },
],
}; };
}, },
computed: { computed: {

View file

@ -1,5 +1,5 @@
<template> <template>
<MainTemplate title="Schadensmeldungen"> <MainTemplate title="Reparaturen">
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-2 grow px-7 overflow-hidden"> <div class="flex flex-col gap-2 grow px-7 overflow-hidden">
<div class="w-full flex flex-row max-lg:flex-wrap justify-center"> <div class="w-full flex flex-row max-lg:flex-wrap justify-center">