Merge pull request 'patches v1.3.3' (#75) from develop into main

Reviewed-on: #75
This commit is contained in:
Julian Krauser 2025-03-21 09:48:01 +00:00
commit 8ad91a3097
19 changed files with 357 additions and 40 deletions

View file

@ -9,7 +9,6 @@
<a ref="download" button primary class="!w-fit">download</a> <a ref="download" button primary class="!w-fit">download</a>
<button primary-outline class="!w-fit" @click="closeModal">schließen</button> <button primary-outline class="!w-fit" @click="closeModal">schließen</button>
</div> </div>
</div> </div>
</template> </template>
@ -31,25 +30,29 @@ export default defineComponent({
}, },
computed: { computed: {
...mapState(useModalStore, ["data"]), ...mapState(useModalStore, ["data"]),
...mapState(useMemberStore, ["activeMemberObj"]),
}, },
mounted() { mounted() {
this.fetchItem(); this.fetchItem();
}, },
methods: { methods: {
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberStore, ["printMemberList"]), ...mapActions(useMemberStore, ["printMemberByActiveId"]),
fetchItem() { fetchItem() {
this.status = "loading"; this.status = "loading";
this.printMemberList() this.printMemberByActiveId()
.then((response) => { .then((response) => {
this.status = { status: "success" }; this.status = { status: "success" };
const blob = new Blob([response.data], { type: "application/pdf" }); const blob = new Blob([response.data], { type: "application/pdf" });
(this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob); (this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob);
const fileURL = window.URL.createObjectURL(new Blob([response.data])); const fileURL = window.URL.createObjectURL(new Blob([response.data]));
const fileLink = (this.$refs.download as HTMLAnchorElement) const fileLink = this.$refs.download as HTMLAnchorElement;
fileLink.href = fileURL; fileLink.href = fileURL;
fileLink.setAttribute("download", "Mitgliederliste.pdf"); fileLink.setAttribute(
"download",
`Mitglied-Ausdruck ${this.activeMemberObj?.firstname}_${this.activeMemberObj?.lastname}.pdf`
);
}) })
.catch(() => { .catch(() => {
this.status = { status: "failed" }; this.status = { status: "failed" };

View file

@ -42,10 +42,11 @@
<div class="flex flex-row gap-2 items-center"> <div class="flex flex-row gap-2 items-center">
<p class="whitespace-nowrap">Höhe [mm]:</p> <p class="whitespace-nowrap">Höhe [mm]:</p>
<input <input
ref="headerHeight"
id="headerHeight" id="headerHeight"
type="number" type="number"
:min="15" :min="15"
v-model="templateUsage.headerHeight" :value="templateUsage.headerHeight"
class="!w-24" class="!w-24"
placeholder="15" placeholder="15"
/> />
@ -71,10 +72,11 @@
<div class="flex flex-row gap-2 items-center"> <div class="flex flex-row gap-2 items-center">
<p class="whitespace-nowrap">Höhe [mm]:</p> <p class="whitespace-nowrap">Höhe [mm]:</p>
<input <input
ref="footerHeight"
id="footerHeight" id="footerHeight"
type="number" type="number"
:min="15" :min="15"
v-model="templateUsage.footerHeight" :value="templateUsage.footerHeight"
class="!w-24" class="!w-24"
placeholder="15" placeholder="15"
/> />
@ -134,8 +136,8 @@ export default defineComponent({
const headerId = fromData.header.value === "def" ? null : fromData.header.value; const headerId = fromData.header.value === "def" ? null : fromData.header.value;
const bodyId = fromData.body.value === "def" ? null : fromData.body.value; const bodyId = fromData.body.value === "def" ? null : fromData.body.value;
const footerId = fromData.footer.value === "def" ? null : fromData.footer.value; const footerId = fromData.footer.value === "def" ? null : fromData.footer.value;
const headerHeight = fromData.footer.value === "" ? null : parseInt(fromData.headerHeight.value); const headerHeight = fromData.headerHeight.value === "" ? null : parseInt(fromData.headerHeight.value);
const footerHeight = fromData.footer.value === "" ? null : parseInt(fromData.footerHeight.value); const footerHeight = fromData.footerHeight.value === "" ? null : parseInt(fromData.footerHeight.value);
this.status = "loading"; this.status = "loading";
this.updateTemplateUsage({ this.updateTemplateUsage({
@ -160,6 +162,8 @@ export default defineComponent({
(this.$refs.header as HTMLSelectElement).value = String(this.templateUsage.header?.id ?? "def"); (this.$refs.header as HTMLSelectElement).value = String(this.templateUsage.header?.id ?? "def");
(this.$refs.body as HTMLSelectElement).value = String(this.templateUsage.body?.id ?? "def"); (this.$refs.body as HTMLSelectElement).value = String(this.templateUsage.body?.id ?? "def");
(this.$refs.footer as HTMLSelectElement).value = String(this.templateUsage.footer?.id ?? "def"); (this.$refs.footer as HTMLSelectElement).value = String(this.templateUsage.footer?.id ?? "def");
(this.$refs.headerHeight as HTMLInputElement).value = (this.templateUsage.headerHeight ?? "").toString();
(this.$refs.footerHeight as HTMLInputElement).value = (this.templateUsage.footerHeight ?? "").toString();
}, },
}, },
}); });

View file

@ -295,6 +295,13 @@ const router = createRouter({
meta: { type: "read", section: "club", module: "query" }, meta: { type: "read", section: "club", module: "query" },
beforeEnter: [abilityAndNavUpdate], beforeEnter: [abilityAndNavUpdate],
}, },
{
path: "listprint",
name: "admin-club-listprint",
component: () => import("@/views/admin/club/listprint/ListPrint.vue"),
meta: { type: "read", section: "club", module: "listprint" },
beforeEnter: [abilityAndNavUpdate],
},
], ],
}, },
{ {

View file

@ -0,0 +1,40 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
export const useListPrintStore = defineStore("listprint", {
actions: {
async printList({
title,
queryStore,
headerId,
bodyId,
footerId,
headerHeight,
footerHeight,
}: {
title: string;
queryStore: string;
headerId?: string;
bodyId?: string;
footerId?: string;
headerHeight?: number;
footerHeight?: number;
}) {
return http.post(
`/admin/listprint`,
{
title,
queryStore,
headerId,
bodyId,
footerId,
headerHeight,
footerHeight,
},
{
responseType: "blob",
}
);
},
},
});

View file

@ -87,6 +87,11 @@ export const useMemberStore = defineStore("member", {
}) })
.catch((err) => {}); .catch((err) => {});
}, },
async printMemberByActiveId() {
return http.get(`/admin/member/${this.activeMember}/print`, {
responseType: "blob",
});
},
fetchMemberStatisticsById(id: string) { fetchMemberStatisticsById(id: string) {
return http.get(`/admin/member/${id}/statistics`); return http.get(`/admin/member/${id}/statistics`);
}, },
@ -119,10 +124,5 @@ export const useMemberStore = defineStore("member", {
this.fetchMembers(); this.fetchMembers();
return result; return result;
}, },
async printMemberList() {
return http.get(`/admin/member/print/namelist`, {
responseType: "blob",
});
},
}, },
}); });

