Files
IPSExtractor/services/desktop_app/src/renderer/components/PoblacionAptaResultsTable.tsx
T

233 lines
7.1 KiB
TypeScript

import { Search } from "lucide-react";
import { useMemo, useState } from "react";
type Row = Record<string, unknown>;
type FilterKey = "all" | "aptos" | "no_aptos";
interface Props {
rows: Row[];
/** Resumen opcional del backend; si falta se calcula desde filas. */
summary?: {
total?: number;
paso_bot?: number;
con_error?: number;
duplicados?: number;
};
}
const PREFERRED_COLUMNS = [
"radicado",
"total_cargos",
"autorizacion",
"ingreso",
"tipo_documento",
"documento",
"tipo_servicio",
"copago",
"cuota_moderadora",
"OBSERVACION",
"archivo_origen",
];
const COLUMN_LABELS: Record<string, string> = {
radicado: "Radicado",
total_cargos: "Bruto",
autorizacion: "Autorización",
ingreso: "Fecha",
tipo_documento: "Tipo ID",
documento: "Documento",
tipo_servicio: "Servicio",
copago: "Copago",
cuota_moderadora: "Moderadora",
OBSERVACION: "Observación",
archivo_origen: "Origen",
tipo_archivo: "Tipo archivo",
};
const HIDDEN_COLUMNS = new Set(["cleaning_job_id", "tipo_archivo"]);
function cellValue(row: Row, col: string): string {
const v = row[col];
if (v === null || v === undefined) return "";
return String(v);
}
function observacionOf(row: Row): string {
return cellValue(row, "OBSERVACION").trim();
}
function isApto(row: Row): boolean {
return observacionOf(row) === "PASO_BOT";
}
function isDuplicado(row: Row): boolean {
return observacionOf(row).includes("RADICADO_DUPLICADO");
}
function orderColumns(rows: Row[]): string[] {
const present = new Set(rows.flatMap((r) => Object.keys(r)));
const ordered: string[] = [];
for (const c of PREFERRED_COLUMNS) {
if (present.has(c) && !HIDDEN_COLUMNS.has(c)) ordered.push(c);
}
for (const c of present) {
if (!ordered.includes(c) && !HIDDEN_COLUMNS.has(c)) ordered.push(c);
}
return ordered;
}
function ObsBadge({ value }: { value: string }) {
if (!value) return <span className="extraction-empty-cell">—</span>;
const apto = value === "PASO_BOT";
const dup = value.includes("RADICADO_DUPLICADO");
const cls = apto
? "pa-obs-badge pa-obs-badge--apto"
: dup
? "pa-obs-badge pa-obs-badge--dup"
: "pa-obs-badge pa-obs-badge--error";
return <span className={cls} title={value}>{value}</span>;
}
export function PoblacionAptaResultsTable({ rows, summary: summaryProp }: Props) {
const [filter, setFilter] = useState<FilterKey>("all");
const [search, setSearch] = useState("");
const columns = useMemo(() => orderColumns(rows), [rows]);
const summary = useMemo(() => {
const aptos = rows.filter(isApto).length;
const dups = rows.filter(isDuplicado).length;
return {
total: summaryProp?.total ?? rows.length,
aptos: summaryProp?.paso_bot ?? aptos,
noAptos: summaryProp?.con_error ?? rows.length - aptos,
duplicados: summaryProp?.duplicados ?? dups,
};
}, [rows, summaryProp]);
const filtered = useMemo(() => {
let list = rows;
if (filter === "aptos") list = list.filter(isApto);
else if (filter === "no_aptos") list = list.filter((r) => !isApto(r));
const q = search.trim().toLowerCase();
if (q) {
list = list.filter((r) =>
columns.some((c) => cellValue(r, c).toLowerCase().includes(q)),
);
}
return list;
}, [rows, columns, filter, search]);
const FILTERS: { key: FilterKey; label: string; count: number }[] = [
{ key: "all", label: "Todos", count: summary.total },
{ key: "aptos", label: "Aptos", count: summary.aptos },
{ key: "no_aptos", label: "No aptos", count: summary.noAptos },
];
return (
<div className="extraction-results pa-results">
<div className="wiz-stats-row count-results-summary pa-stats-row">
<div className="wiz-stat">
<span className="wiz-stat-value">{summary.total}</span>
<span className="wiz-stat-label">Total</span>
</div>
<div className="wiz-stat count-stat--valid">
<span className="wiz-stat-value">{summary.aptos}</span>
<span className="wiz-stat-label">Aptos</span>
</div>
<div className="wiz-stat count-stat--invalid">
<span className="wiz-stat-value">{summary.noAptos}</span>
<span className="wiz-stat-label">No aptos</span>
</div>
{summary.duplicados > 0 && (
<div className="wiz-stat pa-stat--dup">
<span className="wiz-stat-value">{summary.duplicados}</span>
<span className="wiz-stat-label">Duplicados</span>
</div>
)}
</div>
<div className="count-toolbar">
<div className="count-filter-tabs">
{FILTERS.map(({ key, label, count }) => (
<button
key={key}
type="button"
className={`count-filter-tab ${filter === key ? "active" : ""}`}
onClick={() => setFilter(key)}
>
{label}
<span className="count-filter-count">{count}</span>
</button>
))}
</div>
<div className="count-toolbar-right">
<div className="wiz-search-box">
<Search size={14} />
<input
placeholder="Buscar radicado, documento…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
</div>
<div className="results-table-wrapper extraction-table-wrap">
<table className="results-table pa-table">
<thead>
<tr>
{columns.map((c) => (
<th key={c} className={c === "OBSERVACION" ? "pa-th-obs" : undefined}>
{COLUMN_LABELS[c] ?? c}
</th>
))}
</tr>
</thead>
<tbody>
{filtered.length === 0 ? (
<tr>
<td colSpan={Math.max(columns.length, 1)} className="extraction-empty">
{search
? "No hay filas que coincidan con la búsqueda."
: "No hay filas en esta categoría."}
</td>
</tr>
) : (
filtered.map((row, i) => {
const apto = isApto(row);
return (
<tr
key={i}
className={apto ? "pa-row--apto" : "pa-row--no-apto"}
>
{columns.map((c) => (
<td key={c} className={c === "OBSERVACION" ? "pa-td-obs" : undefined}>
{c === "OBSERVACION" ? (
<ObsBadge value={observacionOf(row)} />
) : cellValue(row, c).trim() === "" ? (
<span className="extraction-empty-cell">—</span>
) : (
cellValue(row, c)
)}
</td>
))}
</tr>
);
})
)}
</tbody>
</table>
</div>
{filtered.length !== rows.length && (
<p className="extraction-filter-hint">
Mostrando {filtered.length} de {rows.length} filas
</p>
)}
</div>
);
}