This commit is contained in:
Matthias Hochmeister
2026-03-26 16:12:18 +01:00
parent 19dd528765
commit 03f489d546
5 changed files with 40 additions and 102 deletions

View File

@@ -6,10 +6,9 @@ import {
Card, Card,
CardContent, CardContent,
CircularProgress, CircularProgress,
Link,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { atemschutzApi } from '../../services/atemschutz'; import { atemschutzApi } from '../../services/atemschutz';
import { useCountUp } from '../../hooks/useCountUp'; import { useCountUp } from '../../hooks/useCountUp';
@@ -22,6 +21,7 @@ interface AtemschutzDashboardCardProps {
const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
hideWhenEmpty = false, hideWhenEmpty = false,
}) => { }) => {
const navigate = useNavigate();
const { data: stats, isLoading, isError } = useQuery<AtemschutzStats>({ const { data: stats, isLoading, isError } = useQuery<AtemschutzStats>({
queryKey: ['atemschutz-stats'], queryKey: ['atemschutz-stats'],
queryFn: () => atemschutzApi.getStats(), queryFn: () => atemschutzApi.getStats(),
@@ -68,7 +68,7 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
if (hideWhenEmpty && allGood) return null; if (hideWhenEmpty && allGood) return null;
return ( return (
<Card sx={{ height: '100%' }}> <Card sx={{ height: '100%', cursor: 'pointer' }} onClick={() => navigate('/atemschutz')}>
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Atemschutz Atemschutz
@@ -124,18 +124,6 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
Alle Atemschutzträger einsatzbereit Alle Atemschutzträger einsatzbereit
</Typography> </Typography>
)} )}
{/* Link to management page */}
<Box sx={{ mt: 2 }}>
<Link
component={RouterLink}
to="/atemschutz"
underline="hover"
variant="body2"
>
Zur Verwaltung
</Link>
</Box>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -55,12 +55,15 @@ function AusruestungsanfrageWidget() {
<Typography variant="body2" color="text.secondary">Keine Anfragen vorhanden.</Typography> <Typography variant="body2" color="text.secondary">Keine Anfragen vorhanden.</Typography>
) : ( ) : (
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip label={`${overview.pending_count} Offen`} size="small" color={overview.pending_count > 0 ? 'warning' : 'default'} variant="outlined" />
<Chip label={`${overview.approved_count} Genehmigt`} size="small" color={overview.approved_count > 0 ? 'info' : 'default'} variant="outlined" />
{overview.unhandled_count > 0 && ( {overview.unhandled_count > 0 && (
<Chip label={`${overview.unhandled_count} Neu`} size="small" color="error" variant="outlined" /> <Chip label={`${overview.unhandled_count} Neu`} size="small" color="error" variant="outlined" />
)} )}
<Chip label={`${overview.total_count} Gesamt`} size="small" variant="outlined" /> {overview.pending_count > 0 && (
<Chip label={`${overview.pending_count} Offen`} size="small" color="warning" variant="outlined" />
)}
{overview.approved_count > 0 && (
<Chip label={`${overview.approved_count} Genehmigt`} size="small" color="info" variant="outlined" />
)}
</Box> </Box>
)} )}
</CardContent> </CardContent>

View File

