calendartypes

This commit is contained in:
Julian Krauser 2024-10-27 15:51:15 +01:00
parent 5c2865a9e5
commit 762a9f2b8e
12 changed files with 523 additions and 11 deletions

View file

@ -1,7 +1,7 @@
<template> <template>
<footer <footer
v-if="authCheck && routeName.includes('admin')" v-if="authCheck && routeName.includes('admin')"
class="md:hidden flex flex-row h-16 justify-center md:justify-normal p-1 bg-white" class="md:hidden flex flex-row h-16 min-h-16 justify-center md:justify-normal p-1 bg-white"
> >
<div class="w-full flex flex-row gap-2 h-full align-middle"> <div class="w-full flex flex-row gap-2 h-full align-middle">
<TopLevelLink v-for="item in topLevel" :key="item.key" :link="item" :disableSubLink="true" /> <TopLevelLink v-for="item in topLevel" :key="item.key" :link="item" :disableSubLink="true" />

View file

@ -0,0 +1,54 @@
<template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md overflow-hidden">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<div class="flex flex-row items-center gap-2">
<div class="rounded-md h-5 w-5" :style="'background-color:' + calendarType.color">
<EyeIcon v-if="calendarType.nscdr" class="w-5 h-5" />
</div>
<p>{{ calendarType.type }}</p>
</div>
<div class="flex flex-row">
<RouterLink
v-if="can('update', 'settings', 'calendar_type')"
:to="{ name: 'admin-settings-calendar_type-edit', params: { id: calendarType.id } }"
>
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</RouterLink>
<div v-if="can('delete', 'settings', 'calendar_type')" @click="openDeleteModal">
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, defineAsyncComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { PencilIcon, TrashIcon, EyeIcon } from "@heroicons/vue/24/outline";
import { useAbilityStore } from "@/stores/ability";
import { useModalStore } from "@/stores/modal";
import type { CalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
calendarType: { type: Object as PropType<CalendarTypeViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openDeleteModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/settings/calendarType/DeleteCalendarTypeModal.vue"))
),
this.calendarType.id
);
},
},
});
</script>

View file

