feat(settings): add LabelImport, MacroImport, MacroAssignments, MacroPicker, LabelPicker components

This commit is contained in:
Thorsten Bus 2026-05-04 00:25:57 +02:00
parent c1cb9bf820
commit f494a8a0d7
6 changed files with 751 additions and 28 deletions

View file

@ -0,0 +1,90 @@
<script setup>
import { computed, ref } from 'vue'
const props = defineProps({
labels: { type: Array, default: () => [] },
disabled: { type: Boolean, default: false },
})
const model = defineModel({ type: Number, default: null })
const search = ref('')
const isOpen = ref(false)
const filtered = computed(() =>
props.labels.filter((l) => l.name.toLowerCase().includes(search.value.toLowerCase())),
)
const selected = computed(() => props.labels.find((l) => l.id === model.value))
function select(label) {
model.value = label.id
search.value = ''
isOpen.value = false
}
function open() {
if (!props.disabled) isOpen.value = true
}
function close() {
setTimeout(() => {
isOpen.value = false
}, 150)
}
</script>
<template>
<div class="relative">
<div
class="flex cursor-pointer items-center gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm"
:class="{ 'cursor-not-allowed opacity-50': disabled }"
@click="open"
data-testid="label-picker-trigger"
>
<span
v-if="selected?.color"
class="h-4 w-4 shrink-0 rounded"
:style="{ backgroundColor: selected.color }"
/>
<span class="flex-1 truncate text-gray-700">
{{ selected ? selected.name : 'Label auswählen...' }}
</span>
</div>
<div
v-if="isOpen"
class="absolute z-50 mt-1 w-full rounded-lg border border-gray-200 bg-white shadow-lg"
data-testid="label-picker-dropdown"
>
<div class="border-b border-gray-100 p-2">
<input
v-model="search"
type="text"
placeholder="Label suchen..."
class="w-full rounded border-gray-300 text-sm"
autofocus
@blur="close"
/>
</div>
<div class="max-h-48 overflow-y-auto">
<button
v-for="label in filtered"
:key="label.id"
class="flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-amber-50"
:class="label.hidden_at ? 'text-gray-400' : 'text-gray-700'"
:data-testid="'label-option-' + label.id"
@click="select(label)"
>
<span
class="h-3 w-3 shrink-0 rounded"
:style="label.color ? { backgroundColor: label.color } : { backgroundColor: '#ccc' }"
/>
<span class="truncate">{{ label.name }}{{ label.hidden_at ? ' (deaktiviert)' : '' }}</span>
</button>
<div v-if="filtered.length === 0" class="px-3 py-4 text-center text-sm text-gray-400">
Kein Label gefunden
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,116 @@
<script setup>
import { computed, ref } from 'vue'
const props = defineProps({
macros: { type: Array, default: () => [] },
collections: { type: Array, default: () => [] },
disabled: { type: Boolean, default: false },
})
const model = defineModel({ type: Number, default: null })
const search = ref('')
const isOpen = ref(false)
const filteredMacros = computed(() => {
const q = search.value.toLowerCase()
return props.macros.filter((m) => m.name.toLowerCase().includes(q))
})
const groupedMacros = computed(() => {
const groups = {}
props.collections.forEach((c) => {
groups[c.name] = []
})
groups['Ohne Sammlung'] = []
filteredMacros.value.forEach((m) => {
const coll = props.collections.find((c) => c.macros?.some((cm) => cm.id === m.id))
const key = coll?.name ?? 'Ohne Sammlung'
if (!groups[key]) groups[key] = []
groups[key].push(m)
})
return groups
})
const selectedMacro = computed(() => props.macros.find((m) => m.id === model.value))
function select(macro) {
model.value = macro.id
search.value = ''
isOpen.value = false
}
function open() {
if (!props.disabled) isOpen.value = true
}
function close() {
setTimeout(() => {
isOpen.value = false
}, 150)
}
</script>
<template>
<div class="relative">
<div
class="flex cursor-pointer items-center gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm"
:class="{ 'cursor-not-allowed opacity-50': disabled }"
@click="open"
data-testid="macro-picker-trigger"
>
<span
v-if="selectedMacro?.color"
class="h-4 w-4 shrink-0 rounded"
:style="{ backgroundColor: selectedMacro.color }"
/>
<span class="flex-1 truncate text-gray-700">
{{ selectedMacro ? selectedMacro.name : 'Makro auswählen...' }}
</span>
</div>
<div
v-if="isOpen"
class="absolute z-50 mt-1 w-full rounded-lg border border-gray-200 bg-white shadow-lg"
data-testid="macro-picker-dropdown"
>
<div class="border-b border-gray-100 p-2">
<input
v-model="search"
type="text"
placeholder="Makro suchen..."
class="w-full rounded border-gray-300 text-sm"
data-testid="macro-picker-search"
autofocus
@blur="close"
/>
</div>
<div class="max-h-64 overflow-y-auto">
<template v-for="(group, name) in groupedMacros" :key="name">
<div
v-if="group.length > 0"
class="bg-gray-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-gray-400"
>
{{ name }}
</div>
<button
v-for="macro in group"
:key="macro.id"
class="flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-amber-50"
:class="macro.hidden_at ? 'text-gray-400' : 'text-gray-700'"
:data-testid="'macro-option-' + macro.id"
@click="select(macro)"
>
<span
class="h-3 w-3 shrink-0 rounded"
:style="macro.color ? { backgroundColor: macro.color } : { backgroundColor: '#ccc' }"
/>
<span class="truncate">{{ macro.name }}{{ macro.hidden_at ? ' (deaktiviert)' : '' }}</span>
</button>
</template>
<div v-if="filteredMacros.length === 0" class="px-3 py-4 text-center text-sm text-gray-400">
Kein Makro gefunden
</div>
</div>
</div>
</div>
</template>