View file

@ -54,6 +54,7 @@ export const useProtocolAgendaStore = defineStore("protocolAgenda", {
id: Number(res.data), id: Number(res.data),
topic: "", topic: "",
context: "", context: "",
sort: this.agenda.length,
protocolId: Number(protocolId), protocolId: Number(protocolId),
}); });
}) })

View file

@ -55,6 +55,7 @@ export const useProtocolDecisionStore = defineStore("protocolDecision", {
id: Number(res.data), id: Number(res.data),
topic: "", topic: "",
context: "", context: "",
sort: this.decision.length,
protocolId: Number(protocolId), protocolId: Number(protocolId),
}); });
}) })

View file

@ -58,6 +58,7 @@ export const useProtocolVotingStore = defineStore("protocolVoting", {
favour: 0, favour: 0,
abstain: 0, abstain: 0,
against: 0, against: 0,
sort: this.voting.length,
protocolId: Number(protocolId), protocolId: Number(protocolId),
}); });
}) })

View file

@ -92,6 +92,7 @@ export const useNavigationStore = defineStore("navigation", {
...(abilityStore.can("read", "club", "protocol") ? [{ key: "protocol", title: "Protokolle" }] : []), ...(abilityStore.can("read", "club", "protocol") ? [{ key: "protocol", title: "Protokolle" }] : []),
...(abilityStore.can("read", "club", "newsletter") ? [{ key: "newsletter", title: "Newsletter" }] : []), ...(abilityStore.can("read", "club", "newsletter") ? [{ key: "newsletter", title: "Newsletter" }] : []),
...(abilityStore.can("read", "club", "query") ? [{ key: "query_builder", title: "Query Builder" }] : []), ...(abilityStore.can("read", "club", "query") ? [{ key: "query_builder", title: "Query Builder" }] : []),
...(abilityStore.can("read", "club", "listprint") ? [{ key: "listprint", title: "Liste Drucken" }] : []),
], ],
}, },
configuration: { configuration: {

View file

@ -6,6 +6,7 @@ export type PermissionModule =
| "newsletter" | "newsletter"
| "newsletter_config" | "newsletter_config"
| "protocol" | "protocol"
| "listprint"
| "qualification" | "qualification"
| "award" | "award"
| "executive_position" | "executive_position"
@ -50,6 +51,7 @@ export const permissionModules: Array<PermissionModule> = [
"newsletter", "newsletter",
"newsletter_config", "newsletter_config",
"protocol", "protocol",
"listprint",
"qualification", "qualification",
"award", "award",
"executive_position", "executive_position",
@ -68,7 +70,7 @@ export const permissionModules: Array<PermissionModule> = [
]; ];
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", "protocol", "query"], club: ["member", "calendar", "newsletter", "protocol", "query", "listprint"],
configuration: [ configuration: [
"qualification", "qualification",
"award", "award",

View file

@ -2,6 +2,7 @@ export interface ProtocolAgendaViewModel {
id: number; id: number;
topic: string; topic: string;
context: string; context: string;
sort: number;
protocolId: number; protocolId: number;
} }
@ -9,4 +10,5 @@ export interface SyncProtocolAgendaViewModel {
id?: number; id?: number;
topic: string; topic: string;
context: string; context: string;
sort?: number;
} }

View file

@ -2,6 +2,7 @@ export interface ProtocolDecisionViewModel {
id: number; id: number;
topic: string; topic: string;
context: string; context: string;
sort: number;
protocolId: number; protocolId: number;
} }
@ -9,4 +10,5 @@ export interface SyncProtocolDecisionViewModel {
id?: number; id?: number;
topic: string; topic: string;
context: string; context: string;
sort?: number;
} }