@ -0,0 +1,90 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Termintyp erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<label for="type">Bezeichnung</label>
<input type="text" id="type" required />
</div>
<div class="flex flex-row items-center gap-2">
<input type="color" id="color" required class="!px-1 !py-0 !w-10" />
<label for="color">Farbe</label>
</div>
<div class="flex flex-row items-center gap-2">
<input type="checkbox" id="nscdr" />
<label for="nscdr">Standard Kalender Auslieferung (optional)</label>
</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 != null && status?.status != 'failed'">
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 { useCalendarTypeStore } from "@/stores/admin/calendarType";
import type { CreateCalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import type { CalendarFieldType } from "@/types/fieldTypes";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedFields: [] as Array<CalendarFieldType>,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCalendarTypeStore, ["createCalendarType"]),
triggerCreate(e: any) {
let formData = e.target.elements;
let createCalendarType: CreateCalendarTypeViewModel = {
type: formData.type.value,
color: formData.color.value,
nscdr: formData.nscdr.checked,
};
this.createCalendarType(createCalendarType)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,72 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Termintyp {{ communicationType?.type }} löschen?</p>
</div>
<br />
<div class="flex flex-row gap-2">
<button primary :disabled="status == 'loading' || status?.status == 'success'" @click="triggerDelete">
unwiederuflich 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 != null">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 { useCommunicationTypeStore } from "@/stores/admin/communicationType";
</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) {}
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useCommunicationTypeStore, ["communicationTypes"]),
communicationType() {
return this.communicationTypes.find((r) => r.id == this.data);
},
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCommunicationTypeStore, ["deleteCommunicationType"]),
triggerDelete() {
this.deleteCommunicationType(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -213,13 +213,6 @@ const router = createRouter({
}, },
], ],
}, },
{
path: "executive-position",
name: "admin-settings-executive_position",
component: () => import("@/views/admin/settings/ExecutivePosition.vue"),
meta: { type: "read", section: "settings", module: "executive_position" },
beforeEnter: [abilityAndNavUpdate],
},
{ {
path: "executive-position", path: "executive-position",
name: "admin-settings-executive_position-route", name: "admin-settings-executive_position-route",
@ -231,6 +224,8 @@ const router = createRouter({
path: "", path: "",
name: "admin-settings-executive_position", name: "admin-settings-executive_position",
component: () => import("@/views/admin/settings/ExecutivePosition.vue"), component: () => import("@/views/admin/settings/ExecutivePosition.vue"),
meta: { type: "read", section: "settings", module: "executive_position" },
beforeEnter: [abilityAndNavUpdate],
}, },
{ {
path: ":id/edit", path: ":id/edit",
@ -286,6 +281,28 @@ const router = createRouter({
}, },
], ],
}, },
{
path: "calendar-type",
name: "admin-settings-calendar_type-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "settings", module: "calendar_type" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-settings-calendar_type",
component: () => import("@/views/admin/settings/CalendarType.vue"),
},
{
path: ":id/edit",
name: "admin-settings-calendar_type-edit",
component: () => import("@/views/admin/settings/CalendarTypeEdit.vue"),
meta: { type: "update", section: "settings", module: "calendar_type" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
],
},
], ],
}, },
{ {

View file

@ -0,0 +1,58 @@
import { defineStore } from "pinia";
import type {
CreateCalendarTypeViewModel,
UpdateCalendarTypeViewModel,
CalendarTypeViewModel,
} from "@/viewmodels/admin/calendarType.models";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
export const useCalendarTypeStore = defineStore("calendarType", {
state: () => {
return {
calendarTypes: [] as Array<CalendarTypeViewModel>,
loading: "loading" as "loading" | "fetched" | "failed",
loadingFields: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchCalendarTypes() {
this.loading = "loading";
http
.get("/admin/calendar/types")
.then((result) => {
this.calendarTypes = result.data;
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
fetchCalendarTypeById(id: number): Promise<AxiosResponse<any, any>> {
return http.get(`/admin/calendar/type/${id}`);
},
async createCalendarType(calendarType: CreateCalendarTypeViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/calendar/type`, {
type: calendarType.type,
nscdr: calendarType.nscdr,
color: calendarType.color,
});
this.fetchCalendarTypes();
return result;
},
async updateActiveCalendarType(calendarType: UpdateCalendarTypeViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/calendar/type/${calendarType.id}`, {
type: calendarType.type,
nscdr: calendarType.nscdr,
color: calendarType.color,
});
this.fetchCalendarTypes();
return result;
},
async deleteCalendarType(calendarType: number): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/calendar/type/${calendarType}`);
this.fetchCalendarTypes();
return result;
},
},
});

View file

@ -87,9 +87,9 @@ export const useNavigationStore = defineStore("navigation", {
mainTitle: "Verein", mainTitle: "Verein",
main: [ main: [
...(abilityStore.can("read", "club", "member") ? [{ key: "member", title: "Mitglieder" }] : []), ...(abilityStore.can("read", "club", "member") ? [{ key: "member", title: "Mitglieder" }] : []),
...(abilityStore.can("read", "club", "calendar") ? [{ key: "calendar", title: "Termine" }] : []), ...(abilityStore.can("read", "club", "calendar") ? [{ key: "calendar", title: "Kalender" }] : []),
...(abilityStore.can("read", "club", "newsletter") ? [{ key: "newsletter", title: "Newsletter" }] : []),
...(abilityStore.can("read", "club", "protocoll") ? [{ key: "protocol", title: "Protokolle" }] : []), ...(abilityStore.can("read", "club", "protocoll") ? [{ key: "protocol", title: "Protokolle" }] : []),
...(abilityStore.can("read", "club", "newsletter") ? [{ key: "newsletter", title: "Newsletter" }] : []),
], ],
}, },
settings: { settings: {
@ -108,6 +108,9 @@ export const useNavigationStore = defineStore("navigation", {
...(abilityStore.can("read", "settings", "membership_status") ...(abilityStore.can("read", "settings", "membership_status")
? [{ key: "membership_status", title: "Mitgliedsstatus" }] ? [{ key: "membership_status", title: "Mitgliedsstatus" }]
: []), : []),
...(abilityStore.can("read", "settings", "calendar_type")
? [{ key: "calendar_type", title: "Terminarten" }]
: []),
], ],
}, },
user: { user: {

View file

@ -10,6 +10,7 @@ export type PermissionModule =
| "executive_position" | "executive_position"
| "communication" | "communication"
| "membership_status" | "membership_status"
| "calendar_type"
| "user" | "user"
| "role"; | "role";
@ -45,12 +46,13 @@ export const permissionModules: Array<PermissionModule> = [
"executive_position", "executive_position",
"communication", "communication",
"membership_status", "membership_status",
"calendar_type",
"user", "user",
"role", "role",
]; ];
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 = {
club: ["member", "calendar", "newsletter", "protocoll"], club: ["member", "calendar", "newsletter", "protocoll"],
settings: ["qualification", "award", "executive_position", "communication", "membership_status"], settings: ["qualification", "award", "executive_position", "communication", "membership_status", "calendar_type"],
user: ["user", "role"], user: ["user", "role"],
}; };

View file

@ -0,0 +1,11 @@
import type { CalendarTypeViewModel } from "./calendarType.models";
export interface CalendarViewModel {
id: number;
date: Date;
starttime: Date;
endtime: Date;
title: string;
content: string;
type: CalendarTypeViewModel;
}

View file

@ -0,0 +1,19 @@
export interface CalendarTypeViewModel {
id: number;
type: string;
nscdr: boolean;
color: string;
}
export interface CreateCalendarTypeViewModel {
type: string;
nscdr: boolean;
color: string;
}
export interface UpdateCalendarTypeViewModel {
id: number;
type: string;
nscdr: boolean;
color: string;
}

View file

@ -0,0 +1,54 @@
<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">Termintyp</h1>
</div>
</template>
<template #diffMain>
<div class="flex flex-col gap-4 grow pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
<CalendarTypeListItem
v-for="calendarType in calendarTypes"
:key="calendarType.id"
:calendarType="calendarType"
/>
</div>
<div class="flex flex-row gap-4">
<button primary class="!w-fit" @click="openCreateModal">Termintyp erstellen</button>
</div>
</div>
</template>
</MainTemplate>
</template>
<script setup lang="ts">
import { defineComponent, defineAsyncComponent, markRaw } from "vue";
import { mapState, mapActions } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useCalendarTypeStore } from "@/stores/admin/calendarType";
import CalendarTypeListItem from "@/components/admin/settings/calendarType/CalendarTypeListItem.vue";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
computed: {
...mapState(useCalendarTypeStore, ["calendarTypes"]),
},
mounted() {
this.fetchCalendarTypes();
},
methods: {
...mapActions(useCalendarTypeStore, ["fetchCalendarTypes"]),
...mapActions(useModalStore, ["openModal"]),
openCreateModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/settings/calendarType/CreateCalendarTypeModal.vue"))
)
);
},
},
});
</script>

View file

@ -0,0 +1,132 @@
<template>
<MainTemplate>
<template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template>
<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">Termintyp {{ origin?.type }} - Daten bearbeiten</h1>
</div>
</template>
<template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<form
v-else-if="calendarType != null"
class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto"
@submit.prevent="triggerUpdate"
>
<div>
<label for="type">Bezeichnung</label>
<input type="text" id="type" required v-model="calendarType.type" />
</div>
<div class="flex flex-row items-center gap-2">
<input type="color" id="color" required v-model="calendarType.color" class="!px-1 !py-0 !w-10" />
<label for="color">Farbe</label>
</div>
<div class="flex flex-row items-center gap-2">
<input type="checkbox" id="nscdr" v-model="calendarType.nscdr" />
<label for="nscdr">Standard Kalender Auslieferung (optional)</label>
</div>
<div class="flex flex-row justify-end gap-2">
<button primary-outline type="reset" class="!w-fit" :disabled="canSaveOrReset" @click="resetForm">
verwerfen
</button>
<button primary type="submit" class="!w-fit" :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>
</template>
</MainTemplate>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useCalendarTypeStore } from "@/stores/admin/calendarType";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { RouterLink } from "vue-router";
import type { CalendarTypeViewModel, UpdateCalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual";
</script>
<script lang="ts">
export default defineComponent({
props: {
id: String,
},
data() {
return {
loading: "loading" as "loading" | "fetched" | "failed",
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
origin: null as null | CalendarTypeViewModel,
calendarType: null as null | CalendarTypeViewModel,
timeout: null as any,
};
},
computed: {
canSaveOrReset(): boolean {
return isEqual(this.origin, this.calendarType);
},
},
mounted() {
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useCalendarTypeStore, ["fetchCalendarTypeById", "updateActiveCalendarType"]),
resetForm() {
this.calendarType = cloneDeep(this.origin);
},
fetchItem() {
this.fetchCalendarTypeById(parseInt(this.id ?? ""))
.then((result) => {
this.calendarType = result.data;
this.origin = cloneDeep(result.data);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
triggerUpdate(e: any) {
if (this.calendarType == null) return;
let formData = e.target.elements;
let updateCalendarType: UpdateCalendarTypeViewModel = {
id: this.calendarType.id,
type: formData.type.value,
color: formData.color.value,
nscdr: formData.nscdr.checked,
};
this.status = "loading";
this.updateActiveCalendarType(updateCalendarType)
.then(() => {
this.fetchItem();
this.status = { status: "success" };
})
.catch((err) => {
this.status = { status: "failed" };
})
.finally(() => {
this.timeout = setTimeout(() => {
this.status = null;
}, 2000);
});
},
},
});
</script>