feat: dashboard widgets, auth fix, profile names, dynamic groups
- Add VehicleDashboardCard: self-contained widget modelled after AtemschutzDashboardCard, shows einsatzbereit ratio and inspection warnings inline; replaces StatsCard + InspectionAlerts in Dashboard - Add EquipmentDashboardCard: consolidated equipment status widget showing only aggregated counts (no per-item listing); replaces EquipmentAlerts component in Dashboard - Fix auth race condition: add authInitialized flag to api.ts so 401 responses during initial token validation no longer trigger a spurious redirect to /login; save intended destination before login redirect and restore it after successful auth callback - Fix profile firstname/lastname: add extractNames() helper to auth.controller.ts that falls back to splitting userinfo.name when Authentik does not provide separate given_name/family_name fields; applied on both create and update paths - Dynamic groups endpoint: replace hardcoded KNOWN_GROUPS array in events.controller.ts with a DB query (SELECT DISTINCT unnest (authentik_groups) FROM users); known slugs get German labels via lookup map, unknown slugs are humanized automatically Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,57 +1,32 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Container,
|
||||
Box,
|
||||
Typography,
|
||||
Grid,
|
||||
Fade,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
DirectionsCar,
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import SkeletonCard from '../components/shared/SkeletonCard';
|
||||
import UserProfile from '../components/dashboard/UserProfile';
|
||||
import NextcloudTalkWidget from '../components/dashboard/NextcloudTalkWidget';
|
||||
import UpcomingEventsWidget from '../components/dashboard/UpcomingEventsWidget';
|
||||
import StatsCard from '../components/dashboard/StatsCard';
|
||||
import ActivityFeed from '../components/dashboard/ActivityFeed';
|
||||
import InspectionAlerts from '../components/vehicles/InspectionAlerts';
|
||||
import EquipmentAlerts from '../components/equipment/EquipmentAlerts';
|
||||
import AtemschutzDashboardCard from '../components/atemschutz/AtemschutzDashboardCard';
|
||||
import EquipmentDashboardCard from '../components/equipment/EquipmentDashboardCard';
|
||||
import VehicleDashboardCard from '../components/vehicles/VehicleDashboardCard';
|
||||
import PersonalWarningsBanner from '../components/dashboard/PersonalWarningsBanner';
|
||||
import { vehiclesApi } from '../services/vehicles';
|
||||
import { equipmentApi } from '../services/equipment';
|
||||
import type { VehicleStats } from '../types/vehicle.types';
|
||||
import type { VehicleEquipmentWarning } from '../types/equipment.types';
|
||||
|
||||
function Dashboard() {
|
||||
const { user } = useAuth();
|
||||
const [dataLoading, setDataLoading] = useState(true);
|
||||
const [vehicleStats, setVehicleStats] = useState<VehicleStats | null>(null);
|
||||
const [vehicleWarnings, setVehicleWarnings] = useState<VehicleEquipmentWarning[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDataLoading(false);
|
||||
}, 800);
|
||||
|
||||
// Fetch live vehicle stats for the KPI strip
|
||||
vehiclesApi.getStats()
|
||||
.then((stats) => setVehicleStats(stats))
|
||||
.catch(() => {
|
||||
// Non-critical — KPI will fall back to placeholder
|
||||
});
|
||||
|
||||
// Fetch vehicle equipment warnings
|
||||
equipmentApi.getVehicleWarnings()
|
||||
.then((w) => setVehicleWarnings(w))
|
||||
.catch(() => {
|
||||
// Non-critical — warning indicator simply won't appear
|
||||
});
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
@@ -100,63 +75,20 @@ function Dashboard() {
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Live vehicle KPI — einsatzbereit count from API */}
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
{dataLoading ? (
|
||||
<SkeletonCard variant="basic" />
|
||||
) : (
|
||||
<Fade in={true} timeout={600} style={{ transitionDelay: '200ms' }}>
|
||||
<Box>
|
||||
<StatsCard
|
||||
title="Fahrzeuge einsatzbereit"
|
||||
value={
|
||||
vehicleStats
|
||||
? `${vehicleStats?.einsatzbereit}/${vehicleStats?.total}`
|
||||
: '—'
|
||||
}
|
||||
icon={DirectionsCar}
|
||||
color="success.main"
|
||||
/>
|
||||
{vehicleWarnings.length > 0 && (() => {
|
||||
const errorCount = vehicleWarnings.filter(w =>
|
||||
w.status === 'beschaedigt' || w.status === 'ausser_dienst'
|
||||
).length;
|
||||
const warnCount = vehicleWarnings.filter(w =>
|
||||
w.status === 'in_wartung'
|
||||
).length;
|
||||
const vehicleCount = new Set(vehicleWarnings.map(w => w.fahrzeug_id)).size;
|
||||
const severity = errorCount > 0 ? 'error' : 'warning';
|
||||
return (
|
||||
<Alert
|
||||
severity={severity}
|
||||
variant="outlined"
|
||||
sx={{ mt: 1, py: 0.5, '& .MuiAlert-message': { fontSize: '0.8rem' } }}
|
||||
>
|
||||
{vehicleCount} Fahrzeug{vehicleCount !== 1 ? 'e' : ''} mit Ausrüstungsmangel
|
||||
{errorCount > 0 && ` (${errorCount} kritisch)`}
|
||||
{warnCount > 0 && errorCount === 0 && ` (${warnCount} in Wartung)`}
|
||||
</Alert>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
</Fade>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Inspection Alerts Panel — safety-critical, shown immediately */}
|
||||
<Grid item xs={12}>
|
||||
{/* Vehicle Status Card */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '380ms' }}>
|
||||
<Box>
|
||||
<InspectionAlerts daysAhead={30} hideWhenEmpty={true} />
|
||||
<VehicleDashboardCard />
|
||||
</Box>
|
||||
</Fade>
|
||||
</Grid>
|
||||
|
||||
{/* Equipment Alerts Panel */}
|
||||
<Grid item xs={12}>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '400ms' }}>
|
||||
{/* Equipment Status Card */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '450ms' }}>
|
||||
<Box>
|
||||
<EquipmentAlerts daysAhead={30} hideWhenEmpty={true} />
|
||||
<EquipmentDashboardCard />
|
||||
</Box>
|
||||
</Fade>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user