From bcb39b9cda2063a046b70050849d21901365b314 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Thu, 1 Jan 2026 22:35:48 -0600 Subject: [PATCH] feat: add dashboard with vehicle fleet overview (refs #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements responsive dashboard showing: - Summary cards (vehicle count, upcoming maintenance, recent fuel logs) - Vehicles needing attention with priority highlighting - Quick action buttons for navigation - Loading skeletons and empty states - Mobile-first responsive layout (320px to 1920px+) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/App.tsx | 18 +-- .../dashboard/components/DashboardScreen.tsx | 125 ++++++++++++++++ .../dashboard/components/QuickActions.tsx | 126 ++++++++++++++++ .../dashboard/components/SummaryCards.tsx | 85 +++++++++++ .../dashboard/components/VehicleAttention.tsx | 131 ++++++++++++++++ .../dashboard/hooks/useDashboardData.ts | 141 ++++++++++++++++++ frontend/src/features/dashboard/index.ts | 10 ++ .../src/features/dashboard/types/index.ts | 21 +++ 8 files changed, 644 insertions(+), 13 deletions(-) create mode 100644 frontend/src/features/dashboard/components/DashboardScreen.tsx create mode 100644 frontend/src/features/dashboard/components/QuickActions.tsx create mode 100644 frontend/src/features/dashboard/components/SummaryCards.tsx create mode 100644 frontend/src/features/dashboard/components/VehicleAttention.tsx create mode 100644 frontend/src/features/dashboard/hooks/useDashboardData.ts create mode 100644 frontend/src/features/dashboard/index.ts create mode 100644 frontend/src/features/dashboard/types/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 05a1292..a431fba 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -78,18 +78,7 @@ import { useDataSync } from './core/hooks/useDataSync'; import { MobileDebugPanel } from './core/debug/MobileDebugPanel'; import { MobileErrorBoundary } from './core/error-boundaries/MobileErrorBoundary'; import { useLoginNotifications } from './features/notifications/hooks/useLoginNotifications'; - -// Hoisted mobile screen components to stabilize identity and prevent remounts -const DashboardScreen: React.FC = () => ( -
- -
-

Dashboard

-

Coming soon - Vehicle insights and analytics

-
-
-
-); +import { DashboardScreen as DashboardFeature } from './features/dashboard'; const LogFuelScreen: React.FC = () => { const queryClient = useQueryClient(); @@ -640,7 +629,10 @@ function App() { transition={{ duration: 0.2, ease: "easeOut" }} > - + )} diff --git a/frontend/src/features/dashboard/components/DashboardScreen.tsx b/frontend/src/features/dashboard/components/DashboardScreen.tsx new file mode 100644 index 0000000..efb0760 --- /dev/null +++ b/frontend/src/features/dashboard/components/DashboardScreen.tsx @@ -0,0 +1,125 @@ +/** + * @ai-summary Main dashboard screen component showing fleet overview + */ + +import React from 'react'; +import { SummaryCards, SummaryCardsSkeleton } from './SummaryCards'; +import { VehicleAttention, VehicleAttentionSkeleton } from './VehicleAttention'; +import { QuickActions, QuickActionsSkeleton } from './QuickActions'; +import { useDashboardSummary, useVehiclesNeedingAttention } from '../hooks/useDashboardData'; +import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; + +import { MobileScreen } from '../../../core/store'; +import { Vehicle } from '../../vehicles/types/vehicles.types'; + +interface DashboardScreenProps { + onNavigate?: (screen: MobileScreen, metadata?: Record) => void; + onVehicleClick?: (vehicle: Vehicle) => void; +} + +export const DashboardScreen: React.FC = ({ + onNavigate, + onVehicleClick +}) => { + const { data: summary, isLoading: summaryLoading, error: summaryError } = useDashboardSummary(); + const { data: vehiclesNeedingAttention, isLoading: attentionLoading, error: attentionError } = useVehiclesNeedingAttention(); + + // Error state + if (summaryError || attentionError) { + return ( +
+ +
+
⚠️
+

+ Unable to Load Dashboard +

+

+ There was an error loading your dashboard data +

+ +
+
+
+ ); + } + + // Loading state + if (summaryLoading || attentionLoading || !summary || !vehiclesNeedingAttention) { + return ( +
+ + + +
+ ); + } + + // Empty state - no vehicles + if (summary.totalVehicles === 0) { + return ( +
+ +
+
🚗
+

