@@ -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>
|
||||
<ButtonWithConfirm
|
||||
color="rose"
|
||||
variant="outline"
|
||||
@confirmed="dataStore.updateItem('texttemplates',{id: row.id,archived: true}),
|
||||
setup"
|
||||
>
|
||||
<template #button>
|
||||
Archivieren
|
||||
</template>
|
||||
<template #header>
|
||||
<span class="text-md dark:text-whitetext-black font-bold">Archivieren bestätigen</span>
|
||||
</template>
|
||||
Möchten Sie diesen Ausgangsbeleg wirklich archivieren?
|
||||
</ButtonWithConfirm>
|
||||
<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="soft"
|
||||
icon="i-heroicons-archive-box"
|
||||
@confirmed="handleArchive(row)"
|
||||
>
|
||||
<template #button>Archivieren</template>
|
||||
<template #header>
|
||||
<span class="font-bold">Wirklich archivieren?</span>
|
||||
</template>
|
||||
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"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
label="Dokumententyp:"
|
||||
>
|
||||
<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>
|
||||
|
||||
<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'}]"
|
||||
option-attribute="label"
|
||||
value-attribute="key"
|
||||
v-model="itemInfo.pos"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
label="Text:"
|
||||
>
|
||||
<UTextarea
|
||||
v-model="itemInfo.text"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UForm>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormGroup label="Dokumententyp" required>
|
||||
<USelectMenu
|
||||
v-model="itemInfo.documentType"
|
||||
:options="Object.keys(dataStore.documentTypesForCreation || {})
|
||||
.filter(i => i !== 'serialInvoices')
|
||||
.map(i => ({ label: dataStore.documentTypesForCreation[i].label, key: i }))"
|
||||
option-attribute="label"
|
||||
value-attribute="key"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- TODO: Update und Create -->
|
||||
<template #footer>
|
||||
<UButton
|
||||
@click="dataStore.createNewItem('texttemplates',itemInfo);
|
||||
editTemplateModalOpen = false"
|
||||
v-if="!itemInfo.id"
|
||||
>Erstellen</UButton>
|
||||
<UButton
|
||||
@click="dataStore.updateItem('texttemplates',itemInfo);
|
||||
editTemplateModalOpen = false"
|
||||
v-if="itemInfo.id"
|
||||
>Speichern</UButton>
|
||||
<div class="flex justify-end gap-3">
|
||||
<UButton color="gray" variant="ghost" @click="editTemplateModalOpen = false">
|
||||
Abbrechen
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="!itemInfo.id"
|
||||
color="primary"
|
||||
:loading="isSaving"
|
||||
@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>
|
||||
</UCard>
|
||||
</UModal>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user