View file

@ -1,6 +1,9 @@
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import AgendaSettings from './Settings/AgendaSettings.vue'
import LabelImport from './Settings/LabelImport.vue'
import MacroAssignments from './Settings/MacroAssignments.vue'
import MacroImport from './Settings/MacroImport.vue'
import { Head } from '@inertiajs/vue3'
import { onMounted, ref } from 'vue'
@ -92,37 +95,29 @@ function switchSubmenu(key) {
<!-- Content panel -->
<div class="flex-1 p-6" data-testid="settings-active-panel">
<!-- Makro-Zuweisungen filled in by T3.5 -->
<div v-if="activeSubmenu === 'assignments'">
<h3 class="mb-4 text-sm font-semibold text-gray-900">
Globale Makro-Zuweisungen
</h3>
<p class="text-sm text-gray-500">
Hier werden die globalen Makro-Zuweisungen konfiguriert.
</p>
</div>
<MacroAssignments
v-if="activeSubmenu === 'assignments'"
:assignments="assignments"
:macros="macros"
:labels="labels"
:collections="collections"
@switch-submenu="switchSubmenu"
/>
<!-- Makro-Import filled in by T3.4 -->
<div v-if="activeSubmenu === 'macros'">
<h3 class="mb-4 text-sm font-semibold text-gray-900">
Makro-Import
</h3>
<p class="text-sm text-gray-500">
Importiere eine ProPresenter Makro-Datei.
</p>
</div>
<MacroImport
v-if="activeSubmenu === 'macros'"
:macros="macros"
:collections="collections"
:last_macros_import="last_macros_import"
@switch-submenu="switchSubmenu"
/>
<!-- Label-Import filled in by T3.3 -->
<div v-if="activeSubmenu === 'labels'">
<h3 class="mb-4 text-sm font-semibold text-gray-900">
Label-Import
</h3>
<p class="text-sm text-gray-500">
Importiere eine ProPresenter Labels-Datei.
</p>
</div>
<LabelImport
v-if="activeSubmenu === 'labels'"
:labels="labels"
:last_labels_import="last_labels_import"
/>
<!-- Agenda -->
<AgendaSettings
v-if="activeSubmenu === 'agenda'"
:settings="settings"

View file

@ -0,0 +1,138 @@
<script setup>
import { computed, ref } from 'vue'
import { route } from 'ziggy-js'
const props = defineProps({
labels: { type: Array, default: () => [] },
last_labels_import: { type: Object, default: () => ({}) },
})
const uploading = ref(false)
const result = ref(null)
const error = ref(null)
const sortedLabels = computed(() => [...props.labels].sort((a, b) => a.name.localeCompare(b.name)))
async function handleFileChange(event) {
const file = event.target.files[0]
if (!file) return
uploading.value = true
error.value = null
result.value = null
const form = new FormData()
form.append('file', file)
try {
const res = await fetch(route('settings.labels.import'), {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-XSRF-TOKEN': decodeURIComponent(document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? ''),
},
body: form,
})
if (!res.ok) {
const data = await res.json()
error.value = data.message || 'Import fehlgeschlagen'
return
}
result.value = await res.json()
event.target.value = ''
window.location.reload()
} catch {
error.value = 'Netzwerkfehler beim Upload'
} finally {
uploading.value = false
}
}
</script>
<template>
<div>
<h3 class="mb-1 text-sm font-semibold text-gray-900">Label-Import</h3>
<p class="mb-1 text-xs text-gray-500">
Diese Datei findest du im ProPresenter-Ordner unter <strong>Configuration</strong>.
<span class="group relative inline-block">
<svg
class="ml-1 inline h-3.5 w-3.5 cursor-help text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="absolute bottom-full left-0 z-10 mb-2 hidden w-72 rounded-lg border border-gray-200 bg-white p-3 text-xs text-gray-600 shadow-lg group-hover:block">
<strong>macOS:</strong> ~/Library/Application Support/RenewedVision/ProPresenter/Configuration/Labels<br><br>
<strong>Windows:</strong> %APPDATA%\RenewedVision\ProPresenter\Configuration\Labels
</span>
</span>
</p>
<div v-if="last_labels_import?.at" class="mb-4 text-xs text-gray-400">
Letzter Import: {{ last_labels_import.at }}
<span v-if="last_labels_import.filename">({{ last_labels_import.filename }})</span>
</div>
<label
class="mb-4 flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-gray-200 bg-gray-50 p-6 transition-colors hover:border-amber-300 hover:bg-amber-50"
data-testid="labels-upload-area"
>
<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 importiert...' : 'Labels-Datei auswählen oder hierher ziehen' }}
</span>
<input
type="file"
class="hidden"
:disabled="uploading"
data-testid="labels-file-input"
@change="handleFileChange"
/>
</label>
<div
v-if="error"
class="mb-4 rounded-lg bg-red-50 p-3 text-sm text-red-700"
data-testid="labels-import-error"
>
{{ error }}
</div>
<div
v-if="result"
class="mb-4 rounded-lg bg-green-50 p-3 text-sm text-green-700"
data-testid="labels-import-summary"
>
<strong>Import abgeschlossen:</strong>
{{ result.new }} neue Labels importiert, {{ result.updated }} bestehende Labels aktualisiert.
Insgesamt {{ result.total }} Labels in Datei.
</div>
<div v-if="sortedLabels.length > 0">
<h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500">
Label-Bibliothek ({{ sortedLabels.length }})
</h4>
<div class="divide-y divide-gray-100 rounded-lg border border-gray-100">
<div
v-for="label in sortedLabels"
:key="label.id"
class="flex items-center gap-3 px-3 py-2 text-sm"
:data-testid="'labels-registry-row-' + label.name"
>
<span
class="h-4 w-4 shrink-0 rounded border border-gray-200"
:style="label.color ? { backgroundColor: label.color } : { backgroundColor: '#e5e7eb' }"
/>
<span class="flex-1 text-gray-700">{{ label.name }}</span>
<span v-if="label.color" class="font-mono text-xs text-gray-400">{{ label.color }}</span>
</div>
</div>
</div>
<p v-else class="text-sm text-gray-400">Noch keine Labels importiert.</p>
</div>
</template>