View file

@ -5,6 +5,7 @@ export interface ProtocolVotingViewModel {
favour: number; favour: number;
abstain: number; abstain: number;
against: number; against: number;
sort: number;
protocolId: number; protocolId: number;
} }
@ -15,5 +16,5 @@ export interface SyncProtocolVotingViewModel {
favour: number; favour: number;
abstain: number; abstain: number;
against: number; against: number;
protocolId: number; sort?: number;
} }

View file

@ -0,0 +1,155 @@
<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">Liste Drucken</h1>
</div>
</template>
<template #diffMain>
<div class="flex flex-col w-full h-full gap-2 px-7 overflow-y-auto">
<form
class="flex flex-col h-fit w-full border border-primary rounded-md p-2 gap-2"
@submit.prevent="sendPrintJob"
>
<div class="flex flex-row gap-2 items-center">
<p class="min-w-16">Titel:</p>
<input id="title" type="text" required />
</div>
<div class="flex flex-row gap-2 items-center">
<p class="min-w-16">Query:</p>
<select id="query" value="member">
<option value="member">(system) alle Mitglieder</option>
<option value="memberByRunningMembership">(system) alle Mitglieder mit laufender Mitgliedschaft</option>
<option v-for="query in queries" :key="query.id" :value="query.id">
{{ query.title }}
</option>
</select>
</div>
<div class="flex flex-col md:flex-row gap-2 md:items-center">
<div class="flex flex-row w-full gap-2 items-center">
<p class="min-w-16">Kopfzeile:</p>
<select id="header" value="def">
<option value="def">Standard-Vorlage verwenden</option>
<option v-for="template in templates" :key="template.id" :value="template.id">
{{ template.template }}
</option>
</select>
</div>
<div class="flex flex-row gap-2 items-center">
<p class="whitespace-nowrap">Höhe [mm]:</p>
<input id="headerHeight" type="number" :min="15" class="!w-24" placeholder="15" />
</div>
</div>
<div class="flex flex-row gap-2 items-center">
<p class="min-w-16">Hauptteil:</p>
<select id="body" value="def">
<option value="def">Standard-Vorlage verwenden</option>
<option value="listprint.member">(system) Mitgliederliste</option>
<option v-for="template in templates" :key="template.id" :value="template.id">
{{ template.template }}
</option>
</select>
</div>
<div class="flex flex-col md:flex-row gap-2 md:items-center">
<div class="flex flex-row w-full gap-2 items-center">
<p class="min-w-16">Fußzeile:</p>
<select id="footer" value="def">
<option value="def">Standard-Vorlage verwenden</option>
<option v-for="template in templates" :key="template.id" :value="template.id">
{{ template.template }}
</option>
</select>
</div>
<div class="flex flex-row gap-2 items-center">
<p class="whitespace-nowrap">Höhe [mm]:</p>
<input id="footerHeight" type="number" :min="15" class="!w-24" placeholder="15" />
</div>
</div>
<div class="flex flex-row gap-2">
<button type="submit" primary class="!w-fit">Liste drucken</button>
<button type="reset" primary-outline class="!w-fit">zurücksetzen</button>
</div>
</form>
<div class="w-full grow min-h-[50%] flex flex-col gap-2">
<Spinner v-if="status == 'loading'" />
<div class="grow">
<iframe v-show="status == 'success'" ref="viewer" class="w-full h-full" />
</div>
<div v-show="status == 'success'" class="flex flex-row gap-2 justify-end">
<a ref="download" button primary class="!w-fit">download</a>
</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 { useQueryStoreStore } from "@/stores/admin/configuration/queryStore";
import { useTemplateStore } from "@/stores/admin/configuration/template";
import Spinner from "@/components/Spinner.vue";
import type { AxiosResponse } from "axios";
import { useListPrintStore } from "@/stores/admin/club/listprint";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | "success" | "failed",
};
},
computed: {
...mapState(useQueryStoreStore, ["queries"]),
...mapState(useTemplateStore, ["templates"]),
},
mounted() {
this.fetchQueries();
this.fetchTemplates();
},
methods: {
...mapActions(useQueryStoreStore, ["fetchQueries"]),
...mapActions(useTemplateStore, ["fetchTemplates"]),
...mapActions(useListPrintStore, ["printList"]),
sendPrintJob(e: any) {
this.status = "loading";
const fromData = e.target.elements;
const title = fromData.title.value;
const queryStore = fromData.query.value;
const headerId = fromData.header.value === "def" ? undefined : fromData.header.value;
const bodyId = fromData.body.value === "def" ? undefined : fromData.body.value;
const footerId = fromData.footer.value === "def" ? undefined : fromData.footer.value;
const headerHeight = fromData.headerHeight.value === "" ? undefined : parseInt(fromData.headerHeight.value);
const footerHeight = fromData.footerHeight.value === "" ? undefined : parseInt(fromData.footerHeight.value);
this.printList({
title,
queryStore,
headerId,
bodyId,
footerId,
headerHeight,
footerHeight,
})
.then((response) => {
this.status = "success";
const blob = new Blob([response.data], { type: "application/pdf" });
(this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob);
const fileURL = window.URL.createObjectURL(new Blob([response.data]));
const fileLink = this.$refs.download as HTMLAnchorElement;
fileLink.href = fileURL;
fileLink.setAttribute("download", `Listen-Ausdruck_${title}.pdf`);
})
.catch(() => {
this.status = "failed";
});
},
},
});
</script>

