Files
dh-inspeccion-v2/web-v2/src/pages/InventoryCreatePage.tsx
T

270 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react';
import type { FormEvent } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router';
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
import { Icon } from '../components/Icon';
import { getAsset } from '../lib/api';
import {
createInventoryStructure,
getInventoryFamilyFindings,
getInventoryStructureOptions,
listInventoryStructureParents,
} from '../lib/inventoryStructureApi';
import type {
InventoryFamily,
InventoryFamilyFindings,
InventoryStructureKind,
InventoryStructureOptions,
InventoryStructureParent,
} from '../lib/inventoryStructureApi';
const KINDS: Array<{ kind: InventoryStructureKind; label: string; help: string; step: number }> = [
{ kind: 'AREA', label: 'Área', help: 'Nivel territorial raíz.', step: 1 },
{ kind: 'YACIMIENTO', label: 'Yacimiento', help: 'Debe pertenecer a un Área.', step: 2 },
{ kind: 'INSTALACION', label: 'Instalación', help: 'Debe pertenecer a un Yacimiento.', step: 3 },
{ kind: 'SUBINSTALACION', label: 'Subinstalación', help: 'Debe pertenecer a una Instalación.', step: 4 },
];
const childKindByParentType: Record<string, InventoryStructureKind | undefined> = {
area: 'YACIMIENTO',
yacimiento: 'INSTALACION',
instalacion: 'SUBINSTALACION',
};
function kindLabel(kind: InventoryStructureKind): string {
return KINDS.find((item) => item.kind === kind)?.label ?? kind;
}
export function InventoryCreatePage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const contextParentId = searchParams.get('parentId');
const [options, setOptions] = useState<InventoryStructureOptions | null>(null);
const [kind, setKind] = useState<InventoryStructureKind>('AREA');
const [parents, setParents] = useState<InventoryStructureParent[]>([]);
const [parentSearch, setParentSearch] = useState('');
const [parentId, setParentId] = useState('');
const [familyId, setFamilyId] = useState('');
const [familyFindings, setFamilyFindings] = useState<InventoryFamilyFindings | null>(null);
const [code, setCode] = useState('');
const [name, setName] = useState('');
const [commonName, setCommonName] = useState('');
const [description, setDescription] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
getInventoryStructureOptions()
.then(async (loaded) => {
setOptions(loaded);
if (contextParentId) {
const parent = await getAsset(contextParentId);
const inferred = childKindByParentType[parent.type.code.toLowerCase()];
if (inferred) {
setKind(inferred);
setParentId(parent.id);
}
}
})
.catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false));
}, [contextParentId]);
useEffect(() => {
if (kind === 'AREA') {
setParents([]);
setParentId('');
return;
}
const timer = window.setTimeout(() => {
listInventoryStructureParents(kind, parentSearch)
.then((loaded) => {
setParents(loaded);
if (contextParentId && loaded.some((item) => item.id === contextParentId)) {
setParentId(contextParentId);
}
})
.catch((requestError) => setError(errorMessage(requestError)));
}, 180);
return () => window.clearTimeout(timer);
}, [kind, parentSearch, contextParentId]);
const selectedParent = parents.find((item) => item.id === parentId) ?? null;
const families = useMemo(() => {
if (!options) return [] as InventoryFamily[];
if (kind === 'INSTALACION') return options.installationFamilies;
if (kind === 'SUBINSTALACION') {
const parentFamilyId = selectedParent?.inventoryFamily?.id;
return parentFamilyId
? options.subinstallationFamilies.filter((item) => item.parentFamilyId === parentFamilyId)
: [];
}
return [];
}, [options, kind, selectedParent]);
const selectedFamily = families.find((item) => item.id === familyId) ?? null;
useEffect(() => {
if (!familyId) {
setFamilyFindings(null);
return;
}
getInventoryFamilyFindings(familyId)
.then(setFamilyFindings)
.catch((requestError) => setError(errorMessage(requestError)));
}, [familyId]);
useEffect(() => {
if (kind !== 'INSTALACION' && kind !== 'SUBINSTALACION') setFamilyId('');
if (kind === 'SUBINSTALACION' && familyId && !families.some((item) => item.id === familyId)) setFamilyId('');
}, [kind, familyId, families]);
const chooseKind = (next: InventoryStructureKind) => {
setKind(next);
setParentId('');
setParentSearch('');
setFamilyId('');
setFamilyFindings(null);
setError('');
};
const save = async (event: FormEvent) => {
event.preventDefault();
const requiresParent = kind !== 'AREA';
const requiresFamily = kind === 'INSTALACION' || kind === 'SUBINSTALACION';
if (requiresParent && !parentId) {
setError(`Seleccioná el ${kind === 'YACIMIENTO' ? 'Área' : kind === 'INSTALACION' ? 'Yacimiento' : 'Instalación'} padre.`);
return;
}
if (requiresFamily && !familyId) {
setError(`Seleccioná la familia de ${kindLabel(kind).toLowerCase()}.`);
return;
}
setSaving(true);
setError('');
try {
const created = await createInventoryStructure({
kind,
code: code.trim() || null,
name: name.trim(),
commonName: commonName.trim() || null,
parentId: parentId || null,
familyId: familyId || null,
description: description.trim() || null,
});
navigate(`/inventarios/${created.id}`, { replace: true });
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setSaving(false);
}
};
if (loading) return <LoadingBlock label="Preparando alta de Inventario…" />;
const parentLabel = kind === 'YACIMIENTO' ? 'Área' : kind === 'INSTALACION' ? 'Yacimiento' : 'Instalación';
const currentStep = KINDS.find((item) => item.kind === kind)?.step ?? 1;
return <section className="narrow-section asset-detail-page">
<nav className="breadcrumb asset-detail-breadcrumb" aria-label="Ruta del inventario">
<Link to="/inventarios">Inventarios</Link><span></span><strong>Nuevo registro</strong>
</nav>
<div className="page-heading asset-editor-heading">
<div>
<span className="eyebrow">INVENTARIO</span>
<h1>Agregar a la estructura</h1>
<p>La estructura oficial es Área Yacimiento Instalación Subinstalación. Elegí el nivel y el sistema te guía con los vínculos válidos.</p>
</div>
</div>
{error && <Alert>{error}</Alert>}
<div className="panel" style={{ marginBottom: 18 }}>
<div className="form-section">
<div><h2>1. ¿Qué querés crear?</h2><p className="section-copy">Sólo se pueden crear los cuatro niveles estructurales definidos para DH.</p></div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 10 }}>
{KINDS.map((item) => <button
key={item.kind}
type="button"
className={`button ${kind === item.kind ? 'primary' : 'secondary'}`}
onClick={() => chooseKind(item.kind)}
style={{ minHeight: 76, display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center', gap: 3 }}
>
<strong>{item.step}. {item.label}</strong>
<small>{item.help}</small>
</button>)}
</div>
<div className="temporal-notice" style={{ marginTop: 12 }}>
<Icon name="layers" />
<p><strong>Ruta:</strong> {KINDS.slice(0, currentStep).map((item) => item.label).join(' → ')}</p>
</div>
</div>
</div>
<form className="panel form-panel" onSubmit={save}>
{kind !== 'AREA' && <div className="form-section">
<div><h2>2. Ubicación en la estructura</h2><p className="section-copy">Primero elegí el {parentLabel} al que pertenece este registro.</p></div>
<label className="field">
<span>Buscar {parentLabel.toLowerCase()}</span>
<input value={parentSearch} onChange={(event) => setParentSearch(event.target.value)} placeholder={`Buscar por nombre o código de ${parentLabel.toLowerCase()}…`} />
</label>
<label className="field">
<span>{parentLabel} padre <em>obligatorio</em></span>
<select value={parentId} onChange={(event) => { setParentId(event.target.value); setFamilyId(''); }} required>
<option value="">Seleccionar {parentLabel.toLowerCase()}</option>
{parents.map((parent) => <option key={parent.id} value={parent.id}>{parent.name} · {parent.code}{parent.inventoryFamily ? ` · ${parent.inventoryFamily.name}` : ''}</option>)}
</select>
<small>No se permiten saltos de nivel ni padres incompatibles.</small>
</label>
</div>}
{(kind === 'INSTALACION' || kind === 'SUBINSTALACION') && <div className="form-section">
<div><h2>3. Familia técnica</h2><p className="section-copy">La familia no crea otro nivel. Sirve para aplicar exactamente los Hallazgos del Excel que corresponden.</p></div>
{kind === 'SUBINSTALACION' && !selectedParent?.inventoryFamily ? <Alert>La Instalación seleccionada todavía no tiene una familia técnica F3.1. Revisala antes de crear una Subinstalación.</Alert> : <label className="field">
<span>Familia de {kindLabel(kind).toLowerCase()} <em>obligatorio</em></span>
<select value={familyId} onChange={(event) => setFamilyId(event.target.value)} required>
<option value="">Seleccionar familia</option>
{families.map((family) => <option key={family.id} value={family.id}>{family.name}</option>)}
</select>
{kind === 'SUBINSTALACION' && selectedParent?.inventoryFamily && <small>Se muestran sólo las subinstalaciones válidas para {selectedParent.inventoryFamily.name}.</small>}
</label>}
{selectedFamily && <div className="context-create-banner" style={{ alignItems: 'flex-start' }}>
<Icon name="alert" />
<div style={{ flex: 1 }}>
<strong>Hallazgos asociados automáticamente</strong>
<span>{familyFindings ? `${familyFindings.count} controles del Excel para ${selectedFamily.name}` : 'Cargando catálogo asociado…'}</span>
{familyFindings && familyFindings.items.length > 0 && <ul style={{ margin: '8px 0 0', paddingLeft: 18 }}>
{familyFindings.items.slice(0, 7).map((item) => <li key={item.id}>{item.title}</li>)}
{familyFindings.items.length > 7 && <li><strong>+ {familyFindings.items.length - 7} hallazgos más</strong></li>}
</ul>}
</div>
</div>}
{selectedFamily && selectedFamily.informationLabels.length > 0 && <div className="temporal-notice">
<Icon name="clipboard" />
<p><strong>Información técnica esperada:</strong> {selectedFamily.informationLabels.join(' · ')}</p>
</div>}
</div>}
<div className="form-section">
<div><h2>{kind === 'AREA' ? '2' : kind === 'YACIMIENTO' ? '3' : '4'}. Identificación</h2><p className="section-copy">Usá el nombre real de campo. El código DH puede generarse automáticamente.</p></div>
<div className="form-grid">
<label className="field"><span>Nombre <em>obligatorio</em></span><input value={name} onChange={(event) => setName(event.target.value)} required maxLength={200} placeholder={`Nombre de ${kindLabel(kind).toLowerCase()}`} /></label>
<label className="field"><span>Código DH <em>opcional</em></span><input value={code} onChange={(event) => setCode(event.target.value.toUpperCase())} maxLength={120} pattern="[A-Z0-9][A-Z0-9._/-]*" placeholder="Dejar vacío para generar automáticamente" /><small>Si no lo informás, DH genera un código único.</small></label>
<label className="field"><span>Nombre habitual / sobrenombre <em>opcional</em></span><input value={commonName} onChange={(event) => setCommonName(event.target.value)} maxLength={200} placeholder="Nombre usado por los inspectores en campo" /></label>
</div>
<label className="field"><span>Descripción <em>opcional</em></span><textarea value={description} onChange={(event) => setDescription(event.target.value)} rows={2} maxLength={4000} /></label>
</div>
<div className="form-actions">
<Link className="button secondary" to="/inventarios">Cancelar</Link>
<button className="button primary" disabled={saving || !name.trim() || (kind !== 'AREA' && !parentId) || ((kind === 'INSTALACION' || kind === 'SUBINSTALACION') && !familyId)}>
<Icon name="check" />{saving ? 'Creando…' : `Crear ${kindLabel(kind)}`}
</button>
</div>
</form>
</section>;
}