feat: add recent activity feed to dashboard (refs #166)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-13 19:48:06 -06:00
parent accb0533c6
commit f2b20aab1a
5 changed files with 179 additions and 5 deletions

View File

@@ -10,7 +10,8 @@ import CloseIcon from '@mui/icons-material/Close';
import { SummaryCards, SummaryCardsSkeleton } from './SummaryCards';
import { VehicleAttention, VehicleAttentionSkeleton } from './VehicleAttention';
import { QuickActions, QuickActionsSkeleton } from './QuickActions';
import { useDashboardSummary, useVehiclesNeedingAttention } from '../hooks/useDashboardData';
import { RecentActivity, RecentActivitySkeleton } from './RecentActivity';
import { useDashboardSummary, useVehiclesNeedingAttention, useRecentActivity } from '../hooks/useDashboardData';
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
import { Button } from '../../../shared-minimal/components/Button';
import { PendingAssociationBanner } from '../../email-ingestion/components/PendingAssociationBanner';
@@ -37,6 +38,7 @@ export const DashboardScreen: React.FC<DashboardScreenProps> = ({
const [showPendingReceipts, setShowPendingReceipts] = useState(false);
const { data: summary, isLoading: summaryLoading, error: summaryError } = useDashboardSummary();
const { data: vehiclesNeedingAttention, isLoading: attentionLoading, error: attentionError } = useVehiclesNeedingAttention();
const { data: recentActivity } = useRecentActivity();
// Error state
if (summaryError || attentionError) {
@@ -72,6 +74,7 @@ export const DashboardScreen: React.FC<DashboardScreenProps> = ({
<div className="space-y-6">
<SummaryCardsSkeleton />
<VehicleAttentionSkeleton />
<RecentActivitySkeleton />
<QuickActionsSkeleton />
</div>
);
@@ -127,6 +130,9 @@ export const DashboardScreen: React.FC<DashboardScreenProps> = ({
/>
)}
{/* Recent Activity */}
{recentActivity && <RecentActivity items={recentActivity} />}
{/* Quick Actions */}
<QuickActions
onAddVehicle={onAddVehicle ?? (() => onNavigate?.('Vehicles'))}

View File

@@ -0,0 +1,118 @@
/**
* @ai-summary Recent activity feed showing latest fuel logs and maintenance events
*/
import React from 'react';
import { Box } from '@mui/material';
import LocalGasStationRoundedIcon from '@mui/icons-material/LocalGasStationRounded';
import BuildRoundedIcon from '@mui/icons-material/BuildRounded';
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
import { RecentActivityItem } from '../types';
interface RecentActivityProps {
items: RecentActivityItem[];
}
const formatRelativeTime = (timestamp: string): string => {
const now = new Date();
const date = new Date(timestamp);
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffDays < 0) {
// Future date (upcoming maintenance)
const absDays = Math.abs(diffDays);
if (absDays === 0) return 'Today';
if (absDays === 1) return 'Tomorrow';
return `In ${absDays} days`;
}
if (diffDays === 0) {
if (diffHours === 0) return diffMins <= 1 ? 'Just now' : `${diffMins}m ago`;
return `${diffHours}h ago`;
}
if (diffDays === 1) return 'Yesterday';
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
};
export const RecentActivity: React.FC<RecentActivityProps> = ({ items }) => {
if (items.length === 0) {
return (
<GlassCard padding="md">
<h3 className="text-base font-semibold text-slate-800 dark:text-avus mb-3">
Recent Activity
</h3>
<p className="text-sm text-slate-400 dark:text-canna text-center py-4">
No recent activity. Start by logging fuel or scheduling maintenance.
</p>
</GlassCard>
);
}
return (
<GlassCard padding="md">
<h3 className="text-base font-semibold text-slate-800 dark:text-avus mb-3">
Recent Activity
</h3>
<div className="space-y-1">
{items.map((item, index) => (
<div
key={`${item.type}-${item.timestamp}-${index}`}
className="flex items-start gap-3 py-2"
>
<Box
sx={{
flexShrink: 0,
width: 32,
height: 32,
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'action.hover',
}}
>
{item.type === 'fuel' ? (
<LocalGasStationRoundedIcon sx={{ fontSize: 18, color: 'primary.main' }} />
) : (
<BuildRoundedIcon sx={{ fontSize: 18, color: 'primary.main' }} />
)}
</Box>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800 dark:text-avus truncate">
{item.vehicleName}
</p>
<p className="text-xs text-slate-500 dark:text-titanio truncate">
{item.description}
</p>
</div>
<span className="text-xs text-slate-400 dark:text-canna whitespace-nowrap flex-shrink-0">
{formatRelativeTime(item.timestamp)}
</span>
</div>
))}
</div>
</GlassCard>
);
};
export const RecentActivitySkeleton: React.FC = () => {
return (
<GlassCard padding="md">
<div className="h-5 bg-slate-100 dark:bg-slate-800 rounded animate-pulse w-32 mb-3" />
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-start gap-3">
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-slate-100 dark:bg-slate-800 animate-pulse" />
<div className="flex-1 space-y-1.5">
<div className="h-4 bg-slate-100 dark:bg-slate-800 rounded animate-pulse w-24" />
<div className="h-3 bg-slate-100 dark:bg-slate-800 rounded animate-pulse w-40" />
</div>
</div>
))}
</div>
</GlassCard>
);
};