base data listing and operations

This commit is contained in:
Julian Krauser 2025-02-19 12:57:33 +01:00
parent 8353eca4a2
commit f3b5112bc6
20 changed files with 953 additions and 20 deletions

View file

@ -83,7 +83,7 @@ import {
TransitionRoot,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useForceStore } from "@/stores/admin/configuration/forces";
import { useForceStore } from "@/stores/admin/configuration/force";
import type { ForceViewModel } from "@/viewmodels/admin/configuration/force.models";
import difference from "lodash.difference";
import Spinner from "../Spinner.vue";

View file

@ -0,0 +1,96 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Fahrzeug erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<label for="name">Bezeichnung</label>
<input type="text" id="name" required />
</div>
<div>
<label for="code">Code (optional)</label>
<input type="text" id="code" />
</div>
<div>
<label for="type">Typ (optional)</label>
<input type="text" id="type" />
</div>
<div>
<label for="commissioned">verfügbar ab (optional)</label>
<input type="date" id="commissioned" />
</div>
<div>
<label for="decommissioned">verfügbar bis (optional)</label>
<input type="date" id="decommissioned" />
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</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 class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
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 Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useEquipmentStore } from "@/stores/admin/configuration/equipment";
import type { CreateEquipmentViewModel } from "@/viewmodels/admin/configuration/equipment.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEquipmentStore, ["createEquipment"]),
triggerCreate(e: any) {
let formData = e.target.elements;
let createEquipment: CreateEquipmentViewModel = {
code: formData.code.value,
type: formData.type.value,
name: formData.name.value,
commissioned: formData.commissioned.value,
decommissioned: formData.decommissioned.value,
};
this.status = "loading";
this.createEquipment(createEquipment)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,85 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Fahrzeug löschen</p>
</div>
<br />
<p class="text-center">
Fahrzeug {{ equipment?.name }} {{ equipment?.code ? `(${equipment.code})` : "" }} löschen?
</p>
<br />
<div class="flex flex-row gap-2">
<button
primary
type="submit"
:disabled="status == 'loading' || status?.status == 'success'"
@click="triggerDelete"
>
löschen
</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
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 Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useEquipmentStore } from "@/stores/admin/configuration/equipment";
import type { CreateEquipmentViewModel } from "@/viewmodels/admin/configuration/equipment.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useEquipmentStore, ["equipments"]),
equipment() {
return this.equipments.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEquipmentStore, ["deleteEquipment"]),
triggerDelete() {
this.status = "loading";
this.deleteEquipment(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,66 @@
<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>{{ equipment.name }} {{ equipment.type }} {{ equipment.code }}</p>
<div class="flex flex-row">
<div
v-if="can('update', 'configuration', 'equipment') && !equipment?.decommissioned"
@click="decommissionEquipment(equipment.id)"
>
<ArrowRightStartOnRectangleIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
<div v-if="can('update', 'configuration', 'equipment')" @click="openUpdateModal">
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
<div v-if="can('delete', 'configuration', 'equipment')" @click="openDeleteModal">
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
</div>
</div>
<div class="p-2">
<p>verfügbar ab: {{ equipment.commissioned }}</p>
<p>verfügbar bis: {{ equipment?.decommissioned ?? "---" }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { EquipmentViewModel } from "@/viewmodels/admin/configuration/equipment.models";
import { useModalStore } from "../../../../stores/modal";
import { ArrowRightStartOnRectangleIcon, PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useEquipmentStore } from "../../../../stores/admin/configuration/equipment";
</script>
<script lang="ts">
export default defineComponent({
props: {
equipment: { type: Object as PropType<EquipmentViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
methods: {
...mapActions(useModalStore, ["openModal"]),
...mapActions(useEquipmentStore, ["decommissionEquipment"]),
openUpdateModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/configuration/equipment/UpdateEquipmentModal.vue"))
),
this.equipment.id
);
},
openDeleteModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/configuration/equipment/DeleteEquipmentModal.vue"))
),
this.equipment.id
);
},
},
});
</script>

View file

