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> <script setup>
import { ref, onMounted } from 'vue'
const dataStore = useDataStore() const dataStore = useDataStore()
const toast = useToast()
defineShortcuts({ // useEntities Initialisierung für 'texttemplates'
'+': () => { const { select, create, update } = useEntities("texttemplates")
editTemplateModalOpen.value = true
}
})
// --- State ---
const editTemplateModalOpen = ref(false) const editTemplateModalOpen = ref(false)
const itemInfo = ref({}) const itemInfo = ref({})
const texttemplates = ref([]) const texttemplates = ref([])
const loading = ref(true) const loading = ref(true)
const isSaving = ref(false)
const textareaRef = ref(null)
const setup = async () => { // Tabelle Expand State
texttemplates.value = (await useEntities("texttemplates").select()).filter(i => !i.archived)
loading.value = false
}
setup()
const expand = ref({ const expand = ref({
openedRows: [], openedRows: [],
row: {} 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> </script>
<template> <template>
<UDashboardNavbar <UDashboardNavbar title="Text Vorlagen">
title="Text Vorlagen"
>
<template #right> <template #right>
<UButton <UButton
@click="editTemplateModalOpen = true, itemInfo = {}" icon="i-heroicons-plus"
@click="openModal()"
color="primary"
variant="solid"
> >
+ Erstellen Erstellen
</UButton> </UButton>
</template> </template>
</UDashboardNavbar> </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 <UTable
class="mt-3" class="mt-3"
:rows="texttemplates" :rows="texttemplates"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
:loading="loading" :loading="loading"
v-model:expand="expand" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Textvorlagen anzuzeigen' }" v-model:expand="expand"
:columns="[{key:'name',label:'Name'},{key:'documentType',label:'Dokumententyp'},{key:'default',label:'Standard'},{key:'pos',label:'Position'}]" :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}"> <template #name-data="{ row }">
{{dataStore.documentTypesForCreation[row.documentType].label}} <span class="font-medium text-gray-900 dark:text-white">{{ row.name }}</span>
</template> </template>
<template #default-data="{row}">
{{row.default ? "Ja" : "Nein"}} <template #documentType-data="{ row }">
<UBadge color="gray" variant="soft">
{{ getDocLabel(row.documentType) }}
</UBadge>
</template> </template>
<template #pos-data="{row}">
<span v-if="row.pos === 'startText'">Einleitung</span> <template #pos-data="{ row }">
<span v-else-if="row.pos === 'endText'">Endtext</span> <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>
<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 }"> <template #expand="{ row }">
<div class="p-4"> <div class="p-6 bg-gray-50 dark:bg-gray-800/50 rounded-b-lg border-t border-gray-200 dark:border-gray-700">
<p class="text-2xl">{{dataStore.documentTypesForCreation[row.documentType].label}}</p> <div class="mb-4">
<p class="text-xl mt-3">{{row.pos === 'startText' ? 'Einleitung' : 'Ende'}}</p> <h4 class="text-sm font-bold uppercase text-gray-500 mb-1">Vorschau</h4>
<p class="text-justify mt-3">{{row.text}}</p> <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">
<UButton {{ row.text }}
class="mt-3 mr-3" </p>
@click="itemInfo = row; </div>
editTemplateModalOpen = true"
variant="outline" <div class="flex justify-end gap-3">
>Bearbeiten</UButton> <ButtonWithConfirm
<ButtonWithConfirm color="rose"
color="rose" variant="soft"
variant="outline" icon="i-heroicons-archive-box"
@confirmed="dataStore.updateItem('texttemplates',{id: row.id,archived: true}), @confirmed="handleArchive(row)"
setup" >
> <template #button>Archivieren</template>
<template #button> <template #header>
Archivieren <span class="font-bold">Wirklich archivieren?</span>
</template> </template>
<template #header> Die Vorlage "{{ row.name }}" wird archiviert.
<span class="text-md dark:text-whitetext-black font-bold">Archivieren bestätigen</span> </ButtonWithConfirm>
</template>
Möchten Sie diesen Ausgangsbeleg wirklich archivieren? <UButton
</ButtonWithConfirm> icon="i-heroicons-pencil"
@click="openModal(row)"
>
Bearbeiten
</UButton>
</div>
</div> </div>
</template> </template>
</UTable> </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> </UDashboardPanelContent>
<UModal <UModal v-model="editTemplateModalOpen" :ui="{ width: 'sm:max-w-4xl' }">
v-model="editTemplateModalOpen" <UCard>
>
<UCard class="h-full">
<template #header> <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> </template>
<UForm class="h-full"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<UFormGroup <div class="lg:col-span-2 space-y-4">
label="Name:" <UFormGroup label="Bezeichnung" required>
> <UInput v-model="itemInfo.name" placeholder="z.B. Standard Angebotstext" icon="i-heroicons-tag"/>
<UInput </UFormGroup>
v-model="itemInfo.name"
/>
</UFormGroup>
<UFormGroup
label="Dokumententyp:"
>
<USelectMenu <div class="grid grid-cols-2 gap-4">
:options="Object.keys(dataStore.documentTypesForCreation).filter(i => i !== 'serialInvoices').map(i => { <UFormGroup label="Dokumententyp" required>
return { <USelectMenu
label: dataStore.documentTypesForCreation[i].label, v-model="itemInfo.documentType"
key: i :options="Object.keys(dataStore.documentTypesForCreation || {})
} .filter(i => i !== 'serialInvoices')
})" .map(i => ({ label: dataStore.documentTypesForCreation[i].label, key: i }))"
option-attribute="label" option-attribute="label"
value-attribute="key" value-attribute="key"
v-model="itemInfo.documentType" />
/> </UFormGroup>
</UFormGroup>
<UFormGroup <UFormGroup label="Position" required>
label="Position:" <USelectMenu
> v-model="itemInfo.pos"
<USelectMenu :options="[
:options="[{label:'Einleitung',key: 'startText'},{label:'Ende',key: 'endText'}]" { label: 'Einleitung (Oben)', key: 'startText' },
option-attribute="label" { label: 'Endtext (Unten)', key: 'endText' }
value-attribute="key" ]"
v-model="itemInfo.pos" option-attribute="label"
/> value-attribute="key"
</UFormGroup> />
<UFormGroup </UFormGroup>
label="Text:" </div>
>
<UTextarea <UFormGroup label="Text Inhalt" required help="Klicken Sie rechts auf eine Variable, um sie einzufügen.">
v-model="itemInfo.text" <UTextarea
/> ref="textareaRef"
</UFormGroup> v-model="itemInfo.text"
</UForm> :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>
<!-- TODO: Update und Create -->
<template #footer> <template #footer>
<UButton <div class="flex justify-end gap-3">
@click="dataStore.createNewItem('texttemplates',itemInfo); <UButton color="gray" variant="ghost" @click="editTemplateModalOpen = false">
editTemplateModalOpen = false" Abbrechen
v-if="!itemInfo.id" </UButton>
>Erstellen</UButton>
<UButton <UButton
@click="dataStore.updateItem('texttemplates',itemInfo); v-if="!itemInfo.id"
editTemplateModalOpen = false" color="primary"
v-if="itemInfo.id" :loading="isSaving"
>Speichern</UButton> @click="handleCreate"
icon="i-heroicons-plus"
>
Erstellen
</UButton>
<UButton
v-else
color="primary"
:loading="isSaving"
@click="handleUpdate"
icon="i-heroicons-check"
>
Speichern
</UButton>
</div>
</template> </template>
</UCard> </UCard>
</UModal> </UModal>
</template> </template>
<style scoped> <style scoped>
</style> </style>