View file

@ -3,9 +3,6 @@
<template #topBar> <template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Mitglieder</h1> <h1 class="font-bold text-xl h-8">Mitglieder</h1>
<div title="Mitgliederliste drucken" @click="openPrintModal">
<DocumentTextIcon class="w-5 h-5 cursor-pointer" />
</div>
</div> </div>
</template> </template>
<template #diffMain> <template #diffMain>
@ -69,11 +66,6 @@ export default defineComponent({
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/CreateMemberModal.vue"))) markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/CreateMemberModal.vue")))
); );
}, },
openPrintModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberNameListModal.vue")))
);
},
}, },
}); });
</script> </script>

View file

@ -9,6 +9,10 @@
{{ activeMemberObj?.lastname }}, {{ activeMemberObj?.firstname }} {{ activeMemberObj?.lastname }}, {{ activeMemberObj?.firstname }}
{{ activeMemberObj?.nameaffix ? `- ${activeMemberObj?.nameaffix}` : "" }} {{ activeMemberObj?.nameaffix ? `- ${activeMemberObj?.nameaffix}` : "" }}
</h1> </h1>
<div title="Mitgliederliste drucken" @click="openPrintModal">
<DocumentTextIcon class="w-5 h-5 cursor-pointer" />
</div>
<RouterLink v-if="can('update', 'club', 'member')" :to="{ name: 'admin-club-member-edit' }"> <RouterLink v-if="can('update', 'club', 'member')" :to="{ name: 'admin-club-member-edit' }">
<PencilIcon class="w-5 h-5" /> <PencilIcon class="w-5 h-5" />
</RouterLink> </RouterLink>
@ -49,7 +53,7 @@ import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue"; import MainTemplate from "@/templates/Main.vue";
import { RouterLink, RouterView } from "vue-router"; import { RouterLink, RouterView } from "vue-router";
import { useMemberStore } from "@/stores/admin/club/member/member"; import { useMemberStore } from "@/stores/admin/club/member/member";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline"; import { PencilIcon, TrashIcon, DocumentTextIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal"; import { useModalStore } from "@/stores/modal";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
</script> </script>
@ -87,6 +91,11 @@ export default defineComponent({
parseInt(this.memberId ?? "") parseInt(this.memberId ?? "")
); );
}, },
openPrintModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberPrintModal.vue")))
);
},
}, },
}); });
</script> </script>

