287 lines
11 KiB
TypeScript
287 lines
11 KiB
TypeScript
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>;
|
||
}
|