From 6ff5cc89ad7ba4565162ce7b3feae816078d07fe Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Tue, 24 Mar 2026 08:59:46 +0100 Subject: [PATCH] rework internal order system --- .../ausruestungsanfrage.controller.ts | 29 ++++- .../migrations/048_katalog_eigenschaften.sql | 18 ++- .../src/routes/ausruestungsanfrage.routes.ts | 1 + .../services/ausruestungsanfrage.service.ts | 117 ++++++++++++------ .../dashboard/AusruestungsanfrageWidget.tsx | 85 ++++--------- frontend/src/pages/Ausruestungsanfrage.tsx | 117 ++++++++++++------ frontend/src/services/ausruestungsanfrage.ts | 19 ++- .../src/types/ausruestungsanfrage.types.ts | 8 ++ 8 files changed, 240 insertions(+), 154 deletions(-) diff --git a/backend/src/controllers/ausruestungsanfrage.controller.ts b/backend/src/controllers/ausruestungsanfrage.controller.ts index 262546a..3702bda 100644 --- a/backend/src/controllers/ausruestungsanfrage.controller.ts +++ b/backend/src/controllers/ausruestungsanfrage.controller.ts @@ -21,15 +21,15 @@ class AusruestungsanfrageController { async createKategorie(req: Request, res: Response): Promise { 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 { 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 { + 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(); diff --git a/backend/src/database/migrations/048_katalog_eigenschaften.sql b/backend/src/database/migrations/048_katalog_eigenschaften.sql index e6d8c4a..e8a0326 100644 --- a/backend/src/database/migrations/048_katalog_eigenschaften.sql +++ b/backend/src/database/migrations/048_katalog_eigenschaften.sql @@ -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 ( diff --git a/backend/src/routes/ausruestungsanfrage.routes.ts b/backend/src/routes/ausruestungsanfrage.routes.ts index 09db3fa..2f6b07f 100644 --- a/backend/src/routes/ausruestungsanfrage.routes.ts +++ b/backend/src/routes/ausruestungsanfrage.routes.ts @@ -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 diff --git a/backend/src/services/ausruestungsanfrage.service.ts b/backend/src/services/ausruestungsanfrage.service.ts index de11f07..4950089 100644 --- a/backend/src/services/ausruestungsanfrage.service.ts +++ b/backend/src/services/ausruestungsanfrage.service.ts @@ -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 = {}; 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, }; diff --git a/frontend/src/components/dashboard/AusruestungsanfrageWidget.tsx b/frontend/src/components/dashboard/AusruestungsanfrageWidget.tsx index 3665a83..905d476 100644 --- a/frontend/src/components/dashboard/AusruestungsanfrageWidget.tsx +++ b/frontend/src/components/dashboard/AusruestungsanfrageWidget.tsx @@ -1,17 +1,16 @@ -import { Card, CardContent, Typography, Box, Chip, List, ListItem, ListItemText, Divider, Skeleton } from '@mui/material'; +import { Card, CardContent, Typography, Box, Chip, Skeleton } from '@mui/material'; import { Build } from '@mui/icons-material'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { ausruestungsanfrageApi } from '../../services/ausruestungsanfrage'; -import { AUSRUESTUNG_STATUS_LABELS, AUSRUESTUNG_STATUS_COLORS } from '../../types/ausruestungsanfrage.types'; -import type { AusruestungAnfrageStatus } from '../../types/ausruestungsanfrage.types'; +import type { AusruestungWidgetOverview } from '../../types/ausruestungsanfrage.types'; function AusruestungsanfrageWidget() { const navigate = useNavigate(); - const { data: requests, isLoading, isError } = useQuery({ - queryKey: ['ausruestungsanfrage-widget-requests'], - queryFn: () => ausruestungsanfrageApi.getRequests({ status: 'offen' }), + const { data: overview, isLoading, isError } = useQuery({ + queryKey: ['ausruestungsanfrage-widget-overview'], + queryFn: () => ausruestungsanfrageApi.getWidgetOverview(), refetchInterval: 5 * 60 * 1000, retry: 1, }); @@ -27,76 +26,42 @@ function AusruestungsanfrageWidget() { ); } - if (isError) { + if (isError || !overview) { return ( Interne Bestellungen - Anfragen konnten nicht geladen werden. + Daten konnten nicht geladen werden. ); } - const pendingCount = requests?.length ?? 0; - - if (pendingCount === 0) { - return ( - - - Interne Bestellungen - - - Keine offenen Anfragen - - - - ); - } + const hasAny = overview.total_count > 0; return ( - + navigate('/ausruestungsanfrage')} + > - + Interne Bestellungen - + - - {(requests ?? []).slice(0, 5).map((req, idx) => ( - - {idx > 0 && } - navigate('/ausruestungsanfrage?tab=2')} - > - - - - - ))} - - {pendingCount > 5 && ( - navigate('/ausruestungsanfrage?tab=2')} - > - Alle {pendingCount} Anfragen anzeigen - + {!hasAny ? ( + Keine Anfragen vorhanden. + ) : ( + + 0 ? 'warning' : 'default'} variant="outlined" /> + 0 ? 'info' : 'default'} variant="outlined" /> + {overview.unhandled_count > 0 && ( + + )} + + )} diff --git a/frontend/src/pages/Ausruestungsanfrage.tsx b/frontend/src/pages/Ausruestungsanfrage.tsx index 6c3060d..d24020a 100644 --- a/frontend/src/pages/Ausruestungsanfrage.tsx +++ b/frontend/src/pages/Ausruestungsanfrage.tsx @@ -93,6 +93,7 @@ function KategorieDialog({ open, onClose }: KategorieDialogProps) { const { showSuccess, showError } = useNotification(); const queryClient = useQueryClient(); const [newName, setNewName] = useState(''); + const [newParentId, setNewParentId] = useState(null); const [editId, setEditId] = useState(null); const [editName, setEditName] = useState(''); @@ -102,14 +103,17 @@ function KategorieDialog({ open, onClose }: KategorieDialogProps) { enabled: open, }); + const topLevel = useMemo(() => kategorien.filter(k => !k.parent_id), [kategorien]); + const childrenOf = useCallback((parentId: number) => kategorien.filter(k => k.parent_id === parentId), [kategorien]); + const createMut = useMutation({ - mutationFn: (name: string) => ausruestungsanfrageApi.createKategorie(name), - onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie erstellt'); setNewName(''); }, + mutationFn: ({ name, parentId }: { name: string; parentId?: number | null }) => ausruestungsanfrageApi.createKategorie(name, parentId), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie erstellt'); setNewName(''); setNewParentId(null); }, onError: () => showError('Fehler beim Erstellen'), }); const updateMut = useMutation({ - mutationFn: ({ id, name }: { id: number; name: string }) => ausruestungsanfrageApi.updateKategorie(id, name), + mutationFn: ({ id, name }: { id: number; name: string }) => ausruestungsanfrageApi.updateKategorie(id, { name }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie aktualisiert'); setEditId(null); }, onError: () => showError('Fehler beim Aktualisieren'), }); @@ -120,55 +124,75 @@ function KategorieDialog({ open, onClose }: KategorieDialogProps) { onError: () => showError('Fehler beim Löschen'), }); + const renderKategorie = (k: { id: number; name: string; parent_id?: number | null }, indent: number) => { + const children = childrenOf(k.id); + return ( + + + {editId === k.id ? ( + <> + setEditName(e.target.value)} + sx={{ flexGrow: 1 }} + onKeyDown={e => { if (e.key === 'Enter' && editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }} + /> + { if (editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }}> + setEditId(null)}> + + ) : ( + <> + + {indent > 0 && '└ '}{k.name} + + + setNewParentId(k.id)}> + + { setEditId(k.id); setEditName(k.name); }}> + deleteMut.mutate(k.id)}> + + )} + + {children.map(c => renderKategorie(c, indent + 1))} + + ); + }; + return ( Kategorien verwalten - - + + setNewName(e.target.value)} sx={{ flexGrow: 1 }} - onKeyDown={e => { if (e.key === 'Enter' && newName.trim()) createMut.mutate(newName.trim()); }} + onKeyDown={e => { if (e.key === 'Enter' && newName.trim()) createMut.mutate({ name: newName.trim(), parentId: newParentId }); }} /> + {newParentId && ( + k.id === newParentId)?.name}`} + size="small" + onDelete={() => setNewParentId(null)} + /> + )} - {kategorien.length === 0 ? ( + {topLevel.length === 0 ? ( Keine Kategorien vorhanden. ) : ( - kategorien.map(k => ( - - {editId === k.id ? ( - <> - setEditName(e.target.value)} - sx={{ flexGrow: 1 }} - onKeyDown={e => { if (e.key === 'Enter' && editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }} - /> - { if (editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }}> - setEditId(null)}> - - ) : ( - <> - {k.name} - { setEditId(k.id); setEditName(k.name); }}> - deleteMut.mutate(k.id)}> - - )} - - )) + topLevel.map(k => renderKategorie(k, 0)) )} @@ -419,7 +443,7 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can /> )} - + {isLoading ? ( Lade Details... ) : !detail ? ( @@ -614,7 +638,7 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can {/* Approve/Reject sub-dialog */} setActionDialog(null)} maxWidth="sm" fullWidth> {actionDialog?.action === 'genehmigt' ? 'Anfrage genehmigen' : 'Anfrage ablehnen'} - + { setLinkDialog(false); setSelectedBestellung(null); }} maxWidth="sm" fullWidth> Mit Bestellung verknüpfen - + `#${o.id} – ${o.bezeichnung}`} @@ -693,6 +717,19 @@ function KatalogTab() { queryFn: () => ausruestungsanfrageApi.getKategorien(), }); + // Build display names for hierarchical categories (e.g. "Kleidung > A-Uniform") + const kategorieOptions = useMemo(() => { + const map = new Map(kategorien.map(k => [k.id, k])); + const getDisplayName = (k: { id: number; name: string; parent_id?: number | null }): string => { + if (k.parent_id) { + const parent = map.get(k.parent_id); + if (parent) return `${parent.name} > ${k.name}`; + } + return k.name; + }; + return kategorien.map(k => ({ id: k.id, name: getDisplayName(k), isChild: !!k.parent_id })); + }, [kategorien]); + const createItemMut = useMutation({ mutationFn: (data: AusruestungArtikelFormData) => ausruestungsanfrageApi.createItem(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel erstellt'); setArtikelDialogOpen(false); }, @@ -733,7 +770,7 @@ function KatalogTab() { Kategorie {canManageCategories && ( @@ -772,7 +809,7 @@ function KatalogTab() { )} - {item.kategorie_name || item.kategorie || '-'} + {kategorieOptions.find(k => k.id === item.kategorie_id)?.name || item.kategorie_name || item.kategorie || '-'} {item.beschreibung || '-'} @@ -792,7 +829,7 @@ function KatalogTab() { {/* Artikel create/edit dialog */} setArtikelDialogOpen(false)} maxWidth="sm" fullWidth> {editArtikel ? 'Artikel bearbeiten' : 'Neuer Artikel'} - + setArtikelForm(f => ({ ...f, bezeichnung: e.target.value }))} fullWidth /> setArtikelForm(f => ({ ...f, beschreibung: e.target.value }))} /> @@ -803,7 +840,7 @@ function KatalogTab() { onChange={e => setArtikelForm(f => ({ ...f, kategorie_id: e.target.value ? Number(e.target.value) : null }))} > Keine - {kategorien.map(k => {k.name})} + {kategorieOptions.map(k => {k.name})} {canManage && } @@ -1010,7 +1047,7 @@ function MeineAnfragenTab() { {/* Create Request Dialog */} { setCreateDialogOpen(false); resetCreateForm(); }} maxWidth="sm" fullWidth> Neue Bestellung - + => { const r = await api.get('/api/ausruestungsanfragen/kategorien'); return r.data.data; }, - createKategorie: async (name: string): Promise => { - const r = await api.post('/api/ausruestungsanfragen/kategorien', { name }); + createKategorie: async (name: string, parentId?: number | null): Promise => { + const r = await api.post('/api/ausruestungsanfragen/kategorien', { name, parent_id: parentId ?? null }); return r.data.data; }, - updateKategorie: async (id: number, name: string): Promise => { - const r = await api.patch(`/api/ausruestungsanfragen/kategorien/${id}`, { name }); + updateKategorie: async (id: number, data: { name?: string; parent_id?: number | null }): Promise => { + const r = await api.patch(`/api/ausruestungsanfragen/kategorien/${id}`, data); return r.data.data; }, deleteKategorie: async (id: number): Promise => { @@ -126,6 +127,12 @@ export const ausruestungsanfrageApi = { return r.data.data; }, + // ── Widget overview ── + getWidgetOverview: async (): Promise => { + const r = await api.get('/api/ausruestungsanfragen/widget-overview'); + return r.data.data; + }, + // ── Users ── getOrderUsers: async (): Promise> => { const r = await api.get('/api/permissions/users-with', { params: { permission: 'ausruestungsanfrage:create_request' } }); diff --git a/frontend/src/types/ausruestungsanfrage.types.ts b/frontend/src/types/ausruestungsanfrage.types.ts index ef92e2f..21a7b81 100644 --- a/frontend/src/types/ausruestungsanfrage.types.ts +++ b/frontend/src/types/ausruestungsanfrage.types.ts @@ -5,6 +5,7 @@ export interface AusruestungKategorie { id: number; name: string; + parent_id?: number | null; erstellt_am?: string; } @@ -134,3 +135,10 @@ export interface AusruestungOverview { unhandled_count: number; total_items: number; } + +export interface AusruestungWidgetOverview { + total_count: number; + pending_count: number; + approved_count: number; + unhandled_count: number; +}