Merge branch 'main' into #3-calendar
# Conflicts: # package-lock.json # src/main.css # src/router/authGuards.ts # src/types/permissionTypes.ts
This commit is contained in:
commit
8074dbf5c4
38 changed files with 2228 additions and 66 deletions
66
src/views/admin/club/protocol/Protocol.vue
Normal file
66
src/views/admin/club/protocol/Protocol.vue
Normal file
|
@ -0,0 +1,66 @@
|
|||
<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">Protokolle</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
|
||||
<Pagination
|
||||
:items="protocols"
|
||||
:totalCount="totalCount"
|
||||
:indicateLoading="loading == 'loading'"
|
||||
@load-data="(offset, count, search) => fetchProtocols(offset, count)"
|
||||
@search="(search) => fetchProtocols(0, 25, true)"
|
||||
>
|
||||
<template #pageRow="{ row }: { row: ProtocolViewModel }">
|
||||
<ProtocolListItem :protocol="row" />
|
||||
</template>
|
||||
</Pagination>
|
||||
|
||||
<div class="flex flex-row gap-4">
|
||||
<button primary class="!w-fit" @click="openCreateModal">Protokoll erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/vue/20/solid";
|
||||
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||
import ProtocolListItem from "@/components/admin/club/protocol/ProtocolListItem.vue";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Pagination from "../../../../components/Pagination.vue";
|
||||
import type { ProtocolViewModel } from "../../../../viewmodels/admin/protocol.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
currentPage: 0,
|
||||
maxEntriesPerPage: 25,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useProtocolStore, ["protocols", "totalCount", "loading"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocols(0, this.maxEntriesPerPage, true);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useProtocolStore, ["fetchProtocols"]),
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
openCreateModal() {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/protocol/CreateProtocolModal.vue")))
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
74
src/views/admin/club/protocol/ProtocolAgenda.vue
Normal file
74
src/views/admin/club/protocol/ProtocolAgenda.vue
Normal file
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'" @click="fetchProtocolAgenda" class="cursor-pointer">
|
||||
↺ laden fehlgeschlagen
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||
<details
|
||||
v-for="item in agenda"
|
||||
class="flex flex-col gap-2 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">
|
||||
<svg
|
||||
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z" />
|
||||
</svg>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="TOP"
|
||||
autocomplete="off"
|
||||
v-model="item.topic"
|
||||
@keyup.prevent
|
||||
/>
|
||||
</summary>
|
||||
<QuillEditor
|
||||
id="top"
|
||||
theme="snow"
|
||||
placeholder="TOP Inhalt..."
|
||||
style="height: 250px; max-height: 250px; min-height: 250px"
|
||||
contentType="html"
|
||||
:toolbar="toolbarOptions"
|
||||
v-model:content="item.context"
|
||||
/>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<button primary class="!w-fit" @click="createProtocolAgenda">Eintrag hinzufügen</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import { QuillEditor } from "@vueup/vue-quill";
|
||||
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||
import { useProtocolAgendaStore } from "@/stores/admin/protocolAgenda";
|
||||
import type { ProtocolAgendaViewModel } from "@/viewmodels/admin/protocolAgenda.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
protocolId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapWritableState(useProtocolAgendaStore, ["agenda", "loading"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolAgenda();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda", "createProtocolAgenda"]),
|
||||
},
|
||||
});
|
||||
</script>
|
74
src/views/admin/club/protocol/ProtocolDecisions.vue
Normal file
74
src/views/admin/club/protocol/ProtocolDecisions.vue
Normal file
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'" @click="fetchProtocolDecision" class="cursor-pointer">
|
||||
↺ laden fehlgeschlagen
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||
<details
|
||||
v-for="item in decision"
|
||||
class="flex flex-col gap-2 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">
|
||||
<svg
|
||||
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z" />
|
||||
</svg>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="Einscheidung"
|
||||
autocomplete="off"
|
||||
v-model="item.topic"
|
||||
@keyup.prevent
|
||||
/>
|
||||
</summary>
|
||||
<QuillEditor
|
||||
id="top"
|
||||
theme="snow"
|
||||
placeholder="Entscheidung Inhalt..."
|
||||
style="height: 250px; max-height: 250px; min-height: 250px"
|
||||
contentType="html"
|
||||
:toolbar="toolbarOptions"
|
||||
v-model:content="item.context"
|
||||
/>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<button primary class="!w-fit" @click="createProtocolDecision">Eintrag hinzufügen</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||
import { QuillEditor } from "@vueup/vue-quill";
|
||||
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||
import { useProtocolDecisionStore } from "../../../../stores/admin/protocolDecision";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
protocolId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapWritableState(useProtocolDecisionStore, ["decision", "loading"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolDecision();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision", "createProtocolDecision"]),
|
||||
},
|
||||
});
|
||||
</script>
|
68
src/views/admin/club/protocol/ProtocolOverview.vue
Normal file
68
src/views/admin/club/protocol/ProtocolOverview.vue
Normal file
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||
<div v-if="activeProtocolObj != null" class="flex flex-col gap-2 w-full">
|
||||
<div class="w-full">
|
||||
<label for="title">Titel</label>
|
||||
<input type="text" id="title" v-model="activeProtocolObj.title" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label for="date">Datum</label>
|
||||
<input type="date" id="date" v-model="activeProtocolObj.date" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 w-full">
|
||||
<div class="w-full">
|
||||
<label for="starttime">Startzeit</label>
|
||||
<input type="time" id="starttime" step="1" v-model="activeProtocolObj.starttime" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label for="endtime">Endzeit</label>
|
||||
<input type="time" id="endtime" step="1" v-model="activeProtocolObj.endtime" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col h-1/2">
|
||||
<label for="summary">Zusammenfassung</label>
|
||||
<QuillEditor
|
||||
id="summary"
|
||||
theme="snow"
|
||||
placeholder="Zusammenfassung zur Sitzung..."
|
||||
style="height: 250px; max-height: 250px; min-height: 250px"
|
||||
contentType="html"
|
||||
:toolbar="toolbarOptions"
|
||||
v-model:content="activeProtocolObj.summary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loadingActive == 'failed'" @click="fetchProtocolByActiveId" class="cursor-pointer">
|
||||
↺ laden fehlgeschlagen
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||
import { QuillEditor } from "@vueup/vue-quill";
|
||||
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
protocolId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapWritableState(useProtocolStore, ["loadingActive", "activeProtocolObj"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolByActiveId();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
||||
},
|
||||
});
|
||||
</script>
|
152
src/views/admin/club/protocol/ProtocolPresence.vue
Normal file
152
src/views/admin/club/protocol/ProtocolPresence.vue
Normal file
|
@ -0,0 +1,152 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'" @click="fetchProtocolPresence" class="cursor-pointer">
|
||||
↺ laden fehlgeschlagen
|
||||
</p>
|
||||
|
||||
<div class="w-full">
|
||||
<Combobox v-model="presence" multiple>
|
||||
<ComboboxLabel>Anwesende suchen</ComboboxLabel>
|
||||
<div class="relative mt-1">
|
||||
<ComboboxInput
|
||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||
@input="query = $event.target.value"
|
||||
/>
|
||||
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</ComboboxButton>
|
||||
<TransitionRoot
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
@after-leave="query = ''"
|
||||
>
|
||||
<ComboboxOptions
|
||||
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ComboboxOption v-if="filtered.length === 0" as="template" disabled>
|
||||
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
||||
<span class="font-normal block truncate"> Keine Auswahl</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
|
||||
<ComboboxOption
|
||||
v-for="member in filtered"
|
||||
as="template"
|
||||
:key="member.id"
|
||||
:value="member.id"
|
||||
v-slot="{ selected, active }"
|
||||
>
|
||||
<li
|
||||
class="relative cursor-default select-none py-2 pl-10 pr-4"
|
||||
:class="{
|
||||
'bg-primary text-white': active,
|
||||
'text-gray-900': !active,
|
||||
}"
|
||||
>
|
||||
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
|
||||
{{ member.firstname }} {{ member.lastname }} {{ member.nameaffix }}
|
||||
</span>
|
||||
<span
|
||||
v-if="selected"
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-3"
|
||||
:class="{ 'text-white': active, 'text-primary': !active }"
|
||||
>
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</TransitionRoot>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
<br />
|
||||
<p>Ausgewählte Anwesende</p>
|
||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||
<div
|
||||
v-for="member in selected"
|
||||
:key="member.id"
|
||||
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
||||
>
|
||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="removeSelected(member.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxLabel,
|
||||
ComboboxInput,
|
||||
ComboboxButton,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
TransitionRoot,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||
import { useMemberStore } from "@/stores/admin/member";
|
||||
import type { MemberViewModel } from "@/viewmodels/admin/member.models";
|
||||
import { useProtocolPresenceStore } from "../../../../stores/admin/protocolPresence";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
protocolId: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: "" as String,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapWritableState(useProtocolPresenceStore, ["presence", "loading"]),
|
||||
...mapState(useMemberStore, ["members"]),
|
||||
filtered(): Array<MemberViewModel> {
|
||||
return this.query === ""
|
||||
? this.members
|
||||
: this.members.filter((member) =>
|
||||
(member.firstname + " " + member.lastname)
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "")
|
||||
.includes(this.query.toLowerCase().replace(/\s+/g, ""))
|
||||
);
|
||||
},
|
||||
sorted(): Array<MemberViewModel> {
|
||||
return this.selected.sort((a, b) => {
|
||||
if (a.lastname < b.lastname) return -1;
|
||||
if (a.lastname > b.lastname) return 1;
|
||||
if (a.firstname < b.firstname) return -1;
|
||||
if (a.firstname > b.firstname) return 1;
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
selected(): Array<MemberViewModel> {
|
||||
return this.members.filter((m) => this.presence.includes(m.id));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMembers();
|
||||
this.fetchProtocolPresence();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useMemberStore, ["fetchMembers"]),
|
||||
...mapActions(useProtocolPresenceStore, ["fetchProtocolPresence"]),
|
||||
removeSelected(id: number) {
|
||||
let index = this.presence.findIndex((s) => s == id);
|
||||
if (index != -1) {
|
||||
this.presence.splice(index, 1);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
94
src/views/admin/club/protocol/ProtocolPrintout.vue
Normal file
94
src/views/admin/club/protocol/ProtocolPrintout.vue
Normal file
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'" @click="fetchProtocolPrintout" class="cursor-pointer">
|
||||
↺ laden fehlgeschlagen
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||
<div
|
||||
v-for="print in printout"
|
||||
:key="print.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>{{ print.title }}</p>
|
||||
<div class="flex flex-row">
|
||||
<div>
|
||||
<ViewfinderCircleIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="openPdfShow(print.id)" />
|
||||
</div>
|
||||
<div>
|
||||
<ArrowDownTrayIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="downloadPdf(print.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<p>Ausdruck Nummer: {{ print.iteration }}</p>
|
||||
<p>Ausdruck erstellt: {{ print.createdAt }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-start gap-2">
|
||||
<button primary class="!w-fit" :disabled="printing != undefined" @click="createProtocolPrintout">
|
||||
Ausdruck erstellen
|
||||
</button>
|
||||
<Spinner v-if="printing == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="printing == 'success'" />
|
||||
<FailureXMark v-else-if="printing == 'failed'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { useProtocolPrintoutStore } from "@/stores/admin/protocolPrintout";
|
||||
import { ArrowDownTrayIcon, ViewfinderCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
protocolId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useProtocolPrintoutStore, ["printout", "loading", "printing"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolPrintout();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
...mapActions(useProtocolPrintoutStore, [
|
||||
"fetchProtocolPrintout",
|
||||
"createProtocolPrintout",
|
||||
"fetchProtocolPrintoutById",
|
||||
]),
|
||||
openPdfShow(id: number) {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/protocol/ProtocolPrintoutViewerModal.vue"))),
|
||||
id
|
||||
);
|
||||
},
|
||||
downloadPdf(id: number) {
|
||||
let clickedOn = this.printout.find((p) => p.id == id);
|
||||
this.fetchProtocolPrintoutById(id)
|
||||
.then((response) => {
|
||||
const fileURL = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const fileLink = document.createElement("a");
|
||||
fileLink.href = fileURL;
|
||||
fileLink.setAttribute("download", clickedOn?.title ? clickedOn.title + ".pdf" : "Protokoll.pdf");
|
||||
document.body.appendChild(fileLink);
|
||||
fileLink.click();
|
||||
fileLink.remove();
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
120
src/views/admin/club/protocol/ProtocolRouting.vue
Normal file
120
src/views/admin/club/protocol/ProtocolRouting.vue
Normal file
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #headerInsert>
|
||||
<RouterLink to="../" class="text-primary w-fit">zurück zur Liste</RouterLink>
|
||||
</template>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8 min-h-fit grow">{{ origin?.title }}, {{ origin?.date }}</h1>
|
||||
<ProtocolSyncing
|
||||
:executeSyncAll="executeSyncAll"
|
||||
@syncState="
|
||||
(state) => {
|
||||
syncState = state;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<!-- <TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" /> -->
|
||||
</div>
|
||||
</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.route"
|
||||
v-slot="{ isActive }"
|
||||
:to="{ name: tab.route }"
|
||||
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',
|
||||
isActive ? 'bg-red-200 shadow border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200',
|
||||
]"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</p>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import { RouterLink, RouterView } from "vue-router";
|
||||
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import ProtocolSyncing from "@/components/admin/club/protocol/ProtocolSyncing.vue";
|
||||
import { PrinterIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
protocolId: String,
|
||||
},
|
||||
watch: {
|
||||
syncState() {
|
||||
if (this.wantToClose && this.syncState == "synced") {
|
||||
this.wantToClose = false;
|
||||
this.closeModal();
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
{ route: "admin-club-protocol-overview", title: "Übersicht" },
|
||||
{ route: "admin-club-protocol-presence", title: "Anwesenheit" },
|
||||
{ route: "admin-club-protocol-voting", title: "Abstimmungen" },
|
||||
{ route: "admin-club-protocol-decisions", title: "Beschlüsse" },
|
||||
{ route: "admin-club-protocol-agenda", title: "Protokoll" },
|
||||
{ route: "admin-club-protocol-printout", title: "Druck" },
|
||||
],
|
||||
wantToClose: false as boolean,
|
||||
executeSyncAll: undefined as any,
|
||||
syncState: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useProtocolStore, ["origin"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolByActiveId();
|
||||
},
|
||||
// this.syncState is undefined, so it will never work
|
||||
// beforeRouteLeave(to, from, next) {
|
||||
// if (this.syncState != "synced") {
|
||||
// this.executeSyncAll = Date.now();
|
||||
// this.wantToClose = true;
|
||||
// this.openInfoModal();
|
||||
// next(false);
|
||||
// } else {
|
||||
// next();
|
||||
// }
|
||||
// },
|
||||
methods: {
|
||||
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
openInfoModal() {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/protocol/CurrentlySyncingModal.vue")))
|
||||
);
|
||||
},
|
||||
openDeleteModal() {
|
||||
// this.openModal(
|
||||
// markRaw(defineAsyncComponent(() => import("@/components/admin/club/protocol/DeleteProtocolModal.vue"))),
|
||||
// parseInt(this.protocolId ?? "")
|
||||
// );
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
91
src/views/admin/club/protocol/ProtocolVoting.vue
Normal file
91
src/views/admin/club/protocol/ProtocolVoting.vue
Normal file
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'" @click="fetchProtocolVoting" class="cursor-pointer">
|
||||
↺ laden fehlgeschlagen
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||
<details
|
||||
v-for="item in voting"
|
||||
class="flex flex-col gap-2 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">
|
||||
<svg
|
||||
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z" />
|
||||
</svg>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="Einscheidung"
|
||||
autocomplete="off"
|
||||
v-model="item.topic"
|
||||
@keyup.prevent
|
||||
/>
|
||||
</summary>
|
||||
<QuillEditor
|
||||
id="top"
|
||||
theme="snow"
|
||||
placeholder="Entscheidung Inhalt..."
|
||||
style="height: 100px; max-height: 100px; min-height: 100px"
|
||||
contentType="html"
|
||||
:toolbar="toolbarOptions"
|
||||
v-model:content="item.context"
|
||||
/>
|
||||
<div class="px-2 pb-2">
|
||||
<p>Ergebnis:</p>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="w-full">
|
||||
<p>dafür</p>
|
||||
<input type="number" v-model="item.favour" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<p>dagegen</p>
|
||||
<input type="number" v-model="item.against" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<p>enthalten</p>
|
||||
<input type="number" v-model="item.abstain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<button primary class="!w-fit" @click="createProtocolVoting">Abstimmung hinzufügen</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||
import { QuillEditor } from "@vueup/vue-quill";
|
||||
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||
import { useProtocolVotingStore } from "../../../../stores/admin/protocolVoting";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
protocolId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useProtocolVotingStore, ["voting", "loading"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolVoting();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useProtocolVotingStore, ["fetchProtocolVoting", "createProtocolVoting"]),
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -7,42 +7,17 @@
|
|||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
|
||||
<div class="flex flex-col w-full grow gap-2 pr-2 overflow-y-scroll">
|
||||
<MemberListItem v-for="member in members" :key="member.id" :member="member" />
|
||||
</div>
|
||||
<div class="flex flex-row w-full justify-between select-none">
|
||||
<p class="text-sm font-normal text-gray-500">
|
||||
Elemente <span class="font-semibold text-gray-900">{{ showingText }}</span> von
|
||||
<span class="font-semibold text-gray-900">{{ entryCount }}</span>
|
||||
</p>
|
||||
<ul class="flex flex-row text-sm h-8">
|
||||
<li
|
||||
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg"
|
||||
:class="[currentPage > 0 ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50']"
|
||||
@click="loadPage(currentPage - 1)"
|
||||
>
|
||||
<ChevronLeftIcon class="h-4" />
|
||||
</li>
|
||||
<li
|
||||
v-for="page in displayedPagesNumbers"
|
||||
:key="page"
|
||||
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 first:rounded-s-lg last:rounded-e-lg"
|
||||
:class="[currentPage == page ? 'font-bold border-primary' : '', page != '.' ? ' cursor-pointer' : '']"
|
||||
@click="loadPage(page)"
|
||||
>
|
||||
{{ typeof page == "number" ? page + 1 : "..." }}
|
||||
</li>
|
||||
<li
|
||||
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg"
|
||||
:class="[
|
||||
currentPage + 1 < countOfPages ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50',
|
||||
]"
|
||||
@click="loadPage(currentPage + 1)"
|
||||
>
|
||||
<ChevronRightIcon class="h-4" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Pagination
|
||||
:items="members"
|
||||
:totalCount="totalCount"
|
||||
:indicateLoading="loading == 'loading'"
|
||||
@load-data="(offset, count, search) => fetchMembers(offset, count)"
|
||||
@search="(search) => fetchMembers(0, 25, true)"
|
||||
>
|
||||
<template #pageRow="{ row }: { row: MemberViewModel }">
|
||||
<MemberListItem :member="row" />
|
||||
</template>
|
||||
</Pagination>
|
||||
|
||||
<div class="flex flex-row gap-4">
|
||||
<button primary class="!w-fit" @click="openCreateModal">Mitglied erstellen</button>
|
||||
|
@ -60,6 +35,8 @@ import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/vue/20/solid";
|
|||
import { useMemberStore } from "@/stores/admin/member";
|
||||
import MemberListItem from "@/components/admin/club/member/MemberListItem.vue";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Pagination from "../../../components/Pagination.vue";
|
||||
import type { MemberViewModel } from "../../../viewmodels/admin/member.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -71,7 +48,7 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useMemberStore, ["members", "totalCount"]),
|
||||
...mapState(useMemberStore, ["members", "totalCount", "loading"]),
|
||||
entryCount() {
|
||||
return this.totalCount ?? this.members.length;
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue