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;
|
||||
|
||||
if (Object.keys(permissions).length === 0) {
|
||||
if (Object.keys(permissions ?? {}).length === 0) {
|
||||
auth.setFailed();
|
||||
reject("nopermissions");
|
||||
}
|
||||
|
|
|
@ -174,11 +174,27 @@ const router = createRouter({
|
|||
props: true,
|
||||
},
|
||||
{
|
||||
path: "edit",
|
||||
name: "admin-club-protocol-edit",
|
||||
component: () => import("@/views/admin/protocol/ProtocolEdit.vue"),
|
||||
meta: { type: "update", section: "club", module: "member" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
path: "precense",
|
||||
name: "admin-club-protocol-precense",
|
||||
component: () => import("@/views/admin/protocol/ProtocolPrecense.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -13,6 +13,7 @@ export const useProtocolStore = defineStore("protocol", {
|
|||
activeProtocol: null as number | null,
|
||||
activeProtocolObj: null as ProtocolViewModel | null,
|
||||
loadingActive: "loading" as "loading" | "fetched" | "failed",
|
||||
syncing: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
|
|
|
@ -2,15 +2,24 @@ export interface ProtocolViewModel {
|
|||
id: number;
|
||||
title: string;
|
||||
date: Date;
|
||||
starttime: Date;
|
||||
endtime: Date;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface CreateProtocolViewModel {
|
||||
title: string;
|
||||
date: Date;
|
||||
starttime: Date;
|
||||
endtime: Date;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface UpdateProtocolViewModel {
|
||||
id: number;
|
||||
title: string;
|
||||
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>
|
||||
<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" />
|
||||
<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 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">
|
||||
|
@ -22,7 +56,7 @@ export default defineComponent({
|
|||
protocolId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useProtocolStore, ["activeProtocol", "loadingActive"]),
|
||||
...mapState(useProtocolStore, ["activeProtocol", "loadingActive", "activeProtocolObj"]),
|
||||
},
|
||||
mounted() {
|
||||
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">
|
||||
{{ activeProtocolObj?.title }}, {{ activeProtocolObj?.date }}
|
||||
</h1>
|
||||
<RouterLink :to="{ name: 'admin-club-protocol-edit' }">
|
||||
<PencilIcon class="w-5 h-5" />
|
||||
</RouterLink>
|
||||
<CloudIcon v-if="syncing == 'synced'" class="w-5 h-5" />
|
||||
<CloudArrowUpIcon v-else-if="syncing == 'detectedChanges'" class="w-5 h-5" />
|
||||
<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" />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -48,7 +49,13 @@ import { mapActions, mapState } from "pinia";
|
|||
import MainTemplate from "@/templates/Main.vue";
|
||||
import { RouterLink, RouterView } from "vue-router";
|
||||
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";
|
||||
</script>
|
||||
|
||||
|
@ -59,11 +66,17 @@ export default defineComponent({
|
|||
},
|
||||
data() {
|
||||
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: {
|
||||
...mapState(useProtocolStore, ["activeProtocolObj"]),
|
||||
...mapState(useProtocolStore, ["activeProtocolObj", "syncing"]),
|
||||
},
|
||||
mounted() {
|
||||
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