new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 16:09:42 +01:00
parent e9a9478aac
commit 8c66492b27
40 changed files with 2016 additions and 117 deletions

View File

@@ -20,9 +20,18 @@ 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 } 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 {
@@ -291,7 +300,7 @@ function MeineAnfragenTab() {
<TableHead>
<TableRow>
<TableCell width={40} />
<TableCell>#</TableCell>
<TableCell>Anfrage</TableCell>
<TableCell>Status</TableCell>
<TableCell>Positionen</TableCell>
<TableCell>Erstellt am</TableCell>
@@ -303,9 +312,9 @@ function MeineAnfragenTab() {
<>
<TableRow key={r.id} hover sx={{ cursor: 'pointer' }} onClick={() => setExpandedId(prev => prev === r.id ? null : r.id)}>
<TableCell>{expandedId === r.id ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}</TableCell>
<TableCell>{r.id}</TableCell>
<TableCell>{formatOrderId(r)}</TableCell>
<TableCell><Chip label={SHOP_STATUS_LABELS[r.status]} color={SHOP_STATUS_COLORS[r.status]} size="small" /></TableCell>
<TableCell>{r.items_count ?? '-'}</TableCell>
<TableCell>{r.positionen_count ?? r.items_count ?? '-'}</TableCell>
<TableCell>{new Date(r.erstellt_am).toLocaleDateString('de-AT')}</TableCell>
<TableCell>{r.admin_notizen || '-'}</TableCell>
</TableRow>
@@ -424,7 +433,7 @@ function AlleAnfragenTab() {
<TableHead>
<TableRow>
<TableCell width={40} />
<TableCell>#</TableCell>
<TableCell>Anfrage</TableCell>
<TableCell>Anfrager</TableCell>
<TableCell>Status</TableCell>
<TableCell>Positionen</TableCell>
@@ -437,10 +446,10 @@ function AlleAnfragenTab() {
<>
<TableRow key={r.id} hover sx={{ cursor: 'pointer' }} onClick={() => setExpandedId(prev => prev === r.id ? null : r.id)}>
<TableCell>{expandedId === r.id ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}</TableCell>
<TableCell>{r.id}</TableCell>
<TableCell>{formatOrderId(r)}</TableCell>
<TableCell>{r.anfrager_name || r.anfrager_id}</TableCell>
<TableCell><Chip label={SHOP_STATUS_LABELS[r.status]} color={SHOP_STATUS_COLORS[r.status]} size="small" /></TableCell>
<TableCell>{r.items_count ?? '-'}</TableCell>
<TableCell>{r.positionen_count ?? r.items_count ?? '-'}</TableCell>
<TableCell>{new Date(r.erstellt_am).toLocaleDateString('de-AT')}</TableCell>
<TableCell onClick={e => e.stopPropagation()}>
<Box sx={{ display: 'flex', gap: 0.5 }}>
@@ -558,6 +567,68 @@ function AlleAnfragenTab() {
);
}
// ─── Overview Tab ────────────────────────────────────────────────────────────
function UebersichtTab() {
const { data: overview, isLoading } = useQuery<ShopOverview>({
queryKey: ['shop', 'overview'],
queryFn: () => shopApi.getOverview(),
});
if (isLoading) return <Typography color="text.secondary">Lade Übersicht...</Typography>;
if (!overview) return <Typography color="text.secondary">Keine Daten verfügbar.</Typography>;
return (
<Box>
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} sm={4}>
<Paper variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" fontWeight={700}>{overview.pending_count}</Typography>
<Typography variant="body2" color="text.secondary">Offene Anfragen</Typography>
</Paper>
</Grid>
<Grid item xs={12} sm={4}>
<Paper variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" fontWeight={700}>{overview.approved_count}</Typography>
<Typography variant="body2" color="text.secondary">Genehmigte Anfragen</Typography>
</Paper>
</Grid>
<Grid item xs={12} sm={4}>
<Paper variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" fontWeight={700}>{overview.total_items}</Typography>
<Typography variant="body2" color="text.secondary">Artikel insgesamt</Typography>
</Paper>
</Grid>
</Grid>
{overview.items.length === 0 ? (
<Typography color="text.secondary">Keine offenen/genehmigten Anfragen vorhanden.</Typography>
) : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Artikel</TableCell>
<TableCell align="right">Gesamtmenge</TableCell>
<TableCell align="right">Anfragen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{overview.items.map(item => (
<TableRow key={item.bezeichnung}>
<TableCell>{item.bezeichnung}</TableCell>
<TableCell align="right">{item.total_menge}</TableCell>
<TableCell align="right">{item.anfrage_count}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
);
}
// ─── Main Page ──────────────────────────────────────────────────────────────
export default function Shop() {
@@ -567,8 +638,9 @@ export default function Shop() {
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);
const tabCount = 1 + (canCreate ? 1 : 0) + (canApprove ? 1 : 0) + (canViewOverview ? 1 : 0);
const [activeTab, setActiveTab] = useState(() => {
const t = Number(searchParams.get('tab'));
@@ -584,9 +656,10 @@ export default function Shop() {
const map: Record<string, number> = { katalog: 0 };
let next = 1;
if (canCreate) { map.meine = next; next++; }
if (canApprove) { map.alle = next; }
if (canApprove) { map.alle = next; next++; }
if (canViewOverview) { map.uebersicht = next; }
return map;
}, [canCreate, canApprove]);
}, [canCreate, canApprove, canViewOverview]);
if (!canView) {
return (
@@ -605,12 +678,14 @@ export default function Shop() {
<Tab label="Katalog" />
{canCreate && <Tab label="Meine Anfragen" />}
{canApprove && <Tab label="Alle Anfragen" />}
{canViewOverview && <Tab label="Übersicht" />}
</Tabs>
</Box>
{activeTab === tabIndex.katalog && <KatalogTab />}
{canCreate && activeTab === tabIndex.meine && <MeineAnfragenTab />}
{canApprove && activeTab === tabIndex.alle && <AlleAnfragenTab />}
{canViewOverview && activeTab === tabIndex.uebersicht && <UebersichtTab />}
</DashboardLayout>
);
}