Skip to content

Commit 45db392

Browse files
committed
Auto-detect native Pescamar production workbooks
1 parent 2cd4801 commit 45db392

1 file changed

Lines changed: 51 additions & 140 deletions

File tree

‎src/components/PlantImportModal.tsx‎

Lines changed: 51 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
X,
88
} from "lucide-react";
99
import { useRef, useState } from "react";
10+
import {publishHistoricalProduction,readHistoricalProductionWorkbook,type HistoricalWorkbookPreview} from "../historicalImport";
1011
import type { Plant } from "../plants";
1112
import { readPlantWorkbook, type ValidatedImport } from "../plantImport";
1213

@@ -19,6 +20,7 @@ type Props = {
1920
export function PlantImportModal({ open, plants, onClose, onPublish }: Props) {
2021
const input = useRef<HTMLInputElement>(null);
2122
const [rows, setRows] = useState<ValidatedImport[]>([]);
23+
const [historical,setHistorical]=useState<HistoricalWorkbookPreview|null>(null);
2224
const [fileName, setFileName] = useState("");
2325
const [busy, setBusy] = useState(false);
2426
const [message, setMessage] = useState("");
@@ -27,8 +29,10 @@ export function PlantImportModal({ open, plants, onClose, onPublish }: Props) {
2729
const invalid = rows.length - valid.length;
2830
const reset = () => {
2931
setRows([]);
32+
setHistorical(null);
3033
setFileName("");
3134
setMessage("");
35+
if(input.current)input.current.value='';
3236
};
3337
const close = () => {
3438
reset();
@@ -39,16 +43,22 @@ export function PlantImportModal({ open, plants, onClose, onPublish }: Props) {
3943
setBusy(true);
4044
setMessage("");
4145
try {
46+
const native=await readHistoricalProductionWorkbook(file);
47+
if(native){
48+
setHistorical(native);
49+
setRows([]);
50+
setFileName(file.name);
51+
return;
52+
}
4253
const parsed = await readPlantWorkbook(file, plants);
54+
setHistorical(null);
4355
setRows(parsed);
4456
setFileName(file.name);
45-
if (!parsed.length)
46-
setMessage("La primera hoja no contiene filas de datos.");
47-
} catch {
57+
if (!parsed.length) setMessage("La primera hoja no contiene filas de datos.");
58+
} catch (cause) {
4859
setRows([]);
49-
setMessage(
50-
"No fue posible leer el archivo. Verifica que sea XLSX, XLS o CSV.",
51-
);
60+
setHistorical(null);
61+
setMessage(cause instanceof Error?cause.message:"No fue posible leer el archivo. Verifica que sea XLSX o CSV.");
5262
} finally {
5363
setBusy(false);
5464
}
@@ -61,161 +71,62 @@ export function PlantImportModal({ open, plants, onClose, onPublish }: Props) {
6171
await onPublish(valid);
6272
close();
6373
} catch (cause) {
64-
setMessage(
65-
cause instanceof Error ? cause.message : "No fue posible publicar",
66-
);
74+
setMessage(cause instanceof Error ? cause.message : "No fue posible publicar");
6775
} finally {
6876
setBusy(false);
6977
}
7078
};
79+
const publishHistorical=async()=>{
80+
if(!historical?.rows.length)return;
81+
setBusy(true);setMessage('');
82+
try{
83+
const result=await publishHistoricalProduction(historical);
84+
setMessage(`Publicación completa: ${result.inserted??0} nuevas · ${result.duplicates??0} ya existentes.`);
85+
window.setTimeout(close,900);
86+
}catch(cause){setMessage(cause instanceof Error?cause.message:'No fue posible publicar')}finally{setBusy(false)}
87+
};
7188
const sample =
7289
"planta,periodo,produccion_kg,meta_kg,inventario_kg,producto_terminado_kg,fecha_actualizacion,observacion\nancud,2026-08,18420,18000,6250,3120,2026-08-19T08:45:00,Operación normal\nquellon,2026-08,26880,28000,9340,4820,2026-08-19T09:10:00,Sin novedades";
90+
const hasData=rows.length>0||historical;
7391
return (
7492
<div className="modal-backdrop" role="dialog" aria-modal="true">
7593
<section className="modal-panel import-modal">
7694
<header>
7795
<div>
7896
<span className="overline teal">Importación operacional</span>
79-
<h2>Actualizar plantas desde planilla</h2>
80-
<p>Validamos los datos antes de modificar el centro de control.</p>
97+
<h2>{historical?'Producción Pescamar reconocida':'Actualizar desde planilla'}</h2>
98+
<p>{historical?'Detectamos el formato nativo por guía y lote. Se conserva cada fila y sus observaciones de calidad.':'Validamos los datos antes de modificar el centro de control.'}</p>
8199
</div>
82-
<button className="icon-btn" onClick={close} aria-label="Cerrar">
83-
<X />
84-
</button>
100+
<button className="icon-btn" onClick={close} aria-label="Cerrar"><X /></button>
85101
</header>
86-
{!rows.length ? (
102+
{!hasData ? (
87103
<>
88-
<button
89-
className="import-dropzone"
90-
onClick={() => input.current?.click()}
91-
disabled={busy}
92-
>
104+
<button className="import-dropzone" onClick={() => input.current?.click()} disabled={busy}>
93105
<Upload />
94-
<span>
95-
<b>{busy ? "Leyendo archivo…" : "Seleccionar Excel o CSV"}</b>
96-
<small>Formatos aceptados: .xlsx y .csv · máximo 15 MB</small>
97-
</span>
106+
<span><b>{busy ? "Leyendo archivo…" : "Seleccionar Excel o CSV"}</b><small>Auto-detecta producción Pescamar o snapshot por planta · máximo 15 MB</small></span>
98107
</button>
99-
<input
100-
ref={input}
101-
hidden
102-
type="file"
103-
accept=".xlsx,.csv"
104-
onChange={(event) => void choose(event.target.files?.[0])}
105-
/>
106-
<div className="import-contract">
107-
<FileSpreadsheet />
108-
<div>
109-
<b>Columnas obligatorias</b>
110-
<p>
111-
planta, periodo, produccion_kg, meta_kg, inventario_kg,
112-
producto_terminado_kg y fecha_actualizacion.
113-
</p>
114-
</div>
108+
<input ref={input} hidden type="file" accept=".xlsx,.csv" onChange={(event) => void choose(event.target.files?.[0])}/>
109+
<div className="import-contract"><FileSpreadsheet /><div><b>Dos formatos nativos</b><p>Planilla detallada Pescamar por guía/lote, o snapshot resumido por planta. El sistema detecta cuál corresponde.</p></div></div>
110+
<a className="button secondary import-download" download="plantilla_pescamar.csv" href={`data:text/csv;charset=utf-8,${encodeURIComponent(sample)}`}><Download size={15} />Descargar plantilla de snapshot</a>
111+
{message ? <p className="import-message"><AlertCircle />{message}</p> : null}
112+
</>
113+
) : historical ? (
114+
<>
115+
<div className="import-file"><FileSpreadsheet/><span><b>{historical.fileName}</b><small>Formato nativo · hoja “{historical.sheetName}” · hash {historical.fileHash.slice(0,12)}…</small></span><button className="button secondary" onClick={reset}>Cambiar archivo</button></div>
116+
<div className="inspection-metrics">
117+
<span>Registros<b>{historical.rows.length}</b></span><span>Operacionales<b>{historical.operational}</b></span><span>Anulados<b>{historical.voids}</b></span><span>Con observaciones<b>{historical.flagged}</b></span>
115118
</div>
116-
<a
117-
className="button secondary import-download"
118-
download="plantilla_pescamar.csv"
119-
href={`data:text/csv;charset=utf-8,${encodeURIComponent(sample)}`}
120-
>
121-
<Download size={15} />
122-
Descargar plantilla de ejemplo
123-
</a>
124-
{message ? (
125-
<p className="import-message">
126-
<AlertCircle />
127-
{message}
128-
</p>
129-
) : null}
119+
<div className="import-table-wrap"><table className="import-table"><thead><tr><th>Fila</th><th>Guía</th><th>Proveedor</th><th>Lote</th><th>Kilos guía</th><th>Kilos recibidos</th><th>Resultado</th></tr></thead><tbody>{historical.rows.slice(0,120).map(row=><tr key={row.sourceRow} className={row.dataQualityFlags.length?'has-error':''}><td>{row.sourceRow}</td><td>{row.guideNumber??'—'}</td><td>{row.supplierName??'—'}</td><td>{row.lotCode}</td><td>{row.guideKg?.toLocaleString('es-CL')??'—'}</td><td>{row.receivedKg?.toLocaleString('es-CL')??'—'}</td><td>{row.recordStatus==='void'?<span className="import-errors"><AlertCircle/>Anulado</span>:row.dataQualityFlags.length?<span className="import-errors"><AlertCircle/>{row.dataQualityFlags.length} observación(es)</span>:<span className="import-ok"><CheckCircle2/>Lista</span>}</td></tr>)}</tbody></table></div>
120+
{historical.rows.length>120?<small className="disabled-note">Vista previa de 120 filas de {historical.rows.length}. Todas serán procesadas.</small>:null}
121+
{message?<p className="import-message" role="alert"><AlertCircle/>{message}</p>:null}
122+
<footer><button className="button secondary" onClick={close}>Cancelar</button><button className="button primary" disabled={busy||!historical.rows.length} onClick={()=>void publishHistorical()}>{busy?'Publicando…':`Publicar ${historical.rows.length} registros`}</button></footer>
130123
</>
131124
) : (
132125
<>
133-
<div className="import-file">
134-
<FileSpreadsheet />
135-
<span>
136-
<b>{fileName}</b>
137-
<small>
138-
{rows.length} filas · {valid.length} válidas · {invalid} con
139-
error
140-
</small>
141-
</span>
142-
<button className="button secondary" onClick={reset}>
143-
Cambiar archivo
144-
</button>
145-
</div>
146-
<div className="import-table-wrap">
147-
<table className="import-table">
148-
<thead>
149-
<tr>
150-
<th>Fila</th>
151-
<th>Planta</th>
152-
<th>Período</th>
153-
<th>Producción</th>
154-
<th>Meta</th>
155-
<th>Inventario</th>
156-
<th>Resultado</th>
157-
</tr>
158-
</thead>
159-
<tbody>
160-
{rows.map((row) => (
161-
<tr
162-
key={row.rowNumber}
163-
className={row.errors.length ? "has-error" : ""}
164-
>
165-
<td>{row.rowNumber}</td>
166-
<td>{row.plant?.name ?? row.plantId}</td>
167-
<td>{row.period || "—"}</td>
168-
<td>
169-
{Number.isFinite(row.productionKg)
170-
? row.productionKg.toLocaleString("es-CL")
171-
: "—"}
172-
</td>
173-
<td>
174-
{Number.isFinite(row.targetKg)
175-
? row.targetKg.toLocaleString("es-CL")
176-
: "—"}
177-
</td>
178-
<td>
179-
{Number.isFinite(row.inventoryKg)
180-
? row.inventoryKg.toLocaleString("es-CL")
181-
: "—"}
182-
</td>
183-
<td>
184-
{row.errors.length ? (
185-
<span className="import-errors">
186-
<AlertCircle />
187-
{row.errors.join(" · ")}
188-
</span>
189-
) : (
190-
<span className="import-ok">
191-
<CheckCircle2 />
192-
Lista para publicar
193-
</span>
194-
)}
195-
</td>
196-
</tr>
197-
))}
198-
</tbody>
199-
</table>
200-
</div>
201-
{message ? (
202-
<p className="import-message" role="alert">
203-
<AlertCircle />
204-
{message}
205-
</p>
206-
) : null}
207-
<footer>
208-
<button className="button secondary" onClick={close}>
209-
Cancelar
210-
</button>
211-
<button
212-
className="button primary"
213-
disabled={busy || !valid.length || invalid > 0}
214-
onClick={() => void publish()}
215-
>
216-
{busy ? "Publicando…" : `Publicar ${valid.length} plantas`}
217-
</button>
218-
</footer>
126+
<div className="import-file"><FileSpreadsheet /><span><b>{fileName}</b><small>{rows.length} filas · {valid.length} válidas · {invalid} con error</small></span><button className="button secondary" onClick={reset}>Cambiar archivo</button></div>
127+
<div className="import-table-wrap"><table className="import-table"><thead><tr><th>Fila</th><th>Planta</th><th>Período</th><th>Producción</th><th>Meta</th><th>Inventario</th><th>Resultado</th></tr></thead><tbody>{rows.map((row) => <tr key={row.rowNumber} className={row.errors.length ? "has-error" : ""}><td>{row.rowNumber}</td><td>{row.plant?.name ?? row.plantId}</td><td>{row.period || "—"}</td><td>{Number.isFinite(row.productionKg)?row.productionKg.toLocaleString("es-CL"):"—"}</td><td>{Number.isFinite(row.targetKg)?row.targetKg.toLocaleString("es-CL"):"—"}</td><td>{Number.isFinite(row.inventoryKg)?row.inventoryKg.toLocaleString("es-CL"):"—"}</td><td>{row.errors.length?<span className="import-errors"><AlertCircle />{row.errors.join(" · ")}</span>:<span className="import-ok"><CheckCircle2 />Lista para publicar</span>}</td></tr>)}</tbody></table></div>
128+
{message ? <p className="import-message" role="alert"><AlertCircle />{message}</p> : null}
129+
<footer><button className="button secondary" onClick={close}>Cancelar</button><button className="button primary" disabled={busy || !valid.length || invalid > 0} onClick={() => void publish()}>{busy ? "Publicando…" : `Publicar ${valid.length} plantas`}</button></footer>
219130
</>
220131
)}
221132
</section>

0 commit comments

Comments
 (0)