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 01/11] 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[]; +} From 903c6acd26da7cbb9b2af135d9107f38fcea584c Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:20:38 -0600 Subject: [PATCH 02/11] chore: Update pipeline to deploy on all commits --- .gitea/workflows/staging.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/staging.yaml b/.gitea/workflows/staging.yaml index 106e88e..0630bfb 100644 --- a/.gitea/workflows/staging.yaml +++ b/.gitea/workflows/staging.yaml @@ -1,5 +1,5 @@ # MotoVaultPro Staging Deployment Workflow -# Triggers on push to main, builds and deploys to staging.motovaultpro.com +# Triggers on push to main or any pull request, builds and deploys to staging.motovaultpro.com # After verification, sends notification with link to trigger production deploy name: Deploy to Staging @@ -9,6 +9,8 @@ on: push: branches: - main + pull_request: + types: [opened, synchronize, reopened] env: REGISTRY: git.motovaultpro.com From 82ad40769745c48c6cd730a8a997cb657767ab1e Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:37:24 -0600 Subject: [PATCH 03/11] fix: add dashboard to navigation and set as default landing page (refs #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Dashboard to desktop sidebar navigation (first item) - Add /garage/dashboard route for desktop - Change default redirect from /garage/vehicles to /garage/dashboard - Change mobile default screen from Vehicles to Dashboard - Create DashboardPage wrapper for desktop route 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/App.tsx | 6 ++- frontend/src/components/Layout.tsx | 2 + frontend/src/core/store/navigation.ts | 2 +- frontend/src/features/dashboard/index.ts | 1 + .../dashboard/pages/DashboardPage.tsx | 53 +++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 frontend/src/features/dashboard/pages/DashboardPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a431fba..06d6ad4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,6 +22,7 @@ const DocumentsPage = lazy(() => import('./features/documents/pages/DocumentsPag const DocumentDetailPage = lazy(() => import('./features/documents/pages/DocumentDetailPage').then(m => ({ default: m.DocumentDetailPage }))); const MaintenancePage = lazy(() => import('./features/maintenance/pages/MaintenancePage').then(m => ({ default: m.MaintenancePage }))); const StationsPage = lazy(() => import('./features/stations/pages/StationsPage').then(m => ({ default: m.StationsPage }))); +const DashboardPage = lazy(() => import('./features/dashboard/pages/DashboardPage').then(m => ({ default: m.DashboardPage }))); const StationsMobileScreen = lazy(() => import('./features/stations/mobile/StationsMobileScreen').then(m => ({ default: m.default }))); const VehiclesMobileScreen = lazy(() => import('./features/vehicles/mobile/VehiclesMobileScreen').then(m => ({ default: m.VehiclesMobileScreen }))); const VehicleDetailMobile = lazy(() => import('./features/vehicles/mobile/VehicleDetailMobile').then(m => ({ default: m.VehicleDetailMobile }))); @@ -942,7 +943,8 @@ function App() { - } /> + } /> + } /> } /> } /> } /> @@ -957,7 +959,7 @@ function App() { } /> } /> } /> - } /> + } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 42c3d53..2dd2505 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { useAuth0 } from '@auth0/auth0-react'; import { Link, useLocation } from 'react-router-dom'; import { Container, Paper, Typography, Box, IconButton, Avatar } from '@mui/material'; +import HomeRoundedIcon from '@mui/icons-material/HomeRounded'; import DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded'; import LocalGasStationRoundedIcon from '@mui/icons-material/LocalGasStationRounded'; import BuildRoundedIcon from '@mui/icons-material/BuildRounded'; @@ -40,6 +41,7 @@ export const Layout: React.FC = ({ children, mobileMode = false }) }, [mobileMode, setSidebarOpen]); // Removed sidebarOpen from dependencies const navigation = [ + { name: 'Dashboard', href: '/garage/dashboard', icon: }, { name: 'Vehicles', href: '/garage/vehicles', icon: }, { name: 'Fuel Logs', href: '/garage/fuel-logs', icon: }, { name: 'Maintenance', href: '/garage/maintenance', icon: }, diff --git a/frontend/src/core/store/navigation.ts b/frontend/src/core/store/navigation.ts index 9b4c1d3..89e6327 100644 --- a/frontend/src/core/store/navigation.ts +++ b/frontend/src/core/store/navigation.ts @@ -51,7 +51,7 @@ export const useNavigationStore = create()( persist( (set, get) => ({ // Initial state - activeScreen: 'Vehicles', + activeScreen: 'Dashboard', vehicleSubScreen: 'list', selectedVehicleId: null, navigationHistory: [], diff --git a/frontend/src/features/dashboard/index.ts b/frontend/src/features/dashboard/index.ts index ca27aaf..09b2fde 100644 --- a/frontend/src/features/dashboard/index.ts +++ b/frontend/src/features/dashboard/index.ts @@ -3,6 +3,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'; diff --git a/frontend/src/features/dashboard/pages/DashboardPage.tsx b/frontend/src/features/dashboard/pages/DashboardPage.tsx new file mode 100644 index 0000000..29c9451 --- /dev/null +++ b/frontend/src/features/dashboard/pages/DashboardPage.tsx @@ -0,0 +1,53 @@ +/** + * @ai-summary Desktop dashboard page wrapping the DashboardScreen component + */ + +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Box, Typography } from '@mui/material'; +import { DashboardScreen } from '../components/DashboardScreen'; +import { MobileScreen } from '../../../core/store'; +import { Vehicle } from '../../vehicles/types/vehicles.types'; + +export const DashboardPage: React.FC = () => { + const navigate = useNavigate(); + + // Map mobile screen names to desktop routes + const handleNavigate = (screen: MobileScreen) => { + switch (screen) { + case 'Vehicles': + navigate('/garage/vehicles'); + break; + case 'Log Fuel': + navigate('/garage/fuel-logs'); + break; + case 'Stations': + navigate('/garage/stations'); + break; + case 'Documents': + navigate('/garage/documents'); + break; + case 'Settings': + navigate('/garage/settings'); + break; + default: + navigate('/garage/dashboard'); + } + }; + + const handleVehicleClick = (vehicle: Vehicle) => { + navigate(`/garage/vehicles/${vehicle.id}`); + }; + + return ( + + + Dashboard + + + + ); +}; From 7c8c80b6f40955110368bc6165cca4977b5a2dad Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:45:08 -0600 Subject: [PATCH 04/11] chore: add issue templates with integration criteria (refs #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Gitea issue templates to prevent missed integration points: - feature.yaml: Includes Integration Criteria section for navigation, routing, and state management requirements - bug.yaml: Structured bug reporting with platform selection - chore.yaml: Technical debt and maintenance tasks The Integration Criteria section ensures features specify: - Desktop sidebar / mobile nav placement - Route paths and default landing page - Mobile screen type in navigation store 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitea/issue_template/bug.yaml | 85 ++++++++++++++++++++++ .gitea/issue_template/chore.yaml | 70 ++++++++++++++++++ .gitea/issue_template/feature.yaml | 109 +++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 .gitea/issue_template/bug.yaml create mode 100644 .gitea/issue_template/chore.yaml create mode 100644 .gitea/issue_template/feature.yaml diff --git a/.gitea/issue_template/bug.yaml b/.gitea/issue_template/bug.yaml new file mode 100644 index 0000000..0a41f29 --- /dev/null +++ b/.gitea/issue_template/bug.yaml @@ -0,0 +1,85 @@ +name: Bug Report +about: Report a bug or unexpected behavior +title: "[Bug]: " +labels: + - type/bug + - status/backlog +body: + - type: markdown + attributes: + value: | + ## Bug Report + Use this template to report bugs. Provide as much detail as possible to help reproduce the issue. + + - type: dropdown + id: platform + attributes: + label: Platform + description: Where did you encounter this bug? + options: + - Mobile (iOS) + - Mobile (Android) + - Desktop (Chrome) + - Desktop (Firefox) + - Desktop (Safari) + - Desktop (Edge) + - Multiple platforms + validations: + required: true + + - type: textarea + id: description + attributes: + label: Bug Description + description: What went wrong? Be specific. + placeholder: "When I click X, I expected Y to happen, but instead Z happened." + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: How can we reproduce this bug? + placeholder: | + 1. Navigate to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What should have happened? + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened? + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional Context + description: Screenshots, error messages, console logs, etc. + validations: + required: false + + - type: textarea + id: fix-hints + attributes: + label: Fix Hints (if known) + description: Any ideas on what might be causing this or how to fix it? + placeholder: | + - Might be related to: [file or component] + - Similar issue in: [other feature] + validations: + required: false diff --git a/.gitea/issue_template/chore.yaml b/.gitea/issue_template/chore.yaml new file mode 100644 index 0000000..785b99a --- /dev/null +++ b/.gitea/issue_template/chore.yaml @@ -0,0 +1,70 @@ +name: Chore / Maintenance +about: Technical debt, refactoring, dependency updates, infrastructure +title: "[Chore]: " +labels: + - type/chore + - status/backlog +body: + - type: markdown + attributes: + value: | + ## Chore / Maintenance Task + Use this template for technical debt, refactoring, dependency updates, or infrastructure work. + + - type: dropdown + id: category + attributes: + label: Category + description: What type of chore is this? + options: + - Refactoring + - Dependency update + - Performance optimization + - Technical debt cleanup + - Infrastructure / DevOps + - Testing improvements + - Documentation + - Other + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: What needs to be done and why? + placeholder: "We need to refactor X because Y..." + validations: + required: true + + - type: textarea + id: scope + attributes: + label: Scope / Files Affected + description: What parts of the codebase will be touched? + placeholder: | + - frontend/src/features/[name]/ + - backend/src/features/[name]/ + - docker-compose.yml + validations: + required: false + + - type: textarea + id: acceptance + attributes: + label: Definition of Done + description: How do we know this is complete? + placeholder: | + - [ ] All tests pass + - [ ] No new linting errors + - [ ] Performance benchmark improved by X% + validations: + required: true + + - type: textarea + id: risks + attributes: + label: Risks / Breaking Changes + description: Any potential issues or breaking changes to be aware of? + validations: + required: false diff --git a/.gitea/issue_template/feature.yaml b/.gitea/issue_template/feature.yaml new file mode 100644 index 0000000..e91f5cc --- /dev/null +++ b/.gitea/issue_template/feature.yaml @@ -0,0 +1,109 @@ +name: Feature Request +about: Propose a new feature for MotoVaultPro +title: "[Feature]: " +labels: + - type/feature + - status/backlog +body: + - type: markdown + attributes: + value: | + ## Feature Request + Use this template to propose new features. Be specific about requirements and integration points. + + - type: textarea + id: problem + attributes: + label: Problem / User Need + description: What problem does this feature solve? Who needs it and why? + placeholder: "As a [user type], I want to [goal] so that [benefit]..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: Describe the feature and how it should work + placeholder: "When the user does X, the system should Y..." + validations: + required: true + + - type: textarea + id: non-goals + attributes: + label: Non-goals / Out of Scope + description: What is explicitly NOT part of this feature? + placeholder: | + - Advanced analytics (future enhancement) + - Data export functionality + - etc. + validations: + required: false + + - type: textarea + id: acceptance-criteria + attributes: + label: Acceptance Criteria (Feature Behavior) + description: What must be true for this feature to be complete? + placeholder: | + - [ ] User can see X + - [ ] System displays Y when Z + - [ ] Works on mobile viewport (320px) with touch-friendly targets + - [ ] Works on desktop viewport (1920px) with keyboard navigation + validations: + required: true + + - type: textarea + id: integration-criteria + attributes: + label: Integration Criteria (App Flow) + description: How does this feature integrate into the app? This prevents missed navigation/routing. + value: | + ### Navigation + - [ ] Desktop sidebar: [not needed / add as item #X / replace existing] + - [ ] Mobile bottom nav: [not needed / add to left/right items] + - [ ] Mobile hamburger menu: [not needed / add to menu items] + + ### Routing + - [ ] Desktop route path: `/garage/[feature-name]` + - [ ] Is this the default landing page after login? [yes / no] + - [ ] Replaces existing placeholder/route: [none / specify what] + + ### State Management + - [ ] Mobile screen type needed in navigation store? [yes / no] + - [ ] New Zustand store needed? [yes / no] + validations: + required: true + + - type: textarea + id: implementation-notes + attributes: + label: Implementation Notes + description: Technical hints, existing patterns to follow, files to modify + placeholder: | + - Current placeholder: frontend/src/App.tsx lines X-Y + - Create new feature directory: frontend/src/features/[name]/ + - Backend APIs already exist for X, Y, Z + - Follow pattern in: frontend/src/features/vehicles/ + validations: + required: false + + - type: textarea + id: test-plan + attributes: + label: Test Plan + description: How will this feature be tested? + placeholder: | + **Unit tests:** + - Component tests for X, Y, Z + + **Integration tests:** + - Test data fetching with mocked API responses + + **Manual testing:** + - Verify mobile layout at 320px, 768px viewports + - Verify desktop layout at 1920px viewport + - Test with 0 items, 1 item, 10+ items + validations: + required: false From 7b00dc763126497e4d9fbe142bbdd0925c98c484 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:51:18 -0600 Subject: [PATCH 05/11] chore: add visual integration criteria to feature template (refs #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Visual Integration section to prevent design inconsistencies: - Icons: Must use MUI Rounded icons only, no emoji in UI - Colors: Theme colors only, no hardcoded hex, dark mode support - Components: Use existing shared components (GlassCard, Button, etc.) - Typography & Spacing: MUI variants, consistent spacing multiples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitea/issue_template/feature.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.gitea/issue_template/feature.yaml b/.gitea/issue_template/feature.yaml index e91f5cc..15becc8 100644 --- a/.gitea/issue_template/feature.yaml +++ b/.gitea/issue_template/feature.yaml @@ -76,6 +76,34 @@ body: validations: required: true + - type: textarea + id: visual-integration + attributes: + label: Visual Integration (Design Consistency) + description: Ensure the feature matches the app's visual language. Reference existing patterns. + value: | + ### Icons + - [ ] Use MUI Rounded icons only (e.g., `HomeRoundedIcon`, `DirectionsCarRoundedIcon`) + - [ ] Icon reference: Check `frontend/src/components/Layout.tsx` for existing icons + - [ ] No emoji icons in UI (text content only) + + ### Colors + - [ ] Use theme colors via MUI sx prop: `color: 'primary.main'`, `bgcolor: 'background.paper'` + - [ ] No hardcoded hex colors (use Tailwind theme classes or MUI theme) + - [ ] Dark mode support: Use `dark:` Tailwind variants or MUI `theme.applyStyles('dark', ...)` + + ### Components + - [ ] Use existing shared components: `GlassCard`, `Button`, `Input` from `shared-minimal/` + - [ ] Follow card patterns in: `frontend/src/features/vehicles/` or `frontend/src/features/fuel-logs/` + - [ ] Loading states: Use skeleton patterns from existing features + + ### Typography & Spacing + - [ ] Use MUI Typography variants: `h4`, `h5`, `body1`, `body2`, `caption` + - [ ] Use consistent spacing: `gap-4`, `space-y-4`, `p-4` (multiples of 4) + - [ ] Mobile padding: `px-5 pt-5 pb-3` pattern from Layout.tsx + validations: + required: true + - type: textarea id: implementation-notes attributes: From d3c8d377f8c56b5de0538da2367232369540c0c3 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:57:53 -0600 Subject: [PATCH 06/11] fix: replace emojis with MUI icons and use theme colors in dashboard (refs #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual consistency fixes: - Replace all emojis with MUI Rounded icons - Use theme colors (primary.main, warning.main, success.main, error.main) - Use MUI Box with sx prop for consistent styling - Use shared Button component instead of custom styled buttons - Use theme tokens for dark mode (avus, titanio, canna) Components updated: - SummaryCards: DirectionsCarRoundedIcon, BuildRoundedIcon, LocalGasStationRoundedIcon - QuickActions: MUI icons with primary.main color - VehicleAttention: ErrorRoundedIcon, WarningAmberRoundedIcon, ScheduleRoundedIcon - DashboardScreen: Proper icons for error/empty states, shared Button component 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../dashboard/components/DashboardScreen.tsx | 38 +++--- .../dashboard/components/QuickActions.tsx | 115 ++++++++++++------ .../dashboard/components/SummaryCards.tsx | 83 ++++++++----- .../dashboard/components/VehicleAttention.tsx | 102 ++++++++++------ 4 files changed, 220 insertions(+), 118 deletions(-) diff --git a/frontend/src/features/dashboard/components/DashboardScreen.tsx b/frontend/src/features/dashboard/components/DashboardScreen.tsx index efb0760..77e96ea 100644 --- a/frontend/src/features/dashboard/components/DashboardScreen.tsx +++ b/frontend/src/features/dashboard/components/DashboardScreen.tsx @@ -3,11 +3,15 @@ */ import React from 'react'; +import { Box } from '@mui/material'; +import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; +import DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded'; 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 { Button } from '../../../shared-minimal/components/Button'; import { MobileScreen } from '../../../core/store'; import { Vehicle } from '../../vehicles/types/vehicles.types'; @@ -30,20 +34,22 @@ export const DashboardScreen: React.FC = ({
-
⚠️
-

