From 29ac067a82eb0e7430b9c981e79b5e0574001e50 Mon Sep 17 00:00:00 2001 From: Consultoria AS Date: Tue, 17 Feb 2026 06:21:13 +0000 Subject: [PATCH] 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 --- apps/api/src/controllers/cfdi.controller.ts | 2 + apps/api/src/services/cfdi.service.ts | 10 ++ apps/web/app/(dashboard)/cfdi/page.tsx | 135 ++++++++++++++++++-- apps/web/lib/api/cfdi.ts | 2 + package.json | 1 + packages/shared/src/types/cfdi.ts | 2 + pnpm-lock.yaml | 110 ++++++++++++++++ 7 files changed, 248 insertions(+), 14 deletions(-) diff --git a/apps/api/src/controllers/cfdi.controller.ts b/apps/api/src/controllers/cfdi.controller.ts index d8872d2..2b1f689 100644 --- a/apps/api/src/controllers/cfdi.controller.ts +++ b/apps/api/src/controllers/cfdi.controller.ts @@ -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, diff --git a/apps/api/src/services/cfdi.service.ts b/apps/api/src/services/cfdi.service.ts index 8ca05d0..c984742 100644 --- a/apps/api/src/services/cfdi.service.ts +++ b/apps/api/src/services/cfdi.service.ts @@ -35,6 +35,16 @@ export async function getCfdis(schema: string, filters: CfdiFilters): Promise(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 */} - - - CFDIs ({data?.total || 0}) - +
+ + + CFDIs ({data?.total || 0}) + +
+ {hasActiveColumnFilters && ( + + )} + +
+
{isLoading ? ( @@ -1079,16 +1134,68 @@ export default function CfdiPage() { - - - - - - - - - {canEdit && } + + + + + + + + + {canEdit && } + {showColumnFilters && ( + + + + + + + + + + {canEdit && } + + )} {data?.data.map((cfdi) => ( diff --git a/apps/web/lib/api/cfdi.ts b/apps/web/lib/api/cfdi.ts index 7cca27a..da024c0 100644 --- a/apps/web/lib/api/cfdi.ts +++ b/apps/web/lib/api/cfdi.ts @@ -9,6 +9,8 @@ export async function getCfdis(filters: CfdiFilters): Promise 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()); diff --git a/package.json b/package.json index b5c6783..6a23c98 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "db:seed": "turbo run db:seed" }, "devDependencies": { + "pg": "^8.18.0", "turbo": "^2.3.0", "typescript": "^5.3.0" }, diff --git a/packages/shared/src/types/cfdi.ts b/packages/shared/src/types/cfdi.ts index 92d8cf6..b2944c3 100644 --- a/packages/shared/src/types/cfdi.ts +++ b/packages/shared/src/types/cfdi.ts @@ -37,6 +37,8 @@ export interface CfdiFilters { fechaInicio?: string; fechaFin?: string; rfc?: string; + emisor?: string; + receptor?: string; search?: string; page?: number; limit?: number; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c559f1..8e71b8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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
FechaTipoFolioEmisorReceptorTotalEstadoFechaTipoFolioEmisorReceptorTotalEstado
+
+ setColumnFilters({ ...columnFilters, fechaInicio: e.target.value })} + onKeyDown={(e) => e.key === 'Enter' && applyColumnFilters()} + /> + setColumnFilters({ ...columnFilters, fechaFin: e.target.value })} + onKeyDown={(e) => e.key === 'Enter' && applyColumnFilters()} + /> +
+
+ setColumnFilters({ ...columnFilters, emisor: e.target.value })} + onKeyDown={(e) => e.key === 'Enter' && applyColumnFilters()} + /> + + setColumnFilters({ ...columnFilters, receptor: e.target.value })} + onKeyDown={(e) => e.key === 'Enter' && applyColumnFilters()} + /> + + +