@ -0,0 +1,140 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Fahrzeug aktualisieren</p>
</div>
<br />
<Spinner v-if="loading == 'loading' && equipment == null" class="mx-auto" />
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
<form v-if="equipment" class="flex flex-col gap-4 py-2" @submit.prevent="triggerUpdate">
<div>
<label for="name">Bezeichnung</label>
<input type="text" id="name" required v-model="equipment.name" />
</div>
<div>
<label for="code">Code (optional)</label>
<input type="text" id="code" v-model="equipment.code" />
</div>
<div>
<label for="type">Typ (optional)</label>
<input type="text" id="type" v-model="equipment.type" />
</div>
<div>
<label for="commissioned">verfügbar ab (optional)</label>
<input type="date" id="commissioned" v-model="equipment.commissioned" />
</div>
<div>
<label for="decommissioned">verfügbar bis (optional)</label>
<input type="date" id="decommissioned" v-model="equipment.decommissioned" />
</div>
<div class="flex flex-row gap-2">
<button primary-outline type="reset" :disabled="canSaveOrReset" @click="resetForm">verwerfen</button>
<button primary type="submit" :disabled="status == 'loading' || canSaveOrReset">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 class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
schließen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useEquipmentStore } from "@/stores/admin/configuration/equipment";
import type {
CreateEquipmentViewModel,
EquipmentViewModel,
UpdateEquipmentViewModel,
} from "@/viewmodels/admin/configuration/equipment.models";
import isEqual from "lodash.isequal";
import cloneDeep from "lodash.clonedeep";
</script>
<script lang="ts">
export default defineComponent({
props: {
data: { type: String, default: "" },
},
data() {
return {
loading: "loading" as "loading" | "fetched" | "failed",
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
equipment: null as null | EquipmentViewModel,
origin: null as null | EquipmentViewModel,
timeout: null as any,
};
},
computed: {
canSaveOrReset(): boolean {
return isEqual(this.origin, this.equipment);
},
},
mounted() {
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEquipmentStore, ["updateEquipment", "fetchEquipmentById"]),
resetForm() {
this.equipment = cloneDeep(this.origin);
},
fetchItem() {
this.loading = "loading";
this.fetchEquipmentById(this.data)
.then((res) => {
this.loading = "fetched";
this.origin = res.data;
this.equipment = cloneDeep(this.origin);
})
.catch((err) => {
this.loading = "failed";
});
},
triggerUpdate(e: any) {
let formData = e.target.elements;
let updateEquipment: UpdateEquipmentViewModel = {
id: this.data,
code: formData.code.value,
type: formData.type.value,
name: formData.name.value,
commissioned: formData.commissioned.value,
decommissioned: formData.decommissioned.value,
};
this.status = "loading";
this.updateEquipment(updateEquipment)
.then(() => {
this.fetchItem();
this.status = { status: "success" };
})
.catch(() => {
this.status = { status: "failed" };
})
.finally(() => {
this.timeout = setTimeout(() => {
this.status = null;
}, 2000);
});
},
},
});
</script>

View file

@ -56,7 +56,7 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useForceStore } from "@/stores/admin/configuration/forces";
import { useForceStore } from "@/stores/admin/configuration/force";
import type { CreateForceViewModel } from "@/viewmodels/admin/configuration/force.models";
</script>

View file

@ -40,7 +40,7 @@ import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useForceStore } from "@/stores/admin/configuration/forces";
import { useForceStore } from "@/stores/admin/configuration/force";
import type { CreateForceViewModel } from "@/viewmodels/admin/configuration/force.models";
</script>
@ -73,7 +73,6 @@ export default defineComponent({
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.$router.push({ name: "admin-configuration-force" });
this.closeModal();
}, 1500);
})

View file