View file

@ -5,10 +5,10 @@
&#8634; laden fehlgeschlagen &#8634; laden fehlgeschlagen
</p> </p>
<div class="flex flex-col gap-2 h-full overflow-y-auto"> <div class="flex flex-col gap-2 h-full overflow-y-scroll">
<details <details
v-for="item in agenda" v-for="(item, index) in sortedAgenda"
class="flex flex-col gap-2 rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit" class="flex flex-col rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit"
> >
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer"> <summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
<svg <svg
@ -30,6 +30,19 @@
@keyup.prevent @keyup.prevent
:disabled="!can('create', 'club', 'protocol')" :disabled="!can('create', 'club', 'protocol')"
/> />
<div class="flex flex-col">
<ChevronUpIcon
v-if="index != 0"
class="text-white w-4 h-4 stroke-2"
@click.prevent="changeSort('up', item.id, index)"
/>
<ChevronDownIcon
v-if="index != agenda.length - 1"
class="text-white w-4 h-4 stroke-2"
@click.prevent="changeSort('down', item.id, index)"
/>
</div>
</summary> </summary>
<QuillEditor <QuillEditor
id="top" id="top"
@ -59,8 +72,8 @@ import { QuillEditor } from "@vueup/vue-quill";
import "@vueup/vue-quill/dist/vue-quill.snow.css"; import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { toolbarOptions } from "@/helpers/quillConfig"; import { toolbarOptions } from "@/helpers/quillConfig";
import { useProtocolAgendaStore } from "@/stores/admin/club/protocol/protocolAgenda"; import { useProtocolAgendaStore } from "@/stores/admin/club/protocol/protocolAgenda";
import type { ProtocolAgendaViewModel } from "@/viewmodels/admin/club/protocol/protocolAgenda.models";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -71,12 +84,31 @@ export default defineComponent({
computed: { computed: {
...mapWritableState(useProtocolAgendaStore, ["agenda", "loading"]), ...mapWritableState(useProtocolAgendaStore, ["agenda", "loading"]),
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),
sortedAgenda() {
return this.agenda.slice().sort((a, b) => a.sort - b.sort);
},
}, },
mounted() { mounted() {
// this.fetchProtocolAgenda(); this.normalizeSort();
}, },
methods: { methods: {
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda", "createProtocolAgenda"]), ...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda", "createProtocolAgenda"]),
changeSort(dir: "up" | "down", thisId: number, index: number) {
let affected = this.sortedAgenda[dir == "up" ? index - 1 : index + 1]?.id;
if (affected) {
this.agenda.find((a) => a.id == thisId)!.sort = dir == "up" ? index - 1 : index + 1;
this.agenda.find((a) => a.id == affected)!.sort = dir == "up" ? index + 1 : index - 1;
}
this.normalizeSort();
},
normalizeSort() {
let rightSort = this.agenda.every((val, index) => val.sort == index);
if (!rightSort) {
this.sortedAgenda.forEach((e, index) => {
e.sort = index;
});
}
},
}, },
}); });
</script> </script>

