feat: add Excel export, keyboard shortcuts, and print view for CFDIs
- Add export to Excel button with xlsx library for filtered data - Add keyboard shortcuts (Esc to close popovers/forms) - Add print button to invoice viewer modal with optimized print styles Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,10 @@ import { useCfdis, useCreateCfdi, useDeleteCfdi } from '@/lib/hooks/use-cfdi';
|
||||
import { createManyCfdis, searchEmisores, searchReceptores, type EmisorReceptor } from '@/lib/api/cfdi';
|
||||
import type { CfdiFilters, TipoCfdi, Cfdi } from '@horux/shared';
|
||||
import type { CreateCfdiData } from '@/lib/api/cfdi';
|
||||
import { FileText, Search, ChevronLeft, ChevronRight, Plus, Upload, Trash2, X, FileUp, CheckCircle, AlertCircle, Loader2, Eye, Filter, XCircle, Calendar, User, Building2 } from 'lucide-react';
|
||||
import { FileText, Search, ChevronLeft, ChevronRight, Plus, Upload, Trash2, X, FileUp, CheckCircle, AlertCircle, Loader2, Eye, Filter, XCircle, Calendar, User, Building2, Download, Printer } from 'lucide-react';
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal';
|
||||
import { getCfdiById } from '@/lib/api/cfdi';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
@@ -274,6 +276,7 @@ export default function CfdiPage() {
|
||||
.catch(() => setReceptorSuggestions([]))
|
||||
.finally(() => setLoadingReceptor(false));
|
||||
}, [debouncedReceptor]);
|
||||
|
||||
const [showBulkForm, setShowBulkForm] = useState(false);
|
||||
const [formData, setFormData] = useState<CreateCfdiData>(initialFormData);
|
||||
const [bulkData, setBulkData] = useState('');
|
||||
@@ -323,6 +326,54 @@ export default function CfdiPage() {
|
||||
setFilters({ ...filters, search: searchTerm, page: 1 });
|
||||
};
|
||||
|
||||
// Export to Excel
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const exportToExcel = async () => {
|
||||
if (!data?.data.length) return;
|
||||
|
||||
setExporting(true);
|
||||
try {
|
||||
const exportData = data.data.map(cfdi => ({
|
||||
'Fecha Emisión': new Date(cfdi.fechaEmision).toLocaleDateString('es-MX'),
|
||||
'Tipo': cfdi.tipo === 'ingreso' ? 'Ingreso' : 'Egreso',
|
||||
'Serie': cfdi.serie || '',
|
||||
'Folio': cfdi.folio || '',
|
||||
'RFC Emisor': cfdi.rfcEmisor,
|
||||
'Nombre Emisor': cfdi.nombreEmisor,
|
||||
'RFC Receptor': cfdi.rfcReceptor,
|
||||
'Nombre Receptor': cfdi.nombreReceptor,
|
||||
'Subtotal': cfdi.subtotal,
|
||||
'IVA': cfdi.iva,
|
||||
'Total': cfdi.total,
|
||||
'Moneda': cfdi.moneda,
|
||||
'Estado': cfdi.estado === 'vigente' ? 'Vigente' : 'Cancelado',
|
||||
'UUID': cfdi.uuidFiscal,
|
||||
}));
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'CFDIs');
|
||||
|
||||
// Auto-size columns
|
||||
const colWidths = Object.keys(exportData[0]).map(key => ({
|
||||
wch: Math.max(key.length, ...exportData.map(row => String(row[key as keyof typeof row]).length))
|
||||
}));
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
|
||||
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
|
||||
const fileName = `cfdis_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
saveAs(blob, fileName);
|
||||
} catch (error) {
|
||||
console.error('Error exporting:', error);
|
||||
alert('Error al exportar');
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectEmisor = (emisor: EmisorReceptor) => {
|
||||
setColumnFilters(prev => ({ ...prev, emisor: emisor.nombre }));
|
||||
setEmisorSuggestions([]);
|
||||
@@ -594,6 +645,32 @@ export default function CfdiPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard shortcuts - Esc to close popovers and forms
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
// Close open filter popovers
|
||||
if (openFilter !== null) {
|
||||
setOpenFilter(null);
|
||||
return;
|
||||
}
|
||||
// Close forms
|
||||
if (showForm) {
|
||||
setShowForm(false);
|
||||
return;
|
||||
}
|
||||
if (showBulkForm) {
|
||||
setShowBulkForm(false);
|
||||
clearXmlFiles();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [openFilter, showForm, showBulkForm]);
|
||||
|
||||
const cancelUpload = () => {
|
||||
uploadAbortRef.current = true;
|
||||
setUploadProgress(prev => ({ ...prev, status: 'idle' }));
|
||||
@@ -681,18 +758,30 @@ export default function CfdiPage() {
|
||||
Egresos
|
||||
</Button>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => { setShowForm(true); setShowBulkForm(false); }}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Agregar
|
||||
<div className="flex gap-2">
|
||||
{data && data.data.length > 0 && (
|
||||
<Button variant="outline" onClick={exportToExcel} disabled={exporting}>
|
||||
{exporting ? (
|
||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
Exportar
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => { setShowBulkForm(true); setShowForm(false); }}>
|
||||
<Upload className="h-4 w-4 mr-1" />
|
||||
Carga Masiva
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{canEdit && (
|
||||
<>
|
||||
<Button onClick={() => { setShowForm(true); setShowBulkForm(false); }}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Agregar
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => { setShowBulkForm(true); setShowForm(false); }}>
|
||||
<Upload className="h-4 w-4 mr-1" />
|
||||
Carga Masiva
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user