@@ -1,4 +1,4 @@
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 { LocalShipping } from '@mui/icons-material'; import { LocalShipping } from '@mui/icons-material';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -20,13 +20,18 @@ function BestellungenWidget() {
(o) => !['abgeschlossen'].includes(o.status) (o) => !['abgeschlossen'].includes(o.status)
); );
// Group open orders by status, keep only non-zero counts
const statusCounts = (Object.keys(BESTELLUNG_STATUS_LABELS) as BestellungStatus[])
.filter((s) => s !== 'abgeschlossen')
.map((s) => ({ status: s, count: openOrders.filter((o) => o.status === s).length }))
.filter((s) => s.count > 0);
if (isLoading) { if (isLoading) {
return ( return (
<Card> <Card>
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom>Bestellungen</Typography> <Typography variant="h6" gutterBottom>Bestellungen</Typography>
<Skeleton variant="rectangular" height={60} /> <Skeleton variant="rectangular" height={40} />
<Skeleton variant="rectangular" height={60} sx={{ mt: 1 }} />
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -45,61 +50,27 @@ function BestellungenWidget() {
); );
} }
if (openOrders.length === 0) {
return ( return (
<Card> <Card sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }} onClick={() => navigate('/bestellungen')}>
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom>Bestellungen</Typography> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: 'text.secondary' }}>
<LocalShipping fontSize="small" />
<Typography variant="body2">Keine offenen Bestellungen</Typography>
</Box>
</CardContent>
</Card>
);
}
return (
<Card>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="h6">Bestellungen</Typography> <Typography variant="h6">Bestellungen</Typography>
<Chip label={`${openOrders.length} offen`} size="small" color="primary" /> <LocalShipping fontSize="small" color="action" />
</Box> </Box>
<List dense disablePadding> {statusCounts.length === 0 ? (
{openOrders.slice(0, 5).map((order, idx) => ( <Typography variant="body2" color="text.secondary">Keine offenen Bestellungen</Typography>
<Box key={order.id}> ) : (
{idx > 0 && <Divider />} <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
<ListItem {statusCounts.map(({ status, count }) => (
disablePadding
sx={{ cursor: 'pointer', py: 0.5, '&:hover': { bgcolor: 'action.hover' } }}
onClick={() => navigate(`/bestellungen/${order.id}`)}
>
<ListItemText
primary={order.bezeichnung}
secondary={order.lieferant_name || 'Kein Lieferant'}
primaryTypographyProps={{ variant: 'body2', noWrap: true }}
secondaryTypographyProps={{ variant: 'caption' }}
/>
<Chip <Chip
label={BESTELLUNG_STATUS_LABELS[order.status as BestellungStatus]} key={status}
color={BESTELLUNG_STATUS_COLORS[order.status as BestellungStatus]} label={`${count} ${BESTELLUNG_STATUS_LABELS[status]}`}
color={BESTELLUNG_STATUS_COLORS[status]}
size="small" size="small"
sx={{ ml: 1 }} variant="outlined"
/> />
</ListItem>
</Box>
))} ))}
</List> </Box>
{openOrders.length > 5 && (
<Typography
variant="caption"
color="primary"
sx={{ cursor: 'pointer', mt: 1, display: 'block' }}
onClick={() => navigate('/bestellungen')}
>
Alle {openOrders.length} Bestellungen anzeigen
</Typography>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -6,10 +6,9 @@ import {
Card, Card,
CardContent, CardContent,
CircularProgress, CircularProgress,
Link,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { equipmentApi } from '../../services/equipment'; import { equipmentApi } from '../../services/equipment';
import { useCountUp } from '../../hooks/useCountUp'; import { useCountUp } from '../../hooks/useCountUp';
@@ -22,6 +21,7 @@ interface EquipmentDashboardCardProps {
const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({ const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({
hideWhenEmpty = false, hideWhenEmpty = false,
}) => { }) => {
const navigate = useNavigate();
const { data: stats, isLoading, isError } = useQuery<EquipmentStats>({ const { data: stats, isLoading, isError } = useQuery<EquipmentStats>({
queryKey: ['equipment-stats'], queryKey: ['equipment-stats'],
queryFn: () => equipmentApi.getStats(), queryFn: () => equipmentApi.getStats(),
@@ -68,7 +68,7 @@ const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({
if (hideWhenEmpty && allGood) return null; if (hideWhenEmpty && allGood) return null;
return ( return (
<Card sx={{ height: '100%' }}> <Card sx={{ height: '100%', cursor: 'pointer' }} onClick={() => navigate('/ausruestung')}>
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Ausrüstung Ausrüstung
@@ -134,18 +134,6 @@ const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({
Alle Ausrüstung einsatzbereit Alle Ausrüstung einsatzbereit
</Typography> </Typography>
)} )}
{/* Link to management page */}
<Box sx={{ mt: 2 }}>
<Link
component={RouterLink}
to="/ausruestung"
underline="hover"
variant="body2"
>
Zur Verwaltung
</Link>
</Box>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -6,10 +6,9 @@ import {
Card, Card,
CardContent, CardContent,
CircularProgress, CircularProgress,
Link,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { vehiclesApi } from '../../services/vehicles'; import { vehiclesApi } from '../../services/vehicles';
import { equipmentApi } from '../../services/equipment'; import { equipmentApi } from '../../services/equipment';
@@ -24,6 +23,7 @@ interface VehicleDashboardCardProps {
const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
hideWhenEmpty = false, hideWhenEmpty = false,
}) => { }) => {
const navigate = useNavigate();
const { data: stats, isLoading: statsLoading, isError: statsError } = useQuery<VehicleStats>({ const { data: stats, isLoading: statsLoading, isError: statsError } = useQuery<VehicleStats>({
queryKey: ['vehicle-stats'], queryKey: ['vehicle-stats'],
queryFn: () => vehiclesApi.getStats(), queryFn: () => vehiclesApi.getStats(),
@@ -85,7 +85,7 @@ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
if (hideWhenEmpty && allGood) return null; if (hideWhenEmpty && allGood) return null;
return ( return (
<Card sx={{ height: '100%' }}> <Card sx={{ height: '100%', cursor: 'pointer' }} onClick={() => navigate('/fahrzeuge')}>
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Fahrzeuge Fahrzeuge
@@ -143,18 +143,6 @@ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
Alle Fahrzeuge einsatzbereit Alle Fahrzeuge einsatzbereit
</Typography> </Typography>
)} )}
{/* Link to management page */}
<Box sx={{ mt: 2 }}>
<Link
component={RouterLink}
to="/fahrzeuge"
underline="hover"
variant="body2"
>
Zur Verwaltung
</Link>
</Box>
</CardContent> </CardContent>
</Card> </Card>
); );