Fix #41
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 17s
Build and Push Docker Images / build-frontend (push) Successful in 5m56s

Redesign Textvorlagen
This commit is contained in:
2026-01-10 19:01:40 +01:00
parent 7f6ba99328
commit d901ebe365

View File

@@ -1,220 +1,352 @@
<script setup>
import { ref, onMounted } from 'vue'
const dataStore = useDataStore()
const toast = useToast()
defineShortcuts({
'+': () => {
editTemplateModalOpen.value = true
}
})
// useEntities Initialisierung für 'texttemplates'
const { select, create, update } = useEntities("texttemplates")
// --- State ---
const editTemplateModalOpen = ref(false)
const itemInfo = ref({})
const texttemplates = ref([])
const loading = ref(true)
const isSaving = ref(false)
const textareaRef = ref(null)
const setup = async () => {
texttemplates.value = (await useEntities("texttemplates").select()).filter(i => !i.archived)
loading.value = false
}
setup()
// Tabelle Expand State
const expand = ref({
openedRows: [],
row: {}
})
// --- Variablen Definitionen ---
const variableDefinitions = [
{ key: '{{vorname}}', label: 'Vorname', desc: 'Vorname des Kunden' },
{ key: '{{nachname}}', label: 'Nachname', desc: 'Nachname des Kunden' },
{ key: '{{anrede}}', label: 'Anrede', desc: 'Formelle Anrede' },
{ key: '{{titel}}', label: 'Titel', desc: 'Titel des Kunden' },
{ key: '{{zahlungsziel_in_tagen}}', label: 'Zahlungsziel', desc: 'In Tagen' },
{ key: '{{lohnkosten}}', label: 'Lohnkosten', desc: 'Ausgewiesene Lohnkosten' },
]
// --- Shortcuts ---
defineShortcuts({
'+': () => openModal()
})
// --- Data Fetching ---
const refreshData = async () => {
loading.value = true
try {
// select() filtert bereits archivierte Einträge, wenn dataType.isArchivable true ist
texttemplates.value = await select()
} catch (e) {
toast.add({ title: 'Fehler beim Laden', description: e.message, color: 'rose' })
} finally {
loading.value = false
}
}
// Initialer Load
onMounted(() => {
refreshData()
})
// --- Actions ---
const openModal = (item = null) => {
if (item) {
// Deep Copy um Reaktivitätsprobleme beim Abbrechen zu vermeiden
itemInfo.value = JSON.parse(JSON.stringify(item))
} else {
// Reset für Erstellen
itemInfo.value = {
name: '',
documentType: 'offer', // Default
pos: 'startText',
text: '',
default: false
}
}
editTemplateModalOpen.value = true
}
const insertVariable = (variableKey) => {
itemInfo.value.text = (itemInfo.value.text || '') + variableKey + ' '
}
const handleCreate = async () => {
isSaving.value = true
try {
// create(payload, noRedirect) -> Wir setzen noRedirect auf true
await create(itemInfo.value, true)
// Hinweis: Erfolgs-Toast kommt bereits aus useEntities
editTemplateModalOpen.value = false
await refreshData()
} catch (e) {
toast.add({ title: 'Fehler', description: 'Konnte nicht erstellt werden.', color: 'rose' })
} finally {
isSaving.value = false
}
}
const handleUpdate = async () => {
isSaving.value = true
try {
// update(id, payload, noRedirect) -> Wir setzen noRedirect auf true
await update(itemInfo.value.id, itemInfo.value, true)
// Hinweis: Erfolgs-Toast kommt bereits aus useEntities
editTemplateModalOpen.value = false
await refreshData()
} catch (e) {
toast.add({title: 'Fehler', description: 'Konnte nicht gespeichert werden.', color: 'rose'})
} finally {
isSaving.value = false
}
}
const handleArchive = async (row) => {
try {
// Wir nutzen update mit archived: true und noRedirect, um auf der Seite zu bleiben
await update(row.id, {archived: true}, true)
await refreshData()
} catch (e) {
toast.add({title: 'Fehler', description: 'Konnte nicht archiviert werden.', color: 'rose'})
}
}
// Helper für Labels (falls dataStore noch lädt oder Key fehlt)
const getDocLabel = (type) => {
return dataStore.documentTypesForCreation?.[type]?.label || type
}
</script>
<template>
<UDashboardNavbar
title="Text Vorlagen"
>
<UDashboardNavbar title="Text Vorlagen">
<template #right>
<UButton
@click="editTemplateModalOpen = true, itemInfo = {}"
icon="i-heroicons-plus"
@click="openModal()"
color="primary"
variant="solid"
>
+ Erstellen
Erstellen
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<UCard class="mx-5">
<template #header>
Variablen
</template>
<table>
<tr>
<th class="text-left">Variable</th>
<th class="text-left">Beschreibung</th>
</tr>
<tr>
<td>vorname</td>
<td>Vorname</td>
</tr>
<tr>
<td>nachname</td>
<td>Nachname</td>
</tr>
<tr>
<td>zahlungsziel_in_tagen</td>
<td>Zahlungsziel in Tagen</td>
</tr>
<tr>
<td>lohnkosten</td>
<td>Lohnkosten Verkauf</td>
</tr>
<tr>
<td>titel</td>
<td>Titel</td>
</tr>
<tr>
<td>anrede</td>
<td>Anrede</td>
</tr>
</table>
</UCard>
<UDashboardPanelContent>
<UAlert
icon="i-heroicons-information-circle"
color="primary"
variant="soft"
title="Platzhalter nutzen"
description="Nutzen Sie die Variablen im Editor, um dynamische Inhalte (wie Kundennamen) automatisch einzufügen."
class="mb-4 mx-5 mt-2"
/>
<UTable
class="mt-3"
:rows="texttemplates"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
:loading="loading"
v-model:expand="expand" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Textvorlagen anzuzeigen' }"
:columns="[{key:'name',label:'Name'},{key:'documentType',label:'Dokumententyp'},{key:'default',label:'Standard'},{key:'pos',label:'Position'}]"
v-model:expand="expand"
:empty-state="{ icon: 'i-heroicons-document-text', label: 'Keine Textvorlagen gefunden' }"
:columns="[
{ key: 'name', label: 'Bezeichnung' },
{ key: 'documentType', label: 'Verwendung' },
{ key: 'pos', label: 'Position' },
{ key: 'default', label: 'Standard' },
{ key: 'actions', label: '' }
]"
>
<template #documentType-data="{row}">
{{dataStore.documentTypesForCreation[row.documentType].label}}
<template #name-data="{ row }">
<span class="font-medium text-gray-900 dark:text-white">{{ row.name }}</span>
</template>
<template #default-data="{row}">
{{row.default ? "Ja" : "Nein"}}
<template #documentType-data="{ row }">
<UBadge color="gray" variant="soft">
{{ getDocLabel(row.documentType) }}
</UBadge>
</template>
<template #pos-data="{row}">
<span v-if="row.pos === 'startText'">Einleitung</span>
<span v-else-if="row.pos === 'endText'">Endtext</span>
<template #pos-data="{ row }">
<div class="flex items-center gap-2">
<UIcon
:name="row.pos === 'startText' ? 'i-heroicons-bars-arrow-down' : 'i-heroicons-bars-arrow-up'"
class="w-4 h-4 text-gray-500"
/>
<span>{{ row.pos === 'startText' ? 'Einleitung' : 'Endtext' }}</span>
</div>
</template>
<template #default-data="{ row }">
<UIcon v-if="row.default" name="i-heroicons-check-circle-20-solid" class="text-green-500"/>
<span v-else class="text-gray-400">-</span>
</template>
<template #actions-data="{ row }">
<UButton color="gray" variant="ghost" icon="i-heroicons-pencil-square" @click="openModal(row)"/>
</template>
<template #expand="{ row }">
<div class="p-4">
<p class="text-2xl">{{dataStore.documentTypesForCreation[row.documentType].label}}</p>
<p class="text-xl mt-3">{{row.pos === 'startText' ? 'Einleitung' : 'Ende'}}</p>
<p class="text-justify mt-3">{{row.text}}</p>
<UButton
class="mt-3 mr-3"
@click="itemInfo = row;
editTemplateModalOpen = true"
variant="outline"
>Bearbeiten</UButton>
<div class="p-6 bg-gray-50 dark:bg-gray-800/50 rounded-b-lg border-t border-gray-200 dark:border-gray-700">
<div class="mb-4">
<h4 class="text-sm font-bold uppercase text-gray-500 mb-1">Vorschau</h4>
<p class="text-gray-800 dark:text-gray-200 whitespace-pre-line p-3 bg-white dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-700 text-sm">
{{ row.text }}
</p>
</div>
<div class="flex justify-end gap-3">
<ButtonWithConfirm
color="rose"
variant="outline"
@confirmed="dataStore.updateItem('texttemplates',{id: row.id,archived: true}),
setup"
variant="soft"
icon="i-heroicons-archive-box"
@confirmed="handleArchive(row)"
>
<template #button>
Archivieren
</template>
<template #button>Archivieren</template>
<template #header>
<span class="text-md dark:text-whitetext-black font-bold">Archivieren bestätigen</span>
<span class="font-bold">Wirklich archivieren?</span>
</template>
Möchten Sie diesen Ausgangsbeleg wirklich archivieren?
Die Vorlage "{{ row.name }}" wird archiviert.
</ButtonWithConfirm>
<UButton
icon="i-heroicons-pencil"
@click="openModal(row)"
>
Bearbeiten
</UButton>
</div>
</div>
</template>
</UTable>
<!-- <div class="w-3/4 mx-auto mt-5">
<UCard
v-for="template in dataStore.texttemplates"
class="mb-3"
>
<p class="text-2xl">{{dataStore.documentTypesForCreation[template.documentType].label}}</p>
<p class="text-xl">{{template.pos === 'startText' ? 'Einleitung' : 'Ende'}}</p>
<p class="text-justify">{{template.text}}</p>
<UButton
@click="itemInfo = template;
editTemplateModalOpen = true"
icon="i-heroicons-pencil-solid"
variant="outline"
/>
</UCard>
</div>-->
</UDashboardPanelContent>
<UModal
v-model="editTemplateModalOpen"
>
<UCard class="h-full">
<UModal v-model="editTemplateModalOpen" :ui="{ width: 'sm:max-w-4xl' }">
<UCard>
<template #header>
{{itemInfo.id ? 'Vorlage bearbeiten' : 'Vorlage erstellen'}}
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold">
{{ itemInfo.id ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen' }}
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark" @click="editTemplateModalOpen = false"/>
</div>
</template>
<UForm class="h-full">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<UFormGroup
label="Name:"
>
<UInput
v-model="itemInfo.name"
/>
<div class="lg:col-span-2 space-y-4">
<UFormGroup label="Bezeichnung" required>
<UInput v-model="itemInfo.name" placeholder="z.B. Standard Angebotstext" icon="i-heroicons-tag"/>
</UFormGroup>
<UFormGroup
label="Dokumententyp:"
>
<div class="grid grid-cols-2 gap-4">
<UFormGroup label="Dokumententyp" required>
<USelectMenu
:options="Object.keys(dataStore.documentTypesForCreation).filter(i => i !== 'serialInvoices').map(i => {
return {
label: dataStore.documentTypesForCreation[i].label,
key: i
}
})"
option-attribute="label"
value-attribute="key"
v-model="itemInfo.documentType"
/>
</UFormGroup>
<UFormGroup
label="Position:"
>
<USelectMenu
:options="[{label:'Einleitung',key: 'startText'},{label:'Ende',key: 'endText'}]"
:options="Object.keys(dataStore.documentTypesForCreation || {})
.filter(i => i !== 'serialInvoices')
.map(i => ({ label: dataStore.documentTypesForCreation[i].label, key: i }))"
option-attribute="label"
value-attribute="key"
v-model="itemInfo.pos"
/>
</UFormGroup>
<UFormGroup
label="Text:"
>
<UTextarea
v-model="itemInfo.text"
/>
</UFormGroup>
</UForm>
<!-- TODO: Update und Create -->
<UFormGroup label="Position" required>
<USelectMenu
v-model="itemInfo.pos"
:options="[
{ label: 'Einleitung (Oben)', key: 'startText' },
{ label: 'Endtext (Unten)', key: 'endText' }
]"
option-attribute="label"
value-attribute="key"
/>
</UFormGroup>
</div>
<UFormGroup label="Text Inhalt" required help="Klicken Sie rechts auf eine Variable, um sie einzufügen.">
<UTextarea
ref="textareaRef"
v-model="itemInfo.text"
:rows="10"
placeholder="Sehr geehrte Damen und Herren..."
class="font-mono text-sm"
/>
</UFormGroup>
<UCheckbox v-model="itemInfo.default" label="Als Standard für diesen Typ verwenden"/>
</div>
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 h-fit">
<h4 class="font-semibold mb-3 flex items-center gap-2">
<UIcon name="i-heroicons-variable"/>
Variablen
</h4>
<p class="text-xs text-gray-500 mb-4">
Diese Platzhalter werden beim Erstellen des Dokuments ersetzt.
</p>
<div class="flex flex-col gap-2">
<button
v-for="v in variableDefinitions"
:key="v.key"
@click="insertVariable(v.key)"
class="group flex items-center justify-between p-2 rounded hover:bg-white dark:hover:bg-gray-700 border border-transparent hover:border-gray-200 dark:hover:border-gray-600 transition-colors text-left"
type="button"
>
<div>
<code
class="text-xs font-bold text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-950/50 px-1 py-0.5 rounded">{{
v.key
}}</code>
<div class="text-xs text-gray-500 mt-0.5">{{ v.desc }}</div>
</div>
<UIcon name="i-heroicons-plus-circle" class="w-5 h-5 text-gray-300 group-hover:text-primary-500"/>
</button>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-3">
<UButton color="gray" variant="ghost" @click="editTemplateModalOpen = false">
Abbrechen
</UButton>
<UButton
@click="dataStore.createNewItem('texttemplates',itemInfo);
editTemplateModalOpen = false"
v-if="!itemInfo.id"
>Erstellen</UButton>
color="primary"
:loading="isSaving"
@click="handleCreate"
icon="i-heroicons-plus"
>
Erstellen
</UButton>
<UButton
@click="dataStore.updateItem('texttemplates',itemInfo);
editTemplateModalOpen = false"
v-if="itemInfo.id"
>Speichern</UButton>
v-else
color="primary"
:loading="isSaving"
@click="handleUpdate"
icon="i-heroicons-check"
>
Speichern
</UButton>
</div>
</template>
</UCard>
</UModal>
</template>
<style scoped>
</style>