From 948b211f70932f4ade86ba6a6436c082cf0e12c2 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Mon, 23 Mar 2026 16:54:09 +0100 Subject: [PATCH] new features --- .../src/controllers/bestellung.controller.ts | 6 +- backend/src/services/bestellung.service.ts | 21 ++++- frontend/src/components/shared/Sidebar.tsx | 22 +++-- frontend/src/pages/Bestellungen.tsx | 80 ++++++++++++++++--- frontend/src/pages/Shop.tsx | 11 +-- frontend/src/types/bestellung.types.ts | 1 + 6 files changed, 115 insertions(+), 26 deletions(-) diff --git a/backend/src/controllers/bestellung.controller.ts b/backend/src/controllers/bestellung.controller.ts index 4b5f5f2..4c26fca 100644 --- a/backend/src/controllers/bestellung.controller.ts +++ b/backend/src/controllers/bestellung.controller.ts @@ -113,7 +113,7 @@ class BestellungController { } async createOrder(req: Request, res: Response): Promise { - const { bezeichnung, lieferant_id, budget, besteller_id } = req.body; + const { bezeichnung, lieferant_id, budget, besteller_id, positionen } = req.body; if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) { res.status(400).json({ success: false, message: 'Bezeichnung ist erforderlich' }); return; @@ -130,6 +130,10 @@ class BestellungController { res.status(400).json({ success: false, message: 'Ungültige Besteller-ID' }); return; } + if (positionen != null && !Array.isArray(positionen)) { + res.status(400).json({ success: false, message: 'Positionen muss ein Array sein' }); + return; + } try { const order = await bestellungService.createOrder(req.body, req.user!.id); res.status(201).json({ success: true, data: order }); diff --git a/backend/src/services/bestellung.service.ts b/backend/src/services/bestellung.service.ts index 02482ee..f4b2fee 100644 --- a/backend/src/services/bestellung.service.ts +++ b/backend/src/services/bestellung.service.ts @@ -174,21 +174,38 @@ async function getOrderById(id: number) { } } -async function createOrder(data: { bezeichnung: string; lieferant_id?: number; besteller_id?: string; notizen?: string; budget?: number }, userId: string) { +async function createOrder(data: { bezeichnung: string; lieferant_id?: number; besteller_id?: string; notizen?: string; budget?: number; positionen?: Array<{ bezeichnung: string; menge: number; einheit?: string }> }, userId: string) { + const client = await pool.connect(); try { + await client.query('BEGIN'); const bestellerId = data.besteller_id && data.besteller_id.trim() ? data.besteller_id.trim() : null; - const result = await pool.query( + const result = await client.query( `INSERT INTO bestellungen (bezeichnung, lieferant_id, besteller_id, notizen, budget, erstellt_von) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [data.bezeichnung, data.lieferant_id || null, bestellerId, data.notizen || null, data.budget || null, userId] ); const order = result.rows[0]; + + if (data.positionen && data.positionen.length > 0) { + for (const pos of data.positionen) { + await client.query( + `INSERT INTO bestellpositionen (bestellung_id, bezeichnung, menge, einheit) + VALUES ($1, $2, $3, $4)`, + [order.id, pos.bezeichnung, pos.menge, pos.einheit || 'Stk'] + ); + } + } + await logAction(order.id, 'Bestellung erstellt', `Bestellung "${data.bezeichnung}" erstellt`, userId); + await client.query('COMMIT'); return order; } catch (error) { + await client.query('ROLLBACK'); logger.error('BestellungService.createOrder failed', { error }); throw new Error('Bestellung konnte nicht erstellt werden'); + } finally { + client.release(); } } diff --git a/frontend/src/components/shared/Sidebar.tsx b/frontend/src/components/shared/Sidebar.tsx index 97146a7..d3da998 100644 --- a/frontend/src/components/shared/Sidebar.tsx +++ b/frontend/src/components/shared/Sidebar.tsx @@ -126,12 +126,7 @@ const baseNavigationItems: NavigationItem[] = [ text: 'Shop', icon: , path: '/shop', - subItems: [ - { text: 'Katalog', path: '/shop?tab=0' }, - { text: 'Meine Anfragen', path: '/shop?tab=1' }, - { text: 'Alle Anfragen', path: '/shop?tab=2' }, - { text: 'Übersicht', path: '/shop?tab=3' }, - ], + // subItems computed dynamically in navigationItems useMemo permission: 'shop:view', }, { @@ -194,8 +189,21 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) { subItems: vehicleSubItems.length > 0 ? vehicleSubItems : undefined, permission: 'fahrzeuge:view', }; + + // Build Shop sub-items dynamically based on permissions (tab order must match Shop.tsx) + const shopSubItems: SubItem[] = []; + let shopTabIdx = 0; + if (hasPermission('shop:create_request')) { shopSubItems.push({ text: 'Meine Anfragen', path: `/shop?tab=${shopTabIdx}` }); shopTabIdx++; } + if (hasPermission('shop:approve_requests')) { shopSubItems.push({ text: 'Alle Anfragen', path: `/shop?tab=${shopTabIdx}` }); shopTabIdx++; } + if (hasPermission('shop:view_overview')) { shopSubItems.push({ text: 'Übersicht', path: `/shop?tab=${shopTabIdx}` }); shopTabIdx++; } + shopSubItems.push({ text: 'Katalog', path: `/shop?tab=${shopTabIdx}` }); + const items = baseNavigationItems - .map((item) => item.path === '/fahrzeuge' ? fahrzeugeItem : item) + .map((item) => { + if (item.path === '/fahrzeuge') return fahrzeugeItem; + if (item.path === '/shop') return { ...item, subItems: shopSubItems }; + return item; + }) .filter((item) => !item.permission || hasPermission(item.permission)); return hasPermission('admin:view') ? [...items, adminItem, adminSettingsItem] : items; }, [vehicleSubItems, hasPermission]); diff --git a/frontend/src/pages/Bestellungen.tsx b/frontend/src/pages/Bestellungen.tsx index d8889fc..ab19333 100644 --- a/frontend/src/pages/Bestellungen.tsx +++ b/frontend/src/pages/Bestellungen.tsx @@ -26,7 +26,7 @@ import { Tooltip, Autocomplete, } from '@mui/material'; -import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material'; +import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon, RemoveCircleOutline as RemoveIcon } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate, useSearchParams } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; @@ -35,7 +35,7 @@ import { useNotification } from '../contexts/NotificationContext'; import { usePermissionContext } from '../contexts/PermissionContext'; import { bestellungApi } from '../services/bestellung'; import { BESTELLUNG_STATUS_LABELS, BESTELLUNG_STATUS_COLORS } from '../types/bestellung.types'; -import type { BestellungStatus, BestellungFormData, LieferantFormData, Lieferant } from '../types/bestellung.types'; +import type { BestellungStatus, BestellungFormData, BestellpositionFormData, LieferantFormData, Lieferant } from '../types/bestellung.types'; // ── Helpers ── @@ -61,8 +61,9 @@ const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'erstellt', 'bestellt', 'te // ── Empty form data ── -const emptyOrderForm: BestellungFormData = { bezeichnung: '', lieferant_id: undefined, besteller_id: '', budget: undefined, notizen: '' }; +const emptyOrderForm: BestellungFormData = { bezeichnung: '', lieferant_id: undefined, besteller_id: '', budget: undefined, notizen: '', positionen: [] }; const emptyVendorForm: LieferantFormData = { name: '', kontakt_name: '', email: '', telefon: '', adresse: '', website: '', notizen: '' }; +const emptyPosition: BestellpositionFormData = { bezeichnung: '', menge: 1, einheit: 'Stk' }; // ══════════════════════════════════════════════════════════════════════════════ // Component @@ -330,12 +331,13 @@ export default function Bestellungen() { {/* ── Create Order Dialog ── */} - setOrderDialogOpen(false)} maxWidth="sm" fullWidth> + setOrderDialogOpen(false)} maxWidth="md" fullWidth> Neue Bestellung setOrderForm((f) => ({ ...f, bezeichnung: e.target.value }))} /> @@ -378,13 +380,6 @@ export default function Bestellungen() { onChange={(_e, v) => setOrderForm((f) => ({ ...f, besteller_id: v?.id || '' }))} renderInput={(params) => } /> - setOrderForm((f) => ({ ...f, budget: e.target.value ? Number(e.target.value) : undefined }))} - inputProps={{ min: 0, step: 0.01 }} - /> setOrderForm((f) => ({ ...f, notizen: e.target.value }))} /> + + {/* ── Dynamic Items List ── */} + Positionen + {(orderForm.positionen || []).map((pos, idx) => ( + + { + const next = [...(orderForm.positionen || [])]; + next[idx] = { ...next[idx], bezeichnung: e.target.value }; + setOrderForm((f) => ({ ...f, positionen: next })); + }} + sx={{ flexGrow: 1 }} + /> + { + const next = [...(orderForm.positionen || [])]; + next[idx] = { ...next[idx], menge: Math.max(1, Number(e.target.value) || 1) }; + setOrderForm((f) => ({ ...f, positionen: next })); + }} + sx={{ width: 90 }} + inputProps={{ min: 1 }} + /> + { + const next = [...(orderForm.positionen || [])]; + next[idx] = { ...next[idx], einheit: e.target.value }; + setOrderForm((f) => ({ ...f, positionen: next })); + }} + sx={{ width: 100 }} + /> + { + const next = (orderForm.positionen || []).filter((_, i) => i !== idx); + setOrderForm((f) => ({ ...f, positionen: next })); + }} + > + + + + ))} + diff --git a/frontend/src/pages/Shop.tsx b/frontend/src/pages/Shop.tsx index f6b4796..22dcce0 100644 --- a/frontend/src/pages/Shop.tsx +++ b/frontend/src/pages/Shop.tsx @@ -653,11 +653,12 @@ export default function Shop() { }; const tabIndex = useMemo(() => { - const map: Record = { katalog: 0 }; - let next = 1; + const map: Record = {}; + let next = 0; if (canCreate) { map.meine = next; next++; } if (canApprove) { map.alle = next; next++; } - if (canViewOverview) { map.uebersicht = next; } + if (canViewOverview) { map.uebersicht = next; next++; } + map.katalog = next; return map; }, [canCreate, canApprove, canViewOverview]); @@ -675,17 +676,17 @@ export default function Shop() { - {canCreate && } {canApprove && } {canViewOverview && } + - {activeTab === tabIndex.katalog && } {canCreate && activeTab === tabIndex.meine && } {canApprove && activeTab === tabIndex.alle && } {canViewOverview && activeTab === tabIndex.uebersicht && } + {activeTab === tabIndex.katalog && } ); } diff --git a/frontend/src/types/bestellung.types.ts b/frontend/src/types/bestellung.types.ts index 34a997c..1a09b4c 100644 --- a/frontend/src/types/bestellung.types.ts +++ b/frontend/src/types/bestellung.types.ts @@ -75,6 +75,7 @@ export interface BestellungFormData { status?: BestellungStatus; budget?: number; notizen?: string; + positionen?: BestellpositionFormData[]; } // ── Line Items ──