- Add FielUploadModal component for FIEL credential upload - Add SyncStatus component showing current sync progress - Add SyncHistory component with pagination and retry - Add SAT configuration page at /configuracion/sat - Add API client functions for FIEL and SAT endpoints Features: - File upload with Base64 encoding - Real-time sync progress tracking - Manual sync trigger (initial/daily) - Sync history with retry capability - FIEL status display with expiration warning Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
183 lines
5.2 KiB
TypeScript
183 lines
5.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { getSyncHistory, retrySync } from '@/lib/api/sat';
|
|
import type { SatSyncJob } from '@horux/shared';
|
|
|
|
interface SyncHistoryProps {
|
|
fielConfigured: boolean;
|
|
}
|
|
|
|
const statusLabels: Record<string, string> = {
|
|
pending: 'Pendiente',
|
|
running: 'En progreso',
|
|
completed: 'Completado',
|
|
failed: 'Fallido',
|
|
};
|
|
|
|
const statusColors: Record<string, string> = {
|
|
pending: 'bg-yellow-100 text-yellow-800',
|
|
running: 'bg-blue-100 text-blue-800',
|
|
completed: 'bg-green-100 text-green-800',
|
|
failed: 'bg-red-100 text-red-800',
|
|
};
|
|
|
|
const typeLabels: Record<string, string> = {
|
|
initial: 'Inicial',
|
|
daily: 'Diaria',
|
|
};
|
|
|
|
export function SyncHistory({ fielConfigured }: SyncHistoryProps) {
|
|
const [jobs, setJobs] = useState<SatSyncJob[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [page, setPage] = useState(1);
|
|
const [total, setTotal] = useState(0);
|
|
const limit = 10;
|
|
|
|
const fetchHistory = async () => {
|
|
try {
|
|
const data = await getSyncHistory(page, limit);
|
|
setJobs(data.jobs);
|
|
setTotal(data.total);
|
|
} catch (err) {
|
|
console.error('Error fetching sync history:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (fielConfigured) {
|
|
fetchHistory();
|
|
} else {
|
|
setLoading(false);
|
|
}
|
|
}, [fielConfigured, page]);
|
|
|
|
const handleRetry = async (jobId: string) => {
|
|
try {
|
|
await retrySync(jobId);
|
|
fetchHistory();
|
|
} catch (err) {
|
|
console.error('Error retrying job:', err);
|
|
}
|
|
};
|
|
|
|
if (!fielConfigured) {
|
|
return null;
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Historial de Sincronizaciones</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-muted-foreground">Cargando historial...</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (jobs.length === 0) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Historial de Sincronizaciones</CardTitle>
|
|
<CardDescription>
|
|
Registro de todas las sincronizaciones con el SAT
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-muted-foreground">No hay sincronizaciones registradas.</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Historial de Sincronizaciones</CardTitle>
|
|
<CardDescription>
|
|
Registro de todas las sincronizaciones con el SAT
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{jobs.map((job) => (
|
|
<div
|
|
key={job.id}
|
|
className="flex items-center justify-between p-4 border rounded-lg"
|
|
>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className={`px-2 py-0.5 rounded text-xs ${statusColors[job.status]}`}>
|
|
{statusLabels[job.status]}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{typeLabels[job.type]}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm">
|
|
{job.startedAt ? new Date(job.startedAt).toLocaleString('es-MX') : 'No iniciado'}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{job.cfdisInserted} nuevos, {job.cfdisUpdated} actualizados
|
|
</p>
|
|
{job.errorMessage && (
|
|
<p className="text-xs text-red-500 mt-1">{job.errorMessage}</p>
|
|
)}
|
|
</div>
|
|
{job.status === 'failed' && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handleRetry(job.id)}
|
|
>
|
|
Reintentar
|
|
</Button>
|
|
)}
|
|
{job.status === 'running' && (
|
|
<div className="text-right">
|
|
<p className="text-sm font-medium">{job.progressPercent}%</p>
|
|
<p className="text-xs text-muted-foreground">{job.cfdisDownloaded} descargados</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{totalPages > 1 && (
|
|
<div className="flex justify-center gap-2 mt-4">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={page === 1}
|
|
onClick={() => setPage(p => p - 1)}
|
|
>
|
|
Anterior
|
|
</Button>
|
|
<span className="py-2 px-3 text-sm">
|
|
Pagina {page} de {totalPages}
|
|
</span>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={page === totalPages}
|
|
onClick={() => setPage(p => p + 1)}
|
|
>
|
|
Siguiente
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|