docker build test

This commit is contained in:
Julian Krauser 2024-11-27 17:06:39 +01:00
parent 075c598a86
commit 5bb107e53a
39 changed files with 117 additions and 75 deletions

View file

@ -2,3 +2,4 @@
node_modules/ node_modules/
dist/ dist/
.git/ .git/
.env

View file

@ -8,9 +8,9 @@ RUN npm install
COPY . /app COPY . /app
RUN npm run build RUN npm run build-only
FROM nginx:18-alpine as prod FROM nginx:stable-alpine AS prod
WORKDIR /app WORKDIR /app

View file

@ -1,22 +1,72 @@
# member-administration-ui # member-administration-ui
Memberadministration Mitgliederverwaltung für Feuerwehren und Vereine.
## Einleitung
Dieses Repository dient zur Verwaltung der Mitgliederdaten. Es ist ein Frontend-Client, der auf die Daten des [member-administration-server Backends](https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-server) zugreift. Die Webapp bietet eine Möglichkeit Mitgliederdaten zu verwalten, Protokolle zu schreiben und Kaledereinträge zu erstellen. Benutzer können eingeladen und Rollen zugewiesen werden.
Eine Demo dieser Seite finden Sie unter [https://ff-admin-demo.jk-effects.cloud](https://ff-admin-demo.jk-effects.cloud).
Für die Verwendung muss ein TOTP-Code eingegeben werden.
Die Zugangsdaten (Lesebeschränkt) sind:\
EMAIL: demo-besucher\
TOTP: ![alt text](demo-totp-qrcode.png)\
TOTP-Code: FBMDAJKFOYQXM2DNH47GWWBGJ5KWOUCW
## Installation ## Installation
### Requirements ### Docker Compose Setup
1. Access to the internet Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit folgendem Inhalt:
### Configuration ```yaml
version: "3"
1. Copy the .env.example file to .env and fill in the required information services:
2. Install all packages via `npm install` ff-member-administration-app:
3. Start the backend application image: docker.registry.jk-effects.cloud/ehrenamt/member-administration/app:latest
4. Start the application container_name: ff_member_administration_ui
5. Run `npm run dev` to run inside dev-environment restart: unless-stopped
### Usage #volumes:
# - <volume|local path>/favicon.png:/app/public/favicon.png
# - <volume|local path>/favicon.png:/app/public/FFW-Logo.svg
```
1. Open the browser and navigate to `http://localhost:5173` or the URL you specified in the server configuration Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten:
2. Go to route `/setup` to create the first user (this path is disabled after the first user is created)
```sh
docker-compose up -d
```
### Manuelle Installation
Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten:
```sh
git clone https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-ui.git
cd member-administration-ui
npm install
npm run build
npm run start
```
### Konfiguration
Ein eigenes favicon und Logo kann über ein volume ausgetauscht werden.
## Einrichtung
1. **Admin Benutzer erstellen**: Erstellen Sie einen Admin Benutzer unter dem Pfad [/setup](https://ff-admin-demo.jk-effects.cloud/setup), um auf die Migliederverwaltung Zugriff zu erhalten. Nach der Erstellung des ersten Benutzers wird der Pfad automatisch geblockt.
2. **Rollen und Berechtigungen**: Unter `Benutzer > Rollen` können die Rollen und Berechtigungen für die Benutzer erstellt und angepasst werden.
3. **Nutzer einladen**: Unter `Benutzer > Benutzer` können weitere Nutzer eingeladen werden. Diese erhalten dann eine E-Mail mit einem Link, um ein TOTP zu erhalten.
## Fragen und Wünsche
Bei Fragen, Anregungen oder Wünschen können Sie sich gerne melden.\
Wir freuen uns über Ihr Feedback und helfen Ihnen gerne weiter.\
Schreiben Sie dafür eine Mail an julian.krauser@jk-effects.com.

BIN
demo-totp-qrcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,22 +1,23 @@
<template> <template>
<footer <footer
v-if="authCheck && (routeName.includes('admin') || routeName.includes('account'))" v-if="authCheck && (routeName.includes('admin-') || routeName.includes('account-'))"
class="md:hidden flex flex-row h-16 min-h-16 justify-center md:justify-normal p-1 bg-white" class="md:hidden flex flex-row h-16 min-h-16 justify-center md:justify-normal p-1 bg-white"
> >
<div class="w-full flex flex-row gap-2 h-full align-middle"> <div class="w-full flex flex-row gap-2 h-full align-middle">
<TopLevelLink <TopLevelLink
v-if="routeName.includes('admin')" v-if="routeName.includes('admin-')"
v-for="item in topLevel" v-for="item in topLevel"
:key="item.key" :key="item.key"
:link="item" :link="item"
:disableSubLink="true" :disableSubLink="true"
/> />
<TopLevelLink v-else :link="{ key: 'club', title: 'Zur Verwaltung' }" :disableSubLink="true" /> <TopLevelLink v-else :link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }" :disableSubLink="true" />
</div> </div>
</footer> </footer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue";
import { mapState } from "pinia"; import { mapState } from "pinia";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useNavigationStore } from "@/stores/admin/navigation"; import { useNavigationStore } from "@/stores/admin/navigation";
@ -24,7 +25,6 @@ import TopLevelLink from "./admin/TopLevelLink.vue";
</script> </script>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({ export default defineComponent({
computed: { computed: {
...mapState(useAuthStore, ["authCheck"]), ...mapState(useAuthStore, ["authCheck"]),

View file

@ -6,10 +6,10 @@
</RouterLink> </RouterLink>
<div class="flex flex-row gap-2 items-center"> <div class="flex flex-row gap-2 items-center">
<div v-if="authCheck" class="hidden md:flex flex-row gap-2 h-full align-middle"> <div v-if="authCheck" class="hidden md:flex flex-row gap-2 h-full align-middle">
<TopLevelLink v-if="routeName.includes('admin')" v-for="item in topLevel" :key="item.key" :link="item" /> <TopLevelLink v-if="routeName.includes('admin-')" v-for="item in topLevel" :key="item.key" :link="item" />
<TopLevelLink <TopLevelLink
v-else-if="routeName.includes('account')" v-else-if="routeName.includes('account-')"
:link="{ key: 'club', title: 'Zur Verwaltung' }" :link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
:disable-sub-link="true" :disable-sub-link="true"
/> />
</div> </div>

View file

@ -33,9 +33,9 @@ export default defineComponent({
}, },
methods: { methods: {
copyToClipboard() { copyToClipboard() {
navigator.clipboard.writeText(this.otp ?? ""); navigator.clipboard.writeText(this.copyText ?? "");
this.copySuccess = true; this.copySuccess = true;
this.timputCopy = setTimeout(() => { this.timeoutCopy = setTimeout(() => {
this.copySuccess = false; this.copySuccess = false;
}, 2000); }, 2000);
}, },

View file

@ -95,7 +95,7 @@ import { mapState, mapActions } from "pinia";
import { EyeIcon, PencilIcon, PlusIcon, TrashIcon } from "@heroicons/vue/24/outline"; import { EyeIcon, PencilIcon, PlusIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import Spinner from "@/components/Spinner.vue"; import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue"; import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue"; import FailureXMark from "@/components/FailureXMark.vue";

View file

@ -24,7 +24,7 @@ export default defineComponent({
default: "LINK", default: "LINK",
}, },
link: { link: {
type: [String, Object as PropType<{ name: string }>], type: Object as PropType<string | { name: string }>,
default: "/", default: "/",
}, },
active: { active: {

View file

@ -18,13 +18,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { RouterLink } from "vue-router";
import { useNavigationStore, type topLevelNavigationModel } from "@/stores/admin/navigation"; import { useNavigationStore, type topLevelNavigationModel } from "@/stores/admin/navigation";
import { mapState } from "pinia"; import { mapState } from "pinia";
</script> </script>
<script lang="ts"> <script lang="ts">
import { defineComponent, type PropType } from "vue";
import { RouterLink } from "vue-router";
export default defineComponent({ export default defineComponent({
props: { props: {
link: { link: {

View file

@ -94,7 +94,7 @@
id="startdate" id="startdate"
required required
:value="data.start" :value="data.start"
@change="($event) => ($refs.enddate.max = ($event.target as HTMLInputElement).value)" @change="($event) => (($refs.enddate as HTMLInputElement).max = ($event.target as HTMLInputElement).value)"
/> />
</div> </div>
<div class="w-full"> <div class="w-full">

View file

@ -89,7 +89,9 @@
@change=" @change="
($event) => { ($event) => {
calendar!.starttime = new Date(($event.target as HTMLInputElement).value).toISOString(); calendar!.starttime = new Date(($event.target as HTMLInputElement).value).toISOString();
$refs.endtime.min = formatForDateTimeLocalInput(($event.target as HTMLInputElement).value); ($refs.endtime as HTMLInputElement).min = formatForDateTimeLocalInput(
($event.target as HTMLInputElement).value
);
} }
" "
/> />
@ -122,7 +124,7 @@
@change=" @change="
($event) => { ($event) => {
calendar!.starttime = new Date(($event.target as HTMLInputElement).value).toISOString(); calendar!.starttime = new Date(($event.target as HTMLInputElement).value).toISOString();
$refs.enddate.min = formatForDateInput(($event.target as HTMLInputElement).value); ($refs.enddate as HTMLInputElement).min = formatForDateInput(($event.target as HTMLInputElement).value);
} }
" "
/> />
@ -194,7 +196,7 @@ import { CheckIcon, ChevronUpDownIcon, TrashIcon } from "@heroicons/vue/20/solid
import { useCalendarTypeStore } from "@/stores/admin/calendarType"; import { useCalendarTypeStore } from "@/stores/admin/calendarType";
import type { CalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models"; import type { CalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
</script> </script>

View file

@ -105,7 +105,7 @@ import type {
UpdateMemberAwardViewModel, UpdateMemberAwardViewModel,
} from "@/viewmodels/admin/memberAward.models"; } from "@/viewmodels/admin/memberAward.models";
import { useMemberAwardStore } from "@/stores/admin/memberAward"; import { useMemberAwardStore } from "@/stores/admin/memberAward";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
</script> </script>

View file

@ -77,7 +77,7 @@ import type {
CommunicationViewModel, CommunicationViewModel,
UpdateCommunicationViewModel, UpdateCommunicationViewModel,
} from "@/viewmodels/admin/communication.models"; } from "@/viewmodels/admin/communication.models";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
</script> </script>

View file

@ -111,7 +111,7 @@ import type {
UpdateMemberExecutivePositionViewModel, UpdateMemberExecutivePositionViewModel,
} from "@/viewmodels/admin/memberExecutivePosition.models"; } from "@/viewmodels/admin/memberExecutivePosition.models";
import { useMemberExecutivePositionStore } from "@/stores/admin/memberExecutivePosition"; import { useMemberExecutivePositionStore } from "@/stores/admin/memberExecutivePosition";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
</script> </script>

View file

@ -112,7 +112,7 @@ import type {
UpdateMemberQualificationViewModel, UpdateMemberQualificationViewModel,
} from "@/viewmodels/admin/memberQualification.models"; } from "@/viewmodels/admin/memberQualification.models";
import { useMemberQualificationStore } from "@/stores/admin/memberQualification"; import { useMemberQualificationStore } from "@/stores/admin/memberQualification";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
</script> </script>

View file

@ -112,7 +112,7 @@ import type {
UpdateMembershipViewModel, UpdateMembershipViewModel,
} from "@/viewmodels/admin/membership.models"; } from "@/viewmodels/admin/membership.models";
import { useMembershipStore } from "@/stores/admin/membership"; import { useMembershipStore } from "@/stores/admin/membership";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
</script> </script>

View file

@ -28,7 +28,11 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null && status?.status != 'failed'"> <button
primary-outline
@click="closeModal"
:disabled="status != null && status != 'loading' && status?.status != 'failed'"
>
abbrechen abbrechen
</button> </button>
</div> </div>
@ -47,7 +51,6 @@ import { useCalendarTypeStore } from "@/stores/admin/calendarType";
import type { CreateCalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models"; import type { CreateCalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue"; import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import type { CalendarFieldType } from "@/types/fieldTypes";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -56,7 +59,6 @@ export default defineComponent({
return { return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string }, status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any, timeout: undefined as any,
selectedFields: [] as Array<CalendarFieldType>,
}; };
}, },
beforeUnmount() { beforeUnmount() {

View file

@ -24,7 +24,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, type PropType } from "vue"; import { defineComponent, type PropType } from "vue";
import { mapState, mapActions } from "pinia"; import { mapState, mapActions } from "pinia";
import type { InviteUserModal } from "@/viewmodels/admin/invite.models"; import type { InviteViewModel } from "@/viewmodels/admin/invite.models";
import { PencilIcon, UserGroupIcon, WrenchScrewdriverIcon, TrashIcon } from "@heroicons/vue/24/outline"; import { PencilIcon, UserGroupIcon, WrenchScrewdriverIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { useInviteStore } from "@/stores/admin/invite"; import { useInviteStore } from "@/stores/admin/invite";
@ -33,7 +33,7 @@ import { useInviteStore } from "@/stores/admin/invite";
<script lang="ts"> <script lang="ts">
export default defineComponent({ export default defineComponent({
props: { props: {
invite: { type: Object as PropType<InviteUserModal>, default: {} }, invite: { type: Object as PropType<InviteViewModel>, default: {} },
}, },
computed: { computed: {
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),

View file

@ -57,7 +57,7 @@ body {
} }
/*:not([headlessui]):not([id*="headlessui"]):not([class*="headlessui"])*/ /*:not([headlessui]):not([id*="headlessui"]):not([class*="headlessui"])*/
button:not([class*="ql"] *):not([class*="fc"]), button:not([class*="ql"] *):not([class*="fc"]):not([id*="headlessui-combobox"]),
a[button] { a[button] {
@apply relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-none focus:ring-0; @apply relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-none focus:ring-0;
} }

View file

@ -5,7 +5,7 @@ import router from "./router";
let devMode = process.env.NODE_ENV === "development"; let devMode = process.env.NODE_ENV === "development";
const http = axios.create({ const http = axios.create({
baseURL: devMode ? "http://localhost:5000" : process.env.SERVER_ADDRESS, baseURL: (devMode ? "http://localhost:5000" : "") + "/api",
headers: { headers: {
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
Pragma: "no-cache", Pragma: "no-cache",

View file

@ -4,7 +4,7 @@ import { http } from "@/serverCom";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import type { ProtocolViewModel } from "@/viewmodels/admin/protocol.models"; import type { ProtocolViewModel } from "@/viewmodels/admin/protocol.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import difference from "lodash.difference"; import difference from "lodash.difference";
export const useProtocolStore = defineStore("protocol", { export const useProtocolStore = defineStore("protocol", {

View file

@ -6,7 +6,7 @@ import type {
} from "../../viewmodels/admin/protocolAgenda.models"; } from "../../viewmodels/admin/protocolAgenda.models";
import { useProtocolStore } from "./protocol"; import { useProtocolStore } from "./protocol";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import differenceWith from "lodash.differencewith"; import differenceWith from "lodash.differencewith";
export const useProtocolAgendaStore = defineStore("protocolAgenda", { export const useProtocolAgendaStore = defineStore("protocolAgenda", {

View file

@ -7,7 +7,7 @@ import type {
} from "../../viewmodels/admin/protocolDecision.models"; } from "../../viewmodels/admin/protocolDecision.models";
import { useProtocolStore } from "./protocol"; import { useProtocolStore } from "./protocol";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import differenceWith from "lodash.differencewith"; import differenceWith from "lodash.differencewith";
export const useProtocolDecisionStore = defineStore("protocolDecision", { export const useProtocolDecisionStore = defineStore("protocolDecision", {

View file

@ -7,7 +7,7 @@ import type {
} from "../../viewmodels/admin/protocolPresence.models"; } from "../../viewmodels/admin/protocolPresence.models";
import { useProtocolStore } from "./protocol"; import { useProtocolStore } from "./protocol";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
export const useProtocolPresenceStore = defineStore("protocolPresence", { export const useProtocolPresenceStore = defineStore("protocolPresence", {
state: () => { state: () => {

View file

@ -7,7 +7,7 @@ import type {
} from "../../viewmodels/admin/protocolVoting.models"; } from "../../viewmodels/admin/protocolVoting.models";
import { useProtocolStore } from "./protocol"; import { useProtocolStore } from "./protocol";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import differenceWith from "lodash.differencewith"; import differenceWith from "lodash.differencewith";
export const useProtocolVotingStore = defineStore("protocolVoting", { export const useProtocolVotingStore = defineStore("protocolVoting", {

View file

@ -51,7 +51,7 @@
}" }"
> >
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }"> <span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
{{ user.firstname }} {{ user.lastname }} {{ user.nameaffix }} {{ user.firstname }} {{ user.lastname }}
</span> </span>
<span <span
v-if="selected" v-if="selected"
@ -124,15 +124,6 @@ export default defineComponent({
.includes(this.query.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;
});
},
}, },
mounted() { mounted() {
this.fetchUsers(); this.fetchUsers();

View file

@ -48,12 +48,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, markRaw, defineAsyncComponent } from "vue"; import { defineComponent, markRaw, defineAsyncComponent } from "vue";
import { mapActions, mapState } from "pinia"; import { mapActions, mapState } from "pinia";
import type { UserViewModel } from "@/viewmodels/admin/user.models";
import MainTemplate from "@/templates/Main.vue"; import MainTemplate from "@/templates/Main.vue";
import Spinner from "@/components/Spinner.vue"; import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue"; import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue"; import FailureXMark from "@/components/FailureXMark.vue";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -96,7 +96,7 @@ import FailureXMark from "@/components/FailureXMark.vue";
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue"; import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import { Salutation } from "@/enums/salutation"; import { Salutation } from "@/enums/salutation";
</script> </script>

View file

@ -47,7 +47,7 @@ import FailureXMark from "@/components/FailureXMark.vue";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import type { AwardViewModel, UpdateAwardViewModel } from "@/viewmodels/admin/award.models"; import type { AwardViewModel, UpdateAwardViewModel } from "@/viewmodels/admin/award.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -57,7 +57,7 @@ import { RouterLink } from "vue-router";
import type { CalendarTypeViewModel, UpdateCalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models"; import type { CalendarTypeViewModel, UpdateCalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -15,12 +15,7 @@
/> />
</div> </div>
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<button <button v-if="can('create', 'settings', 'communication')" primary class="!w-fit" @click="openCreateModal">
v-if="can('create', 'settings', 'communication_type')"
primary
class="!w-fit"
@click="openCreateModal"
>
Kommunikationsart erstellen Kommunikationsart erstellen
</button> </button>
</div> </div>

View file

@ -97,7 +97,7 @@ import type {
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue"; import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -50,7 +50,7 @@ import type {
UpdateExecutivePositionViewModel, UpdateExecutivePositionViewModel,
} from "@/viewmodels/admin/executivePosition.models"; } from "@/viewmodels/admin/executivePosition.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -50,7 +50,7 @@ import type {
MembershipStatusViewModel, MembershipStatusViewModel,
} from "@/viewmodels/admin/membershipStatus.models"; } from "@/viewmodels/admin/membershipStatus.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -51,7 +51,7 @@ import FailureXMark from "@/components/FailureXMark.vue";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import type { UpdateQualificationViewModel, QualificationViewModel } from "@/viewmodels/admin/qualification.models"; import type { UpdateQualificationViewModel, QualificationViewModel } from "@/viewmodels/admin/qualification.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -46,7 +46,7 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue"; import FailureXMark from "@/components/FailureXMark.vue";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
import type { RoleViewModel } from "@/viewmodels/admin/role.models"; import type { RoleViewModel } from "@/viewmodels/admin/role.models";
</script> </script>

View file

@ -59,7 +59,7 @@ import { RouterLink } from "vue-router";
import { useUserStore } from "@/stores/admin/user"; import { useUserStore } from "@/stores/admin/user";
import type { UpdateUserViewModel, UserViewModel } from "@/viewmodels/admin/user.models"; import type { UpdateUserViewModel, UserViewModel } from "@/viewmodels/admin/user.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -66,7 +66,7 @@ import FailureXMark from "@/components/FailureXMark.vue";
import { XMarkIcon, PlusIcon } from "@heroicons/vue/24/outline"; import { XMarkIcon, PlusIcon } from "@heroicons/vue/24/outline";
import type { UserViewModel } from "@/viewmodels/admin/user.models"; import type { UserViewModel } from "@/viewmodels/admin/user.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual"; import isEqual from "lodash.isequal";
</script> </script>
<script lang="ts"> <script lang="ts">