Resolves a batch of bugs and feature requests across songs, services, settings and export: Songs & sections - Every song now carries permanent, empty, locked PREFIX (COPYRIGHT) and POSTFIX (BLANK) sections, deduplicated on import; locked sections cannot be edited or deleted via UI or API. - Song edit modal: explicit Speichern/Schließen with dirty-tracking, editable section headline (combobox + custom values), and a fix for the 419 CSRF errors after CCLI "Importieren & Bearbeiten" (token read fresh per request). - CCLI bookmarklet "Importieren & Bearbeiten" now opens the edit dialog. Service schedule & arrangements - Fixed assigned songs showing no sections (slides loaded for all arrangements, not just the default). - Added "Song entfernen / neu zuordnen" to reassign an assigned song. - Worship-leader arrangement is created/selected lazily when the arrangement dialog opens (only when not user-overridden); the leader is resolved from the "Lobpreis" agenda item, and manual create/clone names are prefixed with the leader name. Navigation - "/" redirects to the next upcoming service's edit page (or the list). - Service titles link to the edit page. Settings - Renamed "Makro-Import"/"Label-Import" menu items; fixed drag-and-drop imports (were downloading the dropped file); added label-import hint; made the panel scrollable. - Nametag now uses a single MacroPicker; added song prefix/postfix label defaults (COPYRIGHT #24B34C / BLANK #000000); new "Export-Dateien" menu to upload prefix/postfix .pro files added to every export. Export - Filenames/playlist names are date-first ("YYYY-MM-DD <Title>"). - Keyvisual slide only for the first content-less item after real content; all other content-less items render as headlines. - New "Vorschau herunterladen" for non-finalized services (filename and import name prefixed "Vorschau" with export timestamp). - Uploaded prefix/postfix .pro files wrap every export. Tests updated to the new behavior; full suite green (569 passed).
227 lines
9.6 KiB
Vue
227 lines
9.6 KiB
Vue
<script setup>
|
|
import { ref } from 'vue'
|
|
import { route } from 'ziggy-js'
|
|
|
|
const props = defineProps({
|
|
prefixFiles: { type: Array, default: () => [] },
|
|
postfixFiles: { type: Array, default: () => [] },
|
|
})
|
|
|
|
const prefixDragActive = ref(false)
|
|
const postfixDragActive = ref(false)
|
|
const uploading = ref(false)
|
|
const error = ref(null)
|
|
|
|
function getXsrfToken() {
|
|
return decodeURIComponent(document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? '')
|
|
}
|
|
|
|
async function uploadFiles(type, files) {
|
|
if (!files || files.length === 0) return
|
|
uploading.value = true
|
|
error.value = null
|
|
|
|
const form = new FormData()
|
|
form.append('type', type)
|
|
for (const file of files) {
|
|
form.append('files[]', file)
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(route('settings.export-pro-files.store'), {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'X-XSRF-TOKEN': getXsrfToken(),
|
|
},
|
|
body: form,
|
|
})
|
|
if (!res.ok) {
|
|
const data = await res.json()
|
|
error.value = data.message || 'Upload fehlgeschlagen'
|
|
return
|
|
}
|
|
window.location.reload()
|
|
} catch {
|
|
error.value = 'Netzwerkfehler beim Upload'
|
|
} finally {
|
|
uploading.value = false
|
|
}
|
|
}
|
|
|
|
function handlePrefixChange(event) {
|
|
const files = event.target.files
|
|
if (!files || files.length === 0) return
|
|
event.target.value = ''
|
|
uploadFiles('prefix', files)
|
|
}
|
|
|
|
function handlePostfixChange(event) {
|
|
const files = event.target.files
|
|
if (!files || files.length === 0) return
|
|
event.target.value = ''
|
|
uploadFiles('postfix', files)
|
|
}
|
|
|
|
function handlePrefixDrop(event) {
|
|
prefixDragActive.value = false
|
|
uploadFiles('prefix', event.dataTransfer.files)
|
|
}
|
|
|
|
function handlePostfixDrop(event) {
|
|
postfixDragActive.value = false
|
|
uploadFiles('postfix', event.dataTransfer.files)
|
|
}
|
|
|
|
async function deleteFile(id) {
|
|
try {
|
|
await fetch(route('settings.export-pro-files.destroy', id), {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'X-XSRF-TOKEN': getXsrfToken(),
|
|
},
|
|
})
|
|
window.location.reload()
|
|
} catch {
|
|
// silent fail
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-8">
|
|
<div>
|
|
<h3 class="mb-1 text-sm font-semibold text-gray-900">Export-Dateien</h3>
|
|
<p class="text-xs text-gray-500">
|
|
Diese .pro-Dateien werden bei jedem Export vorne (Prefix) bzw. hinten (Postfix) an den Gottesdienst angehängt.
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
v-if="error"
|
|
class="rounded-lg bg-red-50 p-3 text-sm text-red-700"
|
|
>
|
|
{{ error }}
|
|
</div>
|
|
|
|
<!-- Prefix section -->
|
|
<div>
|
|
<h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500">Prefix-Dateien</h4>
|
|
<p class="mb-3 text-xs text-gray-400">Werden vor dem Gottesdienst eingefügt.</p>
|
|
|
|
<label
|
|
class="mb-4 flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-6 transition-colors"
|
|
:class="prefixDragActive
|
|
? 'border-amber-400 bg-amber-50'
|
|
: 'border-gray-200 bg-gray-50 hover:border-amber-300 hover:bg-amber-50'"
|
|
data-testid="export-prefix-dropzone"
|
|
@dragover.prevent
|
|
@dragenter.prevent="prefixDragActive = true"
|
|
@dragleave.prevent="prefixDragActive = false"
|
|
@drop.prevent="handlePrefixDrop"
|
|
>
|
|
<svg class="mb-2 h-8 w-8 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
|
</svg>
|
|
<span class="text-sm text-gray-500">
|
|
{{ uploading ? 'Wird hochgeladen...' : '.pro-Dateien auswählen oder hierher ziehen' }}
|
|
</span>
|
|
<span class="mt-1 text-xs text-gray-400">Mehrere Dateien möglich</span>
|
|
<input
|
|
type="file"
|
|
class="hidden"
|
|
accept=".pro"
|
|
multiple
|
|
:disabled="uploading"
|
|
data-testid="export-prefix-file-input"
|
|
@change="handlePrefixChange"
|
|
/>
|
|
</label>
|
|
|
|
<div v-if="prefixFiles.length > 0" class="divide-y divide-gray-100 rounded-lg border border-gray-100">
|
|
<div
|
|
v-for="file in prefixFiles"
|
|
:key="file.id"
|
|
class="flex items-center gap-3 px-3 py-2 text-sm"
|
|
>
|
|
<svg class="h-4 w-4 shrink-0 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
</svg>
|
|
<span class="flex-1 truncate text-gray-700">{{ file.original_name }}</span>
|
|
<span class="text-xs text-gray-400">#{{ file.order + 1 }}</span>
|
|
<button
|
|
class="ml-2 text-gray-400 hover:text-red-500"
|
|
:data-testid="'export-pro-file-delete-' + file.id"
|
|
@click="deleteFile(file.id)"
|
|
>
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-sm text-gray-400">Noch keine Prefix-Dateien hochgeladen.</p>
|
|
</div>
|
|
|
|
<!-- Postfix section -->
|
|
<div>
|
|
<h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500">Postfix-Dateien</h4>
|
|
<p class="mb-3 text-xs text-gray-400">Werden nach dem Gottesdienst angehängt.</p>
|
|
|
|
<label
|
|
class="mb-4 flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-6 transition-colors"
|
|
:class="postfixDragActive
|
|
? 'border-amber-400 bg-amber-50'
|
|
: 'border-gray-200 bg-gray-50 hover:border-amber-300 hover:bg-amber-50'"
|
|
data-testid="export-postfix-dropzone"
|
|
@dragover.prevent
|
|
@dragenter.prevent="postfixDragActive = true"
|
|
@dragleave.prevent="postfixDragActive = false"
|
|
@drop.prevent="handlePostfixDrop"
|
|
>
|
|
<svg class="mb-2 h-8 w-8 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
|
</svg>
|
|
<span class="text-sm text-gray-500">
|
|
{{ uploading ? 'Wird hochgeladen...' : '.pro-Dateien auswählen oder hierher ziehen' }}
|
|
</span>
|
|
<span class="mt-1 text-xs text-gray-400">Mehrere Dateien möglich</span>
|
|
<input
|
|
type="file"
|
|
class="hidden"
|
|
accept=".pro"
|
|
multiple
|
|
:disabled="uploading"
|
|
data-testid="export-postfix-file-input"
|
|
@change="handlePostfixChange"
|
|
/>
|
|
</label>
|
|
|
|
<div v-if="postfixFiles.length > 0" class="divide-y divide-gray-100 rounded-lg border border-gray-100">
|
|
<div
|
|
v-for="file in postfixFiles"
|
|
:key="file.id"
|
|
class="flex items-center gap-3 px-3 py-2 text-sm"
|
|
>
|
|
<svg class="h-4 w-4 shrink-0 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
</svg>
|
|
<span class="flex-1 truncate text-gray-700">{{ file.original_name }}</span>
|
|
<span class="text-xs text-gray-400">#{{ file.order + 1 }}</span>
|
|
<button
|
|
class="ml-2 text-gray-400 hover:text-red-500"
|
|
:data-testid="'export-pro-file-delete-' + file.id"
|
|
@click="deleteFile(file.id)"
|
|
>
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-sm text-gray-400">Noch keine Postfix-Dateien hochgeladen.</p>
|
|
</div>
|
|
</div>
|
|
</template>
|