@ -3,6 +3,12 @@
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ force.lastname }}, {{ force.firstname }} {{ force.nameaffix ? `- ${force.nameaffix}` : "" }}</p>
<div class="flex flex-row">
<div
v-if="can('update', 'configuration', 'force') && !force?.decommissioned"
@click="decommissionForce(force.id)"
>
<ArrowRightStartOnRectangleIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
<div v-if="can('update', 'configuration', 'force')" @click="openUpdateModal">
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
@ -24,7 +30,8 @@ import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { ForceViewModel } from "@/viewmodels/admin/configuration/force.models";
import { useModalStore } from "../../../../stores/modal";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { ArrowRightStartOnRectangleIcon, PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useForceStore } from "../../../../stores/admin/configuration/force";
</script>
<script lang="ts">
@ -37,6 +44,7 @@ export default defineComponent({
},
methods: {
...mapActions(useModalStore, ["openModal"]),
...mapActions(useForceStore, ["decommissionForce"]),
openUpdateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/configuration/force/UpdateForceModal.vue"))),

View file

@ -59,7 +59,7 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useForceStore } from "@/stores/admin/configuration/forces";
import { useForceStore } from "@/stores/admin/configuration/force";
import type {
CreateForceViewModel,
ForceViewModel,

View file

@ -0,0 +1,98 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Fahrzeug erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<label for="name">Bezeichnung</label>
<input type="text" id="name" required />
</div>
<div>
<label for="code">Code (optional)</label>
<input type="text" id="code" />
</div>
<div>
<label for="type">Typ (optional)</label>
<input type="text" id="type" />
</div>
<div>
<label for="commissioned">verfügbar ab (optional)</label>
<input type="date" id="commissioned" />
</div>
<div>
<label for="decommissioned">verfügbar bis (optional)</label>
<input type="date" id="decommissioned" />
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</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 class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
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 Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useVehicleStore } from "@/stores/admin/configuration/vehicle";
import type { CreateVehicleViewModel } from "@/viewmodels/admin/configuration/vehicle.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useVehicleStore, ["createVehicle"]),
triggerCreate(e: any) {
let formData = e.target.elements;
let createVehicle: CreateVehicleViewModel = {
code: formData.code.value,
type: formData.type.value,
name: formData.name.value,
commissioned: formData.commissioned.value,
decommissioned: formData.decommissioned.value,
};
this.status = "loading";
this.createVehicle(createVehicle)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,83 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Fahrzeug löschen</p>
</div>
<br />
<p class="text-center">Fahrzeug {{ vehicle?.name }} {{ vehicle?.code ? `(${vehicle.code})` : "" }} löschen?</p>
<br />
<div class="flex flex-row gap-2">
<button
primary
type="submit"
:disabled="status == 'loading' || status?.status == 'success'"
@click="triggerDelete"
>
löschen
</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
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 Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useVehicleStore } from "@/stores/admin/configuration/vehicle";
import type { CreateVehicleViewModel } from "@/viewmodels/admin/configuration/vehicle.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useVehicleStore, ["vehicles"]),
vehicle() {
return this.vehicles.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useVehicleStore, ["deleteVehicle"]),
triggerDelete() {
this.status = "loading";
this.deleteVehicle(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,140 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Fahrzeug aktualisieren</p>
</div>
<br />
<Spinner v-if="loading == 'loading' && vehicle == null" class="mx-auto" />
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
<form v-if="vehicle" class="flex flex-col gap-4 py-2" @submit.prevent="triggerUpdate">
<div>
<label for="name">Bezeichnung</label>
<input type="text" id="name" required v-model="vehicle.name" />
</div>
<div>
<label for="code">Code (optional)</label>
<input type="text" id="code" v-model="vehicle.code" />
</div>
<div>
<label for="type">Typ (optional)</label>
<input type="text" id="type" v-model="vehicle.type" />
</div>
<div>
<label for="commissioned">verfügbar ab (optional)</label>
<input type="date" id="commissioned" v-model="vehicle.commissioned" />
</div>
<div>
<label for="decommissioned">verfügbar bis (optional)</label>
<input type="date" id="decommissioned" v-model="vehicle.decommissioned" />
</div>
<div class="flex flex-row gap-2">
<button primary-outline type="reset" :disabled="canSaveOrReset" @click="resetForm">verwerfen</button>
<button primary type="submit" :disabled="status == 'loading' || canSaveOrReset">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 class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
schließen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useVehicleStore } from "@/stores/admin/configuration/vehicle";
import type {
CreateVehicleViewModel,
VehicleViewModel,
UpdateVehicleViewModel,
} from "@/viewmodels/admin/configuration/vehicle.models";
import isEqual from "lodash.isequal";
import cloneDeep from "lodash.clonedeep";
</script>
<script lang="ts">
export default defineComponent({
props: {
data: { type: String, default: "" },
},
data() {
return {
loading: "loading" as "loading" | "fetched" | "failed",
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
vehicle: null as null | VehicleViewModel,
origin: null as null | VehicleViewModel,
timeout: null as any,
};
},
computed: {
canSaveOrReset(): boolean {
return isEqual(this.origin, this.vehicle);
},
},
mounted() {
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useVehicleStore, ["updateVehicle", "fetchVehicleById"]),
resetForm() {
this.vehicle = cloneDeep(this.origin);
},
fetchItem() {
this.loading = "loading";
this.fetchVehicleById(this.data)
.then((res) => {
this.loading = "fetched";
this.origin = res.data;
this.vehicle = cloneDeep(this.origin);
})
.catch((err) => {
this.loading = "failed";
});
},
triggerUpdate(e: any) {
let formData = e.target.elements;
let updateVehicle: UpdateVehicleViewModel = {
id: this.data,
code: formData.code.value,
type: formData.type.value,
name: formData.name.value,
commissioned: formData.commissioned.value,
decommissioned: formData.decommissioned.value,
};
this.status = "loading";
this.updateVehicle(updateVehicle)
.then(() => {
this.fetchItem();
this.status = { status: "success" };
})
.catch(() => {
this.status = { status: "failed" };
})
.finally(() => {
this.timeout = setTimeout(() => {
this.status = null;
}, 2000);
});
},
},
});
</script>

View file

@ -0,0 +1,62 @@
<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>{{ vehicle.name }} {{ vehicle.type }} {{ vehicle.code }}</p>
<div class="flex flex-row">
<div
v-if="can('update', 'configuration', 'vehicle') && !vehicle?.decommissioned"
@click="decommissionVehicle(vehicle.id)"
>
<ArrowRightStartOnRectangleIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
<div v-if="can('update', 'configuration', 'vehicle')" @click="openUpdateModal">
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
<div v-if="can('delete', 'configuration', 'vehicle')" @click="openDeleteModal">
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
</div>
</div>
<div class="p-2">
<p>verfügbar ab: {{ vehicle.commissioned }}</p>
<p>verfügbar bis: {{ vehicle?.decommissioned ?? "---" }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { VehicleViewModel } from "@/viewmodels/admin/configuration/vehicle.models";
import { useModalStore } from "../../../../stores/modal";
import { ArrowRightStartOnRectangleIcon, PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useVehicleStore } from "../../../../stores/admin/configuration/vehicle";
</script>
<script lang="ts">
export default defineComponent({
props: {
vehicle: { type: Object as PropType<VehicleViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
methods: {
...mapActions(useModalStore, ["openModal"]),
...mapActions(useVehicleStore, ["decommissionVehicle"]),
openUpdateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/configuration/vehicle/UpdateVehicleModal.vue"))),
this.vehicle.id
);
},
openDeleteModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/configuration/vehicle/DeleteVehicleModal.vue"))),
this.vehicle.id
);
},
},
});
</script>

