update
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import bestellungService from '../services/bestellung.service';
|
||||
import { permissionService } from '../services/permission.service';
|
||||
import logger from '../utils/logger';
|
||||
import fs from 'fs';
|
||||
|
||||
@@ -227,7 +228,34 @@ class BestellungController {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const order = await bestellungService.updateOrderStatus(id, status, req.user!.id, !!force);
|
||||
// For force override, require manage_orders
|
||||
if (force) {
|
||||
const canManage = permissionService.hasPermission(req.user!.groups || [], 'bestellungen:manage_orders');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung für manuelle Statusänderung' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For approval/rejection transitions, require bestellungen:approve
|
||||
if (status === 'bereit_zur_bestellung' || status === 'entwurf') {
|
||||
// Check if this is an approval/rejection (from wartet_auf_genehmigung)
|
||||
const currentOrder = await bestellungService.getOrderById(id);
|
||||
if (!currentOrder) {
|
||||
res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
const currentStatus = currentOrder.bestellung.status;
|
||||
if (currentStatus === 'wartet_auf_genehmigung') {
|
||||
const canApprove = permissionService.hasPermission(req.user!.groups || [], 'bestellungen:approve');
|
||||
if (!canApprove) {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung zur Genehmigung/Ablehnung von Bestellungen' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const order = await bestellungService.updateOrderStatus(id, status, req.user!.id, !!force, req.user!.id);
|
||||
if (!order) {
|
||||
res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' });
|
||||
return;
|
||||
@@ -327,7 +355,11 @@ class BestellungController {
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: item });
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.statusCode === 400) {
|
||||
res.status(400).json({ success: false, message: error.message });
|
||||
return;
|
||||
}
|
||||
logger.error('BestellungController.updateReceivedQuantity error', { error });
|
||||
res.status(500).json({ success: false, message: 'Liefermenge konnte nicht aktualisiert werden' });
|
||||
}
|
||||
@@ -501,6 +533,11 @@ class BestellungController {
|
||||
res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
const orderStatus = order.bestellung.status;
|
||||
if (orderStatus === 'entwurf' || orderStatus === 'wartet_auf_genehmigung') {
|
||||
res.status(403).json({ success: false, message: 'Export nur nach Genehmigung verfügbar' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: order });
|
||||
} catch (error) {
|
||||
logger.error('BestellungController.exportOrder error', { error });
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
-- Migration 064: Add spezifikationen to line items, approval columns, and new status workflow
|
||||
-- 1. spezifikationen JSONB on bestellpositionen
|
||||
-- 2. genehmigt_von / genehmigt_am on bestellungen
|
||||
-- 3. Drop old status CHECK constraint
|
||||
-- 4. Data migration: old statuses → new statuses
|
||||
-- 5. Add new status CHECK constraint with approval workflow statuses
|
||||
-- 6. bestellungen:approve permission + seed for dashboard_kommando
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 1. Spezifikationen on line items
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE bestellpositionen
|
||||
ADD COLUMN IF NOT EXISTS spezifikationen JSONB DEFAULT '[]';
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 2. Approval columns on bestellungen
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE bestellungen
|
||||
ADD COLUMN IF NOT EXISTS genehmigt_von UUID REFERENCES users(id),
|
||||
ADD COLUMN IF NOT EXISTS genehmigt_am TIMESTAMPTZ;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 3. Drop old status CHECK constraint
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE bestellungen DROP CONSTRAINT IF EXISTS bestellungen_status_check;
|
||||
EXCEPTION WHEN undefined_object THEN
|
||||
NULL;
|
||||
END $$;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 4. Data migration: map old statuses to new ones (before new constraint)
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
UPDATE bestellungen SET status = 'bereit_zur_bestellung' WHERE status = 'erstellt';
|
||||
UPDATE bestellungen SET status = 'lieferung_pruefen' WHERE status = 'vollstaendig';
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 5. Add new status CHECK constraint
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE bestellungen
|
||||
ADD CONSTRAINT bestellungen_status_check
|
||||
CHECK (status IN ('entwurf','wartet_auf_genehmigung','bereit_zur_bestellung','bestellt','teillieferung','lieferung_pruefen','abgeschlossen'));
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 6. Add bestellungen:approve permission
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||
('bestellungen:approve', 'bestellungen', 'Genehmigen', 'Bestellungen genehmigen', 25)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 7. Seed grant for dashboard_kommando
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||
('dashboard_kommando', 'bestellungen:approve')
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -102,7 +102,7 @@ router.delete(
|
||||
router.patch(
|
||||
'/:id/status',
|
||||
authenticate,
|
||||
requirePermission('bestellungen:manage_orders'),
|
||||
requirePermission('bestellungen:create'),
|
||||
bestellungController.updateStatus.bind(bestellungController)
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user