Merge branch 'devCorrected' into 'beta'

Dev corrected

See merge request fedeo/software!58
This commit is contained in:
2026-01-02 11:44:48 +00:00
3 changed files with 130 additions and 56 deletions

View File

@@ -632,7 +632,7 @@ const findDocumentErrors = computed(() => {
if (itemInfo.value.customer === null) errors.push({message: "Es ist kein Kunde ausgewählt", type: "breaking"}) if (itemInfo.value.customer === null) errors.push({message: "Es ist kein Kunde ausgewählt", type: "breaking"})
if (itemInfo.value.contact === null) errors.push({message: "Es ist kein Kontakt ausgewählt", type: "info"}) if (itemInfo.value.contact === null) errors.push({message: "Es ist kein Kontakt ausgewählt", type: "info"})
if (itemInfo.value.letterhead === null) errors.push({message: "Es ist kein Briefpapier ausgewählt", type: "breaking"}) if (itemInfo.value.letterhead === null) errors.push({message: "Es ist kein Briefpapier ausgewählt", type: "breaking"})
if (itemInfo.value.created_by === null || !itemInfo.value.created_by) errors.push({message: "Es ist kein Ansprechpartner im Unternehmen ausgewählt", type: "breaking"}) if (itemInfo.value.created_by === null || !itemInfo.value.created_by) errors.push({message: "Es ist kein Mitarbeiter ausgewählt", type: "breaking"})
if (itemInfo.value.address.street === null) errors.push({ if (itemInfo.value.address.street === null) errors.push({
message: "Es ist keine Straße im Adressat angegeben", message: "Es ist keine Straße im Adressat angegeben",
type: "breaking" type: "breaking"
@@ -2028,7 +2028,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</UFormGroup> </UFormGroup>
</InputGroup> </InputGroup>
<UFormGroup <UFormGroup
label="Ansprechpartner im Unternehmen:" label="Mitarbeiter:"
> >
<USelectMenu <USelectMenu
:options="tenantUsers" :options="tenantUsers"

View File

@@ -133,6 +133,10 @@
<span v-if="row.serialConfig?.intervall === 'monatlich'">Monatlich</span> <span v-if="row.serialConfig?.intervall === 'monatlich'">Monatlich</span>
<span v-if="row.serialConfig?.intervall === 'vierteljährlich'">Quartalsweise</span> <span v-if="row.serialConfig?.intervall === 'vierteljährlich'">Quartalsweise</span>
</template> </template>
<template #payment_type-data="{row}">
<span v-if="row.payment_type === 'transfer'">Überweisung</span>
<span v-else-if="row.payment_type === 'direct-debit'">SEPA - Einzug</span>
</template>
</UTable> </UTable>
<UModal v-model="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }"> <UModal v-model="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }">
@@ -153,10 +157,40 @@
<UDivider label="Vorlagen auswählen" /> <UDivider label="Vorlagen auswählen" />
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<UInput
v-model="modalSearch"
icon="i-heroicons-magnifying-glass"
placeholder="Kunde oder Vertrag suchen..."
class="w-full sm:w-64"
size="sm"
/>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500 hidden sm:inline">
{{ filteredExecutionList.length }} sichtbar
</span>
<UButton
size="2xs"
color="gray"
variant="soft"
label="Alle auswählen"
@click="selectAllTemplates"
/>
<UButton
size="2xs"
color="gray"
variant="ghost"
label="Keine"
@click="selectedExecutionRows = []"
/>
</div>
</div>
<div class="max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-800 rounded-md"> <div class="max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-800 rounded-md">
<UTable <UTable
v-model="selectedExecutionRows" v-model="selectedExecutionRows"
:rows="activeTemplates" :rows="filteredExecutionList"
:columns="executionColumns" :columns="executionColumns"
:ui="{ th: { base: 'whitespace-nowrap' } }" :ui="{ th: { base: 'whitespace-nowrap' } }"
> >
@@ -169,6 +203,9 @@
<template #serialConfig.intervall-data="{row}"> <template #serialConfig.intervall-data="{row}">
{{ row.serialConfig?.intervall }} {{ row.serialConfig?.intervall }}
</template> </template>
<template #contract-data="{row}">
{{row.contract?.contractNumber}} - {{row.contract?.name}}
</template>
</UTable> </UTable>
</div> </div>
@@ -249,19 +286,16 @@ const showExecutionModal = ref(false)
const executionDate = ref(dayjs().format('YYYY-MM-DD')) const executionDate = ref(dayjs().format('YYYY-MM-DD'))
const selectedExecutionRows = ref([]) const selectedExecutionRows = ref([])
const isExecuting = ref(false) const isExecuting = ref(false)
const modalSearch = ref("") // NEU: Suchstring für das Modal
// --- SerialExecutions State (Liste der Ausführungen) --- // --- SerialExecutions State ---
const showExecutionsSlideover = ref(false) const showExecutionsSlideover = ref(false)
const executionItems = ref([]) const executionItems = ref([])
const executionsLoading = ref(false) const executionsLoading = ref(false)
// NEU: Statusvariable für den Fertigstellungs-Prozess
const finishingId = ref(null) const finishingId = ref(null)
const setupPage = async () => { const setupPage = async () => {
// 1. Vorlagen laden
items.value = await useEntities("createddocuments").select("*, customer(id,name), contract(id,name, contractNumber)","documentDate",undefined,true) items.value = await useEntities("createddocuments").select("*, customer(id,name), contract(id,name, contractNumber)","documentDate",undefined,true)
// 2. Ausführungen laden
await fetchExecutions() await fetchExecutions()
} }
@@ -280,41 +314,30 @@ const fetchExecutions = async () => {
} }
const runningExecutions = computed(() => { const runningExecutions = computed(() => {
// Filtert nach Status 'draft'
return executionItems.value.filter(i => i.status === 'draft') return executionItems.value.filter(i => i.status === 'draft')
}) })
const completedExecutions = computed(() => { const completedExecutions = computed(() => {
// Filtert alles was NICHT läuft (und nicht 'draft' ist, um Dopplungen zu vermeiden)
return executionItems.value.filter(i => i.status !== 'running' && i.status !== 'pending' && i.status !== 'draft') return executionItems.value.filter(i => i.status !== 'running' && i.status !== 'pending' && i.status !== 'draft')
}) })
const openExecutionsSlideover = () => { const openExecutionsSlideover = () => {
showExecutionsSlideover.value = true showExecutionsSlideover.value = true
fetchExecutions() // Refresh beim Öffnen fetchExecutions()
} }
// NEU: Funktion zum Fertigstellen
const finishExecution = async (executionId) => { const finishExecution = async (executionId) => {
if (!executionId) return if (!executionId) return
finishingId.value = executionId finishingId.value = executionId
try { try {
await $api(`/api/functions/serial/finish/${executionId}`, { await $api(`/api/functions/serial/finish/${executionId}`, { method: 'POST' })
method: 'POST'
})
toast.add({ toast.add({
title: 'Ausführung beendet', title: 'Ausführung beendet',
description: 'Der Prozess wurde erfolgreich als fertig markiert.', description: 'Der Prozess wurde erfolgreich als fertig markiert.',
icon: 'i-heroicons-check-circle', icon: 'i-heroicons-check-circle',
color: 'green' color: 'green'
}) })
// Liste aktualisieren, damit die Karte aus "Laufend" verschwindet
await fetchExecutions() await fetchExecutions()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
toast.add({ toast.add({
@@ -330,28 +353,20 @@ const finishExecution = async (executionId) => {
const getStatusColor = (status) => { const getStatusColor = (status) => {
switch (status) { switch (status) {
case 'completed': case 'completed': return 'green'
return 'green' case 'error': return 'red'
case 'error':
return 'red'
case 'running': case 'running':
case 'draft': // Draft auch als Primary färben case 'draft': return 'primary'
return 'primary' default: return 'gray'
default:
return 'gray'
} }
} }
const getStatusLabel = (status) => { const getStatusLabel = (status) => {
switch (status) { switch (status) {
case 'completed': case 'completed': return 'Abgeschlossen'
return 'Abgeschlossen' case 'error': return 'Fehlerhaft'
case 'error': case 'draft': return 'Gestartet'
return 'Fehlerhaft' default: return status
case 'draft':
return 'Gestartet'
default:
return status
} }
} }
@@ -375,16 +390,39 @@ const filteredRows = computed(() => {
return useSearch(searchString.value, temp.slice().reverse()) return useSearch(searchString.value, temp.slice().reverse())
}) })
// Basis Liste für das Modal (nur Aktive)
const activeTemplates = computed(() => { const activeTemplates = computed(() => {
return items.value return items.value
.filter(i => i.type === "serialInvoices" && i.serialConfig?.active === true) .filter(i => i.type === "serialInvoices" && !!i.serialConfig?.active)
.map(i => ({...i})) .map(i => ({...i}))
}) })
// NEU: Schnellaktionen Logik // NEU: Gefilterte Liste für das Modal basierend auf der Suche
const filteredExecutionList = computed(() => {
if (!modalSearch.value) return activeTemplates.value
const term = modalSearch.value.toLowerCase()
return activeTemplates.value.filter(row => {
const customerName = row.customer?.name?.toLowerCase() || ""
const contractNum = row.contract?.contractNumber?.toLowerCase() || ""
const contractName = row.contract?.name?.toLowerCase() || ""
return customerName.includes(term) ||
contractNum.includes(term) ||
contractName.includes(term)
})
})
// NEU: Alle auswählen (nur die aktuell sichtbaren/gefilterten)
const selectAllTemplates = () => {
// WICHTIG: Überschreibt nicht bestehende Auswahl, sondern fügt hinzu oder ersetzt.
// Hier ersetzen wir die Auswahl komplett mit dem aktuellen Filterergebnis
selectedExecutionRows.value = [...filteredExecutionList.value]
}
const getActionItems = (row) => { const getActionItems = (row) => {
const isActive = row.serialConfig && row.serialConfig.active const isActive = row.serialConfig && row.serialConfig.active
return [ return [
[{ [{
label: isActive ? 'Deaktivieren' : 'Aktivieren', label: isActive ? 'Deaktivieren' : 'Aktivieren',
@@ -397,19 +435,11 @@ const getActionItems = (row) => {
const toggleActiveState = async (row) => { const toggleActiveState = async (row) => {
const newState = !row.serialConfig.active const newState = !row.serialConfig.active
// Optimistisches Update im UI
row.serialConfig.active = newState row.serialConfig.active = newState
try { try {
// Annahme: createddocuments Tabelle erlaubt update
await useEntities('createddocuments').update(row.id, { await useEntities('createddocuments').update(row.id, {
serialConfig: { serialConfig: { ...row.serialConfig, active: newState }
...row.serialConfig,
active: newState
}
}) })
toast.add({ toast.add({
title: newState ? 'Aktiviert' : 'Deaktiviert', title: newState ? 'Aktiviert' : 'Deaktiviert',
description: `Die Vorlage wurde ${newState ? 'aktiviert' : 'deaktiviert'}.`, description: `Die Vorlage wurde ${newState ? 'aktiviert' : 'deaktiviert'}.`,
@@ -418,7 +448,6 @@ const toggleActiveState = async (row) => {
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
// Rollback im Fehlerfall
row.serialConfig.active = !newState row.serialConfig.active = !newState
toast.add({ toast.add({
title: 'Fehler', title: 'Fehler',
@@ -429,12 +458,13 @@ const toggleActiveState = async (row) => {
} }
const templateColumns = [ const templateColumns = [
{ key: 'actions', label: '' }, // NEU: Spalte für Menü ganz links { key: 'actions', label: '' },
{ key: 'serialConfig.active', label: "Aktiv" }, { key: 'serialConfig.active', label: "Aktiv" },
{ key: "amount", label: "Betrag" }, { key: "amount", label: "Betrag" },
{ key: 'partner', label: "Kunde" }, { key: 'partner', label: "Kunde" },
{ key: 'contract', label: "Vertrag" }, { key: 'contract', label: "Vertrag" },
{ key: 'serialConfig.intervall', label: "Rhythmus" } { key: 'serialConfig.intervall', label: "Rhythmus" },
{ key: 'payment_type', label: "Zahlart" }
] ]
const executionColumns = [ const executionColumns = [
@@ -480,6 +510,7 @@ const calculateDocSum = (row) => {
const openExecutionModal = () => { const openExecutionModal = () => {
executionDate.value = dayjs().format('YYYY-MM-DD') executionDate.value = dayjs().format('YYYY-MM-DD')
selectedExecutionRows.value = [] selectedExecutionRows.value = []
modalSearch.value = "" // Reset Search
showExecutionModal.value = true showExecutionModal.value = true
} }
@@ -510,7 +541,6 @@ const executeSerialInvoices = async () => {
showExecutionModal.value = false showExecutionModal.value = false
selectedExecutionRows.value = [] selectedExecutionRows.value = []
// Liste aktualisieren, um den "Läuft" Status zu sehen
await fetchExecutions() await fetchExecutions()
} catch (error) { } catch (error) {

View File

@@ -2,6 +2,9 @@
import dayjs from "dayjs" import dayjs from "dayjs"
import {useSum} from "~/composables/useSum.js"; import {useSum} from "~/composables/useSum.js";
// Zugriff auf API und Toast
const { $api } = useNuxtApp()
const toast = useToast()
defineShortcuts({ defineShortcuts({
'/': () => { '/': () => {
@@ -47,6 +50,9 @@ const sort = ref({
direction: 'desc' direction: 'desc'
}) })
// Status für den Button
const isPreparing = ref(false)
const type = "incominginvoices" const type = "incominginvoices"
const dataType = dataStore.dataTypes[type] const dataType = dataStore.dataTypes[type]
@@ -54,6 +60,34 @@ const setupPage = async () => {
items.value = await useEntities(type).select("*, vendor(id,name), statementallocations(id,amount)",sort.value.column,sort.value.direction === "asc") items.value = await useEntities(type).select("*, vendor(id,name), statementallocations(id,amount)",sort.value.column,sort.value.direction === "asc")
} }
// Funktion zum Vorbereiten der Belege
const prepareInvoices = async () => {
isPreparing.value = true
try {
await $api('/api/functions/services/prepareincominginvoices', { method: 'POST' })
toast.add({
title: 'Erfolg',
description: 'Eingangsbelege wurden vorbereitet.',
icon: 'i-heroicons-check-circle',
color: 'green'
})
// Liste neu laden
await setupPage()
} catch (error) {
console.error(error)
toast.add({
title: 'Fehler',
description: 'Beim Vorbereiten der Belege ist ein Fehler aufgetreten.',
icon: 'i-heroicons-exclamation-circle',
color: 'red'
})
} finally {
isPreparing.value = false
}
}
setupPage() setupPage()
const selectedColumns = ref(tempStore.columns[type] ? tempStore.columns[type] : dataType.templateColumns.filter(i => !i.disabledInTable)) const selectedColumns = ref(tempStore.columns[type] ? tempStore.columns[type] : dataType.templateColumns.filter(i => !i.disabledInTable))
@@ -128,6 +162,17 @@ const selectIncomingInvoice = (invoice) => {
<template> <template>
<UDashboardNavbar title="Eingangsbelege" :badge="filteredRows.length"> <UDashboardNavbar title="Eingangsbelege" :badge="filteredRows.length">
<template #right> <template #right>
<UButton
label="Belege vorbereiten"
icon="i-heroicons-sparkles"
color="primary"
variant="solid"
:loading="isPreparing"
@click="prepareInvoices"
class="mr-2"
/>
<UInput <UInput
id="searchinput" id="searchinput"
v-model="searchString" v-model="searchString"
@@ -150,7 +195,6 @@ const selectIncomingInvoice = (invoice) => {
v-if="searchString.length > 0" v-if="searchString.length > 0"
/> />
<!-- <UButton @click="router.push(`/incomingInvoices/create`)">+ Beleg</UButton>-->
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UDashboardToolbar> <UDashboardToolbar>
@@ -193,8 +237,8 @@ const selectIncomingInvoice = (invoice) => {
<template #default="{item}"> <template #default="{item}">
{{item.label}} {{item.label}}
<UBadge <UBadge
variant="outline" variant="outline"
class="ml-2" class="ml-2"
> >
{{filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' ).length}} {{filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' ).length}}
</UBadge> </UBadge>