rework internal order system
This commit is contained in:
@@ -21,15 +21,15 @@ class AusruestungsanfrageController {
|
||||
|
||||
async createKategorie(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
const { name, parent_id } = req.body;
|
||||
if (!name || name.trim().length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
|
||||
return;
|
||||
}
|
||||
const kategorie = await ausruestungsanfrageService.createKategorie(name.trim());
|
||||
const kategorie = await ausruestungsanfrageService.createKategorie(name.trim(), parent_id ?? null);
|
||||
res.status(201).json({ success: true, data: kategorie });
|
||||
} catch (error: any) {
|
||||
if (error?.constraint === 'ausruestung_kategorien_katalog_name_key') {
|
||||
if (error?.constraint?.includes('unique') || error?.code === '23505') {
|
||||
res.status(409).json({ success: false, message: 'Kategorie existiert bereits' });
|
||||
return;
|
||||
}
|
||||
@@ -41,12 +41,15 @@ class AusruestungsanfrageController {
|
||||
async updateKategorie(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const { name } = req.body;
|
||||
if (!name || name.trim().length === 0) {
|
||||
const { name, parent_id } = req.body;
|
||||
if (name !== undefined && (!name || name.trim().length === 0)) {
|
||||
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
|
||||
return;
|
||||
}
|
||||
const kategorie = await ausruestungsanfrageService.updateKategorie(id, name.trim());
|
||||
const kategorie = await ausruestungsanfrageService.updateKategorie(id, {
|
||||
name: name?.trim(),
|
||||
parent_id: parent_id !== undefined ? parent_id : undefined,
|
||||
});
|
||||
if (!kategorie) {
|
||||
res.status(404).json({ success: false, message: 'Kategorie nicht gefunden' });
|
||||
return;
|
||||
@@ -451,6 +454,20 @@ class AusruestungsanfrageController {
|
||||
res.status(500).json({ success: false, message: 'Verknüpfung konnte nicht entfernt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Widget overview (lightweight, for dashboard widget)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
async getWidgetOverview(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const overview = await ausruestungsanfrageService.getWidgetOverview();
|
||||
res.status(200).json({ success: true, data: overview });
|
||||
} catch (error) {
|
||||
logger.error('AusruestungsanfrageController.getWidgetOverview error', { error });
|
||||
res.status(500).json({ success: false, message: 'Widget-Daten konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AusruestungsanfrageController();
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
-- Migration 048: Catalog categories table + item characteristics
|
||||
-- - Admin-managed categories (replacing free-text kategorie)
|
||||
-- - Admin-managed categories with subcategories (replacing free-text kategorie)
|
||||
-- - Per-item characteristics (options or free-text)
|
||||
-- - Characteristic values per request position
|
||||
-- - Remove view_all permission (approve covers it)
|
||||
-- - Add manage_categories permission
|
||||
|
||||
-- 1. Categories table
|
||||
-- 1. Categories table (with parent_id for subcategories)
|
||||
CREATE TABLE IF NOT EXISTS ausruestung_kategorien_katalog (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
erstellt_am TIMESTAMPTZ DEFAULT NOW()
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
parent_id INT REFERENCES ausruestung_kategorien_katalog(id) ON DELETE CASCADE,
|
||||
erstellt_am TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(name, parent_id)
|
||||
);
|
||||
|
||||
-- Add unique constraint for top-level categories (parent_id IS NULL)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ausruestung_kategorien_top_level_unique
|
||||
ON ausruestung_kategorien_katalog (name) WHERE parent_id IS NULL;
|
||||
|
||||
-- Migrate existing categories from free-text
|
||||
INSERT INTO ausruestung_kategorien_katalog (name)
|
||||
SELECT DISTINCT kategorie FROM ausruestung_artikel WHERE kategorie IS NOT NULL AND kategorie != ''
|
||||
@@ -24,7 +30,7 @@ ALTER TABLE ausruestung_artikel ADD COLUMN IF NOT EXISTS kategorie_id INT REFERE
|
||||
UPDATE ausruestung_artikel a
|
||||
SET kategorie_id = k.id
|
||||
FROM ausruestung_kategorien_katalog k
|
||||
WHERE k.name = a.kategorie AND a.kategorie_id IS NULL;
|
||||
WHERE k.name = a.kategorie AND a.kategorie_id IS NULL AND k.parent_id IS NULL;
|
||||
|
||||
-- 2. Characteristics definitions per catalog item
|
||||
CREATE TABLE IF NOT EXISTS ausruestung_artikel_eigenschaften (
|
||||
|
||||
@@ -37,6 +37,7 @@ router.get('/categories', authenticate, requirePermission('ausruestungsanfrage:v
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get('/overview', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.getOverview.bind(ausruestungsanfrageController));
|
||||
router.get('/widget-overview', authenticate, requirePermission('ausruestungsanfrage:widget'), ausruestungsanfrageController.getWidgetOverview.bind(ausruestungsanfrageController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Requests
|
||||
|
||||
@@ -2,28 +2,43 @@ import pool from '../config/database';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Categories (ausruestung_kategorien_katalog)
|
||||
// Categories (ausruestung_kategorien_katalog) — hierarchical with parent_id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getKategorien() {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM ausruestung_kategorien_katalog ORDER BY name',
|
||||
'SELECT * FROM ausruestung_kategorien_katalog ORDER BY parent_id NULLS FIRST, name',
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async function createKategorie(name: string) {
|
||||
async function createKategorie(name: string, parentId?: number | null) {
|
||||
const result = await pool.query(
|
||||
'INSERT INTO ausruestung_kategorien_katalog (name) VALUES ($1) RETURNING *',
|
||||
[name],
|
||||
'INSERT INTO ausruestung_kategorien_katalog (name, parent_id) VALUES ($1, $2) RETURNING *',
|
||||
[name, parentId ?? null],
|
||||
);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async function updateKategorie(id: number, name: string) {
|
||||
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 name = $1 WHERE id = $2 RETURNING *',
|
||||
[name, id],
|
||||
`UPDATE ausruestung_kategorien_katalog SET ${fields.join(', ')} WHERE id = $${params.length} RETURNING *`,
|
||||
params,
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
@@ -279,25 +294,30 @@ async function getRequestById(id: number) {
|
||||
[id],
|
||||
);
|
||||
|
||||
// Load eigenschaft values per position
|
||||
// Load eigenschaft values per position (gracefully handle missing table)
|
||||
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) {
|
||||
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,
|
||||
});
|
||||
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 (err) {
|
||||
// Table may not exist yet if migration hasn't run
|
||||
logger.debug('Position eigenschaften query failed (migration may not have run yet)', { error: err });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,12 +393,16 @@ async function createRequest(
|
||||
// Save eigenschaft values
|
||||
if (item.eigenschaften && item.eigenschaften.length > 0) {
|
||||
for (const e of item.eigenschaften) {
|
||||
await client.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`,
|
||||
[posResult.rows[0].id, e.eigenschaft_id, e.wert],
|
||||
);
|
||||
try {
|
||||
await client.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`,
|
||||
[posResult.rows[0].id, e.eigenschaft_id, e.wert],
|
||||
);
|
||||
} catch (err) {
|
||||
logger.debug('Position eigenschaft insert failed (migration may not have run yet)', { error: err });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,12 +477,16 @@ async function updateRequest(
|
||||
// Save eigenschaft values
|
||||
if (item.eigenschaften && item.eigenschaften.length > 0) {
|
||||
for (const e of item.eigenschaften) {
|
||||
await client.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`,
|
||||
[posResult.rows[0].id, e.eigenschaft_id, e.wert],
|
||||
);
|
||||
try {
|
||||
await client.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`,
|
||||
[posResult.rows[0].id, e.eigenschaft_id, e.wert],
|
||||
);
|
||||
} catch (err) {
|
||||
logger.debug('Position eigenschaft insert failed', { error: err });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -566,6 +594,22 @@ async function getOverview() {
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Widget overview (no permission restriction — 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,
|
||||
@@ -591,4 +635,5 @@ export default {
|
||||
unlinkFromOrder,
|
||||
getLinkedOrders,
|
||||
getOverview,
|
||||
getWidgetOverview,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user