feat(f6): filtrar subinstalaciones por compatibilidad múltiple
This commit is contained in:
@@ -26,24 +26,13 @@ const KINDS: Array<{ kind: InventoryStructureKind; label: string }> = [
|
|||||||
{ kind: 'SUBINSTALACION', label: 'Subinstalación' },
|
{ kind: 'SUBINSTALACION', label: 'Subinstalación' },
|
||||||
{ kind: 'EMPRESA', label: 'Empresa' },
|
{ kind: 'EMPRESA', label: 'Empresa' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const childKindByParentType: Record<string, InventoryStructureKind | undefined> = {
|
const childKindByParentType: Record<string, InventoryStructureKind | undefined> = {
|
||||||
departamento: 'AREA',
|
departamento: 'AREA', area: 'YACIMIENTO', yacimiento: 'INSTALACION', instalacion: 'SUBINSTALACION',
|
||||||
area: 'YACIMIENTO',
|
|
||||||
yacimiento: 'INSTALACION',
|
|
||||||
instalacion: 'SUBINSTALACION',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parentLabelByKind: Partial<Record<InventoryStructureKind,string>> = {
|
const parentLabelByKind: Partial<Record<InventoryStructureKind,string>> = {
|
||||||
AREA:'Departamento',
|
AREA:'Departamento', YACIMIENTO:'Área', INSTALACION:'Yacimiento', SUBINSTALACION:'Instalación',
|
||||||
YACIMIENTO:'Área',
|
|
||||||
INSTALACION:'Yacimiento',
|
|
||||||
SUBINSTALACION:'Instalación',
|
|
||||||
};
|
};
|
||||||
|
function kindLabel(kind: InventoryStructureKind) { return KINDS.find((item) => item.kind===kind)?.label ?? kind; }
|
||||||
function kindLabel(kind: InventoryStructureKind) {
|
|
||||||
return KINDS.find((item) => item.kind===kind)?.label ?? kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InventoryCreatePage() {
|
export function InventoryCreatePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -65,17 +54,14 @@ export function InventoryCreatePage() {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getInventoryStructureOptions()
|
getInventoryStructureOptions().then(async (loaded) => {
|
||||||
.then(async (loaded) => {
|
setOptions(loaded);
|
||||||
setOptions(loaded);
|
if (contextParentId) {
|
||||||
if (contextParentId) {
|
const parent = await getAsset(contextParentId);
|
||||||
const parent = await getAsset(contextParentId);
|
const inferred = childKindByParentType[parent.type.code.toLowerCase()];
|
||||||
const inferred = childKindByParentType[parent.type.code.toLowerCase()];
|
if (inferred) { setKind(inferred); setParentId(parent.id); }
|
||||||
if (inferred) { setKind(inferred); setParentId(parent.id); }
|
}
|
||||||
}
|
}).catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false));
|
||||||
})
|
|
||||||
.catch((requestError) => setError(errorMessage(requestError)))
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, [contextParentId]);
|
}, [contextParentId]);
|
||||||
|
|
||||||
const requiresParent = Boolean(parentLabelByKind[kind]);
|
const requiresParent = Boolean(parentLabelByKind[kind]);
|
||||||
@@ -85,12 +71,10 @@ export function InventoryCreatePage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!requiresParent) { setParents([]); setParentId(''); return; }
|
if (!requiresParent) { setParents([]); setParentId(''); return; }
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
listInventoryStructureParents(kind,parentSearch)
|
listInventoryStructureParents(kind,parentSearch).then((loaded) => {
|
||||||
.then((loaded) => {
|
setParents(loaded);
|
||||||
setParents(loaded);
|
if (contextParentId && loaded.some((item) => item.id===contextParentId)) setParentId(contextParentId);
|
||||||
if (contextParentId && loaded.some((item) => item.id===contextParentId)) setParentId(contextParentId);
|
}).catch((requestError) => setError(errorMessage(requestError)));
|
||||||
})
|
|
||||||
.catch((requestError) => setError(errorMessage(requestError)));
|
|
||||||
},150);
|
},150);
|
||||||
return () => window.clearTimeout(timer);
|
return () => window.clearTimeout(timer);
|
||||||
}, [kind,parentSearch,contextParentId,requiresParent]);
|
}, [kind,parentSearch,contextParentId,requiresParent]);
|
||||||
@@ -101,7 +85,7 @@ export function InventoryCreatePage() {
|
|||||||
if (kind==='INSTALACION') return options.installationFamilies;
|
if (kind==='INSTALACION') return options.installationFamilies;
|
||||||
if (kind==='SUBINSTALACION') {
|
if (kind==='SUBINSTALACION') {
|
||||||
const parentFamilyId=selectedParent?.inventoryFamily?.id;
|
const parentFamilyId=selectedParent?.inventoryFamily?.id;
|
||||||
return parentFamilyId ? options.subinstallationFamilies.filter((item)=>item.parentFamilyId===parentFamilyId) : [];
|
return parentFamilyId ? options.subinstallationFamilies.filter((item)=>item.parentFamilyIds.includes(parentFamilyId)) : [];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
},[options,kind,selectedParent]);
|
},[options,kind,selectedParent]);
|
||||||
@@ -111,7 +95,6 @@ export function InventoryCreatePage() {
|
|||||||
if (!familyId) { setFamilyFindings(null); return; }
|
if (!familyId) { setFamilyFindings(null); return; }
|
||||||
getInventoryFamilyFindings(familyId).then(setFamilyFindings).catch((requestError)=>setError(errorMessage(requestError)));
|
getInventoryFamilyFindings(familyId).then(setFamilyFindings).catch((requestError)=>setError(errorMessage(requestError)));
|
||||||
},[familyId]);
|
},[familyId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!requiresFamily) setFamilyId('');
|
if (!requiresFamily) setFamilyId('');
|
||||||
if (kind==='SUBINSTALACION' && familyId && !families.some((item)=>item.id===familyId)) setFamilyId('');
|
if (kind==='SUBINSTALACION' && familyId && !families.some((item)=>item.id===familyId)) setFamilyId('');
|
||||||
@@ -120,7 +103,6 @@ export function InventoryCreatePage() {
|
|||||||
const changeKind=(next:InventoryStructureKind) => {
|
const changeKind=(next:InventoryStructureKind) => {
|
||||||
setKind(next); setParentId(''); setParentSearch(''); setFamilyId(''); setFamilyFindings(null); setError('');
|
setKind(next); setParentId(''); setParentSearch(''); setFamilyId(''); setFamilyFindings(null); setError('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const save=async(event:FormEvent) => {
|
const save=async(event:FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (requiresParent && !parentId) { setError(`Seleccioná ${parentLabel}.`); return; }
|
if (requiresParent && !parentId) { setError(`Seleccioná ${parentLabel}.`); return; }
|
||||||
@@ -137,46 +119,15 @@ export function InventoryCreatePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <LoadingBlock label="Preparando alta…" />;
|
if (loading) return <LoadingBlock label="Preparando alta…" />;
|
||||||
|
|
||||||
return <section className="narrow-section asset-detail-page">
|
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>
|
<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 className="page-heading asset-editor-heading"><div><span className="eyebrow">CARGA MANUAL</span><h1>Nuevo registro</h1><p>Para crear sólo necesitás ubicación, clasificación cuando corresponda y nombre. DH genera el código automáticamente.</p></div></div>
|
||||||
<div><span className="eyebrow">CARGA MANUAL</span><h1>Nuevo registro</h1><p>Elegí qué querés crear, dónde va y su nombre. El resto es opcional.</p></div>
|
|
||||||
</div>
|
|
||||||
{error && <Alert>{error}</Alert>}
|
{error && <Alert>{error}</Alert>}
|
||||||
|
|
||||||
<form className="panel form-panel" onSubmit={save}>
|
<form className="panel form-panel" onSubmit={save}>
|
||||||
<div className="form-section">
|
<div className="form-section"><div><h2>1. ¿Qué querés crear?</h2><p className="section-copy">Departamento → Área → Yacimiento → Instalación → Subinstalación. Empresa queda fuera del árbol físico.</p></div><label className="field"><span>Tipo</span><select value={kind} onChange={(event)=>changeKind(event.target.value as InventoryStructureKind)}>{KINDS.map((item)=><option key={item.kind} value={item.kind}>{item.label}</option>)}</select></label></div>
|
||||||
<div><h2>1. Tipo de registro</h2><p className="section-copy">La estructura física es Departamento → Área → Yacimiento → Instalación → Subinstalación. Empresa es un maestro independiente.</p></div>
|
{requiresParent && <div className="form-section"><div><h2>2. Ubicación</h2><p className="section-copy">Elegí el {parentLabel.toLowerCase()} concreto.</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} · {parent.code}</option>)}</select></label></div>}
|
||||||
<label className="field"><span>Crear</span><select value={kind} onChange={(event)=>changeKind(event.target.value as InventoryStructureKind)}>{KINDS.map((item)=><option key={item.kind} value={item.kind}>{item.label}</option>)}</select></label>
|
{requiresFamily && <div className="form-section"><div><h2>3. Clasificación técnica</h2><p className="section-copy">Define los datos técnicos y Hallazgos aplicables. En Subinstalaciones sólo aparecen clasificaciones compatibles con la Instalación elegida.</p></div>{kind==='SUBINSTALACION' && !selectedParent?.inventoryFamily ? <Alert>La Instalación elegida todavía no tiene clasificación técnica.</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…</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 compatibles con {selectedParent.inventoryFamily.name}. Configuralos primero en Configuración de Inventarios.</Alert>}{selectedFamily && <div className="context-create-banner"><Icon name="alert" /><div><strong>{selectedFamily.name}</strong><span>{familyFindings ? `${familyFindings.count} Hallazgo${familyFindings.count===1?'':'s'} asociado${familyFindings.count===1?'':'s'}` : 'Cargando Hallazgos asociados…'}</span>{familyFindings && familyFindings.items.length>0 && <ul style={{margin:'8px 0 0',paddingLeft:18}}>{familyFindings.items.slice(0,6).map((item)=><li key={item.id}>{item.title}</li>)}{familyFindings.items.length>6 && <li>+ {familyFindings.items.length-6} más</li>}</ul>}</div></div>}</div>}
|
||||||
</div>
|
<div className="form-section"><div><h2>{requiresFamily ? '4' : requiresParent ? '3' : '2'}. Nombre</h2><p className="section-copy">Los datos técnicos se completan después, en la ficha del elemento.</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><details style={{marginTop:12}}><summary style={{cursor:'pointer',fontWeight:700}}>Datos opcionales</summary><div style={{marginTop:14}}><div className="form-grid"><label className="field"><span>Código DH</span><input value={code} onChange={(event)=>setCode(event.target.value.toUpperCase())} maxLength={120} pattern="[A-Z0-9][A-Z0-9._/-]*" placeholder="Dejalo vacío para generar automáticamente" /></label><label className="field"><span>Nombre habitual</span><input value={commonName} onChange={(event)=>setCommonName(event.target.value)} maxLength={200} /></label></div><label className="field"><span>Descripción</span><textarea value={description} onChange={(event)=>setDescription(event.target.value)} rows={2} maxLength={4000} /></label></div></details></div>
|
||||||
|
|
||||||
{requiresParent && <div className="form-section">
|
|
||||||
<div><h2>2. Ubicación</h2><p className="section-copy">Elegí 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} · {parent.code}</option>)}</select></label>
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
{requiresFamily && <div className="form-section">
|
|
||||||
<div><h2>{requiresParent ? '3' : '2'}. Clasificación</h2><p className="section-copy">Define qué tipo de elemento es y qué Hallazgos le corresponden.</p></div>
|
|
||||||
{kind==='SUBINSTALACION' && !selectedParent?.inventoryFamily
|
|
||||||
? <Alert>La Instalación elegida todavía no tiene clasificación. Asignala 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…</option>{families.map((family)=><option key={family.id} value={family.id}>{family.name}</option>)}</select></label>}
|
|
||||||
{selectedFamily && <div className="context-create-banner"><Icon name="alert" /><div><strong>{selectedFamily.name}</strong><span>{familyFindings ? `${familyFindings.count} Hallazgo${familyFindings.count===1?'':'s'} asociado${familyFindings.count===1?'':'s'}` : 'Cargando Hallazgos asociados…'}</span>{familyFindings && familyFindings.items.length>0 && <ul style={{margin:'8px 0 0',paddingLeft:18}}>{familyFindings.items.slice(0,6).map((item)=><li key={item.id}>{item.title}</li>)}{familyFindings.items.length>6 && <li>+ {familyFindings.items.length-6} más</li>}</ul>}</div></div>}
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
<div className="form-section">
|
|
||||||
<div><h2>{requiresFamily ? '4' : requiresParent ? '3' : '2'}. Identificación</h2><p className="section-copy">Para comenzar sólo necesitamos un nombre claro.</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>
|
|
||||||
<details style={{marginTop:12}}><summary style={{cursor:'pointer',fontWeight:700}}>Datos opcionales</summary><div style={{marginTop:14}}>
|
|
||||||
<div className="form-grid">
|
|
||||||
<label className="field"><span>Código DH</span><input value={code} onChange={(event)=>setCode(event.target.value.toUpperCase())} maxLength={120} pattern="[A-Z0-9][A-Z0-9._/-]*" placeholder="Se genera solo si lo dejás vacío" /></label>
|
|
||||||
<label className="field"><span>Nombre habitual</span><input value={commonName} onChange={(event)=>setCommonName(event.target.value)} maxLength={200} /></label>
|
|
||||||
</div>
|
|
||||||
<label className="field"><span>Descripción</span><textarea value={description} onChange={(event)=>setDescription(event.target.value)} rows={2} maxLength={4000} /></label>
|
|
||||||
</div></details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-actions"><Link className="button secondary" to="/inventarios">Cancelar</Link><button className="button primary" disabled={saving || !name.trim() || (requiresParent && !parentId) || (requiresFamily && !familyId)}><Icon name="check" />{saving?'Creando…':`Crear ${kindLabel(kind)}`}</button></div>
|
<div className="form-actions"><Link className="button secondary" to="/inventarios">Cancelar</Link><button className="button primary" disabled={saving || !name.trim() || (requiresParent && !parentId) || (requiresFamily && !familyId)}><Icon name="check" />{saving?'Creando…':`Crear ${kindLabel(kind)}`}</button></div>
|
||||||
</form>
|
</form>
|
||||||
</section>;
|
</section>;
|
||||||
|
|||||||
Reference in New Issue
Block a user