770 lines
25 KiB
TypeScript
770 lines
25 KiB
TypeScript
import pool from '../config/database';
|
|
import logger from '../utils/logger';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Categories (ausruestung_kategorien_katalog) — hierarchical with parent_id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function getKategorien() {
|
|
try {
|
|
const result = await pool.query(
|
|
'SELECT * FROM ausruestung_kategorien_katalog ORDER BY parent_id NULLS FIRST, name',
|
|
);
|
|
return result.rows;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function createKategorie(name: string, parentId?: number | null) {
|
|
const result = await pool.query(
|
|
'INSERT INTO ausruestung_kategorien_katalog (name, parent_id) VALUES ($1, $2) RETURNING *',
|
|
[name, parentId ?? null],
|
|
);
|
|
return result.rows[0];
|
|
}
|
|
|
|
async function updateKategorie(id: number, data: { name?: string; parent_id?: number | null }) {
|
|
const fields: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
if (data.name !== undefined) {
|
|
params.push(data.name);
|
|
fields.push(`name = $${params.length}`);
|
|
}
|
|
if (data.parent_id !== undefined) {
|
|
params.push(data.parent_id);
|
|
fields.push(`parent_id = $${params.length}`);
|
|
}
|
|
|
|
if (fields.length === 0) return null;
|
|
|
|
params.push(id);
|
|
const result = await pool.query(
|
|
`UPDATE ausruestung_kategorien_katalog SET ${fields.join(', ')} WHERE id = $${params.length} RETURNING *`,
|
|
params,
|
|
);
|
|
return result.rows[0] || null;
|
|
}
|
|
|
|
async function deleteKategorie(id: number) {
|
|
await pool.query('DELETE FROM ausruestung_kategorien_katalog WHERE id = $1', [id]);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Catalog Items (ausruestung_artikel)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function getItems(filters?: { kategorie?: string; kategorie_id?: number; aktiv?: boolean }) {
|
|
const conditions: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
if (filters?.kategorie) {
|
|
params.push(filters.kategorie);
|
|
conditions.push(`kategorie = $${params.length}`);
|
|
}
|
|
if (filters?.kategorie_id) {
|
|
params.push(filters.kategorie_id);
|
|
conditions.push(`kategorie_id = $${params.length}`);
|
|
}
|
|
if (filters?.aktiv !== undefined) {
|
|
params.push(filters.aktiv);
|
|
conditions.push(`aktiv = $${params.length}`);
|
|
}
|
|
|
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
const result = await pool.query(
|
|
`SELECT * FROM ausruestung_artikel ${where} ORDER BY kategorie, bezeichnung`,
|
|
params,
|
|
);
|
|
|
|
// Enrich with kategorie_name and eigenschaften_count if tables exist
|
|
const rows = result.rows;
|
|
try {
|
|
const katRows = await pool.query('SELECT id, name FROM ausruestung_kategorien_katalog');
|
|
const katMap = new Map(katRows.rows.map((k: { id: number; name: string }) => [k.id, k.name]));
|
|
for (const row of rows) {
|
|
row.kategorie_name = row.kategorie_id ? katMap.get(row.kategorie_id) || null : null;
|
|
}
|
|
} catch { /* table doesn't exist yet */ }
|
|
|
|
try {
|
|
const eigCounts = await pool.query(
|
|
'SELECT artikel_id, COUNT(*)::int AS cnt FROM ausruestung_artikel_eigenschaften GROUP BY artikel_id',
|
|
);
|
|
const eigMap = new Map(eigCounts.rows.map((e: { artikel_id: number; cnt: number }) => [e.artikel_id, e.cnt]));
|
|
for (const row of rows) {
|
|
row.eigenschaften_count = eigMap.get(row.id) || 0;
|
|
}
|
|
} catch { /* table doesn't exist yet */ }
|
|
|
|
return rows;
|
|
}
|
|
|
|
async function getItemById(id: number) {
|
|
const result = await pool.query('SELECT * FROM ausruestung_artikel WHERE id = $1', [id]);
|
|
if (!result.rows[0]) return null;
|
|
|
|
const row = result.rows[0];
|
|
|
|
// Enrich with kategorie_name
|
|
if (row.kategorie_id) {
|
|
try {
|
|
const kat = await pool.query('SELECT name FROM ausruestung_kategorien_katalog WHERE id = $1', [row.kategorie_id]);
|
|
row.kategorie_name = kat.rows[0]?.name || null;
|
|
} catch { /* table doesn't exist */ }
|
|
}
|
|
|
|
// Load eigenschaften
|
|
try {
|
|
const eigenschaften = await pool.query(
|
|
'SELECT * FROM ausruestung_artikel_eigenschaften WHERE artikel_id = $1 ORDER BY sort_order, id',
|
|
[id],
|
|
);
|
|
row.eigenschaften = eigenschaften.rows;
|
|
} catch {
|
|
row.eigenschaften = [];
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
async function createItem(
|
|
data: {
|
|
bezeichnung: string;
|
|
beschreibung?: string;
|
|
kategorie?: string;
|
|
kategorie_id?: number;
|
|
geschaetzter_preis?: number;
|
|
aktiv?: boolean;
|
|
},
|
|
userId: string,
|
|
) {
|
|
// Build column list dynamically based on whether kategorie_id column exists
|
|
const cols = ['bezeichnung', 'beschreibung', 'kategorie', 'geschaetzter_preis', 'aktiv', 'erstellt_von'];
|
|
const vals = [data.bezeichnung, data.beschreibung || null, data.kategorie || null, data.geschaetzter_preis || null, data.aktiv ?? true, userId];
|
|
|
|
if (data.kategorie_id) {
|
|
cols.push('kategorie_id');
|
|
vals.push(data.kategorie_id as unknown as string);
|
|
}
|
|
|
|
const placeholders = vals.map((_, i) => `$${i + 1}`).join(', ');
|
|
const result = await pool.query(
|
|
`INSERT INTO ausruestung_artikel (${cols.join(', ')}) VALUES (${placeholders}) RETURNING *`,
|
|
vals,
|
|
);
|
|
return result.rows[0];
|
|
}
|
|
|
|
async function updateItem(
|
|
id: number,
|
|
data: {
|
|
bezeichnung?: string;
|
|
beschreibung?: string;
|
|
kategorie?: string;
|
|
kategorie_id?: number | null;
|
|
geschaetzter_preis?: number;
|
|
aktiv?: boolean;
|
|
},
|
|
_userId: string,
|
|
) {
|
|
const fields: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
if (data.bezeichnung !== undefined) {
|
|
params.push(data.bezeichnung);
|
|
fields.push(`bezeichnung = $${params.length}`);
|
|
}
|
|
if (data.beschreibung !== undefined) {
|
|
params.push(data.beschreibung);
|
|
fields.push(`beschreibung = $${params.length}`);
|
|
}
|
|
if (data.kategorie !== undefined) {
|
|
params.push(data.kategorie);
|
|
fields.push(`kategorie = $${params.length}`);
|
|
}
|
|
if (data.kategorie_id !== undefined) {
|
|
params.push(data.kategorie_id);
|
|
fields.push(`kategorie_id = $${params.length}`);
|
|
}
|
|
if (data.geschaetzter_preis !== undefined) {
|
|
params.push(data.geschaetzter_preis);
|
|
fields.push(`geschaetzter_preis = $${params.length}`);
|
|
}
|
|
if (data.aktiv !== undefined) {
|
|
params.push(data.aktiv);
|
|
fields.push(`aktiv = $${params.length}`);
|
|
}
|
|
|
|
if (fields.length === 0) {
|
|
return getItemById(id);
|
|
}
|
|
|
|
params.push(new Date());
|
|
fields.push(`aktualisiert_am = $${params.length}`);
|
|
|
|
params.push(id);
|
|
const result = await pool.query(
|
|
`UPDATE ausruestung_artikel SET ${fields.join(', ')} WHERE id = $${params.length} RETURNING *`,
|
|
params,
|
|
);
|
|
return result.rows[0] || null;
|
|
}
|
|
|
|
async function deleteItem(id: number) {
|
|
await pool.query('DELETE FROM ausruestung_artikel WHERE id = $1', [id]);
|
|
}
|
|
|
|
async function getCategories() {
|
|
const result = await pool.query(
|
|
'SELECT DISTINCT kategorie FROM ausruestung_artikel WHERE kategorie IS NOT NULL ORDER BY kategorie',
|
|
);
|
|
return result.rows.map((r: { kategorie: string }) => r.kategorie);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Artikel Eigenschaften (characteristics)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function getArtikelEigenschaften(artikelId: number) {
|
|
try {
|
|
const result = await pool.query(
|
|
'SELECT * FROM ausruestung_artikel_eigenschaften WHERE artikel_id = $1 ORDER BY sort_order, id',
|
|
[artikelId],
|
|
);
|
|
return result.rows;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function upsertArtikelEigenschaft(
|
|
artikelId: number,
|
|
data: { id?: number; name: string; typ: string; optionen?: string[]; pflicht?: boolean; sort_order?: number },
|
|
) {
|
|
if (data.id) {
|
|
const result = await pool.query(
|
|
`UPDATE ausruestung_artikel_eigenschaften
|
|
SET name = $1, typ = $2, optionen = $3, pflicht = $4, sort_order = $5
|
|
WHERE id = $6 AND artikel_id = $7
|
|
RETURNING *`,
|
|
[data.name, data.typ, data.optionen || null, data.pflicht ?? false, data.sort_order ?? 0, data.id, artikelId],
|
|
);
|
|
return result.rows[0] || null;
|
|
}
|
|
const result = await pool.query(
|
|
`INSERT INTO ausruestung_artikel_eigenschaften (artikel_id, name, typ, optionen, pflicht, sort_order)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING *`,
|
|
[artikelId, data.name, data.typ, data.optionen || null, data.pflicht ?? false, data.sort_order ?? 0],
|
|
);
|
|
return result.rows[0];
|
|
}
|
|
|
|
async function deleteArtikelEigenschaft(id: number) {
|
|
await pool.query('DELETE FROM ausruestung_artikel_eigenschaften WHERE id = $1', [id]);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Requests (ausruestung_anfragen)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function getRequests(filters?: { status?: string; anfrager_id?: string }) {
|
|
const conditions: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
if (filters?.status) {
|
|
params.push(filters.status);
|
|
conditions.push(`a.status = $${params.length}`);
|
|
}
|
|
if (filters?.anfrager_id) {
|
|
params.push(filters.anfrager_id);
|
|
conditions.push(`a.anfrager_id = $${params.length}`);
|
|
}
|
|
|
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
const result = await pool.query(
|
|
`SELECT a.*,
|
|
COALESCE(u.given_name || ' ' || u.family_name, u.name) AS anfrager_name,
|
|
COALESCE(u2.given_name || ' ' || u2.family_name, u2.name) AS bearbeitet_von_name,
|
|
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count
|
|
FROM ausruestung_anfragen a
|
|
LEFT JOIN users u ON u.id = a.anfrager_id
|
|
LEFT JOIN users u2 ON u2.id = a.bearbeitet_von
|
|
${where}
|
|
ORDER BY a.erstellt_am DESC`,
|
|
params,
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
async function getMyRequests(userId: string) {
|
|
const result = await pool.query(
|
|
`SELECT a.*,
|
|
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count
|
|
FROM ausruestung_anfragen a
|
|
WHERE a.anfrager_id = $1
|
|
ORDER BY a.erstellt_am DESC`,
|
|
[userId],
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
async function getRequestById(id: number) {
|
|
const reqResult = await pool.query(
|
|
`SELECT a.*,
|
|
COALESCE(u.given_name || ' ' || u.family_name, u.name) AS anfrager_name,
|
|
COALESCE(u2.given_name || ' ' || u2.family_name, u2.name) AS bearbeitet_von_name
|
|
FROM ausruestung_anfragen a
|
|
LEFT JOIN users u ON u.id = a.anfrager_id
|
|
LEFT JOIN users u2 ON u2.id = a.bearbeitet_von
|
|
WHERE a.id = $1`,
|
|
[id],
|
|
);
|
|
if (reqResult.rows.length === 0) return null;
|
|
|
|
const positionen = await pool.query(
|
|
`SELECT p.*, sa.bezeichnung AS artikel_bezeichnung, sa.kategorie AS artikel_kategorie
|
|
FROM ausruestung_anfrage_positionen p
|
|
LEFT JOIN ausruestung_artikel sa ON sa.id = p.artikel_id
|
|
WHERE p.anfrage_id = $1
|
|
ORDER BY p.id`,
|
|
[id],
|
|
);
|
|
|
|
// Load eigenschaft values per position
|
|
const positionIds = positionen.rows.map((p: { id: number }) => p.id);
|
|
let eigenschaftenMap: Record<number, { eigenschaft_id: number; eigenschaft_name: string; wert: string }[]> = {};
|
|
if (positionIds.length > 0) {
|
|
try {
|
|
const eigenschaftenResult = await pool.query(
|
|
`SELECT pe.position_id, pe.eigenschaft_id, 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 = ANY($1)
|
|
ORDER BY ae.sort_order, ae.id`,
|
|
[positionIds],
|
|
);
|
|
for (const row of eigenschaftenResult.rows) {
|
|
if (!eigenschaftenMap[row.position_id]) eigenschaftenMap[row.position_id] = [];
|
|
eigenschaftenMap[row.position_id].push({
|
|
eigenschaft_id: row.eigenschaft_id,
|
|
eigenschaft_name: row.eigenschaft_name,
|
|
wert: row.wert,
|
|
});
|
|
}
|
|
} catch { /* table may not exist */ }
|
|
}
|
|
|
|
const positionenWithEigenschaften = positionen.rows.map((p: { id: number }) => ({
|
|
...p,
|
|
eigenschaften: eigenschaftenMap[p.id] || [],
|
|
}));
|
|
|
|
// Load linked bestellungen
|
|
let linkedBestellungen: unknown[] = [];
|
|
try {
|
|
const bestellungen = await pool.query(
|
|
`SELECT b.*
|
|
FROM ausruestung_anfrage_bestellung ab
|
|
JOIN bestellungen b ON b.id = ab.bestellung_id
|
|
WHERE ab.anfrage_id = $1`,
|
|
[id],
|
|
);
|
|
linkedBestellungen = bestellungen.rows;
|
|
} catch { /* table may not exist */ }
|
|
|
|
return {
|
|
anfrage: reqResult.rows[0],
|
|
positionen: positionenWithEigenschaften,
|
|
linked_bestellungen: linkedBestellungen,
|
|
};
|
|
}
|
|
|
|
async function createRequest(
|
|
userId: string,
|
|
items: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[],
|
|
notizen?: string,
|
|
bezeichnung?: string,
|
|
) {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
// Try with bestell_nummer/bestell_jahr (migration 050), fallback without
|
|
let anfrage: Record<string, unknown>;
|
|
try {
|
|
await client.query('SAVEPOINT sp_bestell_nr');
|
|
const maxResult = await client.query(
|
|
`SELECT COALESCE(MAX(bestell_nummer), 0) + 1 AS next_nr
|
|
FROM ausruestung_anfragen
|
|
WHERE bestell_jahr = $1`,
|
|
[currentYear],
|
|
);
|
|
const nextNr = maxResult.rows[0].next_nr;
|
|
const anfrageResult = await client.query(
|
|
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, bestell_nummer, bestell_jahr)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING *`,
|
|
[userId, notizen || null, bezeichnung || null, nextNr, currentYear],
|
|
);
|
|
await client.query('RELEASE SAVEPOINT sp_bestell_nr');
|
|
anfrage = anfrageResult.rows[0];
|
|
} catch {
|
|
await client.query('ROLLBACK TO SAVEPOINT sp_bestell_nr');
|
|
const anfrageResult = await client.query(
|
|
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING *`,
|
|
[userId, notizen || null, bezeichnung || null],
|
|
);
|
|
anfrage = anfrageResult.rows[0];
|
|
}
|
|
|
|
for (const item of items) {
|
|
let itemBezeichnung = item.bezeichnung;
|
|
if (item.artikel_id) {
|
|
const artikelResult = await client.query(
|
|
'SELECT bezeichnung FROM ausruestung_artikel WHERE id = $1',
|
|
[item.artikel_id],
|
|
);
|
|
if (artikelResult.rows.length > 0) {
|
|
itemBezeichnung = artikelResult.rows[0].bezeichnung;
|
|
}
|
|
}
|
|
|
|
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],
|
|
);
|
|
|
|
// NOTE: eigenschaft values are NOT saved in the transaction to avoid
|
|
// aborting the transaction if the table doesn't exist.
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
|
|
// Save eigenschaft values OUTSIDE the transaction so a missing table
|
|
// won't rollback the entire request creation.
|
|
for (const item of items) {
|
|
if (!item.eigenschaften || item.eigenschaften.length === 0) continue;
|
|
// Find the position ID we just inserted
|
|
const posRes = await pool.query(
|
|
`SELECT id FROM ausruestung_anfrage_positionen
|
|
WHERE anfrage_id = $1 AND COALESCE(artikel_id, 0) = $2
|
|
ORDER BY id DESC LIMIT 1`,
|
|
[anfrage.id, item.artikel_id || 0],
|
|
);
|
|
if (posRes.rows.length === 0) continue;
|
|
const posId = posRes.rows[0].id;
|
|
for (const e of item.eigenschaften) {
|
|
try {
|
|
await pool.query(
|
|
`INSERT INTO ausruestung_position_eigenschaften (position_id, eigenschaft_id, wert)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (position_id, eigenschaft_id) DO UPDATE SET wert = $3`,
|
|
[posId, e.eigenschaft_id, e.wert],
|
|
);
|
|
} catch { /* table may not exist */ }
|
|
}
|
|
}
|
|
|
|
return getRequestById(anfrage.id as number);
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
logger.error('ausruestungsanfrageService.createRequest failed', { error });
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
async function updateRequest(
|
|
id: number,
|
|
data: {
|
|
bezeichnung?: string;
|
|
notizen?: string;
|
|
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[];
|
|
},
|
|
) {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const fields: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
if (data.bezeichnung !== undefined) {
|
|
params.push(data.bezeichnung || null);
|
|
fields.push(`bezeichnung = $${params.length}`);
|
|
}
|
|
if (data.notizen !== undefined) {
|
|
params.push(data.notizen || null);
|
|
fields.push(`notizen = $${params.length}`);
|
|
}
|
|
|
|
if (fields.length > 0) {
|
|
params.push(new Date());
|
|
fields.push(`aktualisiert_am = $${params.length}`);
|
|
params.push(id);
|
|
await client.query(
|
|
`UPDATE ausruestung_anfragen SET ${fields.join(', ')} WHERE id = $${params.length}`,
|
|
params,
|
|
);
|
|
}
|
|
|
|
if (data.items) {
|
|
await client.query('DELETE FROM ausruestung_anfrage_positionen WHERE anfrage_id = $1', [id]);
|
|
for (const item of data.items) {
|
|
let itemBezeichnung = item.bezeichnung;
|
|
if (item.artikel_id) {
|
|
const artikelResult = await client.query(
|
|
'SELECT bezeichnung FROM ausruestung_artikel WHERE id = $1',
|
|
[item.artikel_id],
|
|
);
|
|
if (artikelResult.rows.length > 0) {
|
|
itemBezeichnung = artikelResult.rows[0].bezeichnung;
|
|
}
|
|
}
|
|
await client.query(
|
|
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
[id, item.artikel_id || null, itemBezeichnung, item.menge, item.notizen || null],
|
|
);
|
|
}
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
|
|
// Save eigenschaft values outside transaction
|
|
if (data.items) {
|
|
for (const item of data.items) {
|
|
if (!item.eigenschaften || item.eigenschaften.length === 0) continue;
|
|
const posRes = await pool.query(
|
|
`SELECT id FROM ausruestung_anfrage_positionen
|
|
WHERE anfrage_id = $1 AND COALESCE(artikel_id, 0) = $2
|
|
ORDER BY id DESC LIMIT 1`,
|
|
[id, item.artikel_id || 0],
|
|
);
|
|
if (posRes.rows.length === 0) continue;
|
|
const posId = posRes.rows[0].id;
|
|
for (const e of item.eigenschaften) {
|
|
try {
|
|
await pool.query(
|
|
`INSERT INTO ausruestung_position_eigenschaften (position_id, eigenschaft_id, wert)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (position_id, eigenschaft_id) DO UPDATE SET wert = $3`,
|
|
[posId, e.eigenschaft_id, e.wert],
|
|
);
|
|
} catch { /* table may not exist */ }
|
|
}
|
|
}
|
|
}
|
|
|
|
return getRequestById(id);
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
logger.error('ausruestungsanfrageService.updateRequest failed', { error });
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
async function updatePositionGeliefert(positionId: number, geliefert: boolean) {
|
|
const result = await pool.query(
|
|
`UPDATE ausruestung_anfrage_positionen SET geliefert = $1 WHERE id = $2 RETURNING *`,
|
|
[geliefert, positionId],
|
|
);
|
|
const position = result.rows[0];
|
|
if (!position) return null;
|
|
|
|
// Auto-complete: if all positions are geliefert, set anfrage status to 'erledigt'
|
|
if (geliefert) {
|
|
const check = await pool.query(
|
|
`SELECT COUNT(*) FILTER (WHERE NOT geliefert)::int AS remaining
|
|
FROM ausruestung_anfrage_positionen
|
|
WHERE anfrage_id = $1`,
|
|
[position.anfrage_id],
|
|
);
|
|
if (check.rows[0].remaining === 0) {
|
|
await pool.query(
|
|
`UPDATE ausruestung_anfragen SET status = 'erledigt', aktualisiert_am = NOW() WHERE id = $1`,
|
|
[position.anfrage_id],
|
|
);
|
|
}
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
async function updateRequestStatus(
|
|
id: number,
|
|
status: string,
|
|
adminNotizen?: string,
|
|
bearbeitetVon?: string,
|
|
) {
|
|
// Use aktualisiert_am (always exists) + try bearbeitet_am (added in migration 050)
|
|
let updated;
|
|
try {
|
|
const result = await pool.query(
|
|
`UPDATE ausruestung_anfragen
|
|
SET status = $1,
|
|
admin_notizen = COALESCE($2, admin_notizen),
|
|
bearbeitet_von = COALESCE($3, bearbeitet_von),
|
|
bearbeitet_am = NOW(),
|
|
aktualisiert_am = NOW()
|
|
WHERE id = $4
|
|
RETURNING *`,
|
|
[status, adminNotizen || null, bearbeitetVon || null, id],
|
|
);
|
|
updated = result.rows[0] || null;
|
|
} catch {
|
|
// Fallback if bearbeitet_am column doesn't exist yet (migration 050 not run)
|
|
const result = await pool.query(
|
|
`UPDATE ausruestung_anfragen
|
|
SET status = $1,
|
|
admin_notizen = COALESCE($2, admin_notizen),
|
|
bearbeitet_von = COALESCE($3, bearbeitet_von),
|
|
aktualisiert_am = NOW()
|
|
WHERE id = $4
|
|
RETURNING *`,
|
|
[status, adminNotizen || null, bearbeitetVon || null, id],
|
|
);
|
|
updated = result.rows[0] || null;
|
|
}
|
|
|
|
// When status changes to 'erledigt', mark all positions as geliefert
|
|
if (status === 'erledigt') {
|
|
try {
|
|
await pool.query(
|
|
`UPDATE ausruestung_anfrage_positionen SET geliefert = true WHERE anfrage_id = $1`,
|
|
[id],
|
|
);
|
|
} catch { /* column may not exist yet */ }
|
|
}
|
|
|
|
return updated;
|
|
}
|
|
|
|
async function deleteRequest(id: number) {
|
|
await pool.query('DELETE FROM ausruestung_anfragen WHERE id = $1', [id]);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Linking (ausruestung_anfrage_bestellung)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function linkToOrder(anfrageId: number, bestellungId: number) {
|
|
await pool.query(
|
|
`INSERT INTO ausruestung_anfrage_bestellung (anfrage_id, bestellung_id)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT DO NOTHING`,
|
|
[anfrageId, bestellungId],
|
|
);
|
|
}
|
|
|
|
async function unlinkFromOrder(anfrageId: number, bestellungId: number) {
|
|
await pool.query(
|
|
'DELETE FROM ausruestung_anfrage_bestellung WHERE anfrage_id = $1 AND bestellung_id = $2',
|
|
[anfrageId, bestellungId],
|
|
);
|
|
}
|
|
|
|
async function getLinkedOrders(anfrageId: number) {
|
|
const result = await pool.query(
|
|
`SELECT b.*
|
|
FROM ausruestung_anfrage_bestellung ab
|
|
JOIN bestellungen b ON b.id = ab.bestellung_id
|
|
WHERE ab.anfrage_id = $1`,
|
|
[anfrageId],
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Overview (aggregated)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function getOverview() {
|
|
const aggregated = await pool.query(
|
|
`SELECT p.bezeichnung,
|
|
SUM(p.menge)::int AS total_menge,
|
|
COUNT(DISTINCT p.anfrage_id)::int AS anfrage_count
|
|
FROM ausruestung_anfrage_positionen p
|
|
JOIN ausruestung_anfragen a ON a.id = p.anfrage_id
|
|
WHERE a.status IN ('offen', 'genehmigt')
|
|
GROUP BY p.bezeichnung
|
|
ORDER BY total_menge DESC, p.bezeichnung`,
|
|
);
|
|
|
|
const counts = await pool.query(
|
|
`SELECT
|
|
COUNT(*) FILTER (WHERE status = 'offen')::int AS pending_count,
|
|
COUNT(*) FILTER (WHERE status = 'genehmigt')::int AS approved_count,
|
|
COUNT(*) FILTER (WHERE status = 'offen' AND bearbeitet_von IS NULL)::int AS unhandled_count,
|
|
COALESCE(SUM(sub.total), 0)::int AS total_items
|
|
FROM ausruestung_anfragen a
|
|
LEFT JOIN LATERAL (
|
|
SELECT SUM(p.menge) AS total
|
|
FROM ausruestung_anfrage_positionen p
|
|
WHERE p.anfrage_id = a.id
|
|
) sub ON true
|
|
WHERE a.status IN ('offen', 'genehmigt')`,
|
|
);
|
|
|
|
return {
|
|
items: aggregated.rows,
|
|
...counts.rows[0],
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Widget overview (lightweight counts only)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function getWidgetOverview() {
|
|
const result = await pool.query(
|
|
`SELECT
|
|
COUNT(*)::int AS total_count,
|
|
COUNT(*) FILTER (WHERE status = 'offen')::int AS pending_count,
|
|
COUNT(*) FILTER (WHERE status = 'genehmigt')::int AS approved_count,
|
|
COUNT(*) FILTER (WHERE status = 'offen' AND bearbeitet_von IS NULL)::int AS unhandled_count
|
|
FROM ausruestung_anfragen`,
|
|
);
|
|
return result.rows[0];
|
|
}
|
|
|
|
export default {
|
|
getKategorien,
|
|
createKategorie,
|
|
updateKategorie,
|
|
deleteKategorie,
|
|
getItems,
|
|
getItemById,
|
|
createItem,
|
|
updateItem,
|
|
deleteItem,
|
|
getCategories,
|
|
getArtikelEigenschaften,
|
|
upsertArtikelEigenschaft,
|
|
deleteArtikelEigenschaft,
|
|
getRequests,
|
|
getMyRequests,
|
|
getRequestById,
|
|
updatePositionGeliefert,
|
|
createRequest,
|
|
updateRequest,
|
|
updateRequestStatus,
|
|
deleteRequest,
|
|
linkToOrder,
|
|
unlinkFromOrder,
|
|
getLinkedOrders,
|
|
getOverview,
|
|
getWidgetOverview,
|
|
};
|