Files
IPSExtractor/services/desktop_app/src/renderer/views/JobView.tsx
T

255 lines
11 KiB
TypeScript

import { Loader2, Pencil, X } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { ApiClient } from "../lib/apiClient";
import type { BackgroundProgress, Job, Task } from "../../shared/types";
import type { JobSection } from "../types/jobSection";
import { BackgroundProgressBanner } from "../components/BackgroundProgressBanner";
import { CountingWizardModule } from "../modules/counting/CountingWizardModule";
import { CleaningWizardModule } from "../modules/cleaning/CleaningWizardModule";
import { PoblacionAptaWizardModule } from "../modules/poblacion_apta/PoblacionAptaWizardModule";
import { JobExtractionWizard } from "../modules/extraction/JobExtractionWizard";
import { DownloadsWizardModule } from "../modules/downloads/DownloadsWizardModule";
import { DigitadorWizardModule } from "../modules/digitador/DigitadorWizardModule";
import { JobResultsView } from "./JobResultsView";
import { JobCostsView } from "./JobCostsView";
interface Props {
client: ApiClient;
job: Job;
section: JobSection;
sectionContext: Record<string, unknown>;
onActivateSection: (section: JobSection, context?: Record<string, unknown>) => void;
onJobUpdated: (job: Job) => void;
}
export function JobView({ client, job, section, sectionContext, onActivateSection, onJobUpdated }: Props) {
const [tasks, setTasks] = useState<Task[]>([]);
const [editingTitle, setEditingTitle] = useState(false);
const [titleDraft, setTitleDraft] = useState(job.title ?? "");
const [savingTitle, setSavingTitle] = useState(false);
const [countingProgress, setCountingProgress] = useState<BackgroundProgress | null>(null);
const [extractionProgress, setExtractionProgress] = useState<BackgroundProgress | null>(null);
const [cleaningProgress, setCleaningProgress] = useState<BackgroundProgress | null>(null);
const [poblacionAptaProgress, setPoblacionAptaProgress] = useState<BackgroundProgress | null>(null);
const [costsRefreshing, setCostsRefreshing] = useState(false);
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
const backgroundRunning =
countingProgress?.running === true ||
extractionProgress?.running === true ||
cleaningProgress?.running === true ||
poblacionAptaProgress?.running === true;
const loadTasks = useCallback(async () => {
try {
const data = await client.jobTasks(job.id);
setTasks(data);
} catch { /* silent */ }
}, [client, job.id]);
const refreshCosts = useCallback(async () => {
setCostsRefreshing(true);
try { await loadTasks(); } finally { setCostsRefreshing(false); }
}, [loadTasks]);
useEffect(() => {
loadTasks();
return () => { if (pollingRef.current) clearInterval(pollingRef.current); };
}, [loadTasks]);
// Polling más frecuente mientras hay trabajo en segundo plano
useEffect(() => {
if (pollingRef.current) clearInterval(pollingRef.current);
const intervalMs = backgroundRunning ? 3000 : 8000;
pollingRef.current = setInterval(loadTasks, intervalMs);
return () => { if (pollingRef.current) clearInterval(pollingRef.current); };
}, [backgroundRunning, loadTasks]);
function handleTaskCreated(task: Task) {
setTasks((prev) => {
const idx = prev.findIndex((t) => t.id === task.id);
return idx >= 0
? prev.map((t, i) => (i === idx ? task : t))
: [...prev, task];
});
loadTasks();
}
async function saveTitle() {
if (!titleDraft.trim() || titleDraft === job.title) { setEditingTitle(false); return; }
setSavingTitle(true);
try {
const updated = await client.updateJob(job.id, { title: titleDraft.trim() });
onJobUpdated(updated);
} finally {
setSavingTitle(false);
setEditingTitle(false);
}
}
const activeBackground =
countingProgress?.running ? countingProgress
: extractionProgress?.running ? extractionProgress
: cleaningProgress?.running ? cleaningProgress
: poblacionAptaProgress?.running ? poblacionAptaProgress
: null;
return (
<div className="job-view">
<header className="job-content-header">
<div className="job-title-area">
{editingTitle ? (
<div className="job-title-edit">
<input
className="form-input job-title-input"
value={titleDraft}
onChange={(e) => setTitleDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") saveTitle();
if (e.key === "Escape") setEditingTitle(false);
}}
autoFocus
/>
<button type="button" className="btn btn-sm btn-primary" onClick={saveTitle} disabled={savingTitle}>
{savingTitle ? <Loader2 size={13} className="spin" /> : "Guardar"}
</button>
<button type="button" className="btn btn-sm" onClick={() => setEditingTitle(false)}>
<X size={13} />
</button>
</div>
) : (
<button
type="button"
className="job-title-btn"
onClick={() => { setTitleDraft(job.title ?? ""); setEditingTitle(true); }}
title="Editar nombre del trabajo"
>
<h1 className="job-title">{job.title ?? "Sin título"}</h1>
<Pencil size={13} className="job-title-edit-icon" />
</button>
)}
</div>
<div className="job-content-header-section">
{section === "counting" && "Conteo de servicios"}
{section === "extraction" && "Extracción de datos"}
{section === "cleaning" && "Limpieza de datos"}
{section === "poblacion_apta" && "Población apta"}
{section === "downloads" && "Bot de descarga"}
{section === "digitador" && "Bot digitador"}
{section === "costs" && "Costos de API"}
{section === "results" && "Resultados del trabajo"}
</div>
</header>
{/* Banner global visible en cualquier sección mientras corre un proceso */}
{activeBackground && section !== "results" && (
<div className="job-background-banner-wrap">
<BackgroundProgressBanner progress={activeBackground} compact />
<button
type="button"
className="btn btn-sm btn-ghost"
onClick={() => onActivateSection("results")}
>
Ver progreso en Resultados →
</button>
</div>
)}
{/*
Los módulos permanecen montados (solo ocultos) para que el conteo/subida
siga en segundo plano al cambiar de sección en el sidebar.
*/}
<div className="job-section-content">
<div className={`job-section-pane${section === "counting" ? "" : " job-section-pane--hidden"}`}>
<CountingWizardModule
client={client}
job={job}
context={sectionContext}
onTaskCreated={handleTaskCreated}
onProgress={(p) => setCountingProgress(p.running ? p : null)}
onActivateModule={(type, ctx) => onActivateSection(type as JobSection, ctx)}
/>
</div>
<div className={`job-section-pane${section === "extraction" ? "" : " job-section-pane--hidden"}`}>
<JobExtractionWizard
client={client}
job={job}
onTaskCreated={handleTaskCreated}
onProgress={(p) => setExtractionProgress(p.running ? p : null)}
onActivateModule={(type, ctx) => onActivateSection(type as JobSection, ctx)}
context={sectionContext}
countingTaskId={sectionContext.countingTaskId as string | undefined}
validCounts={sectionContext.validCounts as number[] | undefined}
validCount={sectionContext.validCount as number | undefined}
/>
</div>
<div className={`job-section-pane${section === "cleaning" ? "" : " job-section-pane--hidden"}`}>
<CleaningWizardModule
key={`cleaning-${String(sectionContext.extractionTaskId ?? sectionContext.extractionBatchId ?? "default")}`}
client={client}
job={job}
onTaskCreated={handleTaskCreated}
onProgress={(p) => setCleaningProgress(p.running ? p : null)}
onActivateModule={(type, ctx) => onActivateSection(type as JobSection, ctx)}
context={sectionContext}
/>
</div>
<div className={`job-section-pane${section === "poblacion_apta" ? "" : " job-section-pane--hidden"}`}>
<PoblacionAptaWizardModule
client={client}
job={job}
onTaskCreated={handleTaskCreated}
onProgress={(p) => setPoblacionAptaProgress(p.running ? p : null)}
onActivateModule={(type, ctx) => onActivateSection(type as JobSection, ctx)}
/>
</div>
<div className={`job-section-pane${section === "downloads" ? "" : " job-section-pane--hidden"}`}>
<DownloadsWizardModule client={client} job={job} />
</div>
<div className={`job-section-pane${section === "digitador" ? "" : " job-section-pane--hidden"}`}>
<DigitadorWizardModule
key={`digitador-${String(sectionContext.poblacionAptaJobId ?? "default")}`}
client={client}
job={job}
initialSource={
typeof sectionContext.poblacionAptaJobId === "string"
? { poblacionAptaJobId: sectionContext.poblacionAptaJobId }
: null
}
/>
</div>
<div className={`job-section-pane${section === "results" ? "" : " job-section-pane--hidden"}`}>
<JobResultsView
client={client}
job={job}
tasks={tasks}
countingProgress={countingProgress}
extractionProgress={extractionProgress}
cleaningProgress={cleaningProgress}
poblacionAptaProgress={poblacionAptaProgress}
onRefresh={loadTasks}
onStartCounting={() => onActivateSection("counting")}
onStartExtraction={(ctx?: Record<string, unknown>) => onActivateSection("extraction", ctx)}
onStartCleaning={(ctx?: Record<string, unknown>) => onActivateSection("cleaning", ctx)}
onStartPoblacionApta={() => onActivateSection("poblacion_apta")}
onStartDownload={() => onActivateSection("downloads")}
onStartDigitador={() => onActivateSection("digitador")}
onContinueToCounting={(ctx) => onActivateSection("counting", ctx)}
onContinueToDigitador={(ctx) => onActivateSection("digitador", ctx)}
/>
</div>
<div className={`job-section-pane${section === "costs" ? "" : " job-section-pane--hidden"}`}>
<JobCostsView tasks={tasks} refreshing={costsRefreshing} onRefresh={() => void refreshCosts()} />
</div>
</div>
</div>
);
}