@@ -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>
|
||||||
Reference in New Issue
Block a user