View file

@ -0,0 +1,203 @@
<script setup>
import LabelPicker from '@/Components/LabelPicker.vue'
import MacroPicker from '@/Components/MacroPicker.vue'
import { router } from '@inertiajs/vue3'
import { reactive } from 'vue'
import { route } from 'ziggy-js'
const props = defineProps({
assignments: { type: Array, default: () => [] },
macros: { type: Array, default: () => [] },
labels: { type: Array, default: () => [] },
collections: { type: Array, default: () => [] },
})
const parts = [
{ key: 'information', label: 'Informationen' },
{ key: 'moderation', label: 'Moderation' },
{ key: 'sermon', label: 'Predigt' },
{ key: 'song', label: 'Lieder' },
{ key: 'agenda_item', label: 'Agenda-Items' },
]
const positions = [
{ key: 'all_slides', label: 'Alle Folien' },
{ key: 'first_slide', label: 'Erste Folie' },
{ key: 'last_slide', label: 'Letzte Folie' },
{ key: 'by_label', label: 'Nach Label' },
]
function assignmentsForPart(partKey) {
return props.assignments.filter((a) => a.part_type === partKey)
}
function positionsForPart(partKey) {
if (partKey === 'song') return positions
return positions.filter((p) => p.key !== 'by_label')
}
const adding = reactive({})
const newAssignment = reactive({})
function startAdd(partKey) {
adding[partKey] = true
newAssignment[partKey] = { macro_id: null, position: 'all_slides', label_id: null }
}
function cancelAdd(partKey) {
adding[partKey] = false
}
async function saveAssignment(partKey) {
const data = newAssignment[partKey]
if (!data.macro_id) return
await fetch(route('settings.macro-assignments.store'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-XSRF-TOKEN': decodeURIComponent(document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? ''),
},
body: JSON.stringify({
part_type: partKey,
macro_id: data.macro_id,
position: data.position,
label_id: data.position === 'by_label' ? data.label_id : null,
order: assignmentsForPart(partKey).length,
}),
})
adding[partKey] = false
router.reload({ preserveScroll: true })
}
async function deleteAssignment(id) {
await fetch(route('settings.macro-assignments.destroy', id), {
method: 'DELETE',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-XSRF-TOKEN': decodeURIComponent(document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? ''),
},
})
router.reload({ preserveScroll: true })
}
function positionLabel(pos) {
return positions.find((p) => p.key === pos)?.label ?? pos
}
</script>
<template>
<div>
<h3 class="mb-1 text-sm font-semibold text-gray-900">Globale Makro-Zuweisungen</h3>
<p class="mb-4 text-xs text-gray-500">
Diese Zuweisungen gelten für alle Gottesdienste. Pro Gottesdienst können sie überschrieben werden.
</p>
<div class="space-y-6">
<div v-for="part in parts" :key="part.key">
<div class="mb-2 flex items-center justify-between">
<h4 class="text-sm font-medium text-gray-700">{{ part.label }}</h4>
<span class="text-xs text-gray-400">{{ assignmentsForPart(part.key).length }} Zuweisungen</span>
</div>
<div class="space-y-1">
<div
v-for="a in assignmentsForPart(part.key)"
:key="a.id"
class="flex items-center gap-2 rounded-lg border border-gray-100 bg-gray-50 px-3 py-2 text-sm"
:data-testid="'assignment-card-' + a.id"
>
<span
v-if="a.macro?.color"
class="h-3 w-3 shrink-0 rounded"
:style="{ backgroundColor: a.macro.color }"
/>
<span class="flex-1 text-gray-700">{{ a.macro?.name }}</span>
<span class="text-xs text-gray-400">{{ positionLabel(a.position) }}</span>
<span
v-if="a.position === 'by_label' && a.label"
class="text-xs text-gray-500"
>
{{ a.label.name }}
</span>
<span
v-if="a.macro?.hidden_at"
class="rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700"
data-testid="warning-hidden-macro"
>
Makro deaktiviert
</span>
<button
class="ml-2 text-gray-400 hover:text-red-500"
:data-testid="'delete-assignment-' + a.id"
@click="deleteAssignment(a.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>
<div
v-if="adding[part.key]"
class="mt-2 space-y-2 rounded-lg border border-amber-200 bg-amber-50 p-3"
>
<MacroPicker
v-model="newAssignment[part.key].macro_id"
:macros="macros"
:collections="collections"
/>
<div class="flex flex-wrap gap-3">
<label
v-for="pos in positionsForPart(part.key)"
:key="pos.key"
class="flex items-center gap-1.5 text-xs text-gray-700"
>
<input
type="radio"
:value="pos.key"
v-model="newAssignment[part.key].position"
class="text-amber-500"
/>
{{ pos.label }}
</label>
</div>
<LabelPicker
v-if="newAssignment[part.key]?.position === 'by_label'"
v-model="newAssignment[part.key].label_id"
:labels="labels"
/>
<div class="flex gap-2">
<button
class="rounded-lg bg-amber-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-600"
:data-testid="'save-assignment-' + part.key"
@click="saveAssignment(part.key)"
>
Hinzufügen
</button>
<button
class="rounded-lg border border-gray-200 px-3 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-50"
@click="cancelAdd(part.key)"
>
Abbrechen
</button>
</div>
</div>
<button
v-else
class="mt-1 flex items-center gap-1 rounded-lg border border-dashed border-gray-200 px-3 py-1.5 text-xs text-gray-500 transition-colors hover:border-amber-300 hover:text-amber-600"
:data-testid="'add-assignment-' + part.key"
@click="startAdd(part.key)"
>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
Zuweisung hinzufügen
</button>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,181 @@
<script setup>
import { computed, ref } from 'vue'
import { route } from 'ziggy-js'
const props = defineProps({
macros: { type: Array, default: () => [] },
collections: { type: Array, default: () => [] },
last_macros_import: { type: Object, default: () => ({}) },
})
const emit = defineEmits(['switch-submenu'])
const uploading = ref(false)
const result = ref(null)
const error = ref(null)
const selectedCollection = ref(null)
const filteredMacros = computed(() => {
if (!selectedCollection.value) return props.macros
const coll = props.collections.find((c) => c.id === selectedCollection.value)
if (!coll) return props.macros
const ids = coll.macros?.map((m) => m.id) ?? []
return props.macros.filter((m) => ids.includes(m.id))
})
async function handleFileChange(event) {
const file = event.target.files[0]
if (!file) return
uploading.value = true
error.value = null
result.value = null
const form = new FormData()
form.append('file', file)
try {
const res = await fetch(route('settings.macros.import'), {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-XSRF-TOKEN': decodeURIComponent(document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? ''),
},
body: form,
})
if (!res.ok) {
const data = await res.json()
error.value = data.message || 'Import fehlgeschlagen'
return
}
result.value = await res.json()
event.target.value = ''
window.location.reload()
} catch {
error.value = 'Netzwerkfehler beim Upload'
} finally {
uploading.value = false
}
}
</script>
<template>
<div>
<h3 class="mb-1 text-sm font-semibold text-gray-900">Makro-Import</h3>
<p class="mb-1 text-xs text-gray-500">
Diese Datei findest du im ProPresenter-Ordner unter <strong>Configuration</strong>.
<span class="group relative inline-block">
<svg
class="ml-1 inline h-3.5 w-3.5 cursor-help text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="absolute bottom-full left-0 z-10 mb-2 hidden w-72 rounded-lg border border-gray-200 bg-white p-3 text-xs text-gray-600 shadow-lg group-hover:block">
<strong>macOS:</strong> ~/Library/Application Support/RenewedVision/ProPresenter/Configuration/Macros<br><br>
<strong>Windows:</strong> %APPDATA%\RenewedVision\ProPresenter\Configuration\Macros
</span>
</span>
</p>
<div v-if="last_macros_import?.at" class="mb-4 text-xs text-gray-400">
Letzter Import: {{ last_macros_import.at }}
<span v-if="last_macros_import.filename">({{ last_macros_import.filename }})</span>
</div>
<label
class="mb-4 flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed border-gray-200 bg-gray-50 p-6 transition-colors hover:border-amber-300 hover:bg-amber-50"
data-testid="macros-upload-area"
>
<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 importiert...' : 'Makro-Datei auswählen oder hierher ziehen' }}
</span>
<input
type="file"
class="hidden"
:disabled="uploading"
data-testid="macros-file-input"
@change="handleFileChange"
/>
</label>
<div
v-if="error"
class="mb-4 rounded-lg bg-red-50 p-3 text-sm text-red-700"
data-testid="macros-import-error"
>
{{ error }}
</div>
<div
v-if="result"
class="mb-4 space-y-1 rounded-lg bg-green-50 p-3 text-sm text-green-700"
data-testid="macros-import-summary"
>
<p><strong>Import abgeschlossen:</strong></p>
<p>{{ result.stats.new }} neue Makros importiert</p>
<p>{{ result.stats.updated }} bestehende Makros aktualisiert</p>
<p>{{ result.stats.disabled }} Makros deaktiviert (nicht mehr in Datei vorhanden)</p>
<p v-if="result.stats.re_enabled > 0">{{ result.stats.re_enabled }} Makros wieder aktiviert</p>
</div>
<div
v-if="result?.warnings?.length > 0"
class="mb-4 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800"
data-testid="macros-import-warnings"
>
<p class="mb-2 font-semibold"> Achtung: Folgende deaktivierte Makros sind noch zugewiesen:</p>
<ul class="space-y-1 text-xs">
<li v-for="w in result.warnings" :key="w.macro_uuid">
{{ w.macro_name }} (Bereich: {{ w.part_type }})
</li>
</ul>
<button
class="mt-2 text-xs text-amber-700 underline"
@click="emit('switch-submenu', 'assignments')"
>
Zu den Makro-Zuweisungen
</button>
</div>
<div v-if="macros.length > 0">
<div class="mb-2 flex items-center gap-2">
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
Makro-Bibliothek ({{ macros.length }})
</h4>
<select
v-model="selectedCollection"
class="ml-auto rounded border-gray-300 text-xs"
data-testid="macros-collection-filter"
>
<option :value="null">Alle Sammlungen</option>
<option v-for="c in collections" :key="c.id" :value="c.id">{{ c.name }}</option>
</select>
</div>
<div class="divide-y divide-gray-100 rounded-lg border border-gray-100">
<div
v-for="macro in filteredMacros"
:key="macro.id"
class="flex items-center gap-3 px-3 py-2 text-sm"
:class="macro.hidden_at ? 'opacity-50' : ''"
>
<span
class="h-4 w-4 shrink-0 rounded border border-gray-200"
:style="macro.color ? { backgroundColor: macro.color } : { backgroundColor: '#e5e7eb' }"
/>
<span class="flex-1 text-gray-700">
{{ macro.name }}{{ macro.hidden_at ? ' (deaktiviert)' : '' }}
</span>
<span class="text-xs text-gray-400">{{ macro.action_count }} Aktionen</span>
</div>
</div>
</div>
<p v-else class="text-sm text-gray-400">Noch keine Makros importiert.</p>
</div>
</template>