widget icon rework, widget grouping rework
This commit is contained in:
@@ -3,16 +3,16 @@ import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import AirIcon from '@mui/icons-material/Air';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { atemschutzApi } from '../../services/atemschutz';
|
||||
import { useCountUp } from '../../hooks/useCountUp';
|
||||
import type { AtemschutzStats } from '../../types/atemschutz.types';
|
||||
import { WidgetCard } from '../templates/WidgetCard';
|
||||
import { StatSkeleton } from '../templates/SkeletonPresets';
|
||||
|
||||
interface AtemschutzDashboardCardProps {
|
||||
hideWhenEmpty?: boolean;
|
||||
@@ -30,51 +30,26 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
|
||||
const animatedReady = useCountUp(stats?.einsatzbereit ?? 0);
|
||||
const animatedTotal = useCountUp(stats?.total ?? 0);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 2 }}>
|
||||
<CircularProgress size={16} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Atemschutzstatus wird geladen...
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="error">
|
||||
Atemschutzstatus konnte nicht geladen werden.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!stats) return null;
|
||||
|
||||
const hasConcerns =
|
||||
stats.untersuchungAbgelaufen > 0 ||
|
||||
const hasConcerns = stats
|
||||
? stats.untersuchungAbgelaufen > 0 ||
|
||||
stats.leistungstestAbgelaufen > 0 ||
|
||||
stats.untersuchungBaldFaellig > 0 ||
|
||||
stats.leistungstestBaldFaellig > 0;
|
||||
stats.leistungstestBaldFaellig > 0
|
||||
: false;
|
||||
const allGood = stats ? stats.einsatzbereit === stats.total && !hasConcerns : false;
|
||||
|
||||
const allGood = stats.einsatzbereit === stats.total && !hasConcerns;
|
||||
|
||||
if (hideWhenEmpty && allGood) return null;
|
||||
if (!isLoading && !isError && stats && hideWhenEmpty && allGood) return null;
|
||||
|
||||
return (
|
||||
<Card sx={{ height: '100%', cursor: 'pointer' }} onClick={() => navigate('/atemschutz')}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Atemschutz
|
||||
</Typography>
|
||||
|
||||
{/* Main metric */}
|
||||
<WidgetCard
|
||||
title="Atemschutz"
|
||||
icon={<AirIcon />}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
errorMessage="Atemschutzstatus konnte nicht geladen werden."
|
||||
skeleton={<StatSkeleton />}
|
||||
onClick={() => navigate('/atemschutz')}
|
||||
>
|
||||
<Typography variant="h4" fontWeight={700} color={allGood ? 'success.main' : 'text.primary'}>
|
||||
{animatedReady}/{animatedTotal}
|
||||
</Typography>
|
||||
@@ -82,8 +57,7 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
|
||||
einsatzbereit
|
||||
</Typography>
|
||||
|
||||
{/* Concerns list — using Alert components for consistent warning styling */}
|
||||
{hasConcerns && (
|
||||
{hasConcerns && stats && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
||||
{(stats.untersuchungAbgelaufen > 0 || stats.leistungstestAbgelaufen > 0) && (
|
||||
<Alert severity="error" variant="outlined" sx={{ py: 0.5 }}>
|
||||
@@ -118,14 +92,12 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* All good message */}
|
||||
{allGood && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
Alle Atemschutzträger einsatzbereit
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ function AdminStatusWidget() {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Service Status"
|
||||
icon={<MonitorHeartOutlined color={color} />}
|
||||
icon={<MonitorHeartOutlined />}
|
||||
isLoading={!data}
|
||||
skeleton={<StatSkeleton />}
|
||||
onClick={() => navigate('/admin')}
|
||||
|
||||
@@ -22,7 +22,7 @@ function AusruestungsanfrageWidget() {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Interne Bestellungen"
|
||||
icon={<Build fontSize="small" color="action" />}
|
||||
icon={<Build fontSize="small" />}
|
||||
isLoading={isLoading}
|
||||
skeleton={<ChipListSkeleton />}
|
||||
isError={isError || (!isLoading && !overview)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function BannerWidget() {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Mitteilungen"
|
||||
icon={<Campaign color="primary" />}
|
||||
icon={<Campaign />}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
{widgetBanners.map(banner => (
|
||||
|
||||
@@ -31,7 +31,7 @@ function BestellungenWidget() {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Bestellungen"
|
||||
icon={<LocalShipping fontSize="small" color="action" />}
|
||||
icon={<LocalShipping fontSize="small" />}
|
||||
isLoading={isLoading}
|
||||
skeleton={<ChipListSkeleton />}
|
||||
isError={isError}
|
||||
|
||||
@@ -76,7 +76,7 @@ const BookStackRecentWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Wissen — Neueste Seiten"
|
||||
icon={<MenuBook color="disabled" />}
|
||||
icon={<MenuBook />}
|
||||
isEmpty
|
||||
emptyMessage="BookStack nicht eingerichtet"
|
||||
/>
|
||||
@@ -87,7 +87,7 @@ const BookStackRecentWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Wissen — Neueste Seiten"
|
||||
icon={<MenuBook color="primary" />}
|
||||
icon={<MenuBook />}
|
||||
isError
|
||||
errorMessage="BookStack nicht erreichbar"
|
||||
/>
|
||||
@@ -97,7 +97,7 @@ const BookStackRecentWidget: React.FC = () => {
|
||||
return (
|
||||
<ListCard
|
||||
title="Wissen — Neueste Seiten"
|
||||
icon={<MenuBook color="primary" />}
|
||||
icon={<MenuBook />}
|
||||
items={pages}
|
||||
renderItem={(page) => <PageRow key={page.id} page={page} />}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -122,7 +122,7 @@ const BookStackSearchWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Wissen — Suche"
|
||||
icon={<MenuBook color="primary" />}
|
||||
icon={<MenuBook />}
|
||||
isLoading
|
||||
skeleton={<Skeleton variant="rectangular" height={40} sx={{ borderRadius: 1 }} animation="wave" />}
|
||||
/>
|
||||
@@ -133,7 +133,7 @@ const BookStackSearchWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Wissen — Suche"
|
||||
icon={<MenuBook color="disabled" />}
|
||||
icon={<MenuBook />}
|
||||
isEmpty
|
||||
emptyMessage="BookStack nicht eingerichtet"
|
||||
/>
|
||||
@@ -143,7 +143,7 @@ const BookStackSearchWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Wissen — Suche"
|
||||
icon={<MenuBook color="primary" />}
|
||||
icon={<MenuBook />}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
|
||||
@@ -37,7 +37,7 @@ function BuchhaltungWidget() {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Buchhaltung"
|
||||
icon={<AccountBalance fontSize="small" color="action" />}
|
||||
icon={<AccountBalance fontSize="small" />}
|
||||
isLoading={isLoading}
|
||||
skeleton={<ChipListSkeleton />}
|
||||
isError={isError || (!isLoading && !activeJahr)}
|
||||
|
||||
@@ -25,7 +25,7 @@ function ChecklistWidget() {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Checklisten"
|
||||
icon={<AssignmentTurnedIn fontSize="small" color="action" />}
|
||||
icon={<AssignmentTurnedIn fontSize="small" />}
|
||||
action={titleAction}
|
||||
isLoading={isLoading}
|
||||
skeleton={<ChipListSkeleton />}
|
||||
|
||||
@@ -111,7 +111,7 @@ const EventQuickAddWidget: React.FC = () => {
|
||||
return (
|
||||
<FormCard
|
||||
title="Veranstaltung"
|
||||
icon={<CalendarMonth color="primary" />}
|
||||
icon={<CalendarMonth />}
|
||||
isSubmitting={mutation.isPending}
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel="Erstellen"
|
||||
|
||||
@@ -21,7 +21,7 @@ function IssueOverviewWidget() {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Issues"
|
||||
icon={<BugReport fontSize="small" color="action" />}
|
||||
icon={<BugReport fontSize="small" />}
|
||||
isLoading={isLoading}
|
||||
skeleton={<ChipListSkeleton />}
|
||||
isError={isError}
|
||||
|
||||
@@ -66,7 +66,7 @@ const IssueQuickAddWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Issue melden"
|
||||
icon={<BugReport color="primary" />}
|
||||
icon={<BugReport />}
|
||||
isLoading
|
||||
skeleton={<FormSkeleton />}
|
||||
/>
|
||||
@@ -76,7 +76,7 @@ const IssueQuickAddWidget: React.FC = () => {
|
||||
return (
|
||||
<FormCard
|
||||
title="Issue melden"
|
||||
icon={<BugReport color="primary" />}
|
||||
icon={<BugReport />}
|
||||
isSubmitting={mutation.isPending}
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel="Melden"
|
||||
|
||||
@@ -13,7 +13,7 @@ function LinksWidget({ collection }: LinksWidgetProps) {
|
||||
return (
|
||||
<ListCard
|
||||
title={collection.name}
|
||||
icon={<LinkIcon color="primary" />}
|
||||
icon={<LinkIcon />}
|
||||
items={collection.links}
|
||||
renderItem={(link) => (
|
||||
<Link
|
||||
|
||||
@@ -138,7 +138,7 @@ const UpcomingEventsWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Nächste Termine"
|
||||
icon={<CalendarMonthIcon fontSize="small" sx={{ color: 'primary.main' }} />}
|
||||
icon={<CalendarMonthIcon fontSize="small" />}
|
||||
isError
|
||||
errorMessage="Termine konnten nicht geladen werden."
|
||||
/>
|
||||
@@ -148,7 +148,7 @@ const UpcomingEventsWidget: React.FC = () => {
|
||||
return (
|
||||
<ListCard
|
||||
title="Nächste Termine"
|
||||
icon={<CalendarMonthIcon fontSize="small" sx={{ color: 'primary.main' }} />}
|
||||
icon={<CalendarMonthIcon fontSize="small" />}
|
||||
items={entries}
|
||||
renderItem={(entry) => (
|
||||
<Box
|
||||
|
||||
@@ -56,7 +56,7 @@ const VehicleBookingListWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Nächste Fahrzeugbuchungen"
|
||||
icon={<DirectionsCarIcon fontSize="small" sx={{ color: 'primary.main' }} />}
|
||||
icon={<DirectionsCarIcon fontSize="small" />}
|
||||
isError
|
||||
errorMessage="Buchungen konnten nicht geladen werden."
|
||||
/>
|
||||
@@ -66,7 +66,7 @@ const VehicleBookingListWidget: React.FC = () => {
|
||||
return (
|
||||
<ListCard
|
||||
title="Nächste Fahrzeugbuchungen"
|
||||
icon={<DirectionsCarIcon fontSize="small" sx={{ color: 'primary.main' }} />}
|
||||
icon={<DirectionsCarIcon fontSize="small" />}
|
||||
items={items}
|
||||
renderItem={(booking) => {
|
||||
const color = BUCHUNGS_ART_COLORS[booking.buchungs_art] ?? '#9e9e9e';
|
||||
|
||||
@@ -95,7 +95,7 @@ const VehicleBookingQuickAddWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Fahrzeugbuchung"
|
||||
icon={<DirectionsCar color="primary" />}
|
||||
icon={<DirectionsCar />}
|
||||
isLoading
|
||||
skeleton={<FormSkeleton />}
|
||||
/>
|
||||
@@ -105,7 +105,7 @@ const VehicleBookingQuickAddWidget: React.FC = () => {
|
||||
return (
|
||||
<FormCard
|
||||
title="Fahrzeugbuchung"
|
||||
icon={<DirectionsCar color="primary" />}
|
||||
icon={<DirectionsCar />}
|
||||
isSubmitting={mutation.isPending}
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel="Erstellen"
|
||||
|
||||
@@ -96,7 +96,7 @@ const VikunjaMyTasksWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Meine Aufgaben"
|
||||
icon={<AssignmentInd color="disabled" />}
|
||||
icon={<AssignmentInd />}
|
||||
isEmpty
|
||||
emptyMessage="Vikunja nicht eingerichtet"
|
||||
/>
|
||||
@@ -107,7 +107,7 @@ const VikunjaMyTasksWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Meine Aufgaben"
|
||||
icon={<AssignmentInd color="primary" />}
|
||||
icon={<AssignmentInd />}
|
||||
isError
|
||||
errorMessage="Vikunja nicht erreichbar"
|
||||
/>
|
||||
@@ -115,13 +115,13 @@ const VikunjaMyTasksWidget: React.FC = () => {
|
||||
}
|
||||
|
||||
const titleAction = !isLoading && !isError && tasks.length > 0 ? (
|
||||
<Chip label={animatedTaskCount} size="small" color="primary" />
|
||||
<Chip label={animatedTaskCount} size="small" />
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<ListCard
|
||||
title="Meine Aufgaben"
|
||||
icon={<AssignmentInd color="primary" />}
|
||||
icon={<AssignmentInd />}
|
||||
action={titleAction}
|
||||
items={tasks}
|
||||
renderItem={(task) => (
|
||||
|
||||
@@ -62,7 +62,7 @@ const VikunjaQuickAddWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Aufgabe erstellen"
|
||||
icon={<AddTask color="disabled" />}
|
||||
icon={<AddTask />}
|
||||
isEmpty
|
||||
emptyMessage="Vikunja nicht eingerichtet"
|
||||
/>
|
||||
@@ -73,7 +73,7 @@ const VikunjaQuickAddWidget: React.FC = () => {
|
||||
return (
|
||||
<WidgetCard
|
||||
title="Aufgabe erstellen"
|
||||
icon={<AddTask color="primary" />}
|
||||
icon={<AddTask />}
|
||||
isLoading
|
||||
skeleton={<FormSkeleton />}
|
||||
/>
|
||||
@@ -83,7 +83,7 @@ const VikunjaQuickAddWidget: React.FC = () => {
|
||||
return (
|
||||
<FormCard
|
||||
title="Aufgabe erstellen"
|
||||
icon={<AddTask color="primary" />}
|
||||
icon={<AddTask />}
|
||||
isSubmitting={mutation.isPending}
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel="Erstellen"
|
||||
|
||||
@@ -3,16 +3,16 @@ import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import HandymanIcon from '@mui/icons-material/Handyman';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { equipmentApi } from '../../services/equipment';
|
||||
import { useCountUp } from '../../hooks/useCountUp';
|
||||
import type { EquipmentStats } from '../../types/equipment.types';
|
||||
import { WidgetCard } from '../templates/WidgetCard';
|
||||
import { StatSkeleton } from '../templates/SkeletonPresets';
|
||||
|
||||
interface EquipmentDashboardCardProps {
|
||||
hideWhenEmpty?: boolean;
|
||||
@@ -30,51 +30,26 @@ const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({
|
||||
const animatedReady = useCountUp(stats?.einsatzbereit ?? 0);
|
||||
const animatedTotal = useCountUp(stats?.total ?? 0);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 2 }}>
|
||||
<CircularProgress size={16} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Ausrüstungsstatus wird geladen...
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
const hasCritical = stats
|
||||
? stats.wichtigNichtBereit > 0 || stats.inspectionsOverdue > 0 || stats.beschaedigt > 0
|
||||
: false;
|
||||
const hasWarning = stats
|
||||
? stats.inspectionsDue > 0 || stats.inWartung > 0 || stats.ausserDienst > 0
|
||||
: false;
|
||||
const allGood = stats ? stats.einsatzbereit === stats.total && !hasCritical && !hasWarning : false;
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="error">
|
||||
Ausrüstungsstatus konnte nicht geladen werden.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!stats) return null;
|
||||
|
||||
const hasCritical =
|
||||
stats.wichtigNichtBereit > 0 || stats.inspectionsOverdue > 0 || stats.beschaedigt > 0;
|
||||
|
||||
const hasWarning =
|
||||
stats.inspectionsDue > 0 || stats.inWartung > 0 || stats.ausserDienst > 0;
|
||||
|
||||
const allGood = stats.einsatzbereit === stats.total && !hasCritical && !hasWarning;
|
||||
|
||||
if (hideWhenEmpty && allGood) return null;
|
||||
if (!isLoading && !isError && stats && hideWhenEmpty && allGood) return null;
|
||||
|
||||
return (
|
||||
<Card sx={{ height: '100%', cursor: 'pointer' }} onClick={() => navigate('/ausruestung')}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Ausrüstung
|
||||
</Typography>
|
||||
|
||||
{/* Main metric */}
|
||||
<WidgetCard
|
||||
title="Ausrüstung"
|
||||
icon={<HandymanIcon />}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
errorMessage="Ausrüstungsstatus konnte nicht geladen werden."
|
||||
skeleton={<StatSkeleton />}
|
||||
onClick={() => navigate('/ausruestung')}
|
||||
>
|
||||
<Typography variant="h4" fontWeight={700} color={allGood ? 'success.main' : 'text.primary'}>
|
||||
{animatedReady}/{animatedTotal}
|
||||
</Typography>
|
||||
@@ -82,60 +57,45 @@ const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({
|
||||
einsatzbereit
|
||||
</Typography>
|
||||
|
||||
{/* Alerts — consolidated counts only */}
|
||||
{(hasCritical || hasWarning) && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
||||
{hasCritical && (
|
||||
{hasCritical && stats && (
|
||||
<Alert severity="error" variant="outlined" sx={{ py: 0.5 }}>
|
||||
<AlertTitle sx={{ fontWeight: 600, mb: 0.5 }}>Kritisch</AlertTitle>
|
||||
{stats.wichtigNichtBereit > 0 && (
|
||||
<Typography variant="body2">
|
||||
{stats.wichtigNichtBereit} wichtige Teile nicht einsatzbereit
|
||||
</Typography>
|
||||
<Typography variant="body2">{stats.wichtigNichtBereit} wichtige Teile nicht einsatzbereit</Typography>
|
||||
)}
|
||||
{stats.inspectionsOverdue > 0 && (
|
||||
<Typography variant="body2">
|
||||
{stats.inspectionsOverdue} Prüfung{stats.inspectionsOverdue !== 1 ? 'en' : ''} überfällig
|
||||
</Typography>
|
||||
<Typography variant="body2">{stats.inspectionsOverdue} Prüfung{stats.inspectionsOverdue !== 1 ? 'en' : ''} überfällig</Typography>
|
||||
)}
|
||||
{stats.beschaedigt > 0 && (
|
||||
<Typography variant="body2">
|
||||
{stats.beschaedigt} beschädigt
|
||||
</Typography>
|
||||
<Typography variant="body2">{stats.beschaedigt} beschädigt</Typography>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
{hasWarning && (
|
||||
{hasWarning && stats && (
|
||||
<Alert severity="warning" variant="outlined" sx={{ py: 0.5 }}>
|
||||
<AlertTitle sx={{ fontWeight: 600, mb: 0.5 }}>Achtung</AlertTitle>
|
||||
{stats.inspectionsDue > 0 && (
|
||||
<Typography variant="body2">
|
||||
{stats.inspectionsDue} Prüfung{stats.inspectionsDue !== 1 ? 'en' : ''} bald fällig
|
||||
</Typography>
|
||||
<Typography variant="body2">{stats.inspectionsDue} Prüfung{stats.inspectionsDue !== 1 ? 'en' : ''} bald fällig</Typography>
|
||||
)}
|
||||
{stats.inWartung > 0 && (
|
||||
<Typography variant="body2">
|
||||
{stats.inWartung} in Wartung
|
||||
</Typography>
|
||||
<Typography variant="body2">{stats.inWartung} in Wartung</Typography>
|
||||
)}
|
||||
{stats.ausserDienst > 0 && (
|
||||
<Typography variant="body2">
|
||||
{stats.ausserDienst} außer Dienst
|
||||
</Typography>
|
||||
<Typography variant="body2">{stats.ausserDienst} außer Dienst</Typography>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* All good message */}
|
||||
{allGood && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
Alle Ausrüstung einsatzbereit
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@ import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import FireTruckIcon from '@mui/icons-material/FireTruck';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { vehiclesApi } from '../../services/vehicles';
|
||||
@@ -15,6 +13,8 @@ import { equipmentApi } from '../../services/equipment';
|
||||
import { useCountUp } from '../../hooks/useCountUp';
|
||||
import type { VehicleStats, InspectionAlert } from '../../types/vehicle.types';
|
||||
import type { VehicleEquipmentWarning } from '../../types/equipment.types';
|
||||
import { WidgetCard } from '../templates/WidgetCard';
|
||||
import { StatSkeleton } from '../templates/SkeletonPresets';
|
||||
|
||||
interface VehicleDashboardCardProps {
|
||||
hideWhenEmpty?: boolean;
|
||||
@@ -44,54 +44,35 @@ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
|
||||
|
||||
const loading = statsLoading || alertsLoading || warningsLoading;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 2 }}>
|
||||
<CircularProgress size={16} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Fahrzeugstatus wird geladen...
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
if (!loading && !statsError && stats) {
|
||||
const overdueAlerts = alerts.filter((a) => a.tage < 0);
|
||||
const hasConcerns =
|
||||
overdueAlerts.length > 0 ||
|
||||
stats.ausserDienst > 0 ||
|
||||
equipmentWarnings.length > 0;
|
||||
const allGood = stats.einsatzbereit === stats.total && !hasConcerns;
|
||||
if (hideWhenEmpty && allGood) return null;
|
||||
}
|
||||
|
||||
if (statsError) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="error">
|
||||
Fahrzeugstatus konnte nicht geladen werden.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!stats) return null;
|
||||
|
||||
const overdueAlerts = alerts.filter((a) => a.tage < 0);
|
||||
const upcomingAlerts = alerts.filter((a) => a.tage >= 0 && a.tage <= 30);
|
||||
|
||||
const hasConcerns =
|
||||
overdueAlerts.length > 0 ||
|
||||
upcomingAlerts.length > 0 ||
|
||||
stats.ausserDienst > 0 ||
|
||||
(stats?.ausserDienst ?? 0) > 0 ||
|
||||
equipmentWarnings.length > 0;
|
||||
|
||||
const allGood = stats.einsatzbereit === stats.total && !hasConcerns;
|
||||
|
||||
if (hideWhenEmpty && allGood) return null;
|
||||
const allGood = stats ? stats.einsatzbereit === stats.total && !hasConcerns : false;
|
||||
|
||||
return (
|
||||
<Card sx={{ height: '100%', cursor: 'pointer' }} onClick={() => navigate('/fahrzeuge')}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Fahrzeuge
|
||||
</Typography>
|
||||
|
||||
{/* Main metric */}
|
||||
<WidgetCard
|
||||
title="Fahrzeuge"
|
||||
icon={<FireTruckIcon />}
|
||||
isLoading={loading}
|
||||
isError={statsError}
|
||||
errorMessage="Fahrzeugstatus konnte nicht geladen werden."
|
||||
skeleton={<StatSkeleton />}
|
||||
onClick={() => navigate('/fahrzeuge')}
|
||||
>
|
||||
<Typography variant="h4" fontWeight={700} color={allGood ? 'success.main' : 'text.primary'}>
|
||||
{animatedReady}/{animatedTotal}
|
||||
</Typography>
|
||||
@@ -99,7 +80,6 @@ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
|
||||
einsatzbereit
|
||||
</Typography>
|
||||
|
||||
{/* Concerns list — using Alert components for consistent warning styling */}
|
||||
{hasConcerns && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
||||
{overdueAlerts.length > 0 && (
|
||||
@@ -110,11 +90,11 @@ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
{stats.ausserDienst > 0 && (
|
||||
{(stats?.ausserDienst ?? 0) > 0 && (
|
||||
<Alert severity="error" variant="outlined" sx={{ py: 0.5 }}>
|
||||
<AlertTitle sx={{ fontWeight: 600, mb: 0.5 }}>Außer Dienst</AlertTitle>
|
||||
<Typography variant="body2">
|
||||
{stats.ausserDienst} Fahrzeug{stats.ausserDienst !== 1 ? 'e' : ''} außer Dienst
|
||||
{stats!.ausserDienst} Fahrzeug{stats!.ausserDienst !== 1 ? 'e' : ''} außer Dienst
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -137,14 +117,12 @@ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* All good message */}
|
||||
{allGood && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
Alle Fahrzeuge einsatzbereit
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ import type {
|
||||
Kategorie,
|
||||
Transaktion, TransaktionFormData, TransaktionFilters,
|
||||
TransaktionTyp,
|
||||
TransaktionStatus,
|
||||
|
||||
AusgabenTyp,
|
||||
WiederkehrendBuchung, WiederkehrendFormData,
|
||||
WiederkehrendIntervall,
|
||||
@@ -83,14 +83,11 @@ import type {
|
||||
TransferFormData,
|
||||
} from '../types/buchhaltung.types';
|
||||
import {
|
||||
TRANSAKTION_STATUS_LABELS,
|
||||
TRANSAKTION_STATUS_COLORS,
|
||||
TRANSAKTION_TYP_LABELS,
|
||||
INTERVALL_LABELS,
|
||||
KONTO_ART_LABELS,
|
||||
} from '../types/buchhaltung.types';
|
||||
|
||||
import { StatusChip } from '../components/templates';
|
||||
|
||||
// ─── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -181,12 +181,21 @@ function Dashboard() {
|
||||
for (const k of (groupKeys as string[])) allSavedKeys.add(k);
|
||||
}
|
||||
|
||||
// Widgets in custom groups take priority — don't let built-in groups claim them
|
||||
const customGroupKeys = new Set<string>();
|
||||
if (preferences.customGroups) {
|
||||
for (const cg of preferences.customGroups as { name: string; title: string }[]) {
|
||||
const cgOrder = preferences.widgetOrder[cg.name] as string[] | undefined;
|
||||
if (cgOrder) { for (const k of cgOrder) customGroupKeys.add(k); }
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of Object.keys(DEFAULT_ORDER)) {
|
||||
if (preferences.widgetOrder[group]) {
|
||||
// Merge: saved order first, then any new widgets not in saved order
|
||||
// Merge: saved order first (excluding custom-group widgets), then new widgets
|
||||
const saved = preferences.widgetOrder[group] as string[];
|
||||
const allKeys = DEFAULT_ORDER[group];
|
||||
const ordered = saved.filter((k: string) => allKeys.includes(k));
|
||||
const ordered = saved.filter((k: string) => allKeys.includes(k) && !customGroupKeys.has(k));
|
||||
const remaining = allKeys.filter((k) => !allSavedKeys.has(k));
|
||||
merged[group] = [...ordered, ...remaining];
|
||||
}
|
||||
@@ -377,8 +386,10 @@ function Dashboard() {
|
||||
if (keys.includes(widgetKey)) { targetGroup = group; break; }
|
||||
}
|
||||
if (!updatedOrder[targetGroup]) updatedOrder[targetGroup] = [];
|
||||
if (!updatedOrder[targetGroup].includes(widgetKey)) {
|
||||
updatedOrder[targetGroup].push(widgetKey);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedCustomGroups = localCustomGroups.filter((g) => g.name !== groupName);
|
||||
const updatedGroupOrder = localGroupOrder.filter(n => n !== groupName);
|
||||
|
||||
Reference in New Issue
Block a user