feat(web): create companies independently from physical inventory hierarchy

This commit is contained in:
2026-09-08 21:47:12 -03:00
parent 53c7ca45f1
commit b4d81cff68
+42 -26
View File
@@ -18,7 +18,7 @@ import type {
InventoryStructureParent,
} from '../lib/inventoryStructureApi';
const KINDS: Array<{ kind: InventoryStructureKind; label: string; help: string; step: number }> = [
const STRUCTURE_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 },
@@ -32,7 +32,8 @@ const childKindByParentType: Record<string, InventoryStructureKind | undefined>
};
function kindLabel(kind: InventoryStructureKind): string {
return KINDS.find((item) => item.kind === kind)?.label ?? kind;
if (kind === 'EMPRESA') return 'Empresa';
return STRUCTURE_KINDS.find((item) => item.kind === kind)?.label ?? kind;
}
export function InventoryCreatePage() {
@@ -72,7 +73,7 @@ export function InventoryCreatePage() {
}, [contextParentId]);
useEffect(() => {
if (kind === 'AREA') {
if (kind === 'AREA' || kind === 'EMPRESA') {
setParents([]);
setParentId('');
return;
@@ -130,7 +131,7 @@ export function InventoryCreatePage() {
const save = async (event: FormEvent) => {
event.preventDefault();
const requiresParent = kind !== 'AREA';
const requiresParent = kind !== 'AREA' && kind !== 'EMPRESA';
const requiresFamily = kind === 'INSTALACION' || kind === 'SUBINSTALACION';
if (requiresParent && !parentId) {
setError(`Seleccioná el ${kind === 'YACIMIENTO' ? 'Área' : kind === 'INSTALACION' ? 'Yacimiento' : 'Instalación'} padre.`);
@@ -160,10 +161,13 @@ export function InventoryCreatePage() {
}
};
if (loading) return <LoadingBlock label="Preparando alta de Inventario…" />;
if (loading) return <LoadingBlock label="Preparando alta…" />;
const isIndependentCompany = kind === 'EMPRESA';
const parentLabel = kind === 'YACIMIENTO' ? 'Área' : kind === 'INSTALACION' ? 'Yacimiento' : 'Instalación';
const currentStep = KINDS.find((item) => item.kind === kind)?.step ?? 1;
const currentStep = STRUCTURE_KINDS.find((item) => item.kind === kind)?.step ?? 1;
const identificationStep = isIndependentCompany ? '2' : kind === 'AREA' ? '2' : kind === 'YACIMIENTO' ? '3' : '4';
const requiresParent = kind !== 'AREA' && kind !== 'EMPRESA';
return <section className="narrow-section asset-detail-page">
<nav className="breadcrumb asset-detail-breadcrumb" aria-label="Ruta del inventario">
@@ -172,9 +176,9 @@ export function InventoryCreatePage() {
<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>
<span className="eyebrow">CONFIGURACIÓN E INVENTARIO</span>
<h1>Agregar registro</h1>
<p>Empresa es un maestro independiente. La estructura física es Área Yacimiento Instalación Subinstalación.</p>
</div>
</div>
@@ -182,9 +186,18 @@ export function InventoryCreatePage() {
<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
<div><h2>1. ¿Qué querés crear?</h2><p className="section-copy">Creá una Empresa independiente o agregá un nivel a la estructura física.</p></div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, minmax(0, 1fr))', gap: 10 }}>
<button
type="button"
className={`button ${kind === 'EMPRESA' ? 'primary' : 'secondary'}`}
onClick={() => chooseKind('EMPRESA')}
style={{ minHeight: 76, display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center', gap: 3 }}
>
<strong>Empresa</strong>
<small>Maestro independiente.</small>
</button>
{STRUCTURE_KINDS.map((item) => <button
key={item.kind}
type="button"
className={`button ${kind === item.kind ? 'primary' : 'secondary'}`}
@@ -195,15 +208,18 @@ export function InventoryCreatePage() {
<small>{item.help}</small>
</button>)}
</div>
<div className="temporal-notice" style={{ marginTop: 12 }}>
{isIndependentCompany ? <div className="temporal-notice" style={{ marginTop: 12 }}>
<Icon name="users" />
<p><strong>Empresa:</strong> no forma parte de la jerarquía física. Su vínculo con un Área se administra como relación temporal de operación/concesión.</p>
</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>
<p><strong>Ruta física:</strong> {STRUCTURE_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">
{requiresParent && <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>
@@ -220,24 +236,24 @@ export function InventoryCreatePage() {
</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>
<div><h2>3. Clasificación técnica</h2><p className="section-copy">La clasificación no crea otro nivel: define el tipo de Instalación/Subinstalación y los Hallazgos aplicables.</p></div>
{kind === 'SUBINSTALACION' && !selectedParent?.inventoryFamily ? <Alert>La Instalación seleccionada todavía no tiene una clasificación técnica. Revisala antes de crear una Subinstalación.</Alert> : <label className="field">
<span>Tipo de {kindLabel(kind).toLowerCase()} <em>obligatorio</em></span>
<select value={familyId} onChange={(event) => setFamilyId(event.target.value)} required>
<option value="">Seleccionar familia</option>
<option value="">Seleccionar tipo</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>}
{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>
<span>{familyFindings ? `${familyFindings.count} Hallazgos aplicables 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>}
{familyFindings.items.length > 7 && <li><strong>+ {familyFindings.items.length - 7} Hallazgos más</strong></li>}
</ul>}
</div>
</div>}
@@ -249,18 +265,18 @@ export function InventoryCreatePage() {
</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><h2>{identificationStep}. Identificación</h2><p className="section-copy">{isIndependentCompany ? 'Registrá la denominación de la Empresa.' : '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>
<label className="field"><span>Nombre habitual / sobrenombre <em>opcional</em></span><input value={commonName} onChange={(event) => setCommonName(event.target.value)} maxLength={200} placeholder={isIndependentCompany ? 'Nombre habitual o abreviado' : '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)}>
<button className="button primary" disabled={saving || !name.trim() || (requiresParent && !parentId) || ((kind === 'INSTALACION' || kind === 'SUBINSTALACION') && !familyId)}>
<Icon name="check" />{saving ? 'Creando…' : `Crear ${kindLabel(kind)}`}
</button>
</div>