feat(cfdi): add inline column filters for date, emisor, receptor

- Add emisor and receptor filters to CfdiFilters type
- Update backend service to filter by emisor/receptor (RFC or nombre)
- Update controller and API client to pass new filters
- Add toggle button to show/hide column filters in table
- Add date range inputs for fecha filter
- Add text inputs for emisor and receptor filters
- Apply filters on Enter key or search button click
- Add clear filters button when filters are active

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-02-17 06:21:13 +00:00
parent 8c3fb76406
commit 29ac067a82
7 changed files with 248 additions and 14 deletions

View File

@@ -11,7 +11,7 @@ import { useCfdis, useCreateCfdi, useDeleteCfdi } from '@/lib/hooks/use-cfdi';
import { createManyCfdis } 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 } from 'lucide-react';
import { FileText, Search, ChevronLeft, ChevronRight, Plus, Upload, Trash2, X, FileUp, CheckCircle, AlertCircle, Loader2, Eye, Filter, XCircle } from 'lucide-react';
import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal';
import { getCfdiById } from '@/lib/api/cfdi';
import { useAuthStore } from '@/stores/auth-store';
@@ -230,6 +230,13 @@ export default function CfdiPage() {
limit: 20,
});
const [searchTerm, setSearchTerm] = useState('');
const [showColumnFilters, setShowColumnFilters] = useState(false);
const [columnFilters, setColumnFilters] = useState({
fechaInicio: '',
fechaFin: '',
emisor: '',
receptor: '',
});
const [showForm, setShowForm] = useState(false);
const [showBulkForm, setShowBulkForm] = useState(false);
const [formData, setFormData] = useState<CreateCfdiData>(initialFormData);
@@ -280,6 +287,36 @@ export default function CfdiPage() {
setFilters({ ...filters, search: searchTerm, page: 1 });
};
const applyColumnFilters = () => {
setFilters({
...filters,
fechaInicio: columnFilters.fechaInicio || undefined,
fechaFin: columnFilters.fechaFin || undefined,
emisor: columnFilters.emisor || undefined,
receptor: columnFilters.receptor || undefined,
page: 1,
});
};
const clearColumnFilters = () => {
setColumnFilters({
fechaInicio: '',
fechaFin: '',
emisor: '',
receptor: '',
});
setFilters({
...filters,
fechaInicio: undefined,
fechaFin: undefined,
emisor: undefined,
receptor: undefined,
page: 1,
});
};
const hasActiveColumnFilters = columnFilters.fechaInicio || columnFilters.fechaFin || columnFilters.emisor || columnFilters.receptor;
const handleFilterType = (tipo?: TipoCfdi) => {
setFilters({ ...filters, tipo, page: 1 });
};
@@ -1060,10 +1097,28 @@ export default function CfdiPage() {
{/* Table */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<FileText className="h-4 w-4" />
CFDIs ({data?.total || 0})
</CardTitle>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-base">
<FileText className="h-4 w-4" />
CFDIs ({data?.total || 0})
</CardTitle>
<div className="flex items-center gap-2">
{hasActiveColumnFilters && (
<Button variant="ghost" size="sm" onClick={clearColumnFilters} className="text-muted-foreground">
<XCircle className="h-4 w-4 mr-1" />
Limpiar filtros
</Button>
)}
<Button
variant={showColumnFilters ? 'default' : 'outline'}
size="sm"
onClick={() => setShowColumnFilters(!showColumnFilters)}
>
<Filter className="h-4 w-4 mr-1" />
Filtros
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
@@ -1079,16 +1134,68 @@ export default function CfdiPage() {
<table className="w-full">
<thead>
<tr className="border-b text-left text-sm text-muted-foreground">
<th className="pb-3 font-medium">Fecha</th>
<th className="pb-3 font-medium">Tipo</th>
<th className="pb-3 font-medium">Folio</th>
<th className="pb-3 font-medium">Emisor</th>
<th className="pb-3 font-medium">Receptor</th>
<th className="pb-3 font-medium text-right">Total</th>
<th className="pb-3 font-medium">Estado</th>
<th className="pb-3 font-medium"></th>
{canEdit && <th className="pb-3 font-medium"></th>}
<th className="pb-2 font-medium">Fecha</th>
<th className="pb-2 font-medium">Tipo</th>
<th className="pb-2 font-medium">Folio</th>
<th className="pb-2 font-medium">Emisor</th>
<th className="pb-2 font-medium">Receptor</th>
<th className="pb-2 font-medium text-right">Total</th>
<th className="pb-2 font-medium">Estado</th>
<th className="pb-2 font-medium"></th>
{canEdit && <th className="pb-2 font-medium"></th>}
</tr>
{showColumnFilters && (
<tr className="border-b bg-muted/30">
<td className="py-2 pr-2">
<div className="flex flex-col gap-1">
<Input
type="date"
placeholder="Desde"
className="h-7 text-xs"
value={columnFilters.fechaInicio}
onChange={(e) => setColumnFilters({ ...columnFilters, fechaInicio: e.target.value })}
onKeyDown={(e) => e.key === 'Enter' && applyColumnFilters()}
/>
<Input
type="date"
placeholder="Hasta"
className="h-7 text-xs"
value={columnFilters.fechaFin}
onChange={(e) => setColumnFilters({ ...columnFilters, fechaFin: e.target.value })}
onKeyDown={(e) => e.key === 'Enter' && applyColumnFilters()}
/>
</div>
</td>
<td className="py-2 px-1"></td>
<td className="py-2 px-1"></td>
<td className="py-2 px-1">
<Input
placeholder="Buscar emisor..."
className="h-7 text-xs"
value={columnFilters.emisor}
onChange={(e) => setColumnFilters({ ...columnFilters, emisor: e.target.value })}
onKeyDown={(e) => e.key === 'Enter' && applyColumnFilters()}
/>
</td>
<td className="py-2 px-1">
<Input
placeholder="Buscar receptor..."
className="h-7 text-xs"
value={columnFilters.receptor}
onChange={(e) => setColumnFilters({ ...columnFilters, receptor: e.target.value })}
onKeyDown={(e) => e.key === 'Enter' && applyColumnFilters()}
/>
</td>
<td className="py-2 px-1"></td>
<td className="py-2 px-1"></td>
<td className="py-2 pl-1">
<Button size="sm" className="h-7 text-xs" onClick={applyColumnFilters}>
<Search className="h-3 w-3" />
</Button>
</td>
{canEdit && <td className="py-2"></td>}
</tr>
)}
</thead>
<tbody className="text-sm">
{data?.data.map((cfdi) => (