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:
Julian Krauser 2024-11-07 11:06:52 +01:00
commit 8074dbf5c4
38 changed files with 2228 additions and 66 deletions

View 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>

View 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">
&#8634; 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>

View 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">
&#8634; 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>

View 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">
&#8634; 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>

View 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">
&#8634; 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>

View 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">
&#8634; 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>

View 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>

View 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">
&#8634; 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>

View file

@ -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;
},