import { useState, useMemo } from 'react'; import { Box, Tab, Tabs, Typography, Card, CardContent, CardActions, Grid, Button, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, Badge, MenuItem, Select, FormControl, InputLabel, Autocomplete, Collapse, Divider, Tooltip, } from '@mui/material'; import { Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, ShoppingCart, Check as CheckIcon, Close as CloseIcon, Link as LinkIcon, ExpandMore, ExpandLess, } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useSearchParams } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import ChatAwareFab from '../components/shared/ChatAwareFab'; import { useNotification } from '../contexts/NotificationContext'; import { usePermissionContext } from '../contexts/PermissionContext'; import { shopApi } from '../services/shop'; import { bestellungApi } from '../services/bestellung'; import { SHOP_STATUS_LABELS, SHOP_STATUS_COLORS } from '../types/shop.types'; import type { ShopArtikel, ShopArtikelFormData, ShopAnfrageFormItem, ShopAnfrageDetailResponse, ShopAnfrageStatus, ShopAnfrage, ShopOverview } from '../types/shop.types'; import type { Bestellung } from '../types/bestellung.types'; // ─── Helpers ───────────────────────────────────────────────────────────────── function formatOrderId(r: ShopAnfrage): string { if (r.bestell_jahr && r.bestell_nummer) { return `${r.bestell_jahr}/${String(r.bestell_nummer).padStart(3, '0')}`; } return `#${r.id}`; } // ─── Catalog Tab ──────────────────────────────────────────────────────────── interface DraftItem { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; } function KatalogTab() { const { showSuccess, showError } = useNotification(); const { hasPermission } = usePermissionContext(); const queryClient = useQueryClient(); const canManage = hasPermission('shop:manage_catalog'); const canCreate = hasPermission('shop:create_request'); const [filterKategorie, setFilterKategorie] = useState(''); const [draft, setDraft] = useState([]); const [customText, setCustomText] = useState(''); const [submitOpen, setSubmitOpen] = useState(false); const [submitNotizen, setSubmitNotizen] = useState(''); const [artikelDialogOpen, setArtikelDialogOpen] = useState(false); const [editArtikel, setEditArtikel] = useState(null); const [artikelForm, setArtikelForm] = useState({ bezeichnung: '' }); const { data: items = [], isLoading } = useQuery({ queryKey: ['shop', 'items', filterKategorie], queryFn: () => shopApi.getItems(filterKategorie ? { kategorie: filterKategorie } : undefined), }); const { data: categories = [] } = useQuery({ queryKey: ['shop', 'categories'], queryFn: () => shopApi.getCategories(), }); const createItemMut = useMutation({ mutationFn: (data: ShopArtikelFormData) => shopApi.createItem(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['shop'] }); showSuccess('Artikel erstellt'); setArtikelDialogOpen(false); }, onError: () => showError('Fehler beim Erstellen'), }); const updateItemMut = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => shopApi.updateItem(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['shop'] }); showSuccess('Artikel aktualisiert'); setArtikelDialogOpen(false); }, onError: () => showError('Fehler beim Aktualisieren'), }); const deleteItemMut = useMutation({ mutationFn: (id: number) => shopApi.deleteItem(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['shop'] }); showSuccess('Artikel gelöscht'); }, onError: () => showError('Fehler beim Löschen'), }); const createRequestMut = useMutation({ mutationFn: ({ items, notizen }: { items: ShopAnfrageFormItem[]; notizen?: string }) => shopApi.createRequest(items, notizen), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['shop'] }); showSuccess('Anfrage gesendet'); setDraft([]); setSubmitOpen(false); setSubmitNotizen(''); }, onError: () => showError('Fehler beim Senden der Anfrage'), }); const addToDraft = (item: ShopArtikel) => { setDraft(prev => { const existing = prev.find(d => d.artikel_id === item.id); if (existing) return prev.map(d => d.artikel_id === item.id ? { ...d, menge: d.menge + 1 } : d); return [...prev, { artikel_id: item.id, bezeichnung: item.bezeichnung, menge: 1 }]; }); }; const addCustomToDraft = () => { const text = customText.trim(); if (!text) return; setDraft(prev => [...prev, { bezeichnung: text, menge: 1 }]); setCustomText(''); }; const removeDraftItem = (idx: number) => setDraft(prev => prev.filter((_, i) => i !== idx)); const updateDraftMenge = (idx: number, menge: number) => { if (menge < 1) return; setDraft(prev => prev.map((d, i) => i === idx ? { ...d, menge } : d)); }; const openNewArtikel = () => { setEditArtikel(null); setArtikelForm({ bezeichnung: '' }); setArtikelDialogOpen(true); }; const openEditArtikel = (a: ShopArtikel) => { setEditArtikel(a); setArtikelForm({ bezeichnung: a.bezeichnung, beschreibung: a.beschreibung, kategorie: a.kategorie }); setArtikelDialogOpen(true); }; const saveArtikel = () => { if (!artikelForm.bezeichnung.trim()) return; if (editArtikel) updateItemMut.mutate({ id: editArtikel.id, data: artikelForm }); else createItemMut.mutate(artikelForm); }; const handleSubmitRequest = () => { if (draft.length === 0) return; createRequestMut.mutate({ items: draft.map(d => ({ artikel_id: d.artikel_id, bezeichnung: d.bezeichnung, menge: d.menge, notizen: d.notizen })), notizen: submitNotizen || undefined, }); }; return ( {/* Filter */} Kategorie {canCreate && ( )} {/* Catalog grid */} {isLoading ? ( Lade Katalog... ) : items.length === 0 ? ( Keine Artikel vorhanden. ) : ( {items.map(item => ( {item.bild_pfad ? ( ) : ( )} {item.bezeichnung} {item.beschreibung && {item.beschreibung}} {item.kategorie && ( )} {canCreate && ( )} {canManage && ( openEditArtikel(item)}> deleteItemMut.mutate(item.id)}> )} ))} )} {/* Free-text item + draft summary */} {canCreate && ( {draft.length > 0 ? 'Anfrage-Entwurf' : 'Freitext-Position hinzufügen'} {draft.map((d, idx) => ( {d.bezeichnung} updateDraftMenge(idx, Number(e.target.value))} sx={{ width: 70 }} inputProps={{ min: 1 }} /> removeDraftItem(idx)}> ))} {draft.length > 0 && } setCustomText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') addCustomToDraft(); }} sx={{ flexGrow: 1 }} /> {draft.length > 0 && ( )} )} {/* Submit dialog */} setSubmitOpen(false)} maxWidth="sm" fullWidth> Anfrage absenden Folgende Positionen werden angefragt: {draft.map((d, idx) => ( - {d.menge}x {d.bezeichnung} ))} setSubmitNotizen(e.target.value)} multiline rows={2} sx={{ mt: 2 }} /> {/* 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 }))} /> setArtikelForm(f => ({ ...f, kategorie: val || undefined }))} renderInput={params => } /> {/* FAB for new catalog item */} {canManage && ( )} ); } // ─── My Requests Tab ──────────────────────────────────────────────────────── function MeineAnfragenTab() { const [expandedId, setExpandedId] = useState(null); const { data: requests = [], isLoading } = useQuery({ queryKey: ['shop', 'myRequests'], queryFn: () => shopApi.getMyRequests(), }); const { data: detail } = useQuery({ queryKey: ['shop', 'request', expandedId], queryFn: () => shopApi.getRequest(expandedId!), enabled: expandedId != null, }); if (isLoading) return Lade Anfragen...; if (requests.length === 0) return Keine Anfragen vorhanden.; return ( Anfrage Status Positionen Erstellt am Admin Notizen {requests.map(r => ( <> setExpandedId(prev => prev === r.id ? null : r.id)}> {expandedId === r.id ? : } {formatOrderId(r)} {r.positionen_count ?? r.items_count ?? '-'} {new Date(r.erstellt_am).toLocaleDateString('de-AT')} {r.admin_notizen || '-'} {expandedId === r.id && ( {r.notizen && Notizen: {r.notizen}} {detail ? ( <> {detail.positionen.map(p => ( - {p.menge}x {p.bezeichnung}{p.notizen ? ` (${p.notizen})` : ''} ))} {detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && ( Verknüpfte Bestellungen: {detail.linked_bestellungen.map(b => ( ))} )} ) : ( Lade Details... )} )} ))}
); } // ─── Admin All Requests Tab ───────────────────────────────────────────────── function AlleAnfragenTab() { const { showSuccess, showError } = useNotification(); const queryClient = useQueryClient(); const [statusFilter, setStatusFilter] = useState(''); const [expandedId, setExpandedId] = useState(null); const [actionDialog, setActionDialog] = useState<{ id: number; action: 'genehmigt' | 'abgelehnt' } | null>(null); const [adminNotizen, setAdminNotizen] = useState(''); const [linkDialog, setLinkDialog] = useState(null); const [selectedBestellung, setSelectedBestellung] = useState(null); const { data: requests = [], isLoading } = useQuery({ queryKey: ['shop', 'requests', statusFilter], queryFn: () => shopApi.getRequests(statusFilter ? { status: statusFilter } : undefined), }); const { data: detail } = useQuery({ queryKey: ['shop', 'request', expandedId], queryFn: () => shopApi.getRequest(expandedId!), enabled: expandedId != null, }); const { data: bestellungen = [] } = useQuery({ queryKey: ['bestellungen'], queryFn: () => bestellungApi.getOrders(), enabled: linkDialog != null, }); const statusMut = useMutation({ mutationFn: ({ id, status, notes }: { id: number; status: string; notes?: string }) => shopApi.updateRequestStatus(id, status, notes), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['shop'] }); showSuccess('Status aktualisiert'); setActionDialog(null); setAdminNotizen(''); }, onError: () => showError('Fehler beim Aktualisieren'), }); const linkMut = useMutation({ mutationFn: ({ anfrageId, bestellungId }: { anfrageId: number; bestellungId: number }) => shopApi.linkToOrder(anfrageId, bestellungId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['shop'] }); showSuccess('Verknüpfung erstellt'); setLinkDialog(null); setSelectedBestellung(null); }, onError: () => showError('Fehler beim Verknüpfen'), }); const handleAction = () => { if (!actionDialog) return; statusMut.mutate({ id: actionDialog.id, status: actionDialog.action, notes: adminNotizen || undefined }); }; if (isLoading) return Lade Anfragen...; return ( Status Filter {requests.length === 0 ? ( Keine Anfragen vorhanden. ) : ( Anfrage Anfrager Status Positionen Erstellt am Aktionen {requests.map(r => ( <> setExpandedId(prev => prev === r.id ? null : r.id)}> {expandedId === r.id ? : } {formatOrderId(r)} {r.anfrager_name || r.anfrager_id} {r.positionen_count ?? r.items_count ?? '-'} {new Date(r.erstellt_am).toLocaleDateString('de-AT')} e.stopPropagation()}> {r.status === 'offen' && ( <> { setActionDialog({ id: r.id, action: 'genehmigt' }); setAdminNotizen(''); }}> { setActionDialog({ id: r.id, action: 'abgelehnt' }); setAdminNotizen(''); }}> )} {r.status === 'genehmigt' && ( setLinkDialog(r.id)}> )} {expandedId === r.id && ( {r.notizen && Notizen: {r.notizen}} {r.admin_notizen && Admin Notizen: {r.admin_notizen}} {detail && detail.anfrage.id === r.id ? ( <> {detail.positionen.map(p => ( - {p.menge}x {p.bezeichnung}{p.notizen ? ` (${p.notizen})` : ''} ))} {detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && ( Verknüpfte Bestellungen: {detail.linked_bestellungen.map(b => ( ))} )} ) : ( Lade Details... )} )} ))}
)} {/* Approve/Reject dialog */} setActionDialog(null)} maxWidth="sm" fullWidth> {actionDialog?.action === 'genehmigt' ? 'Anfrage genehmigen' : 'Anfrage ablehnen'} setAdminNotizen(e.target.value)} multiline rows={2} sx={{ mt: 1 }} /> {/* Link to order dialog */} { setLinkDialog(null); setSelectedBestellung(null); }} maxWidth="sm" fullWidth> Mit Bestellung verknüpfen `#${o.id} – ${o.bezeichnung}`} value={selectedBestellung} onChange={(_, v) => setSelectedBestellung(v)} renderInput={params => } />
); } // ─── Overview Tab ──────────────────────────────────────────────────────────── function UebersichtTab() { const { data: overview, isLoading } = useQuery({ queryKey: ['shop', 'overview'], queryFn: () => shopApi.getOverview(), }); if (isLoading) return Lade Übersicht...; if (!overview) return Keine Daten verfügbar.; return ( {overview.pending_count} Offene Anfragen {overview.approved_count} Genehmigte Anfragen {overview.total_items} Artikel insgesamt {overview.items.length === 0 ? ( Keine offenen/genehmigten Anfragen vorhanden. ) : ( Artikel Gesamtmenge Anfragen {overview.items.map(item => ( {item.bezeichnung} {item.total_menge} {item.anfrage_count} ))}
)}
); } // ─── Main Page ────────────────────────────────────────────────────────────── export default function Shop() { const [searchParams, setSearchParams] = useSearchParams(); const { hasPermission } = usePermissionContext(); const canView = hasPermission('shop:view'); const canCreate = hasPermission('shop:create_request'); const canApprove = hasPermission('shop:approve_requests'); const canViewOverview = hasPermission('shop:view_overview'); const tabCount = 1 + (canCreate ? 1 : 0) + (canApprove ? 1 : 0) + (canViewOverview ? 1 : 0); const [activeTab, setActiveTab] = useState(() => { const t = Number(searchParams.get('tab')); return t >= 0 && t < tabCount ? t : 0; }); const handleTabChange = (_: React.SyntheticEvent, val: number) => { setActiveTab(val); setSearchParams({ tab: String(val) }, { replace: true }); }; const tabIndex = useMemo(() => { const map: Record = {}; let next = 0; if (canCreate) { map.meine = next; next++; } if (canApprove) { map.alle = next; next++; } if (canViewOverview) { map.uebersicht = next; next++; } map.katalog = next; return map; }, [canCreate, canApprove, canViewOverview]); if (!canView) { return ( Keine Berechtigung. ); } return ( Shop {canCreate && } {canApprove && } {canViewOverview && } {canCreate && activeTab === tabIndex.meine && } {canApprove && activeTab === tabIndex.alle && } {canViewOverview && activeTab === tabIndex.uebersicht && } {activeTab === tabIndex.katalog && } ); }