feat: Add poblacion_apta service to docker-compose, enhance API gateway for new service integration, and update README with service details. Introduce new database tables for poblacion_apta_jobs and poblacion_apta_rows, and implement functionality for handling cop/mod calculations in the cleaning service.
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user