shared catalog in Bestellungen, catalog picker in line items, Ersatzbeschaffung flag, vendor detail flash fix

This commit is contained in:
Matthias Hochmeister
2026-03-27 14:50:31 +01:00
parent c704e2c173
commit 29d66e37a1
16 changed files with 506 additions and 32 deletions

View File

@@ -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,

View File

@@ -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,