wulf-pulse/components/admin/ChunkedSyncProgress.tsx
root 6eee14f8af Add comprehensive admin features and multi-system integration
- Add admin dashboard with sync controls and data browser
- Implement RMM, Auvik, and Addigy organization mappings
- Add chunked ticket sync with progress tracking
- Implement entity sync service with rate limiting
- Add analytics engine and performance optimizer
- Create data browser for all PSA entities
- Add navigation components and UI improvements
- Implement background processing and sync services
- Add comprehensive documentation and migration scripts
- Update configuration items with multi-system support
- Enhance contact management and purchase history
- Add issue type assignment and LLM analyzer
- Improve error handling and logging utilities
2025-11-19 14:18:16 -05:00

174 lines
6.2 KiB
TypeScript

/**
* Chunked Sync Progress Component
* Displays animated progress bar for chunked ticket sync operations
*/
'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 { CheckCircle2, XCircle, Loader2, Calendar } from 'lucide-react';
interface ChunkInfo {
index: number;
total: number;
description: string;
recordsProcessed: number;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
}
interface ChunkedSyncProgressProps {
isActive: boolean;
currentChunk?: ChunkInfo;
completedChunks?: number;
totalChunks?: number;
totalRecords?: number;
failedChunks?: string[];
}
export default function ChunkedSyncProgress({
isActive,
currentChunk,
completedChunks = 0,
totalChunks = 0,
totalRecords = 0,
failedChunks = [],
}: ChunkedSyncProgressProps) {
const [animatedProgress, setAnimatedProgress] = useState(0);
// Animate progress bar
useEffect(() => {
if (totalChunks === 0) return;
const targetProgress = (completedChunks / totalChunks) * 100;
// Smooth animation
const step = (targetProgress - animatedProgress) / 10;
const interval = setInterval(() => {
setAnimatedProgress(prev => {
const next = prev + step;
if (Math.abs(next - targetProgress) < 0.5) {
clearInterval(interval);
return targetProgress;
}
return next;
});
}, 50);
return () => clearInterval(interval);
}, [completedChunks, totalChunks]);
if (!isActive && totalChunks === 0) {
return null;
}
const progressPercentage = totalChunks > 0 ? (completedChunks / totalChunks) * 100 : 0;
const hasFailures = failedChunks.length > 0;
return (
<Card className="border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/20">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-blue-600" />
<CardTitle className="text-lg">Chunked Ticket Sync Progress</CardTitle>
</div>
{isActive ? (
<Badge variant="default" className="bg-blue-600">
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
Syncing
</Badge>
) : hasFailures ? (
<Badge variant="destructive">
<XCircle className="w-3 h-3 mr-1" />
Completed with Errors
</Badge>
) : (
<Badge variant="default" className="bg-green-600">
<CheckCircle2 className="w-3 h-3 mr-1" />
Completed
</Badge>
)}
</div>
<CardDescription>
Processing tickets in monthly chunks to prevent timeouts
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Progress Bar */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="font-medium">
{currentChunk?.description || 'Preparing...'}
</span>
<span className="text-muted-foreground">
{completedChunks} / {totalChunks} chunks
</span>
</div>
<Progress
value={animatedProgress}
className="h-3 transition-all duration-300"
aria-label={`Sync progress: ${Math.round(progressPercentage)}% complete`}
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>{Math.round(progressPercentage)}% complete</span>
<span>{totalRecords.toLocaleString()} records processed</span>
</div>
</div>
{/* Chunk Details */}
{currentChunk && isActive && (
<div className="p-3 bg-white dark:bg-card rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-center gap-2 mb-1">
<Loader2 className="w-4 h-4 text-blue-600 dark:text-blue-400 animate-spin" />
<span className="font-medium text-sm">
Processing: {currentChunk.description}
</span>
</div>
<p className="text-xs text-muted-foreground">
Chunk {currentChunk.index} of {currentChunk.total} {currentChunk.recordsProcessed.toLocaleString()} records so far
</p>
</div>
)}
{/* Failed Chunks */}
{failedChunks.length > 0 && (
<div className="p-3 bg-red-50 dark:bg-red-950/20 rounded-lg border border-red-200 dark:border-red-800">
<div className="flex items-center gap-2 mb-2">
<XCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
<span className="font-medium text-sm text-red-900 dark:text-red-100">
{failedChunks.length} chunk{failedChunks.length > 1 ? 's' : ''} failed
</span>
</div>
<ul className="space-y-1">
{failedChunks.slice(0, 3).map((chunk, idx) => (
<li key={idx} className="text-xs text-red-800 dark:text-red-200">
{chunk}
</li>
))}
{failedChunks.length > 3 && (
<li className="text-xs text-red-600 dark:text-red-400 italic">
... and {failedChunks.length - 3} more
</li>
)}
</ul>
</div>
)}
{/* Completion Summary */}
{!isActive && totalChunks > 0 && (
<div className="p-3 bg-green-50 dark:bg-green-950/20 rounded-lg border border-green-200 dark:border-green-800">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="font-medium text-sm text-green-900 dark:text-green-100">
Sync completed: {totalRecords.toLocaleString()} records processed across {completedChunks} chunks
</span>
</div>
</div>
)}
</CardContent>
</Card>
);
}