From 588d8e81db95ac26a79ec6744993951cf62bd928 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Tue, 14 Apr 2026 10:53:03 +0200 Subject: [PATCH] widget icon rework, widget grouping rework --- .../atemschutz/AtemschutzDashboardCard.tsx | 160 +++++++---------- .../dashboard/AdminStatusWidget.tsx | 2 +- .../dashboard/AusruestungsanfrageWidget.tsx | 2 +- .../src/components/dashboard/BannerWidget.tsx | 2 +- .../dashboard/BestellungenWidget.tsx | 2 +- .../dashboard/BookStackRecentWidget.tsx | 6 +- .../dashboard/BookStackSearchWidget.tsx | 6 +- .../dashboard/BuchhaltungWidget.tsx | 2 +- .../components/dashboard/ChecklistWidget.tsx | 2 +- .../dashboard/EventQuickAddWidget.tsx | 2 +- .../dashboard/IssueOverviewWidget.tsx | 2 +- .../dashboard/IssueQuickAddWidget.tsx | 4 +- .../src/components/dashboard/LinksWidget.tsx | 2 +- .../dashboard/UpcomingEventsWidget.tsx | 4 +- .../dashboard/VehicleBookingListWidget.tsx | 4 +- .../VehicleBookingQuickAddWidget.tsx | 4 +- .../dashboard/VikunjaMyTasksWidget.tsx | 8 +- .../dashboard/VikunjaQuickAddWidget.tsx | 6 +- .../equipment/EquipmentDashboardCard.tsx | 168 +++++++----------- .../vehicles/VehicleDashboardCard.tsx | 162 ++++++++--------- frontend/src/pages/Buchhaltung.tsx | 5 +- frontend/src/pages/Dashboard.tsx | 17 +- 22 files changed, 245 insertions(+), 327 deletions(-) diff --git a/frontend/src/components/atemschutz/AtemschutzDashboardCard.tsx b/frontend/src/components/atemschutz/AtemschutzDashboardCard.tsx index 7c32163..3db4322 100644 --- a/frontend/src/components/atemschutz/AtemschutzDashboardCard.tsx +++ b/frontend/src/components/atemschutz/AtemschutzDashboardCard.tsx @@ -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,102 +30,74 @@ const AtemschutzDashboardCard: React.FC = ({ const animatedReady = useCountUp(stats?.einsatzbereit ?? 0); const animatedTotal = useCountUp(stats?.total ?? 0); - if (isLoading) { - return ( - - - - - Atemschutzstatus wird geladen... - - - - ); - } + const hasConcerns = stats + ? stats.untersuchungAbgelaufen > 0 || + stats.leistungstestAbgelaufen > 0 || + stats.untersuchungBaldFaellig > 0 || + stats.leistungstestBaldFaellig > 0 + : false; + const allGood = stats ? stats.einsatzbereit === stats.total && !hasConcerns : false; - if (isError) { - return ( - - - - Atemschutzstatus konnte nicht geladen werden. - - - - ); - } - - if (!stats) return null; - - const hasConcerns = - stats.untersuchungAbgelaufen > 0 || - stats.leistungstestAbgelaufen > 0 || - stats.untersuchungBaldFaellig > 0 || - stats.leistungstestBaldFaellig > 0; - - const allGood = stats.einsatzbereit === stats.total && !hasConcerns; - - if (hideWhenEmpty && allGood) return null; + if (!isLoading && !isError && stats && hideWhenEmpty && allGood) return null; return ( - navigate('/atemschutz')}> - - - Atemschutz - + } + isLoading={isLoading} + isError={isError} + errorMessage="Atemschutzstatus konnte nicht geladen werden." + skeleton={} + onClick={() => navigate('/atemschutz')} + > + + {animatedReady}/{animatedTotal} + + + einsatzbereit + - {/* Main metric */} - - {animatedReady}/{animatedTotal} - - - einsatzbereit - + {hasConcerns && stats && ( + + {(stats.untersuchungAbgelaufen > 0 || stats.leistungstestAbgelaufen > 0) && ( + + Abgelaufen + {stats.untersuchungAbgelaufen > 0 && ( + + {stats.untersuchungAbgelaufen} Untersuchung{stats.untersuchungAbgelaufen !== 1 ? 'en' : ''} abgelaufen + + )} + {stats.leistungstestAbgelaufen > 0 && ( + + {stats.leistungstestAbgelaufen} Leistungstest{stats.leistungstestAbgelaufen !== 1 ? 's' : ''} abgelaufen + + )} + + )} + {(stats.untersuchungBaldFaellig > 0 || stats.leistungstestBaldFaellig > 0) && ( + + Bald fällig + {stats.untersuchungBaldFaellig > 0 && ( + + {stats.untersuchungBaldFaellig} Untersuchung{stats.untersuchungBaldFaellig !== 1 ? 'en' : ''} bald fällig + + )} + {stats.leistungstestBaldFaellig > 0 && ( + + {stats.leistungstestBaldFaellig} Leistungstest{stats.leistungstestBaldFaellig !== 1 ? 's' : ''} bald fällig + + )} + + )} + + )} - {/* Concerns list — using Alert components for consistent warning styling */} - {hasConcerns && ( - - {(stats.untersuchungAbgelaufen > 0 || stats.leistungstestAbgelaufen > 0) && ( - - Abgelaufen - {stats.untersuchungAbgelaufen > 0 && ( - - {stats.untersuchungAbgelaufen} Untersuchung{stats.untersuchungAbgelaufen !== 1 ? 'en' : ''} abgelaufen - - )} - {stats.leistungstestAbgelaufen > 0 && ( - - {stats.leistungstestAbgelaufen} Leistungstest{stats.leistungstestAbgelaufen !== 1 ? 's' : ''} abgelaufen - - )} - - )} - {(stats.untersuchungBaldFaellig > 0 || stats.leistungstestBaldFaellig > 0) && ( - - Bald fällig - {stats.untersuchungBaldFaellig > 0 && ( - - {stats.untersuchungBaldFaellig} Untersuchung{stats.untersuchungBaldFaellig !== 1 ? 'en' : ''} bald fällig - - )} - {stats.leistungstestBaldFaellig > 0 && ( - - {stats.leistungstestBaldFaellig} Leistungstest{stats.leistungstestBaldFaellig !== 1 ? 's' : ''} bald fällig - - )} - - )} - - )} - - {/* All good message */} - {allGood && ( - - Alle Atemschutzträger einsatzbereit - - )} - - + {allGood && ( + + Alle Atemschutzträger einsatzbereit + + )} + ); }; diff --git a/frontend/src/components/dashboard/AdminStatusWidget.tsx b/frontend/src/components/dashboard/AdminStatusWidget.tsx index fad5345..76d53a0 100644 --- a/frontend/src/components/dashboard/AdminStatusWidget.tsx +++ b/frontend/src/components/dashboard/AdminStatusWidget.tsx @@ -33,7 +33,7 @@ function AdminStatusWidget() { return ( } + icon={} isLoading={!data} skeleton={} onClick={() => navigate('/admin')} diff --git a/frontend/src/components/dashboard/AusruestungsanfrageWidget.tsx b/frontend/src/components/dashboard/AusruestungsanfrageWidget.tsx index 7a086c0..707148c 100644 --- a/frontend/src/components/dashboard/AusruestungsanfrageWidget.tsx +++ b/frontend/src/components/dashboard/AusruestungsanfrageWidget.tsx @@ -22,7 +22,7 @@ function AusruestungsanfrageWidget() { return ( } + icon={} isLoading={isLoading} skeleton={} isError={isError || (!isLoading && !overview)} diff --git a/frontend/src/components/dashboard/BannerWidget.tsx b/frontend/src/components/dashboard/BannerWidget.tsx index 4b361ff..d0f608e 100644 --- a/frontend/src/components/dashboard/BannerWidget.tsx +++ b/frontend/src/components/dashboard/BannerWidget.tsx @@ -26,7 +26,7 @@ export default function BannerWidget() { return ( } + icon={} > {widgetBanners.map(banner => ( diff --git a/frontend/src/components/dashboard/BestellungenWidget.tsx b/frontend/src/components/dashboard/BestellungenWidget.tsx index 436d5e6..725f321 100644 --- a/frontend/src/components/dashboard/BestellungenWidget.tsx +++ b/frontend/src/components/dashboard/BestellungenWidget.tsx @@ -31,7 +31,7 @@ function BestellungenWidget() { return ( } + icon={} isLoading={isLoading} skeleton={} isError={isError} diff --git a/frontend/src/components/dashboard/BookStackRecentWidget.tsx b/frontend/src/components/dashboard/BookStackRecentWidget.tsx index 7375e9e..f02330f 100644 --- a/frontend/src/components/dashboard/BookStackRecentWidget.tsx +++ b/frontend/src/components/dashboard/BookStackRecentWidget.tsx @@ -76,7 +76,7 @@ const BookStackRecentWidget: React.FC = () => { return ( } + icon={} isEmpty emptyMessage="BookStack nicht eingerichtet" /> @@ -87,7 +87,7 @@ const BookStackRecentWidget: React.FC = () => { return ( } + icon={} isError errorMessage="BookStack nicht erreichbar" /> @@ -97,7 +97,7 @@ const BookStackRecentWidget: React.FC = () => { return ( } + icon={} items={pages} renderItem={(page) => } isLoading={isLoading} diff --git a/frontend/src/components/dashboard/BookStackSearchWidget.tsx b/frontend/src/components/dashboard/BookStackSearchWidget.tsx index fa0326e..e1d2631 100644 --- a/frontend/src/components/dashboard/BookStackSearchWidget.tsx +++ b/frontend/src/components/dashboard/BookStackSearchWidget.tsx @@ -122,7 +122,7 @@ const BookStackSearchWidget: React.FC = () => { return ( } + icon={} isLoading skeleton={} /> @@ -133,7 +133,7 @@ const BookStackSearchWidget: React.FC = () => { return ( } + icon={} isEmpty emptyMessage="BookStack nicht eingerichtet" /> @@ -143,7 +143,7 @@ const BookStackSearchWidget: React.FC = () => { return ( } + icon={} > } + icon={} isLoading={isLoading} skeleton={} isError={isError || (!isLoading && !activeJahr)} diff --git a/frontend/src/components/dashboard/ChecklistWidget.tsx b/frontend/src/components/dashboard/ChecklistWidget.tsx index 8b6994b..4947c41 100644 --- a/frontend/src/components/dashboard/ChecklistWidget.tsx +++ b/frontend/src/components/dashboard/ChecklistWidget.tsx @@ -25,7 +25,7 @@ function ChecklistWidget() { return ( } + icon={} action={titleAction} isLoading={isLoading} skeleton={} diff --git a/frontend/src/components/dashboard/EventQuickAddWidget.tsx b/frontend/src/components/dashboard/EventQuickAddWidget.tsx index 4c96daf..4394cd4 100644 --- a/frontend/src/components/dashboard/EventQuickAddWidget.tsx +++ b/frontend/src/components/dashboard/EventQuickAddWidget.tsx @@ -111,7 +111,7 @@ const EventQuickAddWidget: React.FC = () => { return ( } + icon={} isSubmitting={mutation.isPending} onSubmit={handleSubmit} submitLabel="Erstellen" diff --git a/frontend/src/components/dashboard/IssueOverviewWidget.tsx b/frontend/src/components/dashboard/IssueOverviewWidget.tsx index 75d9866..ebe30cc 100644 --- a/frontend/src/components/dashboard/IssueOverviewWidget.tsx +++ b/frontend/src/components/dashboard/IssueOverviewWidget.tsx @@ -21,7 +21,7 @@ function IssueOverviewWidget() { return ( } + icon={} isLoading={isLoading} skeleton={} isError={isError} diff --git a/frontend/src/components/dashboard/IssueQuickAddWidget.tsx b/frontend/src/components/dashboard/IssueQuickAddWidget.tsx index 8eed281..e6c2c6c 100644 --- a/frontend/src/components/dashboard/IssueQuickAddWidget.tsx +++ b/frontend/src/components/dashboard/IssueQuickAddWidget.tsx @@ -66,7 +66,7 @@ const IssueQuickAddWidget: React.FC = () => { return ( } + icon={} isLoading skeleton={} /> @@ -76,7 +76,7 @@ const IssueQuickAddWidget: React.FC = () => { return ( } + icon={} isSubmitting={mutation.isPending} onSubmit={handleSubmit} submitLabel="Melden" diff --git a/frontend/src/components/dashboard/LinksWidget.tsx b/frontend/src/components/dashboard/LinksWidget.tsx index 22b5128..eb7ea55 100644 --- a/frontend/src/components/dashboard/LinksWidget.tsx +++ b/frontend/src/components/dashboard/LinksWidget.tsx @@ -13,7 +13,7 @@ function LinksWidget({ collection }: LinksWidgetProps) { return ( } + icon={} items={collection.links} renderItem={(link) => ( { return ( } + icon={} isError errorMessage="Termine konnten nicht geladen werden." /> @@ -148,7 +148,7 @@ const UpcomingEventsWidget: React.FC = () => { return ( } + icon={} items={entries} renderItem={(entry) => ( { return ( } + icon={} isError errorMessage="Buchungen konnten nicht geladen werden." /> @@ -66,7 +66,7 @@ const VehicleBookingListWidget: React.FC = () => { return ( } + icon={} items={items} renderItem={(booking) => { const color = BUCHUNGS_ART_COLORS[booking.buchungs_art] ?? '#9e9e9e'; diff --git a/frontend/src/components/dashboard/VehicleBookingQuickAddWidget.tsx b/frontend/src/components/dashboard/VehicleBookingQuickAddWidget.tsx index 2a1c417..e43db12 100644 --- a/frontend/src/components/dashboard/VehicleBookingQuickAddWidget.tsx +++ b/frontend/src/components/dashboard/VehicleBookingQuickAddWidget.tsx @@ -95,7 +95,7 @@ const VehicleBookingQuickAddWidget: React.FC = () => { return ( } + icon={} isLoading skeleton={} /> @@ -105,7 +105,7 @@ const VehicleBookingQuickAddWidget: React.FC = () => { return ( } + icon={} isSubmitting={mutation.isPending} onSubmit={handleSubmit} submitLabel="Erstellen" diff --git a/frontend/src/components/dashboard/VikunjaMyTasksWidget.tsx b/frontend/src/components/dashboard/VikunjaMyTasksWidget.tsx index 47f04bb..1660466 100644 --- a/frontend/src/components/dashboard/VikunjaMyTasksWidget.tsx +++ b/frontend/src/components/dashboard/VikunjaMyTasksWidget.tsx @@ -96,7 +96,7 @@ const VikunjaMyTasksWidget: React.FC = () => { return ( } + icon={} isEmpty emptyMessage="Vikunja nicht eingerichtet" /> @@ -107,7 +107,7 @@ const VikunjaMyTasksWidget: React.FC = () => { return ( } + icon={} isError errorMessage="Vikunja nicht erreichbar" /> @@ -115,13 +115,13 @@ const VikunjaMyTasksWidget: React.FC = () => { } const titleAction = !isLoading && !isError && tasks.length > 0 ? ( - + ) : undefined; return ( } + icon={} action={titleAction} items={tasks} renderItem={(task) => ( diff --git a/frontend/src/components/dashboard/VikunjaQuickAddWidget.tsx b/frontend/src/components/dashboard/VikunjaQuickAddWidget.tsx index 366d3d0..c89421a 100644 --- a/frontend/src/components/dashboard/VikunjaQuickAddWidget.tsx +++ b/frontend/src/components/dashboard/VikunjaQuickAddWidget.tsx @@ -62,7 +62,7 @@ const VikunjaQuickAddWidget: React.FC = () => { return ( } + icon={} isEmpty emptyMessage="Vikunja nicht eingerichtet" /> @@ -73,7 +73,7 @@ const VikunjaQuickAddWidget: React.FC = () => { return ( } + icon={} isLoading skeleton={} /> @@ -83,7 +83,7 @@ const VikunjaQuickAddWidget: React.FC = () => { return ( } + icon={} isSubmitting={mutation.isPending} onSubmit={handleSubmit} submitLabel="Erstellen" diff --git a/frontend/src/components/equipment/EquipmentDashboardCard.tsx b/frontend/src/components/equipment/EquipmentDashboardCard.tsx index 8216569..799831b 100644 --- a/frontend/src/components/equipment/EquipmentDashboardCard.tsx +++ b/frontend/src/components/equipment/EquipmentDashboardCard.tsx @@ -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,112 +30,72 @@ const EquipmentDashboardCard: React.FC = ({ const animatedReady = useCountUp(stats?.einsatzbereit ?? 0); const animatedTotal = useCountUp(stats?.total ?? 0); - if (isLoading) { - return ( - - - - - Ausrüstungsstatus wird geladen... - - - - ); - } + 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 ( - - - - Ausrüstungsstatus konnte nicht geladen werden. - - - - ); - } - - 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 ( - navigate('/ausruestung')}> - - - Ausrüstung - + } + isLoading={isLoading} + isError={isError} + errorMessage="Ausrüstungsstatus konnte nicht geladen werden." + skeleton={} + onClick={() => navigate('/ausruestung')} + > + + {animatedReady}/{animatedTotal} + + + einsatzbereit + - {/* Main metric */} - - {animatedReady}/{animatedTotal} - - - einsatzbereit - + {(hasCritical || hasWarning) && ( + + {hasCritical && stats && ( + + Kritisch + {stats.wichtigNichtBereit > 0 && ( + {stats.wichtigNichtBereit} wichtige Teile nicht einsatzbereit + )} + {stats.inspectionsOverdue > 0 && ( + {stats.inspectionsOverdue} Prüfung{stats.inspectionsOverdue !== 1 ? 'en' : ''} überfällig + )} + {stats.beschaedigt > 0 && ( + {stats.beschaedigt} beschädigt + )} + + )} + {hasWarning && stats && ( + + Achtung + {stats.inspectionsDue > 0 && ( + {stats.inspectionsDue} Prüfung{stats.inspectionsDue !== 1 ? 'en' : ''} bald fällig + )} + {stats.inWartung > 0 && ( + {stats.inWartung} in Wartung + )} + {stats.ausserDienst > 0 && ( + {stats.ausserDienst} außer Dienst + )} + + )} + + )} - {/* Alerts — consolidated counts only */} - {(hasCritical || hasWarning) && ( - - {hasCritical && ( - - Kritisch - {stats.wichtigNichtBereit > 0 && ( - - {stats.wichtigNichtBereit} wichtige Teile nicht einsatzbereit - - )} - {stats.inspectionsOverdue > 0 && ( - - {stats.inspectionsOverdue} Prüfung{stats.inspectionsOverdue !== 1 ? 'en' : ''} überfällig - - )} - {stats.beschaedigt > 0 && ( - - {stats.beschaedigt} beschädigt - - )} - - )} - {hasWarning && ( - - Achtung - {stats.inspectionsDue > 0 && ( - - {stats.inspectionsDue} Prüfung{stats.inspectionsDue !== 1 ? 'en' : ''} bald fällig - - )} - {stats.inWartung > 0 && ( - - {stats.inWartung} in Wartung - - )} - {stats.ausserDienst > 0 && ( - - {stats.ausserDienst} außer Dienst - - )} - - )} - - )} - - {/* All good message */} - {allGood && ( - - Alle Ausrüstung einsatzbereit - - )} - - + {allGood && ( + + Alle Ausrüstung einsatzbereit + + )} + ); }; diff --git a/frontend/src/components/vehicles/VehicleDashboardCard.tsx b/frontend/src/components/vehicles/VehicleDashboardCard.tsx index dfc29ab..4133b6a 100644 --- a/frontend/src/components/vehicles/VehicleDashboardCard.tsx +++ b/frontend/src/components/vehicles/VehicleDashboardCard.tsx @@ -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,107 +44,85 @@ const VehicleDashboardCard: React.FC = ({ const loading = statsLoading || alertsLoading || warningsLoading; - if (loading) { - return ( - - - - - Fahrzeugstatus wird geladen... - - - - ); + 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 ( - - - - Fahrzeugstatus konnte nicht geladen werden. - - - - ); - } - - 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 ( - navigate('/fahrzeuge')}> - - - Fahrzeuge - + } + isLoading={loading} + isError={statsError} + errorMessage="Fahrzeugstatus konnte nicht geladen werden." + skeleton={} + onClick={() => navigate('/fahrzeuge')} + > + + {animatedReady}/{animatedTotal} + + + einsatzbereit + - {/* Main metric */} - - {animatedReady}/{animatedTotal} - - - einsatzbereit - + {hasConcerns && ( + + {overdueAlerts.length > 0 && ( + + Überfällig + + {overdueAlerts.length} Prüfung{overdueAlerts.length !== 1 ? 'en' : ''} überfällig + + + )} + {(stats?.ausserDienst ?? 0) > 0 && ( + + Außer Dienst + + {stats!.ausserDienst} Fahrzeug{stats!.ausserDienst !== 1 ? 'e' : ''} außer Dienst + + + )} + {upcomingAlerts.length > 0 && ( + + Bald fällig + + {upcomingAlerts.length} Prüfung{upcomingAlerts.length !== 1 ? 'en' : ''} bald fällig + + + )} + {equipmentWarnings.length > 0 && ( + + Ausrüstung nicht verfügbar + + {equipmentWarnings.length} Ausrüstungsgegenstand{equipmentWarnings.length !== 1 ? 'stände' : 'stand'} nicht einsatzbereit + + + )} + + )} - {/* Concerns list — using Alert components for consistent warning styling */} - {hasConcerns && ( - - {overdueAlerts.length > 0 && ( - - Überfällig - - {overdueAlerts.length} Prüfung{overdueAlerts.length !== 1 ? 'en' : ''} überfällig - - - )} - {stats.ausserDienst > 0 && ( - - Außer Dienst - - {stats.ausserDienst} Fahrzeug{stats.ausserDienst !== 1 ? 'e' : ''} außer Dienst - - - )} - {upcomingAlerts.length > 0 && ( - - Bald fällig - - {upcomingAlerts.length} Prüfung{upcomingAlerts.length !== 1 ? 'en' : ''} bald fällig - - - )} - {equipmentWarnings.length > 0 && ( - - Ausrüstung nicht verfügbar - - {equipmentWarnings.length} Ausrüstungsgegenstand{equipmentWarnings.length !== 1 ? 'stände' : 'stand'} nicht einsatzbereit - - - )} - - )} - - {/* All good message */} - {allGood && ( - - Alle Fahrzeuge einsatzbereit - - )} - - + {allGood && ( + + Alle Fahrzeuge einsatzbereit + + )} + ); }; diff --git a/frontend/src/pages/Buchhaltung.tsx b/frontend/src/pages/Buchhaltung.tsx index 5f5aae1..7fa36f3 100644 --- a/frontend/src/pages/Buchhaltung.tsx +++ b/frontend/src/pages/Buchhaltung.tsx @@ -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 ─────────────────────────────────────────────────────────────────── diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 2a1e607..2681a0d 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -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(); + 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,7 +386,9 @@ function Dashboard() { if (keys.includes(widgetKey)) { targetGroup = group; break; } } if (!updatedOrder[targetGroup]) updatedOrder[targetGroup] = []; - updatedOrder[targetGroup].push(widgetKey); + if (!updatedOrder[targetGroup].includes(widgetKey)) { + updatedOrder[targetGroup].push(widgetKey); + } } const updatedCustomGroups = localCustomGroups.filter((g) => g.name !== groupName);