update
This commit is contained in:
238
frontend/src/components/dashboard/VehicleBookingListWidget.tsx
Normal file
238
frontend/src/components/dashboard/VehicleBookingListWidget.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Chip,
|
||||
Divider,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { DirectionsCar as DirectionsCarIcon } from '@mui/icons-material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { bookingApi } from '../../services/bookings';
|
||||
import type { FahrzeugBuchungListItem } from '../../types/booking.types';
|
||||
import { BUCHUNGS_ART_COLORS, BUCHUNGS_ART_LABELS } from '../../types/booking.types';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const WEEKDAY_SHORT = ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'];
|
||||
|
||||
function formatDateShort(isoString: string): string {
|
||||
const d = new Date(isoString);
|
||||
const weekday = WEEKDAY_SHORT[d.getDay()];
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
return `${weekday} ${day}.${month}.`;
|
||||
}
|
||||
|
||||
function formatTime(isoString: string): string {
|
||||
const d = new Date(isoString);
|
||||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')} Uhr`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FETCH_LIMIT = 10;
|
||||
const DISPLAY_LIMIT = 5;
|
||||
|
||||
const VehicleBookingListWidget: React.FC = () => {
|
||||
const { data: rawItems = [], isLoading, isError } = useQuery<FahrzeugBuchungListItem[]>({
|
||||
queryKey: ['upcoming-vehicle-bookings'],
|
||||
queryFn: () => bookingApi.getUpcoming(FETCH_LIMIT),
|
||||
});
|
||||
|
||||
const items = React.useMemo(
|
||||
() => rawItems.filter((b) => !b.abgesagt).slice(0, DISPLAY_LIMIT),
|
||||
[rawItems],
|
||||
);
|
||||
|
||||
// ── Loading state ─────────────────────────────────────────────────────────
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 1.5, py: 2 }}>
|
||||
<CircularProgress size={18} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Buchungen werden geladen...
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Error state ───────────────────────────────────────────────────────────
|
||||
if (isError) {
|
||||
return (
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||
<DirectionsCarIcon fontSize="small" color="action" />
|
||||
<Typography variant="h6">Nächste Fahrzeugbuchungen</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="error">
|
||||
Buchungen konnten nicht geladen werden.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main render ───────────────────────────────────────────────────────────
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
'&:hover': { boxShadow: 3 },
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ pb: '8px !important' }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
<DirectionsCarIcon fontSize="small" sx={{ color: 'primary.main' }} />
|
||||
<Typography variant="h6" component="div">
|
||||
Nächste Fahrzeugbuchungen
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 1 }} />
|
||||
|
||||
{/* Empty state */}
|
||||
{items.length === 0 ? (
|
||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Keine bevorstehenden Buchungen
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<List disablePadding>
|
||||
{items.map((booking, index) => {
|
||||
const color = BUCHUNGS_ART_COLORS[booking.buchungs_art] ?? '#9e9e9e';
|
||||
const label = BUCHUNGS_ART_LABELS[booking.buchungs_art] ?? booking.buchungs_art;
|
||||
return (
|
||||
<React.Fragment key={booking.id}>
|
||||
<ListItem
|
||||
disableGutters
|
||||
sx={{
|
||||
py: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
{/* Colored type indicator dot */}
|
||||
<Box
|
||||
sx={{
|
||||
mt: '4px',
|
||||
flexShrink: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '50%',
|
||||
bgcolor: color,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Date + title block */}
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
flexWrap: 'wrap',
|
||||
mb: 0.25,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{formatDateShort(booking.beginn)}
|
||||
{' '}·{' '}
|
||||
{formatTime(booking.beginn)}
|
||||
</Typography>
|
||||
|
||||
<Chip
|
||||
label={label}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 16,
|
||||
fontSize: '0.65rem',
|
||||
bgcolor: `${color}22`,
|
||||
color: color,
|
||||
border: `1px solid ${color}55`,
|
||||
fontWeight: 600,
|
||||
'& .MuiChip-label': { px: '5px' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{booking.titel}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{booking.fahrzeug_name}
|
||||
{booking.fahrzeug_kennzeichen ? ` · ${booking.fahrzeug_kennzeichen}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListItem>
|
||||
|
||||
{index < items.length - 1 && (
|
||||
<Divider component="li" sx={{ listStyle: 'none' }} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
|
||||
{/* Footer link */}
|
||||
<Divider sx={{ mt: 1, mb: 1 }} />
|
||||
<Box sx={{ textAlign: 'right' }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/kalender?tab=1"
|
||||
underline="hover"
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Alle Buchungen
|
||||
</Link>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default VehicleBookingListWidget;
|
||||
@@ -10,6 +10,7 @@ export { default as VikunjaMyTasksWidget } from './VikunjaMyTasksWidget';
|
||||
export { default as VikunjaQuickAddWidget } from './VikunjaQuickAddWidget';
|
||||
export { default as VikunjaOverdueNotifier } from './VikunjaOverdueNotifier';
|
||||
export { default as AdminStatusWidget } from './AdminStatusWidget';
|
||||
export { default as VehicleBookingListWidget } from './VehicleBookingListWidget';
|
||||
export { default as VehicleBookingQuickAddWidget } from './VehicleBookingQuickAddWidget';
|
||||
export { default as EventQuickAddWidget } from './EventQuickAddWidget';
|
||||
export { default as AnnouncementBanner } from './AnnouncementBanner';
|
||||
|
||||
Reference in New Issue
Block a user