'use client'; import { useEffect, useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { Badge } from '@/components/ui/badge'; import { Loader2, CheckCircle2, XCircle, Database, ArrowDownToLine, FileEdit, Trash2 } from 'lucide-react'; interface SyncProgressState { syncId: string; entityType: string; status: 'idle' | 'running' | 'completed' | 'failed'; currentPage: number; totalRecords: number; estimatedTotal?: number; startTime: number; endTime?: number; error?: string; phase: 'fetching' | 'mapping' | 'upserting' | 'deleting' | 'completed'; } interface EntitySyncProgressProps { entityType: string; syncId?: string; onComplete?: () => void; onError?: (error: string) => void; } const PHASE_LABELS = { fetching: 'Fetching from Autotask', mapping: 'Mapping records', upserting: 'Saving to database', deleting: 'Cleaning up', completed: 'Completed', }; const PHASE_ICONS = { fetching: ArrowDownToLine, mapping: FileEdit, upserting: Database, deleting: Trash2, completed: CheckCircle2, }; export default function EntitySyncProgress({ entityType, syncId, onComplete, onError, }: EntitySyncProgressProps) { const [progress, setProgress] = useState(null); const [animatedProgress, setAnimatedProgress] = useState(0); // Poll for progress updates useEffect(() => { let pollInterval: NodeJS.Timeout; let mounted = true; const fetchProgress = async () => { try { const params = new URLSearchParams(); if (syncId) { params.append('syncId', syncId); } else { params.append('entityType', entityType); } const response = await fetch(`/api/sync/progress?${params}`); if (!response.ok) { // No progress found yet return; } const data = await response.json(); const progressData = data.progress; if (mounted && progressData) { setProgress(progressData); // Handle completion if (progressData.status === 'completed' && onComplete) { onComplete(); } // Handle errors if (progressData.status === 'failed' && onError) { onError(progressData.error || 'Sync failed'); } } } catch (error) { console.error('Error fetching sync progress:', error); } }; // Initial fetch fetchProgress(); // Poll every 2 seconds while sync is running pollInterval = setInterval(() => { if (progress?.status === 'running') { fetchProgress(); } else if (progress?.status === 'completed' || progress?.status === 'failed') { clearInterval(pollInterval); } }, 2000); return () => { mounted = false; clearInterval(pollInterval); }; }, [entityType, syncId, progress?.status, onComplete, onError]); // Animate progress bar useEffect(() => { if (!progress) return; let targetProgress = 0; // Calculate progress based on phase switch (progress.phase) { case 'fetching': targetProgress = 25; break; case 'mapping': targetProgress = 50; break; case 'upserting': targetProgress = 75; break; case 'deleting': targetProgress = 90; break; case 'completed': targetProgress = 100; break; } // Smooth animation const step = (targetProgress - animatedProgress) / 10; const interval = setInterval(() => { setAnimatedProgress((prev) => { const next = prev + step; if (Math.abs(next - targetProgress) < 1) { clearInterval(interval); return targetProgress; } return next; }); }, 50); return () => clearInterval(interval); }, [progress?.phase]); if (!progress || progress.status === 'idle') { return null; } const PhaseIcon = PHASE_ICONS[progress.phase]; const duration = progress.endTime ? Math.round((progress.endTime - progress.startTime) / 1000) : Math.round((Date.now() - progress.startTime) / 1000); return (
{progress.status === 'running' && ( )} {progress.status === 'completed' && ( )} {progress.status === 'failed' && ( )} {entityType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} Sync
{progress.status}
{PHASE_LABELS[progress.phase]}
{/* Progress Bar */}
{Math.round(animatedProgress)}% complete {duration}s elapsed
{/* Stats */} {progress.totalRecords > 0 && (

Records Processed

{progress.totalRecords.toLocaleString()}

Current Phase

{progress.phase}

)} {/* Error Message */} {progress.status === 'failed' && progress.error && (

{progress.error}

)} {/* Completion Message */} {progress.status === 'completed' && (

✓ Successfully synced {progress.totalRecords.toLocaleString()} records in {duration}s

)}
); }