force management

This commit is contained in:
Julian Krauser 2025-02-18 11:39:12 +01:00
parent f50dff99f3
commit 8353eca4a2
14 changed files with 453 additions and 30 deletions

View file

@ -5,6 +5,10 @@
</div> </div>
<br /> <br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate"> <form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<label for="internalId">Interne Id (optional)</label>
<input type="text" id="internalId" />
</div>
<div> <div>
<label for="firstname">Vorname</label> <label for="firstname">Vorname</label>
<input type="text" id="firstname" required /> <input type="text" id="firstname" required />
@ -17,6 +21,14 @@
<label for="nameaffix">Nameaffix (optional)</label> <label for="nameaffix">Nameaffix (optional)</label>
<input type="text" id="nameaffix" /> <input type="text" id="nameaffix" />
</div> </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"> <div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button> <button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
<Spinner v-if="status == 'loading'" class="my-auto" /> <Spinner v-if="status == 'loading'" class="my-auto" />
@ -67,9 +79,12 @@ export default defineComponent({
triggerCreate(e: any) { triggerCreate(e: any) {
let formData = e.target.elements; let formData = e.target.elements;
let createForce: CreateForceViewModel = { let createForce: CreateForceViewModel = {
internalId: formData.internalId.value,
firstname: formData.firstname.value, firstname: formData.firstname.value,
lastname: formData.lastname.value, lastname: formData.lastname.value,
nameaffix: formData.nameaffix.value, nameaffix: formData.nameaffix.value,
commissioned: formData.commissioned.value,
decommissioned: formData.decommissioned.value,
}; };
this.status = "loading"; this.status = "loading";
this.createForce(createForce) this.createForce(createForce)

View file

@ -1,12 +1,11 @@
<template> <template>
<div class="w-full md:max-w-md"> <div class="w-full md:max-w-md">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied löschen</p> <p class="text-xl font-medium">Kraft löschen</p>
</div> </div>
<br /> <br />
<p class="text-center"> <p class="text-center">
Mitglied {{ force?.lastname }}, {{ force?.firstname }} Kraft {{ force?.lastname }}, {{ force?.firstname }} {{ force?.nameaffix ? `- ${force.nameaffix}` : "" }} löschen?
{{ force?.nameaffix ? `- ${force.nameaffix}` : "" }} löschen?
</p> </p>
<br /> <br />
@ -74,7 +73,7 @@ export default defineComponent({
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
this.$router.push({ name: "admin-club-force" }); this.$router.push({ name: "admin-configuration-force" });
this.closeModal(); this.closeModal();
}, 1500); }, 1500);
}) })

View file

@ -1,22 +1,30 @@
<template> <template>
<RouterLink <div class="flex flex-col h-fit w-full border border-primary rounded-md">
:to="{ name: 'admin-club-force-overview', params: { forceId: force.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>{{ force.lastname }}, {{ force.firstname }} {{ force.nameaffix ? `- ${force.nameaffix}` : "" }}</p> <p>{{ force.lastname }}, {{ force.firstname }} {{ force.nameaffix ? `- ${force.nameaffix}` : "" }}</p>
<div class="flex flex-row">
<div v-if="can('update', 'configuration', 'force')" @click="openUpdateModal">
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
<div v-if="can('delete', 'configuration', 'force')" @click="openDeleteModal">
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
</div>
</div> </div>
<div class="p-2"> <div class="p-2">
<p>Daten</p> <p>verfügbar ab: {{ force.commissioned }}</p>
<p>verfügbar bis: {{ force?.decommissioned ?? "---" }}</p>
</div> </div>
</RouterLink> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, type PropType } from "vue"; import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia"; import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import type { ForceViewModel } from "@/viewmodels/admin/configuration/force.models"; import type { ForceViewModel } from "@/viewmodels/admin/configuration/force.models";
import { useModalStore } from "../../../../stores/modal";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -27,5 +35,20 @@ export default defineComponent({
computed: { computed: {
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),
}, },
methods: {
...mapActions(useModalStore, ["openModal"]),
openUpdateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/configuration/force/UpdateForceModal.vue"))),
this.force.id
);
},
openDeleteModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/configuration/force/DeleteForceModal.vue"))),
this.force.id
);
},
},
}); });
</script> </script>

View file

