- 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
174 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|