+ Welcome to MotoVaultPro +

+

+ Get started by adding your first vehicle to track fuel logs, maintenance, and more +

+ +
+
+
+ ); + } + + // Main dashboard view + return ( +
+ {/* Summary Cards */} + + + {/* Vehicles Needing Attention */} + {vehiclesNeedingAttention && vehiclesNeedingAttention.length > 0 && ( + { + const vehicle = vehiclesNeedingAttention.find(v => v.id === vehicleId); + if (vehicle && onVehicleClick) { + onVehicleClick(vehicle); + } + }} + /> + )} + + {/* Quick Actions */} + onNavigate?.('Vehicles')} + onLogFuel={() => onNavigate?.('Log Fuel')} + onViewMaintenance={() => onNavigate?.('Vehicles')} // Navigate to vehicles then maintenance + onViewVehicles={() => onNavigate?.('Vehicles')} + /> + + {/* Footer Hint */} +
+

+ Dashboard updates every 2 minutes +

+
+
+ ); +}; diff --git a/frontend/src/features/dashboard/components/QuickActions.tsx b/frontend/src/features/dashboard/components/QuickActions.tsx new file mode 100644 index 0000000..e8c7370 --- /dev/null +++ b/frontend/src/features/dashboard/components/QuickActions.tsx @@ -0,0 +1,126 @@ +/** + * @ai-summary Quick action buttons for common tasks + */ + +import React from 'react'; +import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; + +interface QuickAction { + id: string; + title: string; + description: string; + icon: string; + color: string; + bgColor: string; + 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: '🚗', + color: 'text-blue-600', + bgColor: 'bg-blue-50', + onClick: onAddVehicle, + }, + { + id: 'log-fuel', + title: 'Log Fuel', + description: 'Record a fuel purchase', + icon: '⛽', + color: 'text-green-600', + bgColor: 'bg-green-50', + onClick: onLogFuel, + }, + { + id: 'view-maintenance', + title: 'Maintenance', + description: 'View maintenance records', + icon: '🔧', + color: 'text-orange-600', + bgColor: 'bg-orange-50', + onClick: onViewMaintenance, + }, + { + id: 'view-vehicles', + title: 'My Vehicles', + description: 'View all vehicles', + icon: '📋', + color: 'text-purple-600', + bgColor: 'bg-purple-50', + onClick: onViewVehicles, + }, + ]; + + return ( + +
+

+ Quick Actions +

+

+ Common tasks and navigation +

+
+ +
+ {actions.map((action) => ( + + ))} +
+
+ ); +}; + +export const QuickActionsSkeleton: React.FC = () => { + return ( + +
+
+
+
+
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+
+ ))} +
+ + ); +}; diff --git a/frontend/src/features/dashboard/components/SummaryCards.tsx b/frontend/src/features/dashboard/components/SummaryCards.tsx new file mode 100644 index 0000000..3e8d6ec --- /dev/null +++ b/frontend/src/features/dashboard/components/SummaryCards.tsx @@ -0,0 +1,85 @@ +/** + * @ai-summary Summary cards showing key dashboard metrics + */ + +import React from 'react'; +import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; +import { DashboardSummary } from '../types'; + +interface SummaryCardsProps { + summary: DashboardSummary; +} + +export const SummaryCards: React.FC = ({ summary }) => { + const cards = [ + { + title: 'Total Vehicles', + value: summary.totalVehicles, + icon: '🚗', + color: 'text-blue-600', + bgColor: 'bg-blue-50', + }, + { + title: 'Upcoming Maintenance', + value: summary.upcomingMaintenanceCount, + subtitle: 'Next 30 days', + icon: '🔧', + color: 'text-orange-600', + bgColor: 'bg-orange-50', + }, + { + title: 'Recent Fuel Logs', + value: summary.recentFuelLogsCount, + subtitle: 'Last 7 days', + icon: '⛽', + color: 'text-green-600', + bgColor: 'bg-green-50', + }, + ]; + + return ( +
+ {cards.map((card) => ( + +
+
+ {card.icon} +
+
+

+ {card.title} +

+

+ {card.value} +

+ {card.subtitle && ( +

+ {card.subtitle} +

+ )} +
+
+
+ ))} +
+ ); +}; + +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 new file mode 100644 index 0000000..291d748 --- /dev/null +++ b/frontend/src/features/dashboard/components/VehicleAttention.tsx @@ -0,0 +1,131 @@ +/** + * @ai-summary List of vehicles needing attention (overdue maintenance) + */ + +import React from 'react'; +import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; +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 = { + high: { + color: 'text-red-600', + bgColor: 'bg-red-50', + borderColor: 'border-red-200', + icon: '🚨', + }, + medium: { + color: 'text-orange-600', + bgColor: 'bg-orange-50', + borderColor: 'border-orange-200', + icon: '⚠️', + }, + low: { + color: 'text-yellow-600', + bgColor: 'bg-yellow-50', + borderColor: 'border-yellow-200', + icon: '⏰', + }, + }; + + return ( + +
+

+ Needs Attention +

+

+ Vehicles with overdue maintenance +

+
+ +
+ {vehicles.map((vehicle) => { + const config = priorityConfig[vehicle.priority]; + return ( +
onVehicleClick?.(vehicle.id)} + role={onVehicleClick ? 'button' : undefined} + tabIndex={onVehicleClick ? 0 : undefined} + onKeyDown={(e) => { + if (onVehicleClick && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + onVehicleClick(vehicle.id); + } + }} + > +
+
+ {config.icon} +
+
+

+ {vehicle.nickname || `${vehicle.year} ${vehicle.make} ${vehicle.model}`} +

+

+ {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 new file mode 100644 index 0000000..de61bc9 --- /dev/null +++ b/frontend/src/features/dashboard/hooks/useDashboardData.ts @@ -0,0 +1,141 @@ +/** + * @ai-summary React Query hooks for dashboard data + */ + +import { useQuery } from '@tanstack/react-query'; +import { useAuth0 } from '@auth0/auth0-react'; +import { vehiclesApi } from '../../vehicles/api/vehicles.api'; +import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api'; +import { maintenanceApi } from '../../maintenance/api/maintenance.api'; +import { DashboardSummary, VehicleNeedingAttention } from '../types'; + +/** + * Hook to fetch dashboard summary stats + */ +export const useDashboardSummary = () => { + const { isAuthenticated, isLoading: authLoading } = useAuth0(); + + return useQuery({ + queryKey: ['dashboard', 'summary'], + queryFn: async (): Promise => { + // Fetch all required data in parallel + const [vehicles, fuelLogs] = await Promise.all([ + vehiclesApi.getAll(), + fuelLogsApi.getUserFuelLogs(), + ]); + + // Fetch schedules for all vehicles to count upcoming maintenance + const allSchedules = await Promise.all( + vehicles.map(v => maintenanceApi.getSchedulesByVehicle(v.id)) + ); + const flatSchedules = allSchedules.flat(); + + // Calculate upcoming maintenance (next 30 days) + const thirtyDaysFromNow = new Date(); + thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30); + + const upcomingMaintenance = flatSchedules.filter(schedule => { + // Count schedules as upcoming if they have a next due date within 30 days + if (!schedule.nextDueDate) return false; + const dueDate = new Date(schedule.nextDueDate); + return dueDate >= new Date() && dueDate <= thirtyDaysFromNow; + }); + + // Calculate recent fuel logs (last 7 days) + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + const recentFuelLogs = fuelLogs.filter(log => { + const logDate = new Date(log.dateTime); + return logDate >= sevenDaysAgo; + }); + + return { + totalVehicles: vehicles.length, + upcomingMaintenanceCount: upcomingMaintenance.length, + recentFuelLogsCount: recentFuelLogs.length, + }; + }, + enabled: isAuthenticated && !authLoading, + staleTime: 2 * 60 * 1000, // 2 minutes - fresher than other queries for dashboard + gcTime: 5 * 60 * 1000, // 5 minutes cache time + retry: (failureCount, error: any) => { + // Retry 401 errors up to 3 times for mobile auth timing issues + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] Dashboard retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + }); +}; + +/** + * Hook to fetch vehicles needing attention (overdue maintenance) + */ +export const useVehiclesNeedingAttention = () => { + const { isAuthenticated, isLoading: authLoading } = useAuth0(); + + return useQuery({ + queryKey: ['dashboard', 'vehiclesNeedingAttention'], + queryFn: async (): Promise => { + // Fetch vehicles + const vehicles = await vehiclesApi.getAll(); + + const vehiclesNeedingAttention: VehicleNeedingAttention[] = []; + const now = new Date(); + + // Check each vehicle for overdue maintenance + for (const vehicle of vehicles) { + const schedules = await maintenanceApi.getSchedulesByVehicle(vehicle.id); + + // Find overdue schedules + const overdueSchedules = schedules.filter(schedule => { + if (!schedule.nextDueDate) return false; + const dueDate = new Date(schedule.nextDueDate); + return dueDate < now; + }); + + if (overdueSchedules.length > 0) { + // Calculate priority based on how overdue the maintenance is + const mostOverdue = overdueSchedules.reduce((oldest, current) => { + const oldestDate = new Date(oldest.nextDueDate!); + const currentDate = new Date(current.nextDueDate!); + return currentDate < oldestDate ? current : oldest; + }); + + const daysOverdue = Math.floor((now.getTime() - new Date(mostOverdue.nextDueDate!).getTime()) / (1000 * 60 * 60 * 24)); + + let priority: 'high' | 'medium' | 'low' = 'low'; + if (daysOverdue > 30) { + priority = 'high'; + } else if (daysOverdue > 14) { + priority = 'medium'; + } + + vehiclesNeedingAttention.push({ + ...vehicle, + reason: `${overdueSchedules.length} overdue maintenance ${overdueSchedules.length === 1 ? 'item' : 'items'}`, + priority, + }); + } + } + + // Sort by priority (high -> medium -> low) + const priorityOrder = { high: 0, medium: 1, low: 2 }; + return vehiclesNeedingAttention.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); + }, + enabled: isAuthenticated && !authLoading, + staleTime: 2 * 60 * 1000, // 2 minutes + gcTime: 5 * 60 * 1000, // 5 minutes cache time + retry: (failureCount, error: any) => { + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] Vehicles attention retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + }); +}; diff --git a/frontend/src/features/dashboard/index.ts b/frontend/src/features/dashboard/index.ts new file mode 100644 index 0000000..ca27aaf --- /dev/null +++ b/frontend/src/features/dashboard/index.ts @@ -0,0 +1,10 @@ +/** + * @ai-summary Dashboard feature public exports + */ + +export { DashboardScreen } from './components/DashboardScreen'; +export { SummaryCards, SummaryCardsSkeleton } from './components/SummaryCards'; +export { VehicleAttention, VehicleAttentionSkeleton } from './components/VehicleAttention'; +export { QuickActions, QuickActionsSkeleton } from './components/QuickActions'; +export { useDashboardSummary, useVehiclesNeedingAttention } from './hooks/useDashboardData'; +export type { DashboardSummary, VehicleNeedingAttention, DashboardData } from './types'; diff --git a/frontend/src/features/dashboard/types/index.ts b/frontend/src/features/dashboard/types/index.ts new file mode 100644 index 0000000..2b87066 --- /dev/null +++ b/frontend/src/features/dashboard/types/index.ts @@ -0,0 +1,21 @@ +/** + * @ai-summary Dashboard feature types + */ + +import { Vehicle } from '../../vehicles/types/vehicles.types'; + +export interface DashboardSummary { + totalVehicles: number; + upcomingMaintenanceCount: number; + recentFuelLogsCount: number; +} + +export interface VehicleNeedingAttention extends Vehicle { + reason: string; + priority: 'high' | 'medium' | 'low'; +} + +export interface DashboardData { + summary: DashboardSummary; + vehiclesNeedingAttention: VehicleNeedingAttention[]; +}