Problema: el boton 'Sincronizacion inicial (6 años)' desaparecia cuando existia CUALQUIER job completado (daily, incremental, etc.). Esto era inconsistente con el cron incremental del backend, que requiere especificamente un job de tipo 'initial' completado. Resultado: usuarios que solo habian hecho sync diaria perdian la opcion de hacer la extraccion inicial completa, y el cron incremental tampoco corria porque no habia initial. Fix: - Backend getSyncStatus: agrega lastCompletedInitialJob (busca solo jobs type='initial' status='completed') - Frontend SyncStatus: muestra el boton de inicial si !lastCompletedInitialJob (ignora jobs diarios/incrementales) - SatSyncStatusResponse: agrega campo lastCompletedInitialJob
252 lines
8.3 KiB
TypeScript
252 lines
8.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Button, Input, Label } from '@horux/shared-ui';
|
|
import { getSyncStatus, startSync } from '@/lib/api/sat';
|
|
import type { SatSyncStatusResponse } from '@horux/shared';
|
|
|
|
interface SyncStatusProps {
|
|
fielConfigured: boolean;
|
|
onSyncStarted?: () => void;
|
|
contribuyenteId?: string | null;
|
|
}
|
|
|
|
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',
|
|
};
|
|
|
|
export function SyncStatus({ fielConfigured, onSyncStarted, contribuyenteId }: SyncStatusProps) {
|
|
const [status, setStatus] = useState<SatSyncStatusResponse | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [startingSync, setStartingSync] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [showCustomDate, setShowCustomDate] = useState(false);
|
|
const [dateFrom, setDateFrom] = useState('');
|
|
const [dateTo, setDateTo] = useState('');
|
|
|
|
const fetchStatus = async () => {
|
|
try {
|
|
const data = await getSyncStatus(contribuyenteId);
|
|
setStatus(data);
|
|
} catch (err) {
|
|
console.error('Error fetching sync status:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (fielConfigured) {
|
|
fetchStatus();
|
|
// Actualizar cada 30 segundos si hay sync activo
|
|
const interval = setInterval(fetchStatus, 30000);
|
|
return () => clearInterval(interval);
|
|
} else {
|
|
setLoading(false);
|
|
}
|
|
}, [fielConfigured, contribuyenteId]);
|
|
|
|
const handleStartSync = async (type: 'initial' | 'daily', customDates?: boolean) => {
|
|
setStartingSync(true);
|
|
setError('');
|
|
try {
|
|
const params: { type: 'initial' | 'daily'; dateFrom?: string; dateTo?: string } = { type };
|
|
|
|
if (customDates && dateFrom && dateTo) {
|
|
// Convertir a formato completo con hora
|
|
params.dateFrom = `${dateFrom}T00:00:00`;
|
|
params.dateTo = `${dateTo}T23:59:59`;
|
|
}
|
|
|
|
await startSync(params, contribuyenteId);
|
|
await fetchStatus();
|
|
setShowCustomDate(false);
|
|
onSyncStarted?.();
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.error || 'Error al iniciar sincronizacion');
|
|
} finally {
|
|
setStartingSync(false);
|
|
}
|
|
};
|
|
|
|
if (!fielConfigured) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Sincronizacion SAT</CardTitle>
|
|
<CardDescription>
|
|
Configura tu FIEL para habilitar la sincronizacion automatica
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-muted-foreground">
|
|
La sincronizacion con el SAT requiere una FIEL valida configurada.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Sincronizacion SAT</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-muted-foreground">Cargando estado...</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Sincronizacion SAT</CardTitle>
|
|
<CardDescription>
|
|
Estado de la sincronizacion automatica de CFDIs
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{status?.hasActiveSync && status.currentJob && (
|
|
<div className="p-4 bg-blue-50 rounded-lg">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className={`px-2 py-1 rounded text-sm ${statusColors[status.currentJob.status]}`}>
|
|
{statusLabels[status.currentJob.status]}
|
|
</span>
|
|
<span className="text-sm text-muted-foreground">
|
|
{status.currentJob.type === 'initial' ? 'Sincronizacion inicial' : 'Sincronizacion diaria'}
|
|
</span>
|
|
</div>
|
|
{status.currentJob.status === 'running' && (
|
|
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
|
<div
|
|
className="bg-blue-600 h-2 rounded-full transition-all"
|
|
style={{ width: `${status.currentJob.progressPercent}%` }}
|
|
/>
|
|
</div>
|
|
)}
|
|
<p className="text-sm mt-2">
|
|
{status.currentJob.cfdisDownloaded} CFDIs descargados
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{status?.lastCompletedJob && !status.hasActiveSync && (
|
|
<div className="p-4 bg-green-50 rounded-lg">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className={`px-2 py-1 rounded text-sm ${statusColors.completed}`}>
|
|
Ultima sincronizacion exitosa
|
|
</span>
|
|
</div>
|
|
<p className="text-sm">
|
|
{new Date(status.lastCompletedJob.completedAt!).toLocaleString('es-MX')}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{status.lastCompletedJob.cfdisInserted} CFDIs nuevos, {status.lastCompletedJob.cfdisUpdated} actualizados
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-center">
|
|
<div className="p-4 bg-gray-50 rounded-lg">
|
|
<p className="text-2xl font-bold">{status?.totalCfdisSynced || 0}</p>
|
|
<p className="text-sm text-muted-foreground">CFDIs sincronizados</p>
|
|
</div>
|
|
<div className="p-4 bg-gray-50 rounded-lg">
|
|
<p className="text-2xl font-bold">3:00 AM</p>
|
|
<p className="text-sm text-muted-foreground">Sincronizacion diaria</p>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="text-sm text-red-500">{error}</p>
|
|
)}
|
|
|
|
{/* Formulario de fechas personalizadas */}
|
|
{showCustomDate && (
|
|
<div className="p-4 bg-gray-50 rounded-lg space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="dateFrom">Fecha inicio</Label>
|
|
<Input
|
|
id="dateFrom"
|
|
type="date"
|
|
value={dateFrom}
|
|
onChange={(e) => setDateFrom(e.target.value)}
|
|
max={dateTo || undefined}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="dateTo">Fecha fin</Label>
|
|
<Input
|
|
id="dateTo"
|
|
type="date"
|
|
value={dateTo}
|
|
onChange={(e) => setDateTo(e.target.value)}
|
|
min={dateFrom || undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
disabled={startingSync || status?.hasActiveSync || !dateFrom || !dateTo}
|
|
onClick={() => handleStartSync('initial', true)}
|
|
className="flex-1"
|
|
>
|
|
{startingSync ? 'Iniciando...' : 'Sincronizar periodo'}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowCustomDate(false)}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-3">
|
|
<Button
|
|
variant="outline"
|
|
disabled={startingSync || status?.hasActiveSync}
|
|
onClick={() => handleStartSync('daily')}
|
|
className="flex-1"
|
|
>
|
|
{startingSync ? 'Iniciando...' : 'Sincronizar mes actual'}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
disabled={startingSync || status?.hasActiveSync}
|
|
onClick={() => setShowCustomDate(!showCustomDate)}
|
|
className="flex-1"
|
|
>
|
|
Periodo personalizado
|
|
</Button>
|
|
</div>
|
|
|
|
{!status?.lastCompletedInitialJob && (
|
|
<Button
|
|
disabled={startingSync || status?.hasActiveSync}
|
|
onClick={() => handleStartSync('initial')}
|
|
className="w-full"
|
|
>
|
|
{startingSync ? 'Iniciando...' : 'Sincronizacion inicial (6 años)'}
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|