+ + + +

Unable to Load Dashboard

-

+

There was an error loading your dashboard data

- +
@@ -67,20 +73,22 @@ export const DashboardScreen: React.FC = ({
-
🚗
-

+ + + +

Welcome to MotoVaultPro

-

+

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

- +
@@ -116,7 +124,7 @@ export const DashboardScreen: React.FC = ({ {/* 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 index e8c7370..e9afc96 100644 --- a/frontend/src/features/dashboard/components/QuickActions.tsx +++ b/frontend/src/features/dashboard/components/QuickActions.tsx @@ -3,15 +3,18 @@ */ import React from 'react'; +import { Box, SvgIconProps } from '@mui/material'; +import DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded'; +import LocalGasStationRoundedIcon from '@mui/icons-material/LocalGasStationRounded'; +import BuildRoundedIcon from '@mui/icons-material/BuildRounded'; +import FormatListBulletedRoundedIcon from '@mui/icons-material/FormatListBulletedRounded'; import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; interface QuickAction { id: string; title: string; description: string; - icon: string; - color: string; - bgColor: string; + icon: React.ComponentType; onClick: () => void; } @@ -33,36 +36,28 @@ export const QuickActions: React.FC = ({ id: 'add-vehicle', title: 'Add Vehicle', description: 'Register a new vehicle', - icon: '🚗', - color: 'text-blue-600', - bgColor: 'bg-blue-50', + icon: DirectionsCarRoundedIcon, onClick: onAddVehicle, }, { id: 'log-fuel', title: 'Log Fuel', description: 'Record a fuel purchase', - icon: '⛽', - color: 'text-green-600', - bgColor: 'bg-green-50', + icon: LocalGasStationRoundedIcon, onClick: onLogFuel, }, { id: 'view-maintenance', title: 'Maintenance', description: 'View maintenance records', - icon: '🔧', - color: 'text-orange-600', - bgColor: 'bg-orange-50', + icon: BuildRoundedIcon, onClick: onViewMaintenance, }, { id: 'view-vehicles', title: 'My Vehicles', description: 'View all vehicles', - icon: '📋', - color: 'text-purple-600', - bgColor: 'bg-purple-50', + icon: FormatListBulletedRoundedIcon, onClick: onViewVehicles, }, ]; @@ -70,33 +65,79 @@ export const QuickActions: React.FC = ({ return (
-

+

Quick Actions

-

+

Common tasks and navigation

- {actions.map((action) => ( - - ))} + {actions.map((action) => { + const IconComponent = action.icon; + return ( + + + + + + + {action.title} + + + {action.description} + + + + ); + })}
); @@ -113,9 +154,9 @@ export const QuickActionsSkeleton: React.FC = () => { {[1, 2, 3, 4].map((i) => (
-
+
diff --git a/frontend/src/features/dashboard/components/SummaryCards.tsx b/frontend/src/features/dashboard/components/SummaryCards.tsx index 3e8d6ec..47bcc76 100644 --- a/frontend/src/features/dashboard/components/SummaryCards.tsx +++ b/frontend/src/features/dashboard/components/SummaryCards.tsx @@ -3,6 +3,10 @@ */ 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'; @@ -15,52 +19,71 @@ export const SummaryCards: React.FC = ({ summary }) => { { title: 'Total Vehicles', value: summary.totalVehicles, - icon: '🚗', - color: 'text-blue-600', - bgColor: 'bg-blue-50', + icon: DirectionsCarRoundedIcon, + color: 'primary.main', }, { title: 'Upcoming Maintenance', value: summary.upcomingMaintenanceCount, subtitle: 'Next 30 days', - icon: '🔧', - color: 'text-orange-600', - bgColor: 'bg-orange-50', + icon: BuildRoundedIcon, + color: 'warning.main', }, { title: 'Recent Fuel Logs', value: summary.recentFuelLogsCount, subtitle: 'Last 7 days', - icon: '⛽', - color: 'text-green-600', - bgColor: 'bg-green-50', + icon: LocalGasStationRoundedIcon, + color: 'success.main', }, ]; return (
- {cards.map((card) => ( - -
-
- {card.icon} -
-
-

- {card.title} -

-

- {card.value} -

- {card.subtitle && ( -

- {card.subtitle} + {cards.map((card) => { + const IconComponent = card.icon; + return ( + +

+ + + +
+

+ {card.title}

- )} + + {card.value} + + {card.subtitle && ( +

+ {card.subtitle} +

+ )} +
-
- - ))} + + ); + })}
); }; @@ -71,7 +94,7 @@ export const SummaryCardsSkeleton: React.FC = () => { {[1, 2, 3].map((i) => (
-
+
diff --git a/frontend/src/features/dashboard/components/VehicleAttention.tsx b/frontend/src/features/dashboard/components/VehicleAttention.tsx index 291d748..2ee6b67 100644 --- a/frontend/src/features/dashboard/components/VehicleAttention.tsx +++ b/frontend/src/features/dashboard/components/VehicleAttention.tsx @@ -3,6 +3,11 @@ */ 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 { VehicleNeedingAttention } from '../types'; @@ -16,11 +21,13 @@ export const VehicleAttention: React.FC = ({ vehicles, on return (
-
-

+ + + +

All Caught Up!

-

+

No vehicles need immediate attention

@@ -28,34 +35,28 @@ export const VehicleAttention: React.FC = ({ vehicles, on ); } - const priorityConfig = { + const priorityConfig: Record }> = { high: { - color: 'text-red-600', - bgColor: 'bg-red-50', - borderColor: 'border-red-200', - icon: '🚨', + color: 'error.main', + icon: ErrorRoundedIcon, }, medium: { - color: 'text-orange-600', - bgColor: 'bg-orange-50', - borderColor: 'border-orange-200', - icon: '⚠️', + color: 'warning.main', + icon: WarningAmberRoundedIcon, }, low: { - color: 'text-yellow-600', - bgColor: 'bg-yellow-50', - borderColor: 'border-yellow-200', - icon: '⏰', + color: 'info.main', + icon: ScheduleRoundedIcon, }, }; return (
-

+

Needs Attention

-

+

Vehicles with overdue maintenance

@@ -63,41 +64,70 @@ export const VehicleAttention: React.FC = ({ vehicles, on
{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) => { + 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', + } : {}, + }} >
-
- {config.icon} -
+ + +
-

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

-

+ +

{vehicle.reason}

-
- - {vehicle.priority.toUpperCase()} PRIORITY - -
+ + {vehicle.priority.toUpperCase()} PRIORITY +
-
+ ); })}
@@ -114,7 +144,7 @@ export const VehicleAttentionSkeleton: React.FC = () => {
{[1, 2].map((i) => ( -
+
From 927b1a41285d33089f1d33bed1852aa2af4378f6 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:07:43 -0600 Subject: [PATCH 07/11] fix: use primary color for all summary card icons (refs #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All summary cards now use primary.main for consistent branding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitea/workflows/staging.yaml | 2 +- frontend/src/features/dashboard/components/SummaryCards.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/staging.yaml b/.gitea/workflows/staging.yaml index 0630bfb..fec55b3 100644 --- a/.gitea/workflows/staging.yaml +++ b/.gitea/workflows/staging.yaml @@ -3,7 +3,7 @@ # After verification, sends notification with link to trigger production deploy name: Deploy to Staging -run-name: Staging Deploy - ${{ gitea.sha }} +run-name: "Staging - ${{ gitea.event.pull_request.title || gitea.ref_name }}" on: push: diff --git a/frontend/src/features/dashboard/components/SummaryCards.tsx b/frontend/src/features/dashboard/components/SummaryCards.tsx index 47bcc76..eaf6ec8 100644 --- a/frontend/src/features/dashboard/components/SummaryCards.tsx +++ b/frontend/src/features/dashboard/components/SummaryCards.tsx @@ -27,14 +27,14 @@ export const SummaryCards: React.FC = ({ summary }) => { value: summary.upcomingMaintenanceCount, subtitle: 'Next 30 days', icon: BuildRoundedIcon, - color: 'warning.main', + color: 'primary.main', }, { title: 'Recent Fuel Logs', value: summary.recentFuelLogsCount, subtitle: 'Last 7 days', icon: LocalGasStationRoundedIcon, - color: 'success.main', + color: 'primary.main', }, ]; From 55fb01d5bd4fe07a51ddcbc694d4f0e7eeae03ca Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:10:28 -0600 Subject: [PATCH 08/11] fix: reduce border radius on quick action buttons (refs #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed from borderRadius 3 (24px) to 1.5 (12px) for more rectangular look 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/features/dashboard/components/QuickActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/dashboard/components/QuickActions.tsx b/frontend/src/features/dashboard/components/QuickActions.tsx index e9afc96..0a8b9fe 100644 --- a/frontend/src/features/dashboard/components/QuickActions.tsx +++ b/frontend/src/features/dashboard/components/QuickActions.tsx @@ -83,7 +83,7 @@ export const QuickActions: React.FC = ({ onClick={action.onClick} sx={{ p: 2, - borderRadius: 3, + borderRadius: 1.5, bgcolor: 'action.hover', border: '1px solid transparent', display: 'flex', From 544428fca2990962b714561b4503f45c6ae6cefe Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:16:47 -0600 Subject: [PATCH 09/11] fix: maintenance button navigates to maintenance screen (refs #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add onViewMaintenance prop to DashboardScreen - Desktop: navigates to /garage/maintenance - Mobile: falls back to Vehicles (no dedicated mobile maintenance screen) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/features/dashboard/components/DashboardScreen.tsx | 6 ++++-- frontend/src/features/dashboard/pages/DashboardPage.tsx | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/dashboard/components/DashboardScreen.tsx b/frontend/src/features/dashboard/components/DashboardScreen.tsx index 77e96ea..7a8c585 100644 --- a/frontend/src/features/dashboard/components/DashboardScreen.tsx +++ b/frontend/src/features/dashboard/components/DashboardScreen.tsx @@ -19,11 +19,13 @@ import { Vehicle } from '../../vehicles/types/vehicles.types'; interface DashboardScreenProps { onNavigate?: (screen: MobileScreen, metadata?: Record) => void; onVehicleClick?: (vehicle: Vehicle) => void; + onViewMaintenance?: () => void; } export const DashboardScreen: React.FC = ({ onNavigate, - onVehicleClick + onVehicleClick, + onViewMaintenance }) => { const { data: summary, isLoading: summaryLoading, error: summaryError } = useDashboardSummary(); const { data: vehiclesNeedingAttention, isLoading: attentionLoading, error: attentionError } = useVehiclesNeedingAttention(); @@ -118,7 +120,7 @@ export const DashboardScreen: React.FC = ({ onNavigate?.('Vehicles')} onLogFuel={() => onNavigate?.('Log Fuel')} - onViewMaintenance={() => onNavigate?.('Vehicles')} // Navigate to vehicles then maintenance + onViewMaintenance={onViewMaintenance ?? (() => onNavigate?.('Vehicles'))} onViewVehicles={() => onNavigate?.('Vehicles')} /> diff --git a/frontend/src/features/dashboard/pages/DashboardPage.tsx b/frontend/src/features/dashboard/pages/DashboardPage.tsx index 29c9451..f539271 100644 --- a/frontend/src/features/dashboard/pages/DashboardPage.tsx +++ b/frontend/src/features/dashboard/pages/DashboardPage.tsx @@ -39,6 +39,10 @@ export const DashboardPage: React.FC = () => { navigate(`/garage/vehicles/${vehicle.id}`); }; + const handleViewMaintenance = () => { + navigate('/garage/maintenance'); + }; + return ( @@ -47,6 +51,7 @@ export const DashboardPage: React.FC = () => { ); From 98a4a62ea51276eb414ca7772fd721d2ca00c261 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:25:35 -0600 Subject: [PATCH 10/11] fix: add vehicle button opens add vehicle form (refs #2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add onAddVehicle prop to DashboardScreen - Mobile: triggers setShowAddVehicle(true) in App.tsx - Desktop: navigates to /garage/vehicles with showAddForm state - VehiclesPage auto-opens form when receiving showAddForm state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/App.tsx | 1 + .../dashboard/components/DashboardScreen.tsx | 8 +++++--- .../features/dashboard/pages/DashboardPage.tsx | 5 +++++ .../src/features/vehicles/pages/VehiclesPage.tsx | 15 +++++++++++++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 06d6ad4..945c56c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -633,6 +633,7 @@ function App() { setShowAddVehicle(true)} /> diff --git a/frontend/src/features/dashboard/components/DashboardScreen.tsx b/frontend/src/features/dashboard/components/DashboardScreen.tsx index 7a8c585..ae7d827 100644 --- a/frontend/src/features/dashboard/components/DashboardScreen.tsx +++ b/frontend/src/features/dashboard/components/DashboardScreen.tsx @@ -20,12 +20,14 @@ interface DashboardScreenProps { onNavigate?: (screen: MobileScreen, metadata?: Record) => void; onVehicleClick?: (vehicle: Vehicle) => void; onViewMaintenance?: () => void; + onAddVehicle?: () => void; } export const DashboardScreen: React.FC = ({ onNavigate, onVehicleClick, - onViewMaintenance + onViewMaintenance, + onAddVehicle }) => { const { data: summary, isLoading: summaryLoading, error: summaryError } = useDashboardSummary(); const { data: vehiclesNeedingAttention, isLoading: attentionLoading, error: attentionError } = useVehiclesNeedingAttention(); @@ -87,7 +89,7 @@ export const DashboardScreen: React.FC = ({ @@ -118,7 +120,7 @@ export const DashboardScreen: React.FC = ({ {/* Quick Actions */} onNavigate?.('Vehicles')} + onAddVehicle={onAddVehicle ?? (() => onNavigate?.('Vehicles'))} onLogFuel={() => onNavigate?.('Log Fuel')} onViewMaintenance={onViewMaintenance ?? (() => onNavigate?.('Vehicles'))} onViewVehicles={() => onNavigate?.('Vehicles')} diff --git a/frontend/src/features/dashboard/pages/DashboardPage.tsx b/frontend/src/features/dashboard/pages/DashboardPage.tsx index f539271..39bd4d7 100644 --- a/frontend/src/features/dashboard/pages/DashboardPage.tsx +++ b/frontend/src/features/dashboard/pages/DashboardPage.tsx @@ -43,6 +43,10 @@ export const DashboardPage: React.FC = () => { navigate('/garage/maintenance'); }; + const handleAddVehicle = () => { + navigate('/garage/vehicles', { state: { showAddForm: true } }); + }; + return ( @@ -52,6 +56,7 @@ export const DashboardPage: React.FC = () => { onNavigate={handleNavigate} onVehicleClick={handleVehicleClick} onViewMaintenance={handleViewMaintenance} + onAddVehicle={handleAddVehicle} /> ); diff --git a/frontend/src/features/vehicles/pages/VehiclesPage.tsx b/frontend/src/features/vehicles/pages/VehiclesPage.tsx index c060903..f0873d4 100644 --- a/frontend/src/features/vehicles/pages/VehiclesPage.tsx +++ b/frontend/src/features/vehicles/pages/VehiclesPage.tsx @@ -3,7 +3,7 @@ * @ai-context Enhanced with Suspense, useOptimistic, and useTransition */ -import React, { useState, useTransition, useMemo } from 'react'; +import React, { useState, useTransition, useMemo, useEffect } from 'react'; import { Box, Typography, Grid, Button as MuiButton, TextField, IconButton } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import SearchIcon from '@mui/icons-material/Search'; @@ -16,12 +16,13 @@ import { VehicleForm } from '../components/VehicleForm'; import { Card } from '../../../shared-minimal/components/Card'; import { VehicleListSuspense, FormSuspense } from '../../../components/SuspenseWrappers'; import { useAppStore } from '../../../core/store'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { vehiclesApi } from '../api/vehicles.api'; export const VehiclesPage: React.FC = () => { const navigate = useNavigate(); + const location = useLocation(); const queryClient = useQueryClient(); const { data: vehicles, isLoading } = useVehicles(); const setSelectedVehicle = useAppStore(state => state.setSelectedVehicle); @@ -52,6 +53,16 @@ export const VehiclesPage: React.FC = () => { const [showForm, setShowForm] = useState(false); const [stagedImageFile, setStagedImageFile] = useState(null); + // Auto-show form if navigated with showAddForm state (from dashboard) + useEffect(() => { + const state = location.state as { showAddForm?: boolean } | null; + if (state?.showAddForm) { + setShowForm(true); + // Clear the state to prevent re-triggering on refresh + navigate(location.pathname, { replace: true, state: {} }); + } + }, [location.state, location.pathname, navigate]); + const handleSelectVehicle = (id: string) => { // Use transition for navigation to avoid blocking UI startTransition(() => { From 2059afaaeff662b69e03f7bfed5791c7a8612603 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:40:42 -0600 Subject: [PATCH 11/11] fix: mobile dashboard navigation for Add Vehicle and Maintenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Vehicle now navigates to Vehicles screen and opens add form - Add Maintenance mobile screen with records/schedules tabs - Add 'Maintenance' to MobileScreen type - Wire up onViewMaintenance callback to navigate to Maintenance screen refs #2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/App.tsx | 33 ++- frontend/src/core/store/navigation.ts | 2 +- .../mobile/MaintenanceMobileScreen.tsx | 240 ++++++++++++++++++ 3 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 945c56c..2f6c58c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -27,6 +27,7 @@ const StationsMobileScreen = lazy(() => import('./features/stations/mobile/Stati const VehiclesMobileScreen = lazy(() => import('./features/vehicles/mobile/VehiclesMobileScreen').then(m => ({ default: m.VehiclesMobileScreen }))); const VehicleDetailMobile = lazy(() => import('./features/vehicles/mobile/VehicleDetailMobile').then(m => ({ default: m.VehicleDetailMobile }))); const DocumentsMobileScreen = lazy(() => import('./features/documents/mobile/DocumentsMobileScreen')); +const MaintenanceMobileScreen = lazy(() => import('./features/maintenance/mobile/MaintenanceMobileScreen')); // Admin pages (lazy-loaded) const AdminUsersPage = lazy(() => import('./pages/admin/AdminUsersPage').then(m => ({ default: m.AdminUsersPage }))); @@ -633,7 +634,12 @@ function App() { setShowAddVehicle(true)} + onViewMaintenance={() => navigateToScreen('Maintenance', { source: 'dashboard-maintenance' })} + onAddVehicle={() => { + setShowAddVehicle(true); + navigateToScreen('Vehicles', { source: 'dashboard-add-vehicle' }); + navigateToVehicleSubScreen('add', undefined, { source: 'dashboard-add-vehicle' }); + }} /> @@ -685,6 +691,31 @@ function App() { )} + {activeScreen === "Maintenance" && ( + + + + +
+
+ Loading maintenance screen... +
+
+
+
+ }> + + + + + )} {activeScreen === "Settings" && ( { + const queryClient = useQueryClient(); + const { records, schedules, isRecordsLoading, isSchedulesLoading, recordsError, schedulesError, updateRecord, deleteRecord, updateSchedule, deleteSchedule } = useMaintenanceRecords(); + + const [activeTab, setActiveTab] = useState<'records' | 'schedules'>('records'); + const [showForm, setShowForm] = useState(false); + const [editingRecord, setEditingRecord] = useState(null); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [editingSchedule, setEditingSchedule] = useState(null); + const [scheduleEditDialogOpen, setScheduleEditDialogOpen] = useState(false); + + const handleEdit = (record: MaintenanceRecordResponse) => { + setEditingRecord(record); + setEditDialogOpen(true); + }; + + const handleEditSave = async (id: string, data: UpdateMaintenanceRecordRequest) => { + try { + await updateRecord({ id, data }); + queryClient.refetchQueries({ queryKey: ['maintenanceRecords'] }); + setEditDialogOpen(false); + setEditingRecord(null); + } catch (error) { + console.error('Failed to update maintenance record:', error); + throw error; + } + }; + + const handleEditClose = () => { + setEditDialogOpen(false); + setEditingRecord(null); + }; + + const handleDelete = async (recordId: string) => { + try { + await deleteRecord(recordId); + queryClient.refetchQueries({ queryKey: ['maintenanceRecords', 'all'] }); + } catch (error) { + console.error('Failed to delete maintenance record:', error); + } + }; + + const handleScheduleEdit = (schedule: MaintenanceScheduleResponse) => { + setEditingSchedule(schedule); + setScheduleEditDialogOpen(true); + }; + + const handleScheduleEditSave = async (id: string, data: UpdateScheduleRequest) => { + try { + await updateSchedule({ id, data }); + queryClient.refetchQueries({ queryKey: ['maintenanceSchedules'] }); + setScheduleEditDialogOpen(false); + setEditingSchedule(null); + } catch (error) { + console.error('Failed to update maintenance schedule:', error); + throw error; + } + }; + + const handleScheduleEditClose = () => { + setScheduleEditDialogOpen(false); + setEditingSchedule(null); + }; + + const handleScheduleDelete = async (scheduleId: string) => { + try { + await deleteSchedule(scheduleId); + queryClient.refetchQueries({ queryKey: ['maintenanceSchedules'] }); + } catch (error) { + console.error('Failed to delete maintenance schedule:', error); + } + }; + + const isLoading = activeTab === 'records' ? isRecordsLoading : isSchedulesLoading; + const hasError = activeTab === 'records' ? recordsError : schedulesError; + + return ( +
+ {/* Header */} + +
+

Maintenance

+ + {/* Tabs */} + + { + setActiveTab(v as 'records' | 'schedules'); + setShowForm(false); + }} + aria-label="Maintenance tabs" + sx={{ + minHeight: 40, + '& .MuiTab-root': { + minHeight: 40, + fontSize: '0.875rem', + textTransform: 'none', + }, + }} + > + + + + + + {/* Add Button */} +
+ +
+ + {/* Loading State */} + {isLoading && ( +
+ Loading maintenance {activeTab}... +
+ )} + + {/* Error State */} + {hasError && ( +
+

Failed to load maintenance {activeTab}

+ +
+ )} +
+
+ + {/* Form */} + {showForm && !isLoading && !hasError && ( + +
+

+ {activeTab === 'records' ? 'New Maintenance Record' : 'New Maintenance Schedule'} +

+ {activeTab === 'records' ? ( + + ) : ( + + )} +
+
+ )} + + {/* Records List */} + {activeTab === 'records' && !isLoading && !hasError && ( + +
+

+ Recent Records +

+ {records && records.length === 0 ? ( +
+

No maintenance records yet

+

+ Add a record to track your vehicle maintenance +

+
+ ) : ( + + )} +
+
+ )} + + {/* Schedules List */} + {activeTab === 'schedules' && !isLoading && !hasError && ( + +
+

+ Maintenance Schedules +

+ {schedules && schedules.length === 0 ? ( +
+

No schedules yet

+

+ Create a schedule to get maintenance reminders +

+
+ ) : ( + + )} +
+
+ )} + + {/* Edit Dialogs */} + + +
+ ); +}; + +export default MaintenanceMobileScreen;