- 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
153 lines
5.5 KiB
TypeScript
153 lines
5.5 KiB
TypeScript
/**
|
|
* Sync Dashboard Component
|
|
* Displays sync status and last sync information
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import { getEntityDisplayName } from '@/lib/utils/sync-helpers';
|
|
import { EntityType } from '@/lib/types/sync';
|
|
|
|
interface LastSyncInfo {
|
|
[key: string]: {
|
|
completed_at: string;
|
|
status: string;
|
|
records_added: number;
|
|
records_updated: number;
|
|
records_deleted: number;
|
|
};
|
|
}
|
|
|
|
interface SyncDashboardProps {
|
|
refreshKey: number;
|
|
}
|
|
|
|
export default function SyncDashboard({ refreshKey }: SyncDashboardProps) {
|
|
const [lastSyncInfo, setLastSyncInfo] = useState<LastSyncInfo>({});
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetchLastSyncInfo();
|
|
}, [refreshKey]);
|
|
|
|
const fetchLastSyncInfo = async () => {
|
|
try {
|
|
const response = await fetch('/api/sync/last-sync');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setLastSyncInfo(data.lastSync || {});
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch last sync info:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Sync Status</CardTitle>
|
|
<CardDescription>
|
|
Last sync information for each entity
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 md:gap-4">
|
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
<div key={i} className="border rounded-lg p-4 space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="h-4 w-24 bg-muted animate-pulse rounded" />
|
|
<div className="h-5 w-16 bg-muted animate-pulse rounded-full" />
|
|
</div>
|
|
<div className="h-3 w-32 bg-muted animate-pulse rounded" />
|
|
<div className="space-y-1">
|
|
<div className="h-3 w-full bg-muted animate-pulse rounded" />
|
|
<div className="h-3 w-full bg-muted animate-pulse rounded" />
|
|
<div className="h-3 w-full bg-muted animate-pulse rounded" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const entityKeys = Object.keys(lastSyncInfo);
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Sync Status</CardTitle>
|
|
<CardDescription>
|
|
Last sync information for each entity
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{entityKeys.length === 0 ? (
|
|
<p className="text-muted-foreground">No sync history available</p>
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 md:gap-4">
|
|
{entityKeys.map((entityKey) => {
|
|
const info = lastSyncInfo[entityKey];
|
|
const completedAt = new Date(info.completed_at);
|
|
|
|
return (
|
|
<div
|
|
key={entityKey}
|
|
className="group relative overflow-hidden rounded-lg border bg-card p-4 transition-all hover:shadow-md hover:border-primary/50 space-y-2"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="font-semibold text-sm truncate">
|
|
{getEntityDisplayName(entityKey as EntityType)}
|
|
</h4>
|
|
<Badge
|
|
variant={info.status === 'completed' ? 'default' : 'destructive'}
|
|
className="text-xs shrink-0"
|
|
>
|
|
{info.status}
|
|
</Badge>
|
|
</div>
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
{formatDistanceToNow(completedAt, { addSuffix: true })}
|
|
</p>
|
|
|
|
<div className="text-xs space-y-1 pt-1">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-muted-foreground">Added:</span>
|
|
<span className="font-medium text-green-600 dark:text-green-400">
|
|
+{info.records_added.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-muted-foreground">Updated:</span>
|
|
<span className="font-medium text-blue-600 dark:text-blue-400">
|
|
~{info.records_updated.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-muted-foreground">Deleted:</span>
|
|
<span className="font-medium text-red-600 dark:text-red-400">
|
|
-{info.records_deleted.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Subtle hover indicator */}
|
|
<div className="absolute inset-x-0 bottom-0 h-0.5 bg-primary/50 transform scale-x-0 group-hover:scale-x-100 transition-transform" />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|