233 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|