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:
@@ -15,6 +15,8 @@ export async function getCfdis(req: Request, res: Response, next: NextFunction)
|
||||
fechaInicio: req.query.fechaInicio as string,
|
||||
fechaFin: req.query.fechaFin as string,
|
||||
rfc: req.query.rfc as string,
|
||||
emisor: req.query.emisor as string,
|
||||
receptor: req.query.receptor as string,
|
||||
search: req.query.search as string,
|
||||
page: parseInt(req.query.page as string) || 1,
|
||||
limit: parseInt(req.query.limit as string) || 20,
|
||||
|
||||
@@ -35,6 +35,16 @@ export async function getCfdis(schema: string, filters: CfdiFilters): Promise<Cf
|
||||
params.push(`%${filters.rfc}%`);
|
||||
}
|
||||
|
||||
if (filters.emisor) {
|
||||
whereClause += ` AND (rfc_emisor ILIKE $${paramIndex} OR nombre_emisor ILIKE $${paramIndex++})`;
|
||||
params.push(`%${filters.emisor}%`);
|
||||
}
|
||||
|
||||
if (filters.receptor) {
|
||||
whereClause += ` AND (rfc_receptor ILIKE $${paramIndex} OR nombre_receptor ILIKE $${paramIndex++})`;
|
||||
params.push(`%${filters.receptor}%`);
|
||||
}
|
||||
|
||||
if (filters.search) {
|
||||
whereClause += ` AND (uuid_fiscal ILIKE $${paramIndex} OR nombre_emisor ILIKE $${paramIndex} OR nombre_receptor ILIKE $${paramIndex++})`;
|
||||
params.push(`%${filters.search}%`);
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -9,6 +9,8 @@ export async function getCfdis(filters: CfdiFilters): Promise<CfdiListResponse>
|
||||
if (filters.fechaInicio) params.set('fechaInicio', filters.fechaInicio);
|
||||
if (filters.fechaFin) params.set('fechaFin', filters.fechaFin);
|
||||
if (filters.rfc) params.set('rfc', filters.rfc);
|
||||
if (filters.emisor) params.set('emisor', filters.emisor);
|
||||
if (filters.receptor) params.set('receptor', filters.receptor);
|
||||
if (filters.search) params.set('search', filters.search);
|
||||
if (filters.page) params.set('page', filters.page.toString());
|
||||
if (filters.limit) params.set('limit', filters.limit.toString());
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"db:seed": "turbo run db:seed"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pg": "^8.18.0",
|
||||
"turbo": "^2.3.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface CfdiFilters {
|
||||
fechaInicio?: string;
|
||||
fechaFin?: string;
|
||||
rfc?: string;
|
||||
emisor?: string;
|
||||
receptor?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
|
||||
110
pnpm-lock.yaml
generated
110
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
pg:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0
|
||||
turbo:
|
||||
specifier: ^2.3.0
|
||||
version: 2.7.5
|
||||
@@ -1948,6 +1951,40 @@ packages:
|
||||
performance-now@2.1.0:
|
||||
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
|
||||
|
||||
pg-connection-string@2.11.0:
|
||||
resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==}
|
||||
|
||||
pg-int8@1.0.1:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
pg-pool@3.11.0:
|
||||
resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
|
||||
pg-protocol@1.11.0:
|
||||
resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==}
|
||||
|
||||
pg-types@2.2.0:
|
||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg@8.18.0:
|
||||
resolution: {integrity: sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
peerDependencies:
|
||||
pg-native: '>=3.0.1'
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
|
||||
pgpass@1.0.5:
|
||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -2018,6 +2055,22 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postgres-array@2.0.0:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postgres-bytea@1.0.1:
|
||||
resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-date@1.0.7:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
prisma@5.22.0:
|
||||
resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==}
|
||||
engines: {node: '>=16.13'}
|
||||
@@ -2226,6 +2279,10 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
||||
stackblur-canvas@2.7.0:
|
||||
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
|
||||
engines: {node: '>=0.1.14'}
|
||||
@@ -2442,6 +2499,10 @@ packages:
|
||||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
zip-stream@4.1.1:
|
||||
resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -4130,6 +4191,41 @@ snapshots:
|
||||
performance-now@2.1.0:
|
||||
optional: true
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
optional: true
|
||||
|
||||
pg-connection-string@2.11.0: {}
|
||||
|
||||
pg-int8@1.0.1: {}
|
||||
|
||||
pg-pool@3.11.0(pg@8.18.0):
|
||||
dependencies:
|
||||
pg: 8.18.0
|
||||
|
||||
pg-protocol@1.11.0: {}
|
||||
|
||||
pg-types@2.2.0:
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
postgres-array: 2.0.0
|
||||
postgres-bytea: 1.0.1
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
|
||||
pg@8.18.0:
|
||||
dependencies:
|
||||
pg-connection-string: 2.11.0
|
||||
pg-pool: 3.11.0(pg@8.18.0)
|
||||
pg-protocol: 1.11.0
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
optionalDependencies:
|
||||
pg-cloudflare: 1.3.0
|
||||
|
||||
pgpass@1.0.5:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
@@ -4184,6 +4280,16 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postgres-array@2.0.0: {}
|
||||
|
||||
postgres-bytea@1.0.1: {}
|
||||
|
||||
postgres-date@1.0.7: {}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
prisma@5.22.0:
|
||||
dependencies:
|
||||
'@prisma/engines': 5.22.0
|
||||
@@ -4433,6 +4539,8 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
stackblur-canvas@2.7.0:
|
||||
optional: true
|
||||
|
||||
@@ -4659,6 +4767,8 @@ snapshots:
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
zip-stream@4.1.1:
|
||||
dependencies:
|
||||
archiver-utils: 3.0.4
|
||||
|
||||
Reference in New Issue
Block a user