Files
dashboard/frontend/src/pages/Dashboard.tsx
Matthias Hochmeister 5032e1593b new features
2026-03-23 13:08:19 +01:00

248 lines
9.5 KiB
TypeScript

import { useState, useEffect } from 'react';
import {
Container,
Box,
Fade,
} from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import SkeletonCard from '../components/shared/SkeletonCard';
import UserProfile from '../components/dashboard/UserProfile';
import UpcomingEventsWidget from '../components/dashboard/UpcomingEventsWidget';
import AtemschutzDashboardCard from '../components/atemschutz/AtemschutzDashboardCard';
import EquipmentDashboardCard from '../components/equipment/EquipmentDashboardCard';
import VehicleDashboardCard from '../components/vehicles/VehicleDashboardCard';
import BookStackRecentWidget from '../components/dashboard/BookStackRecentWidget';
import BookStackSearchWidget from '../components/dashboard/BookStackSearchWidget';
import VikunjaMyTasksWidget from '../components/dashboard/VikunjaMyTasksWidget';
import VikunjaQuickAddWidget from '../components/dashboard/VikunjaQuickAddWidget';
import VikunjaOverdueNotifier from '../components/dashboard/VikunjaOverdueNotifier';
import AtemschutzExpiryNotifier from '../components/dashboard/AtemschutzExpiryNotifier';
import AdminStatusWidget from '../components/dashboard/AdminStatusWidget';
import AnnouncementBanner from '../components/dashboard/AnnouncementBanner';
import VehicleBookingQuickAddWidget from '../components/dashboard/VehicleBookingQuickAddWidget';
import VehicleBookingListWidget from '../components/dashboard/VehicleBookingListWidget';
import EventQuickAddWidget from '../components/dashboard/EventQuickAddWidget';
import LinksWidget from '../components/dashboard/LinksWidget';
import BannerWidget from '../components/dashboard/BannerWidget';
import WidgetGroup from '../components/dashboard/WidgetGroup';
import BestellungenWidget from '../components/dashboard/BestellungenWidget';
import ShopWidget from '../components/dashboard/ShopWidget';
import { preferencesApi } from '../services/settings';
import { configApi } from '../services/config';
import { WidgetKey } from '../constants/widgets';
function Dashboard() {
const { user } = useAuth();
const { hasPermission } = usePermissionContext();
const [dataLoading, setDataLoading] = useState(true);
const { data: preferences } = useQuery({
queryKey: ['user-preferences'],
queryFn: preferencesApi.get,
});
const { data: externalLinks } = useQuery({
queryKey: ['external-links'],
queryFn: () => configApi.getExternalLinks(),
staleTime: 10 * 60 * 1000,
});
const linkCollections = (externalLinks?.linkCollections ?? []).filter(
(c) => c.links.length > 0
);
const widgetVisible = (key: WidgetKey) => {
return preferences?.widgets?.[key] !== false;
};
useEffect(() => {
const timer = setTimeout(() => {
setDataLoading(false);
}, 800);
return () => clearTimeout(timer);
}, []);
return (
<DashboardLayout>
{/* Vikunja — Overdue Notifier (invisible, polling component — outside grid) */}
<VikunjaOverdueNotifier />
{/* Atemschutz — Expiry Notifier (invisible, polling component — outside grid) */}
<AtemschutzExpiryNotifier />
<Container maxWidth={false} disableGutters>
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: 2.5,
alignItems: 'start',
}}
>
{/* Announcement Banner — spans full width, renders null when no banners */}
<AnnouncementBanner gridColumn="1 / -1" />
{/* User Profile Card — full width, contains welcome greeting */}
{user && (
<Box sx={{ gridColumn: '1 / -1' }}>
{dataLoading ? (
<SkeletonCard variant="detailed" />
) : (
<Fade in={true} timeout={600} style={{ transitionDelay: '100ms' }}>
<Box>
<UserProfile user={user} />
</Box>
</Fade>
)}
</Box>
)}
{/* Status Group */}
<WidgetGroup title="Status" gridColumn="1 / -1">
{hasPermission('fahrzeuge:widget') && widgetVisible('vehicles') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '300ms' }}>
<Box>
<VehicleDashboardCard />
</Box>
</Fade>
)}
{hasPermission('ausruestung:widget') && widgetVisible('equipment') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '350ms' }}>
<Box>
<EquipmentDashboardCard />
</Box>
</Fade>
)}
{hasPermission('atemschutz:widget') && widgetVisible('atemschutz') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '400ms' }}>
<Box>
<AtemschutzDashboardCard />
</Box>
</Fade>
)}
{hasPermission('admin:view') && widgetVisible('adminStatus') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '440ms' }}>
<Box>
<AdminStatusWidget />
</Box>
</Fade>
)}
{hasPermission('bestellungen:widget') && widgetVisible('bestellungen') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '460ms' }}>
<Box>
<BestellungenWidget />
</Box>
</Fade>
)}
{hasPermission('shop:widget') && widgetVisible('shopRequests') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '470ms' }}>
<Box>
<ShopWidget />
</Box>
</Fade>
)}
</WidgetGroup>
{/* Kalender Group */}
<WidgetGroup title="Kalender" gridColumn="1 / -1">
{hasPermission('kalender:view') && widgetVisible('events') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '480ms' }}>
<Box>
<UpcomingEventsWidget />
</Box>
</Fade>
)}
{hasPermission('kalender:view_bookings') && widgetVisible('vehicleBookingList') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '520ms' }}>
<Box>
<VehicleBookingListWidget />
</Box>
</Fade>
)}
{hasPermission('kalender:manage_bookings') && widgetVisible('vehicleBooking') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '560ms' }}>
<Box>
<VehicleBookingQuickAddWidget />
</Box>
</Fade>
)}
{hasPermission('kalender:create') && widgetVisible('eventQuickAdd') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '600ms' }}>
<Box>
<EventQuickAddWidget />
</Box>
</Fade>
)}
</WidgetGroup>
{/* Dienste Group */}
<WidgetGroup title="Dienste" gridColumn="1 / -1">
{hasPermission('wissen:widget_recent') && widgetVisible('bookstackRecent') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '600ms' }}>
<Box>
<BookStackRecentWidget />
</Box>
</Fade>
)}
{hasPermission('wissen:widget_search') && widgetVisible('bookstackSearch') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '640ms' }}>
<Box>
<BookStackSearchWidget />
</Box>
</Fade>
)}
{hasPermission('vikunja:widget_tasks') && widgetVisible('vikunjaTasks') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '680ms' }}>
<Box>
<VikunjaMyTasksWidget />
</Box>
</Fade>
)}
{hasPermission('vikunja:widget_quick_add') && widgetVisible('vikunjaQuickAdd') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '720ms' }}>
<Box>
<VikunjaQuickAddWidget />
</Box>
</Fade>
)}
</WidgetGroup>
{/* Information Group */}
<WidgetGroup title="Information" gridColumn="1 / -1">
{hasPermission('dashboard:widget_links') && widgetVisible('links') && linkCollections.map((collection, idx) => (
<Fade key={collection.id} in={!dataLoading} timeout={600} style={{ transitionDelay: `${760 + idx * 40}ms` }}>
<Box>
<LinksWidget collection={collection} />
</Box>
</Fade>
))}
{hasPermission('dashboard:widget_banner') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: `${760 + linkCollections.length * 40}ms` }}>
<Box>
<BannerWidget />
</Box>
</Fade>
)}
</WidgetGroup>
</Box>
</Container>
</DashboardLayout>
);
}
export default Dashboard;