fix: dashboard layout, widget caching, and backend stability
Layout: - Remove Container maxWidth cap so widgets scale fluidly on wide screens - Fix ActivityFeed Card missing height:100% and overflow:hidden that caused the timeline connector pseudo-element to bleed outside the card boundary Performance (frontend): - Migrate VehicleDashboardCard, EquipmentDashboardCard, AtemschutzDashboardCard, UpcomingEventsWidget, and PersonalWarningsBanner from useEffect+useState to TanStack Query — cached for 5 min, so navigating back to the dashboard no longer re-fires all 9 API requests - Add gcTime:10min and refetchOnWindowFocus:false to QueryClient defaults to prevent spurious refetches on tab-switch Backend stability: - Raise default RATE_LIMIT_MAX from 100 to 300 req/15min — the previous limit was easily exceeded by a single active user during normal dashboard navigation - Increase DB connectionTimeoutMillis from 2s to 5s to handle burst-load scenarios where multiple requests compete for pool slots simultaneously Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ const poolConfig: PoolConfig = {
|
|||||||
password: environment.database.password,
|
password: environment.database.password,
|
||||||
max: 20, // Maximum number of clients in the pool
|
max: 20, // Maximum number of clients in the pool
|
||||||
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
|
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
|
||||||
connectionTimeoutMillis: 2000, // Return an error if connection takes longer than 2 seconds
|
connectionTimeoutMillis: 5000, // Return an error if connection takes longer than 5 seconds
|
||||||
};
|
};
|
||||||
|
|
||||||
const pool = new Pool(poolConfig);
|
const pool = new Pool(poolConfig);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const environment: EnvironmentConfig = {
|
|||||||
},
|
},
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10), // 15 minutes
|
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10), // 15 minutes
|
||||||
max: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
|
max: parseInt(process.env.RATE_LIMIT_MAX || '300', 10),
|
||||||
},
|
},
|
||||||
authentik: {
|
authentik: {
|
||||||
issuer: process.env.AUTHENTIK_ISSUER || 'https://auth.firesuite.feuerwehr-rems.at/application/o/feuerwehr-dashboard/',
|
issuer: process.env.AUTHENTIK_ISSUER || 'https://auth.firesuite.feuerwehr-rems.at/application/o/feuerwehr-dashboard/',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { atemschutzApi } from '../../services/atemschutz';
|
import { atemschutzApi } from '../../services/atemschutz';
|
||||||
import type { AtemschutzStats } from '../../types/atemschutz.types';
|
import type { AtemschutzStats } from '../../types/atemschutz.types';
|
||||||
|
|
||||||
@@ -20,31 +21,12 @@ interface AtemschutzDashboardCardProps {
|
|||||||
const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
|
const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
|
||||||
hideWhenEmpty = false,
|
hideWhenEmpty = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [stats, setStats] = useState<AtemschutzStats | null>(null);
|
const { data: stats, isLoading, isError } = useQuery<AtemschutzStats>({
|
||||||
const [loading, setLoading] = useState(true);
|
queryKey: ['atemschutz-stats'],
|
||||||
const [error, setError] = useState<string | null>(null);
|
queryFn: () => atemschutzApi.getStats(),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
if (isLoading) {
|
||||||
let mounted = true;
|
|
||||||
const fetchStats = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
const data = await atemschutzApi.getStats();
|
|
||||||
if (mounted) setStats(data);
|
|
||||||
} catch {
|
|
||||||
if (mounted) setError('Atemschutzstatus konnte nicht geladen werden.');
|
|
||||||
} finally {
|
|
||||||
if (mounted) setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchStats();
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 2 }}>
|
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 2 }}>
|
||||||
@@ -57,12 +39,12 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="body2" color="error">
|
<Typography variant="body2" color="error">
|
||||||
{error}
|
Atemschutzstatus konnte nicht geladen werden.
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -71,7 +53,6 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
|
|||||||
|
|
||||||
if (!stats) return null;
|
if (!stats) return null;
|
||||||
|
|
||||||
// Determine if there are any concerns
|
|
||||||
const hasConcerns =
|
const hasConcerns =
|
||||||
stats.untersuchungAbgelaufen > 0 ||
|
stats.untersuchungAbgelaufen > 0 ||
|
||||||
stats.leistungstestAbgelaufen > 0 ||
|
stats.leistungstestAbgelaufen > 0 ||
|
||||||
@@ -80,7 +61,6 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
|
|||||||
|
|
||||||
const allGood = stats.einsatzbereit === stats.total && !hasConcerns;
|
const allGood = stats.einsatzbereit === stats.total && !hasConcerns;
|
||||||
|
|
||||||
// If hideWhenEmpty and everything is fine, render nothing
|
|
||||||
if (hideWhenEmpty && allGood) return null;
|
if (hideWhenEmpty && allGood) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const ActivityFeed: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card sx={{ height: '100%', overflow: 'hidden' }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
Letzte Aktivitäten
|
Letzte Aktivitäten
|
||||||
@@ -110,7 +110,7 @@ const ActivityFeed: React.FC = () => {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 19,
|
left: 19,
|
||||||
top: 56,
|
top: 56,
|
||||||
bottom: -8,
|
bottom: 0,
|
||||||
width: 2,
|
width: 2,
|
||||||
bgcolor: 'divider',
|
bgcolor: 'divider',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { NotificationsActive as NotificationsActiveIcon } from '@mui/icons-material';
|
import { NotificationsActive as NotificationsActiveIcon } from '@mui/icons-material';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { atemschutzApi } from '../../services/atemschutz';
|
import { atemschutzApi } from '../../services/atemschutz';
|
||||||
import type { User } from '../../types/auth.types';
|
import type { User } from '../../types/auth.types';
|
||||||
import type { AtemschutzUebersicht } from '../../types/atemschutz.types';
|
import type { AtemschutzUebersicht } from '../../types/atemschutz.types';
|
||||||
@@ -74,33 +75,14 @@ interface PersonalWarningsBannerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PersonalWarningsBanner: React.FC<PersonalWarningsBannerProps> = ({ user: _user, onWarningCount }) => {
|
const PersonalWarningsBanner: React.FC<PersonalWarningsBannerProps> = ({ user: _user, onWarningCount }) => {
|
||||||
const [record, setRecord] = useState<AtemschutzUebersicht | null>(null);
|
const { data: record, isLoading } = useQuery<AtemschutzUebersicht | null>({
|
||||||
const [loading, setLoading] = useState(true);
|
queryKey: ['atemschutz-my-status'],
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
queryFn: () => atemschutzApi.getMyStatus(),
|
||||||
|
});
|
||||||
useEffect(() => {
|
|
||||||
let mounted = true;
|
|
||||||
|
|
||||||
const fetchStatus = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setFetchError(null);
|
|
||||||
const data = await atemschutzApi.getMyStatus();
|
|
||||||
if (mounted) setRecord(data);
|
|
||||||
} catch {
|
|
||||||
if (mounted) setFetchError('Persönlicher Atemschutz-Status konnte nicht geladen werden.');
|
|
||||||
} finally {
|
|
||||||
if (mounted) setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchStatus();
|
|
||||||
return () => { mounted = false; };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Loading state ──────────────────────────────────────────────────────────
|
// ── Loading state ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (loading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 1 }}>
|
||||||
<CircularProgress size={16} />
|
<CircularProgress size={16} />
|
||||||
@@ -111,13 +93,6 @@ const PersonalWarningsBanner: React.FC<PersonalWarningsBannerProps> = ({ user: _
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fetch error ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
if (fetchError) {
|
|
||||||
// Non-critical — silently swallow so the dashboard still loads cleanly.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── No atemschutz record for this user ─────────────────────────────────────
|
// ── No atemschutz record for this user ─────────────────────────────────────
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { CalendarMonth as CalendarMonthIcon } from '@mui/icons-material';
|
import { CalendarMonth as CalendarMonthIcon } from '@mui/icons-material';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { trainingApi } from '../../services/training';
|
import { trainingApi } from '../../services/training';
|
||||||
import { eventsApi } from '../../services/events';
|
import { eventsApi } from '../../services/events';
|
||||||
import type { UebungListItem, UebungTyp } from '../../types/training.types';
|
import type { UebungListItem, UebungTyp } from '../../types/training.types';
|
||||||
@@ -106,28 +107,23 @@ const FETCH_LIMIT = 20; // fetch more than 5 so filtering from today leaves enou
|
|||||||
const DISPLAY_LIMIT = 5;
|
const DISPLAY_LIMIT = 5;
|
||||||
|
|
||||||
const UpcomingEventsWidget: React.FC = () => {
|
const UpcomingEventsWidget: React.FC = () => {
|
||||||
const [entries, setEntries] = useState<CalendarEntry[]>([]);
|
const { data: trainingItems = [], isLoading: trainingLoading, isError: trainingError } = useQuery<UebungListItem[]>({
|
||||||
const [loading, setLoading] = useState(true);
|
queryKey: ['upcoming-trainings'],
|
||||||
const [error, setError] = useState<string | null>(null);
|
queryFn: () => trainingApi.getUpcoming(FETCH_LIMIT),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: eventItems = [], isLoading: eventsLoading, isError: eventsError } = useQuery<VeranstaltungListItem[]>({
|
||||||
let mounted = true;
|
queryKey: ['upcoming-events'],
|
||||||
|
queryFn: () => eventsApi.getUpcoming(FETCH_LIMIT),
|
||||||
|
});
|
||||||
|
|
||||||
const fetchData = async () => {
|
const loading = trainingLoading || eventsLoading;
|
||||||
try {
|
const error = trainingError || eventsError;
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const [trainingItems, eventItems] = await Promise.all([
|
|
||||||
trainingApi.getUpcoming(FETCH_LIMIT),
|
|
||||||
eventsApi.getUpcoming(FETCH_LIMIT),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
|
const entries = React.useMemo(() => {
|
||||||
|
if (loading) return [];
|
||||||
const today = startOfToday();
|
const today = startOfToday();
|
||||||
|
return [
|
||||||
const combined: CalendarEntry[] = [
|
|
||||||
...trainingItems
|
...trainingItems
|
||||||
.filter((t) => !t.abgesagt)
|
.filter((t) => !t.abgesagt)
|
||||||
.map(mapTraining),
|
.map(mapTraining),
|
||||||
@@ -138,20 +134,7 @@ const UpcomingEventsWidget: React.FC = () => {
|
|||||||
.filter((e) => e.date >= today)
|
.filter((e) => e.date >= today)
|
||||||
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||||
.slice(0, DISPLAY_LIMIT);
|
.slice(0, DISPLAY_LIMIT);
|
||||||
|
}, [trainingItems, eventItems, loading]);
|
||||||
setEntries(combined);
|
|
||||||
} catch {
|
|
||||||
if (mounted) setError('Termine konnten nicht geladen werden.');
|
|
||||||
} finally {
|
|
||||||
if (mounted) setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Loading state ─────────────────────────────────────────────────────────
|
// ── Loading state ─────────────────────────────────────────────────────────
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -177,7 +160,7 @@ const UpcomingEventsWidget: React.FC = () => {
|
|||||||
<Typography variant="h6">Nächste Termine</Typography>
|
<Typography variant="h6">Nächste Termine</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body2" color="error">
|
<Typography variant="body2" color="error">
|
||||||
{error}
|
Termine konnten nicht geladen werden.
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { equipmentApi } from '../../services/equipment';
|
import { equipmentApi } from '../../services/equipment';
|
||||||
import type { EquipmentStats } from '../../types/equipment.types';
|
import type { EquipmentStats } from '../../types/equipment.types';
|
||||||
|
|
||||||
@@ -20,31 +21,12 @@ interface EquipmentDashboardCardProps {
|
|||||||
const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({
|
const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({
|
||||||
hideWhenEmpty = false,
|
hideWhenEmpty = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [stats, setStats] = useState<EquipmentStats | null>(null);
|
const { data: stats, isLoading, isError } = useQuery<EquipmentStats>({
|
||||||
const [loading, setLoading] = useState(true);
|
queryKey: ['equipment-stats'],
|
||||||
const [error, setError] = useState<string | null>(null);
|
queryFn: () => equipmentApi.getStats(),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
if (isLoading) {
|
||||||
let mounted = true;
|
|
||||||
const fetchStats = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
const data = await equipmentApi.getStats();
|
|
||||||
if (mounted) setStats(data);
|
|
||||||
} catch {
|
|
||||||
if (mounted) setError('Ausrüstungsstatus konnte nicht geladen werden.');
|
|
||||||
} finally {
|
|
||||||
if (mounted) setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchStats();
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 2 }}>
|
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 2 }}>
|
||||||
@@ -57,12 +39,12 @@ const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="body2" color="error">
|
<Typography variant="body2" color="error">
|
||||||
{error}
|
Ausrüstungsstatus konnte nicht geladen werden.
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
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';
|
||||||
import type { VehicleStats, InspectionAlert } from '../../types/vehicle.types';
|
import type { VehicleStats, InspectionAlert } from '../../types/vehicle.types';
|
||||||
@@ -22,39 +23,22 @@ interface VehicleDashboardCardProps {
|
|||||||
const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
|
const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
|
||||||
hideWhenEmpty = false,
|
hideWhenEmpty = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [stats, setStats] = useState<VehicleStats | null>(null);
|
const { data: stats, isLoading: statsLoading, isError: statsError } = useQuery<VehicleStats>({
|
||||||
const [alerts, setAlerts] = useState<InspectionAlert[]>([]);
|
queryKey: ['vehicle-stats'],
|
||||||
const [equipmentWarnings, setEquipmentWarnings] = useState<VehicleEquipmentWarning[]>([]);
|
queryFn: () => vehiclesApi.getStats(),
|
||||||
const [loading, setLoading] = useState(true);
|
});
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: alerts = [], isLoading: alertsLoading } = useQuery<InspectionAlert[]>({
|
||||||
let mounted = true;
|
queryKey: ['vehicle-alerts'],
|
||||||
const fetchData = async () => {
|
queryFn: () => vehiclesApi.getAlerts(30),
|
||||||
try {
|
});
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
const { data: equipmentWarnings = [], isLoading: warningsLoading } = useQuery<VehicleEquipmentWarning[]>({
|
||||||
const [statsData, alertsData, warningsData] = await Promise.all([
|
queryKey: ['vehicle-equipment-warnings'],
|
||||||
vehiclesApi.getStats(),
|
queryFn: () => equipmentApi.getVehicleWarnings(),
|
||||||
vehiclesApi.getAlerts(30),
|
});
|
||||||
equipmentApi.getVehicleWarnings(),
|
|
||||||
]);
|
const loading = statsLoading || alertsLoading || warningsLoading;
|
||||||
if (mounted) {
|
|
||||||
setStats(statsData);
|
|
||||||
setAlerts(alertsData);
|
|
||||||
setEquipmentWarnings(warningsData);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (mounted) setError('Fahrzeugstatus konnte nicht geladen werden.');
|
|
||||||
} finally {
|
|
||||||
if (mounted) setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -69,12 +53,12 @@ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (statsError) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="body2" color="error">
|
<Typography variant="body2" color="error">
|
||||||
{error}
|
Fahrzeugstatus konnte nicht geladen werden.
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -94,7 +78,6 @@ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
|
|||||||
|
|
||||||
const allGood = stats.einsatzbereit === stats.total && !hasConcerns;
|
const allGood = stats.einsatzbereit === stats.total && !hasConcerns;
|
||||||
|
|
||||||
// If hideWhenEmpty and everything is fine, render nothing
|
|
||||||
if (hideWhenEmpty && allGood) return null;
|
if (hideWhenEmpty && allGood) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ const queryClient = new QueryClient({
|
|||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
gcTime: 10 * 60 * 1000, // keep cache 10 minutes
|
||||||
retry: 1,
|
retry: 1,
|
||||||
|
refetchOnWindowFocus: false, // prevent refetch on every tab switch
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<Container maxWidth="xl">
|
<Container maxWidth={false} disableGutters>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{/* Welcome Message */}
|
{/* Welcome Message */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
|||||||
Reference in New Issue
Block a user