- Migration 084: new persoenliche_ausruestung table with catalog link, user assignment, soft delete; adds zuweisung_typ/ausruestung_id/persoenlich_id columns to ausruestung_anfrage_positionen; seeds feature group + 5 permissions - Fix user data purge: table was shop_anfragen, renamed to ausruestung_anfragen in mig 046 — caused full transaction rollback. Also keep mitglieder_profile row but NULL FDISK-synced fields (dienstgrad, geburtsdatum, etc.) instead of deleting the profile - Personal equipment CRUD: backend service/controller/routes at /api/persoenliche-ausruestung; frontend page with DataTable, user filter, catalog Autocomplete, FAB create dialog; widget in Status group; sidebar entry (Checkroom icon); card in MitgliedDetail Tab 0 - Ausruestungsanfrage item assignment: when a request reaches erledigt, auto-opens ItemAssignmentDialog listing all delivered positions; each item can be assigned as general equipment (vehicle/storage), personal item (user, prefilled with requester), or not tracked; POST /requests/:id/assign backend - StatCard refactored to use WidgetCard as outer shell for consistent header styling across all dashboard widget templates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
6.2 KiB
TypeScript
201 lines
6.2 KiB
TypeScript
import pool from '../config/database';
|
|
import logger from '../utils/logger';
|
|
|
|
interface PersonalEquipmentFilters {
|
|
userId?: string;
|
|
kategorie?: string;
|
|
zustand?: string;
|
|
}
|
|
|
|
interface CreatePersonalEquipmentData {
|
|
bezeichnung: string;
|
|
kategorie?: string;
|
|
artikel_id?: number;
|
|
user_id?: string;
|
|
benutzer_name?: string;
|
|
groesse?: string;
|
|
seriennummer?: string;
|
|
inventarnummer?: string;
|
|
anschaffung_datum?: string;
|
|
zustand?: string;
|
|
notizen?: string;
|
|
}
|
|
|
|
interface UpdatePersonalEquipmentData {
|
|
bezeichnung?: string;
|
|
kategorie?: string;
|
|
artikel_id?: number | null;
|
|
user_id?: string | null;
|
|
benutzer_name?: string | null;
|
|
groesse?: string | null;
|
|
seriennummer?: string | null;
|
|
inventarnummer?: string | null;
|
|
anschaffung_datum?: string | null;
|
|
zustand?: string;
|
|
notizen?: string | null;
|
|
}
|
|
|
|
const BASE_SELECT = `
|
|
SELECT pa.*,
|
|
COALESCE(u.given_name || ' ' || u.family_name, u.name) AS user_display_name,
|
|
aa.bezeichnung AS artikel_bezeichnung
|
|
FROM persoenliche_ausruestung pa
|
|
LEFT JOIN users u ON u.id = pa.user_id
|
|
LEFT JOIN ausruestung_artikel aa ON aa.id = pa.artikel_id
|
|
WHERE pa.geloescht_am IS NULL
|
|
`;
|
|
|
|
async function getAll(filters: PersonalEquipmentFilters = {}) {
|
|
try {
|
|
const conditions: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
if (filters.userId) {
|
|
params.push(filters.userId);
|
|
conditions.push(`pa.user_id = $${params.length}`);
|
|
}
|
|
if (filters.kategorie) {
|
|
params.push(filters.kategorie);
|
|
conditions.push(`pa.kategorie = $${params.length}`);
|
|
}
|
|
if (filters.zustand) {
|
|
params.push(filters.zustand);
|
|
conditions.push(`pa.zustand = $${params.length}`);
|
|
}
|
|
|
|
const where = conditions.length > 0 ? ` AND ${conditions.join(' AND ')}` : '';
|
|
const result = await pool.query(
|
|
`${BASE_SELECT}${where} ORDER BY pa.bezeichnung`,
|
|
params,
|
|
);
|
|
return result.rows;
|
|
} catch (error) {
|
|
logger.error('personalEquipmentService.getAll failed', { error });
|
|
throw new Error('Failed to fetch personal equipment');
|
|
}
|
|
}
|
|
|
|
async function getByUserId(userId: string) {
|
|
try {
|
|
const result = await pool.query(
|
|
`${BASE_SELECT} AND pa.user_id = $1 ORDER BY pa.bezeichnung`,
|
|
[userId],
|
|
);
|
|
return result.rows;
|
|
} catch (error) {
|
|
logger.error('personalEquipmentService.getByUserId failed', { error, userId });
|
|
throw new Error('Failed to fetch personal equipment for user');
|
|
}
|
|
}
|
|
|
|
async function getById(id: string) {
|
|
try {
|
|
const result = await pool.query(
|
|
`${BASE_SELECT} AND pa.id = $1`,
|
|
[id],
|
|
);
|
|
return result.rows[0] || null;
|
|
} catch (error) {
|
|
logger.error('personalEquipmentService.getById failed', { error, id });
|
|
throw new Error('Failed to fetch personal equipment item');
|
|
}
|
|
}
|
|
|
|
async function create(data: CreatePersonalEquipmentData, requestingUserId: string) {
|
|
try {
|
|
const result = await pool.query(
|
|
`INSERT INTO persoenliche_ausruestung (
|
|
bezeichnung, kategorie, artikel_id, user_id, benutzer_name,
|
|
groesse, seriennummer, inventarnummer, anschaffung_datum,
|
|
zustand, notizen, erstellt_von
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
|
RETURNING *`,
|
|
[
|
|
data.bezeichnung,
|
|
data.kategorie ?? null,
|
|
data.artikel_id ?? null,
|
|
data.user_id ?? null,
|
|
data.benutzer_name ?? null,
|
|
data.groesse ?? null,
|
|
data.seriennummer ?? null,
|
|
data.inventarnummer ?? null,
|
|
data.anschaffung_datum ?? null,
|
|
data.zustand ?? 'gut',
|
|
data.notizen ?? null,
|
|
requestingUserId,
|
|
],
|
|
);
|
|
logger.info('Personal equipment created', { id: result.rows[0].id, by: requestingUserId });
|
|
return result.rows[0];
|
|
} catch (error) {
|
|
logger.error('personalEquipmentService.create failed', { error });
|
|
throw new Error('Failed to create personal equipment');
|
|
}
|
|
}
|
|
|
|
async function update(id: string, data: UpdatePersonalEquipmentData) {
|
|
try {
|
|
const fields: string[] = [];
|
|
const values: unknown[] = [];
|
|
let p = 1;
|
|
|
|
const addField = (col: string, value: unknown) => {
|
|
fields.push(`${col} = $${p++}`);
|
|
values.push(value);
|
|
};
|
|
|
|
if (data.bezeichnung !== undefined) addField('bezeichnung', data.bezeichnung);
|
|
if (data.kategorie !== undefined) addField('kategorie', data.kategorie);
|
|
if (data.artikel_id !== undefined) addField('artikel_id', data.artikel_id);
|
|
if (data.user_id !== undefined) addField('user_id', data.user_id);
|
|
if (data.benutzer_name !== undefined) addField('benutzer_name', data.benutzer_name);
|
|
if (data.groesse !== undefined) addField('groesse', data.groesse);
|
|
if (data.seriennummer !== undefined) addField('seriennummer', data.seriennummer);
|
|
if (data.inventarnummer !== undefined) addField('inventarnummer', data.inventarnummer);
|
|
if (data.anschaffung_datum !== undefined) addField('anschaffung_datum', data.anschaffung_datum);
|
|
if (data.zustand !== undefined) addField('zustand', data.zustand);
|
|
if (data.notizen !== undefined) addField('notizen', data.notizen);
|
|
|
|
if (fields.length === 0) {
|
|
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,
|
|
);
|
|
|
|
if (result.rows.length === 0) return null;
|
|
logger.info('Personal equipment updated', { id });
|
|
return result.rows[0];
|
|
} catch (error) {
|
|
logger.error('personalEquipmentService.update failed', { error, id });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function softDelete(id: string) {
|
|
try {
|
|
const result = await pool.query(
|
|
`UPDATE persoenliche_ausruestung SET geloescht_am = NOW() WHERE id = $1 AND geloescht_am IS NULL RETURNING id`,
|
|
[id],
|
|
);
|
|
if (result.rows.length === 0) return false;
|
|
logger.info('Personal equipment soft-deleted', { id });
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('personalEquipmentService.softDelete failed', { error, id });
|
|
throw new Error('Failed to delete personal equipment');
|
|
}
|
|
}
|
|
|
|
export default {
|
|
getAll,
|
|
getByUserId,
|
|
getById,
|
|
create,
|
|
update,
|
|
delete: softDelete,
|
|
};
|