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..15becc8
--- /dev/null
+++ b/.gitea/issue_template/feature.yaml
@@ -0,0 +1,137 @@
+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: 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:
+ 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
diff --git a/.gitea/workflows/staging.yaml b/.gitea/workflows/staging.yaml
index 106e88e..fec55b3 100644
--- a/.gitea/workflows/staging.yaml
+++ b/.gitea/workflows/staging.yaml
@@ -1,14 +1,16 @@
# 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
-run-name: Staging Deploy - ${{ gitea.sha }}
+run-name: "Staging - ${{ gitea.event.pull_request.title || gitea.ref_name }}"
on:
push:
branches:
- main
+ pull_request:
+ types: [opened, synchronize, reopened]
env:
REGISTRY: git.motovaultpro.com
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 05a1292..2f6c58c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -22,10 +22,12 @@ 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 })));
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 })));
@@ -78,18 +80,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 +631,16 @@ function App() {
transition={{ duration: 0.2, ease: "easeOut" }}
>
-
+ navigateToScreen('Maintenance', { source: 'dashboard-maintenance' })}
+ onAddVehicle={() => {
+ setShowAddVehicle(true);
+ navigateToScreen('Vehicles', { source: 'dashboard-add-vehicle' });
+ navigateToVehicleSubScreen('add', undefined, { source: 'dashboard-add-vehicle' });
+ }}
+ />
)}
@@ -691,6 +691,31 @@ function App() {
)}
+ {activeScreen === "Maintenance" && (
+
+
+
+
+
+
+ Loading maintenance screen...
+
+
+
+
+ }>
+
+
+
+
+ )}
{activeScreen === "Settings" && (
- } />
+ } />
+ } />
} />
} />
} />
@@ -965,7 +991,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..b4f677b 100644
--- a/frontend/src/core/store/navigation.ts
+++ b/frontend/src/core/store/navigation.ts
@@ -2,7 +2,7 @@ import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { safeStorage } from '../utils/safe-storage';
-export type MobileScreen = 'Dashboard' | 'Vehicles' | 'Log Fuel' | 'Stations' | 'Documents' | 'Settings' | 'Security' | 'AdminUsers' | 'AdminCatalog' | 'AdminCommunityStations' | 'AdminEmailTemplates' | 'AdminBackup';
+export type MobileScreen = 'Dashboard' | 'Vehicles' | 'Log Fuel' | 'Maintenance' | 'Stations' | 'Documents' | 'Settings' | 'Security' | 'AdminUsers' | 'AdminCatalog' | 'AdminCommunityStations' | 'AdminEmailTemplates' | 'AdminBackup';
export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit';
interface NavigationHistory {
@@ -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/components/DashboardScreen.tsx b/frontend/src/features/dashboard/components/DashboardScreen.tsx
new file mode 100644
index 0000000..ae7d827
--- /dev/null
+++ b/frontend/src/features/dashboard/components/DashboardScreen.tsx
@@ -0,0 +1,137 @@
+/**
+ * @ai-summary Main dashboard screen component showing fleet overview
+ */
+
+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';
+
+interface DashboardScreenProps {
+ onNavigate?: (screen: MobileScreen, metadata?: Record) => void;
+ onVehicleClick?: (vehicle: Vehicle) => void;
+ onViewMaintenance?: () => void;
+ onAddVehicle?: () => void;
+}
+
+export const DashboardScreen: React.FC = ({
+ onNavigate,
+ onVehicleClick,
+ onViewMaintenance,
+ onAddVehicle
+}) => {
+ 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={onViewMaintenance ?? (() => onNavigate?.('Vehicles'))}
+ 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..0a8b9fe
--- /dev/null
+++ b/frontend/src/features/dashboard/components/QuickActions.tsx
@@ -0,0 +1,167 @@
+/**
+ * @ai-summary Quick action buttons for common tasks
+ */
+
+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: React.ComponentType;
+ 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/SummaryCards.tsx b/frontend/src/features/dashboard/components/SummaryCards.tsx
new file mode 100644
index 0000000..eaf6ec8
--- /dev/null
+++ b/frontend/src/features/dashboard/components/SummaryCards.tsx
@@ -0,0 +1,108 @@
+/**
+ * @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';
+
+interface SummaryCardsProps {
+ summary: DashboardSummary;
+}
+
+export const SummaryCards: React.FC = ({ summary }) => {
+ const cards = [
+ {
+ title: 'Total Vehicles',
+ value: summary.totalVehicles,
+ icon: DirectionsCarRoundedIcon,
+ color: 'primary.main',
+ },
+ {
+ title: 'Upcoming Maintenance',
+ value: summary.upcomingMaintenanceCount,
+ subtitle: 'Next 30 days',
+ icon: BuildRoundedIcon,
+ color: 'primary.main',
+ },
+ {
+ title: 'Recent Fuel Logs',
+ value: summary.recentFuelLogsCount,
+ subtitle: 'Last 7 days',
+ icon: LocalGasStationRoundedIcon,
+ color: 'primary.main',
+ },
+ ];
+
+ return (
+
+ {cards.map((card) => {
+ const IconComponent = card.icon;
+ return (
+
+
+
+
+
+
+
+ {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..2ee6b67
--- /dev/null
+++ b/frontend/src/features/dashboard/components/VehicleAttention.tsx
@@ -0,0 +1,161 @@
+/**
+ * @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 { 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',
+ } : {},
+ }}
+ >
+
+
+
+
+
+
+ {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..09b2fde
--- /dev/null
+++ b/frontend/src/features/dashboard/index.ts
@@ -0,0 +1,11 @@
+/**
+ * @ai-summary Dashboard feature public exports
+ */
+
+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 { useDashboardSummary, useVehiclesNeedingAttention } from './hooks/useDashboardData';
+export type { DashboardSummary, VehicleNeedingAttention, DashboardData } from './types';
diff --git a/frontend/src/features/dashboard/pages/DashboardPage.tsx b/frontend/src/features/dashboard/pages/DashboardPage.tsx
new file mode 100644
index 0000000..39bd4d7
--- /dev/null
+++ b/frontend/src/features/dashboard/pages/DashboardPage.tsx
@@ -0,0 +1,63 @@
+/**
+ * @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}`);
+ };
+
+ const handleViewMaintenance = () => {
+ navigate('/garage/maintenance');
+ };
+
+ const handleAddVehicle = () => {
+ navigate('/garage/vehicles', { state: { showAddForm: true } });
+ };
+
+ return (
+
+
+ Dashboard
+
+
+
+ );
+};
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[];
+}
diff --git a/frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx b/frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx
new file mode 100644
index 0000000..1186481
--- /dev/null
+++ b/frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx
@@ -0,0 +1,240 @@
+/**
+ * @ai-summary Mobile maintenance screen with tabs for records and schedules
+ */
+
+import React, { useState } from 'react';
+import { useQueryClient } from '@tanstack/react-query';
+import { Box, Tabs, Tab } from '@mui/material';
+import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
+import { Button } from '../../../shared-minimal/components/Button';
+import { useMaintenanceRecords } from '../hooks/useMaintenanceRecords';
+import { MaintenanceRecordForm } from '../components/MaintenanceRecordForm';
+import { MaintenanceRecordsList } from '../components/MaintenanceRecordsList';
+import { MaintenanceRecordEditDialog } from '../components/MaintenanceRecordEditDialog';
+import { MaintenanceScheduleForm } from '../components/MaintenanceScheduleForm';
+import { MaintenanceSchedulesList } from '../components/MaintenanceSchedulesList';
+import { MaintenanceScheduleEditDialog } from '../components/MaintenanceScheduleEditDialog';
+import type { MaintenanceRecordResponse, UpdateMaintenanceRecordRequest, MaintenanceScheduleResponse, UpdateScheduleRequest } from '../types/maintenance.types';
+
+export const MaintenanceMobileScreen: React.FC = () => {
+ 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;
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(() => {