refactor(cfdi): descarga masiva de XMLs por filtros en lugar de checkboxes
- Backend: POST /cfdi/download-xmls acepta CfdiFilters, usa getXmlsByFilters con LIMIT 1000 - Frontend: eliminados checkboxes y estado selectedIds; botón Descargar XMLs usa filtros activos - Si >1000 resultados, muestra confirm() de advertencia pero permite proceder - Agregada documentación técnica y changelog
This commit is contained in:
@@ -261,7 +261,6 @@ export default function CfdiPage() {
|
||||
const [loadingEmisor, setLoadingEmisor] = useState(false);
|
||||
const [loadingReceptor, setLoadingReceptor] = useState(false);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||
const [downloadingXmls, setDownloadingXmls] = useState(false);
|
||||
|
||||
// Debounced values for autocomplete
|
||||
@@ -1762,54 +1761,48 @@ export default function CfdiPage() {
|
||||
<FileText className="h-4 w-4" />
|
||||
CFDIs ({data?.total || 0})
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedIds.size > 0 && (
|
||||
<>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{selectedIds.size} seleccionados
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
if (selectedIds.size > 1000) {
|
||||
alert('Máximo 1,000 CFDIs por descarga');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setDownloadingXmls(true);
|
||||
const blob = await downloadXmlsZip(Array.from(selectedIds));
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `cfdis-xml-${Date.now()}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err: any) {
|
||||
alert(err?.response?.data?.message || err?.message || 'Error al descargar XMLs');
|
||||
} finally {
|
||||
setDownloadingXmls(false);
|
||||
}
|
||||
}}
|
||||
disabled={downloadingXmls}
|
||||
>
|
||||
{downloadingXmls ? (
|
||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
Descargar XMLs
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedIds(new Set())}
|
||||
>
|
||||
Limpiar
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
if ((data?.total || 0) > 1000) {
|
||||
if (!confirm('Solo se descargarán los primeros 1,000 XMLs. ¿Continuar?')) return;
|
||||
}
|
||||
try {
|
||||
setDownloadingXmls(true);
|
||||
const blob = await downloadXmlsZip({
|
||||
tipo: filters.tipo,
|
||||
tipoComprobante: filters.tipoComprobante,
|
||||
estado: filters.estado,
|
||||
fechaInicio: filters.fechaInicio,
|
||||
fechaFin: filters.fechaFin,
|
||||
rfc: filters.rfc,
|
||||
emisor: filters.emisor,
|
||||
receptor: filters.receptor,
|
||||
search: filters.search,
|
||||
contribuyenteId: filters.contribuyenteId,
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `cfdis-xml-${Date.now()}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err: any) {
|
||||
alert(err?.response?.data?.message || err?.message || 'Error al descargar XMLs');
|
||||
} finally {
|
||||
setDownloadingXmls(false);
|
||||
}
|
||||
}}
|
||||
disabled={downloadingXmls || !data?.total}
|
||||
>
|
||||
{downloadingXmls ? (
|
||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
</div>
|
||||
Descargar XMLs
|
||||
</Button>
|
||||
{hasActiveColumnFilters && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>Filtros activos:</span>
|
||||
@@ -1867,19 +1860,6 @@ export default function CfdiPage() {
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b text-center text-sm text-muted-foreground">
|
||||
<th className="pb-3 w-8">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data?.data.length ? selectedIds.size === data.data.length : false}
|
||||
onChange={() => {
|
||||
if (selectedIds.size === data?.data.length) {
|
||||
setSelectedIds(new Set());
|
||||
} else {
|
||||
setSelectedIds(new Set(data?.data.map((c: any) => c.id) || []));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<th className="pb-3 font-medium">
|
||||
<div className="flex items-center gap-1 justify-center">
|
||||
Fecha
|
||||
@@ -2055,20 +2035,6 @@ export default function CfdiPage() {
|
||||
<tbody className="text-sm text-center">
|
||||
{data?.data.map((cfdi) => (
|
||||
<tr key={cfdi.id} className="border-b hover:bg-muted/50">
|
||||
<td className="py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(cfdi.id)}
|
||||
onChange={() => {
|
||||
setSelectedIds(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(cfdi.id)) next.delete(cfdi.id);
|
||||
else next.add(cfdi.id);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-3">{formatDate(cfdi.fechaEmision)}</td>
|
||||
<td className="py-3">
|
||||
<span className="text-xs" title={formatTipoComprobante(cfdi.tipoComprobante)}>
|
||||
|
||||
@@ -91,8 +91,8 @@ export async function getCfdiById(id: string): Promise<Cfdi> {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function downloadXmlsZip(ids: number[]): Promise<Blob> {
|
||||
const response = await apiClient.post('/cfdi/download-xmls', { ids }, { responseType: 'blob' });
|
||||
export async function downloadXmlsZip(filters: CfdiFilters): Promise<Blob> {
|
||||
const response = await apiClient.post('/cfdi/download-xmls', filters, { responseType: 'blob' });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user