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/
dist/
.git/
.env

View file

@ -8,9 +8,9 @@ RUN npm install
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

View file

@ -1,22 +1,72 @@
# 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
### 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
2. Install all packages via `npm install`
3. Start the backend application
4. Start the application
5. Run `npm run dev` to run inside dev-environment
services:
ff-member-administration-app:
image: docker.registry.jk-effects.cloud/ehrenamt/member-administration/app:latest
container_name: ff_member_administration_ui
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
2. Go to route `/setup` to create the first user (this path is disabled after the first user is created)
Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten:
```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>
<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"
>
<div class="w-full flex flex-row gap-2 h-full align-middle">
<TopLevelLink
v-if="routeName.includes('admin')"
v-if="routeName.includes('admin-')"
v-for="item in topLevel"
:key="item.key"
:link="item"
: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>
</footer>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useNavigationStore } from "@/stores/admin/navigation";
@ -24,7 +25,6 @@ import TopLevelLink from "./admin/TopLevelLink.vue";
</script>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
computed: {
...mapState(useAuthStore, ["authCheck"]),

View file

@ -6,10 +6,10 @@
</RouterLink>
<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">
<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
v-else-if="routeName.includes('account')"
:link="{ key: 'club', title: 'Zur Verwaltung' }"
v-else-if="routeName.includes('account-')"
:link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
:disable-sub-link="true"
/>
</div>

View file

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

View file

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

View file

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

View file

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

View file

@ -94,7 +94,7 @@
id="startdate"
required
: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 class="w-full">

View file

@ -89,7 +89,9 @@
@change="
($event) => {
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="
($event) => {
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 type { CalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual";
import isEqual from "lodash.isequal";
import { useAbilityStore } from "@/stores/ability";
</script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
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 { useAbilityStore } from "@/stores/ability";
import { useInviteStore } from "@/stores/admin/invite";
@ -33,7 +33,7 @@ import { useInviteStore } from "@/stores/admin/invite";
<script lang="ts">
export default defineComponent({
props: {
invite: { type: Object as PropType<InviteUserModal>, default: {} },
invite: { type: Object as PropType<InviteViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),

View file

@ -57,7 +57,7 @@ body {
}
/*: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] {
@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";
const http = axios.create({
baseURL: devMode ? "http://localhost:5000" : process.env.SERVER_ADDRESS,
baseURL: (devMode ? "http://localhost:5000" : "") + "/api",
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -48,12 +48,13 @@
<script setup lang="ts">
import { defineComponent, markRaw, defineAsyncComponent } from "vue";
import { mapActions, mapState } from "pinia";
import type { UserViewModel } from "@/viewmodels/admin/user.models";
import MainTemplate from "@/templates/Main.vue";
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 isEqual from "lodash.isequal";
</script>
<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 { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isEqual";
import isEqual from "lodash.isequal";
import { Salutation } from "@/enums/salutation";
</script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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