feat(accounting): allow cancelling serial invoice executions

This commit is contained in:
2026-10-02 18:48:06 +02:00
parent 7e6c2fbac5
commit fdcb9bfc1d
6 changed files with 269 additions and 87 deletions

View File

@@ -28,6 +28,7 @@
color="white"
variant="solid"
label="Ausführen"
:loading="isExecuting"
@click="openExecutionModal"
/>
@@ -70,21 +71,33 @@
<div>
<p class="text-sm font-medium">Start: {{ dayjs(exec.createdAt).format('DD.MM.YYYY HH:mm') }}</p>
</div>
<UBadge color="primary" variant="subtle">Läuft</UBadge>
<UBadge :color="getStatusColor(exec.status)" variant="subtle">{{ getStatusLabel(exec.status) }}</UBadge>
</div>
<div class="text-xs text-gray-500 flex justify-between mb-3">
<span>{{exec.summary}}</span>
</div>
<div class="flex justify-end pt-2 border-t border-gray-100 dark:border-gray-800">
<div class="flex justify-end gap-2 pt-2 border-t border-gray-100 dark:border-gray-800">
<UButton
size="xs"
color="error"
variant="soft"
icon="i-heroicons-stop"
label="Durchlauf abbrechen"
:loading="cancellingId === exec.id"
:disabled="exec.status === 'cancelling' || cancellingId !== null"
@click="cancelExecution(exec.id)"
/>
<UButton
v-if="['draft', 'error'].includes(exec.status)"
size="xs"
color="white"
variant="solid"
icon="i-heroicons-check"
label="Fertigstellen"
:loading="finishingId === exec.id"
:disabled="finishingId !== null || cancellingId !== null"
@click="finishExecution(exec.id)"
/>
</div>
@@ -343,6 +356,14 @@ const showExecutionsSlideover = ref(false)
const executionItems = ref([])
const executionsLoading = ref(false)
const finishingId = ref(null)
const cancellingId = ref(null)
let executionsPoll
onMounted(() => {
executionsPoll = setInterval(() => {
if (!executionsLoading.value) fetchExecutions()
}, 3000)
})
onUnmounted(() => clearInterval(executionsPoll))
const setupPage = async () => {
items.value = await useEntities("createddocuments").select("*, customer(id,name), contract(id,name, contractNumber), plant(id,name)","documentDate",undefined,true)
@@ -364,11 +385,11 @@ const fetchExecutions = async () => {
}
const runningExecutions = computed(() => {
return executionItems.value.filter(i => i.status === 'draft')
return executionItems.value.filter(i => ['generating', 'draft', 'processing', 'cancelling', 'error'].includes(i.status))
})
const completedExecutions = computed(() => {
return executionItems.value.filter(i => i.status !== 'running' && i.status !== 'pending' && i.status !== 'draft')
return executionItems.value.filter(i => ['completed', 'cancelled'].includes(i.status))
})
const openExecutionsSlideover = () => {
@@ -376,16 +397,34 @@ const openExecutionsSlideover = () => {
fetchExecutions()
}
const cancelExecution = async (executionId) => {
if (cancellingId.value || !window.confirm('Durchlauf abbrechen? Ungebuchte Rechnungsentwürfe werden archiviert. Bereits gebuchte Rechnungen bleiben erhalten.')) return
cancellingId.value = executionId
try {
const result = await $api(`/api/functions/serial/cancel/${executionId}`, { method: 'POST' })
toast.add({
title: result.execution.status === 'cancelling' ? 'Abbruch angefordert' : 'Durchlauf abgebrochen',
description: result.execution.status === 'cancelling' ? 'Die gerade bearbeitete Rechnung wird noch abgeschlossen.' : 'Ungebuchte Rechnungsentwürfe wurden archiviert.',
color: 'success'
})
await fetchExecutions()
} catch (error) {
toast.add({ title: 'Abbruch fehlgeschlagen', description: error.data?.message || 'Der Durchlauf konnte nicht abgebrochen werden.', color: 'error' })
} finally {
cancellingId.value = null
}
}
const finishExecution = async (executionId) => {
if (!executionId) return
finishingId.value = executionId
try {
await $api(`/api/functions/serial/finish/${executionId}`, { method: 'POST' })
const result = await $api(`/api/functions/serial/finish/${executionId}`, { method: 'POST' })
toast.add({
title: 'Ausführung beendet',
description: 'Der Prozess wurde erfolgreich als fertig markiert.',
title: getStatusLabel(result.execution.status),
description: result.execution.summary,
icon: 'i-heroicons-check-circle',
color: 'green'
color: result.errors > 0 ? 'error' : 'success'
})
await fetchExecutions()
} catch (error) {
@@ -403,11 +442,13 @@ const finishExecution = async (executionId) => {
const getStatusColor = (status) => {
switch (status) {
case 'completed': return 'green'
case 'error': return 'red'
case 'completed': return 'success'
case 'error': return 'error'
case 'generating':
case 'processing':
case 'running':
case 'draft': return 'primary'
default: return 'gray'
default: return 'neutral'
}
}
@@ -415,7 +456,11 @@ const getStatusLabel = (status) => {
switch (status) {
case 'completed': return 'Abgeschlossen'
case 'error': return 'Fehlerhaft'
case 'draft': return 'Gestartet'
case 'draft': return 'Vorbereitet'
case 'generating': return 'Wird vorbereitet'
case 'processing': return 'Wird fertiggestellt'
case 'cancelling': return 'Wird abgebrochen'
case 'cancelled': return 'Abgebrochen'
default: return status
}
}
@@ -645,14 +690,15 @@ const executeSerialInvoices = async () => {
templateIds: selectedExecutionRows.value.map(row => row.id)
}
showExecutionModal.value = false
const res = await $api('/api/functions/serial/start', {
method: 'POST',
body: payload
})
toast.add({
title: 'Ausführung gestartet',
description: `${res.results.length} Rechnungen wurden für den Lauf vorbereitet.`,
title: res.execution.status === 'cancelled' ? 'Durchlauf abgebrochen' : 'Ausführung gestartet',
description: res.execution.status === 'cancelled' ? res.execution.summary : `${res.results.filter(result => result.status === 'success').length} Rechnungen wurden für den Lauf vorbereitet.`,
icon: 'i-heroicons-check-circle',
color: 'green'
})