Merge PR #54 · simplificar ficha de Instalación/Subinstalación
Compacta el Resumen de Instalaciones y Subinstalaciones, mueve el contexto físico al Historial y mantiene las funciones avanzadas accesibles sin alargar la ficha principal.
This commit is contained in:
@@ -0,0 +1,215 @@
|
|||||||
|
.asset-compact-form {
|
||||||
|
gap: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-section-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-section-heading h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-primary-grid {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-meta > span {
|
||||||
|
min-width: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid #e0e5ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-meta small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-meta strong {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-context-button {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 7px 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-context-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-context-item {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 12px 13px;
|
||||||
|
border: 1px solid #e1e6ee;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-context-item small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 750;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-context-item strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-context-item span {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 9px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details {
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details > summary {
|
||||||
|
min-height: 54px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 13px 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details > summary:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details > summary > span:first-child {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details > summary strong {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details > summary small {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details > summary > span:last-child {
|
||||||
|
color: #7b8496;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: transform .18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details[open] > summary > span:last-child {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 2px 22px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-technical {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-technical h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-form > .form-actions {
|
||||||
|
padding: 16px 22px 18px;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.asset-compact-section {
|
||||||
|
padding: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-section-heading {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-context-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-context-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details > summary {
|
||||||
|
padding: 13px 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-details-body {
|
||||||
|
padding: 2px 17px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-compact-form > .form-actions {
|
||||||
|
padding: 14px 17px 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import './AssetEditorPage.css';
|
||||||
import { SearchableSelect } from '../components/SearchableSelect';
|
import { SearchableSelect } from '../components/SearchableSelect';
|
||||||
import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
|
import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
|
||||||
import type { FormEvent } from 'react';
|
import type { FormEvent, ReactNode } from 'react';
|
||||||
import { Link, useNavigate, useParams, useSearchParams } from 'react-router';
|
import { Link, useNavigate, useParams, useSearchParams } from 'react-router';
|
||||||
import { useAuth } from '../auth/AuthContext';
|
import { useAuth } from '../auth/AuthContext';
|
||||||
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
||||||
import { Icon } from '../components/Icon';
|
import { Icon } from '../components/Icon';
|
||||||
import { ASSET_OPERATIONAL_STATUSES, ASSET_STATUSES, assetOperationalStatusLabel, assetStatusClass, assetStatusLabel } from '../features/assets/assetPresentation';
|
import {
|
||||||
|
ASSET_OPERATIONAL_STATUSES,
|
||||||
|
ASSET_STATUSES,
|
||||||
|
assetOperationalStatusLabel,
|
||||||
|
assetStatusClass,
|
||||||
|
assetStatusLabel,
|
||||||
|
} from '../features/assets/assetPresentation';
|
||||||
import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel';
|
import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel';
|
||||||
import { AssetDossierPanel } from '../features/assets/AssetDossierPanel';
|
import { AssetDossierPanel } from '../features/assets/AssetDossierPanel';
|
||||||
import { AssetMediaPanel } from '../features/assets/AssetMediaPanel';
|
import { AssetMediaPanel } from '../features/assets/AssetMediaPanel';
|
||||||
@@ -15,15 +22,31 @@ import { AssetRegistryPanel } from '../features/assets/AssetRegistryPanel';
|
|||||||
import { AssetContextHistoryPanel } from '../features/assets/AssetContextHistoryPanel';
|
import { AssetContextHistoryPanel } from '../features/assets/AssetContextHistoryPanel';
|
||||||
import { AssetFindingCatalogPanel } from '../features/assets/AssetFindingCatalogPanel';
|
import { AssetFindingCatalogPanel } from '../features/assets/AssetFindingCatalogPanel';
|
||||||
import {
|
import {
|
||||||
createAsset, getAsset, getAssetLineage, listAssetParentOptions, listAssetTypes, listCompaniesForArea,
|
createAsset,
|
||||||
listOperationalAreas, updateAsset, updateAssetInformationStatus, updateAssetOperationalStatus,
|
getAsset,
|
||||||
|
getAssetLineage,
|
||||||
|
listAssetParentOptions,
|
||||||
|
listAssetTypes,
|
||||||
|
listCompaniesForArea,
|
||||||
|
listOperationalAreas,
|
||||||
|
updateAsset,
|
||||||
|
updateAssetInformationStatus,
|
||||||
|
updateAssetOperationalStatus,
|
||||||
} from '../lib/api';
|
} from '../lib/api';
|
||||||
import type {
|
import type {
|
||||||
AssetAttributeDefinition, AssetDetail, AssetInformationStatus, AssetOperationalStatus,
|
AssetAttributeDefinition,
|
||||||
AssetLineageItem, AssetListItem, AssetType, OperationalAssetSummary,
|
AssetDetail,
|
||||||
|
AssetInformationStatus,
|
||||||
|
AssetOperationalStatus,
|
||||||
|
AssetLineageItem,
|
||||||
|
AssetListItem,
|
||||||
|
AssetType,
|
||||||
|
OperationalAssetSummary,
|
||||||
} from '../lib/api';
|
} from '../lib/api';
|
||||||
|
|
||||||
const AssetGeometryEditor = lazy(() => import('../features/map/AssetGeometryEditor').then((module) => ({ default: module.AssetGeometryEditor })));
|
const AssetGeometryEditor = lazy(() =>
|
||||||
|
import('../features/map/AssetGeometryEditor').then((module) => ({ default: module.AssetGeometryEditor })),
|
||||||
|
);
|
||||||
|
|
||||||
type DetailTab = 'summary' | 'dossier' | 'findings' | 'location' | 'registry' | 'files' | 'history';
|
type DetailTab = 'summary' | 'dossier' | 'findings' | 'location' | 'registry' | 'files' | 'history';
|
||||||
|
|
||||||
@@ -35,16 +58,26 @@ function localDateTime(value: unknown): string {
|
|||||||
return local.toISOString().slice(0, 16);
|
return local.toISOString().slice(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAttributeValues(definitions: AssetAttributeDefinition[], values: Record<string, unknown>): Record<string, unknown> {
|
function normalizeAttributeValues(
|
||||||
|
definitions: AssetAttributeDefinition[],
|
||||||
|
values: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
const result: Record<string, unknown> = {};
|
const result: Record<string, unknown> = {};
|
||||||
definitions.filter((item) => item.isActive).forEach((definition) => {
|
definitions.filter((item) => item.isActive).forEach((definition) => {
|
||||||
const raw = values[definition.id];
|
const raw = values[definition.id];
|
||||||
if (definition.dataType === 'BOOLEAN') result[definition.id] = Boolean(raw);
|
if (definition.dataType === 'BOOLEAN') result[definition.id] = Boolean(raw);
|
||||||
else if (raw !== undefined && raw !== null && raw !== '') result[definition.id] = definition.dataType === 'NUMBER' ? Number(raw) : raw;
|
else if (raw !== undefined && raw !== null && raw !== '') {
|
||||||
|
result[definition.id] = definition.dataType === 'NUMBER' ? Number(raw) : raw;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasAttributeValue(definition: AssetAttributeDefinition, value: unknown): boolean {
|
||||||
|
if (definition.dataType === 'BOOLEAN') return value === true;
|
||||||
|
return value !== undefined && value !== null && String(value).trim() !== '';
|
||||||
|
}
|
||||||
|
|
||||||
export function AssetEditorPage() {
|
export function AssetEditorPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const editing = Boolean(id);
|
const editing = Boolean(id);
|
||||||
@@ -52,7 +85,11 @@ export function AssetEditorPage() {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const contextParentId = !editing ? searchParams.get('parentId') : null;
|
const contextParentId = !editing ? searchParams.get('parentId') : null;
|
||||||
const requestedTab = searchParams.get('tab') as DetailTab | null;
|
const requestedTab = searchParams.get('tab') as DetailTab | null;
|
||||||
const tab: DetailTab = requestedTab && ['summary','dossier','findings','location','registry','files','history'].includes(requestedTab) ? requestedTab : 'summary';
|
const tab: DetailTab = requestedTab
|
||||||
|
&& ['summary', 'dossier', 'findings', 'location', 'registry', 'files', 'history'].includes(requestedTab)
|
||||||
|
? requestedTab
|
||||||
|
: 'summary';
|
||||||
|
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const canEdit = editing ? hasPermission('assets.update') : hasPermission('assets.create');
|
const canEdit = editing ? hasPermission('assets.update') : hasPermission('assets.create');
|
||||||
const canCreate = hasPermission('assets.create');
|
const canCreate = hasPermission('assets.create');
|
||||||
@@ -104,10 +141,14 @@ export function AssetEditorPage() {
|
|||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
const [historyRefreshKey, setHistoryRefreshKey] = useState(0);
|
const [historyRefreshKey, setHistoryRefreshKey] = useState(0);
|
||||||
|
|
||||||
const canDirectContextEdit = !editing || Boolean(asset?.dataOrigin === 'FIELD_SURVEY' && asset.informationStatus === 'DRAFT');
|
const canDirectContextEdit = !editing
|
||||||
|
|| Boolean(asset?.dataOrigin === 'FIELD_SURVEY' && asset.informationStatus === 'DRAFT');
|
||||||
|
|
||||||
const selectedType = types.find((type) => type.id === typeId) ?? null;
|
const selectedType = types.find((type) => type.id === typeId) ?? null;
|
||||||
const definitions = useMemo(() => selectedType?.attributes.filter((item) => item.isActive) ?? [], [selectedType]);
|
const definitions = useMemo(
|
||||||
|
() => selectedType?.attributes.filter((item) => item.isActive) ?? [],
|
||||||
|
[selectedType],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -119,13 +160,26 @@ export function AssetEditorPage() {
|
|||||||
setTypes(loadedTypes);
|
setTypes(loadedTypes);
|
||||||
setLineage(loadedLineage);
|
setLineage(loadedLineage);
|
||||||
if (loadedAsset) {
|
if (loadedAsset) {
|
||||||
setAsset(loadedAsset); setCode(loadedAsset.code); setName(loadedAsset.name); setCommonName(loadedAsset.commonName ?? ''); setTypeId(loadedAsset.type.id);
|
setAsset(loadedAsset);
|
||||||
setParentId(loadedAsset.parent?.id ?? ''); setOperationalAreaId(loadedAsset.operationalArea?.id ?? '');
|
setCode(loadedAsset.code);
|
||||||
setOperatorCompanyId(loadedAsset.operatorCompany?.id ?? ''); setDescription(loadedAsset.description ?? '');
|
setName(loadedAsset.name);
|
||||||
setStatus(loadedAsset.informationStatus); setOperationalStatus(loadedAsset.operationalStatus);
|
setCommonName(loadedAsset.commonName ?? '');
|
||||||
setAttributeValues(Object.fromEntries(loadedAsset.attributes.map((attribute) => [attribute.definitionId, attribute.dataType === 'DATETIME' ? localDateTime(attribute.value) : attribute.value ?? ''])));
|
setTypeId(loadedAsset.type.id);
|
||||||
|
setParentId(loadedAsset.parent?.id ?? '');
|
||||||
|
setOperationalAreaId(loadedAsset.operationalArea?.id ?? '');
|
||||||
|
setOperatorCompanyId(loadedAsset.operatorCompany?.id ?? '');
|
||||||
|
setDescription(loadedAsset.description ?? '');
|
||||||
|
setStatus(loadedAsset.informationStatus);
|
||||||
|
setOperationalStatus(loadedAsset.operationalStatus);
|
||||||
|
setAttributeValues(Object.fromEntries(
|
||||||
|
loadedAsset.attributes.map((attribute) => [
|
||||||
|
attribute.definitionId,
|
||||||
|
attribute.dataType === 'DATETIME' ? localDateTime(attribute.value) : attribute.value ?? '',
|
||||||
|
]),
|
||||||
|
));
|
||||||
} else if (!contextParentId) {
|
} else if (!contextParentId) {
|
||||||
const first = loadedTypes.find((type) => type.isActive && type.canBeRoot) ?? loadedTypes.find((type) => type.isActive);
|
const first = loadedTypes.find((type) => type.isActive && type.canBeRoot)
|
||||||
|
?? loadedTypes.find((type) => type.isActive);
|
||||||
if (first) setTypeId(first.id);
|
if (first) setTypeId(first.id);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -135,38 +189,56 @@ export function AssetEditorPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editing || !contextParentId || types.length === 0) return;
|
if (editing || !contextParentId || types.length === 0) return;
|
||||||
Promise.all([getAsset(contextParentId), getAssetLineage(contextParentId)]).then(([parent, parentLineage]) => {
|
Promise.all([getAsset(contextParentId), getAssetLineage(contextParentId)])
|
||||||
setContextParent(parent);
|
.then(([parent, parentLineage]) => {
|
||||||
setLineage(parentLineage);
|
setContextParent(parent);
|
||||||
const compatible = types.find((type) => type.isActive && type.allowedParentTypes.some((allowed) => allowed.id === parent.type.id));
|
setLineage(parentLineage);
|
||||||
if (compatible) {
|
const compatible = types.find(
|
||||||
setTypeId(compatible.id); setParentId(parent.id);
|
(type) => type.isActive && type.allowedParentTypes.some((allowed) => allowed.id === parent.type.id),
|
||||||
const parentType = types.find((type) => type.id === parent.type.id);
|
);
|
||||||
if (parentType?.operationalRole === 'AREA') setOperationalAreaId(parent.id);
|
if (compatible) {
|
||||||
else if (parent.operationalArea) setOperationalAreaId(parent.operationalArea.id);
|
setTypeId(compatible.id);
|
||||||
}
|
setParentId(parent.id);
|
||||||
}).catch((requestError) => setError(errorMessage(requestError)));
|
const parentType = types.find((type) => type.id === parent.type.id);
|
||||||
|
if (parentType?.operationalRole === 'AREA') setOperationalAreaId(parent.id);
|
||||||
|
else if (parent.operationalArea) setOperationalAreaId(parent.operationalArea.id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((requestError) => setError(errorMessage(requestError)));
|
||||||
}, [editing, contextParentId, types]);
|
}, [editing, contextParentId, types]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!typeId) { setParents([]); return; }
|
if (!typeId) {
|
||||||
const timer = window.setTimeout(() => listAssetParentOptions(typeId, id, parentSearch).then(setParents).catch((requestError) => setError(errorMessage(requestError))), 220);
|
setParents([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = window.setTimeout(
|
||||||
|
() => listAssetParentOptions(typeId, id, parentSearch)
|
||||||
|
.then(setParents)
|
||||||
|
.catch((requestError) => setError(errorMessage(requestError))),
|
||||||
|
220,
|
||||||
|
);
|
||||||
return () => window.clearTimeout(timer);
|
return () => window.clearTimeout(timer);
|
||||||
}, [typeId, id, parentSearch]);
|
}, [typeId, id, parentSearch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canReadRelations || selectedType?.operationalRole !== 'GENERIC' || !parentId) {
|
if (!canReadRelations || selectedType?.operationalRole !== 'GENERIC' || !parentId) {
|
||||||
setOperationalAreas([]);
|
setOperationalAreas([]);
|
||||||
if (!parentId) { setOperationalAreaId(''); if (!editing) setOperatorCompanyId(''); }
|
if (!parentId) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
listOperationalAreas(parentId).then((loadedAreas) => {
|
|
||||||
setOperationalAreas(loadedAreas);
|
|
||||||
if (operationalAreaId && !loadedAreas.some((area) => area.id === operationalAreaId)) {
|
|
||||||
setOperationalAreaId('');
|
setOperationalAreaId('');
|
||||||
if (!editing) setOperatorCompanyId('');
|
if (!editing) setOperatorCompanyId('');
|
||||||
}
|
}
|
||||||
}).catch((requestError) => setError(errorMessage(requestError)));
|
return;
|
||||||
|
}
|
||||||
|
listOperationalAreas(parentId)
|
||||||
|
.then((loadedAreas) => {
|
||||||
|
setOperationalAreas(loadedAreas);
|
||||||
|
if (operationalAreaId && !loadedAreas.some((area) => area.id === operationalAreaId)) {
|
||||||
|
setOperationalAreaId('');
|
||||||
|
if (!editing) setOperatorCompanyId('');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((requestError) => setError(errorMessage(requestError)));
|
||||||
}, [canReadRelations, selectedType?.operationalRole, parentId, editing]);
|
}, [canReadRelations, selectedType?.operationalRole, parentId, editing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -174,39 +246,85 @@ export function AssetEditorPage() {
|
|||||||
setOperationalCompanies([]);
|
setOperationalCompanies([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
listCompaniesForArea(operationalAreaId).then((loadedCompanies) => {
|
listCompaniesForArea(operationalAreaId)
|
||||||
setOperationalCompanies(loadedCompanies);
|
.then((loadedCompanies) => {
|
||||||
setOperatorCompanyId((current) => loadedCompanies.some((company) => company.id === current)
|
setOperationalCompanies(loadedCompanies);
|
||||||
? current
|
setOperatorCompanyId((current) => loadedCompanies.some((company) => company.id === current)
|
||||||
: loadedCompanies.length === 1 ? (loadedCompanies[0]?.id ?? '') : '');
|
? current
|
||||||
}).catch((requestError) => {
|
: loadedCompanies.length === 1 ? (loadedCompanies[0]?.id ?? '') : '');
|
||||||
setOperationalCompanies([]);
|
})
|
||||||
setOperatorCompanyId('');
|
.catch((requestError) => {
|
||||||
setError(errorMessage(requestError));
|
setOperationalCompanies([]);
|
||||||
});
|
setOperatorCompanyId('');
|
||||||
|
setError(errorMessage(requestError));
|
||||||
|
});
|
||||||
}, [editing, canReadRelations, operationalAreaId, selectedType?.operationalRole]);
|
}, [editing, canReadRelations, operationalAreaId, selectedType?.operationalRole]);
|
||||||
|
|
||||||
const changeType = (nextTypeId: string) => { setTypeId(nextTypeId); setParentId(''); setOperationalAreaId(''); if (!editing) setOperatorCompanyId(''); setAttributeValues({}); setParentSearch(''); };
|
const changeType = (nextTypeId: string) => {
|
||||||
const setAttribute = (definitionId: string, value: unknown) => setAttributeValues((current) => ({ ...current, [definitionId]: value }));
|
setTypeId(nextTypeId);
|
||||||
|
setParentId('');
|
||||||
|
setOperationalAreaId('');
|
||||||
|
if (!editing) setOperatorCompanyId('');
|
||||||
|
setAttributeValues({});
|
||||||
|
setParentSearch('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAttribute = (definitionId: string, value: unknown) =>
|
||||||
|
setAttributeValues((current) => ({ ...current, [definitionId]: value }));
|
||||||
|
|
||||||
const save = async (event: FormEvent) => {
|
const save = async (event: FormEvent) => {
|
||||||
event.preventDefault(); if (!selectedType) return;
|
event.preventDefault();
|
||||||
setSaving(true); setError(''); setSuccess('');
|
if (!selectedType) return;
|
||||||
|
setSaving(true);
|
||||||
|
setError('');
|
||||||
|
setSuccess('');
|
||||||
try {
|
try {
|
||||||
const attributes = normalizeAttributeValues(definitions, attributeValues);
|
const attributes = normalizeAttributeValues(definitions, attributeValues);
|
||||||
let saved: AssetDetail;
|
let saved: AssetDetail;
|
||||||
if (editing && id) {
|
if (editing && id) {
|
||||||
if (canEdit) saved = await updateAsset(id, { typeId: canDirectContextEdit ? typeId : undefined, code, name, commonName: commonName.trim() || null, parentId: canDirectContextEdit ? parentId || null : undefined, operationalAreaId: canDirectContextEdit ? operationalAreaId || null : undefined, description: description.trim() || null, attributes });
|
if (canEdit) {
|
||||||
else if (asset) saved = asset; else return;
|
saved = await updateAsset(id, {
|
||||||
if (canChangeStatus && saved.informationStatus !== status) saved = await updateAssetInformationStatus(id, status);
|
typeId: canDirectContextEdit ? typeId : undefined,
|
||||||
if (canChangeOperationalStatus && saved.operationalStatus !== operationalStatus) saved = await updateAssetOperationalStatus(id, operationalStatus);
|
code,
|
||||||
setAsset(saved); setSuccess('Registro actualizado correctamente'); setHistoryRefreshKey((current) => current + 1);
|
name,
|
||||||
|
commonName: commonName.trim() || null,
|
||||||
|
parentId: canDirectContextEdit ? parentId || null : undefined,
|
||||||
|
operationalAreaId: canDirectContextEdit ? operationalAreaId || null : undefined,
|
||||||
|
description: description.trim() || null,
|
||||||
|
attributes,
|
||||||
|
});
|
||||||
|
} else if (asset) saved = asset;
|
||||||
|
else return;
|
||||||
|
|
||||||
|
if (canChangeStatus && saved.informationStatus !== status) {
|
||||||
|
saved = await updateAssetInformationStatus(id, status);
|
||||||
|
}
|
||||||
|
if (canChangeOperationalStatus && saved.operationalStatus !== operationalStatus) {
|
||||||
|
saved = await updateAssetOperationalStatus(id, operationalStatus);
|
||||||
|
}
|
||||||
|
setAsset(saved);
|
||||||
|
setSuccess('Registro actualizado correctamente');
|
||||||
|
setHistoryRefreshKey((current) => current + 1);
|
||||||
} else {
|
} else {
|
||||||
saved = await createAsset({ code, name, commonName: commonName.trim() || null, typeId, parentId: parentId || null, operationalAreaId: operationalAreaId || null, operatorCompanyId: operatorCompanyId || null, description: description.trim() || null, informationStatus: canChangeStatus ? status : 'DRAFT', attributes });
|
saved = await createAsset({
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
commonName: commonName.trim() || null,
|
||||||
|
typeId,
|
||||||
|
parentId: parentId || null,
|
||||||
|
operationalAreaId: operationalAreaId || null,
|
||||||
|
operatorCompanyId: operatorCompanyId || null,
|
||||||
|
description: description.trim() || null,
|
||||||
|
informationStatus: canChangeStatus ? status : 'DRAFT',
|
||||||
|
attributes,
|
||||||
|
});
|
||||||
navigate(`/inventarios/${saved.id}`, { replace: true });
|
navigate(`/inventarios/${saved.id}`, { replace: true });
|
||||||
}
|
}
|
||||||
} catch (requestError) { setError(errorMessage(requestError)); }
|
} catch (requestError) {
|
||||||
finally { setSaving(false); }
|
setError(errorMessage(requestError));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <LoadingBlock label="Cargando registro…" />;
|
if (loading) return <LoadingBlock label="Cargando registro…" />;
|
||||||
@@ -221,44 +339,621 @@ export function AssetEditorPage() {
|
|||||||
{ key: 'history', label: 'Historial', show: editing && canReadHistory },
|
{ key: 'history', label: 'Historial', show: editing && canReadHistory },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const breadcrumbSection = asset?.type.code === 'empresa' || contextParent?.type.code === 'empresa' ? 'companies' : 'territory';
|
const breadcrumbSection = asset?.type.code === 'empresa' || contextParent?.type.code === 'empresa'
|
||||||
|
? 'companies'
|
||||||
|
: 'territory';
|
||||||
|
|
||||||
|
const structuralTypeCode = selectedType?.code.trim().toLowerCase() ?? '';
|
||||||
|
const compactStructuralSummary = editing
|
||||||
|
&& (structuralTypeCode === 'instalacion' || structuralTypeCode === 'subinstalacion');
|
||||||
|
|
||||||
|
const technicalFilledCount = definitions.filter(
|
||||||
|
(definition) => hasAttributeValue(definition, attributeValues[definition.id]),
|
||||||
|
).length;
|
||||||
|
const missingRequiredTechnical = definitions.some(
|
||||||
|
(definition) => definition.isRequired && !hasAttributeValue(definition, attributeValues[definition.id]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleContextChanged = (saved: AssetDetail) => {
|
||||||
|
setAsset(saved);
|
||||||
|
setParentId(saved.parent?.id ?? '');
|
||||||
|
setOperationalAreaId(saved.operationalArea?.id ?? '');
|
||||||
|
setOperatorCompanyId(saved.operatorCompany?.id ?? '');
|
||||||
|
setLineage([]);
|
||||||
|
getAssetLineage(saved.id).then(setLineage).catch(() => undefined);
|
||||||
|
setHistoryRefreshKey((current) => current + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAttributes = (): ReactNode => {
|
||||||
|
if (definitions.length === 0) {
|
||||||
|
return <div className="inline-empty">Este tipo no requiere datos técnicos adicionales.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="dynamic-attributes">
|
||||||
|
{definitions.map((definition) => {
|
||||||
|
const value = attributeValues[definition.id];
|
||||||
|
const label = <span>
|
||||||
|
{definition.name}{definition.unit ? ` (${definition.unit})` : ''}
|
||||||
|
{definition.isRequired ? <em>obligatorio</em> : <em>opcional</em>}
|
||||||
|
</span>;
|
||||||
|
|
||||||
|
if (definition.dataType === 'BOOLEAN') {
|
||||||
|
return <label className="check-row attribute-check" key={definition.id}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(value)}
|
||||||
|
onChange={(event) => setAttribute(definition.id, event.target.checked)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
/>
|
||||||
|
<span><strong>{definition.name}</strong><small>{definition.code}</small></span>
|
||||||
|
</label>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.dataType === 'SELECT') {
|
||||||
|
return <label className="field" key={definition.id}>
|
||||||
|
{label}
|
||||||
|
<SearchableSelect
|
||||||
|
value={String(value ?? '')}
|
||||||
|
onChange={(event) => setAttribute(definition.id, event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
required={definition.isRequired}
|
||||||
|
>
|
||||||
|
<option value="">Seleccionar…</option>
|
||||||
|
{definition.options?.map((option) =>
|
||||||
|
<option key={option} value={option}>{option}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
</label>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputType = definition.dataType === 'NUMBER'
|
||||||
|
? 'number'
|
||||||
|
: definition.dataType === 'DATE'
|
||||||
|
? 'date'
|
||||||
|
: definition.dataType === 'DATETIME'
|
||||||
|
? 'datetime-local'
|
||||||
|
: 'text';
|
||||||
|
|
||||||
|
return <label className="field" key={definition.id}>
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
type={inputType}
|
||||||
|
value={String(value ?? '')}
|
||||||
|
onChange={(event) => setAttribute(definition.id, event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
required={definition.isRequired}
|
||||||
|
step={definition.dataType === 'NUMBER' ? 'any' : undefined}
|
||||||
|
maxLength={definition.dataType === 'TEXT' ? 4000 : undefined}
|
||||||
|
/>
|
||||||
|
</label>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSaveActions = () => canSave && <div className="form-actions">
|
||||||
|
<Link className="button secondary" to="/inventarios">Cancelar</Link>
|
||||||
|
<button className="button primary" disabled={saving}>
|
||||||
|
<Icon name="check" />
|
||||||
|
{saving ? 'Guardando…' : editing && !canEdit ? 'Actualizar estado' : editing ? 'Guardar cambios' : 'Crear registro'}
|
||||||
|
</button>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
const renderCompactStructuralForm = () => <form className="panel form-panel asset-compact-form" onSubmit={save}>
|
||||||
|
<section className="asset-compact-section">
|
||||||
|
<div className="asset-compact-section-heading">
|
||||||
|
<div>
|
||||||
|
<h2>Datos principales</h2>
|
||||||
|
<p className="section-copy">Lo que se usa todos los días para reconocer este registro.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-grid asset-compact-primary-grid">
|
||||||
|
<label className="field">
|
||||||
|
<span>Nombre técnico</span>
|
||||||
|
<input
|
||||||
|
value={name}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
required
|
||||||
|
maxLength={200}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>Nombre habitual / sobrenombre <em>opcional</em></span>
|
||||||
|
<input
|
||||||
|
value={commonName}
|
||||||
|
onChange={(event) => setCommonName(event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
maxLength={200}
|
||||||
|
placeholder="Ej.: planta vieja, celda principal…"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="asset-compact-meta">
|
||||||
|
<span><small>Código DH</small><strong>{code}</strong></span>
|
||||||
|
<span><small>Tipo</small><strong>{selectedType?.name ?? 'Sin tipo'}</strong></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="asset-compact-section">
|
||||||
|
<div className="asset-compact-section-heading">
|
||||||
|
<div>
|
||||||
|
<h2>Ubicación actual</h2>
|
||||||
|
<p className="section-copy">Dónde está contenido el elemento dentro del Inventario.</p>
|
||||||
|
</div>
|
||||||
|
{canReadHistory && id && <Link className="button secondary asset-compact-context-button" to={`/inventarios/${id}?tab=history`}>
|
||||||
|
<Icon name="edit" />Cambiar / ver historial
|
||||||
|
</Link>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="asset-compact-context-grid">
|
||||||
|
<div className="asset-compact-context-item">
|
||||||
|
<small>Registro padre</small>
|
||||||
|
<strong>{asset?.parent?.name ?? 'Sin padre'}</strong>
|
||||||
|
{asset?.parent?.code && <span>{asset.parent.code}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="asset-compact-context-item">
|
||||||
|
<small>Área</small>
|
||||||
|
<strong>{asset?.operationalArea?.name ?? 'Sin área asignada'}</strong>
|
||||||
|
{asset?.operationalArea?.code && <span>{asset.operationalArea.code}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{(canChangeStatus || canChangeOperationalStatus) && <details className="asset-compact-details">
|
||||||
|
<summary>
|
||||||
|
<span><strong>Estado y opciones</strong><small>Calidad del dato y situación operativa</small></span>
|
||||||
|
<span aria-hidden="true">⌄</span>
|
||||||
|
</summary>
|
||||||
|
<div className="asset-compact-details-body">
|
||||||
|
<div className="form-grid">
|
||||||
|
{canChangeStatus && <label className="field">
|
||||||
|
<span>Estado del dato</span>
|
||||||
|
<SearchableSelect
|
||||||
|
value={status}
|
||||||
|
onChange={(event) => setStatus(event.target.value as AssetInformationStatus)}
|
||||||
|
disabled={!canEdit && !canChangeStatus}
|
||||||
|
>
|
||||||
|
{ASSET_STATUSES.map((item) =>
|
||||||
|
<option key={item.value} value={item.value}>{item.label}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
</label>}
|
||||||
|
{canChangeOperationalStatus && <label className="field">
|
||||||
|
<span>Estado operativo</span>
|
||||||
|
<SearchableSelect
|
||||||
|
value={operationalStatus}
|
||||||
|
onChange={(event) => setOperationalStatus(event.target.value as AssetOperationalStatus)}
|
||||||
|
>
|
||||||
|
{ASSET_OPERATIONAL_STATUSES.map((item) =>
|
||||||
|
<option key={item.value} value={item.value}>{item.label}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
</label>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>}
|
||||||
|
|
||||||
|
<details className="asset-compact-details" open={missingRequiredTechnical || canDirectContextEdit}>
|
||||||
|
<summary>
|
||||||
|
<span>
|
||||||
|
<strong>Más datos</strong>
|
||||||
|
<small>
|
||||||
|
{description.trim() ? 'Descripción cargada' : 'Sin descripción'}
|
||||||
|
{' · '}
|
||||||
|
{technicalFilledCount}/{definitions.length} datos técnicos cargados
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<span aria-hidden="true">⌄</span>
|
||||||
|
</summary>
|
||||||
|
<div className="asset-compact-details-body">
|
||||||
|
<div className="form-grid">
|
||||||
|
<label className="field">
|
||||||
|
<span>Código DH</span>
|
||||||
|
<input
|
||||||
|
value={code}
|
||||||
|
onChange={(event) => setCode(event.target.value.toUpperCase())}
|
||||||
|
disabled={!canEdit}
|
||||||
|
required
|
||||||
|
maxLength={120}
|
||||||
|
pattern="[A-Z0-9][A-Z0-9._/-]*"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{canDirectContextEdit && <label className="field">
|
||||||
|
<span>Tipo de elemento</span>
|
||||||
|
<SearchableSelect
|
||||||
|
value={typeId}
|
||||||
|
onChange={(event) => changeType(event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Seleccionar…</option>
|
||||||
|
{types.filter((type) => type.isActive || type.id === typeId).map((type) =>
|
||||||
|
<option key={type.id} value={type.id}>{type.name}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
</label>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canDirectContextEdit && <div className="form-grid">
|
||||||
|
<div className="field parent-picker">
|
||||||
|
<span>Registro padre {!selectedType?.canBeRoot && <em>obligatorio</em>}</span>
|
||||||
|
<input
|
||||||
|
className="parent-search"
|
||||||
|
value={parentSearch}
|
||||||
|
onChange={(event) => setParentSearch(event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
placeholder="Buscar planta, batería, estación…"
|
||||||
|
/>
|
||||||
|
<SearchableSelect
|
||||||
|
value={parentId}
|
||||||
|
onChange={(event) => {
|
||||||
|
setParentId(event.target.value);
|
||||||
|
setParentSearch('');
|
||||||
|
}}
|
||||||
|
disabled={!canEdit}
|
||||||
|
required={!selectedType?.canBeRoot}
|
||||||
|
>
|
||||||
|
<option value="">
|
||||||
|
{selectedType?.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>
|
||||||
|
|
||||||
|
{selectedType?.operationalRole === 'GENERIC' && canReadRelations && <label className="field">
|
||||||
|
<span>Área</span>
|
||||||
|
<SearchableSelect
|
||||||
|
value={operationalAreaId}
|
||||||
|
onChange={(event) => setOperationalAreaId(event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
>
|
||||||
|
<option value="">Sin asignación operativa</option>
|
||||||
|
{operationalAreas.map((area) =>
|
||||||
|
<option key={area.id} value={area.id}>{area.name}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
</label>}
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<label className="field">
|
||||||
|
<span>Descripción <em>opcional</em></span>
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(event) => setDescription(event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
maxLength={4000}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="asset-compact-technical">
|
||||||
|
<div>
|
||||||
|
<h3>Datos técnicos</h3>
|
||||||
|
<p className="section-copy">Sólo completá lo que corresponda para este tipo.</p>
|
||||||
|
</div>
|
||||||
|
{renderAttributes()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
{renderSaveActions()}
|
||||||
|
</form>;
|
||||||
|
|
||||||
|
const renderFullForm = () => <form className="panel form-panel" onSubmit={save}>
|
||||||
|
<div className="form-section">
|
||||||
|
<div>
|
||||||
|
<h2>Identificación</h2>
|
||||||
|
<p className="section-copy">Conservá el nombre técnico y, cuando exista, agregá el nombre habitual usado en campo.</p>
|
||||||
|
</div>
|
||||||
|
<div className="form-grid">
|
||||||
|
<label className="field">
|
||||||
|
<span>Código DH</span>
|
||||||
|
<input
|
||||||
|
value={code}
|
||||||
|
onChange={(event) => setCode(event.target.value.toUpperCase())}
|
||||||
|
disabled={!canEdit}
|
||||||
|
required
|
||||||
|
maxLength={120}
|
||||||
|
pattern="[A-Z0-9][A-Z0-9._/-]*"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>Nombre técnico</span>
|
||||||
|
<input value={name} onChange={(event) => setName(event.target.value)} disabled={!canEdit} required maxLength={200} />
|
||||||
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>Nombre habitual / sobrenombre <em>opcional</em></span>
|
||||||
|
<input
|
||||||
|
value={commonName}
|
||||||
|
onChange={(event) => setCommonName(event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
maxLength={200}
|
||||||
|
placeholder="Ej.: tanque grande, ET vieja, batería norte…"
|
||||||
|
/>
|
||||||
|
<small>También se usa en las búsquedas del Inventario.</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label className="field">
|
||||||
|
<span>Descripción <em>opcional</em></span>
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(event) => setDescription(event.target.value)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
maxLength={4000}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-section">
|
||||||
|
<div>
|
||||||
|
<h2>Ubicación en la estructura</h2>
|
||||||
|
<p className="section-copy">
|
||||||
|
Elegí qué es y dónde está contenido. La ubicación física es independiente de la Operadora del Área.
|
||||||
|
Los registros ya consolidados cambian de ubicación desde el bloque histórico inferior.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="form-grid">
|
||||||
|
<label className="field">
|
||||||
|
<span>Tipo de elemento</span>
|
||||||
|
<SearchableSelect
|
||||||
|
value={typeId}
|
||||||
|
onChange={(event) => changeType(event.target.value)}
|
||||||
|
disabled={(editing && !(asset?.dataOrigin === 'FIELD_SURVEY' && asset.informationStatus === 'DRAFT')) || !canEdit}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Seleccionar…</option>
|
||||||
|
{types.filter((type) => type.isActive || type.id === typeId).map((type) =>
|
||||||
|
<option key={type.id} value={type.id}>{type.name}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="field parent-picker">
|
||||||
|
<span>Registro padre {!selectedType?.canBeRoot && <em>obligatorio</em>}</span>
|
||||||
|
<input
|
||||||
|
className="parent-search"
|
||||||
|
value={parentSearch}
|
||||||
|
onChange={(event) => setParentSearch(event.target.value)}
|
||||||
|
disabled={!canEdit || !canDirectContextEdit}
|
||||||
|
placeholder="Buscar planta, batería, estación…"
|
||||||
|
/>
|
||||||
|
<SearchableSelect
|
||||||
|
value={parentId}
|
||||||
|
onChange={(event) => {
|
||||||
|
setParentId(event.target.value);
|
||||||
|
setParentSearch('');
|
||||||
|
}}
|
||||||
|
disabled={!canEdit || !canDirectContextEdit}
|
||||||
|
required={!selectedType?.canBeRoot}
|
||||||
|
>
|
||||||
|
<option value="">
|
||||||
|
{selectedType?.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>
|
||||||
|
<small>Escribí para buscar entre los registros compatibles.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-grid">
|
||||||
|
{canChangeStatus && <label className="field">
|
||||||
|
<span>Estado del dato</span>
|
||||||
|
<SearchableSelect
|
||||||
|
value={status}
|
||||||
|
onChange={(event) => setStatus(event.target.value as AssetInformationStatus)}
|
||||||
|
disabled={!canEdit && !canChangeStatus}
|
||||||
|
>
|
||||||
|
{ASSET_STATUSES.map((item) =>
|
||||||
|
<option key={item.value} value={item.value}>{item.label}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
<small>Calidad y validación del registro.</small>
|
||||||
|
</label>}
|
||||||
|
|
||||||
|
{editing && canChangeOperationalStatus && <label className="field">
|
||||||
|
<span>Estado operativo</span>
|
||||||
|
<SearchableSelect
|
||||||
|
value={operationalStatus}
|
||||||
|
onChange={(event) => setOperationalStatus(event.target.value as AssetOperationalStatus)}
|
||||||
|
>
|
||||||
|
{ASSET_OPERATIONAL_STATUSES.map((item) =>
|
||||||
|
<option key={item.value} value={item.value}>{item.label}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
<small>Situación física u operativa del elemento.</small>
|
||||||
|
</label>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedType?.operationalRole === 'GENERIC' && canReadRelations && <div className="form-section">
|
||||||
|
<div>
|
||||||
|
<h2>{editing ? 'Área física' : 'Área y operadora al alta'}</h2>
|
||||||
|
<p className="section-copy">
|
||||||
|
{editing
|
||||||
|
? 'El Inventario pertenece físicamente al Área. La Operadora vigente se administra en las relaciones temporales del Área y no se reescribe dentro del Inventario.'
|
||||||
|
: 'Al crear el registro, la Operadora activa del Área queda guardada únicamente como snapshot histórico de alta.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="form-grid">
|
||||||
|
<label className="field">
|
||||||
|
<span>Área</span>
|
||||||
|
<SearchableSelect
|
||||||
|
value={operationalAreaId}
|
||||||
|
onChange={(event) => {
|
||||||
|
setOperationalAreaId(event.target.value);
|
||||||
|
if (!editing) setOperatorCompanyId('');
|
||||||
|
}}
|
||||||
|
disabled={!canEdit || !canDirectContextEdit}
|
||||||
|
>
|
||||||
|
<option value="">Sin asignación operativa</option>
|
||||||
|
{operationalAreas.map((area) =>
|
||||||
|
<option key={area.id} value={area.id}>{area.name}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
{editing && <small>La Operadora se resuelve por la relación Área↔Empresa vigente.</small>}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{!editing && <label className="field">
|
||||||
|
<span>Operadora {operationalAreaId && <em>obligatoria</em>}</span>
|
||||||
|
<SearchableSelect
|
||||||
|
value={operatorCompanyId}
|
||||||
|
onChange={(event) => setOperatorCompanyId(event.target.value)}
|
||||||
|
disabled={!canEdit || !operationalAreaId}
|
||||||
|
required={Boolean(operationalAreaId)}
|
||||||
|
>
|
||||||
|
<option value="">{operationalAreaId ? 'Seleccionar operadora…' : 'Primero seleccioná un área'}</option>
|
||||||
|
{operationalCompanies.map((company) =>
|
||||||
|
<option key={company.id} value={company.id}>{company.name}</option>)}
|
||||||
|
</SearchableSelect>
|
||||||
|
</label>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!editing && operationalAreaId && operatorCompanyId && <div className="temporal-notice">
|
||||||
|
<Icon name="check" />
|
||||||
|
<p><strong>Contexto de alta confirmado.</strong> La Operadora seleccionada se conservará como referencia histórica; futuros cambios se harán en la relación temporal del Área.</p>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{editing && asset?.operatorCompany && <div className="temporal-notice">
|
||||||
|
<Icon name="layers" />
|
||||||
|
<p><strong>Snapshot histórico de alta:</strong> {asset.operatorCompany.name}. No se modifica desde este registro.</p>
|
||||||
|
</div>}
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<div className="form-section">
|
||||||
|
<div>
|
||||||
|
<h2>Datos técnicos</h2>
|
||||||
|
<p className="section-copy">Campos definidos para el tipo seleccionado.</p>
|
||||||
|
</div>
|
||||||
|
{renderAttributes()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderSaveActions()}
|
||||||
|
</form>;
|
||||||
|
|
||||||
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">
|
<nav className="breadcrumb asset-detail-breadcrumb" aria-label="Ruta del inventario">
|
||||||
<Link to="/inventarios">Inventarios</Link><span>›</span>
|
<Link to="/inventarios">Inventarios</Link><span>›</span>
|
||||||
<Link to={breadcrumbSection === 'companies' ? '/inventarios?section=companies' : '/inventarios?section=territory'}>{breadcrumbSection === 'companies' ? 'Empresas' : 'Áreas y yacimientos'}</Link>
|
<Link to={breadcrumbSection === 'companies' ? '/inventarios?section=companies' : '/inventarios?section=territory'}>
|
||||||
|
{breadcrumbSection === 'companies' ? 'Empresas' : 'Áreas y yacimientos'}
|
||||||
|
</Link>
|
||||||
{lineage.map((item, index) => {
|
{lineage.map((item, index) => {
|
||||||
const isLast = index === lineage.length - 1;
|
const isLast = index === lineage.length - 1;
|
||||||
const showAsCurrent = editing ? isLast : false;
|
const showAsCurrent = editing ? isLast : false;
|
||||||
return <span className="asset-detail-crumb-part" key={item.id}><span>›</span>{showAsCurrent ? <strong>{item.name}</strong> : <Link to={`/inventarios?section=${breadcrumbSection}&parentId=${item.id}`}>{item.name}</Link>}</span>;
|
return <span className="asset-detail-crumb-part" key={item.id}>
|
||||||
|
<span>›</span>
|
||||||
|
{showAsCurrent
|
||||||
|
? <strong>{item.name}</strong>
|
||||||
|
: <Link to={`/inventarios?section=${breadcrumbSection}&parentId=${item.id}`}>{item.name}</Link>}
|
||||||
|
</span>;
|
||||||
})}
|
})}
|
||||||
{!editing && <><span>›</span><strong>Nuevo registro</strong></>}
|
{!editing && <><span>›</span><strong>Nuevo registro</strong></>}
|
||||||
</nav>
|
</nav>
|
||||||
<div className="page-heading asset-editor-heading"><div><span className="eyebrow">INVENTARIO</span><h1>{editing ? asset?.name ?? 'Registro' : contextParent ? `Agregar en ${contextParent.name}` : 'Nuevo registro'}</h1><p>{editing ? `${asset?.type.name} · ${asset?.code}` : contextParent ? `El sistema heredará el contexto disponible de ${contextParent.code}.` : 'Creá una entidad con identidad, jerarquía y atributos propios.'}</p></div><div className="asset-heading-actions">{editing && asset && <div className="heading-statuses"><span className={`status-badge ${assetStatusClass(asset.informationStatus)}`}>{assetStatusLabel(asset.informationStatus)}</span><small>{assetOperationalStatusLabel(asset.operationalStatus)}</small></div>}{editing && id && canCreate && <Link className="button primary" to={`/inventarios/nuevo?parentId=${id}`}><Icon name="plus" />Agregar registro aquí</Link>}</div></div>
|
|
||||||
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
|
|
||||||
|
|
||||||
{editing && <nav className="asset-detail-tabs">{detailTabs.filter((item) => item.show).map((item) => <Link key={item.key} className={tab === item.key ? 'active' : ''} to={`/inventarios/${id}${item.key === 'summary' ? '' : `?tab=${item.key}`}`}>{item.label}</Link>)}</nav>}
|
<div className="page-heading asset-editor-heading">
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">INVENTARIO</span>
|
||||||
|
<h1>{editing ? asset?.name ?? 'Registro' : contextParent ? `Agregar en ${contextParent.name}` : 'Nuevo registro'}</h1>
|
||||||
|
<p>
|
||||||
|
{editing
|
||||||
|
? `${asset?.type.name} · ${asset?.code}`
|
||||||
|
: contextParent
|
||||||
|
? `El sistema heredará el contexto disponible de ${contextParent.code}.`
|
||||||
|
: 'Creá una entidad con identidad, jerarquía y atributos propios.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="asset-heading-actions">
|
||||||
|
{editing && asset && <div className="heading-statuses">
|
||||||
|
<span className={`status-badge ${assetStatusClass(asset.informationStatus)}`}>
|
||||||
|
{assetStatusLabel(asset.informationStatus)}
|
||||||
|
</span>
|
||||||
|
<small>{assetOperationalStatusLabel(asset.operationalStatus)}</small>
|
||||||
|
</div>}
|
||||||
|
{editing && id && canCreate && <Link className="button primary" to={`/inventarios/nuevo?parentId=${id}`}>
|
||||||
|
<Icon name="plus" />Agregar registro aquí
|
||||||
|
</Link>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <Alert>{error}</Alert>}
|
||||||
|
{success && <Alert type="success">{success}</Alert>}
|
||||||
|
|
||||||
|
{editing && <nav className="asset-detail-tabs">
|
||||||
|
{detailTabs.filter((item) => item.show).map((item) =>
|
||||||
|
<Link
|
||||||
|
key={item.key}
|
||||||
|
className={tab === item.key ? 'active' : ''}
|
||||||
|
to={`/inventarios/${id}${item.key === 'summary' ? '' : `?tab=${item.key}`}`}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>)}
|
||||||
|
</nav>}
|
||||||
|
|
||||||
{(!editing || tab === 'summary') && <>
|
{(!editing || tab === 'summary') && <>
|
||||||
{contextParent && !editing && <div className="context-create-banner"><Icon name="layers" /><div><strong>Alta contextual</strong><span>{contextParent.name} · {contextParent.code}</span></div><Link to={`/inventarios/${contextParent.id}`}>Ver padre</Link></div>}
|
{contextParent && !editing && <div className="context-create-banner">
|
||||||
<form className="panel form-panel" onSubmit={save}>
|
<Icon name="layers" />
|
||||||
<div className="form-section"><div><h2>Identificación</h2><p className="section-copy">Conservá el nombre técnico y, cuando exista, agregá el nombre habitual usado en campo.</p></div><div className="form-grid"><label className="field"><span>Código DH</span><input value={code} onChange={(event) => setCode(event.target.value.toUpperCase())} disabled={!canEdit} required maxLength={120} pattern="[A-Z0-9][A-Z0-9._/-]*" /></label><label className="field"><span>Nombre técnico</span><input value={name} onChange={(event) => setName(event.target.value)} disabled={!canEdit} required maxLength={200} /></label><label className="field"><span>Nombre habitual / sobrenombre <em>opcional</em></span><input value={commonName} onChange={(event) => setCommonName(event.target.value)} disabled={!canEdit} maxLength={200} placeholder="Ej.: tanque grande, ET vieja, batería norte…" /><small>También se usa en las búsquedas del Inventario.</small></label></div><label className="field"><span>Descripción <em>opcional</em></span><textarea value={description} onChange={(event) => setDescription(event.target.value)} disabled={!canEdit} maxLength={4000} rows={2} /></label></div>
|
<div><strong>Alta contextual</strong><span>{contextParent.name} · {contextParent.code}</span></div>
|
||||||
|
<Link to={`/inventarios/${contextParent.id}`}>Ver padre</Link>
|
||||||
|
</div>}
|
||||||
|
|
||||||
<div className="form-section"><div><h2>Ubicación en la estructura</h2><p className="section-copy">Elegí qué es y dónde está contenido. La ubicación física es independiente de la Operadora del Área. Los registros ya consolidados cambian de ubicación desde el bloque histórico inferior.</p></div><div className="form-grid"><label className="field"><span>Tipo de elemento</span><SearchableSelect value={typeId} onChange={(event) => changeType(event.target.value)} disabled={(editing && !(asset?.dataOrigin === 'FIELD_SURVEY' && asset.informationStatus === 'DRAFT')) || !canEdit} required><option value="">Seleccionar…</option>{types.filter((type) => type.isActive || type.id === typeId).map((type) => <option key={type.id} value={type.id}>{type.name}</option>)}</SearchableSelect></label><div className="field parent-picker"><span>Registro padre {!selectedType?.canBeRoot && <em>obligatorio</em>}</span><input className="parent-search" value={parentSearch} onChange={(event) => setParentSearch(event.target.value)} disabled={!canEdit || !canDirectContextEdit} placeholder="Buscar planta, batería, estación…" /><SearchableSelect value={parentId} onChange={(event) => { setParentId(event.target.value); setParentSearch(''); }} disabled={!canEdit || !canDirectContextEdit} required={!selectedType?.canBeRoot}><option value="">{selectedType?.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><small>Escribí para buscar entre los registros compatibles.</small></div></div><div className="form-grid">{canChangeStatus && <label className="field"><span>Estado del dato</span><SearchableSelect value={status} onChange={(event) => setStatus(event.target.value as AssetInformationStatus)} disabled={!canEdit && !canChangeStatus}>{ASSET_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect><small>Calidad y validación del registro.</small></label>}{editing && canChangeOperationalStatus && <label className="field"><span>Estado operativo</span><SearchableSelect value={operationalStatus} onChange={(event) => setOperationalStatus(event.target.value as AssetOperationalStatus)}>{ASSET_OPERATIONAL_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect><small>Situación física u operativa del elemento.</small></label>}</div></div>
|
{compactStructuralSummary ? renderCompactStructuralForm() : renderFullForm()}
|
||||||
|
|
||||||
{selectedType?.operationalRole === 'GENERIC' && canReadRelations && <div className="form-section"><div><h2>{editing ? 'Área física' : 'Área y operadora al alta'}</h2><p className="section-copy">{editing ? 'El Inventario pertenece físicamente al Área. La Operadora vigente se administra en las relaciones temporales del Área y no se reescribe dentro del Inventario.' : 'Al crear el registro, la Operadora activa del Área queda guardada únicamente como snapshot histórico de alta.'}</p></div><div className="form-grid"><label className="field"><span>Área</span><SearchableSelect value={operationalAreaId} onChange={(event) => { setOperationalAreaId(event.target.value); if (!editing) setOperatorCompanyId(''); }} disabled={!canEdit || !canDirectContextEdit}><option value="">Sin asignación operativa</option>{operationalAreas.map((area) => <option key={area.id} value={area.id}>{area.name}</option>)}</SearchableSelect>{editing && <small>La Operadora se resuelve por la relación Área↔Empresa vigente.</small>}</label>{!editing && <label className="field"><span>Operadora {operationalAreaId && <em>obligatoria</em>}</span><SearchableSelect value={operatorCompanyId} onChange={(event) => setOperatorCompanyId(event.target.value)} disabled={!canEdit || !operationalAreaId} required={Boolean(operationalAreaId)}><option value="">{operationalAreaId ? 'Seleccionar operadora…' : 'Primero seleccioná un área'}</option>{operationalCompanies.map((company) => <option key={company.id} value={company.id}>{company.name}</option>)}</SearchableSelect></label>}</div>{!editing && operationalAreaId && operatorCompanyId && <div className="temporal-notice"><Icon name="check" /><p><strong>Contexto de alta confirmado.</strong> La Operadora seleccionada se conservará como referencia histórica; futuros cambios se harán en la relación temporal del Área.</p></div>}{editing && asset?.operatorCompany && <div className="temporal-notice"><Icon name="layers" /><p><strong>Snapshot histórico de alta:</strong> {asset.operatorCompany.name}. No se modifica desde este registro.</p></div>}</div>}
|
{!compactStructuralSummary && editing && id && asset && selectedType && canReadHistory
|
||||||
|
&& <AssetContextHistoryPanel
|
||||||
|
asset={asset}
|
||||||
|
type={selectedType}
|
||||||
|
canManage={canManageContext}
|
||||||
|
onChanged={handleContextChanged}
|
||||||
|
/>}
|
||||||
|
|
||||||
<div className="form-section"><div><h2>Datos técnicos</h2><p className="section-copy">Campos definidos para el tipo seleccionado.</p></div>{definitions.length === 0 ? <div className="inline-empty">Este tipo no requiere datos técnicos adicionales.</div> : <div className="dynamic-attributes">{definitions.map((definition) => { const value = attributeValues[definition.id]; const label = <span>{definition.name}{definition.unit ? ` (${definition.unit})` : ''}{definition.isRequired ? <em>obligatorio</em> : <em>opcional</em>}</span>; if (definition.dataType === 'BOOLEAN') return <label className="check-row attribute-check" key={definition.id}><input type="checkbox" checked={Boolean(value)} onChange={(event) => setAttribute(definition.id, event.target.checked)} disabled={!canEdit} /><span><strong>{definition.name}</strong><small>{definition.code}</small></span></label>; if (definition.dataType === 'SELECT') return <label className="field" key={definition.id}>{label}<SearchableSelect value={String(value ?? '')} onChange={(event) => setAttribute(definition.id, event.target.value)} disabled={!canEdit} required={definition.isRequired}><option value="">Seleccionar…</option>{definition.options?.map((option) => <option key={option} value={option}>{option}</option>)}</SearchableSelect></label>; const inputType = definition.dataType === 'NUMBER' ? 'number' : definition.dataType === 'DATE' ? 'date' : definition.dataType === 'DATETIME' ? 'datetime-local' : 'text'; return <label className="field" key={definition.id}>{label}<input type={inputType} value={String(value ?? '')} onChange={(event) => setAttribute(definition.id, event.target.value)} disabled={!canEdit} required={definition.isRequired} step={definition.dataType === 'NUMBER' ? 'any' : undefined} maxLength={definition.dataType === 'TEXT' ? 4000 : undefined} /></label>; })}</div>}</div>
|
{editing && id && asset && canReadRelations && selectedType && selectedType.operationalRole !== 'GENERIC'
|
||||||
{canSave && <div className="form-actions"><Link className="button secondary" to="/inventarios">Cancelar</Link><button className="button primary" disabled={saving}><Icon name="check" />{saving ? 'Guardando…' : editing && !canEdit ? 'Actualizar estado' : editing ? 'Guardar cambios' : 'Crear registro'}</button></div>}
|
&& <AssetOperationalRelationsPanel assetId={id} role={selectedType.operationalRole} canManage={canManageRelations} />}
|
||||||
</form>
|
|
||||||
{editing && id && asset && selectedType && canReadHistory && <AssetContextHistoryPanel asset={asset} type={selectedType} canManage={canManageContext} onChanged={(saved) => { setAsset(saved); setParentId(saved.parent?.id ?? ''); setOperationalAreaId(saved.operationalArea?.id ?? ''); setOperatorCompanyId(saved.operatorCompany?.id ?? ''); setLineage([]); getAssetLineage(saved.id).then(setLineage).catch(() => undefined); setHistoryRefreshKey((current) => current + 1); }} />}
|
|
||||||
{editing && id && asset && canReadRelations && selectedType && selectedType.operationalRole !== 'GENERIC' && <AssetOperationalRelationsPanel assetId={id} role={selectedType.operationalRole} canManage={canManageRelations} />}
|
|
||||||
</>}
|
</>}
|
||||||
|
|
||||||
{editing && id && asset && tab === 'findings' && canReadFindingCatalog && <AssetFindingCatalogPanel assetId={id} canManage={canManageFindingCatalog} />}
|
{editing && id && asset && tab === 'findings' && canReadFindingCatalog
|
||||||
{editing && id && asset && tab === 'dossier' && canReadDossier && <AssetDossierPanel assetId={id} />}
|
&& <AssetFindingCatalogPanel assetId={id} canManage={canManageFindingCatalog} />}
|
||||||
{editing && id && asset && tab === 'location' && <Suspense fallback={<div className="panel"><LoadingBlock label="Cargando ubicación…" /></div>}><AssetGeometryEditor assetId={id} assetName={asset.name} canEdit={canEditGeometry} onChanged={() => setHistoryRefreshKey((current) => current + 1)} /></Suspense>}
|
|
||||||
{editing && id && asset && tab === 'registry' && <div className="asset-tab-stack">{canReadRegistry && selectedType && <AssetRegistryPanel assetId={id} assetName={asset.name} role={selectedType.operationalRole} canManage={canManageRegistry} onChanged={() => setHistoryRefreshKey((current) => current + 1)} />}{canReadProvenance && <AssetProvenancePanel assetId={id} canManage={canManageProvenance} canVerify={canVerifyProvenance} onChanged={() => setHistoryRefreshKey((current) => current + 1)} />}</div>}
|
{editing && id && asset && tab === 'dossier' && canReadDossier
|
||||||
{editing && id && asset && tab === 'files' && canReadMedia && <AssetMediaPanel assetId={id} assetName={asset.name} canManage={canManageMedia} onChanged={() => setHistoryRefreshKey((current) => current + 1)} />}
|
&& <AssetDossierPanel assetId={id} />}
|
||||||
{editing && id && tab === 'history' && canReadHistory && <AssetHistoryPanel assetId={id} refreshKey={historyRefreshKey} />}
|
|
||||||
|
{editing && id && asset && tab === 'location'
|
||||||
|
&& <Suspense fallback={<div className="panel"><LoadingBlock label="Cargando ubicación…" /></div>}>
|
||||||
|
<AssetGeometryEditor
|
||||||
|
assetId={id}
|
||||||
|
assetName={asset.name}
|
||||||
|
canEdit={canEditGeometry}
|
||||||
|
onChanged={() => setHistoryRefreshKey((current) => current + 1)}
|
||||||
|
/>
|
||||||
|
</Suspense>}
|
||||||
|
|
||||||
|
{editing && id && asset && tab === 'registry' && <div className="asset-tab-stack">
|
||||||
|
{canReadRegistry && selectedType && <AssetRegistryPanel
|
||||||
|
assetId={id}
|
||||||
|
assetName={asset.name}
|
||||||
|
role={selectedType.operationalRole}
|
||||||
|
canManage={canManageRegistry}
|
||||||
|
onChanged={() => setHistoryRefreshKey((current) => current + 1)}
|
||||||
|
/>}
|
||||||
|
{canReadProvenance && <AssetProvenancePanel
|
||||||
|
assetId={id}
|
||||||
|
canManage={canManageProvenance}
|
||||||
|
canVerify={canVerifyProvenance}
|
||||||
|
onChanged={() => setHistoryRefreshKey((current) => current + 1)}
|
||||||
|
/>}
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{editing && id && asset && tab === 'files' && canReadMedia
|
||||||
|
&& <AssetMediaPanel
|
||||||
|
assetId={id}
|
||||||
|
assetName={asset.name}
|
||||||
|
canManage={canManageMedia}
|
||||||
|
onChanged={() => setHistoryRefreshKey((current) => current + 1)}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
{editing && id && tab === 'history' && canReadHistory && <div className="asset-tab-stack">
|
||||||
|
{compactStructuralSummary && asset && selectedType && <AssetContextHistoryPanel
|
||||||
|
asset={asset}
|
||||||
|
type={selectedType}
|
||||||
|
canManage={canManageContext}
|
||||||
|
onChanged={handleContextChanged}
|
||||||
|
/>}
|
||||||
|
<AssetHistoryPanel assetId={id} refreshKey={historyRefreshKey} />
|
||||||
|
</div>}
|
||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user