@ -0,0 +1,145 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Kraft aktualisieren</p>
</div>
<br />
<Spinner v-if="loading == 'loading' && force == null" class="mx-auto" />
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
<form v-if="force" class="flex flex-col gap-4 py-2" @submit.prevent="triggerUpdate">
<div>
<label for="internalId">Interne Id (optional)</label>
<input type="text" id="internalId" v-model="force.internalId" />
</div>
<div>
<label for="firstname">Vorname</label>
<input type="text" id="firstname" required v-model="force.firstname" />
</div>
<div>
<label for="lastname">Nachname</label>
<input type="text" id="lastname" required v-model="force.lastname" />
</div>
<div>
<label for="nameaffix">Nameaffix (optional)</label>
<input type="text" id="nameaffix" v-model="force.nameaffix" />
</div>
<div>
<label for="commissioned">verfügbar ab (optional)</label>
<input type="date" id="commissioned" v-model="force.commissioned" />
</div>
<div>
<label for="decommissioned">verfügbar bis (optional)</label>
<input type="date" id="decommissioned" v-model="force.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 { useForceStore } from "@/stores/admin/configuration/forces";
import type {
CreateForceViewModel,
ForceViewModel,
UpdateForceViewModel,
} from "@/viewmodels/admin/configuration/force.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 },
force: null as null | ForceViewModel,
origin: null as null | ForceViewModel,
timeout: null as any,
};
},
computed: {
canSaveOrReset(): boolean {
return isEqual(this.origin, this.force);
},
},
mounted() {
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useForceStore, ["updateForce", "fetchForceById"]),
resetForm() {
this.force = cloneDeep(this.origin);
},
fetchItem() {
this.loading = "loading";
this.fetchForceById(this.data)
.then((res) => {
this.loading = "fetched";
this.origin = res.data;
this.force = cloneDeep(this.origin);
})
.catch((err) => {
this.loading = "failed";
});
},
triggerUpdate(e: any) {
let formData = e.target.elements;
let updateForce: UpdateForceViewModel = {
id: this.data,
internalId: formData.internalId.value,
firstname: formData.firstname.value,
lastname: formData.lastname.value,
nameaffix: formData.nameaffix.value,
commissioned: formData.commissioned.value,
decommissioned: formData.decommissioned.value,
};
this.status = "loading";
this.updateForce(updateForce)
.then(() => {
this.fetchItem();
this.status = { status: "success" };
})
.catch(() => {
this.status = { status: "failed" };
})
.finally(() => {
this.timeout = setTimeout(() => {
this.status = null;
}, 2000);
});
},
},
});
</script>

View file

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

View file

@ -0,0 +1,81 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
import type {
EquipmentViewModel,
CreateEquipmentViewModel,
UpdateEquipmentViewModel,
} from "../../../viewmodels/admin/configuration/equipment.models";
export const useEquipmentStore = defineStore("equipment", {
state: () => {
return {
equipments: [] as Array<EquipmentViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchEquipments(offset = 0, count = 25, search = "", clear = false) {
if (clear) this.equipments = [];
this.loading = "loading";
http
.get(`/admin/equipment?offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.then((result) => {
this.totalCount = result.data.total;
result.data.equipments
.filter((elem: EquipmentViewModel) => this.equipments.findIndex((m) => m.id == elem.id) == -1)
.map((elem: EquipmentViewModel, index: number): EquipmentViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: EquipmentViewModel & { tab_pos: number }) => {
this.equipments.push(elem);
});
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
async getAllEquipments(): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/equipment?noLimit=true`).then((res) => {
return { ...res, data: res.data.equipments };
});
},
async getEquipmentsByIds(ids: Array<string>): Promise<AxiosResponse<any, any>> {
return await http
.post(`/admin/equipment/ids`, {
ids,
})
.then((res) => {
return { ...res, data: res.data.equipments };
});
},
async searchEquipments(search: string): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/equipment?search=${search}&noLimit=true`).then((res) => {
return { ...res, data: res.data.equipments };
});
},
fetchEquipmentById(id: string) {
return http.get(`/admin/equipment/${id}`);
},
async createEquipment(equipment: CreateEquipmentViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/equipment`, equipment);
this.fetchEquipments();
return result;
},
async updateActiveEquipment(equipment: UpdateEquipmentViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/equipment/${equipment.id}`, equipment);
this.fetchEquipments();
return result;
},
async deleteEquipment(equipment: number): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/equipment/${equipment}`);
this.fetchEquipments();
return result;
},
},
});

