new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 18:43:30 +01:00
parent 202a658b8d
commit 1b13e4f89e
31 changed files with 1022 additions and 517 deletions

View File

@@ -2,7 +2,7 @@ import pool from '../config/database';
import logger from '../utils/logger';
// ---------------------------------------------------------------------------
// Catalog Items (shop_artikel)
// Catalog Items (ausruestung_artikel)
// ---------------------------------------------------------------------------
async function getItems(filters?: { kategorie?: string; aktiv?: boolean }) {
@@ -20,14 +20,14 @@ async function getItems(filters?: { kategorie?: string; aktiv?: boolean }) {
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const result = await pool.query(
`SELECT * FROM shop_artikel ${where} ORDER BY kategorie, bezeichnung`,
`SELECT * FROM ausruestung_artikel ${where} ORDER BY kategorie, bezeichnung`,
params,
);
return result.rows;
}
async function getItemById(id: number) {
const result = await pool.query('SELECT * FROM shop_artikel WHERE id = $1', [id]);
const result = await pool.query('SELECT * FROM ausruestung_artikel WHERE id = $1', [id]);
return result.rows[0] || null;
}
@@ -42,7 +42,7 @@ async function createItem(
userId: string,
) {
const result = await pool.query(
`INSERT INTO shop_artikel (bezeichnung, beschreibung, kategorie, geschaetzter_preis, aktiv, erstellt_von)
`INSERT INTO ausruestung_artikel (bezeichnung, beschreibung, kategorie, geschaetzter_preis, aktiv, erstellt_von)
VALUES ($1, $2, $3, $4, COALESCE($5, true), $6)
RETURNING *`,
[data.bezeichnung, data.beschreibung || null, data.kategorie || null, data.geschaetzter_preis || null, data.aktiv ?? true, userId],
@@ -94,25 +94,25 @@ async function updateItem(
params.push(id);
const result = await pool.query(
`UPDATE shop_artikel SET ${fields.join(', ')} WHERE id = $${params.length} RETURNING *`,
`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 shop_artikel WHERE id = $1', [id]);
await pool.query('DELETE FROM ausruestung_artikel WHERE id = $1', [id]);
}
async function getCategories() {
const result = await pool.query(
'SELECT DISTINCT kategorie FROM shop_artikel WHERE kategorie IS NOT NULL ORDER BY kategorie',
'SELECT DISTINCT kategorie FROM ausruestung_artikel WHERE kategorie IS NOT NULL ORDER BY kategorie',
);
return result.rows.map((r: { kategorie: string }) => r.kategorie);
}
// ---------------------------------------------------------------------------
// Requests (shop_anfragen)
// Requests (ausruestung_anfragen)
// ---------------------------------------------------------------------------
async function getRequests(filters?: { status?: string; anfrager_id?: string }) {
@@ -133,8 +133,8 @@ async function getRequests(filters?: { status?: string; anfrager_id?: string })
`SELECT a.*,
u.vorname || ' ' || u.nachname AS anfrager_name,
u2.vorname || ' ' || u2.nachname AS bearbeitet_von_name,
(SELECT COUNT(*)::int FROM shop_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count
FROM shop_anfragen a
(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}
@@ -147,8 +147,8 @@ async function getRequests(filters?: { status?: string; anfrager_id?: string })
async function getMyRequests(userId: string) {
const result = await pool.query(
`SELECT a.*,
(SELECT COUNT(*)::int FROM shop_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count
FROM shop_anfragen 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],
@@ -161,7 +161,7 @@ async function getRequestById(id: number) {
`SELECT a.*,
u.vorname || ' ' || u.nachname AS anfrager_name,
u2.vorname || ' ' || u2.nachname AS bearbeitet_von_name
FROM shop_anfragen a
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`,
@@ -171,8 +171,8 @@ async function getRequestById(id: number) {
const positionen = await pool.query(
`SELECT p.*, sa.bezeichnung AS artikel_bezeichnung, sa.kategorie AS artikel_kategorie
FROM shop_anfrage_positionen p
LEFT JOIN shop_artikel sa ON sa.id = p.artikel_id
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],
@@ -180,7 +180,7 @@ async function getRequestById(id: number) {
const bestellungen = await pool.query(
`SELECT b.*
FROM shop_anfrage_bestellung ab
FROM ausruestung_anfrage_bestellung ab
JOIN bestellungen b ON b.id = ab.bestellung_id
WHERE ab.anfrage_id = $1`,
[id],
@@ -206,14 +206,14 @@ async function createRequest(
const currentYear = new Date().getFullYear();
const maxResult = await client.query(
`SELECT COALESCE(MAX(bestell_nummer), 0) + 1 AS next_nr
FROM shop_anfragen
FROM ausruestung_anfragen
WHERE bestell_jahr = $1`,
[currentYear],
);
const nextNr = maxResult.rows[0].next_nr;
const anfrageResult = await client.query(
`INSERT INTO shop_anfragen (anfrager_id, notizen, bestell_nummer, bestell_jahr)
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bestell_nummer, bestell_jahr)
VALUES ($1, $2, $3, $4)
RETURNING *`,
[userId, notizen || null, nextNr, currentYear],
@@ -226,7 +226,7 @@ async function createRequest(
// If artikel_id is provided, copy bezeichnung from catalog
if (item.artikel_id) {
const artikelResult = await client.query(
'SELECT bezeichnung FROM shop_artikel WHERE id = $1',
'SELECT bezeichnung FROM ausruestung_artikel WHERE id = $1',
[item.artikel_id],
);
if (artikelResult.rows.length > 0) {
@@ -235,7 +235,7 @@ async function createRequest(
}
await client.query(
`INSERT INTO shop_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen)
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen)
VALUES ($1, $2, $3, $4, $5)`,
[anfrage.id, item.artikel_id || null, bezeichnung, item.menge, item.notizen || null],
);
@@ -245,7 +245,7 @@ async function createRequest(
return getRequestById(anfrage.id);
} catch (error) {
await client.query('ROLLBACK');
logger.error('shopService.createRequest failed', { error });
logger.error('ausruestungsanfrageService.createRequest failed', { error });
throw error;
} finally {
client.release();
@@ -259,7 +259,7 @@ async function updateRequestStatus(
bearbeitetVon?: string,
) {
const result = await pool.query(
`UPDATE shop_anfragen
`UPDATE ausruestung_anfragen
SET status = $1,
admin_notizen = COALESCE($2, admin_notizen),
bearbeitet_von = COALESCE($3, bearbeitet_von),
@@ -272,16 +272,16 @@ async function updateRequestStatus(
}
async function deleteRequest(id: number) {
await pool.query('DELETE FROM shop_anfragen WHERE id = $1', [id]);
await pool.query('DELETE FROM ausruestung_anfragen WHERE id = $1', [id]);
}
// ---------------------------------------------------------------------------
// Linking (shop_anfrage_bestellung)
// Linking (ausruestung_anfrage_bestellung)
// ---------------------------------------------------------------------------
async function linkToOrder(anfrageId: number, bestellungId: number) {
await pool.query(
`INSERT INTO shop_anfrage_bestellung (anfrage_id, bestellung_id)
`INSERT INTO ausruestung_anfrage_bestellung (anfrage_id, bestellung_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING`,
[anfrageId, bestellungId],
@@ -290,7 +290,7 @@ async function linkToOrder(anfrageId: number, bestellungId: number) {
async function unlinkFromOrder(anfrageId: number, bestellungId: number) {
await pool.query(
'DELETE FROM shop_anfrage_bestellung WHERE anfrage_id = $1 AND bestellung_id = $2',
'DELETE FROM ausruestung_anfrage_bestellung WHERE anfrage_id = $1 AND bestellung_id = $2',
[anfrageId, bestellungId],
);
}
@@ -298,7 +298,7 @@ async function unlinkFromOrder(anfrageId: number, bestellungId: number) {
async function getLinkedOrders(anfrageId: number) {
const result = await pool.query(
`SELECT b.*
FROM shop_anfrage_bestellung ab
FROM ausruestung_anfrage_bestellung ab
JOIN bestellungen b ON b.id = ab.bestellung_id
WHERE ab.anfrage_id = $1`,
[anfrageId],
@@ -315,8 +315,8 @@ async function getOverview() {
`SELECT p.bezeichnung,
SUM(p.menge)::int AS total_menge,
COUNT(DISTINCT p.anfrage_id)::int AS anfrage_count
FROM shop_anfrage_positionen p
JOIN shop_anfragen a ON a.id = p.anfrage_id
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`,
@@ -327,10 +327,10 @@ async function getOverview() {
COUNT(*) FILTER (WHERE status = 'offen')::int AS pending_count,
COUNT(*) FILTER (WHERE status = 'genehmigt')::int AS approved_count,
COALESCE(SUM(sub.total), 0)::int AS total_items
FROM shop_anfragen a
FROM ausruestung_anfragen a
LEFT JOIN LATERAL (
SELECT SUM(p.menge) AS total
FROM shop_anfrage_positionen p
FROM ausruestung_anfrage_positionen p
WHERE p.anfrage_id = a.id
) sub ON true
WHERE a.status IN ('offen', 'genehmigt')`,

View File

@@ -197,8 +197,8 @@ async function createOrder(data: { bezeichnung: string; lieferant_id?: number; b
}
}
await logAction(order.id, 'Bestellung erstellt', `Bestellung "${data.bezeichnung}" erstellt`, userId);
await client.query('COMMIT');
await logAction(order.id, 'Bestellung erstellt', `Bestellung "${data.bezeichnung}" erstellt`, userId);
return order;
} catch (error) {
await client.query('ROLLBACK');

View File

@@ -126,6 +126,45 @@ class CleanupService {
logger.info(`Cleanup: deleted ${rowCount} equipment history entries older than ${olderThanDays} days`);
return { count: rowCount ?? 0, deleted: true };
}
async resetBestellungenSequence(confirm: boolean): Promise<CleanupResult> {
if (!confirm) {
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM bestellungen');
return { count: rows[0].count, deleted: false };
}
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM bestellungen');
const count = rows[0].count;
await pool.query('TRUNCATE bestellungen CASCADE');
await pool.query('ALTER SEQUENCE bestellungen_id_seq RESTART WITH 1');
logger.info(`Cleanup: truncated bestellungen (${count} rows) and reset sequence`);
return { count, deleted: true };
}
async resetAusruestungAnfragenSequence(confirm: boolean): Promise<CleanupResult> {
if (!confirm) {
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM ausruestung_anfragen');
return { count: rows[0].count, deleted: false };
}
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM ausruestung_anfragen');
const count = rows[0].count;
await pool.query('TRUNCATE ausruestung_anfragen CASCADE');
await pool.query('ALTER SEQUENCE ausruestung_anfragen_id_seq RESTART WITH 1');
logger.info(`Cleanup: truncated ausruestung_anfragen (${count} rows) and reset sequence`);
return { count, deleted: true };
}
async resetIssuesSequence(confirm: boolean): Promise<CleanupResult> {
if (!confirm) {
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM issues');
return { count: rows[0].count, deleted: false };
}
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM issues');
const count = rows[0].count;
await pool.query('TRUNCATE issues CASCADE');
await pool.query('ALTER SEQUENCE issues_id_seq RESTART WITH 1');
logger.info(`Cleanup: truncated issues (${count} rows) and reset sequence`);
return { count, deleted: true };
}
}
export default new CleanupService();

View File

@@ -714,7 +714,8 @@ class EventsService {
FROM users
WHERE authentik_groups IS NOT NULL
) g
WHERE group_name != 'dashboard_admin'
WHERE group_name LIKE 'dashboard_%'
AND group_name != 'dashboard_admin'
ORDER BY group_name`
);