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
This commit is contained in:
parent
e8462ef301
commit
6eee14f8af
171 changed files with 32671 additions and 621 deletions
246
components/admin/EntitySyncProgress.tsx
Normal file
246
components/admin/EntitySyncProgress.tsx
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
'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<SyncProgressState | null>(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 (
|
||||
<Card className="border-2 dark:border-gray-700">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{progress.status === 'running' && (
|
||||
<Loader2 className="h-5 w-5 animate-spin text-blue-500" />
|
||||
)}
|
||||
{progress.status === 'completed' && (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
)}
|
||||
{progress.status === 'failed' && (
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
)}
|
||||
<CardTitle className="text-lg">
|
||||
{entityType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} Sync
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Badge
|
||||
variant={
|
||||
progress.status === 'running' ? 'default' :
|
||||
progress.status === 'completed' ? 'secondary' :
|
||||
'destructive'
|
||||
}
|
||||
className={
|
||||
progress.status === 'running' ? 'bg-blue-500 hover:bg-blue-600' : ''
|
||||
}
|
||||
>
|
||||
{progress.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="flex items-center gap-2 mt-2">
|
||||
<PhaseIcon className="h-4 w-4" />
|
||||
<span>{PHASE_LABELS[progress.phase]}</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{/* Progress Bar */}
|
||||
<div className="space-y-2">
|
||||
<Progress
|
||||
value={animatedProgress}
|
||||
className="h-2"
|
||||
aria-label={`Sync progress: ${Math.round(animatedProgress)}%`}
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>{Math.round(animatedProgress)}% complete</span>
|
||||
<span>{duration}s elapsed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{progress.totalRecords > 0 && (
|
||||
<div className="grid grid-cols-2 gap-4 pt-2 border-t dark:border-gray-700">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">Records Processed</p>
|
||||
<p className="text-2xl font-bold">{progress.totalRecords.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">Current Phase</p>
|
||||
<p className="text-lg font-semibold capitalize">{progress.phase}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{progress.status === 'failed' && progress.error && (
|
||||
<div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-md p-3">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">{progress.error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Completion Message */}
|
||||
{progress.status === 'completed' && (
|
||||
<div className="bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-md p-3">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
✓ Successfully synced {progress.totalRecords.toLocaleString()} records in {duration}s
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue