mission structure

This commit is contained in:
Julian Krauser 2025-02-20 12:01:12 +01:00
parent f3b5112bc6
commit 83988ad68a
10 changed files with 258 additions and 5 deletions

View file

@ -0,0 +1,28 @@
<template>
<RouterLink
:to="{ name: 'admin-operation-mission-form', params: { id: mission.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>{{ mission.title || "---" }} {{ mission.keyword ? `(${mission.keyword})` : "" }}</p>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import type { MissionShortViewModel } from "@/viewmodels/admin/operation/mission.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
mission: { type: Object as PropType<MissionShortViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
});
</script>

View file

@ -7,7 +7,10 @@
> >
<slot name="sidebar"></slot> <slot name="sidebar"></slot>
</div> </div>
<div class="max-w-full grow flex-col gap-2" :class="defaultRoute && defaultSidebar ? 'hidden md:flex' : 'flex'"> <div
class="max-w-full grow flex-col gap-2"
:class="defaultRoute && defaultSidebar && showSidebar ? 'hidden md:flex' : 'flex'"
>
<slot name="main"></slot> <slot name="main"></slot>
</div> </div>
</div> </div>

View file

@ -96,9 +96,25 @@ const router = createRouter({
{ {
path: "mission", path: "mission",
name: "admin-operation-mission", name: "admin-operation-mission",
component: () => import("@/views/admin/ViewSelect.vue"), component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "operation", module: "mission" }, meta: { type: "read", section: "operation", module: "mission" },
beforeEnter: [abilityAndNavUpdate], beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-operation-mission-default",
component: () => import("@/views/admin/operation/mission/MissionList.vue"),
meta: { type: "read", section: "operation" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: ":id",
name: "admin-operation-mission-form",
component: () => import("@/views/admin/operation/mission/MissionOverview.vue"),
meta: { type: "read", section: "operation", module: "force" },
beforeEnter: [abilityAndNavUpdate],
},
],
}, },
], ],
}, },

View file

@ -0,0 +1,42 @@
import { defineStore } from "pinia";
import type { MissionShortViewModel } from "@/viewmodels/admin/operation/mission.models";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
export const useMissionStore = defineStore("mission", {
state: () => {
return {
missions: [] as Array<MissionShortViewModel>,
totalCount: 0 as number,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchMissions(offset = 0, count = 25, clear = false) {
if (clear) this.missions = [];
this.loading = "loading";
http
.get(`/admin/mission?offset=${offset}&count=${count}`)
.then((result) => {
this.totalCount = result.data.total;
result.data.missions
.filter((elem: MissionShortViewModel) => this.missions.findIndex((m) => m.id == elem.id) == -1)
.forEach((elem: MissionShortViewModel) => {
this.missions.push(elem);
});
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
prependMission(mission: MissionShortViewModel) {
this.missions.unshift(mission);
},
async createMission(): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/mission`);
this.fetchMissions(0, 25, true);
return result;
},
},
});

View file

@ -0,0 +1,5 @@
export interface MissionShortViewModel {
id: string;
title: string;
keyword?: string;
}

View file

@ -21,7 +21,7 @@
</Pagination> </Pagination>
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<button v-if="can('create', 'operation', 'equipment')" primary class="!w-fit" @click="openCreateModal"> <button v-if="can('create', 'configuration', 'equipment')" primary class="!w-fit" @click="openCreateModal">
Ausrüstung erstellen Ausrüstung erstellen
</button> </button>
</div> </div>

View file

@ -21,7 +21,7 @@
</Pagination> </Pagination>
<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', 'configuration', 'force')" primary class="!w-fit" @click="openCreateModal">
Kraft erstellen Kraft erstellen
</button> </button>
</div> </div>

View file

@ -21,7 +21,7 @@
</Pagination> </Pagination>
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<button v-if="can('create', 'operation', 'vehicle')" primary class="!w-fit" @click="openCreateModal"> <button v-if="can('create', 'configuration', 'vehicle')" primary class="!w-fit" @click="openCreateModal">
Fahrzeug erstellen Fahrzeug erstellen
</button> </button>
</div> </div>

View file

@ -0,0 +1,83 @@
<template>
<MainTemplate :show-back="false">
<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">Einsätze</h1>
<ArrowPathIcon
v-if="can('delete', 'operation', 'mission')"
class="w-5 h-5 cursor-pointer"
:class="loading == 'loading' ? 'animate-spin' : 'animate-[spin_1s_linear_1]'"
@click="fetchMissions()"
/>
</div>
</template>
<template #diffMain>
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll" @scrollend="reachedEnd">
<MissionListItem v-for="mission in missions" :key="mission.id" :mission="mission" />
<Spinner v-if="loading == 'loading'" class="mt-auto mx-auto" />
</div>
<div class="flex flex-row gap-4">
<button v-if="can('create', 'operation', 'mission')" primary class="!w-fit" @click="createNewMission">
Einsatz anlegen
</button>
</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 { useAbilityStore } from "@/stores/ability";
import MissionListItem from "@/components/admin/operation/mission/MissionListItem.vue";
import { useMissionStore } from "@/stores/admin/operation/mission";
import { ArrowPathIcon } from "@heroicons/vue/24/outline";
import Spinner from "@/components/Spinner.vue";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
hasReachedEnd: false as boolean,
};
},
watch: {
missions(before, after) {
if (before.length != after.length) {
this.hasReachedEnd = false;
}
},
},
computed: {
...mapState(useMissionStore, ["missions", "loading"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchMissions();
},
methods: {
...mapActions(useMissionStore, ["fetchMissions", "createMission"]),
reachedEnd() {
this.hasReachedEnd = true;
this.fetchMissions(this.missions.length);
},
createNewMission() {
this.createMission()
.then((res) => {
this.$router.push({
name: "admin-operation-mission-form",
params: {
id: res.data,
},
});
})
.catch(() => {});
},
},
});
</script>

View file

@ -0,0 +1,76 @@
<template>
<MainTemplate :showBack="false">
<template #headerInsert>
<RouterLink :to="{ name: 'admin-operation-mission-default' }" class="text-primary">zurück zur Liste</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">
<RouterLink
v-for="tab in tabs"
:key="tab.hash"
:to="{ hash: tab.hash }"
replace
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-none',
routeHash == tab.hash
? 'bg-red-200 shadow border-b-2 border-primary rounded-b-none'
: ' hover:bg-red-200',
]"
>
{{ tab.title }}
</p>
</RouterLink>
</div>
<div v-if="routeHash == '#edit'">hi</div>
</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 { useAbilityStore } from "@/stores/ability";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
tabs: [
{ hash: "#edit", title: "Einträge" },
{ hash: "#presence", title: "Anwesenheit" },
{ hash: "#protocol", title: "Protokoll" },
],
};
},
watch: {
"$route.hash"() {
this.manageHash();
},
},
computed: {
...mapState(useAbilityStore, ["can"]),
routeHash() {
return this.$route.hash;
},
},
mounted() {
this.manageHash();
},
methods: {
manageHash() {
if (!this.$route.hash || !this.tabs.map((t) => t.hash).includes(this.$route.hash)) {
this.$router.replace({ hash: this.tabs[0].hash });
}
},
},
});
</script>