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 VikunjaQuickAddWidget } from './VikunjaQuickAddWidget';
|
||||||
export { default as VikunjaOverdueNotifier } from './VikunjaOverdueNotifier';
|
export { default as VikunjaOverdueNotifier } from './VikunjaOverdueNotifier';
|
||||||
export { default as AdminStatusWidget } from './AdminStatusWidget';
|
export { default as AdminStatusWidget } from './AdminStatusWidget';
|
||||||
|
export { default as VehicleBookingListWidget } from './VehicleBookingListWidget';
|
||||||
export { default as VehicleBookingQuickAddWidget } from './VehicleBookingQuickAddWidget';
|
export { default as VehicleBookingQuickAddWidget } from './VehicleBookingQuickAddWidget';
|
||||||
export { default as EventQuickAddWidget } from './EventQuickAddWidget';
|
export { default as EventQuickAddWidget } from './EventQuickAddWidget';
|
||||||
export { default as AnnouncementBanner } from './AnnouncementBanner';
|
export { default as AnnouncementBanner } from './AnnouncementBanner';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const WIDGETS = [
|
|||||||
{ key: 'bookstackSearch', label: 'Wissen — Suche', defaultVisible: true },
|
{ key: 'bookstackSearch', label: 'Wissen — Suche', defaultVisible: true },
|
||||||
{ key: 'vikunjaTasks', label: 'Vikunja Aufgaben', defaultVisible: true },
|
{ key: 'vikunjaTasks', label: 'Vikunja Aufgaben', defaultVisible: true },
|
||||||
{ key: 'vikunjaQuickAdd', label: 'Vikunja Schnelleingabe', defaultVisible: true },
|
{ key: 'vikunjaQuickAdd', label: 'Vikunja Schnelleingabe', defaultVisible: true },
|
||||||
|
{ key: 'vehicleBookingList', label: 'Fahrzeugbuchungen', defaultVisible: true },
|
||||||
{ key: 'vehicleBooking', label: 'Fahrzeugbuchung', defaultVisible: true },
|
{ key: 'vehicleBooking', label: 'Fahrzeugbuchung', defaultVisible: true },
|
||||||
{ key: 'eventQuickAdd', label: 'Termin erstellen', defaultVisible: true },
|
{ key: 'eventQuickAdd', label: 'Termin erstellen', defaultVisible: true },
|
||||||
{ key: 'adminStatus', label: 'Admin Status', defaultVisible: true },
|
{ key: 'adminStatus', label: 'Admin Status', defaultVisible: true },
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import VikunjaOverdueNotifier from '../components/dashboard/VikunjaOverdueNotifi
|
|||||||
import AdminStatusWidget from '../components/dashboard/AdminStatusWidget';
|
import AdminStatusWidget from '../components/dashboard/AdminStatusWidget';
|
||||||
import AnnouncementBanner from '../components/dashboard/AnnouncementBanner';
|
import AnnouncementBanner from '../components/dashboard/AnnouncementBanner';
|
||||||
import VehicleBookingQuickAddWidget from '../components/dashboard/VehicleBookingQuickAddWidget';
|
import VehicleBookingQuickAddWidget from '../components/dashboard/VehicleBookingQuickAddWidget';
|
||||||
|
import VehicleBookingListWidget from '../components/dashboard/VehicleBookingListWidget';
|
||||||
import EventQuickAddWidget from '../components/dashboard/EventQuickAddWidget';
|
import EventQuickAddWidget from '../components/dashboard/EventQuickAddWidget';
|
||||||
import LinksWidget from '../components/dashboard/LinksWidget';
|
import LinksWidget from '../components/dashboard/LinksWidget';
|
||||||
import BannerWidget from '../components/dashboard/BannerWidget';
|
import BannerWidget from '../components/dashboard/BannerWidget';
|
||||||
@@ -144,8 +145,16 @@ function Dashboard() {
|
|||||||
</Fade>
|
</Fade>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canWrite && widgetVisible('vehicleBooking') && (
|
{widgetVisible('vehicleBookingList') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '520ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '520ms' }}>
|
||||||
|
<Box>
|
||||||
|
<VehicleBookingListWidget />
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canWrite && widgetVisible('vehicleBooking') && (
|
||||||
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '560ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<VehicleBookingQuickAddWidget />
|
<VehicleBookingQuickAddWidget />
|
||||||
</Box>
|
</Box>
|
||||||
@@ -153,7 +162,7 @@ function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{canWrite && widgetVisible('eventQuickAdd') && (
|
{canWrite && widgetVisible('eventQuickAdd') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '560ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '600ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<EventQuickAddWidget />
|
<EventQuickAddWidget />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user