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

287 lines
11 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,
getInventoryStructureOptions,
listCompaniesForInventoryArea,
listInventoryStructureParents,
} from '../lib/inventoryStructureApi';
import type {
InventoryAreaCompanyOption,
InventoryFamily,
InventoryStructureKind,
InventoryStructureOptions,
InventoryStructureParent,
} from '../lib/inventoryStructureApi';
const KINDS: Array<{ kind: InventoryStructureKind; label: string }> = [
{ kind: 'DEPARTAMENTO', label: 'Departamento' },
{ kind: 'AREA', label: 'Área' },
{ kind: 'YACIMIENTO', label: 'Yacimiento' },
{ kind: 'INSTALACION', label: 'Instalación' },
{ kind: 'SUBINSTALACION', label: 'Subinstalación' },
{ kind: 'EMPRESA', label: 'Empresa' },
];
const childKindByParentType: Record<string, InventoryStructureKind | undefined> = {
departamento: 'AREA',
area: 'YACIMIENTO',
yacimiento: 'INSTALACION',
instalacion: 'SUBINSTALACION',
};
const parentLabelByKind: Partial<Record<InventoryStructureKind, string>> = {
AREA: 'Departamento',
YACIMIENTO: 'Área',
INSTALACION: 'Yacimiento',
SUBINSTALACION: 'Instalación',
};
function kindLabel(kind: InventoryStructureKind) {
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>('DEPARTAMENTO');
const [parents, setParents] = useState<InventoryStructureParent[]>([]);
const [parentSearch, setParentSearch] = useState('');
const [parentId, setParentId] = useState('');
const [familyId, setFamilyId] = useState('');
const [areaCompanies, setAreaCompanies] = useState<InventoryAreaCompanyOption[]>([]);
const [operatorCompanyId, setOperatorCompanyId] = useState('');
const [name, setName] = useState('');
const [code, setCode] = 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]);
const requiresParent = Boolean(parentLabelByKind[kind]);
const requiresFamily = kind === 'INSTALACION' || kind === 'SUBINSTALACION';
const requiresCompany = kind === 'YACIMIENTO';
const parentLabel = parentLabelByKind[kind] ?? '';
useEffect(() => {
if (!requiresParent) {
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)));
}, 150);
return () => window.clearTimeout(timer);
}, [kind, parentSearch, contextParentId, requiresParent]);
useEffect(() => {
if (kind !== 'YACIMIENTO' || !parentId) {
setAreaCompanies([]);
setOperatorCompanyId('');
return;
}
setOperatorCompanyId('');
listCompaniesForInventoryArea(parentId)
.then(setAreaCompanies)
.catch((requestError) => setError(errorMessage(requestError)));
}, [kind, parentId]);
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.parentFamilyIds.includes(parentFamilyId))
: [];
}
return [];
}, [options, kind, selectedParent]);
useEffect(() => {
if (!requiresFamily) setFamilyId('');
if (kind === 'SUBINSTALACION' && familyId && !families.some((item) => item.id === familyId)) setFamilyId('');
}, [kind, requiresFamily, families, familyId]);
const changeKind = (next: InventoryStructureKind) => {
setKind(next);
setParentId('');
setParentSearch('');
setFamilyId('');
setAreaCompanies([]);
setOperatorCompanyId('');
setError('');
};
const save = async (event: FormEvent) => {
event.preventDefault();
if (requiresParent && !parentId) {
setError(`Seleccioná ${parentLabel}.`);
return;
}
if (requiresCompany && !operatorCompanyId) {
setError('Seleccioná la Empresa que opera este Yacimiento.');
return;
}
if (requiresFamily && !familyId) {
setError(`Seleccioná el tipo de ${kindLabel(kind).toLowerCase()}.`);
return;
}
setSaving(true);
setError('');
try {
const created = await createInventoryStructure({
kind,
name: name.trim(),
parentId: parentId || null,
familyId: familyId || null,
operatorCompanyId: operatorCompanyId || null,
code: code.trim() || null,
});
navigate(`/inventarios/${created.id}`, { replace: true });
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setSaving(false);
}
};
if (loading) return <LoadingBlock label="Preparando alta…" />;
const disableSave = saving
|| !name.trim()
|| (requiresParent && !parentId)
|| (requiresCompany && !operatorCompanyId)
|| (requiresFamily && !familyId);
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">ALTA RÁPIDA</span>
<h1>Agregar al Inventario</h1>
<p>Elegí qué querés crear, dónde está y completá sólo los datos necesarios.</p>
</div>
</div>
{error && <Alert>{error}</Alert>}
<form className="panel form-panel" onSubmit={save}>
<div className="form-section">
<div>
<h2>¿Qué querés crear?</h2>
<p className="section-copy">Departamento Área Yacimiento Instalación Subinstalación.</p>
</div>
<div className="quick-view-row" aria-label="Tipo de registro">
{KINDS.map((item) => <button
type="button"
key={item.kind}
className={kind === item.kind ? 'active' : ''}
onClick={() => changeKind(item.kind)}
>{item.label}</button>)}
</div>
</div>
{requiresParent && <div className="form-section">
<div>
<h2>¿Dónde está?</h2>
<p className="section-copy">Seleccioná el {parentLabel.toLowerCase()} al que pertenece.</p>
</div>
<label className="field">
<span>Buscar {parentLabel.toLowerCase()}</span>
<input value={parentSearch} onChange={(event) => setParentSearch(event.target.value)} placeholder={`Buscar ${parentLabel.toLowerCase()}…`} />
</label>
<label className="field">
<span>{parentLabel} <em>obligatorio</em></span>
<select value={parentId} onChange={(event) => { setParentId(event.target.value); setFamilyId(''); }} required>
<option value="">Seleccionar</option>
{parents.map((parent) => <option key={parent.id} value={parent.id}>{parent.name}</option>)}
</select>
</label>
</div>}
{requiresCompany && <div className="form-section">
<div>
<h2>Empresa operadora</h2>
<p className="section-copy">Un Área puede tener varias Empresas. Cada Yacimiento queda asociado a una sola de ellas.</p>
</div>
{!parentId ? <Alert>Primero seleccioná el Área.</Alert> : areaCompanies.length === 0 ? <Alert>El Área elegida todavía no tiene Empresas operadoras vinculadas.</Alert> : <label className="field">
<span>Empresa <em>obligatorio</em></span>
<select value={operatorCompanyId} onChange={(event) => setOperatorCompanyId(event.target.value)} required>
<option value="">Seleccionar</option>
{areaCompanies.map((company) => <option key={company.id} value={company.id}>{company.name}</option>)}
</select>
</label>}
</div>}
{requiresFamily && <div className="form-section">
<div>
<h2>{kind === 'INSTALACION' ? 'Tipo de instalación' : 'Tipo / función'}</h2>
<p className="section-copy">{kind === 'SUBINSTALACION' ? 'Sólo aparecen tipos permitidos dentro de la Instalación seleccionada.' : 'Elegí la clasificación simple que corresponda.'}</p>
</div>
{kind === 'SUBINSTALACION' && !selectedParent?.inventoryFamily ? <Alert>La Instalación elegida todavía no tiene un tipo configurado.</Alert> : <label className="field">
<span>{kind === 'INSTALACION' ? 'Tipo' : 'Tipo / función'} <em>obligatorio</em></span>
<select value={familyId} onChange={(event) => setFamilyId(event.target.value)} required>
<option value="">Seleccionar</option>
{families.map((family) => <option key={family.id} value={family.id}>{family.name}</option>)}
</select>
</label>}
{kind === 'SUBINSTALACION' && selectedParent?.inventoryFamily && families.length === 0 && <Alert>No hay tipos de Subinstalación configurados para esta Instalación.</Alert>}
</div>}
<div className="form-section">
<div>
<h2>Datos básicos</h2>
<p className="section-copy">El nombre alcanza para identificar el registro. El código puede generarse automáticamente.</p>
</div>
<label className="field">
<span>Nombre <em>obligatorio</em></span>
<input autoFocus value={name} onChange={(event) => setName(event.target.value)} required maxLength={200} placeholder={`Nombre de ${kindLabel(kind).toLowerCase()}`} />
</label>
<label className="field">
<span>Código <em>opcional</em></span>
<input value={code} onChange={(event) => setCode(event.target.value.toUpperCase())} maxLength={120} pattern="[A-Z0-9][A-Z0-9._/-]*" placeholder="Se genera automáticamente si lo dejás vacío" />
</label>
</div>
<div className="form-actions">
<Link className="button secondary" to="/inventarios">Cancelar</Link>
<button className="button primary" disabled={disableSave}><Icon name="check" />{saving ? 'Guardando…' : `Crear ${kindLabel(kind)}`}</button>
</div>
</form>
</section>;
}