View file

@ -5,10 +5,10 @@
&#8634; laden fehlgeschlagen &#8634; laden fehlgeschlagen
</p> </p>
<div class="flex flex-col gap-2 h-full overflow-y-auto"> <div class="flex flex-col gap-2 h-full overflow-y-scroll">
<details <details
v-for="item in decision" v-for="(item, index) in sortedDecision"
class="flex flex-col gap-2 rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit" class="flex flex-col rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit"
> >
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer"> <summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
<svg <svg
@ -30,6 +30,19 @@
@keyup.prevent @keyup.prevent
:disabled="!can('create', 'club', 'protocol')" :disabled="!can('create', 'club', 'protocol')"
/> />
<div class="flex flex-col">
<ChevronUpIcon
v-if="index != 0"
class="text-white w-4 h-4 stroke-2"
@click.prevent="changeSort('up', item.id, index)"
/>
<ChevronDownIcon
v-if="index != decision.length - 1"
class="text-white w-4 h-4 stroke-2"
@click.prevent="changeSort('down', item.id, index)"
/>
</div>
</summary> </summary>
<QuillEditor <QuillEditor
id="top" id="top"
@ -55,12 +68,12 @@
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia"; import { mapActions, mapState, mapWritableState } from "pinia";
import Spinner from "@/components/Spinner.vue"; import Spinner from "@/components/Spinner.vue";
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
import { QuillEditor } from "@vueup/vue-quill"; import { QuillEditor } from "@vueup/vue-quill";
import "@vueup/vue-quill/dist/vue-quill.snow.css"; import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { toolbarOptions } from "@/helpers/quillConfig"; import { toolbarOptions } from "@/helpers/quillConfig";
import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision"; import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -71,12 +84,31 @@ export default defineComponent({
computed: { computed: {
...mapWritableState(useProtocolDecisionStore, ["decision", "loading"]), ...mapWritableState(useProtocolDecisionStore, ["decision", "loading"]),
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),
sortedDecision() {
return this.decision.slice().sort((a, b) => a.sort - b.sort);
},
}, },
mounted() { mounted() {
// this.fetchProtocolDecision(); this.normalizeSort();
}, },
methods: { methods: {
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision", "createProtocolDecision"]), ...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision", "createProtocolDecision"]),
changeSort(dir: "up" | "down", thisId: number, index: number) {
let affected = this.sortedDecision[dir == "up" ? index - 1 : index + 1]?.id;
if (affected) {
this.decision.find((a) => a.id == thisId)!.sort = dir == "up" ? index - 1 : index + 1;
this.decision.find((a) => a.id == affected)!.sort = dir == "up" ? index + 1 : index - 1;
}
this.normalizeSort();
},
normalizeSort() {
let rightSort = this.decision.every((val, index) => val.sort == index);
if (!rightSort) {
this.sortedDecision.forEach((e, index) => {
e.sort = index;
});
}
},
}, },
}); });
</script> </script>

