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>
<br />
<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>
<label for="firstname">Vorname</label>
<input type="text" id="firstname" required />
@ -17,6 +21,14 @@
<label for="nameaffix">Nameaffix (optional)</label>
<input type="text" id="nameaffix" />
</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" />
@ -67,9 +79,12 @@ export default defineComponent({
triggerCreate(e: any) {
let formData = e.target.elements;
let createForce: CreateForceViewModel = {
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.createForce(createForce)

View file

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

View file

@ -1,22 +1,30 @@
<template>
<RouterLink
: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="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>{{ 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 class="p-2">
<p>Daten</p>
<p>verfügbar ab: {{ force.commissioned }}</p>
<p>verfügbar bis: {{ force?.decommissioned ?? "---" }}</p>
</div>
</RouterLink>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
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";
</script>
<script lang="ts">
@ -27,5 +35,20 @@ export default defineComponent({
computed: {
...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>

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",
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" },
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}`);
},
async createForce(force: CreateForceViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/force`, {
firstname: force.firstname,
lastname: force.lastname,
nameaffix: force.nameaffix,
});
const result = await http.post(`/admin/force`, force);
this.fetchForces();
return result;
},
async updateActiveForce(force: UpdateForceViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/force/${force.id}`, {
firstname: force.firstname,
lastname: force.lastname,
nameaffix: force.nameaffix,
});
async updateForce(force: UpdateForceViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/force/${force.id}`, force);
this.fetchForces();
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: {
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: {
mainTitle: "Verwaltung",

View file

@ -1,6 +1,6 @@
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";
@ -24,10 +24,18 @@ export type SectionsAndModulesObject = {
};
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 sectionsAndModules: SectionsAndModulesObject = {
operation: ["mission"],
configuration: ["force"],
configuration: ["force", "vehicle", "equipment"],
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 {
id: string;
internalId?: string;
firstname: string;
lastname: string;
nameaffix: string;
commissioned: Date;
decommissioned?: Date;
}
export interface CreateForceViewModel {
internalId?: string;
firstname: string;
lastname: string;
nameaffix: string;
commissioned: Date;
decommissioned?: Date;
}
export interface UpdateForceViewModel {
id: string;
internalId?: string;
firstname: string;
lastname: 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">
<button v-if="can('create', 'operation', 'force')" primary class="!w-fit" @click="openCreateModal">
Mitglied erstellen
Kraft erstellen
</button>
</div>
</div>
@ -35,7 +35,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 ForceListItem from "@/components/admin/club/force/ForceListItem.vue";
import ForceListItem from "@/components/admin/configuration/force/ForceListItem.vue";
import { useModalStore } from "@/stores/modal";
import Pagination from "@/components/Pagination.vue";
import type { ForceViewModel } from "@/viewmodels/admin/configuration/force.models";
@ -62,7 +62,9 @@ export default defineComponent({
...mapActions(useForceStore, ["fetchForces"]),
...mapActions(useModalStore, ["openModal"]),
openCreateModal() {
this.openModal(markRaw(defineAsyncComponent(() => import("@/components/admin/club/force/CreateForceModal.vue"))));
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/configuration/force/CreateForceModal.vue")))
);
},
},
});