basic routing and info display
This commit is contained in:
parent
c1e9784b4a
commit
e70e6644a6
12 changed files with 211 additions and 125 deletions
8
src/helpers/quillConfig.ts
Normal file
8
src/helpers/quillConfig.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const toolbarOptions = [
|
||||||
|
[{ header: [1, 2, false] }, { font: [] }],
|
||||||
|
[{ header: 1 }, { header: 2 }],
|
||||||
|
["bold", "italic", "underline", "strike"],
|
||||||
|
["blockquote", "code-block", "link"],
|
||||||
|
[{ list: "ordered" }, { list: "bullet" }, { list: "check" }],
|
||||||
|
["clean"],
|
||||||
|
];
|
|
@ -68,7 +68,7 @@ export async function isAuthenticatedPromise(): Promise<Payload> {
|
||||||
|
|
||||||
var { firstname, lastname, mail, username, permissions } = decoded;
|
var { firstname, lastname, mail, username, permissions } = decoded;
|
||||||
|
|
||||||
if (Object.keys(permissions).length === 0) {
|
if (Object.keys(permissions ?? {}).length === 0) {
|
||||||
auth.setFailed();
|
auth.setFailed();
|
||||||
reject("nopermissions");
|
reject("nopermissions");
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,11 +174,27 @@ const router = createRouter({
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "edit",
|
path: "precense",
|
||||||
name: "admin-club-protocol-edit",
|
name: "admin-club-protocol-precense",
|
||||||
component: () => import("@/views/admin/protocol/ProtocolEdit.vue"),
|
component: () => import("@/views/admin/protocol/ProtocolPrecense.vue"),
|
||||||
meta: { type: "update", section: "club", module: "member" },
|
props: true,
|
||||||
beforeEnter: [abilityAndNavUpdate],
|
},
|
||||||
|
{
|
||||||
|
path: "voting",
|
||||||
|
name: "admin-club-protocol-voting",
|
||||||
|
component: () => import("@/views/admin/protocol/ProtocolVoting.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "decisions",
|
||||||
|
name: "admin-club-protocol-decisions",
|
||||||
|
component: () => import("@/views/admin/protocol/ProtocolDecisions.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "protocol",
|
||||||
|
name: "admin-club-protocol-protocol",
|
||||||
|
component: () => import("@/views/admin/protocol/ProtocolProtocol.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const useProtocolStore = defineStore("protocol", {
|
||||||
activeProtocol: null as number | null,
|
activeProtocol: null as number | null,
|
||||||
activeProtocolObj: null as ProtocolViewModel | null,
|
activeProtocolObj: null as ProtocolViewModel | null,
|
||||||
loadingActive: "loading" as "loading" | "fetched" | "failed",
|
loadingActive: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
syncing: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
@ -2,15 +2,24 @@ export interface ProtocolViewModel {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
starttime: Date;
|
||||||
|
endtime: Date;
|
||||||
|
summary: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateProtocolViewModel {
|
export interface CreateProtocolViewModel {
|
||||||
title: string;
|
title: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
starttime: Date;
|
||||||
|
endtime: Date;
|
||||||
|
summary: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProtocolViewModel {
|
export interface UpdateProtocolViewModel {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
starttime: Date;
|
||||||
|
endtime: Date;
|
||||||
|
summary: string;
|
||||||
}
|
}
|
||||||
|
|
29
src/views/admin/protocol/ProtocolDecisions.vue
Normal file
29
src/views/admin/protocol/ProtocolDecisions.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loadingActive == 'failed'" @click="" class="cursor-pointer">↺ laden fehlgeschlagen</p>
|
||||||
|
</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";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolStore, ["loadingActive"]),
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
methods: {},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,111 +0,0 @@
|
||||||
<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'">laden fehlgeschlagen</p>
|
|
||||||
<form
|
|
||||||
v-else-if="protocol != null"
|
|
||||||
class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto"
|
|
||||||
@submit.prevent="triggerUpdate"
|
|
||||||
>
|
|
||||||
<p class="mx-auto">Protokoll bearbeiten</p>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
import { mapActions, mapState } from "pinia";
|
|
||||||
import MainTemplate from "@/templates/Main.vue";
|
|
||||||
import { useProtocolStore } from "@/stores/admin/protocol";
|
|
||||||
import type { ProtocolViewModel, UpdateProtocolViewModel } from "@/viewmodels/admin/protocol.models";
|
|
||||||
import Spinner from "@/components/Spinner.vue";
|
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
|
||||||
import cloneDeep from "lodash.clonedeep";
|
|
||||||
import isEqual from "lodash.isEqual";
|
|
||||||
import { Salutation } from "@/enums/salutation";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
protocolId: String,
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
loadingActive() {
|
|
||||||
if (this.loading == "loading") {
|
|
||||||
this.loading = this.loadingActive;
|
|
||||||
}
|
|
||||||
if (this.loadingActive == "fetched") this.protocol = cloneDeep(this.activeProtocolObj);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loading: "loading" as "loading" | "fetched" | "failed",
|
|
||||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
|
||||||
protocol: null as null | ProtocolViewModel,
|
|
||||||
timeout: null as any,
|
|
||||||
salutations: [] as Array<string>,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
canSaveOrReset(): boolean {
|
|
||||||
return isEqual(this.activeProtocolObj, this.protocol);
|
|
||||||
},
|
|
||||||
...mapState(useProtocolStore, ["activeProtocolObj", "loadingActive"]),
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.fetchItem();
|
|
||||||
this.salutations = Object.values(Salutation);
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
try {
|
|
||||||
clearTimeout(this.timeout);
|
|
||||||
} catch (error) {}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useProtocolStore, ["fetchProtocolByActiveId", "updateActiveProtocol"]),
|
|
||||||
resetForm() {
|
|
||||||
this.protocol = cloneDeep(this.activeProtocolObj);
|
|
||||||
},
|
|
||||||
fetchItem() {
|
|
||||||
this.fetchProtocolByActiveId();
|
|
||||||
},
|
|
||||||
triggerUpdate(e: any) {
|
|
||||||
if (this.protocol == null) return;
|
|
||||||
let formData = e.target.elements;
|
|
||||||
let updateProtocol: UpdateProtocolViewModel = {
|
|
||||||
id: this.protocol.id,
|
|
||||||
title: formData.title.value,
|
|
||||||
date: formData.date.value,
|
|
||||||
};
|
|
||||||
this.status = "loading";
|
|
||||||
this.updateActiveProtocol(updateProtocol)
|
|
||||||
.then(() => {
|
|
||||||
this.fetchItem();
|
|
||||||
this.status = { status: "success" };
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
this.status = { status: "failed" };
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.timeout = setTimeout(() => {
|
|
||||||
this.status = null;
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,6 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
<div v-if="activeProtocol != null" class="flex flex-col gap-2 w-full">Protokoll Details</div>
|
<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" v-model="activeProtocolObj.starttime" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="endtime">Endzeit</label>
|
||||||
|
<input type="time" id="endtime" 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" />
|
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
|
||||||
<p v-else-if="loadingActive == 'failed'" @click="fetchProtocolByActiveId" class="cursor-pointer">
|
<p v-else-if="loadingActive == 'failed'" @click="fetchProtocolByActiveId" class="cursor-pointer">
|
||||||
|
@ -14,6 +45,9 @@ 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/protocol";
|
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>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -22,7 +56,7 @@ export default defineComponent({
|
||||||
protocolId: String,
|
protocolId: String,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useProtocolStore, ["activeProtocol", "loadingActive"]),
|
...mapState(useProtocolStore, ["activeProtocol", "loadingActive", "activeProtocolObj"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchProtocolByActiveId();
|
this.fetchProtocolByActiveId();
|
||||||
|
|
29
src/views/admin/protocol/ProtocolPrecense.vue
Normal file
29
src/views/admin/protocol/ProtocolPrecense.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loadingActive == 'failed'" @click="" class="cursor-pointer">↺ laden fehlgeschlagen</p>
|
||||||
|
</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";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolStore, ["loadingActive"]),
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
methods: {},
|
||||||
|
});
|
||||||
|
</script>
|
29
src/views/admin/protocol/ProtocolProtocol.vue
Normal file
29
src/views/admin/protocol/ProtocolProtocol.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loadingActive == 'failed'" @click="" class="cursor-pointer">↺ laden fehlgeschlagen</p>
|
||||||
|
</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";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolStore, ["loadingActive"]),
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
methods: {},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -8,9 +8,10 @@
|
||||||
<h1 class="font-bold text-xl h-8 min-h-fit grow">
|
<h1 class="font-bold text-xl h-8 min-h-fit grow">
|
||||||
{{ activeProtocolObj?.title }}, {{ activeProtocolObj?.date }}
|
{{ activeProtocolObj?.title }}, {{ activeProtocolObj?.date }}
|
||||||
</h1>
|
</h1>
|
||||||
<RouterLink :to="{ name: 'admin-club-protocol-edit' }">
|
<CloudIcon v-if="syncing == 'synced'" class="w-5 h-5" />
|
||||||
<PencilIcon class="w-5 h-5" />
|
<CloudArrowUpIcon v-else-if="syncing == 'detectedChanges'" class="w-5 h-5" />
|
||||||
</RouterLink>
|
<ArrowPathIcon v-else-if="syncing == 'syncing'" class="w-5 h-5 animate-spin" />
|
||||||
|
<ExclamationTriangleIcon v-else class="w-5 h-5 animate-[ping_1s_ease-in-out_3] text-red-500" />
|
||||||
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -48,7 +49,13 @@ 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 { useProtocolStore } from "@/stores/admin/protocol";
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import {
|
||||||
|
ArrowPathIcon,
|
||||||
|
CloudArrowUpIcon,
|
||||||
|
CloudIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@heroicons/vue/24/outline";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -59,11 +66,17 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tabs: [{ route: "admin-club-protocol-overview", title: "Übersicht" }],
|
tabs: [
|
||||||
|
{ route: "admin-club-protocol-overview", title: "Übersicht" },
|
||||||
|
{ route: "admin-club-protocol-precense", title: "Anwesenheit" },
|
||||||
|
{ route: "admin-club-protocol-voting", title: "Abstimmungen" },
|
||||||
|
{ route: "admin-club-protocol-decisions", title: "Entscheidungen" },
|
||||||
|
{ route: "admin-club-protocol-protocol", title: "Protokoll" },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useProtocolStore, ["activeProtocolObj"]),
|
...mapState(useProtocolStore, ["activeProtocolObj", "syncing"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchProtocolByActiveId();
|
this.fetchProtocolByActiveId();
|
||||||
|
|
29
src/views/admin/protocol/ProtocolVoting.vue
Normal file
29
src/views/admin/protocol/ProtocolVoting.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loadingActive == 'failed'" @click="" class="cursor-pointer">↺ laden fehlgeschlagen</p>
|
||||||
|
</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";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolStore, ["loadingActive"]),
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
methods: {},
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
Reference in a new issue