fix(f6.1): separate physical context from area operator in web

This commit is contained in:
2026-09-09 15:18:28 -03:00
parent b713be4d15
commit ca1fddc834
@@ -7,7 +7,6 @@ import {
changeAssetContext,
listAssetContextHistory,
listAssetParentOptions,
listCompaniesForArea,
listOperationalAreas,
} from '../../lib/api';
import type {
@@ -29,7 +28,6 @@ function contextLabel(item: AssetContextHistoryItem) {
const parts = [
item.parent ? `Padre: ${item.parent.name}` : 'Sin padre',
item.operationalArea ? `Área: ${item.operationalArea.name}` : null,
item.operatorCompany ? `Operadora: ${item.operatorCompany.name}` : null,
].filter(Boolean);
return parts.join(' · ');
}
@@ -55,9 +53,7 @@ export function AssetContextHistoryPanel({
const [parentSearch, setParentSearch] = useState('');
const [parents, setParents] = useState<AssetListItem[]>([]);
const [operationalAreaId, setOperationalAreaId] = useState(asset.operationalArea?.id ?? '');
const [operatorCompanyId, setOperatorCompanyId] = useState(asset.operatorCompany?.id ?? '');
const [areas, setAreas] = useState<OperationalAssetSummary[]>([]);
const [companies, setCompanies] = useState<OperationalAssetSummary[]>([]);
const [effectiveAt, setEffectiveAt] = useState(localDateTimeNow());
const [reason, setReason] = useState('');
@@ -94,29 +90,14 @@ export function AssetContextHistoryPanel({
setAreas(items);
if (operationalAreaId && !items.some((item) => item.id === operationalAreaId)) {
setOperationalAreaId('');
setOperatorCompanyId('');
}
})
.catch((requestError) => setError(errorMessage(requestError)));
}, [editing, genericContext, parentId]);
useEffect(() => {
if (!editing || !genericContext || !operationalAreaId) {
setCompanies([]);
return;
}
listCompaniesForArea(operationalAreaId)
.then((items) => {
setCompanies(items);
if (operatorCompanyId && !items.some((item) => item.id === operatorCompanyId)) setOperatorCompanyId('');
})
.catch((requestError) => setError(errorMessage(requestError)));
}, [editing, genericContext, operationalAreaId]);
const beginEdit = () => {
setParentId(asset.parent?.id ?? '');
setOperationalAreaId(asset.operationalArea?.id ?? '');
setOperatorCompanyId(asset.operatorCompany?.id ?? '');
setEffectiveAt(localDateTimeNow());
setReason('');
setError('');
@@ -133,13 +114,12 @@ export function AssetContextHistoryPanel({
const saved = await changeAssetContext(asset.id, {
parentId: parentId || null,
operationalAreaId: genericContext ? operationalAreaId || null : asset.operationalArea?.id ?? null,
operatorCompanyId: genericContext ? operatorCompanyId || null : asset.operatorCompany?.id ?? null,
effectiveAt: effectiveAt ? new Date(effectiveAt).toISOString() : undefined,
reason,
});
onChanged(saved);
setEditing(false);
setSuccess('Contexto actualizado. La asignación anterior quedó preservada en el historial.');
setSuccess('Contexto físico actualizado. La asignación anterior quedó preservada en el historial.');
loadHistory();
} catch (requestError) {
setError(errorMessage(requestError));
@@ -150,30 +130,29 @@ export function AssetContextHistoryPanel({
return <article className="panel asset-history-panel">
<div className="panel-heading">
<div><span className="eyebrow">CONTEXTO TEMPORAL</span><h2>Jerarquía, Área y Operadora</h2></div>
{canManage && !editing && <button type="button" className="button secondary" onClick={beginEdit}><Icon name="edit" />Cambiar contexto</button>}
<div><span className="eyebrow">CONTEXTO TEMPORAL</span><h2>Jerarquía y Área</h2></div>
{canManage && !editing && <button type="button" className="button secondary" onClick={beginEdit}><Icon name="edit" />Cambiar contexto físico</button>}
</div>
<p className="section-copy">Los cambios no reemplazan la historia. Cada asignación conserva desde cuándo fue válida y qué relación la reemplazó.</p>
<p className="section-copy">Los cambios físicos no reemplazan la historia. La Operadora se administra por separado en las relaciones temporales del Área.</p>
{error && <Alert>{error}</Alert>}
{success && <Alert type="success">{success}</Alert>}
{current && <div className="temporal-notice"><Icon name="layers" /><p><strong>Contexto vigente.</strong> {contextLabel(current)}</p></div>}
{current && <div className="temporal-notice"><Icon name="layers" /><p><strong>Contexto físico vigente.</strong> {contextLabel(current)}</p></div>}
{editing && <form className="form-section" onSubmit={save}>
<div><h3>Cambiar contexto vigente</h3><p className="section-copy">Indicá el nuevo lugar dentro del Inventario y el motivo. La relación anterior se cierra automáticamente.</p></div>
<div><h3>Cambiar contexto físico vigente</h3><p className="section-copy">Indicá la nueva ubicación dentro del Inventario y el motivo. La relación física anterior se cierra automáticamente.</p></div>
<div className="form-grid">
<div className="field parent-picker"><span>Registro padre</span><input className="parent-search" value={parentSearch} onChange={(event) => setParentSearch(event.target.value)} placeholder="Buscar registro padre…" /><SearchableSelect value={parentId} onChange={(event) => { setParentId(event.target.value); setParentSearch(''); }} required={!type.canBeRoot}><option value="">{type.canBeRoot ? 'Sin padre · registro raíz' : 'Seleccionar registro padre…'}</option>{parents.map((parent) => <option key={parent.id} value={parent.id}>{parent.name} · {parent.code} ({parent.type.name})</option>)}</SearchableSelect></div>
<label className="field"><span>Vigente desde</span><input type="datetime-local" value={effectiveAt} onChange={(event) => setEffectiveAt(event.target.value)} required /><small>Puede registrarse una vigencia pasada si corresponde a un cambio ya ocurrido.</small></label>
</div>
{genericContext && <div className="form-grid">
<label className="field"><span>Área</span><SearchableSelect value={operationalAreaId} onChange={(event) => { setOperationalAreaId(event.target.value); setOperatorCompanyId(''); }} required><option value="">Seleccionar Área</option>{areas.map((area) => <option key={area.id} value={area.id}>{area.name}</option>)}</SearchableSelect></label>
<label className="field"><span>Operadora</span><SearchableSelect value={operatorCompanyId} onChange={(event) => setOperatorCompanyId(event.target.value)} required disabled={!operationalAreaId}><option value="">{operationalAreaId ? 'Seleccionar Operadora…' : 'Primero seleccioná un Área'}</option>{companies.map((company) => <option key={company.id} value={company.id}>{company.name}</option>)}</SearchableSelect></label>
<label className="field"><span>Área</span><SearchableSelect value={operationalAreaId} onChange={(event) => setOperationalAreaId(event.target.value)} required><option value="">Seleccionar Área</option>{areas.map((area) => <option key={area.id} value={area.id}>{area.name}</option>)}</SearchableSelect><small>La Operadora vigente se resuelve desde la relación temporal del Área.</small></label>
</div>}
<label className="field"><span>Motivo del cambio</span><textarea value={reason} onChange={(event) => setReason(event.target.value)} minLength={5} maxLength={2000} rows={3} required placeholder="Ej.: transferencia operativa, corrección documental, reubicación física…" /></label>
<div className="form-actions"><button type="button" className="button secondary" onClick={() => setEditing(false)} disabled={saving}>Cancelar</button><button className="button primary" disabled={saving || reason.trim().length < 5}><Icon name="check" />{saving ? 'Registrando…' : 'Registrar cambio'}</button></div>
<label className="field"><span>Motivo del cambio</span><textarea value={reason} onChange={(event) => setReason(event.target.value)} minLength={5} maxLength={2000} rows={3} required placeholder="Ej.: corrección documental, reubicación física, ajuste de jerarquía…" /></label>
<div className="form-actions"><button type="button" className="button secondary" onClick={() => setEditing(false)} disabled={saving}>Cancelar</button><button className="button primary" disabled={saving || reason.trim().length < 5}><Icon name="check" />{saving ? 'Registrando…' : 'Registrar cambio físico'}</button></div>
</form>}
<div className="form-section"><div><h3>Historial de contexto</h3><p className="section-copy">Se muestra la secuencia completa de relaciones conocidas del elemento.</p></div>
<div className="form-section"><div><h3>Historial de contexto físico</h3><p className="section-copy">Se muestra la secuencia completa de ubicaciones conocidas del elemento.</p></div>
{loading ? <LoadingBlock label="Cargando contexto…" /> : history.length === 0 ? <div className="inline-empty">Todavía no hay contexto histórico registrado.</div> : <div className="asset-timeline">{history.map((item) => <div className="timeline-entry" key={item.id}><span className={`timeline-dot ${item.isCurrent ? 'current' : ''}`} /><span><strong>{item.isCurrent ? 'Vigente' : 'Histórico'} · v{item.assetVersionNumber}</strong><small>{formatDate(item.validFrom)} {item.validUntil ? formatDate(item.validUntil) : 'actualidad'}{item.creator ? ` · ${item.creator.username}` : ''}</small><span>{contextLabel(item)}</span><small>{item.changeReason}</small></span></div>)}</div>}
</div>
</article>;