feat(ausruestungsanfrage): add personal item tracking, catalog enforcement, and detail pages
This commit is contained in:
@@ -428,7 +428,7 @@ async function getRequestById(id: number) {
|
||||
|
||||
async function createRequest(
|
||||
userId: string,
|
||||
items: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; ist_ersatz?: boolean; 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 }[]; persoenlich_id?: string; neuer_zustand?: string }[],
|
||||
notizen?: string,
|
||||
bezeichnung?: string,
|
||||
fuerBenutzerName?: string,
|
||||
@@ -471,7 +471,26 @@ async function createRequest(
|
||||
|
||||
for (const item of items) {
|
||||
let itemBezeichnung = item.bezeichnung;
|
||||
if (item.artikel_id) {
|
||||
let itemArtikelId = item.artikel_id || null;
|
||||
let itemPersoenlichId: string | null = null;
|
||||
let itemAktuellerZustand: string | null = null;
|
||||
let itemNeuerZustand: string | null = null;
|
||||
|
||||
if (item.persoenlich_id) {
|
||||
// Load personal item details
|
||||
const persResult = await client.query(
|
||||
'SELECT bezeichnung, zustand, artikel_id FROM persoenliche_ausruestung WHERE id = $1',
|
||||
[item.persoenlich_id],
|
||||
);
|
||||
if (persResult.rows.length > 0) {
|
||||
const pers = persResult.rows[0];
|
||||
itemBezeichnung = pers.bezeichnung;
|
||||
itemArtikelId = pers.artikel_id || itemArtikelId;
|
||||
itemPersoenlichId = item.persoenlich_id;
|
||||
itemAktuellerZustand = pers.zustand || null;
|
||||
itemNeuerZustand = item.neuer_zustand || null;
|
||||
}
|
||||
} else if (item.artikel_id) {
|
||||
const artikelResult = await client.query(
|
||||
'SELECT bezeichnung FROM ausruestung_artikel WHERE id = $1',
|
||||
[item.artikel_id],
|
||||
@@ -482,9 +501,9 @@ async function createRequest(
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`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],
|
||||
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen, ist_ersatz, persoenlich_id, aktueller_zustand, neuer_zustand)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[anfrage.id, itemArtikelId, itemBezeichnung, item.menge, item.notizen || null, item.ist_ersatz ?? false, itemPersoenlichId, itemAktuellerZustand, itemNeuerZustand],
|
||||
);
|
||||
|
||||
// NOTE: eigenschaft values are NOT saved in the transaction to avoid
|
||||
@@ -710,6 +729,19 @@ async function updateRequestStatus(
|
||||
[id],
|
||||
);
|
||||
} catch { /* column may not exist yet */ }
|
||||
|
||||
// Apply personal equipment status changes
|
||||
const statusChangePositionen = await pool.query(
|
||||
`SELECT persoenlich_id, neuer_zustand FROM ausruestung_anfrage_positionen
|
||||
WHERE anfrage_id = $1 AND persoenlich_id IS NOT NULL AND neuer_zustand IS NOT NULL`,
|
||||
[id],
|
||||
);
|
||||
for (const row of statusChangePositionen.rows) {
|
||||
await pool.query(
|
||||
`UPDATE persoenliche_ausruestung SET zustand = $1 WHERE id = $2`,
|
||||
[row.neuer_zustand, row.persoenlich_id],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
@@ -1006,6 +1038,22 @@ async function assignDeliveredItems(
|
||||
);
|
||||
const newId = insertResult.rows[0].id;
|
||||
|
||||
// Copy position eigenschaften to persoenliche_ausruestung_eigenschaften
|
||||
const posEigResult = await client.query(
|
||||
`SELECT poe.eigenschaft_id, aae.name, poe.wert
|
||||
FROM ausruestung_position_eigenschaften poe
|
||||
JOIN ausruestung_artikel_eigenschaften aae ON aae.id = poe.eigenschaft_id
|
||||
WHERE poe.position_id = $1`,
|
||||
[a.positionId],
|
||||
);
|
||||
for (const row of posEigResult.rows) {
|
||||
await client.query(
|
||||
`INSERT INTO persoenliche_ausruestung_eigenschaften (persoenlich_id, eigenschaft_id, name, wert)
|
||||
VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`,
|
||||
[newId, row.eigenschaft_id, row.name, row.wert],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`UPDATE ausruestung_anfrage_positionen
|
||||
SET zuweisung_typ = 'persoenlich', zuweisung_persoenlich_id = $1
|
||||
@@ -1034,6 +1082,38 @@ async function assignDeliveredItems(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unassigned positions + position artikel update
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getUnassignedPositions() {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
p.id, p.bezeichnung, p.menge, p.artikel_id,
|
||||
a.id AS anfrage_id,
|
||||
a.bezeichnung AS anfrage_bezeichnung,
|
||||
a.bestell_nummer,
|
||||
a.bestell_jahr,
|
||||
COALESCE(a.fuer_benutzer_name,
|
||||
NULLIF(TRIM(COALESCE(u.given_name,'') || ' ' || COALESCE(u.family_name,'')), ''),
|
||||
u.name) AS fuer_wen
|
||||
FROM ausruestung_anfrage_positionen p
|
||||
JOIN ausruestung_anfragen a ON a.id = p.anfrage_id
|
||||
LEFT JOIN users u ON u.id = a.anfrager_id
|
||||
WHERE p.geliefert = true AND p.zuweisung_typ IS NULL
|
||||
ORDER BY a.bestell_jahr DESC NULLS LAST, a.bestell_nummer DESC NULLS LAST, p.bezeichnung
|
||||
`);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async function updatePositionArtikelId(positionId: number, artikelId: number) {
|
||||
const result = await pool.query(
|
||||
`UPDATE ausruestung_anfrage_positionen SET artikel_id = $1 WHERE id = $2 RETURNING *`,
|
||||
[artikelId, positionId],
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
export default {
|
||||
getAllUsers,
|
||||
getKategorien,
|
||||
@@ -1065,4 +1145,6 @@ export default {
|
||||
getOverview,
|
||||
getWidgetOverview,
|
||||
assignDeliveredItems,
|
||||
getUnassignedPositions,
|
||||
updatePositionArtikelId,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ interface CreatePersonalEquipmentData {
|
||||
anschaffung_datum?: string;
|
||||
zustand?: string;
|
||||
notizen?: string;
|
||||
eigenschaften?: { eigenschaft_id?: number; name: string; wert: string }[];
|
||||
}
|
||||
|
||||
interface UpdatePersonalEquipmentData {
|
||||
@@ -33,6 +34,7 @@ interface UpdatePersonalEquipmentData {
|
||||
anschaffung_datum?: string | null;
|
||||
zustand?: string;
|
||||
notizen?: string | null;
|
||||
eigenschaften?: { eigenschaft_id?: number | null; name: string; wert: string }[] | null;
|
||||
}
|
||||
|
||||
const BASE_SELECT = `
|
||||
@@ -45,6 +47,23 @@ const BASE_SELECT = `
|
||||
WHERE pa.geloescht_am IS NULL
|
||||
`;
|
||||
|
||||
async function loadEigenschaften(ids: string[]) {
|
||||
if (ids.length === 0) return new Map<string, { id: number; persoenlich_id: string; eigenschaft_id: number | null; name: string; wert: string }[]>();
|
||||
const result = await pool.query(
|
||||
`SELECT pae.id, pae.persoenlich_id, pae.eigenschaft_id, pae.name, pae.wert
|
||||
FROM persoenliche_ausruestung_eigenschaften pae
|
||||
WHERE pae.persoenlich_id = ANY($1)`,
|
||||
[ids],
|
||||
);
|
||||
const map = new Map<string, typeof result.rows>();
|
||||
for (const row of result.rows) {
|
||||
const arr = map.get(row.persoenlich_id) || [];
|
||||
arr.push(row);
|
||||
map.set(row.persoenlich_id, arr);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async function getAll(filters: PersonalEquipmentFilters = {}) {
|
||||
try {
|
||||
const conditions: string[] = [];
|
||||
@@ -68,6 +87,13 @@ async function getAll(filters: PersonalEquipmentFilters = {}) {
|
||||
`${BASE_SELECT}${where} ORDER BY pa.bezeichnung`,
|
||||
params,
|
||||
);
|
||||
|
||||
const ids = result.rows.map((r: { id: string }) => r.id);
|
||||
const eigMap = await loadEigenschaften(ids);
|
||||
for (const row of result.rows) {
|
||||
row.eigenschaften = eigMap.get(row.id) || [];
|
||||
}
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('personalEquipmentService.getAll failed', { error });
|
||||
@@ -81,6 +107,13 @@ async function getByUserId(userId: string) {
|
||||
`${BASE_SELECT} AND pa.user_id = $1 ORDER BY pa.bezeichnung`,
|
||||
[userId],
|
||||
);
|
||||
|
||||
const ids = result.rows.map((r: { id: string }) => r.id);
|
||||
const eigMap = await loadEigenschaften(ids);
|
||||
for (const row of result.rows) {
|
||||
row.eigenschaften = eigMap.get(row.id) || [];
|
||||
}
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('personalEquipmentService.getByUserId failed', { error, userId });
|
||||
@@ -94,7 +127,17 @@ async function getById(id: string) {
|
||||
`${BASE_SELECT} AND pa.id = $1`,
|
||||
[id],
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
if (!result.rows[0]) return null;
|
||||
|
||||
const eigResult = await pool.query(
|
||||
`SELECT pae.id, pae.persoenlich_id, pae.eigenschaft_id, pae.name, pae.wert
|
||||
FROM persoenliche_ausruestung_eigenschaften pae
|
||||
WHERE pae.persoenlich_id = $1`,
|
||||
[id],
|
||||
);
|
||||
result.rows[0].eigenschaften = eigResult.rows;
|
||||
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('personalEquipmentService.getById failed', { error, id });
|
||||
throw new Error('Failed to fetch personal equipment item');
|
||||
@@ -125,8 +168,20 @@ async function create(data: CreatePersonalEquipmentData, requestingUserId: strin
|
||||
requestingUserId,
|
||||
],
|
||||
);
|
||||
logger.info('Personal equipment created', { id: result.rows[0].id, by: requestingUserId });
|
||||
return result.rows[0];
|
||||
const created = result.rows[0];
|
||||
logger.info('Personal equipment created', { id: created.id, by: requestingUserId });
|
||||
|
||||
if (data.eigenschaften && data.eigenschaften.length > 0) {
|
||||
for (const e of data.eigenschaften) {
|
||||
await pool.query(
|
||||
`INSERT INTO persoenliche_ausruestung_eigenschaften (persoenlich_id, eigenschaft_id, name, wert)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[created.id, e.eigenschaft_id ?? null, e.name, e.wert],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return created;
|
||||
} catch (error) {
|
||||
logger.error('personalEquipmentService.create failed', { error });
|
||||
throw new Error('Failed to create personal equipment');
|
||||
@@ -156,19 +211,46 @@ async function update(id: string, data: UpdatePersonalEquipmentData) {
|
||||
if (data.zustand !== undefined) addField('zustand', data.zustand);
|
||||
if (data.notizen !== undefined) addField('notizen', data.notizen);
|
||||
|
||||
if (fields.length === 0) {
|
||||
if (fields.length === 0 && data.eigenschaften === undefined) {
|
||||
throw new Error('No fields to update');
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE persoenliche_ausruestung SET ${fields.join(', ')} WHERE id = $${p} AND geloescht_am IS NULL RETURNING *`,
|
||||
values,
|
||||
);
|
||||
let updated = null;
|
||||
if (fields.length > 0) {
|
||||
values.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE persoenliche_ausruestung SET ${fields.join(', ')} WHERE id = $${p} AND geloescht_am IS NULL RETURNING *`,
|
||||
values,
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
updated = result.rows[0];
|
||||
}
|
||||
|
||||
if (data.eigenschaften !== undefined) {
|
||||
await pool.query('DELETE FROM persoenliche_ausruestung_eigenschaften WHERE persoenlich_id = $1', [id]);
|
||||
if (data.eigenschaften && data.eigenschaften.length > 0) {
|
||||
for (const e of data.eigenschaften) {
|
||||
await pool.query(
|
||||
`INSERT INTO persoenliche_ausruestung_eigenschaften (persoenlich_id, eigenschaft_id, name, wert)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[id, e.eigenschaft_id ?? null, e.name, e.wert],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
// Only eigenschaften were updated, fetch the row
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM persoenliche_ausruestung WHERE id = $1 AND geloescht_am IS NULL`,
|
||||
[id],
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
updated = result.rows[0];
|
||||
}
|
||||
|
||||
if (result.rows.length === 0) return null;
|
||||
logger.info('Personal equipment updated', { id });
|
||||
return result.rows[0];
|
||||
return updated;
|
||||
} catch (error) {
|
||||
logger.error('personalEquipmentService.update failed', { error, id });
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user