Files
Horux360/apps/web/components/sat/SyncHistory.tsx
Consultoria AS 31c66f2823 feat(sat): add frontend components for SAT configuration (Phase 8)
- 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>
2026-01-25 01:00:08 +00:00

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>
);
}