Files
vibn-frontend/app/[workspace]/project/[projectId]/plan/page.tsx

769 lines
33 KiB
TypeScript

'use client';
import { use, useState, useEffect, useCallback } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Loader2, CheckCircle2, Circle, Clock, RefreshCw, Eye, Cog, GitBranch, ChevronDown, ChevronRight } from 'lucide-react';
import { CollapsibleSidebar } from '@/components/ui/collapsible-sidebar';
interface WorkItem {
id: string;
title: string;
category: string;
path: string;
status: 'built' | 'missing' | 'in_progress';
priority: string;
assigned?: string;
startDate: string | null;
endDate: string | null;
duration: number;
sessionsCount: number;
commitsCount: number;
totalActivity: number;
estimatedCost?: number;
requirements: Array<{
id: number;
text: string;
status: 'built' | 'missing' | 'in_progress';
}>;
evidence: string[];
note?: string;
}
interface TimelineData {
workItems: WorkItem[];
timeline: {
start: string;
end: string;
totalDays: number;
};
summary: {
totalWorkItems: number;
withActivity: number;
noActivity: number;
built: number;
missing: number;
};
projectCreator?: string;
}
export default function TimelinePlanPage({
params,
}: {
params: Promise<{ workspace: string; projectId: string }>;
}) {
const { projectId } = use(params);
const [data, setData] = useState<TimelineData | null>(null);
const [loading, setLoading] = useState(true);
const [regenerating, setRegenerating] = useState(false);
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const [viewMode, setViewMode] = useState<'touchpoints' | 'technical' | 'journey'>('touchpoints');
const [collapsedJourneySections, setCollapsedJourneySections] = useState<Set<string>>(new Set());
// Map work items to types based on path and category
const getWorkItemType = (item: WorkItem): string => {
// API endpoints are System
if (item.path.startsWith('/api/')) return 'System';
// Flows are Flow
if (item.path.startsWith('flow/')) return 'Flow';
// Auth/OAuth is System
if (item.path.includes('auth') || item.path.includes('oauth')) return 'System';
// Settings is System
if (item.path.includes('settings')) return 'System';
// Marketing/Content pages
if (item.category === 'Marketing' || item.category === 'Content') return 'Screen';
// Social
if (item.category === 'Social') return 'Screen';
// Everything else is a Screen
return 'Screen';
};
// Determine if item is a user-facing touchpoint
const isTouchpoint = (item: WorkItem): boolean => {
const path = item.path.toLowerCase();
const title = item.title.toLowerCase();
// Exclude APIs and backend systems
if (path.startsWith('/api/')) return false;
if (title.includes(' api') || title.includes('api ')) return false;
// Exclude pure auth infrastructure (OAuth endpoints)
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return false;
// Include everything else - screens, pages, social posts, blogs, invites, etc.
return true;
};
// Determine if item is technical infrastructure
const isTechnical = (item: WorkItem): boolean => {
const path = item.path.toLowerCase();
const title = item.title.toLowerCase();
// APIs and backend
if (path.startsWith('/api/')) return true;
if (title.includes(' api') || title.includes('api ')) return true;
// Auth infrastructure
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return true;
// System settings
if (item.category === 'Settings' && title.includes('api')) return true;
return false;
};
// Map work items to customer lifecycle journey sections
const getJourneySection = (item: WorkItem): string => {
const title = item.title.toLowerCase();
const path = item.path.toLowerCase();
// Discovery - "I just found you online via social post, blog article, advertisement"
if (path === '/' || title.includes('landing') || title.includes('marketing page')) return 'Discovery';
if (item.category === 'Social' && !path.includes('settings')) return 'Discovery';
if (item.category === 'Content' && (title.includes('blog') || title.includes('article'))) return 'Discovery';
// Research - "Checking out your marketing website - features, price, home page"
if (title.includes('marketing dashboard')) return 'Research';
if (item.category === 'Marketing' && path !== '/') return 'Research';
if (path.includes('/features') || path.includes('/pricing') || path.includes('/about')) return 'Research';
if (item.category === 'Content' && path.includes('/docs') && !title.includes('getting started')) return 'Research';
// Onboarding - "Creating an account to try the product for the first time"
if (path.includes('auth') || path.includes('oauth')) return 'Onboarding';
if (path.includes('signup') || path.includes('signin') || path.includes('login')) return 'Onboarding';
if (title.includes('authentication') && !title.includes('api')) return 'Onboarding';
// First Use - "Zero state to experiencing the magic solution"
if (title.includes('onboarding')) return 'First Use';
if (title.includes('getting started')) return 'First Use';
if (path.includes('workspace') && !path.includes('settings')) return 'First Use';
if (title.includes('creation flow') || title.includes('project creation')) return 'First Use';
if (path.includes('/projects') && path.match(/\/projects\/?$/)) return 'First Use'; // Projects list page
// Active - "I've seen the magic and come back to use it again and again"
if (path.includes('overview') || path.includes('/dashboard')) return 'Active';
if (path.includes('timeline-plan') || path.includes('audit') || path.includes('mission')) return 'Active';
if (path.includes('/api/projects') || path.includes('mvp-checklist')) return 'Active';
if (title.includes('plan generation') || title.includes('marketing plan')) return 'Active';
if (path.includes('projects/') && path.length > '/projects/'.length) return 'Active'; // Specific project pages
// Support - "I've got questions, need quick answers to get back to the magic"
if (path.includes('settings')) return 'Support';
if (path.includes('/help') || path.includes('/faq') || path.includes('/support')) return 'Support';
if (item.category === 'Content' && path.includes('/docs') && title.includes('help')) return 'Support';
// Purchase - "Time to pay so I can keep using the magic"
if (path.includes('billing') || path.includes('payment') || path.includes('subscription')) return 'Purchase';
if (path.includes('upgrade') || path.includes('checkout') || path.includes('pricing/buy')) return 'Purchase';
// Default to Active for core product features
return 'Active';
};
const toggleJourneySection = (sectionId: string) => {
setCollapsedJourneySections(prev => {
const newSet = new Set(prev);
if (newSet.has(sectionId)) {
newSet.delete(sectionId);
} else {
newSet.add(sectionId);
}
return newSet;
});
};
// Get emoji icon for journey section
const getJourneySectionIcon = (section: string): string => {
const icons: Record<string, string> = {
'Discovery': '🔍',
'Research': '📚',
'Onboarding': '🎯',
'First Use': '🚀',
'Active': '⚡',
'Support': '💡',
'Purchase': '💳'
};
return icons[section] || '📋';
};
// Get phase status based on overall item status
const getPhaseStatus = (itemStatus: string, phase: 'scope' | 'design' | 'code'): 'built' | 'in_progress' | 'missing' => {
if (itemStatus === 'built') return 'built';
if (itemStatus === 'missing') return 'missing';
// If in_progress, show progression through phases
if (phase === 'scope') return 'built';
if (phase === 'design') return 'in_progress';
return 'missing';
};
// Render status badge
const renderStatusBadge = (status: 'built' | 'in_progress' | 'missing') => {
if (status === 'built') {
return (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-100 text-green-800 text-xs font-medium">
<CheckCircle2 className="h-3 w-3" />
Done
</span>
);
}
if (status === 'in_progress') {
return (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-blue-100 text-blue-800 text-xs font-medium">
<Clock className="h-3 w-3" />
Started
</span>
);
}
return (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-gray-100 text-gray-800 text-xs font-medium">
<Circle className="h-3 w-3" />
To-do
</span>
);
};
const loadTimelineData = useCallback(async () => {
try {
setLoading(true);
const response = await fetch(`/api/projects/${projectId}/timeline-view`);
const result = await response.json();
// Check if the response is an error
if (result.error) {
console.error('API Error:', result.error, result.details);
alert(`Failed to load timeline: ${result.details || result.error}`);
return;
}
setData(result);
} catch (error) {
console.error('Error loading timeline:', error);
alert('Failed to load timeline data. Check console for details.');
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
loadTimelineData();
}, [loadTimelineData]);
const regeneratePlan = async () => {
if (!confirm('Regenerate the plan? This will analyze your project and create a fresh MVP checklist.')) {
return;
}
try {
setRegenerating(true);
const response = await fetch(`/api/projects/${projectId}/mvp-checklist`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to regenerate plan');
}
// Reload the timeline data
await loadTimelineData();
} catch (error) {
console.error('Error regenerating plan:', error);
alert('Failed to regenerate plan. Check console for details.');
} finally {
setRegenerating(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (!data) {
return <div className="p-8 text-center text-muted-foreground">No timeline data available</div>;
}
return (
<div className="relative h-full w-full bg-background overflow-hidden flex">
{/* Left Sidebar */}
<CollapsibleSidebar>
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold mb-2">Quick Stats</h3>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Total Items</span>
<span className="font-medium">{data.summary.totalWorkItems}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Built</span>
<span className="font-medium text-green-600">{data.summary.built}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">In Progress</span>
<span className="font-medium text-blue-600">{data.summary.withActivity - data.summary.built}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">To Build</span>
<span className="font-medium text-gray-600">{data.summary.missing}</span>
</div>
</div>
</div>
</div>
</CollapsibleSidebar>
{/* Main Content */}
<div className="flex-1 flex flex-col p-4 space-y-3 overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold">MVP Checklist</h1>
<p className="text-sm text-muted-foreground mt-0.5">
{data.summary.built} of {data.summary.totalWorkItems} pages built
{data.summary.withActivity} with development activity
</p>
</div>
<div className="flex gap-3 items-center">
{/* View Mode Switcher */}
<div className="flex items-center border rounded-lg p-1">
<Button
variant={viewMode === 'touchpoints' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setViewMode('touchpoints')}
className="gap-2 h-7"
>
<Eye className="h-4 w-4" />
Touchpoints
</Button>
<Button
variant={viewMode === 'technical' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setViewMode('technical')}
className="gap-2 h-7"
>
<Cog className="h-4 w-4" />
Technical
</Button>
<Button
variant={viewMode === 'journey' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setViewMode('journey')}
className="gap-2 h-7"
>
<GitBranch className="h-4 w-4" />
Journey
</Button>
</div>
{/* Regenerate Button */}
<Button
variant="outline"
size="sm"
onClick={regeneratePlan}
disabled={regenerating}
className="gap-2"
>
{regenerating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Regenerating...
</>
) : (
<>
<RefreshCw className="h-4 w-4" />
Regenerate Plan
</>
)}
</Button>
{/* Summary Stats */}
<div className="text-xs px-3 py-1 bg-green-100 text-green-800 rounded">
{data.summary.built} Built
</div>
<div className="text-xs px-3 py-1 bg-gray-100 text-gray-800 rounded">
{data.summary.missing} To Build
</div>
</div>
</div>
{/* Touchpoints View - What users see and engage with */}
{viewMode === 'touchpoints' && (
<Card className="flex-1 overflow-hidden flex flex-col p-0">
<div className="p-4 border-b bg-muted/30">
<p className="text-sm text-muted-foreground">Everything users see and engage with - screens, features, social posts, blogs, invites, and all customer-facing elements.</p>
</div>
<div className="overflow-auto">
<table className="w-full">
<thead className="bg-muted/50 border-b sticky top-0">
<tr>
<th className="text-left px-4 py-3 text-sm font-semibold">Type</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Touchpoint</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Scope</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Design</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Code</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Assigned</th>
<th className="text-center px-4 py-3 text-sm font-semibold">Sessions</th>
<th className="text-center px-4 py-3 text-sm font-semibold">Commits</th>
<th className="text-right px-4 py-3 text-sm font-semibold">Cost</th>
</tr>
</thead>
<tbody className="divide-y">
{data?.workItems.filter(item => isTouchpoint(item)).map((item, index) => (
<tr
key={item.id}
className="hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => {
setExpandedItems(prev => {
const newSet = new Set(prev);
if (newSet.has(item.id)) {
newSet.delete(item.id);
} else {
newSet.add(item.id);
}
return newSet;
});
}}
>
<td className="px-4 py-3 text-sm text-muted-foreground">
{getWorkItemType(item)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
{item.status === 'built' ? (
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
) : item.status === 'in_progress' ? (
<Clock className="h-4 w-4 text-blue-600 flex-shrink-0" />
) : (
<Circle className="h-4 w-4 text-gray-400 flex-shrink-0" />
)}
<div>
<div className="text-sm font-medium">{item.title}</div>
{expandedItems.has(item.id) && (
<div className="mt-2 space-y-1">
{item.requirements.map((req) => (
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground ml-6">
{req.status === 'built' ? (
<CheckCircle2 className="h-3 w-3 text-green-600" />
) : (
<Circle className="h-3 w-3 text-gray-400" />
)}
<span>{req.text}</span>
</div>
))}
</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
</td>
<td className="px-4 py-3">
<span className="text-sm text-muted-foreground">
{item.assigned || data?.projectCreator || 'You'}
</span>
</td>
<td className="px-4 py-3 text-sm text-center">
<span className={item.sessionsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
{item.sessionsCount}
</span>
</td>
<td className="px-4 py-3 text-sm text-center">
<span className={item.commitsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
{item.commitsCount}
</span>
</td>
<td className="px-4 py-3 text-sm text-right text-muted-foreground">
{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
{/* Journey View - Customer lifecycle stages */}
{viewMode === 'journey' && (
<Card className="flex-1 overflow-auto p-0">
<div className="p-4 border-b bg-muted/30">
<p className="text-sm text-muted-foreground">Customer lifecycle journey from discovery to purchase - organizing all touchpoints and technical components by user stage.</p>
</div>
<div className="divide-y">
{/* Journey Sections - Customer Lifecycle */}
{['Discovery', 'Research', 'Onboarding', 'First Use', 'Active', 'Support', 'Purchase'].map(sectionName => {
const sectionItems = data.workItems.filter(item => getJourneySection(item) === sectionName);
if (sectionItems.length === 0) return null;
const sectionStats = {
done: sectionItems.filter(i => i.status === 'built').length,
started: sectionItems.filter(i => i.status === 'in_progress').length,
todo: sectionItems.filter(i => i.status === 'missing').length,
total: sectionItems.length
};
const isCollapsed = collapsedJourneySections.has(sectionName);
return (
<div key={sectionName}>
{/* Section Header */}
<div
className="bg-muted/30 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-between sticky top-0 z-10"
onClick={() => toggleJourneySection(sectionName)}
>
<div className="flex items-center gap-3">
{isCollapsed ? (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
)}
<span className="text-lg">{getJourneySectionIcon(sectionName)}</span>
<h3 className="font-semibold text-base">{sectionName}</h3>
<span className="text-xs text-muted-foreground">
{sectionStats.done}/{sectionStats.total} complete
</span>
</div>
<div className="flex gap-2 text-xs">
{sectionStats.done > 0 && (
<span className="px-2 py-1 bg-green-100 text-green-800 rounded">
{sectionStats.done} done
</span>
)}
{sectionStats.started > 0 && (
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded">
{sectionStats.started} started
</span>
)}
{sectionStats.todo > 0 && (
<span className="px-2 py-1 bg-gray-100 text-gray-800 rounded">
{sectionStats.todo} to-do
</span>
)}
</div>
</div>
{/* Section Items */}
{!isCollapsed && (
<div className="divide-y">
{sectionItems.map(item => (
<div key={item.id} className="px-4 py-3 hover:bg-accent/30 transition-colors">
<div
className="flex items-start justify-between cursor-pointer"
onClick={() => {
setExpandedItems(prev => {
const newSet = new Set(prev);
if (newSet.has(item.id)) {
newSet.delete(item.id);
} else {
newSet.add(item.id);
}
return newSet;
});
}}
>
<div className="flex items-start gap-3 flex-1">
{/* Status Icon */}
{item.status === 'built' ? (
<CheckCircle2 className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5" />
) : item.status === 'in_progress' ? (
<Clock className="h-5 w-5 text-blue-600 flex-shrink-0 mt-0.5" />
) : (
<Circle className="h-5 w-5 text-gray-400 flex-shrink-0 mt-0.5" />
)}
<div className="flex-1">
{/* Title and Type */}
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{item.title}</span>
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-700 rounded">
{getWorkItemType(item)}
</span>
</div>
{/* Phase Status */}
<div className="flex gap-2 mt-2">
<div className="text-xs">
<span className="text-muted-foreground">Spec:</span>{' '}
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
</div>
<div className="text-xs">
<span className="text-muted-foreground">Design:</span>{' '}
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
</div>
<div className="text-xs">
<span className="text-muted-foreground">Code:</span>{' '}
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
</div>
</div>
{/* Expanded Requirements */}
{expandedItems.has(item.id) && (
<div className="mt-3 space-y-1 pl-4 border-l-2 border-gray-200">
<p className="text-xs font-semibold text-muted-foreground mb-2">Requirements:</p>
{item.requirements.map((req) => (
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground">
{req.status === 'built' ? (
<CheckCircle2 className="h-3 w-3 text-green-600" />
) : (
<Circle className="h-3 w-3 text-gray-400" />
)}
<span>{req.text}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Right Side Stats */}
<div className="flex items-start gap-4 text-xs text-muted-foreground">
<div className="text-center">
<div className="font-medium">Sessions</div>
<div className={item.sessionsCount > 0 ? 'text-blue-600 font-bold' : ''}>{item.sessionsCount}</div>
</div>
<div className="text-center">
<div className="font-medium">Commits</div>
<div className={item.commitsCount > 0 ? 'text-blue-600 font-bold' : ''}>{item.commitsCount}</div>
</div>
<div className="text-center min-w-[60px]">
<div className="font-medium">Cost</div>
<div>{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</Card>
)}
{/* Technical View - Infrastructure that powers everything */}
{viewMode === 'technical' && (
<Card className="flex-1 overflow-hidden flex flex-col p-0">
<div className="p-4 border-b bg-muted/30">
<p className="text-sm text-muted-foreground">Technical infrastructure that powers the product - APIs, backend services, authentication, and system integrations.</p>
</div>
<div className="overflow-auto">
<table className="w-full">
<thead className="bg-muted/50 border-b sticky top-0">
<tr>
<th className="text-left px-4 py-3 text-sm font-semibold">Type</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Technical Component</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Scope</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Design</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Code</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Assigned</th>
<th className="text-center px-4 py-3 text-sm font-semibold">Sessions</th>
<th className="text-center px-4 py-3 text-sm font-semibold">Commits</th>
<th className="text-right px-4 py-3 text-sm font-semibold">Cost</th>
</tr>
</thead>
<tbody className="divide-y">
{data?.workItems.filter(item => isTechnical(item)).map((item, index) => (
<tr
key={item.id}
className="hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => {
setExpandedItems(prev => {
const newSet = new Set(prev);
if (newSet.has(item.id)) {
newSet.delete(item.id);
} else {
newSet.add(item.id);
}
return newSet;
});
}}
>
<td className="px-4 py-3 text-sm text-muted-foreground">
{getWorkItemType(item)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
{item.status === 'built' ? (
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
) : item.status === 'in_progress' ? (
<Clock className="h-4 w-4 text-blue-600 flex-shrink-0" />
) : (
<Circle className="h-4 w-4 text-gray-400 flex-shrink-0" />
)}
<div>
<div className="text-sm font-medium">{item.title}</div>
{expandedItems.has(item.id) && (
<div className="mt-2 space-y-1">
{item.requirements.map((req) => (
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground ml-6">
{req.status === 'built' ? (
<CheckCircle2 className="h-3 w-3 text-green-600" />
) : (
<Circle className="h-3 w-3 text-gray-400" />
)}
<span>{req.text}</span>
</div>
))}
</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
</td>
<td className="px-4 py-3">
<span className="text-sm text-muted-foreground">
{item.assigned || data?.projectCreator || 'You'}
</span>
</td>
<td className="px-4 py-3 text-sm text-center">
<span className={item.sessionsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
{item.sessionsCount}
</span>
</td>
<td className="px-4 py-3 text-sm text-center">
<span className={item.commitsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
{item.commitsCount}
</span>
</td>
<td className="px-4 py-3 text-sm text-right text-muted-foreground">
{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
{/* End Main Content */}
</div>
);
}