feat(cfdi): agrega columna y filtro no_identificacion en tabla Conceptos

- Backend (cfdi.service.ts): getConceptosList ahora soporta filtro noIdentificacion
  via cc.no_identificacion ILIKE

- Frontend API (cfdi.ts): ConceptosFilters incluye noIdentificacion; se envía
  como query param

- Frontend página (cfdi/page.tsx):
  * Nuevo estado noIdentificacion en conceptosFilters
  * Nueva columna 'No. Identificación' en header de tabla con Popover filtro
  * Celda no_identificacion renderizada en cada fila
  * Export a Excel respeta el nuevo filtro
This commit is contained in:
Horux Dev
2026-05-15 23:22:38 +00:00
parent 7b1f60cbf2
commit 552a7c7716
3 changed files with 35 additions and 2 deletions

View File

@@ -181,6 +181,7 @@ export async function getConceptosList(
uuidLike?: string; uuidLike?: string;
claveProdServ?: string; claveProdServ?: string;
descripcionConcepto?: string; descripcionConcepto?: string;
noIdentificacion?: string;
orderBy?: 'fecha' | 'importe'; orderBy?: 'fecha' | 'importe';
orderDir?: 'asc' | 'desc'; orderDir?: 'asc' | 'desc';
}, },
@@ -261,6 +262,10 @@ export async function getConceptosList(
whereClause += ` AND cc.descripcion ILIKE $${paramIndex++}`; whereClause += ` AND cc.descripcion ILIKE $${paramIndex++}`;
params.push(`%${filters.descripcionConcepto}%`); params.push(`%${filters.descripcionConcepto}%`);
} }
if (filters.noIdentificacion) {
whereClause += ` AND cc.no_identificacion ILIKE $${paramIndex++}`;
params.push(`%${filters.noIdentificacion}%`);
}
// Ordenamiento configurable. Default: fecha DESC, id ASC (estable). // Ordenamiento configurable. Default: fecha DESC, id ASC (estable).
const orderDir = filters.orderDir === 'asc' ? 'ASC' : 'DESC'; const orderDir = filters.orderDir === 'asc' ? 'ASC' : 'DESC';

View File

