dev #241
@@ -14,7 +14,7 @@ export const serialExecutions = pgTable("serial_executions", {
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id), executionDate: timestamp("execution_date").notNull(),
|
||||
status: text("status").default("draft"), // 'draft', 'completed'
|
||||
status: text("status").default("draft"), // generating, draft, processing, cancelling, cancelled, completed, error
|
||||
createdBy: text("created_by"), // oder UUID, je nach Auth-System
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
summary: text("summary"), // z.B. "25 Rechnungen erstellt"
|
||||
|
||||
@@ -802,7 +802,7 @@ export const accountingTools: McpTool[] = [
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: "string", enum: ["draft", "completed", "error"] },
|
||||
status: { type: "string", enum: ["generating", "draft", "processing", "cancelling", "cancelled", "completed", "error"] },
|
||||
...paginationProperties,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,6 +14,60 @@ import { documentTemplateHandlebars } from "../utils/handlebars";
|
||||
dayjs.extend(quarterOfYear);
|
||||
|
||||
|
||||
// Cancellation and finalization compete through conditional status updates.
|
||||
export const cancelManualGeneration = async (server: FastifyInstance, executionId: string, tenantId: number) => {
|
||||
return server.db.transaction(async (tx) => {
|
||||
const [execution] = await tx.select().from(schema.serialExecutions).where(and(
|
||||
eq(schema.serialExecutions.id, executionId),
|
||||
eq(schema.serialExecutions.tenant, tenantId),
|
||||
)).for("update");
|
||||
if (!execution) throw Object.assign(new Error("Durchlauf nicht gefunden."), { statusCode: 404 });
|
||||
if (execution.status === "completed") {
|
||||
throw Object.assign(new Error("Abgeschlossene Durchläufe können nicht abgebrochen werden."), { statusCode: 409 });
|
||||
}
|
||||
if (["cancelled", "cancelling"].includes(execution.status)) return { execution };
|
||||
const status = ["generating", "processing"].includes(execution.status) ? "cancelling" : "cancelled";
|
||||
if (status === "cancelled") await archiveExecutionDrafts(tx, executionId, tenantId);
|
||||
const [updated] = await tx.update(schema.serialExecutions).set({
|
||||
status,
|
||||
summary: status === "cancelling" ? "Abbruch angefordert." : "Abgebrochen. Ungebuchte Entwürfe wurden archiviert.",
|
||||
}).where(and(eq(schema.serialExecutions.id, executionId), eq(schema.serialExecutions.tenant, tenantId))).returning();
|
||||
return { execution: updated };
|
||||
});
|
||||
};
|
||||
|
||||
async function archiveExecutionDrafts(db: any, executionId: string, tenantId: number) {
|
||||
await db.update(schema.createddocuments).set({ archived: true }).where(and(
|
||||
eq(schema.createddocuments.serialexecution, executionId),
|
||||
eq(schema.createddocuments.tenant, tenantId),
|
||||
eq(schema.createddocuments.state, "Entwurf"),
|
||||
eq(schema.createddocuments.archived, false),
|
||||
));
|
||||
}
|
||||
|
||||
async function isCancellationRequested(server: FastifyInstance, executionId: string, tenantId: number) {
|
||||
const [record] = await server.db.select().from(schema.serialExecutions).where(and(
|
||||
eq(schema.serialExecutions.id, executionId), eq(schema.serialExecutions.tenant, tenantId),
|
||||
));
|
||||
return record?.status === "cancelling";
|
||||
}
|
||||
|
||||
async function settleExecution(server: FastifyInstance, executionId: string, tenantId: number, status: string, summary?: string) {
|
||||
return server.db.transaction(async (tx) => {
|
||||
const [record] = await tx.select().from(schema.serialExecutions).where(and(
|
||||
eq(schema.serialExecutions.id, executionId), eq(schema.serialExecutions.tenant, tenantId),
|
||||
)).for("update");
|
||||
if (record.status === "cancelling") {
|
||||
await archiveExecutionDrafts(tx, executionId, tenantId);
|
||||
status = "cancelled";
|
||||
summary = "Abgebrochen. Ungebuchte Entwürfe wurden archiviert; gebuchte Rechnungen bleiben erhalten.";
|
||||
}
|
||||
const [updated] = await tx.update(schema.serialExecutions).set({ status, ...(summary ? { summary } : {}) })
|
||||
.where(and(eq(schema.serialExecutions.id, executionId), eq(schema.serialExecutions.tenant, tenantId))).returning();
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
export const executeManualGeneration = async (
|
||||
server: FastifyInstance,
|
||||
executionDate: string | Date,
|
||||
@@ -66,7 +120,7 @@ export const executeManualGeneration = async (
|
||||
.values({
|
||||
tenant: tenantId,
|
||||
executionDate: executionDayjs.toDate(),
|
||||
status: "draft",
|
||||
status: "generating",
|
||||
createdBy: executedBy,
|
||||
summary: `${uniqueTemplateIds.length} Vorlagen verarbeitet`
|
||||
})
|
||||
@@ -74,50 +128,45 @@ export const executeManualGeneration = async (
|
||||
|
||||
console.log(executionRecord);
|
||||
|
||||
// 4. Loop durch die Templates
|
||||
for (const template of templates) {
|
||||
try {
|
||||
const resultId = await processSingleTemplate(
|
||||
server,
|
||||
template,
|
||||
tenant,
|
||||
executionDayjs,
|
||||
folderId,
|
||||
fileTypeId,
|
||||
executedBy,
|
||||
executionRecord.id
|
||||
);
|
||||
results.push({ id: template.id, status: "success", newDocumentId: resultId });
|
||||
} catch (e: any) {
|
||||
console.error(`Fehler bei Template ${template.id}:`, e);
|
||||
results.push({ id: template.id, status: "error", error: e.message });
|
||||
try {
|
||||
// 4. Loop durch die Templates
|
||||
for (const template of templates) {
|
||||
if (await isCancellationRequested(server, executionRecord.id, tenantId)) break;
|
||||
try {
|
||||
const resultId = await processSingleTemplate(
|
||||
server,
|
||||
template,
|
||||
tenant,
|
||||
executionDayjs,
|
||||
folderId,
|
||||
fileTypeId,
|
||||
executedBy,
|
||||
executionRecord.id
|
||||
);
|
||||
results.push({ id: template.id, status: "success", newDocumentId: resultId });
|
||||
} catch (e: any) {
|
||||
console.error(`Fehler bei Template ${template.id}:`, e);
|
||||
results.push({ id: template.id, status: "error", error: e.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { execution: executionRecord, results };
|
||||
const execution = await settleExecution(server, executionRecord.id, tenantId, "draft");
|
||||
return { execution, results };
|
||||
} catch (error) {
|
||||
await settleExecution(server, executionRecord.id, tenantId, "error", "Fehler beim Vorbereiten der Rechnungen.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const finishManualGeneration = async (server: FastifyInstance, executionId: string, tenantId: number) => {
|
||||
const [executionRecord] = await server.db.update(schema.serialExecutions)
|
||||
.set({ status: "processing" }).where(and(
|
||||
eq(schema.serialExecutions.id, executionId),
|
||||
eq(schema.serialExecutions.tenant, tenantId),
|
||||
inArray(schema.serialExecutions.status, ["draft", "error"]),
|
||||
)).returning();
|
||||
if (!executionRecord) throw Object.assign(new Error("Durchlauf kann nicht fertiggestellt werden."), { statusCode: 409 });
|
||||
try {
|
||||
console.log(`Beende Ausführung ${executionId}...`);
|
||||
|
||||
// 1. Execution und Tenant laden
|
||||
|
||||
const [executionRecord] = await server.db
|
||||
.select()
|
||||
.from(schema.serialExecutions)// @ts-ignore
|
||||
.where(and(
|
||||
eq(schema.serialExecutions.id, executionId),
|
||||
eq(schema.serialExecutions.tenant, tenantId),
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (!executionRecord) throw new Error("Execution nicht gefunden");
|
||||
|
||||
console.log(executionRecord);
|
||||
|
||||
console.log(tenantId)
|
||||
|
||||
// Tenant laden (für Settings etc.)
|
||||
const [tenant] = await server.db
|
||||
.select()
|
||||
@@ -125,19 +174,13 @@ export const finishManualGeneration = async (server: FastifyInstance, executionI
|
||||
.where(eq(schema.tenants.id, tenantId))
|
||||
.limit(1);
|
||||
|
||||
// 2. Status auf "processing" setzen (optional, damit UI feedback hat)
|
||||
|
||||
/*await server.db
|
||||
.update(schema.serialExecutions)
|
||||
.set({ status: "processing" })// @ts-ignore
|
||||
.where(eq(schema.serialExecutions.id, executionId));*/
|
||||
|
||||
// 3. Alle erstellten Dokumente dieser Execution laden
|
||||
const documents = await server.db
|
||||
.select()
|
||||
.from(schema.createddocuments)
|
||||
.where(and(
|
||||
eq(schema.createddocuments.serialexecution, executionId),
|
||||
eq(schema.createddocuments.state, "Entwurf"),
|
||||
eq(schema.createddocuments.tenant, tenantId),
|
||||
eq(schema.createddocuments.archived, false),
|
||||
));
|
||||
@@ -160,6 +203,7 @@ export const finishManualGeneration = async (server: FastifyInstance, executionI
|
||||
|
||||
// 5. Loop durch Dokumente
|
||||
for (const doc of documents) {
|
||||
if (await isCancellationRequested(server, executionId, tenantId)) break;
|
||||
try {
|
||||
|
||||
const [letterhead] = await Promise.all([
|
||||
@@ -230,31 +274,11 @@ export const finishManualGeneration = async (server: FastifyInstance, executionI
|
||||
const finalStatus = errorCount > 0 ? "error" : "completed"; // Oder 'completed' auch wenn Teilerfolge
|
||||
|
||||
|
||||
await server.db
|
||||
.update(schema.serialExecutions)
|
||||
.set({
|
||||
status: finalStatus,
|
||||
summary: `Abgeschlossen: ${successCount} erfolgreich, ${errorCount} Fehler.`
|
||||
})// @ts-ignore
|
||||
.where(and(
|
||||
eq(schema.serialExecutions.id, executionId),
|
||||
eq(schema.serialExecutions.tenant, tenantId),
|
||||
));
|
||||
|
||||
return { success: true, processed: successCount, errors: errorCount };
|
||||
|
||||
const execution = await settleExecution(server, executionId, tenantId, finalStatus,
|
||||
`Abgeschlossen: ${successCount} erfolgreich, ${errorCount} Fehler.`);
|
||||
return { success: execution.status === "completed", execution, processed: successCount, errors: errorCount };
|
||||
} catch (error) {
|
||||
console.error("Critical Error in finishManualGeneration:", error);
|
||||
// Execution auf Error setzen
|
||||
// @ts-ignore
|
||||
await server.db
|
||||
.update(schema.serialExecutions)
|
||||
.set({ status: "error", summary: "Kritischer Fehler beim Finalisieren." })
|
||||
//@ts-ignore
|
||||
.where(and(
|
||||
eq(schema.serialExecutions.id, executionId),
|
||||
eq(schema.serialExecutions.tenant, tenantId),
|
||||
));
|
||||
await settleExecution(server, executionId, tenantId, "error", "Kritischer Fehler beim Finalisieren.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import timezone from "dayjs/plugin/timezone.js";
|
||||
import {generateTimesEvaluation} from "../modules/time/evaluation.service";
|
||||
import {citys, files} from "../../db/schema";
|
||||
import {and, eq, isNull, not} from "drizzle-orm";
|
||||
import {executeManualGeneration, finishManualGeneration} from "../modules/serialexecution.service";
|
||||
import {cancelManualGeneration, executeManualGeneration, finishManualGeneration} from "../modules/serialexecution.service";
|
||||
import { updateOutgoingDocumentCostCentres } from "../modules/outgoing-document-cost-centres.service";
|
||||
import { s3 } from "../utils/s3";
|
||||
import { secrets } from "../utils/secrets";
|
||||
@@ -301,6 +301,13 @@ export default async function functionRoutes(server: FastifyInstance) {
|
||||
return executeManualGeneration(server, executionDate, templateIds, req.user.tenant_id, req.user.user_id)
|
||||
})
|
||||
|
||||
server.post('/functions/serial/cancel/:execution_id', {
|
||||
schema: { params: { type: 'object', required: ['execution_id'], properties: { execution_id: { type: 'string', format: 'uuid' } } } },
|
||||
}, async (req) => {
|
||||
const { execution_id } = req.params as { execution_id: string }
|
||||
return cancelManualGeneration(server, execution_id, req.user.tenant_id)
|
||||
})
|
||||
|
||||
server.post('/functions/serial/finish/:execution_id', async (req, reply) => {
|
||||
const {execution_id} = req.params as { execution_id: string }
|
||||
//@ts-ignore
|
||||
|
||||
105
backend/tests/serialExecutionCancellation.test.ts
Normal file
105
backend/tests/serialExecutionCancellation.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { PgDialect } from 'drizzle-orm/pg-core'
|
||||
import * as schema from '../db/schema'
|
||||
import { cancelManualGeneration, finishManualGeneration } from '../src/modules/serialexecution.service'
|
||||
|
||||
const id = '00000000-0000-4000-8000-000000000001'
|
||||
const dialect = new PgDialect()
|
||||
|
||||
function fixture(status: string | null, options: { cancelDuringFinish?: boolean } = {}) {
|
||||
const execution = status ? { id, tenant: 7, status } : null
|
||||
const calls: any[] = []
|
||||
const db: any = {
|
||||
transaction: async (fn: any) => fn(db),
|
||||
select: () => query('select'),
|
||||
update: (table: any) => query('update', table),
|
||||
}
|
||||
function query(kind: string, table?: any) {
|
||||
let values: any
|
||||
let condition: any
|
||||
const builder: any = {
|
||||
from: (value: any) => { table = value; return builder },
|
||||
set: (value: any) => { values = value; return builder },
|
||||
where: (value: any) => { condition = value; return builder },
|
||||
limit: () => builder,
|
||||
for: (lock: string) => { assert.equal(lock, 'update'); return builder },
|
||||
returning: () => builder,
|
||||
then: (resolve: any, reject: any) => Promise.resolve().then(() => {
|
||||
const sql = condition ? dialect.sqlToQuery(condition) : null
|
||||
calls.push({ kind, table, values, sql })
|
||||
if (table === schema.serialExecutions) {
|
||||
if (!execution || !sql.params.includes(7)) return []
|
||||
if (values?.status === 'processing' && !['draft', 'error'].includes(execution.status)) return []
|
||||
if (values) Object.assign(execution, values)
|
||||
return [{ ...execution }]
|
||||
}
|
||||
if (table === schema.createddocuments && kind === 'select' && options.cancelDuringFinish) {
|
||||
execution!.status = 'cancelling'
|
||||
return [{ id: 123, state: 'Entwurf' }]
|
||||
}
|
||||
if (table === schema.tenants) return [{ id: 7 }]
|
||||
return []
|
||||
}).then(resolve, reject),
|
||||
}
|
||||
return builder
|
||||
}
|
||||
return { server: { db } as any, execution, calls }
|
||||
}
|
||||
|
||||
test('cancels drafts and archives only unarchived draft invoices of the same tenant and run', async () => {
|
||||
const f = fixture('draft')
|
||||
const result = await cancelManualGeneration(f.server, id, 7)
|
||||
assert.equal(result.execution.status, 'cancelled')
|
||||
const archive = f.calls.find(c => c.table === schema.createddocuments)
|
||||
assert.deepEqual(archive.values, { archived: true })
|
||||
assert.deepEqual(archive.sql.params, [id, 7, 'Entwurf', false])
|
||||
for (const column of ['serialexecution', 'tenant', 'state', 'archived']) {
|
||||
assert.ok(archive.sql.sql.includes(`"${column}"`))
|
||||
}
|
||||
})
|
||||
|
||||
for (const status of ['generating', 'processing']) {
|
||||
test(`requests cooperative cancellation while ${status}`, async () => {
|
||||
const f = fixture(status)
|
||||
const result = await cancelManualGeneration(f.server, id, 7)
|
||||
assert.equal(result.execution.status, 'cancelling')
|
||||
assert.equal(f.calls.some(c => c.table === schema.createddocuments), false)
|
||||
})
|
||||
}
|
||||
|
||||
for (const status of ['cancelled', 'cancelling']) {
|
||||
test(`repeated cancellation of ${status} is idempotent`, async () => {
|
||||
const f = fixture(status)
|
||||
await cancelManualGeneration(f.server, id, 7)
|
||||
assert.equal(f.calls.some(c => c.kind === 'update'), false)
|
||||
})
|
||||
}
|
||||
|
||||
test('rejects completed runs and missing or foreign-tenant executions without writes', async () => {
|
||||
for (const [status, tenant, code] of [['completed', 7, 409], [null, 7, 404], ['draft', 8, 404]] as const) {
|
||||
const f = fixture(status)
|
||||
await assert.rejects(cancelManualGeneration(f.server, id, tenant), { statusCode: code })
|
||||
assert.equal(f.calls.some(c => c.kind === 'update'), false)
|
||||
}
|
||||
})
|
||||
|
||||
test('cannot finalize cancelled, cancelling, completed or already processing runs', async () => {
|
||||
for (const status of ['cancelled', 'cancelling', 'completed', 'processing', 'generating']) {
|
||||
const f = fixture(status)
|
||||
await assert.rejects(finishManualGeneration(f.server, id, 7), { statusCode: 409 })
|
||||
assert.equal(f.execution!.status, status)
|
||||
assert.equal(f.calls.length, 1)
|
||||
}
|
||||
})
|
||||
|
||||
test('cancellation during finalization skips remaining invoices and settles as cancelled', async () => {
|
||||
const f = fixture('draft', { cancelDuringFinish: true })
|
||||
const result = await finishManualGeneration(f.server, id, 7)
|
||||
assert.equal(result.execution.status, 'cancelled')
|
||||
assert.equal(result.processed, 0)
|
||||
assert.equal(result.success, false)
|
||||
const writes = f.calls.filter(c => c.kind === 'update' && c.table === schema.createddocuments)
|
||||
assert.equal(writes.length, 1)
|
||||
assert.deepEqual(writes[0].values, { archived: true })
|
||||
})
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user