From 1b151fe964c7301703894fc9179f1848b11b17e8 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 14:26:42 -0300 Subject: [PATCH] =?UTF-8?q?feat(web):=20simplificar=20ficha=20de=20instala?= =?UTF-8?q?ci=C3=B3n=20y=20subinstalaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web-v2/src/pages/AssetEditorPage.tsx | 859 ++++++++++++++++++++++++--- 1 file changed, 777 insertions(+), 82 deletions(-) diff --git a/web-v2/src/pages/AssetEditorPage.tsx b/web-v2/src/pages/AssetEditorPage.tsx index e3acf00..5dcb14b 100644 --- a/web-v2/src/pages/AssetEditorPage.tsx +++ b/web-v2/src/pages/AssetEditorPage.tsx @@ -1,11 +1,18 @@ +import './AssetEditorPage.css'; import { SearchableSelect } from '../components/SearchableSelect'; 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 { useAuth } from '../auth/AuthContext'; import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; 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 { AssetDossierPanel } from '../features/assets/AssetDossierPanel'; import { AssetMediaPanel } from '../features/assets/AssetMediaPanel'; @@ -15,15 +22,31 @@ import { AssetRegistryPanel } from '../features/assets/AssetRegistryPanel'; import { AssetContextHistoryPanel } from '../features/assets/AssetContextHistoryPanel'; import { AssetFindingCatalogPanel } from '../features/assets/AssetFindingCatalogPanel'; import { - createAsset, getAsset, getAssetLineage, listAssetParentOptions, listAssetTypes, listCompaniesForArea, - listOperationalAreas, updateAsset, updateAssetInformationStatus, updateAssetOperationalStatus, + createAsset, + getAsset, + getAssetLineage, + listAssetParentOptions, + listAssetTypes, + listCompaniesForArea, + listOperationalAreas, + updateAsset, + updateAssetInformationStatus, + updateAssetOperationalStatus, } from '../lib/api'; import type { - AssetAttributeDefinition, AssetDetail, AssetInformationStatus, AssetOperationalStatus, - AssetLineageItem, AssetListItem, AssetType, OperationalAssetSummary, + AssetAttributeDefinition, + AssetDetail, + AssetInformationStatus, + AssetOperationalStatus, + AssetLineageItem, + AssetListItem, + AssetType, + OperationalAssetSummary, } 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'; @@ -35,16 +58,26 @@ function localDateTime(value: unknown): string { return local.toISOString().slice(0, 16); } -function normalizeAttributeValues(definitions: AssetAttributeDefinition[], values: Record): Record { +function normalizeAttributeValues( + definitions: AssetAttributeDefinition[], + values: Record, +): Record { const result: Record = {}; definitions.filter((item) => item.isActive).forEach((definition) => { const raw = values[definition.id]; 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; } +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() { const { id } = useParams(); const editing = Boolean(id); @@ -52,7 +85,11 @@ export function AssetEditorPage() { const [searchParams] = useSearchParams(); const contextParentId = !editing ? searchParams.get('parentId') : 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 canEdit = editing ? hasPermission('assets.update') : hasPermission('assets.create'); const canCreate = hasPermission('assets.create'); @@ -104,10 +141,14 @@ export function AssetEditorPage() { const [success, setSuccess] = useState(''); 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 definitions = useMemo(() => selectedType?.attributes.filter((item) => item.isActive) ?? [], [selectedType]); + const definitions = useMemo( + () => selectedType?.attributes.filter((item) => item.isActive) ?? [], + [selectedType], + ); useEffect(() => { Promise.all([ @@ -119,13 +160,26 @@ export function AssetEditorPage() { setTypes(loadedTypes); setLineage(loadedLineage); if (loadedAsset) { - setAsset(loadedAsset); setCode(loadedAsset.code); setName(loadedAsset.name); setCommonName(loadedAsset.commonName ?? ''); 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 ?? '']))); + setAsset(loadedAsset); + setCode(loadedAsset.code); + setName(loadedAsset.name); + setCommonName(loadedAsset.commonName ?? ''); + 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) { - 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); } }) @@ -135,38 +189,56 @@ export function AssetEditorPage() { useEffect(() => { if (editing || !contextParentId || types.length === 0) return; - Promise.all([getAsset(contextParentId), getAssetLineage(contextParentId)]).then(([parent, parentLineage]) => { - setContextParent(parent); - setLineage(parentLineage); - const compatible = types.find((type) => type.isActive && type.allowedParentTypes.some((allowed) => allowed.id === parent.type.id)); - if (compatible) { - setTypeId(compatible.id); setParentId(parent.id); - 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))); + Promise.all([getAsset(contextParentId), getAssetLineage(contextParentId)]) + .then(([parent, parentLineage]) => { + setContextParent(parent); + setLineage(parentLineage); + const compatible = types.find( + (type) => type.isActive && type.allowedParentTypes.some((allowed) => allowed.id === parent.type.id), + ); + if (compatible) { + setTypeId(compatible.id); + setParentId(parent.id); + 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]); useEffect(() => { - if (!typeId) { setParents([]); return; } - const timer = window.setTimeout(() => listAssetParentOptions(typeId, id, parentSearch).then(setParents).catch((requestError) => setError(errorMessage(requestError))), 220); + if (!typeId) { + setParents([]); + return; + } + const timer = window.setTimeout( + () => listAssetParentOptions(typeId, id, parentSearch) + .then(setParents) + .catch((requestError) => setError(errorMessage(requestError))), + 220, + ); return () => window.clearTimeout(timer); }, [typeId, id, parentSearch]); useEffect(() => { if (!canReadRelations || selectedType?.operationalRole !== 'GENERIC' || !parentId) { setOperationalAreas([]); - if (!parentId) { setOperationalAreaId(''); if (!editing) setOperatorCompanyId(''); } - return; - } - listOperationalAreas(parentId).then((loadedAreas) => { - setOperationalAreas(loadedAreas); - if (operationalAreaId && !loadedAreas.some((area) => area.id === operationalAreaId)) { + if (!parentId) { setOperationalAreaId(''); 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]); useEffect(() => { @@ -174,39 +246,85 @@ export function AssetEditorPage() { setOperationalCompanies([]); return; } - listCompaniesForArea(operationalAreaId).then((loadedCompanies) => { - setOperationalCompanies(loadedCompanies); - setOperatorCompanyId((current) => loadedCompanies.some((company) => company.id === current) - ? current - : loadedCompanies.length === 1 ? (loadedCompanies[0]?.id ?? '') : ''); - }).catch((requestError) => { - setOperationalCompanies([]); - setOperatorCompanyId(''); - setError(errorMessage(requestError)); - }); + listCompaniesForArea(operationalAreaId) + .then((loadedCompanies) => { + setOperationalCompanies(loadedCompanies); + setOperatorCompanyId((current) => loadedCompanies.some((company) => company.id === current) + ? current + : loadedCompanies.length === 1 ? (loadedCompanies[0]?.id ?? '') : ''); + }) + .catch((requestError) => { + setOperationalCompanies([]); + setOperatorCompanyId(''); + setError(errorMessage(requestError)); + }); }, [editing, canReadRelations, operationalAreaId, selectedType?.operationalRole]); - const changeType = (nextTypeId: string) => { setTypeId(nextTypeId); setParentId(''); setOperationalAreaId(''); if (!editing) setOperatorCompanyId(''); setAttributeValues({}); setParentSearch(''); }; - const setAttribute = (definitionId: string, value: unknown) => setAttributeValues((current) => ({ ...current, [definitionId]: value })); + const changeType = (nextTypeId: string) => { + 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) => { - event.preventDefault(); if (!selectedType) return; - setSaving(true); setError(''); setSuccess(''); + event.preventDefault(); + if (!selectedType) return; + setSaving(true); + setError(''); + setSuccess(''); try { const attributes = normalizeAttributeValues(definitions, attributeValues); let saved: AssetDetail; 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 }); - 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); + 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, + }); + } 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 { - 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 }); } - } catch (requestError) { setError(errorMessage(requestError)); } - finally { setSaving(false); } + } catch (requestError) { + setError(errorMessage(requestError)); + } finally { + setSaving(false); + } }; if (loading) return ; @@ -221,44 +339,621 @@ export function AssetEditorPage() { { key: 'history', label: 'Historial', show: editing && canReadHistory }, ] 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
Este tipo no requiere datos técnicos adicionales.
; + } + + return
+ {definitions.map((definition) => { + const value = attributeValues[definition.id]; + const label = + {definition.name}{definition.unit ? ` (${definition.unit})` : ''} + {definition.isRequired ? obligatorio : opcional} + ; + + if (definition.dataType === 'BOOLEAN') { + return ; + } + + if (definition.dataType === 'SELECT') { + return ; + } + + const inputType = definition.dataType === 'NUMBER' + ? 'number' + : definition.dataType === 'DATE' + ? 'date' + : definition.dataType === 'DATETIME' + ? 'datetime-local' + : 'text'; + + return ; + })} +
; + }; + + const renderSaveActions = () => canSave &&
+ Cancelar + +
; + + const renderCompactStructuralForm = () =>
+
+
+
+