View file

@ -126,14 +126,14 @@ const router = createRouter({
{
path: "equipment",
name: "admin-configuration-equipment",
component: () => import("@/views/admin/configuration/force/Force.vue"),
component: () => import("@/views/admin/configuration/equipment/Equipment.vue"),
meta: { type: "read", section: "configuration", module: "equipment" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: "vehicle",
name: "admin-configuration-vehicle",
component: () => import("@/views/admin/configuration/force/Force.vue"),
component: () => import("@/views/admin/configuration/vehicle/Vehicle.vue"),
meta: { type: "read", section: "configuration", module: "vehicle" },
beforeEnter: [abilityAndNavUpdate],
},

View file

@ -64,17 +64,21 @@ export const useEquipmentStore = defineStore("equipment", {
},
async createEquipment(equipment: CreateEquipmentViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/equipment`, equipment);
this.fetchEquipments();
this.fetchEquipments(0, 25, "", true);
return result;
},
async updateActiveEquipment(equipment: UpdateEquipmentViewModel): Promise<AxiosResponse<any, any>> {
async updateEquipment(equipment: UpdateEquipmentViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/equipment/${equipment.id}`, equipment);
this.fetchEquipments();
this.fetchEquipments(0, 25, "", true);
return result;
},
async decommissionEquipment(equipmentId: string) {
await http.patch(`/admin/equipment/${equipmentId}/decommission`).catch(() => {});
this.fetchEquipments(0, 25, "", true);
},
async deleteEquipment(equipment: number): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/equipment/${equipment}`);
this.fetchEquipments();
this.fetchEquipments(0, 25, "", true);
return result;
},
},

View file

@ -64,17 +64,21 @@ export const useForceStore = defineStore("force", {
},
async createForce(force: CreateForceViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/force`, force);
this.fetchForces();
this.fetchForces(0, 25, "", true);
return result;
},
async updateForce(force: UpdateForceViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/force/${force.id}`, force);
this.fetchForces();
this.fetchForces(0, 25, "", true);
return result;
},
async decommissionForce(forceId: string) {
await http.patch(`/admin/force/${forceId}/decommission`).catch(() => {});
this.fetchForces(0, 25, "", true);
},
async deleteForce(force: number): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/force/${force}`);
this.fetchForces();
this.fetchForces(0, 25, "", true);
return result;
},
},

View file

