diff --git a/frontend/src/features/dashboard/components/DashboardScreen.tsx b/frontend/src/features/dashboard/components/DashboardScreen.tsx index 01b76aa..ee0fc7c 100644 --- a/frontend/src/features/dashboard/components/DashboardScreen.tsx +++ b/frontend/src/features/dashboard/components/DashboardScreen.tsx @@ -1,47 +1,72 @@ /** - * @ai-summary Main dashboard screen component showing fleet overview + * @ai-summary Main dashboard screen showing vehicle fleet roster with health indicators */ import React, { useState } from 'react'; -import { Box, Dialog, DialogTitle, DialogContent, IconButton, useMediaQuery, useTheme } from '@mui/material'; +import { Box, Dialog, DialogTitle, DialogContent, IconButton, Skeleton, Typography, useMediaQuery, useTheme } from '@mui/material'; import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; import DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded'; import CloseIcon from '@mui/icons-material/Close'; -import { SummaryCards, SummaryCardsSkeleton } from './SummaryCards'; -import { VehicleAttention, VehicleAttentionSkeleton } from './VehicleAttention'; -import { QuickActions, QuickActionsSkeleton } from './QuickActions'; -import { RecentActivity, RecentActivitySkeleton } from './RecentActivity'; -import { useDashboardSummary, useVehiclesNeedingAttention, useRecentActivity } from '../hooks/useDashboardData'; +import { VehicleRosterCard } from './VehicleRosterCard'; +import { ActionBar } from './ActionBar'; +import { useVehicleRoster } from '../hooks/useDashboardData'; import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; import { Button } from '../../../shared-minimal/components/Button'; import { PendingAssociationBanner } from '../../email-ingestion/components/PendingAssociationBanner'; import { PendingAssociationList } from '../../email-ingestion/components/PendingAssociationList'; - import { MobileScreen } from '../../../core/store'; import { Vehicle } from '../../vehicles/types/vehicles.types'; interface DashboardScreenProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- matches navigation store type signature onNavigate?: (screen: MobileScreen, metadata?: Record) => void; onVehicleClick?: (vehicle: Vehicle) => void; onViewMaintenance?: () => void; onAddVehicle?: () => void; } +const RosterSkeleton: React.FC = () => ( +
+ {[0, 1, 2, 3].map(i => ( + +
+ +
+ +
+ +
+
+ + +
+ +
+ ))} +
+); + export const DashboardScreen: React.FC = ({ onNavigate, onVehicleClick, - onViewMaintenance, - onAddVehicle + onAddVehicle, }) => { const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down('sm')); const [showPendingReceipts, setShowPendingReceipts] = useState(false); - const { data: summary, isLoading: summaryLoading, error: summaryError } = useDashboardSummary(); - const { data: vehiclesNeedingAttention, isLoading: attentionLoading, error: attentionError } = useVehiclesNeedingAttention(); - const { data: recentActivity } = useRecentActivity(); + const { data: roster, vehicles, isLoading, error } = useVehicleRoster(); + + const handleAddVehicle = onAddVehicle ?? (() => onNavigate?.('Vehicles')); + const handleLogFuel = () => onNavigate?.('Log Fuel'); + const handleVehicleClick = (vehicleId: string) => { + const vehicle = vehicles?.find(v => v.id === vehicleId); + if (vehicle && onVehicleClick) { + onVehicleClick(vehicle); + } + }; // Error state - if (summaryError || attentionError) { + if (error) { return (
@@ -69,19 +94,21 @@ export const DashboardScreen: React.FC = ({ } // Loading state - if (summaryLoading || attentionLoading || !summary || !vehiclesNeedingAttention) { + if (isLoading || !roster) { return (
- - - - +
+ + Your Fleet + +
+
); } // Empty state - no vehicles - if (summary.totalVehicles === 0) { + if (roster.length === 0) { return (
@@ -98,7 +125,7 @@ export const DashboardScreen: React.FC = ({ @@ -114,32 +141,24 @@ export const DashboardScreen: React.FC = ({ {/* Pending Receipts Banner */} setShowPendingReceipts(true)} /> - {/* Summary Cards */} - + {/* Heading + Action Bar */} +
+ + Your Fleet + + +
- {/* Vehicles Needing Attention */} - {vehiclesNeedingAttention && vehiclesNeedingAttention.length > 0 && ( - { - const vehicle = vehiclesNeedingAttention.find(v => v.id === vehicleId); - if (vehicle && onVehicleClick) { - onVehicleClick(vehicle); - } - }} - /> - )} - - {/* Recent Activity */} - {recentActivity && } - - {/* Quick Actions */} - onNavigate?.('Vehicles'))} - onLogFuel={() => onNavigate?.('Log Fuel')} - onViewMaintenance={onViewMaintenance ?? (() => onNavigate?.('Vehicles'))} - onViewVehicles={() => onNavigate?.('Vehicles')} - /> + {/* Vehicle Roster Grid */} +
+ {roster.map(rosterData => ( + + ))} +
{/* Pending Receipts Dialog */} ; - onClick: () => void; -} - -interface QuickActionsProps { - onAddVehicle: () => void; - onLogFuel: () => void; - onViewMaintenance: () => void; - onViewVehicles: () => void; -} - -export const QuickActions: React.FC = ({ - onAddVehicle, - onLogFuel, - onViewMaintenance, - onViewVehicles, -}) => { - const actions: QuickAction[] = [ - { - id: 'add-vehicle', - title: 'Add Vehicle', - description: 'Register a new vehicle', - icon: DirectionsCarRoundedIcon, - onClick: onAddVehicle, - }, - { - id: 'log-fuel', - title: 'Log Fuel', - description: 'Record a fuel purchase', - icon: LocalGasStationRoundedIcon, - onClick: onLogFuel, - }, - { - id: 'view-maintenance', - title: 'Maintenance', - description: 'View maintenance records', - icon: BuildRoundedIcon, - onClick: onViewMaintenance, - }, - { - id: 'view-vehicles', - title: 'My Vehicles', - description: 'View all vehicles', - icon: FormatListBulletedRoundedIcon, - onClick: onViewVehicles, - }, - ]; - - return ( - -
-

- Quick Actions -

-

- Common tasks and navigation -

-
- -
- {actions.map((action) => { - const IconComponent = action.icon; - return ( - - - - - - - {action.title} - - - {action.description} - - - - ); - })} -
-
- ); -}; - -export const QuickActionsSkeleton: React.FC = () => { - return ( - -
-
-
-
-
- {[1, 2, 3, 4].map((i) => ( -
-
-
-
-
- ))} -
- - ); -}; diff --git a/frontend/src/features/dashboard/components/RecentActivity.tsx b/frontend/src/features/dashboard/components/RecentActivity.tsx deleted file mode 100644 index 51e6b9f..0000000 --- a/frontend/src/features/dashboard/components/RecentActivity.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/** - * @ai-summary Recent activity feed showing latest fuel logs and maintenance events - */ - -import React from 'react'; -import { Box } from '@mui/material'; -import LocalGasStationRoundedIcon from '@mui/icons-material/LocalGasStationRounded'; -import BuildRoundedIcon from '@mui/icons-material/BuildRounded'; -import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; -import { RecentActivityItem } from '../types'; - -interface RecentActivityProps { - items: RecentActivityItem[]; -} - -const formatRelativeTime = (timestamp: string): string => { - const now = new Date(); - const date = new Date(timestamp); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffDays < 0) { - // Future date (upcoming maintenance) - const absDays = Math.abs(diffDays); - if (absDays === 0) return 'Today'; - if (absDays === 1) return 'Tomorrow'; - return `In ${absDays} days`; - } - if (diffDays === 0) { - if (diffHours === 0) return diffMins <= 1 ? 'Just now' : `${diffMins}m ago`; - return `${diffHours}h ago`; - } - if (diffDays === 1) return 'Yesterday'; - if (diffDays < 7) return `${diffDays}d ago`; - return date.toLocaleDateString(); -}; - -export const RecentActivity: React.FC = ({ items }) => { - if (items.length === 0) { - return ( - -

- Recent Activity -

-

- No recent activity. Start by logging fuel or scheduling maintenance. -

-
- ); - } - - return ( - -

- Recent Activity -

-
- {items.map((item, index) => ( -
- - {item.type === 'fuel' ? ( - - ) : ( - - )} - -
-

- {item.vehicleName} -

-

- {item.description} -

-
- - {formatRelativeTime(item.timestamp)} - -
- ))} -
-
- ); -}; - -export const RecentActivitySkeleton: React.FC = () => { - return ( - -
-
- {[1, 2, 3].map((i) => ( -
-
-
-
-
-
-
- ))} -
- - ); -}; diff --git a/frontend/src/features/dashboard/components/SummaryCards.tsx b/frontend/src/features/dashboard/components/SummaryCards.tsx deleted file mode 100644 index 9b14463..0000000 --- a/frontend/src/features/dashboard/components/SummaryCards.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/** - * @ai-summary Summary cards showing key dashboard metrics - */ - -import React from 'react'; -import { Box } from '@mui/material'; -import DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded'; -import BuildRoundedIcon from '@mui/icons-material/BuildRounded'; -import LocalGasStationRoundedIcon from '@mui/icons-material/LocalGasStationRounded'; -import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; -import { DashboardSummary } from '../types'; -import { MobileScreen } from '../../../core/store'; - -interface SummaryCardsProps { - summary: DashboardSummary; - onNavigate?: (screen: MobileScreen) => void; -} - -export const SummaryCards: React.FC = ({ summary, onNavigate }) => { - const cards = [ - { - title: 'Total Vehicles', - value: summary.totalVehicles, - icon: DirectionsCarRoundedIcon, - color: 'primary.main', - ctaText: 'Add a vehicle', - ctaScreen: 'Vehicles' as MobileScreen, - }, - { - title: 'Upcoming Maintenance', - value: summary.upcomingMaintenanceCount, - subtitle: 'Next 30 days', - icon: BuildRoundedIcon, - color: 'primary.main', - ctaText: 'Schedule maintenance', - ctaScreen: 'Maintenance' as MobileScreen, - }, - { - title: 'Recent Fuel Logs', - value: summary.recentFuelLogsCount, - subtitle: 'Last 7 days', - icon: LocalGasStationRoundedIcon, - color: 'primary.main', - ctaText: 'Log your first fill-up', - ctaScreen: 'Log Fuel' as MobileScreen, - }, - ]; - - return ( -
- {cards.map((card) => { - const IconComponent = card.icon; - return ( - -
- - - -
-

- {card.title} -

- - {card.value} - - {card.value === 0 && card.ctaText ? ( - onNavigate?.(card.ctaScreen)} - sx={{ - background: 'none', - border: 'none', - padding: 0, - cursor: 'pointer', - color: 'primary.main', - fontSize: '0.75rem', - fontWeight: 500, - mt: 0.5, - '&:hover': { textDecoration: 'underline' }, - }} - > - {card.ctaText} - - ) : card.subtitle ? ( -

- {card.subtitle} -

- ) : null} -
-
-
- ); - })} -
- ); -}; - -export const SummaryCardsSkeleton: React.FC = () => { - return ( -
- {[1, 2, 3].map((i) => ( - -
-
-
-
-
-
-
-
- - ))} -
- ); -}; diff --git a/frontend/src/features/dashboard/components/VehicleAttention.tsx b/frontend/src/features/dashboard/components/VehicleAttention.tsx deleted file mode 100644 index 89fbabc..0000000 --- a/frontend/src/features/dashboard/components/VehicleAttention.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @ai-summary List of vehicles needing attention (overdue maintenance) - */ - -import React from 'react'; -import { Box, SvgIconProps } from '@mui/material'; -import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded'; -import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded'; -import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; -import ScheduleRoundedIcon from '@mui/icons-material/ScheduleRounded'; -import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; -import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; -import { VehicleNeedingAttention } from '../types'; - -interface VehicleAttentionProps { - vehicles: VehicleNeedingAttention[]; - onVehicleClick?: (vehicleId: string) => void; -} - -export const VehicleAttention: React.FC = ({ vehicles, onVehicleClick }) => { - if (vehicles.length === 0) { - return ( - -
- - - -

- All Caught Up! -

-

- No vehicles need immediate attention -

-
-
- ); - } - - const priorityConfig: Record }> = { - high: { - color: 'error.main', - icon: ErrorRoundedIcon, - }, - medium: { - color: 'warning.main', - icon: WarningAmberRoundedIcon, - }, - low: { - color: 'info.main', - icon: ScheduleRoundedIcon, - }, - }; - - return ( - -
-

- Needs Attention -

-

- Vehicles with overdue maintenance -

-
- -
- {vehicles.map((vehicle) => { - const config = priorityConfig[vehicle.priority]; - const IconComponent = config.icon; - return ( - onVehicleClick?.(vehicle.id)} - role={onVehicleClick ? 'button' : undefined} - tabIndex={onVehicleClick ? 0 : undefined} - onKeyDown={(e: React.KeyboardEvent) => { - if (onVehicleClick && (e.key === 'Enter' || e.key === ' ')) { - e.preventDefault(); - onVehicleClick(vehicle.id); - } - }} - sx={{ - p: 2, - borderRadius: 3, - bgcolor: 'action.hover', - border: '1px solid', - borderColor: 'divider', - cursor: onVehicleClick ? 'pointer' : 'default', - transition: 'all 0.2s', - '&:hover': onVehicleClick ? { - bgcolor: 'action.selected', - } : {}, - }} - > -
- - - -
- - {getVehicleLabel(vehicle)} - -

- {vehicle.reason} -

- - {vehicle.priority.toUpperCase()} PRIORITY - -
-
-
- ); - })} -
-
- ); -}; - -export const VehicleAttentionSkeleton: React.FC = () => { - return ( - -
-
-
-
-
- {[1, 2].map((i) => ( -
-
-
-
-
-
-
-
-
-
- ))} -
- - ); -}; diff --git a/frontend/src/features/dashboard/hooks/useDashboardData.ts b/frontend/src/features/dashboard/hooks/useDashboardData.ts index 91fc7d7..e9ec5b0 100644 --- a/frontend/src/features/dashboard/hooks/useDashboardData.ts +++ b/frontend/src/features/dashboard/hooks/useDashboardData.ts @@ -144,8 +144,9 @@ export const useDashboardData = () => { enabled: isAuthenticated && !authLoading, staleTime: 2 * 60 * 1000, gcTime: 5 * 60 * 1000, - retry: (failureCount, error: any) => { - if (error?.response?.status === 401 && failureCount < 3) { + retry: (failureCount, error: unknown) => { + const status = (error as { response?: { status?: number } })?.response?.status; + if (status === 401 && failureCount < 3) { return true; } return false; diff --git a/frontend/src/features/dashboard/index.ts b/frontend/src/features/dashboard/index.ts index cac9ef7..cc7963a 100644 --- a/frontend/src/features/dashboard/index.ts +++ b/frontend/src/features/dashboard/index.ts @@ -4,9 +4,7 @@ export { DashboardScreen } from './components/DashboardScreen'; export { DashboardPage } from './pages/DashboardPage'; -export { SummaryCards, SummaryCardsSkeleton } from './components/SummaryCards'; -export { VehicleAttention, VehicleAttentionSkeleton } from './components/VehicleAttention'; -export { QuickActions, QuickActionsSkeleton } from './components/QuickActions'; -export { RecentActivity, RecentActivitySkeleton } from './components/RecentActivity'; -export { useDashboardSummary, useVehiclesNeedingAttention, useRecentActivity } from './hooks/useDashboardData'; -export type { DashboardSummary, VehicleNeedingAttention, RecentActivityItem, DashboardData } from './types'; +export { VehicleRosterCard } from './components/VehicleRosterCard'; +export { ActionBar } from './components/ActionBar'; +export { useVehicleRoster } from './hooks/useDashboardData'; +export type { VehicleHealth, AttentionItem, VehicleRosterData } from './types'; diff --git a/frontend/src/features/dashboard/pages/DashboardPage.tsx b/frontend/src/features/dashboard/pages/DashboardPage.tsx index 39bd4d7..68f7084 100644 --- a/frontend/src/features/dashboard/pages/DashboardPage.tsx +++ b/frontend/src/features/dashboard/pages/DashboardPage.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; -import { Box, Typography } from '@mui/material'; +import { Box } from '@mui/material'; import { DashboardScreen } from '../components/DashboardScreen'; import { MobileScreen } from '../../../core/store'; import { Vehicle } from '../../vehicles/types/vehicles.types'; @@ -49,9 +49,6 @@ export const DashboardPage: React.FC = () => { return ( - - Dashboard -