@@ -325,10 +325,11 @@ export default function CfdiPage() {
uuidLike: string; uuidLike: string;
claveProdServ: string; claveProdServ: string;
descripcionConcepto: string; descripcionConcepto: string;
noIdentificacion: string;
orderBy?: 'fecha' | 'importe'; orderBy?: 'fecha' | 'importe';
orderDir?: 'asc' | 'desc'; orderDir?: 'asc' | 'desc';
}>({ uuidLike: '', claveProdServ: '', descripcionConcepto: '' }); }>({ uuidLike: '', claveProdServ: '', descripcionConcepto: '', noIdentificacion: '' });
const [conceptosOpenFilter, setConceptosOpenFilter] = useState<'uuid' | 'clave' | 'descripcion' | null>(null); const [conceptosOpenFilter, setConceptosOpenFilter] = useState<'uuid' | 'clave' | 'descripcion' | 'noIdentificacion' | null>(null);
const conceptosQuery = useQuery({ const conceptosQuery = useQuery({
queryKey: ['cfdi-conceptos', filters, selectedContribuyenteId, conceptosFilters], queryKey: ['cfdi-conceptos', filters, selectedContribuyenteId, conceptosFilters],
@@ -338,6 +339,7 @@ export default function CfdiPage() {
uuidLike: conceptosFilters.uuidLike || undefined, uuidLike: conceptosFilters.uuidLike || undefined,
claveProdServ: conceptosFilters.claveProdServ || undefined, claveProdServ: conceptosFilters.claveProdServ || undefined,
descripcionConcepto: conceptosFilters.descripcionConcepto || undefined, descripcionConcepto: conceptosFilters.descripcionConcepto || undefined,
noIdentificacion: conceptosFilters.noIdentificacion || undefined,
orderBy: conceptosFilters.orderBy, orderBy: conceptosFilters.orderBy,
orderDir: conceptosFilters.orderDir, orderDir: conceptosFilters.orderDir,
}), }),
@@ -481,6 +483,7 @@ export default function CfdiPage() {
uuidLike: conceptosFilters.uuidLike || undefined, uuidLike: conceptosFilters.uuidLike || undefined,
claveProdServ: conceptosFilters.claveProdServ || undefined, claveProdServ: conceptosFilters.claveProdServ || undefined,
descripcionConcepto: conceptosFilters.descripcionConcepto || undefined, descripcionConcepto: conceptosFilters.descripcionConcepto || undefined,
noIdentificacion: conceptosFilters.noIdentificacion || undefined,
orderBy: conceptosFilters.orderBy, orderBy: conceptosFilters.orderBy,
orderDir: conceptosFilters.orderDir, orderDir: conceptosFilters.orderDir,
page: 1, page: 1,
@@ -1647,6 +1650,28 @@ export default function CfdiPage() {
</Popover> </Popover>
</div> </div>
</th> </th>
<th className="pb-3 font-medium">
<div className="flex items-center gap-1 justify-center">
No. Identificación
<Popover open={conceptosOpenFilter === 'noIdentificacion'} onOpenChange={(open) => setConceptosOpenFilter(open ? 'noIdentificacion' : null)}>
<PopoverTrigger asChild>
<button className={`p-1 rounded hover:bg-muted ${conceptosFilters.noIdentificacion ? 'text-primary' : ''}`}>
<Filter className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-64" align="start">
<div className="space-y-3">
<h4 className="font-medium text-sm">Filtrar por No. Identificación</h4>
<Input className="h-8 text-sm font-mono" placeholder="Ej: PROD-001" value={conceptosFilters.noIdentificacion} onChange={(e) => setConceptosFilters({ ...conceptosFilters, noIdentificacion: e.target.value })} />
<div className="flex gap-2">
<Button size="sm" className="flex-1" onClick={() => { setFilters({ ...filters, page: 1 }); setConceptosOpenFilter(null); }}>Aplicar</Button>
{conceptosFilters.noIdentificacion && <Button size="sm" variant="outline" onClick={() => { setConceptosFilters({ ...conceptosFilters, noIdentificacion: '' }); setFilters({ ...filters, page: 1 }); }}>Limpiar</Button>}
</div>
</div>
</PopoverContent>
</Popover>
</div>
</th>
<th className="pb-3 font-medium">RFC Emisor</th> <th className="pb-3 font-medium">RFC Emisor</th>
<th className="pb-3 font-medium">RFC Receptor</th> <th className="pb-3 font-medium">RFC Receptor</th>
<th className="pb-3 font-medium text-right">Cantidad</th> <th className="pb-3 font-medium text-right">Cantidad</th>
@@ -1676,6 +1701,7 @@ export default function CfdiPage() {
<td className="py-2 font-mono text-xs" title={row.uuid}>{row.uuid?.substring(0, 8) || '-'}</td> <td className="py-2 font-mono text-xs" title={row.uuid}>{row.uuid?.substring(0, 8) || '-'}</td>
<td className="py-2 font-mono text-xs">{row.clave_prod_serv || '-'}</td> <td className="py-2 font-mono text-xs">{row.clave_prod_serv || '-'}</td>
<td className="py-2 text-left max-w-[280px] truncate" title={row.descripcion}>{row.descripcion}</td> <td className="py-2 text-left max-w-[280px] truncate" title={row.descripcion}>{row.descripcion}</td>
<td className="py-2 font-mono text-xs" title={row.no_identificacion || ''}>{row.no_identificacion || '-'}</td>
<td className="py-2 font-mono text-xs">{row.rfcEmisor}</td> <td className="py-2 font-mono text-xs">{row.rfcEmisor}</td>
<td className="py-2 font-mono text-xs">{row.rfcReceptor}</td> <td className="py-2 font-mono text-xs">{row.rfcReceptor}</td>
<td className="py-2 text-right">{Number(row.cantidad ?? 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })}</td> <td className="py-2 text-right">{Number(row.cantidad ?? 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })}</td>

View File

@@ -37,6 +37,7 @@ export interface ConceptosFilters extends CfdiFilters {
uuidLike?: string; uuidLike?: string;
claveProdServ?: string; claveProdServ?: string;
descripcionConcepto?: string; descripcionConcepto?: string;
noIdentificacion?: string;
orderBy?: 'fecha' | 'importe'; orderBy?: 'fecha' | 'importe';
orderDir?: 'asc' | 'desc'; orderDir?: 'asc' | 'desc';
} }
@@ -58,6 +59,7 @@ export async function getConceptosList(filters: ConceptosFilters): Promise<Conce
if (filters.uuidLike) params.set('uuidLike', filters.uuidLike); if (filters.uuidLike) params.set('uuidLike', filters.uuidLike);
if (filters.claveProdServ) params.set('claveProdServ', filters.claveProdServ); if (filters.claveProdServ) params.set('claveProdServ', filters.claveProdServ);
if (filters.descripcionConcepto) params.set('descripcionConcepto', filters.descripcionConcepto); if (filters.descripcionConcepto) params.set('descripcionConcepto', filters.descripcionConcepto);
if (filters.noIdentificacion) params.set('noIdentificacion', filters.noIdentificacion);
if (filters.orderBy) params.set('orderBy', filters.orderBy); if (filters.orderBy) params.set('orderBy', filters.orderBy);
if (filters.orderDir) params.set('orderDir', filters.orderDir); if (filters.orderDir) params.set('orderDir', filters.orderDir);
const response = await apiClient.get<ConceptosListResponse>(`/cfdi/conceptos?${params}`); const response = await apiClient.get<ConceptosListResponse>(`/cfdi/conceptos?${params}`);