@ -64,17 +64,21 @@ export const useVehicleStore = defineStore("vehicle", {
},
async createVehicle(vehicle: CreateVehicleViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/vehicle`, vehicle);
this.fetchVehicles();
this.fetchVehicles(0, 25, "", true);
return result;
},
async updateActiveVehicle(vehicle: UpdateVehicleViewModel): Promise<AxiosResponse<any, any>> {
async updateVehicle(vehicle: UpdateVehicleViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/vehicle/${vehicle.id}`, vehicle);
this.fetchVehicles();
this.fetchVehicles(0, 25, "", true);
return result;
},
async decommissionVehicle(vehicleId: string) {
await http.patch(`/admin/vehicle/${vehicleId}/decommission`).catch(() => {});
this.fetchVehicles(0, 25, "", true);
},
async deleteVehicle(vehicle: number): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/vehicle/${vehicle}`);
this.fetchVehicles();
this.fetchVehicles(0, 25, "", true);
return result;
},
},

View file

@ -0,0 +1,73 @@
<template>
<MainTemplate>
<template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Ausrüstung</h1>
</div>
</template>
<template #diffMain>
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
<Pagination
:items="equipments"
:totalCount="totalCount"
:indicateLoading="loading == 'loading'"
:useSearch="true"
@load-data="(offset, count, search) => fetchEquipments(offset, count, search)"
@search="(search) => fetchEquipments(0, maxEntriesPerPage, search, true)"
>
<template #pageRow="{ row }: { row: EquipmentViewModel }">
<EquipmentListItem :equipment="row" />
</template>
</Pagination>
<div class="flex flex-row gap-4">
<button v-if="can('create', 'operation', 'equipment')" primary class="!w-fit" @click="openCreateModal">
Ausrüstung erstellen
</button>
</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 { useEquipmentStore } from "@/stores/admin/configuration/equipment";
import EquipmentListItem from "@/components/admin/configuration/equipment/EquipmentListItem.vue";
import { useModalStore } from "@/stores/modal";
import Pagination from "@/components/Pagination.vue";
import type { EquipmentViewModel } from "@/viewmodels/admin/configuration/equipment.models";
import { useAbilityStore } from "@/stores/ability";
import { DocumentTextIcon, PencilIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
currentPage: 0,
maxEntriesPerPage: 25,
};
},
computed: {
...mapState(useEquipmentStore, ["equipments", "totalCount", "loading"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchEquipments(0, this.maxEntriesPerPage, "", true);
},
methods: {
...mapActions(useEquipmentStore, ["fetchEquipments"]),
...mapActions(useModalStore, ["openModal"]),
openCreateModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/configuration/equipment/CreateEquipmentModal.vue"))
)
);
},
},
});
</script>

View file

@ -34,7 +34,7 @@
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useForceStore } from "@/stores/admin/configuration/forces";
import { useForceStore } from "@/stores/admin/configuration/force";
import ForceListItem from "@/components/admin/configuration/force/ForceListItem.vue";
import { useModalStore } from "@/stores/modal";
import Pagination from "@/components/Pagination.vue";

View file

@ -0,0 +1,71 @@
<template>
<MainTemplate>
<template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Fahrzeuge</h1>
</div>
</template>
<template #diffMain>
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
<Pagination
:items="vehicles"
:totalCount="totalCount"
:indicateLoading="loading == 'loading'"
:useSearch="true"
@load-data="(offset, count, search) => fetchVehicles(offset, count, search)"
@search="(search) => fetchVehicles(0, maxEntriesPerPage, search, true)"
>
<template #pageRow="{ row }: { row: VehicleViewModel }">
<VehicleListItem :vehicle="row" />
</template>
</Pagination>
<div class="flex flex-row gap-4">
<button v-if="can('create', 'operation', 'vehicle')" primary class="!w-fit" @click="openCreateModal">
Fahrzeug erstellen
</button>
</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 { useVehicleStore } from "@/stores/admin/configuration/vehicle";
import VehicleListItem from "@/components/admin/configuration/vehicle/VehicleListItem.vue";
import { useModalStore } from "@/stores/modal";
import Pagination from "@/components/Pagination.vue";
import type { VehicleViewModel } from "@/viewmodels/admin/configuration/vehicle.models";
import { useAbilityStore } from "@/stores/ability";
import { DocumentTextIcon, PencilIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
currentPage: 0,
maxEntriesPerPage: 25,
};
},
computed: {
...mapState(useVehicleStore, ["vehicles", "totalCount", "loading"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchVehicles(0, this.maxEntriesPerPage, "", true);
},
methods: {
...mapActions(useVehicleStore, ["fetchVehicles"]),
...mapActions(useModalStore, ["openModal"]),
openCreateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/configuration/vehicle/CreateVehicleModal.vue")))
);
},
},
});
</script>