View file

@ -5,10 +5,10 @@
&#8634; laden fehlgeschlagen &#8634; laden fehlgeschlagen
</p> </p>
<div class="flex flex-col gap-2 h-full overflow-y-auto"> <div class="flex flex-col gap-2 h-full overflow-y-scroll">
<details <details
v-for="item in voting" v-for="(item, index) in sortedVoting"
class="flex flex-col gap-2 rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit" class="flex flex-col rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit"
> >
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer"> <summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
<svg <svg
@ -30,6 +30,19 @@
@keyup.prevent @keyup.prevent
:disabled="!can('create', 'club', 'protocol')" :disabled="!can('create', 'club', 'protocol')"
/> />
<div class="flex flex-col">
<ChevronUpIcon
v-if="index != 0"
class="text-white w-4 h-4 stroke-2"
@click.prevent="changeSort('up', item.id, index)"
/>
<ChevronDownIcon
v-if="index != voting.length - 1"
class="text-white w-4 h-4 stroke-2"
@click.prevent="changeSort('down', item.id, index)"
/>
</div>
</summary> </summary>
<QuillEditor <QuillEditor
id="top" id="top"
@ -72,12 +85,12 @@
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia"; import { mapActions, mapState } from "pinia";
import Spinner from "@/components/Spinner.vue"; import Spinner from "@/components/Spinner.vue";
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
import { QuillEditor } from "@vueup/vue-quill"; import { QuillEditor } from "@vueup/vue-quill";
import "@vueup/vue-quill/dist/vue-quill.snow.css"; import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { toolbarOptions } from "@/helpers/quillConfig"; import { toolbarOptions } from "@/helpers/quillConfig";
import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting"; import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -88,12 +101,31 @@ export default defineComponent({
computed: { computed: {
...mapState(useProtocolVotingStore, ["voting", "loading"]), ...mapState(useProtocolVotingStore, ["voting", "loading"]),
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),
sortedVoting() {
return this.voting.slice().sort((a, b) => a.sort - b.sort);
},
}, },
mounted() { mounted() {
// this.fetchProtocolVoting(); this.normalizeSort();
}, },
methods: { methods: {
...mapActions(useProtocolVotingStore, ["fetchProtocolVoting", "createProtocolVoting"]), ...mapActions(useProtocolVotingStore, ["fetchProtocolVoting", "createProtocolVoting"]),
changeSort(dir: "up" | "down", thisId: number, index: number) {
let affected = this.sortedVoting[dir == "up" ? index - 1 : index + 1]?.id;
if (affected) {
this.voting.find((a) => a.id == thisId)!.sort = dir == "up" ? index - 1 : index + 1;
this.voting.find((a) => a.id == affected)!.sort = dir == "up" ? index + 1 : index - 1;
}
this.normalizeSort();
},
normalizeSort() {
let rightSort = this.voting.every((val, index) => val.sort == index);
if (!rightSort) {
this.sortedVoting.forEach((e, index) => {
e.sort = index;
});
}
},
}, },
}); });
</script> </script>