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:
@@ -27,6 +27,7 @@ import { ExtractionResultsTable } from "../../components/ExtractionResultsTable"
|
||||
import type { ApiClient } from "../../lib/apiClient";
|
||||
import { loadCopModConfig } from "../../lib/cleaningSettings";
|
||||
import { downloadCsv, toCsv, buildExportCsvFilename } from "../../lib/csv";
|
||||
import { taskTimingLabel, timingDuration } from "../../lib/time";
|
||||
|
||||
type SourceMode = "extraction" | "csv";
|
||||
|
||||
@@ -43,20 +44,6 @@ function batchIdFromTask(task: Task): string {
|
||||
return String((task.result as Record<string, unknown>)?.extraction_batch_id ?? "");
|
||||
}
|
||||
|
||||
function formatTaskDate(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleString("es-CO", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
export function CleaningWizard({
|
||||
client,
|
||||
job,
|
||||
@@ -78,6 +65,8 @@ export function CleaningWizard({
|
||||
const [analysis, setAnalysis] = useState<CleaningAnalysisResponse | null>(null);
|
||||
const [rulesPerColumn, setRulesPerColumn] = useState<Record<string, string[]>>({});
|
||||
const [copModFechaColumn, setCopModFechaColumn] = useState<Record<string, string>>({});
|
||||
const [copModBrutoColumn, setCopModBrutoColumn] = useState<Record<string, string>>({});
|
||||
const [copModNetoColumn, setCopModNetoColumn] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -179,6 +168,26 @@ export function CleaningWizard({
|
||||
return preferred?.name ?? "";
|
||||
}
|
||||
|
||||
function guessBrutoColumn(columns: CleaningColumnAnalysis[], preferCol?: string): string {
|
||||
if (preferCol && /bruto|total[_\s]?cargos|valor[_\s]?bruto|valor[_\s]?factura/i.test(preferCol)) {
|
||||
return preferCol;
|
||||
}
|
||||
const preferred =
|
||||
columns.find((c) => /valor[_\s]?bruto|^bruto$|total[_\s]?cargos/i.test(c.name)) ??
|
||||
columns.find((c) => /bruto/i.test(c.name)) ??
|
||||
(preferCol ? columns.find((c) => c.name === preferCol) : undefined);
|
||||
return preferred?.name ?? preferCol ?? "";
|
||||
}
|
||||
|
||||
function guessNetoColumn(columns: CleaningColumnAnalysis[], brutoCol: string): string {
|
||||
const preferred =
|
||||
columns.find(
|
||||
(c) => c.name !== brutoCol && /valor[_\s]?neto|^neto$|valor[_\s]?pagado/i.test(c.name),
|
||||
) ??
|
||||
columns.find((c) => c.name !== brutoCol && /neto/i.test(c.name));
|
||||
return preferred?.name ?? "";
|
||||
}
|
||||
|
||||
function toggleRule(column: string, ruleId: string) {
|
||||
setRulesPerColumn((prev) => {
|
||||
const current = prev[column] ?? [];
|
||||
@@ -202,6 +211,27 @@ export function CleaningWizard({
|
||||
return guess ? { ...prev, [column]: guess } : prev;
|
||||
});
|
||||
}
|
||||
if (ruleId === "cop_mod_bruto_neto") {
|
||||
const current = rulesPerColumn[column] ?? [];
|
||||
const turningOn = !current.includes(ruleId);
|
||||
if (!turningOn) {
|
||||
setCopModBrutoColumn((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[column];
|
||||
return next;
|
||||
});
|
||||
setCopModNetoColumn((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[column];
|
||||
return next;
|
||||
});
|
||||
} else if (analysis) {
|
||||
const bruto = guessBrutoColumn(analysis.columns, column);
|
||||
const neto = guessNetoColumn(analysis.columns, bruto || column);
|
||||
setCopModBrutoColumn((prev) => (prev[column] ? prev : { ...prev, [column]: bruto || column }));
|
||||
setCopModNetoColumn((prev) => (prev[column] ? prev : { ...prev, [column]: neto }));
|
||||
}
|
||||
}
|
||||
setSampleMode("after");
|
||||
}
|
||||
|
||||
@@ -213,20 +243,38 @@ export function CleaningWizard({
|
||||
}
|
||||
setRulesPerColumn(suggested);
|
||||
setCopModFechaColumn({});
|
||||
setCopModBrutoColumn({});
|
||||
setCopModNetoColumn({});
|
||||
}
|
||||
|
||||
function cleaningExtraPayload(): {
|
||||
cop_mod_fecha_column?: Record<string, string>;
|
||||
cop_mod_bruto_column?: Record<string, string>;
|
||||
cop_mod_neto_column?: Record<string, string>;
|
||||
cop_mod_config: ReturnType<typeof loadCopModConfig>;
|
||||
} {
|
||||
const fechaEntries = Object.entries(copModFechaColumn).filter(
|
||||
([col, fecha]) =>
|
||||
Boolean(fecha) && (rulesPerColumn[col] ?? []).includes("cop_mod_split"),
|
||||
);
|
||||
const brutoEntries = Object.entries(copModBrutoColumn).filter(
|
||||
([col, bruto]) =>
|
||||
Boolean(bruto) && (rulesPerColumn[col] ?? []).includes("cop_mod_bruto_neto"),
|
||||
);
|
||||
const netoEntries = Object.entries(copModNetoColumn).filter(
|
||||
([col, neto]) =>
|
||||
Boolean(neto) && (rulesPerColumn[col] ?? []).includes("cop_mod_bruto_neto"),
|
||||
);
|
||||
return {
|
||||
...(fechaEntries.length > 0
|
||||
? { cop_mod_fecha_column: Object.fromEntries(fechaEntries) }
|
||||
: {}),
|
||||
...(brutoEntries.length > 0
|
||||
? { cop_mod_bruto_column: Object.fromEntries(brutoEntries) }
|
||||
: {}),
|
||||
...(netoEntries.length > 0
|
||||
? { cop_mod_neto_column: Object.fromEntries(netoEntries) }
|
||||
: {}),
|
||||
cop_mod_config: loadCopModConfig(),
|
||||
};
|
||||
}
|
||||
@@ -241,17 +289,24 @@ export function CleaningWizard({
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Columnas nuevas respecto al análisis (p. ej. *_doc_tipo / *_doc_numeros). */
|
||||
/** Columnas nuevas respecto al análisis (p. ej. *_doc_tipo / *_doc_numeros / cop/mod). */
|
||||
function derivedColumnNames(sourceCol: string): string[] {
|
||||
if (!preview) return [];
|
||||
const original = new Set(analysis?.columns.map((c) => c.name) ?? []);
|
||||
const selected = rulesPerColumn[sourceCol] ?? [];
|
||||
return preview.columns
|
||||
.map((c) => c.name)
|
||||
.filter(
|
||||
(name) =>
|
||||
!original.has(name) &&
|
||||
name.toLowerCase().startsWith(`${sourceCol.toLowerCase()}_`),
|
||||
);
|
||||
.filter((name) => {
|
||||
if (original.has(name)) return false;
|
||||
if (name.toLowerCase().startsWith(`${sourceCol.toLowerCase()}_`)) return true;
|
||||
if (
|
||||
selected.includes("cop_mod_bruto_neto") &&
|
||||
(name === "cop/mod" || name.startsWith("cop/mod_"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/** Orden: columna activa → columnas derivadas → resto (sin cortar). */
|
||||
@@ -309,7 +364,7 @@ export function CleaningWizard({
|
||||
return () => {
|
||||
if (previewTimerRef.current) clearTimeout(previewTimerRef.current);
|
||||
};
|
||||
}, [step, analysis, rulesPerColumn, copModFechaColumn, batchId, client]);
|
||||
}, [step, analysis, rulesPerColumn, copModFechaColumn, copModBrutoColumn, copModNetoColumn, batchId, client]);
|
||||
|
||||
async function loadAnalysis(mode: SourceMode, batch?: string, file?: File | null) {
|
||||
setLoading(true);
|
||||
@@ -393,6 +448,12 @@ export function CleaningWizard({
|
||||
if (extra.cop_mod_fecha_column) {
|
||||
form.append("cop_mod_fecha_column", JSON.stringify(extra.cop_mod_fecha_column));
|
||||
}
|
||||
if (extra.cop_mod_bruto_column) {
|
||||
form.append("cop_mod_bruto_column", JSON.stringify(extra.cop_mod_bruto_column));
|
||||
}
|
||||
if (extra.cop_mod_neto_column) {
|
||||
form.append("cop_mod_neto_column", JSON.stringify(extra.cop_mod_neto_column));
|
||||
}
|
||||
form.append("cop_mod_config", JSON.stringify(extra.cop_mod_config));
|
||||
cleaningJob = await client.runCleaningFromCsv(form);
|
||||
}
|
||||
@@ -528,7 +589,7 @@ export function CleaningWizard({
|
||||
{task.title ?? `Extracción ${extractionTasks.length - index}`}
|
||||
</div>
|
||||
<div className="cleaning-source-item-meta">
|
||||
{formatTaskDate(task.created_at)}
|
||||
{taskTimingLabel(task)}
|
||||
{index === 0 ? " · más reciente" : ""}
|
||||
</div>
|
||||
</div>
|
||||
@@ -791,6 +852,63 @@ export function CleaningWizard({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{selected.includes("cop_mod_bruto_neto") && (
|
||||
<div className="cleaning-copmod-fecha">
|
||||
<label className="wiz-field-label" htmlFor={`copmod-bruto-${col.name}`}>
|
||||
Valor bruto
|
||||
</label>
|
||||
<select
|
||||
id={`copmod-bruto-${col.name}`}
|
||||
className="form-input"
|
||||
value={copModBrutoColumn[col.name] ?? col.name}
|
||||
onChange={(e) =>
|
||||
setCopModBrutoColumn((prev) => ({
|
||||
...prev,
|
||||
[col.name]: e.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{analysis.columns.map((c) => (
|
||||
<option key={c.name} value={c.name}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label
|
||||
className="wiz-field-label"
|
||||
htmlFor={`copmod-neto-${col.name}`}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
Valor neto
|
||||
</label>
|
||||
<select
|
||||
id={`copmod-neto-${col.name}`}
|
||||
className="form-input"
|
||||
value={copModNetoColumn[col.name] ?? ""}
|
||||
onChange={(e) =>
|
||||
setCopModNetoColumn((prev) => ({
|
||||
...prev,
|
||||
[col.name]: e.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="">Selecciona columna neto…</option>
|
||||
{analysis.columns
|
||||
.filter(
|
||||
(c) =>
|
||||
c.name !== (copModBrutoColumn[col.name] ?? col.name),
|
||||
)
|
||||
.map((c) => (
|
||||
<option key={c.name} value={c.name}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="form-hint">
|
||||
Se crea la columna <strong>cop/mod</strong> = bruto − neto.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -911,6 +1029,7 @@ export function CleaningWizard({
|
||||
Limpieza completada
|
||||
{resultJob.log?.length ? ` · ${resultJob.log.length} operaciones` : null}
|
||||
{resultRows.length > 0 ? ` · ${resultRows.length} filas` : null}
|
||||
{` · duración ${timingDuration(resultJob)}`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user