From 44d0daf24690e06cd5e1023f135ae745146201b8 Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Mon, 2 Mar 2026 21:28:32 +0100 Subject: [PATCH] feat(ui): add finalize/reopen buttons to service edit page --- resources/js/Pages/Services/Edit.vue | 417 ++++++++++++++++++++++++--- 1 file changed, 384 insertions(+), 33 deletions(-) diff --git a/resources/js/Pages/Services/Edit.vue b/resources/js/Pages/Services/Edit.vue index 3e3b510..74fc7ce 100644 --- a/resources/js/Pages/Services/Edit.vue +++ b/resources/js/Pages/Services/Edit.vue @@ -32,6 +32,14 @@ const props = defineProps({ type: Array, default: () => [], }, + prevService: { + type: Object, + default: null, + }, + nextService: { + type: Object, + default: null, + }, }) const formattedDate = computed(() => { @@ -44,6 +52,15 @@ const formattedDate = computed(() => { }) }) +function formatShortDate(dateStr) { + if (!dateStr) return '' + return new Date(dateStr).toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }) +} + /* ── Collapsible block state ─────────────────────────────── */ const expandedBlocks = ref({ information: true, @@ -117,6 +134,160 @@ function blockBadgeLabel(key) { if (key === 'songs') return `${count} Song${count !== 1 ? 's' : ''}` return `${count} Folie${count !== 1 ? 'n' : ''}` } + +/* ── Finalize / Reopen / Download ───────────────────────── */ +const toastMessage = ref('') +const toastType = ref('info') +const confirmDialog = ref(false) +const confirmWarnings = ref([]) +const finalizing = ref(false) +const downloadAfterFinalize = ref(false) + +const isFinalized = computed(() => !!props.service.finalized_at) + +function formatDateTime(dateStr) { + if (!dateStr) return '' + const d = new Date(dateStr) + return d.toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) +} + +function showToast(message, type = 'info') { + toastMessage.value = message + toastType.value = type + setTimeout(() => { + toastMessage.value = '' + }, 3500) +} + +function toastClasses() { + if (toastType.value === 'success') return 'border-emerald-300 bg-emerald-50 text-emerald-700' + if (toastType.value === 'warning') return 'border-orange-300 bg-orange-50 text-orange-700' + return 'border-blue-300 bg-blue-50 text-blue-700' +} + +async function finalizeService(andDownload = false) { + if (andDownload) downloadAfterFinalize.value = true + finalizing.value = true + try { + const response = await fetch(route('services.finalize', props.service.id), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '', + }, + body: JSON.stringify({ confirmed: false }), + }) + const data = await response.json() + + if (data.needs_confirmation) { + confirmWarnings.value = data.warnings + confirmDialog.value = true + return + } + + props.service.finalized_at = new Date().toISOString() + showToast(data.success, 'success') + + if (downloadAfterFinalize.value) { + downloadAfterFinalize.value = false + await downloadService() + } + } finally { + finalizing.value = false + } +} + +async function confirmFinalize() { + finalizing.value = true + confirmDialog.value = false + try { + const response = await fetch(route('services.finalize', props.service.id), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '', + }, + body: JSON.stringify({ confirmed: true }), + }) + const data = await response.json() + + props.service.finalized_at = new Date().toISOString() + showToast(data.success, 'success') + + if (downloadAfterFinalize.value) { + downloadAfterFinalize.value = false + await downloadService() + } + } finally { + finalizing.value = false + confirmWarnings.value = [] + } +} + +function cancelFinalize() { + confirmDialog.value = false + confirmWarnings.value = [] + downloadAfterFinalize.value = false +} + +async function reopenService() { + try { + const response = await fetch(route('services.reopen', props.service.id), { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '', + }, + }) + if (response.ok) { + props.service.finalized_at = null + showToast('Service wurde wieder geöffnet.', 'success') + } else { + showToast('Fehler beim Öffnen.', 'warning') + } + } catch { + showToast('Fehler beim Öffnen.', 'warning') + } +} + +async function downloadService() { + try { + const response = await fetch(route('services.download', props.service.id), { + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '', + }, + }) + + if (!response.ok) { + const data = await response.json().catch(() => ({})) + showToast(data.message || 'Fehler beim Herunterladen.', 'warning') + return + } + + const disposition = response.headers.get('content-disposition') + const filenameMatch = disposition?.match(/filename="?([^"]+)"?/) + const filename = filenameMatch?.[1] || 'playlist.proplaylist' + + const blob = await response.blob() + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) + + showToast('Playlist wurde heruntergeladen.', 'success') + } catch { + showToast('Fehler beim Herunterladen.', 'warning') + } +}