Support oft parent groups

This commit is contained in:
2026-09-29 14:35:33 +02:00
parent c4ca101594
commit a5e4e55df5
86 changed files with 1936 additions and 80 deletions
+116
View File
@@ -0,0 +1,116 @@
<script setup>
import { ref, onMounted } from 'vue';
import AdminAppLayout from "../../../../resources/js/layouts/AdminAppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
const { request } = useAjax()
const form = ref({
localGroup: '',
localGroupPlural: '',
parentGroup: '',
parentGroupPlural: '',
})
onMounted(async () => {
const response = await request('/api/v1/admin/settings')
Object.assign(form.value, response?.groupLabels ?? {})
})
async function save() {
const response = await request('/api/v1/admin/settings', {
method: 'POST',
body: form.value,
})
if (response && response.status === 'success') {
toast.success(response.message)
} else {
toast.error(response?.message ?? 'Fehler beim Speichern')
}
}
</script>
<template>
<AdminAppLayout title="Einstellungen">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<h3>Bezeichnungen der Gruppen</h3>
<p class="hint">
Gelten für alle Mandanten: in Anmeldungen, Teilnehmendenlisten, Exporten und in der Verwaltung.
</p>
<table class="data-table">
<thead>
<tr>
<th></th>
<th>Einzahl</th>
<th>Mehrzahl</th>
</tr>
</thead>
<tbody>
<tr>
<th>Gruppe (Mandant)</th>
<td><input type="text" v-model="form.localGroup" class="form-input" placeholder="z. B. Stamm" /></td>
<td><input type="text" v-model="form.localGroupPlural" class="form-input" placeholder="z. B. Stämme" /></td>
</tr>
<tr>
<th>Übergeordnete Gruppe</th>
<td><input type="text" v-model="form.parentGroup" class="form-input" placeholder="z. B. Bund" /></td>
<td><input type="text" v-model="form.parentGroupPlural" class="form-input" placeholder="z. B. Bünde" /></td>
</tr>
</tbody>
</table>
<button class="btn-save" @click="save">Speichern</button>
</shadowed-box>
</AdminAppLayout>
</template>
<style scoped>
.hint {
color: #6b7280;
font-size: 0.9rem;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
text-align: left;
padding: 8px 12px;
color: #374151;
border-bottom: 1px solid #e5e7eb;
}
.data-table td {
padding: 8px 12px;
border-bottom: 1px solid #e5e7eb;
}
.form-input {
width: 100%;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.95rem;
box-sizing: border-box;
}
.btn-save {
margin-top: 15px;
padding: 8px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
background-color: #16a34a;
color: #ffffff;
}
.btn-save:hover {
background-color: #15803d;
}
</style>
+249
View File
@@ -0,0 +1,249 @@
<script setup>
import { ref, onMounted } from 'vue';
import AdminAppLayout from "../../../../resources/js/layouts/AdminAppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
const props = defineProps({
groupLabels: {
type: Object,
default: () => ({ localGroupPlural: 'Stämme', parentGroup: 'Bund', parentGroupPlural: 'Bünde' }),
},
})
const { request } = useAjax()
const parentGroups = ref([])
const newName = ref('')
const showCreate = ref(false)
// ID des Eintrags, der gerade umbenannt wird, samt Entwurf des neuen Namens.
const editingId = ref(null)
const editName = ref('')
onMounted(async () => {
await loadParentGroups()
})
async function loadParentGroups() {
const response = await request('/api/v1/admin/parent-groups/list')
parentGroups.value = response?.parentGroups ?? []
}
async function createParentGroup() {
const response = await request('/api/v1/admin/parent-groups/create', {
method: 'POST',
body: { name: newName.value },
})
if (response && response.status === 'success') {
toast.success(response.message)
newName.value = ''
showCreate.value = false
await loadParentGroups()
} else {
toast.error(response?.message ?? 'Fehler beim Anlegen')
}
}
function startEditing(parentGroup) {
editingId.value = parentGroup.id
editName.value = parentGroup.name
}
async function saveName(parentGroup) {
const response = await request('/api/v1/admin/parent-groups/' + parentGroup.id, {
method: 'POST',
body: { name: editName.value },
})
if (response && response.status === 'success') {
toast.success(response.message)
editingId.value = null
await loadParentGroups()
} else {
toast.error(response?.message ?? 'Fehler beim Speichern')
}
}
async function deleteParentGroup(parentGroup) {
if (!confirm('„' + parentGroup.name + '" wirklich löschen?')) {
return
}
const response = await request('/api/v1/admin/parent-groups/' + parentGroup.id, {
method: 'DELETE',
})
if (response && response.status === 'success') {
toast.success(response.message)
await loadParentGroups()
} else {
toast.error(response?.message ?? 'Fehler beim Löschen')
}
}
</script>
<template>
<AdminAppLayout :title="props.groupLabels.parentGroupPlural">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<table class="parent-group-table">
<thead>
<tr>
<th>Name</th>
<th>Zugeordnete {{ props.groupLabels.localGroupPlural }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="parentGroup in parentGroups" :key="parentGroup.id">
<td>
<input v-if="editingId === parentGroup.id" type="text" v-model="editName" class="form-input"
@keyup.enter="saveName(parentGroup)" />
<span v-else>{{ parentGroup.name }}</span>
</td>
<td>{{ parentGroup.tenantCount }}</td>
<td class="actions">
<template v-if="editingId === parentGroup.id">
<button class="btn-save" @click="saveName(parentGroup)">Speichern</button>
<button class="btn-cancel" @click="editingId = null">Abbrechen</button>
</template>
<template v-else>
<button class="btn-edit" @click="startEditing(parentGroup)">Umbenennen</button>
<button v-if="parentGroup.deletable" class="btn-delete" @click="deleteParentGroup(parentGroup)">Löschen</button>
</template>
</td>
</tr>
</tbody>
</table>
<p class="hint">
Löschen ist nur möglich, solange keine {{ props.groupLabels.localGroupPlural }} und keine Anmeldungen
zugeordnet sind. „Ohne Zuordnung" bleibt immer bestehen.
</p>
<div style="margin-top: 20px;">
<button v-if="!showCreate" class="btn-create" @click="showCreate = true">Neuen Eintrag anlegen</button>
<div v-if="showCreate" class="create-form">
<h3>Neuen Eintrag anlegen ({{ props.groupLabels.parentGroup }})</h3>
<input type="text" v-model="newName" class="form-input" placeholder="Name"
@keyup.enter="createParentGroup" />
<div class="btn-group">
<button class="btn-save" @click="createParentGroup">Anlegen</button>
<button class="btn-cancel" @click="showCreate = false">Abbrechen</button>
</div>
</div>
</div>
</shadowed-box>
</AdminAppLayout>
</template>
<style scoped>
.parent-group-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #d1d5db;
border-radius: 8px;
overflow: hidden;
}
.parent-group-table thead th {
text-align: left;
padding: 10px 16px;
background-color: #f9fafb;
color: #374151;
font-weight: bold;
border-bottom: 2px solid #d1d5db;
}
.parent-group-table tbody td {
padding: 10px 16px;
border-bottom: 1px solid #e5e7eb;
}
.actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.hint {
margin-top: 12px;
color: #6b7280;
font-size: 0.9rem;
}
.create-form {
margin-top: 15px;
padding: 20px;
border: 1px solid #d1d5db;
border-radius: 8px;
background-color: #f9fafb;
}
.create-form h3 {
margin: 0 0 15px 0;
color: #374151;
}
.form-input {
width: 100%;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.95rem;
box-sizing: border-box;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn-create, .btn-save, .btn-cancel, .btn-edit, .btn-delete {
padding: 6px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
}
.btn-create, .btn-edit {
background-color: #1d4899;
color: #ffffff;
}
.btn-create:hover, .btn-edit:hover {
background-color: #163a7a;
}
.btn-save {
background-color: #16a34a;
color: #ffffff;
}
.btn-save:hover {
background-color: #15803d;
}
.btn-cancel {
background-color: #e5e7eb;
color: #374151;
}
.btn-cancel:hover {
background-color: #d1d5db;
}
.btn-delete {
background-color: #dc2626;
color: #ffffff;
}
.btn-delete:hover {
background-color: #b91c1c;
}
</style>
@@ -1,5 +1,5 @@
<script setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
@@ -17,8 +17,16 @@ const form = ref({
name: props.data.name ?? '',
slug: props.data.slug ?? '',
url: props.data.url ?? '',
parent_group_id: props.data.parent_group_id ?? null,
})
// Übergeordnete Gruppe (z. B. „Bund") -- Pflichtangabe, Auswahl aus der LV-seitig gepflegten Liste.
const parentGroups = props.data.parentGroups ?? []
const parentGroupLabel = props.data.parentGroupLabel ?? 'Bund'
const parentGroupName = computed(() =>
parentGroups.find(g => g.id === form.value.parent_group_id)?.name ?? '—'
)
async function save() {
const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenants/' + props.data.slug + '/general'
const response = await request(saveUrl, {
@@ -39,6 +47,7 @@ async function save() {
<div v-if="!editing">
<table class="data-table">
<tr><th>Name:</th><td>{{ form.name }}</td></tr>
<tr><th>{{ parentGroupLabel }}:</th><td>{{ parentGroupName }}</td></tr>
<tr><th>Slug:</th><td>{{ form.slug }}</td></tr>
<tr><th>URL:</th><td>{{ form.url }}</td></tr>
<tr><th>Status:</th><td>
@@ -54,6 +63,14 @@ async function save() {
<th>Name:</th>
<td><input type="text" v-model="form.name" class="form-input" /></td>
</tr>
<tr>
<th>{{ parentGroupLabel }}:</th>
<td>
<select v-model="form.parent_group_id" class="form-input">
<option v-for="group in parentGroups" :key="group.id" :value="group.id">{{ group.name }}</option>
</select>
</td>
</tr>
<tr>
<th>Slug:</th>
<td><input type="text" v-model="form.slug" class="form-input" /></td>
+31 -5
View File
@@ -5,6 +5,13 @@ import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
const props = defineProps({
groupLabels: {
type: Object,
default: () => ({ localGroup: 'Stamm', localGroupPlural: 'Stämme', parentGroup: 'Bund' }),
},
})
const { request } = useAjax()
function openTenant(slug) {
@@ -12,12 +19,16 @@ function openTenant(slug) {
}
const tenants = ref([])
const parentGroups = ref([])
const showCreate = ref(false)
const createForm = ref({
const emptyForm = () => ({
name: '',
slug: '',
url: '',
// Pflichtangabe; vorbelegt mit „Ohne Zuordnung", sobald die Liste geladen ist.
parent_group_id: parentGroups.value.find(g => g.isFallback)?.id ?? null,
})
const createForm = ref(emptyForm())
onMounted(async () => {
await loadTenants()
@@ -33,6 +44,11 @@ async function loadTenants() {
})
const data = await response.json()
tenants.value = data.tenants
parentGroups.value = data.parentGroups ?? []
if (createForm.value.parent_group_id === null) {
createForm.value.parent_group_id = parentGroups.value.find(g => g.isFallback)?.id ?? null
}
}
async function createTenant() {
@@ -44,7 +60,7 @@ async function createTenant() {
if (response && response.status === 'success') {
toast.success(response.message)
showCreate.value = false
createForm.value = { name: '', slug: '', url: '' }
createForm.value = emptyForm()
await loadTenants()
} else {
toast.error(response?.message ?? 'Fehler beim Anlegen')
@@ -53,12 +69,13 @@ async function createTenant() {
</script>
<template>
<AdminAppLayout title="Stämme">
<AdminAppLayout :title="props.groupLabels.localGroupPlural">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<table class="tenant-table">
<thead>
<tr>
<th>Name</th>
<th>{{ props.groupLabels.parentGroup }}</th>
<th>Slug</th>
<th>URL</th>
<th>Status</th>
@@ -67,6 +84,7 @@ async function createTenant() {
<tbody>
<tr v-for="tenant in tenants" :key="tenant.slug" class="tenant-row" @click="openTenant(tenant.slug)">
<td>{{ tenant.name }}</td>
<td>{{ tenant.parentGroup ?? '—' }}</td>
<td>{{ tenant.slug }}</td>
<td>{{ tenant.url }}</td>
<td>
@@ -79,15 +97,23 @@ async function createTenant() {
</table>
<div style="margin-top: 20px;">
<button v-if="!showCreate" class="btn-create" @click="showCreate = true">Neuen Stamm anlegen</button>
<button v-if="!showCreate" class="btn-create" @click="showCreate = true">Neuen Eintrag anlegen ({{ props.groupLabels.localGroup }})</button>
<div v-if="showCreate" class="create-form">
<h3>Neuen Stamm anlegen</h3>
<h3>Neuen Eintrag anlegen ({{ props.groupLabels.localGroup }})</h3>
<table class="form-table">
<tr>
<th>Name</th>
<td><input type="text" v-model="createForm.name" class="form-input" /></td>
</tr>
<tr>
<th>{{ props.groupLabels.parentGroup }}</th>
<td>
<select v-model="createForm.parent_group_id" class="form-input">
<option v-for="group in parentGroups" :key="group.id" :value="group.id">{{ group.name }}</option>
</select>
</td>
</tr>
<tr>
<th>Slug</th>
<td><input type="text" v-model="createForm.slug" class="form-input" /></td>