fix(f6.1): separate physical context from area operator in web
This commit is contained in:
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user