This commit is contained in:
Matthias Hochmeister
2026-03-26 14:22:35 +01:00
parent 3c95b7506b
commit c29b21f714
9 changed files with 400 additions and 103 deletions

View File

@@ -5,6 +5,8 @@
import pool from '../config/database';
import logger from '../utils/logger';
import fs from 'fs';
import notificationService from './notification.service';
import { permissionService } from './permission.service';
// ---------------------------------------------------------------------------
// Vendors (Lieferanten)
@@ -318,20 +320,22 @@ async function deleteOrder(id: number, _userId: string) {
}
const VALID_STATUS_TRANSITIONS: Record<string, string[]> = {
entwurf: ['erstellt', 'bestellt'],
erstellt: ['bestellt'],
bestellt: ['teillieferung', 'vollstaendig'],
teillieferung: ['vollstaendig'],
vollstaendig: ['abgeschlossen'],
abgeschlossen: [],
entwurf: ['wartet_auf_genehmigung'],
wartet_auf_genehmigung: ['bereit_zur_bestellung', 'entwurf'],
bereit_zur_bestellung: ['bestellt'],
bestellt: ['teillieferung', 'lieferung_pruefen'],
teillieferung: ['lieferung_pruefen'],
lieferung_pruefen: ['abgeschlossen'],
abgeschlossen: [],
};
async function updateOrderStatus(id: number, status: string, userId: string, force?: boolean) {
async function updateOrderStatus(id: number, status: string, userId: string, force?: boolean, approverUserId?: string) {
try {
const current = await pool.query(`SELECT status FROM bestellungen WHERE id = $1`, [id]);
const current = await pool.query(`SELECT * FROM bestellungen WHERE id = $1`, [id]);
if (current.rows.length === 0) return null;
const oldStatus = current.rows[0].status;
const order = current.rows[0];
const oldStatus = order.status;
if (!force) {
const allowed = VALID_STATUS_TRANSITIONS[oldStatus] || [];
if (!allowed.includes(status)) {
@@ -349,6 +353,12 @@ async function updateOrderStatus(id: number, status: string, userId: string, for
if (status === 'abgeschlossen') {
updates.push(`abgeschlossen_am = COALESCE(abgeschlossen_am, NOW())`);
}
if (status === 'bereit_zur_bestellung' && approverUserId) {
updates.push(`genehmigt_von = $${paramIndex}`);
params.push(approverUserId);
paramIndex++;
updates.push(`genehmigt_am = NOW()`);
}
params.push(id);
const result = await pool.query(
@@ -357,6 +367,53 @@ async function updateOrderStatus(id: number, status: string, userId: string, for
);
await logAction(id, 'Status geändert', `${oldStatus}${status}${force ? ' (manuell)' : ''}`, userId);
// Notifications based on transition
const orderLabel = order.laufende_nummer ? `#${order.laufende_nummer}` : `#${order.id}`;
const bestellerId = order.besteller_id || order.erstellt_von;
if (status === 'wartet_auf_genehmigung') {
try {
const approvers = await permissionService.getUsersWithPermission('bestellungen:approve');
for (const user of approvers) {
await notificationService.createNotification({
user_id: user.id,
typ: 'bestellung',
titel: 'Bestellung wartet auf Genehmigung',
nachricht: `Bestellung ${orderLabel} wurde zur Genehmigung eingereicht`,
schwere: 'info',
quell_typ: 'bestellung_genehmigung',
quell_id: order.id.toString(),
link: `/bestellungen/${id}`,
});
}
} catch (err) {
logger.error('Failed to notify approvers', { err, orderId: id });
}
} else if (status === 'bereit_zur_bestellung') {
await notificationService.createNotification({
user_id: bestellerId,
typ: 'bestellung',
titel: 'Bestellung genehmigt',
nachricht: `Bestellung ${orderLabel} wurde genehmigt und ist bereit zur Bestellung`,
schwere: 'info',
quell_typ: 'bestellung_genehmigung',
quell_id: order.id.toString(),
link: `/bestellungen/${id}`,
});
} else if (status === 'entwurf' && oldStatus === 'wartet_auf_genehmigung') {
await notificationService.createNotification({
user_id: bestellerId,
typ: 'bestellung',
titel: 'Bestellung abgelehnt',
nachricht: `Bestellung ${orderLabel} wurde abgelehnt und zurück in den Entwurf gesetzt`,
schwere: 'warnung',
quell_typ: 'bestellung_genehmigung',
quell_id: order.id.toString(),
link: `/bestellungen/${id}`,
});
}
return result.rows[0];
} catch (error) {
logger.error('BestellungService.updateOrderStatus failed', { error, id });
@@ -368,13 +425,13 @@ async function updateOrderStatus(id: number, status: string, userId: string, for
// Line Items (Bestellpositionen)
// ---------------------------------------------------------------------------
async function addLineItem(bestellungId: number, data: { bezeichnung: string; artikelnummer?: string; menge: number; einheit?: string; einzelpreis?: number; notizen?: string }, userId: string) {
async function addLineItem(bestellungId: number, data: { bezeichnung: string; artikelnummer?: string; menge: number; einheit?: string; einzelpreis?: number; notizen?: string; spezifikationen?: string[] }, userId: string) {
try {
const result = await pool.query(
`INSERT INTO bestellpositionen (bestellung_id, bezeichnung, artikelnummer, menge, einheit, einzelpreis, notizen)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`INSERT INTO bestellpositionen (bestellung_id, bezeichnung, artikelnummer, menge, einheit, einzelpreis, notizen, spezifikationen)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)
RETURNING *`,
[bestellungId, data.bezeichnung, data.artikelnummer || null, data.menge, data.einheit || 'Stk', data.einzelpreis || 0, data.notizen || null]
[bestellungId, data.bezeichnung, data.artikelnummer || null, data.menge, data.einheit || 'Stk', data.einzelpreis || 0, data.notizen || null, JSON.stringify(data.spezifikationen || [])]
);
await logAction(bestellungId, 'Position hinzugefügt', `"${data.bezeichnung}" x${data.menge}`, userId);
return result.rows[0];
@@ -384,7 +441,7 @@ async function addLineItem(bestellungId: number, data: { bezeichnung: string; ar
}
}
async function updateLineItem(id: number, data: { bezeichnung?: string; artikelnummer?: string; menge?: number; einheit?: string; einzelpreis?: number; notizen?: string }, userId: string) {
async function updateLineItem(id: number, data: { bezeichnung?: string; artikelnummer?: string; menge?: number; einheit?: string; einzelpreis?: number; notizen?: string; spezifikationen?: string[] }, userId: string) {
try {
const result = await pool.query(
`UPDATE bestellpositionen
@@ -393,10 +450,11 @@ async function updateLineItem(id: number, data: { bezeichnung?: string; artikeln
menge = COALESCE($3, menge),
einheit = COALESCE($4, einheit),
einzelpreis = COALESCE($5, einzelpreis),
notizen = COALESCE($6, notizen)
WHERE id = $7
notizen = COALESCE($6, notizen),
spezifikationen = COALESCE($7::jsonb, spezifikationen)
WHERE id = $8
RETURNING *`,
[data.bezeichnung, data.artikelnummer, data.menge, data.einheit, data.einzelpreis, data.notizen, id]
[data.bezeichnung, data.artikelnummer, data.menge, data.einheit, data.einzelpreis, data.notizen, data.spezifikationen ? JSON.stringify(data.spezifikationen) : null, id]
);
if (result.rows.length === 0) return null;
@@ -425,6 +483,21 @@ async function deleteLineItem(id: number, userId: string) {
async function updateReceivedQuantity(id: number, menge: number, userId: string) {
try {
// First check if the item's order is in a valid status for receiving
const itemCheck = await pool.query(
`SELECT bp.bestellung_id, b.status
FROM bestellpositionen bp
JOIN bestellungen b ON b.id = bp.bestellung_id
WHERE bp.id = $1`,
[id]
);
if (itemCheck.rows.length > 0) {
const orderStatus = itemCheck.rows[0].status;
if (orderStatus !== 'bestellt' && orderStatus !== 'teillieferung') {
throw Object.assign(new Error('Mengenaktualisierung nur bei Status bestellt oder Teillieferung möglich'), { statusCode: 400 });
}
}
const result = await pool.query(
`UPDATE bestellpositionen SET erhalten_menge = $1 WHERE id = $2 RETURNING *`,
[menge, id]
@@ -447,10 +520,10 @@ async function updateReceivedQuantity(id: number, menge: number, userId: string)
if (order.rows.length > 0 && (order.rows[0].status === 'bestellt' || order.rows[0].status === 'teillieferung')) {
if (allReceived) {
await pool.query(
`UPDATE bestellungen SET status = 'vollstaendig', aktualisiert_am = NOW() WHERE id = $1`,
`UPDATE bestellungen SET status = 'lieferung_pruefen', aktualisiert_am = NOW() WHERE id = $1`,
[item.bestellung_id]
);
await logAction(item.bestellung_id, 'Status geändert', 'Alle Positionen vollständig erhalten → vollstaendig', userId);
await logAction(item.bestellung_id, 'Status geändert', 'Alle Positionen vollständig erhalten → lieferung_pruefen', userId);
} else if (someReceived && order.rows[0].status === 'bestellt') {
await pool.query(
`UPDATE bestellungen SET status = 'teillieferung', aktualisiert_am = NOW() WHERE id = $1`,
@@ -463,7 +536,7 @@ async function updateReceivedQuantity(id: number, menge: number, userId: string)
return item;
} catch (error) {
logger.error('BestellungService.updateReceivedQuantity failed', { error, id });
throw new Error('Liefermenge konnte nicht aktualisiert werden');
throw error;
}
}