feat(ui): restructure Edit.vue with agenda view
This commit is contained in:
parent
d2eef5abe2
commit
f78d20fc59
|
|
@ -3,9 +3,9 @@ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
|
||||||
import { Head, router } from '@inertiajs/vue3'
|
import { Head, router } from '@inertiajs/vue3'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import InformationBlock from '@/Components/Blocks/InformationBlock.vue'
|
import InformationBlock from '@/Components/Blocks/InformationBlock.vue'
|
||||||
import ModerationBlock from '@/Components/Blocks/ModerationBlock.vue'
|
import AgendaItemRow from '@/Components/AgendaItemRow.vue'
|
||||||
import SongsBlock from '@/Components/Blocks/SongsBlock.vue'
|
import SongAgendaItem from '@/Components/SongAgendaItem.vue'
|
||||||
import SermonBlock from '@/Components/Blocks/SermonBlock.vue'
|
import ArrangementDialog from '@/Components/ArrangementDialog.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
service: {
|
service: {
|
||||||
|
|
@ -20,14 +20,6 @@ const props = defineProps({
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
moderationSlides: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
sermonSlides: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
songsCatalog: {
|
songsCatalog: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
|
|
@ -40,6 +32,14 @@ const props = defineProps({
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
agendaItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
agendaSettings: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const formattedDate = computed(() => {
|
const formattedDate = computed(() => {
|
||||||
|
|
@ -61,18 +61,6 @@ function formatShortDate(dateStr) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Collapsible block state ─────────────────────────────── */
|
|
||||||
const expandedBlocks = ref({
|
|
||||||
information: true,
|
|
||||||
moderation: true,
|
|
||||||
sermon: true,
|
|
||||||
songs: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
function toggleBlock(key) {
|
|
||||||
expandedBlocks.value[key] = !expandedBlocks.value[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
router.get(route('services.index'))
|
router.get(route('services.index'))
|
||||||
}
|
}
|
||||||
|
|
@ -81,58 +69,36 @@ function refreshPage() {
|
||||||
router.reload({ preserveScroll: true })
|
router.reload({ preserveScroll: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Block definitions ───────────────────────────────────── */
|
/* ── Agenda helpers ──────────────────────────────────────── */
|
||||||
const blocks = [
|
|
||||||
{
|
|
||||||
key: 'information',
|
|
||||||
label: 'Information',
|
|
||||||
description: 'Info-Folien fuer alle kommenden Services',
|
|
||||||
icon: 'info',
|
|
||||||
accentFrom: 'from-sky-400',
|
|
||||||
accentTo: 'to-blue-500',
|
|
||||||
badgeColor: 'bg-sky-100 text-sky-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'moderation',
|
|
||||||
label: 'Moderation',
|
|
||||||
description: 'Moderationsfolien fuer diesen Service',
|
|
||||||
icon: 'moderation',
|
|
||||||
accentFrom: 'from-violet-400',
|
|
||||||
accentTo: 'to-purple-500',
|
|
||||||
badgeColor: 'bg-violet-100 text-violet-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sermon',
|
|
||||||
label: 'Predigt',
|
|
||||||
description: 'Predigtfolien fuer diesen Service',
|
|
||||||
icon: 'sermon',
|
|
||||||
accentFrom: 'from-amber-400',
|
|
||||||
accentTo: 'to-orange-500',
|
|
||||||
badgeColor: 'bg-amber-100 text-amber-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'songs',
|
|
||||||
label: 'Songs',
|
|
||||||
description: 'Songs und Arrangements verwalten',
|
|
||||||
icon: 'songs',
|
|
||||||
accentFrom: 'from-emerald-400',
|
|
||||||
accentTo: 'to-teal-500',
|
|
||||||
badgeColor: 'bg-emerald-100 text-emerald-700',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function blockSlideCount(key) {
|
const arrangementDialogItem = ref(null)
|
||||||
if (key === 'information') return props.informationSlides.length
|
|
||||||
if (key === 'moderation') return props.moderationSlides.length
|
function openArrangementDialog(item) {
|
||||||
if (key === 'sermon') return props.sermonSlides.length
|
arrangementDialogItem.value = item
|
||||||
if (key === 'songs') return props.serviceSongs.length
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockBadgeLabel(key) {
|
function getArrangements(item) {
|
||||||
const count = blockSlideCount(key)
|
const song = item.service_song?.song ?? item.serviceSong?.song
|
||||||
if (key === 'songs') return `${count} Song${count !== 1 ? 's' : ''}`
|
return song?.arrangements ?? []
|
||||||
return `${count} Folie${count !== 1 ? 'n' : ''}`
|
}
|
||||||
|
|
||||||
|
function getAvailableGroups(item) {
|
||||||
|
const song = item.service_song?.song ?? item.serviceSong?.song
|
||||||
|
return song?.groups ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedArrangementId(item) {
|
||||||
|
return item.service_song?.song_arrangement_id ?? item.serviceSong?.song_arrangement_id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHeaderType(item) {
|
||||||
|
return item.type?.toLowerCase() === 'header' ||
|
||||||
|
(item.service_song_id === null && item.type !== 'Song' && item.type !== 'song' && item.type !== 'Default' && item.type !== 'default')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onArrangementSelected() {
|
||||||
|
router.reload({ preserveScroll: true })
|
||||||
|
arrangementDialogItem.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Finalize / Reopen / Download ───────────────────────── */
|
/* ── Finalize / Reopen / Download ───────────────────────── */
|
||||||
|
|
@ -361,144 +327,88 @@ async function downloadService() {
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="py-8">
|
<!-- Information Block -->
|
||||||
<div class="mx-auto max-w-5xl px-4 pb-24 sm:px-6 lg:px-8">
|
<div class="py-6">
|
||||||
<!-- Block accordion sections -->
|
<div class="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||||
<div class="space-y-4">
|
<div class="overflow-hidden rounded-xl border border-gray-200/80 bg-white p-5 shadow-sm">
|
||||||
<div
|
<div class="mb-3 flex items-center gap-3">
|
||||||
v-for="block in blocks"
|
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-sky-400 to-blue-500 shadow-sm">
|
||||||
:key="block.key"
|
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
class="overflow-hidden rounded-xl border border-gray-200/80 bg-white shadow-sm transition-shadow hover:shadow-md"
|
|
||||||
>
|
|
||||||
<!-- Block header (clickable) -->
|
|
||||||
<button
|
|
||||||
data-testid="service-edit-block-toggle"
|
|
||||||
type="button"
|
|
||||||
class="flex w-full items-center gap-4 px-5 py-4 text-left transition-colors hover:bg-gray-50/60"
|
|
||||||
@click="toggleBlock(block.key)"
|
|
||||||
>
|
|
||||||
<!-- Accent icon -->
|
|
||||||
<div
|
|
||||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br shadow-sm"
|
|
||||||
:class="[block.accentFrom, block.accentTo]"
|
|
||||||
>
|
|
||||||
<!-- Information icon -->
|
|
||||||
<svg v-if="block.icon === 'info'" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Moderation icon -->
|
|
||||||
<svg v-else-if="block.icon === 'moderation'" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z" />
|
|
||||||
</svg>
|
|
||||||
<!-- Sermon icon -->
|
|
||||||
<svg v-else-if="block.icon === 'sermon'" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
|
|
||||||
</svg>
|
|
||||||
<!-- Songs icon -->
|
|
||||||
<svg v-else-if="block.icon === 'songs'" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9l10.5-3m0 6.553v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 11-.99-3.467l2.31-.66a2.25 2.25 0 001.632-2.163zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 01-.99-3.467l2.31-.66A2.25 2.25 0 009 15.553z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<!-- Label + description -->
|
<h3 class="text-[15px] font-semibold text-gray-900">Information</h3>
|
||||||
<div class="min-w-0 flex-1">
|
<p class="text-xs text-gray-500">Info-Folien fuer alle kommenden Services</p>
|
||||||
<div class="flex items-center gap-2.5">
|
|
||||||
<h3 class="text-[15px] font-semibold text-gray-900">
|
|
||||||
{{ block.label }}
|
|
||||||
</h3>
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium"
|
|
||||||
:class="block.badgeColor"
|
|
||||||
>
|
|
||||||
{{ blockBadgeLabel(block.key) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 text-xs text-gray-500">
|
|
||||||
{{ block.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chevron -->
|
|
||||||
<svg
|
|
||||||
class="h-5 w-5 shrink-0 text-gray-400 transition-transform duration-200"
|
|
||||||
:class="{ 'rotate-180': expandedBlocks[block.key] }"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Block content (collapsible) -->
|
|
||||||
<Transition
|
|
||||||
enter-active-class="transition-all duration-200 ease-out"
|
|
||||||
enter-from-class="max-h-0 opacity-0"
|
|
||||||
enter-to-class="max-h-[2000px] opacity-100"
|
|
||||||
leave-active-class="transition-all duration-150 ease-in"
|
|
||||||
leave-from-class="max-h-[2000px] opacity-100"
|
|
||||||
leave-to-class="max-h-0 opacity-0"
|
|
||||||
>
|
|
||||||
<div v-show="expandedBlocks[block.key]" class="overflow-hidden">
|
|
||||||
<div class="border-t border-gray-100 px-5 py-6">
|
|
||||||
<InformationBlock
|
<InformationBlock
|
||||||
v-if="block.key === 'information'"
|
|
||||||
:service-id="service.id"
|
:service-id="service.id"
|
||||||
:service-date="service.date"
|
:service-date="service.date"
|
||||||
:slides="informationSlides"
|
:slides="informationSlides"
|
||||||
@slides-updated="refreshPage"
|
@slides-updated="refreshPage"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ModerationBlock
|
<!-- Ablauf (Agenda) -->
|
||||||
v-else-if="block.key === 'moderation'"
|
<div class="py-6">
|
||||||
:service-id="service.id"
|
<div class="mx-auto max-w-4xl px-4 pb-24 sm:px-6 lg:px-8">
|
||||||
:slides="moderationSlides"
|
<h3 class="mb-4 text-base font-semibold text-gray-900">Ablauf</h3>
|
||||||
@slides-updated="refreshPage"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SermonBlock
|
<!-- Empty state -->
|
||||||
v-else-if="block.key === 'sermon'"
|
<div v-if="!agendaItems || agendaItems.length === 0"
|
||||||
:service-id="service.id"
|
class="rounded-lg border border-dashed border-gray-300 p-8 text-center text-sm text-gray-500">
|
||||||
:slides="sermonSlides"
|
Keine Ablauf-Elemente vorhanden. Bitte synchronisiere die Daten zuerst.
|
||||||
@slides-updated="refreshPage"
|
</div>
|
||||||
/>
|
|
||||||
|
|
||||||
<SongsBlock
|
<!-- Agenda items list -->
|
||||||
v-else-if="block.key === 'songs'"
|
<div v-else class="flex flex-col gap-2" data-testid="agenda-section">
|
||||||
:service-songs="serviceSongs"
|
<template v-for="item in agendaItems" :key="item.id">
|
||||||
|
<!-- Title/header items -->
|
||||||
|
<div v-if="isHeaderType(item)"
|
||||||
|
class="border-b border-gray-100 pb-1 pt-3"
|
||||||
|
data-testid="agenda-header-item">
|
||||||
|
<h4 class="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||||
|
{{ item.title }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Song items -->
|
||||||
|
<SongAgendaItem
|
||||||
|
v-else-if="item.service_song_id !== null || item.type === 'Song'"
|
||||||
|
:agenda-item="item"
|
||||||
:songs-catalog="songsCatalog"
|
:songs-catalog="songsCatalog"
|
||||||
|
:service-id="service.id"
|
||||||
|
@arrangement-selected="openArrangementDialog(item)"
|
||||||
|
@slides-updated="refreshPage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<!-- Generic agenda items -->
|
||||||
|
<AgendaItemRow
|
||||||
v-else
|
v-else
|
||||||
class="flex items-center justify-center rounded-lg border-2 border-dashed border-gray-200 bg-gray-50/50 py-12"
|
:agenda-item="item"
|
||||||
>
|
:service-id="service.id"
|
||||||
<div class="text-center">
|
:service-date="service.date"
|
||||||
<div
|
@slides-updated="refreshPage"
|
||||||
class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-br opacity-20"
|
/>
|
||||||
:class="[block.accentFrom, block.accentTo]"
|
</template>
|
||||||
>
|
|
||||||
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p class="mt-3 text-sm font-medium text-gray-400">
|
|
||||||
{{ block.label }}-Block wird hier angezeigt
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-xs text-gray-300">
|
|
||||||
Platzhalter — Komponente folgt
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Arrangement Dialog -->
|
||||||
|
<ArrangementDialog
|
||||||
|
v-if="arrangementDialogItem"
|
||||||
|
:song-id="arrangementDialogItem.service_song?.song_id ?? arrangementDialogItem.serviceSong?.song_id"
|
||||||
|
:arrangements="getArrangements(arrangementDialogItem)"
|
||||||
|
:available-groups="getAvailableGroups(arrangementDialogItem)"
|
||||||
|
:selected-arrangement-id="getSelectedArrangementId(arrangementDialogItem)"
|
||||||
|
@close="arrangementDialogItem = null"
|
||||||
|
@arrangement-selected="onArrangementSelected"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Sticky bottom action bar -->
|
<!-- Sticky bottom action bar -->
|
||||||
<div data-testid="service-edit-action-bar" class="fixed bottom-0 left-0 right-0 z-30">
|
<div data-testid="service-edit-action-bar" class="fixed bottom-0 left-0 right-0 z-30">
|
||||||
<div class="border-t border-gray-200 bg-white/90 backdrop-blur-xl shadow-[0_-2px_12px_rgba(0,0,0,0.08)]">
|
<div class="border-t border-gray-200 bg-white/90 backdrop-blur-xl shadow-[0_-2px_12px_rgba(0,0,0,0.08)]">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue