shared catalog in Bestellungen, catalog picker in line items, Ersatzbeschaffung flag, vendor detail flash fix
This commit is contained in:
@@ -460,6 +460,29 @@ class AusruestungsanfrageController {
|
||||
}
|
||||
}
|
||||
|
||||
async updatePositionZurueckgegeben(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const positionId = Number(req.params.positionId);
|
||||
const { altes_geraet_zurueckgegeben } = req.body as { altes_geraet_zurueckgegeben?: boolean };
|
||||
|
||||
if (typeof altes_geraet_zurueckgegeben !== 'boolean') {
|
||||
res.status(400).json({ success: false, message: 'altes_geraet_zurueckgegeben (boolean) ist erforderlich' });
|
||||
return;
|
||||
}
|
||||
|
||||
const position = await ausruestungsanfrageService.updatePositionZurueckgegeben(positionId, altes_geraet_zurueckgegeben);
|
||||
if (!position) {
|
||||
res.status(404).json({ success: false, message: 'Position nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true, data: position });
|
||||
} catch (error) {
|
||||
logger.error('AusruestungsanfrageController.updatePositionZurueckgegeben error', { error });
|
||||
res.status(500).json({ success: false, message: 'Rückgabestatus konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Overview
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -8,6 +8,51 @@ import fs from 'fs';
|
||||
const param = (req: Request, key: string): string => req.params[key] as string;
|
||||
|
||||
class BestellungController {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catalog (shared ausruestung_artikel)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async listKatalogItems(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const search = req.query.search as string | undefined;
|
||||
const kategorie = req.query.kategorie as string | undefined;
|
||||
const items = await bestellungService.getKatalogItems({ search, kategorie });
|
||||
res.status(200).json({ success: true, data: items });
|
||||
} catch (error) {
|
||||
logger.error('BestellungController.listKatalogItems error', { error });
|
||||
res.status(500).json({ success: false, message: 'Katalogartikel konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getKatalogItem(req: Request, res: Response): Promise<void> {
|
||||
const id = parseInt(param(req, 'itemId'), 10);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige ID' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const item = await bestellungService.getKatalogItem(id);
|
||||
if (!item) {
|
||||
res.status(404).json({ success: false, message: 'Katalogartikel nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: item });
|
||||
} catch (error) {
|
||||
logger.error('BestellungController.getKatalogItem error', { error });
|
||||
res.status(500).json({ success: false, message: 'Katalogartikel konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async listKatalogKategorien(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const kategorien = await bestellungService.getKatalogKategorien();
|
||||
res.status(200).json({ success: true, data: kategorien });
|
||||
} catch (error) {
|
||||
logger.error('BestellungController.listKatalogKategorien error', { error });
|
||||
res.status(500).json({ success: false, message: 'Kategorien konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vendors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Migration 065: Shared catalog + request type fields
|
||||
-- Link bestellpositionen to ausruestung_artikel (shared catalog)
|
||||
ALTER TABLE bestellpositionen
|
||||
ADD COLUMN IF NOT EXISTS artikel_id INT REFERENCES ausruestung_artikel(id) ON DELETE SET NULL;
|
||||
|
||||
-- Add replacement/return fields to ausruestung_anfrage_positionen
|
||||
ALTER TABLE ausruestung_anfrage_positionen
|
||||
ADD COLUMN IF NOT EXISTS ist_ersatz BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS altes_geraet_zurueckgegeben BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -62,6 +62,7 @@ router.delete('/requests/:id', authenticate, requirePermission('ausruestungsanfr
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.patch('/positionen/:positionId/geliefert', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.updatePositionGeliefert.bind(ausruestungsanfrageController));
|
||||
router.patch('/positionen/:positionId/zurueckgegeben', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.updatePositionZurueckgegeben.bind(ausruestungsanfrageController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Linking requests to orders
|
||||
|
||||
@@ -52,6 +52,31 @@ router.delete(
|
||||
bestellungController.deleteVendor.bind(bestellungController)
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catalog (shared ausruestung_artikel, accessed via bestellungen context)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get(
|
||||
'/katalog/items',
|
||||
authenticate,
|
||||
requirePermission('bestellungen:view'),
|
||||
bestellungController.listKatalogItems.bind(bestellungController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/katalog/items/:itemId',
|
||||
authenticate,
|
||||
requirePermission('bestellungen:view'),
|
||||
bestellungController.getKatalogItem.bind(bestellungController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/katalog/kategorien',
|
||||
authenticate,
|
||||
requirePermission('bestellungen:view'),
|
||||
bestellungController.listKatalogKategorien.bind(bestellungController)
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orders (Bestellungen)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -422,7 +422,7 @@ async function getRequestById(id: number) {
|
||||
|
||||
async function createRequest(
|
||||
userId: string,
|
||||
items: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[],
|
||||
items: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; ist_ersatz?: boolean; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[],
|
||||
notizen?: string,
|
||||
bezeichnung?: string,
|
||||
fuerBenutzerName?: string,
|
||||
@@ -476,9 +476,9 @@ async function createRequest(
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[anfrage.id, item.artikel_id || null, itemBezeichnung, item.menge, item.notizen || null],
|
||||
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen, ist_ersatz)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[anfrage.id, item.artikel_id || null, itemBezeichnung, item.menge, item.notizen || null, item.ist_ersatz ?? false],
|
||||
);
|
||||
|
||||
// NOTE: eigenschaft values are NOT saved in the transaction to avoid
|
||||
@@ -527,7 +527,7 @@ async function updateRequest(
|
||||
data: {
|
||||
bezeichnung?: string;
|
||||
notizen?: string;
|
||||
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[];
|
||||
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; ist_ersatz?: boolean; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[];
|
||||
},
|
||||
) {
|
||||
const client = await pool.connect();
|
||||
@@ -581,9 +581,9 @@ async function updateRequest(
|
||||
}
|
||||
const prevGeliefert = geliefertMap.get(`${item.artikel_id ?? 0}:${itemBezeichnung}`) ?? false;
|
||||
await client.query(
|
||||
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen, geliefert)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[id, item.artikel_id || null, itemBezeichnung, item.menge, item.notizen || null, prevGeliefert],
|
||||
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen, geliefert, ist_ersatz)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[id, item.artikel_id || null, itemBezeichnung, item.menge, item.notizen || null, prevGeliefert, item.ist_ersatz ?? false],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -652,6 +652,14 @@ async function updatePositionGeliefert(positionId: number, geliefert: boolean) {
|
||||
return position;
|
||||
}
|
||||
|
||||
async function updatePositionZurueckgegeben(positionId: number, zurueckgegeben: boolean) {
|
||||
const result = await pool.query(
|
||||
`UPDATE ausruestung_anfrage_positionen SET altes_geraet_zurueckgegeben = $1 WHERE id = $2 RETURNING *`,
|
||||
[zurueckgegeben, positionId],
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function updateRequestStatus(
|
||||
id: number,
|
||||
status: string,
|
||||
@@ -768,10 +776,35 @@ async function createOrdersFromRequest(
|
||||
const bestellung = bestellungResult.rows[0];
|
||||
|
||||
for (const pos of orderData.positionen) {
|
||||
// Look up the anfrage position to get artikel_id and eigenschaften
|
||||
let artikelId: number | null = null;
|
||||
let spezifikationen: string[] = [];
|
||||
if (pos.position_id) {
|
||||
const posResult = await client.query(
|
||||
`SELECT p.artikel_id FROM ausruestung_anfrage_positionen p WHERE p.id = $1`,
|
||||
[pos.position_id]
|
||||
);
|
||||
if (posResult.rows.length > 0) {
|
||||
artikelId = posResult.rows[0].artikel_id || null;
|
||||
}
|
||||
// Load eigenschaften and map to spezifikationen strings
|
||||
try {
|
||||
const eigResult = await client.query(
|
||||
`SELECT ae.name AS eigenschaft_name, pe.wert
|
||||
FROM ausruestung_position_eigenschaften pe
|
||||
JOIN ausruestung_artikel_eigenschaften ae ON ae.id = pe.eigenschaft_id
|
||||
WHERE pe.position_id = $1
|
||||
ORDER BY ae.sort_order, ae.id`,
|
||||
[pos.position_id]
|
||||
);
|
||||
spezifikationen = eigResult.rows.map((e: { eigenschaft_name: string; wert: string }) => `${e.eigenschaft_name}: ${e.wert}`);
|
||||
} catch { /* table may not exist */ }
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO bestellpositionen (bestellung_id, bezeichnung, menge, einheit, notizen)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[bestellung.id, pos.bezeichnung, pos.menge, pos.einheit || 'Stk', pos.notizen || null]
|
||||
`INSERT INTO bestellpositionen (bestellung_id, bezeichnung, menge, einheit, notizen, artikel_id, spezifikationen)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,
|
||||
[bestellung.id, pos.bezeichnung, pos.menge, pos.einheit || 'Stk', pos.notizen || null, artikelId, JSON.stringify(spezifikationen)]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -875,6 +908,7 @@ export default {
|
||||
getMyRequests,
|
||||
getRequestById,
|
||||
updatePositionGeliefert,
|
||||
updatePositionZurueckgegeben,
|
||||
createRequest,
|
||||
updateRequest,
|
||||
updateRequestStatus,
|
||||
|
||||
@@ -7,6 +7,38 @@ import logger from '../utils/logger';
|
||||
import fs from 'fs';
|
||||
import notificationService from './notification.service';
|
||||
import { permissionService } from './permission.service';
|
||||
import ausruestungsanfrageService from './ausruestungsanfrage.service';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catalog (shared ausruestung_artikel via ausruestungsanfrageService)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getKatalogItems(filters?: { search?: string; kategorie?: string }) {
|
||||
try {
|
||||
return await ausruestungsanfrageService.getItems({ search: filters?.search, kategorie: filters?.kategorie, aktiv: true });
|
||||
} catch (error) {
|
||||
logger.error('BestellungService.getKatalogItems failed', { error });
|
||||
throw new Error('Katalogartikel konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function getKatalogItem(id: number) {
|
||||
try {
|
||||
return await ausruestungsanfrageService.getItemById(id);
|
||||
} catch (error) {
|
||||
logger.error('BestellungService.getKatalogItem failed', { error, id });
|
||||
throw new Error('Katalogartikel konnte nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function getKatalogKategorien() {
|
||||
try {
|
||||
return await ausruestungsanfrageService.getKategorien();
|
||||
} catch (error) {
|
||||
logger.error('BestellungService.getKatalogKategorien failed', { error });
|
||||
throw new Error('Katalogkategorien konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vendors (Lieferanten)
|
||||
@@ -168,7 +200,13 @@ async function getOrderById(id: number) {
|
||||
if (orderResult.rows.length === 0) return null;
|
||||
|
||||
const [positionen, dateien, erinnerungen, historie] = await Promise.all([
|
||||
pool.query(`SELECT * FROM bestellpositionen WHERE bestellung_id = $1 ORDER BY id`, [id]),
|
||||
pool.query(
|
||||
`SELECT bp.*, aa.bezeichnung AS artikel_bezeichnung
|
||||
FROM bestellpositionen bp
|
||||
LEFT JOIN ausruestung_artikel aa ON aa.id = bp.artikel_id
|
||||
WHERE bp.bestellung_id = $1 ORDER BY bp.id`,
|
||||
[id]
|
||||
),
|
||||
pool.query(`SELECT * FROM bestellung_dateien WHERE bestellung_id = $1 ORDER BY hochgeladen_am DESC`, [id]),
|
||||
pool.query(`SELECT * FROM bestellung_erinnerungen WHERE bestellung_id = $1 ORDER BY faellig_am`, [id]),
|
||||
pool.query(`SELECT h.*, COALESCE(u.name, u.preferred_username, u.email) AS benutzer_name FROM bestellung_historie h LEFT JOIN users u ON u.id = h.erstellt_von WHERE h.bestellung_id = $1 ORDER BY h.erstellt_am DESC`, [id]),
|
||||
@@ -425,13 +463,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; spezifikationen?: string[] }, userId: string) {
|
||||
async function addLineItem(bestellungId: number, data: { bezeichnung: string; artikelnummer?: string; menge: number; einheit?: string; einzelpreis?: number; notizen?: string; spezifikationen?: string[]; artikel_id?: number }, userId: string) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO bestellpositionen (bestellung_id, bezeichnung, artikelnummer, menge, einheit, einzelpreis, notizen, spezifikationen)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)
|
||||
`INSERT INTO bestellpositionen (bestellung_id, bezeichnung, artikelnummer, menge, einheit, einzelpreis, notizen, spezifikationen, artikel_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
|
||||
RETURNING *`,
|
||||
[bestellungId, data.bezeichnung, data.artikelnummer || null, data.menge, data.einheit || 'Stk', data.einzelpreis || 0, data.notizen || null, JSON.stringify(data.spezifikationen || [])]
|
||||
[bestellungId, data.bezeichnung, data.artikelnummer || null, data.menge, data.einheit || 'Stk', data.einzelpreis || 0, data.notizen || null, JSON.stringify(data.spezifikationen || []), data.artikel_id || null]
|
||||
);
|
||||
await logAction(bestellungId, 'Position hinzugefügt', `"${data.bezeichnung}" x${data.menge}`, userId);
|
||||
return result.rows[0];
|
||||
@@ -693,6 +731,10 @@ async function getHistory(bestellungId: number) {
|
||||
}
|
||||
|
||||
export default {
|
||||
// Catalog
|
||||
getKatalogItems,
|
||||
getKatalogItem,
|
||||
getKatalogKategorien,
|
||||
// Vendors
|
||||
getVendors,
|
||||
getVendorById,
|
||||
|
||||
Reference in New Issue
Block a user