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.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.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({
message: "Es ist keine Straße im Adressat angegeben",
type: "breaking"
@@ -2028,7 +2028,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</UFormGroup>
</InputGroup>
<UFormGroup
label="Ansprechpartner im Unternehmen:"
label="Mitarbeiter:"
>
<USelectMenu
:options="tenantUsers"

View File

@@ -133,6 +133,10 @@
<span v-if="row.serialConfig?.intervall === 'monatlich'">Monatlich</span>
<span v-if="row.serialConfig?.intervall === 'vierteljährlich'">Quartalsweise</span>
</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>
<UModal v-model="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }">
@@ -153,10 +157,40 @@
<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">
<UTable
v-model="selectedExecutionRows"
:rows="activeTemplates"
:rows="filteredExecutionList"
:columns="executionColumns"
:ui="{ th: { base: 'whitespace-nowrap' } }"
>
@@ -169,6 +203,9 @@
<template #serialConfig.intervall-data="{row}">
{{ row.serialConfig?.intervall }}
</template>
<template #contract-data="{row}">
{{row.contract?.contractNumber}} - {{row.contract?.name}}
</template>
</UTable>
</div>
@@ -249,19 +286,16 @@ const showExecutionModal = ref(false)
const executionDate = ref(dayjs().format('YYYY-MM-DD'))
const selectedExecutionRows = ref([])
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 executionItems = ref([])
const executionsLoading = ref(false)
// NEU: Statusvariable für den Fertigstellungs-Prozess
const finishingId = ref(null)
const setupPage = async () => {
// 1. Vorlagen laden
items.value = await useEntities("createddocuments").select("*, customer(id,name), contract(id,name, contractNumber)","documentDate",undefined,true)
// 2. Ausführungen laden
await fetchExecutions()
}
@@ -280,41 +314,30 @@ const fetchExecutions = async () => {
}
const runningExecutions = computed(() => {
// Filtert nach Status 'draft'
return executionItems.value.filter(i => i.status === 'draft')
})
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')
})
const openExecutionsSlideover = () => {
showExecutionsSlideover.value = true
fetchExecutions() // Refresh beim Öffnen
fetchExecutions()
}
// NEU: Funktion zum Fertigstellen
const finishExecution = async (executionId) => {
if (!executionId) return
finishingId.value = executionId
try {
await $api(`/api/functions/serial/finish/${executionId}`, {
method: 'POST'
})
await $api(`/api/functions/serial/finish/${executionId}`, { method: 'POST' })
toast.add({
title: 'Ausführung beendet',
description: 'Der Prozess wurde erfolgreich als fertig markiert.',
icon: 'i-heroicons-check-circle',
color: 'green'
})
// Liste aktualisieren, damit die Karte aus "Laufend" verschwindet
await fetchExecutions()
} catch (error) {
console.error(error)
toast.add({
@@ -330,28 +353,20 @@ const finishExecution = async (executionId) => {
const getStatusColor = (status) => {
switch (status) {
case 'completed':
return 'green'
case 'error':
return 'red'
case 'completed': return 'green'
case 'error': return 'red'
case 'running':
case 'draft': // Draft auch als Primary färben
return 'primary'
default:
return 'gray'
case 'draft': return 'primary'
default: return 'gray'
}
}
const getStatusLabel = (status) => {
switch (status) {
case 'completed':
return 'Abgeschlossen'
case 'error':
return 'Fehlerhaft'
case 'draft':
return 'Gestartet'
default:
return status
case 'completed': return 'Abgeschlossen'
case 'error': return 'Fehlerhaft'
case 'draft': return 'Gestartet'
default: return status
}
}
@@ -375,16 +390,39 @@ const filteredRows = computed(() => {
return useSearch(searchString.value, temp.slice().reverse())
})
// Basis Liste für das Modal (nur Aktive)
const activeTemplates = computed(() => {
return items.value
.filter(i => i.type === "serialInvoices" && i.serialConfig?.active === true)
.filter(i => i.type === "serialInvoices" && !!i.serialConfig?.active)
.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 isActive = row.serialConfig && row.serialConfig.active
return [
[{
label: isActive ? 'Deaktivieren' : 'Aktivieren',
@@ -397,19 +435,11 @@ const getActionItems = (row) => {
const toggleActiveState = async (row) => {
const newState = !row.serialConfig.active
// Optimistisches Update im UI
row.serialConfig.active = newState
try {
// Annahme: createddocuments Tabelle erlaubt update
await useEntities('createddocuments').update(row.id, {
serialConfig: {
...row.serialConfig,
active: newState
}
serialConfig: { ...row.serialConfig, active: newState }
})
toast.add({
title: newState ? 'Aktiviert' : 'Deaktiviert',
description: `Die Vorlage wurde ${newState ? 'aktiviert' : 'deaktiviert'}.`,
@@ -418,7 +448,6 @@ const toggleActiveState = async (row) => {
})
} catch (e) {
console.error(e)
// Rollback im Fehlerfall
row.serialConfig.active = !newState
toast.add({
title: 'Fehler',
@@ -429,12 +458,13 @@ const toggleActiveState = async (row) => {
}
const templateColumns = [
{ key: 'actions', label: '' }, // NEU: Spalte für Menü ganz links
{ key: 'actions', label: '' },
{ key: 'serialConfig.active', label: "Aktiv" },
{ key: "amount", label: "Betrag" },
{ key: 'partner', label: "Kunde" },
{ key: 'contract', label: "Vertrag" },
{ key: 'serialConfig.intervall', label: "Rhythmus" }
{ key: 'serialConfig.intervall', label: "Rhythmus" },
{ key: 'payment_type', label: "Zahlart" }
]
const executionColumns = [
@@ -480,6 +510,7 @@ const calculateDocSum = (row) => {
const openExecutionModal = () => {
executionDate.value = dayjs().format('YYYY-MM-DD')
selectedExecutionRows.value = []
modalSearch.value = "" // Reset Search
showExecutionModal.value = true
}
@@ -510,7 +541,6 @@ const executeSerialInvoices = async () => {
showExecutionModal.value = false
selectedExecutionRows.value = []
// Liste aktualisieren, um den "Läuft" Status zu sehen
await fetchExecutions()
} catch (error) {

View File

@@ -2,6 +2,9 @@
import dayjs from "dayjs"
import {useSum} from "~/composables/useSum.js";
// Zugriff auf API und Toast
const { $api } = useNuxtApp()
const toast = useToast()
defineShortcuts({
'/': () => {
@@ -47,6 +50,9 @@ const sort = ref({
direction: 'desc'
})
// Status für den Button
const isPreparing = ref(false)
const type = "incominginvoices"
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")
}
// 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()
const selectedColumns = ref(tempStore.columns[type] ? tempStore.columns[type] : dataType.templateColumns.filter(i => !i.disabledInTable))
@@ -128,6 +162,17 @@ const selectIncomingInvoice = (invoice) => {
<template>
<UDashboardNavbar title="Eingangsbelege" :badge="filteredRows.length">
<template #right>
<UButton
label="Belege vorbereiten"
icon="i-heroicons-sparkles"
color="primary"
variant="solid"
:loading="isPreparing"
@click="prepareInvoices"
class="mr-2"
/>
<UInput
id="searchinput"
v-model="searchString"
@@ -150,7 +195,6 @@ const selectIncomingInvoice = (invoice) => {
v-if="searchString.length > 0"
/>
<!-- <UButton @click="router.push(`/incomingInvoices/create`)">+ Beleg</UButton>-->
</template>
</UDashboardNavbar>
<UDashboardToolbar>
@@ -193,8 +237,8 @@ const selectIncomingInvoice = (invoice) => {
<template #default="{item}">
{{item.label}}
<UBadge
variant="outline"
class="ml-2"
variant="outline"
class="ml-2"
>
{{filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' ).length}}
</UBadge>