View file

@ -63,20 +63,12 @@ export const useForceStore = defineStore("force", {
return http.get(`/admin/force/${id}`); return http.get(`/admin/force/${id}`);
}, },
async createForce(force: CreateForceViewModel): Promise<AxiosResponse<any, any>> { async createForce(force: CreateForceViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/force`, { const result = await http.post(`/admin/force`, force);
firstname: force.firstname,
lastname: force.lastname,
nameaffix: force.nameaffix,
});
this.fetchForces(); this.fetchForces();
return result; return result;
}, },
async updateActiveForce(force: UpdateForceViewModel): Promise<AxiosResponse<any, any>> { async updateForce(force: UpdateForceViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/force/${force.id}`, { const result = await http.patch(`/admin/force/${force.id}`, force);
firstname: force.firstname,
lastname: force.lastname,
nameaffix: force.nameaffix,
});
this.fetchForces(); this.fetchForces();
return result; return result;
}, },

View file

@ -0,0 +1,81 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
import type {
VehicleViewModel,
CreateVehicleViewModel,
UpdateVehicleViewModel,
} from "../../../viewmodels/admin/configuration/vehicle.models";
export const useVehicleStore = defineStore("vehicle", {
state: () => {
return {
vehicles: [] as Array<VehicleViewModel & { tab_pos: number }>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchVehicles(offset = 0, count = 25, search = "", clear = false) {
if (clear) this.vehicles = [];
this.loading = "loading";
http
.get(`/admin/vehicle?offset=${offset}&count=${count}${search != "" ? "&search=" + search : ""}`)
.then((result) => {
this.totalCount = result.data.total;
result.data.vehicles
.filter((elem: VehicleViewModel) => this.vehicles.findIndex((m) => m.id == elem.id) == -1)
.map((elem: VehicleViewModel, index: number): VehicleViewModel & { tab_pos: number } => {
return {
...elem,
tab_pos: index + offset,
};
})
.forEach((elem: VehicleViewModel & { tab_pos: number }) => {
this.vehicles.push(elem);
});
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
async getAllVehicles(): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/vehicle?noLimit=true`).then((res) => {
return { ...res, data: res.data.vehicles };
});
},
async getVehiclesByIds(ids: Array<string>): Promise<AxiosResponse<any, any>> {
return await http
.post(`/admin/vehicle/ids`, {
ids,
})
.then((res) => {
return { ...res, data: res.data.vehicles };
});
},
async searchVehicles(search: string): Promise<AxiosResponse<any, any>> {
return await http.get(`/admin/vehicle?search=${search}&noLimit=true`).then((res) => {
return { ...res, data: res.data.vehicles };
});
},
fetchVehicleById(id: string) {
return http.get(`/admin/vehicle/${id}`);
},
async createVehicle(vehicle: CreateVehicleViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/vehicle`, vehicle);
this.fetchVehicles();
return result;
},
async updateActiveVehicle(vehicle: UpdateVehicleViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/vehicle/${vehicle.id}`, vehicle);
this.fetchVehicles();
return result;
},
async deleteVehicle(vehicle: number): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/vehicle/${vehicle}`);
this.fetchVehicles();
return result;
},
},
});

View file

@ -94,7 +94,11 @@ export const useNavigationStore = defineStore("navigation", {
}, },
configuration: { configuration: {
mainTitle: "Konfiguration", mainTitle: "Konfiguration",
main: [...(abilityStore.can("read", "configuration", "force") ? [{ key: "force", title: "Kräfte" }] : [])], main: [
...(abilityStore.can("read", "configuration", "force") ? [{ key: "force", title: "Kräfte" }] : []),
...(abilityStore.can("read", "configuration", "equipment") ? [{ key: "equipment", title: "Geräte" }] : []),
...(abilityStore.can("read", "configuration", "vehicle") ? [{ key: "vehicle", title: "Fahrzeuge" }] : []),
],
}, },
management: { management: {
mainTitle: "Verwaltung", mainTitle: "Verwaltung",

View file

@ -1,6 +1,6 @@
export type PermissionSection = "operation" | "configuration" | "management"; export type PermissionSection = "operation" | "configuration" | "management";
export type PermissionModule = "mission" | "force" | "user" | "role" | "backup"; export type PermissionModule = "mission" | "force" | "vehicle" | "equipment" | "user" | "role" | "backup";
export type PermissionType = "read" | "create" | "update" | "delete"; export type PermissionType = "read" | "create" | "update" | "delete";
@ -24,10 +24,18 @@ export type SectionsAndModulesObject = {
}; };
export const permissionSections: Array<PermissionSection> = ["operation", "configuration", "management"]; export const permissionSections: Array<PermissionSection> = ["operation", "configuration", "management"];
export const permissionModules: Array<PermissionModule> = ["mission", "force", "user", "role", "backup"]; export const permissionModules: Array<PermissionModule> = [
"mission",
"force",
"vehicle",
"equipment",
"user",
"role",
"backup",
];
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"]; export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
export const sectionsAndModules: SectionsAndModulesObject = { export const sectionsAndModules: SectionsAndModulesObject = {
operation: ["mission"], operation: ["mission"],
configuration: ["force"], configuration: ["force", "vehicle", "equipment"],
management: ["user", "role", "backup"], management: ["user", "role", "backup"],
}; };

View file

@ -0,0 +1,25 @@
export interface EquipmentViewModel {
id: string;
code?: string;
type?: string;
name: string;
commissioned: Date;
decommissioned?: Date;
}
export interface CreateEquipmentViewModel {
code?: string;
type?: string;
name: string;
commissioned: Date;
decommissioned?: Date;
}
export interface UpdateEquipmentViewModel {
id: string;
code?: string;
type?: string;
name: string;
commissioned: Date;
decommissioned?: Date;
}

View file

@ -1,19 +1,28 @@
export interface ForceViewModel { export interface ForceViewModel {
id: string; id: string;
internalId?: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
nameaffix: string; nameaffix: string;
commissioned: Date;
decommissioned?: Date;
} }
export interface CreateForceViewModel { export interface CreateForceViewModel {
internalId?: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
nameaffix: string; nameaffix: string;
commissioned: Date;
decommissioned?: Date;
} }
export interface UpdateForceViewModel { export interface UpdateForceViewModel {
id: string; id: string;
internalId?: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
nameaffix: string; nameaffix: string;
commissioned: Date;
decommissioned?: Date;
} }

View file

@ -0,0 +1,25 @@
export interface VehicleViewModel {
id: string;
code?: string;
type?: string;
name: string;
commissioned: Date;
decommissioned?: Date;
}
export interface CreateVehicleViewModel {
code?: string;
type?: string;
name: string;
commissioned: Date;
decommissioned?: Date;
}
export interface UpdateVehicleViewModel {
id: string;
code?: string;
type?: string;
name: string;
commissioned: Date;
decommissioned?: Date;
}

View file

@ -22,7 +22,7 @@
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<button v-if="can('create', 'operation', 'force')" primary class="!w-fit" @click="openCreateModal"> <button v-if="can('create', 'operation', 'force')" primary class="!w-fit" @click="openCreateModal">
Mitglied erstellen Kraft erstellen
</button> </button>
</div> </div>
</div> </div>
@ -35,7 +35,7 @@ import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapActions, mapState } from "pinia"; import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue"; import MainTemplate from "@/templates/Main.vue";
import { useForceStore } from "@/stores/admin/configuration/forces"; import { useForceStore } from "@/stores/admin/configuration/forces";
import ForceListItem from "@/components/admin/club/force/ForceListItem.vue"; import ForceListItem from "@/components/admin/configuration/force/ForceListItem.vue";
import { useModalStore } from "@/stores/modal"; import { useModalStore } from "@/stores/modal";
import Pagination from "@/components/Pagination.vue"; import Pagination from "@/components/Pagination.vue";
import type { ForceViewModel } from "@/viewmodels/admin/configuration/force.models"; import type { ForceViewModel } from "@/viewmodels/admin/configuration/force.models";
@ -62,7 +62,9 @@ export default defineComponent({
...mapActions(useForceStore, ["fetchForces"]), ...mapActions(useForceStore, ["fetchForces"]),
...mapActions(useModalStore, ["openModal"]), ...mapActions(useModalStore, ["openModal"]),
openCreateModal() { openCreateModal() {
this.openModal(markRaw(defineAsyncComponent(() => import("@/components/admin/club/force/CreateForceModal.vue")))); this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/configuration/force/CreateForceModal.vue")))
);
}, },
}, },
}); });