Datos principales

+

Lo que se usa todos los días para reconocer este registro.

+
+
+ +
+ + +
+ +
+ Código DH{code} + Tipo{selectedType?.name ?? 'Sin tipo'} +
+
+ +
+
+
+

Ubicación actual

+

Dónde está contenido el elemento dentro del Inventario.

+
+ {canReadHistory && id && + Cambiar / ver historial + } +
+ +
+
+ Registro padre + {asset?.parent?.name ?? 'Sin padre'} + {asset?.parent?.code && {asset.parent.code}} +
+
+ Área + {asset?.operationalArea?.name ?? 'Sin área asignada'} + {asset?.operationalArea?.code && {asset.operationalArea.code}} +
+
+
+ + {(canChangeStatus || canChangeOperationalStatus) &&
+ + Estado y opcionesCalidad del dato y situación operativa + + +
+
+ {canChangeStatus && } + {canChangeOperationalStatus && } +
+
+
} + +
+ + + Más datos + + {description.trim() ? 'Descripción cargada' : 'Sin descripción'} + {' · '} + {technicalFilledCount}/{definitions.length} datos técnicos cargados + + + + +
+
+ + + {canDirectContextEdit && } +
+ + {canDirectContextEdit &&
+
+ Registro padre {!selectedType?.canBeRoot && obligatorio} + setParentSearch(event.target.value)} + disabled={!canEdit} + placeholder="Buscar planta, batería, estación…" + /> + { + setParentId(event.target.value); + setParentSearch(''); + }} + disabled={!canEdit} + required={!selectedType?.canBeRoot} + > + + {parents.map((parent) => + )} + +
+ + {selectedType?.operationalRole === 'GENERIC' && canReadRelations && } +
} + +