feat(ausruestungsanfrage): add personal item tracking, catalog enforcement, and detail pages

This commit is contained in:
Matthias Hochmeister
2026-04-14 16:49:20 +02:00
parent e6b6639fe9
commit 633a75cb0b
15 changed files with 1031 additions and 26 deletions

View File

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