Simplify ArrangementConfigurator: replace color pickers with compact pills, add click-to-add from pool, use watcher-based auto-select for new/cloned arrangements, remove group_colors from save payload. Enhance SongsBlock preview: color-coded group headers with tinted backgrounds, PDF download button inside preview modal, .pro download link per matched song, show DB ccli_id with fallback to CTS ccli_id. Fix Modal z-index for nested dialogs. Fix SlideUploader duplicate upload on watch by adding deep option and upload guard. Expand API log detail sections by default and increase JSON tree depth. Convert song download button from emit to direct .pro download link.
271 lines
8.4 KiB
Vue
271 lines
8.4 KiB
Vue
<script setup>
|
||
import { computed, ref, watch } from 'vue'
|
||
import { router } from '@inertiajs/vue3'
|
||
import { VueDraggable } from 'vue-draggable-plus'
|
||
|
||
const props = defineProps({
|
||
songId: {
|
||
type: Number,
|
||
required: true,
|
||
},
|
||
arrangements: {
|
||
type: Array,
|
||
required: true,
|
||
},
|
||
availableGroups: {
|
||
type: Array,
|
||
required: true,
|
||
},
|
||
selectedArrangementId: {
|
||
type: Number,
|
||
default: null,
|
||
},
|
||
})
|
||
|
||
const emit = defineEmits(['arrangement-selected'])
|
||
|
||
const selectedId = ref(
|
||
props.selectedArrangementId ?? props.arrangements.find((item) => item.is_default)?.id ?? props.arrangements[0]?.id ?? null,
|
||
)
|
||
|
||
const arrangementGroups = ref([])
|
||
const poolGroups = ref([])
|
||
|
||
const selectedArrangement = computed(() =>
|
||
props.arrangements.find((arrangement) => arrangement.id === Number(selectedId.value)) ?? null,
|
||
)
|
||
|
||
watch(
|
||
() => props.availableGroups,
|
||
(groups) => {
|
||
poolGroups.value = groups.map((group) => ({ ...group }))
|
||
},
|
||
{ immediate: true, deep: true },
|
||
)
|
||
|
||
watch(
|
||
selectedArrangement,
|
||
(arrangement) => {
|
||
arrangementGroups.value = arrangement?.groups?.map((group) => ({ ...group })) ?? []
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
|
||
watch(
|
||
selectedId,
|
||
(arrangementId) => {
|
||
emit('arrangement-selected', Number(arrangementId))
|
||
},
|
||
)
|
||
|
||
const groupPillStyle = (group) => ({
|
||
backgroundColor: group.color,
|
||
})
|
||
|
||
const pendingAutoSelect = ref(false)
|
||
|
||
watch(
|
||
() => props.arrangements.length,
|
||
(newLen, oldLen) => {
|
||
if (pendingAutoSelect.value && newLen > oldLen) {
|
||
const newest = props.arrangements.reduce((a, b) => a.id > b.id ? a : b)
|
||
selectedId.value = newest.id
|
||
pendingAutoSelect.value = false
|
||
}
|
||
},
|
||
)
|
||
|
||
const addArrangement = () => {
|
||
const name = window.prompt('Name des neuen Arrangements')
|
||
if (!name) return
|
||
pendingAutoSelect.value = true
|
||
router.post(`/songs/${props.songId}/arrangements`, { name }, { preserveScroll: true })
|
||
}
|
||
|
||
const cloneArrangement = () => {
|
||
if (!selectedArrangement.value) 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 })
|
||
}
|
||
|
||
const addGroupFromPool = (group) => {
|
||
arrangementGroups.value.push({ ...group })
|
||
saveArrangement()
|
||
}
|
||
|
||
const removeGroupAt = (index) => {
|
||
arrangementGroups.value.splice(index, 1)
|
||
saveArrangement()
|
||
}
|
||
|
||
const saveArrangement = () => {
|
||
if (!selectedArrangement.value) {
|
||
return
|
||
}
|
||
|
||
router.put(
|
||
`/arrangements/${selectedArrangement.value.id}`,
|
||
{
|
||
groups: arrangementGroups.value.map((group, index) => ({
|
||
song_group_id: group.id,
|
||
order: index + 1,
|
||
})),
|
||
},
|
||
{
|
||
preserveScroll: true,
|
||
preserveState: true,
|
||
},
|
||
)
|
||
}
|
||
|
||
const deleteArrangement = () => {
|
||
if (!selectedArrangement.value) {
|
||
return
|
||
}
|
||
|
||
router.delete(`/arrangements/${selectedArrangement.value.id}`, {
|
||
preserveScroll: true,
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div data-testid="arrangement-configurator" class="space-y-4 rounded-lg border border-gray-200 bg-white p-4">
|
||
<div class="flex flex-wrap items-end gap-3">
|
||
<div class="min-w-64 flex-1">
|
||
<label
|
||
for="arrangement-select"
|
||
class="mb-1 block text-sm font-medium text-gray-700"
|
||
>
|
||
Arrangement
|
||
</label>
|
||
<select
|
||
data-testid="arrangement-select"
|
||
id="arrangement-select"
|
||
v-model="selectedId"
|
||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||
>
|
||
<option
|
||
v-for="arrangement in arrangements"
|
||
:key="arrangement.id"
|
||
:value="arrangement.id"
|
||
>
|
||
{{ arrangement.name }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<button
|
||
data-testid="arrangement-add-button"
|
||
type="button"
|
||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-700"
|
||
@click="addArrangement"
|
||
>
|
||
Hinzufügen
|
||
</button>
|
||
|
||
<button
|
||
data-testid="arrangement-clone-button"
|
||
type="button"
|
||
class="rounded-md bg-slate-700 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-slate-800"
|
||
@click="cloneArrangement"
|
||
>
|
||
Klonen
|
||
</button>
|
||
|
||
<button
|
||
data-testid="arrangement-delete-button"
|
||
type="button"
|
||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-red-700"
|
||
@click="deleteArrangement"
|
||
>
|
||
Löschen
|
||
</button>
|
||
</div>
|
||
|
||
<div class="grid gap-4 lg:grid-cols-2">
|
||
<div class="space-y-1">
|
||
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||
Verfügbare Gruppen
|
||
</h4>
|
||
|
||
<VueDraggable
|
||
v-model="poolGroups"
|
||
:group="{ name: 'song-groups', pull: 'clone', put: false }"
|
||
:sort="false"
|
||
ghost-class="drag-ghost"
|
||
chosen-class="drag-chosen"
|
||
drag-class="drag-active"
|
||
class="flex flex-wrap gap-1.5 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-2"
|
||
>
|
||
<span
|
||
v-for="group in poolGroups"
|
||
:key="group.id"
|
||
class="inline-flex cursor-grab rounded-full px-2.5 py-0.5 text-xs font-semibold text-white transition-opacity hover:opacity-80"
|
||
:style="groupPillStyle(group)"
|
||
@click="addGroupFromPool(group)"
|
||
>
|
||
{{ group.name }}
|
||
</span>
|
||
</VueDraggable>
|
||
</div>
|
||
|
||
<div class="space-y-1">
|
||
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||
Gruppenfolge
|
||
</h4>
|
||
|
||
<VueDraggable
|
||
v-model="arrangementGroups"
|
||
:group="{ name: 'song-groups', pull: true, put: true }"
|
||
ghost-class="drag-ghost"
|
||
chosen-class="drag-chosen"
|
||
drag-class="drag-active"
|
||
class="flex min-h-10 flex-wrap gap-1.5 rounded-lg border border-gray-200 bg-gray-50 p-2"
|
||
@end="saveArrangement"
|
||
>
|
||
<span
|
||
v-for="(group, index) in arrangementGroups"
|
||
:key="`${group.id}-${index}`"
|
||
class="inline-flex cursor-grab items-center gap-1 rounded-full py-0.5 pl-2.5 pr-1 text-xs font-semibold text-white"
|
||
:style="groupPillStyle(group)"
|
||
>
|
||
{{ group.name }}
|
||
<button
|
||
data-testid="arrangement-remove-button"
|
||
type="button"
|
||
class="ml-0.5 flex h-4 w-4 items-center justify-center rounded-full text-white/70 transition-colors hover:bg-white/20 hover:text-white"
|
||
@click.stop="removeGroupAt(index)"
|
||
>
|
||
×
|
||
</button>
|
||
</span>
|
||
</VueDraggable>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
:deep(.drag-ghost) {
|
||
opacity: 0.4;
|
||
ring: 2px solid white;
|
||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 0 0 4px rgba(99, 102, 241, 0.6);
|
||
border-radius: 9999px;
|
||
}
|
||
|
||
:deep(.drag-chosen) {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
:deep(.drag-active) {
|
||
opacity: 1 !important;
|
||
transform: scale(1.1);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(255, 255, 255, 0.8);
|
||
border-radius: 9999px;
|
||
z-index: 50;
|
||
}
|
||
</style>
|