From b8b92f094ef07645c56ec9767f3b8f4f1f8c8c22 Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Sun, 29 Mar 2026 17:41:26 +0200 Subject: [PATCH] feat(ui): add MASTER arrangement, fix slide upload/thumbnail bugs, add slide preview with navigation - Add virtual MASTER arrangement to all songs (read-only, shows all groups in song order, clonable) - Fix drop zone staying open after slide upload and thumbnail 403 (double slides/ path) - Add click-to-preview overlay with download button, prev/next navigation, and slide counter - Add X delete button with confirmation dialog and hover tooltip preview on agenda thumbnails - Fix arrangement select not updating after add/clone (emit + re-fetch pattern) - Move InformationBlock below agenda; announcement row scrolls to it instead of showing upload - Create storage symlink (public/storage -> storage/app/public) --- resources/js/Components/AgendaItemRow.vue | 288 +++++++++++++++++- .../js/Components/ArrangementConfigurator.vue | 104 +++++-- resources/js/Components/ArrangementDialog.vue | 183 +++++++---- resources/js/Components/SongEditModal.vue | 1 + resources/js/Pages/Services/Edit.vue | 59 ++-- 5 files changed, 518 insertions(+), 117 deletions(-) diff --git a/resources/js/Components/AgendaItemRow.vue b/resources/js/Components/AgendaItemRow.vue index 525aed7..0518d02 100644 --- a/resources/js/Components/AgendaItemRow.vue +++ b/resources/js/Components/AgendaItemRow.vue @@ -1,6 +1,9 @@ diff --git a/resources/js/Components/ArrangementConfigurator.vue b/resources/js/Components/ArrangementConfigurator.vue index 9922513..066f42b 100644 --- a/resources/js/Components/ArrangementConfigurator.vue +++ b/resources/js/Components/ArrangementConfigurator.vue @@ -3,6 +3,8 @@ import { computed, ref, watch } from 'vue' import { router } from '@inertiajs/vue3' import { VueDraggable } from 'vue-draggable-plus' +const MASTER_ID = 'master' + const props = defineProps({ songId: { type: Number, @@ -17,22 +19,41 @@ const props = defineProps({ required: true, }, selectedArrangementId: { - type: Number, + type: [Number, String], default: null, }, }) -const emit = defineEmits(['arrangement-selected']) +const emit = defineEmits(['arrangement-selected', 'arrangements-changed']) + +// Virtual MASTER arrangement — always first, computed from availableGroups +const masterArrangement = computed(() => ({ + id: MASTER_ID, + name: 'MASTER', + is_default: false, + is_master: true, + groups: props.availableGroups.map((g) => ({ ...g })), +})) + +// All arrangements with MASTER prepended +const allArrangements = computed(() => [ + masterArrangement.value, + ...props.arrangements, +]) const selectedId = ref( - props.selectedArrangementId ?? props.arrangements.find((item) => item.is_default)?.id ?? props.arrangements[0]?.id ?? null, + props.selectedArrangementId ?? props.arrangements.find((item) => item.is_default)?.id ?? props.arrangements[0]?.id ?? MASTER_ID, ) +const isMasterSelected = computed(() => selectedId.value === MASTER_ID) + const arrangementGroups = ref([]) const poolGroups = ref([]) const selectedArrangement = computed(() => - props.arrangements.find((arrangement) => arrangement.id === Number(selectedId.value)) ?? null, + allArrangements.value.find((arrangement) => arrangement.id === selectedId.value) + ?? allArrangements.value.find((arrangement) => arrangement.id === Number(selectedId.value)) + ?? null, ) watch( @@ -54,7 +75,7 @@ watch( watch( selectedId, (arrangementId) => { - emit('arrangement-selected', Number(arrangementId)) + emit('arrangement-selected', arrangementId === MASTER_ID ? null : Number(arrangementId)) }, ) @@ -75,33 +96,60 @@ watch( }, ) -const addArrangement = () => { +function addArrangement() { const name = window.prompt('Name des neuen Arrangements') if (!name) return pendingAutoSelect.value = true - router.post(`/songs/${props.songId}/arrangements`, { name }, { preserveScroll: true }) + router.post(`/songs/${props.songId}/arrangements`, { name }, { + preserveScroll: true, + onSuccess: () => { + emit('arrangements-changed') + }, + }) } -const cloneArrangement = () => { +function cloneArrangement() { if (!selectedArrangement.value) return + + // Cloning from MASTER = creating a new arrangement (store() already uses all groups in master order) + if (isMasterSelected.value) { + const name = window.prompt('Name des neuen Arrangements', 'MASTER Kopie') + if (!name) return + pendingAutoSelect.value = true + router.post(`/songs/${props.songId}/arrangements`, { name }, { + preserveScroll: true, + onSuccess: () => { + emit('arrangements-changed') + }, + }) + return + } + const name = window.prompt('Name des neuen Arrangements', `${selectedArrangement.value.name} Kopie`) if (!name) return pendingAutoSelect.value = true - router.post(`/arrangements/${selectedArrangement.value.id}/clone`, { name }, { preserveScroll: true }) + router.post(`/arrangements/${selectedArrangement.value.id}/clone`, { name }, { + preserveScroll: true, + onSuccess: () => { + emit('arrangements-changed') + }, + }) } -const addGroupFromPool = (group) => { +function addGroupFromPool(group) { + if (isMasterSelected.value) return arrangementGroups.value.push({ ...group }) saveArrangement() } -const removeGroupAt = (index) => { +function removeGroupAt(index) { + if (isMasterSelected.value) return arrangementGroups.value.splice(index, 1) saveArrangement() } -const saveArrangement = () => { - if (!selectedArrangement.value) { +function saveArrangement() { + if (!selectedArrangement.value || isMasterSelected.value) { return } @@ -120,13 +168,16 @@ const saveArrangement = () => { ) } -const deleteArrangement = () => { - if (!selectedArrangement.value) { +function deleteArrangement() { + if (!selectedArrangement.value || isMasterSelected.value) { return } router.delete(`/arrangements/${selectedArrangement.value.id}`, { preserveScroll: true, + onSuccess: () => { + emit('arrangements-changed') + }, }) } @@ -148,11 +199,11 @@ const deleteArrangement = () => { class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" > @@ -178,7 +229,8 @@ const deleteArrangement = () => { - - -
+
diff --git a/resources/js/Components/SongEditModal.vue b/resources/js/Components/SongEditModal.vue index f948f96..42a2420 100644 --- a/resources/js/Components/SongEditModal.vue +++ b/resources/js/Components/SongEditModal.vue @@ -491,6 +491,7 @@ onUnmounted(() => { :song-id="songData.id" :arrangements="arrangements" :available-groups="availableGroups" + @arrangements-changed="fetchSong" /> diff --git a/resources/js/Pages/Services/Edit.vue b/resources/js/Pages/Services/Edit.vue index aba4b82..82e046c 100644 --- a/resources/js/Pages/Services/Edit.vue +++ b/resources/js/Pages/Services/Edit.vue @@ -65,10 +65,16 @@ function goBack() { router.get(route('services.index')) } +const informationBlockRef = ref(null) + function refreshPage() { router.reload({ preserveScroll: true }) } +function scrollToInfoBlock() { + informationBlockRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' }) +} + /* ── Agenda helpers ──────────────────────────────────────── */ const arrangementDialogItem = ref(null) @@ -349,34 +355,9 @@ async function downloadService() { - -
-
-
-
-
- - - -
-
-

Information

-

Info-Folien fuer alle kommenden Services

-
-
- -
-
-
-
-
+

Ablauf

@@ -427,6 +408,7 @@ async function downloadService() { :service-id="service.id" :service-date="service.date" @slides-updated="refreshPage" + @scroll-to-info="scrollToInfoBlock" /> @@ -434,6 +416,31 @@ async function downloadService() {
+ +
+
+
+
+
+ + + +
+
+

Information

+

Info-Folien für alle kommenden Services

+
+
+ +
+
+
+