fix(web): clarify act context and operational map
DH V2 CI / API · typecheck, tests, build (push) Successful in 36s
Production dependency audit / API · production dependencies (push) Successful in 8s
Production dependency audit / WEB · production dependencies (push) Successful in 9s
DH V2 CI / WEB · typecheck, build (push) Successful in 1m37s
DH V2 CI / Docker / migrations / production images (push) Successful in 1m52s
DH V2 CI / Promote verified main to deploy (push) Successful in 4s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 4m11s

This commit is contained in:
2026-09-16 08:29:19 -03:00
parent 0b731b722f
commit a414d0ed36
23 changed files with 643 additions and 76 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "dhv2-web",
"version": "0.23.0-10",
"version": "0.23.0-11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dhv2-web",
"version": "0.23.0-10",
"version": "0.23.0-11",
"dependencies": {
"maplibre-gl": "6.4.1",
"react": "^19.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dhv2-web",
"version": "0.23.0-10",
"version": "0.23.0-11",
"private": true,
"type": "module",
"engines": {
+2 -2
View File
@@ -1,2 +1,2 @@
export const APP_VERSION = '0.23.0-10';
export const APP_PHASE = 'F6.13 · Mapa operativo';
export const APP_VERSION = '0.23.0-11';
export const APP_PHASE = 'F6.15 · Actas y mapa operativo';
@@ -30,9 +30,18 @@ function Photo({ id, title, caption, load }: { id: string; title: string; captio
function Finding({ item }: { item: FindingWithPhotos }) {
const { finding, photos } = item;
const hierarchy = finding.asset.hierarchy;
return <article className="act-finding-record">
<div className="inspection-finding-heading"><div><span className="eyebrow">{finding.code}</span><h3>{finding.title}</h3><p>{finding.asset.name} · {finding.asset.code}</p></div></div>
<p className="inspection-finding-description">{finding.description}</p>
<div className="inspection-finding-heading"><div><span className="eyebrow">{finding.code}</span><h3>{finding.title}</h3><p><strong>Elemento afectado:</strong> {finding.asset.typeName} · {finding.asset.name} · {finding.asset.code}</p></div></div>
{hierarchy && <div className="responsible-summary act-finding-context">
<div><small>Departamento</small><strong>{hierarchy.department?.name ?? '—'}</strong></div>
<div><small>Área</small><strong>{hierarchy.area?.name ?? '—'}</strong></div>
<div><small>Yacimiento</small><strong>{hierarchy.yacimiento?.name ?? '—'}</strong></div>
<div><small>Empresa</small><strong>{hierarchy.company?.name ?? '—'}</strong></div>
<div><small>Instalación</small><strong>{hierarchy.installation?.name ?? 'No corresponde'}</strong></div>
<div><small>Subinstalación</small><strong>{hierarchy.subinstallation?.name ?? 'No corresponde'}</strong></div>
</div>}
<p className="inspection-finding-description"><strong>Constatación:</strong> {finding.description}</p>
{finding.legalBasis && <p><strong>Normativa:</strong> {finding.legalBasis}</p>}
{finding.severity != null && <p>Gravedad {finding.severity}/10</p>}
{photos.length > 0 && <div className="act-finding-photos">
+23 -6
View File
@@ -20,7 +20,7 @@ const osmStyle = {
layers: [{ id: 'osm', type: 'raster' as const, source: 'osm' }],
};
const interactiveLayers = ['assets-points', 'assets-lines', 'assets-polygons'];
const interactiveLayers = ['assets-points', 'yacimiento-points', 'company-points', 'act-points', 'finding-points', 'assets-lines', 'assets-polygons'];
function boundsFromFeatures(collection: MapAssetFeatureCollection) {
const positions: Array<[number, number]> = [];
@@ -79,11 +79,28 @@ export function DhMap({
});
map.addLayer({
id: 'assets-points', type: 'circle', source: 'assets',
filter: ['==', ['geometry-type'], 'Point'],
paint: {
'circle-radius': 7, 'circle-color': '#2864dc',
'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2,
},
filter: ['all', ['==', ['geometry-type'], 'Point'], ['==', ['get', 'entityKind'], 'ASSET']],
paint: { 'circle-radius': 7, 'circle-color': '#64748b', 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2 },
});
map.addLayer({
id: 'yacimiento-points', type: 'circle', source: 'assets',
filter: ['all', ['==', ['geometry-type'], 'Point'], ['==', ['get', 'entityKind'], 'YACIMIENTO']],
paint: { 'circle-radius': 9, 'circle-color': '#2563eb', 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2 },
});
map.addLayer({
id: 'company-points', type: 'circle', source: 'assets',
filter: ['all', ['==', ['geometry-type'], 'Point'], ['==', ['get', 'entityKind'], 'COMPANY']],
paint: { 'circle-radius': 8, 'circle-color': '#059669', 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2 },
});
map.addLayer({
id: 'act-points', type: 'circle', source: 'assets',
filter: ['all', ['==', ['geometry-type'], 'Point'], ['==', ['get', 'entityKind'], 'ACT']],
paint: { 'circle-radius': 9, 'circle-color': '#7c3aed', 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2 },
});
map.addLayer({
id: 'finding-points', type: 'circle', source: 'assets',
filter: ['all', ['==', ['geometry-type'], 'Point'], ['==', ['get', 'entityKind'], 'FINDING']],
paint: { 'circle-radius': 8, 'circle-color': '#dc2626', 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2 },
});
map.addLayer({
id: 'assets-selected-polygons', type: 'line', source: 'assets',
+39 -8
View File
@@ -553,21 +553,34 @@ export interface AssetGeometry {
updatedBy: string | null;
}
export type MapEntityKind = 'ASSET' | 'YACIMIENTO' | 'COMPANY' | 'ACT' | 'FINDING';
export interface MapAssetProperties {
id: string;
entityId?: string;
entityKind?: MapEntityKind;
code: string;
name: string;
commonName?: string | null;
typeId: string;
typeCode: string;
typeId?: string | null;
typeCode?: string | null;
typeName: string;
parentId: string | null;
parentName: string | null;
informationStatus: AssetInformationStatus;
parentId?: string | null;
parentName?: string | null;
informationStatus?: AssetInformationStatus | null;
geometryType: AssetGeometryType;
accuracyM: number | null;
capturedAt: string | null;
accuracyM?: number | null;
capturedAt?: string | null;
updatedAt: string;
href?: string | null;
contextLine?: string | null;
departmentName?: string | null;
areaName?: string | null;
yacimientoName?: string | null;
companyName?: string | null;
actCode?: string | null;
assetName?: string | null;
sourceGeometries?: number | null;
}
export interface MapAssetFeature {
@@ -1473,7 +1486,17 @@ export interface InspectionFinding {
currentVersion: number;
closedAt: string | null;
closureNotes: string | null;
asset: InspectionAssetSummary;
asset: InspectionAssetSummary & {
typeCode?: string;
hierarchy?: {
department: { id: string; code: string; name: string } | null;
area: { id: string; code: string; name: string } | null;
yacimiento: { id: string; code: string; name: string } | null;
installation: { id: string; code: string; name: string } | null;
subinstallation: { id: string; code: string; name: string } | null;
company: { id: string; code: string; name: string } | null;
};
};
catalog: {
id: string;
code: string;
@@ -2188,6 +2211,14 @@ export function removeAssetGeometry(assetId: string) {
});
}
export function getMapOperationalContext() {
return apiRequest<MapAssetFeatureCollection>('/map/context');
}
export function getMapDocuments() {
return apiRequest<MapAssetFeatureCollection>('/map/documents');
}
export function getMapAssets(params: {
bbox?: string;
typeId?: string;
+12
View File
@@ -73,11 +73,23 @@ export interface InspectionActListItemF4 {
findingCount: number;
companies: Array<{ id: string; code: string; name: string }>;
areas: Array<{ id: string; code: string; name: string }>;
context: {
department: { id: string; code: string; name: string } | null;
area: { id: string; code: string; name: string } | null;
yacimiento: { id: string; code: string; name: string } | null;
company: { id: string; code: string; name: string } | null;
installations: Array<{ id: string; code: string; name: string }>;
subinstallations: Array<{ id: string; code: string; name: string }>;
legacyAreaScope: boolean;
};
report: null | {
id: string;
code: string;
status: InspectionReportStatusF4;
pdfStatus: InspectionReportPdfStatus;
wordStatus: 'PENDING' | 'READY' | 'FAILED';
gedoIfIdentifier: string | null;
gedoOfficializedAt: string | null;
generatedAt: string;
};
createdBy: InspectionPerson | null;
+3 -3
View File
@@ -111,13 +111,13 @@ export function ActsPage() {
{error && <Alert>{error}</Alert>}
{loading ? <LoadingBlock label="Cargando actas…" /> : items.length === 0 ? <EmptyState title="Sin actas" text="No hay actas para los filtros seleccionados." /> : <div className="table-panel document-table">
<div className="table-summary"><strong>{meta.total} acta{meta.total === 1 ? '' : 's'}</strong><span>Página {page} de {Math.max(meta.totalPages, 1)}</span></div>
<div className="table-scroll"><table><thead><tr><th>Acta</th><th>Empresa / área</th><th>Inspección</th><th>Hallazgos</th><th>Estado</th><th>Informe</th><th /></tr></thead><tbody>{items.map((act) => <tr key={act.id}>
<div className="table-scroll"><table><thead><tr><th>Acta</th><th>Empresa / territorio</th><th>Inspección</th><th>Hallazgos</th><th>Estado</th><th>Informe</th><th /></tr></thead><tbody>{items.map((act) => <tr key={act.id}>
<td><div className="document-primary"><strong>{act.code}</strong><small>{formatDate(act.occurredAt)}</small></div></td>
<td><div className="document-primary"><strong>{contextLabel(act.companies, 'Empresa sin asignar')}</strong><small>{contextLabel(act.areas, 'Área sin asignar')}</small></div></td>
<td><div className="document-primary"><strong>{act.context.company?.name ?? contextLabel(act.companies, 'Empresa sin asignar')}</strong><small>{[act.context.department?.name, act.context.area?.name, act.context.yacimiento?.name].filter(Boolean).join(' · ') || contextLabel(act.areas, 'Área sin asignar')}</small></div></td>
<td><Link className="text-link" to={`/inspecciones/${act.visitId}`}>{act.visit.code}</Link></td>
<td><Link className="text-link" to={`/hallazgos?search=${encodeURIComponent(act.code)}`}>{act.findingCount}</Link></td>
<td><span className={`status-badge ${inspectionActStatusClass(act.status)}`}>{inspectionActStatusLabel(act.status)}</span></td>
<td>{act.report ? <Link className="text-link" to={`/informes/${act.report.id}`}>{act.report.code}<small className="block-muted">{act.report.pdfStatus === 'READY' ? 'PDF disponible' : 'PDF pendiente'}</small></Link> : ['SEALED', 'CLOSED'].includes(act.status) ? <span className="status-badge pending">Pendiente de emisión</span> : <span className="muted"></span>}</td>
<td>{act.report ? <Link className="text-link" to={`/informes/${act.report.id}`}>{act.report.code}<small className="block-muted">{act.report.status === 'OFFICIALIZED' ? `Oficializado en GEDO${act.report.gedoIfIdentifier ? ` · ${act.report.gedoIfIdentifier}` : ''}` : act.report.wordStatus === 'READY' ? 'INF listo' : 'En preparación'}</small></Link> : ['SEALED', 'CLOSED'].includes(act.status) ? <span className="status-badge pending">Pendiente de emisión</span> : <span className="muted"></span>}</td>
<td className="action-cell"><Link className="icon-button" to={`/inspecciones/actas/${act.id}`} aria-label={`Abrir ${act.code}`}><Icon name="chevron" /></Link></td>
</tr>)}</tbody></table></div>
<div className="pagination"><button className="button secondary" disabled={page <= 1} onClick={() => setPage(page - 1)}>Anterior</button><span>{meta.total ? `${(page - 1) * meta.pageSize + 1}${Math.min(page * meta.pageSize, meta.total)} de ${meta.total}` : '0 resultados'}</span><button className="button secondary" disabled={page >= meta.totalPages} onClick={() => setPage(page + 1)}>Siguiente</button></div>
+14 -1
View File
@@ -80,10 +80,23 @@ export function InspectionActEditorPage() {
<div className="act-document-primary-copy"><span className="asset-symbol"><Icon name="clipboard" /></span><div><span className="eyebrow">DOCUMENTO DEL ACTA</span><h2>{isSealed ? 'Acta consolidada disponible' : 'Acta en preparación'}</h2><p>{isSealed ? 'Abrí o descargá el Acta firmada, con sus Hallazgos y constancias de integridad.' : 'El documento definitivo se genera al firmar y cerrar el Acta.'}</p></div></div>
<div className="act-primary-actions">{isSealed ? <><button className="button primary" type="button" disabled={pdfBusy} onClick={() => void openActPdf(false)}>{pdfBusy ? 'Preparando…' : 'Abrir PDF del Acta'}</button><button className="button secondary" type="button" disabled={pdfBusy} onClick={() => void openActPdf(true)}>Descargar PDF</button></> : <span className="status-badge pending">{inspectionActStatusLabel(act.status)}</span>}</div>
</section>}
{act?.report && <div className="temporal-notice"><Icon name="clipboard" /><p><strong>Informe relacionado: <Link to={`/informes/${act.report.id}`}>{act.report.code}</Link>.</strong> {act.report.pdfStatus === 'READY' ? ' Disponible.' : ' En preparación.'}</p></div>}
{act?.report && <div className="temporal-notice"><Icon name="clipboard" /><p><strong>Informe relacionado: <Link to={`/informes/${act.report.id}`}>{act.report.code}</Link>.</strong> {act.report.status === 'OFFICIALIZED' ? ` Oficializado en GEDO${act.report.gedoIfIdentifier ? ` · ${act.report.gedoIfIdentifier}` : ''}.` : act.report.status === 'FROZEN' ? ' Informe consolidado.' : act.report.wordStatus === 'READY' ? ' INF listo para enviar a GEDO.' : ' En preparación.'}</p></div>}
{isSealed && act && !act.report && <div className="temporal-notice"><Icon name="clipboard" /><p><strong>INF pendiente de emisión.</strong> El Acta ya está sellada y disponible como documento fuente.</p></div>}
{act?.status === 'CANCELLED' && <Alert>Cancelada: {act.cancellationReason}</Alert>}
{act && <section className="panel inspection-act-context">
<div className="panel-heading"><div><span className="eyebrow">CONTEXTO DEL ACTA</span><h2>Ubicación territorial y operativa</h2><p className="section-copy">El Acta corresponde a un Yacimiento y los Hallazgos se ubican dentro de sus Instalaciones y Subinstalaciones.</p></div></div>
<div className="responsible-summary">
<div><small>Departamento</small><strong>{act.context.department?.name ?? 'Sin definir'}</strong>{act.context.department && <span>{act.context.department.code}</span>}</div>
<div><small>Área</small><strong>{act.context.area?.name ?? 'Sin definir'}</strong>{act.context.area && <span>{act.context.area.code}</span>}</div>
<div><small>Yacimiento</small><strong>{act.context.yacimiento?.name ?? 'No definido en la Inspección histórica'}</strong>{act.context.yacimiento && <span>{act.context.yacimiento.code}</span>}</div>
<div><small>Empresa / Operadora</small><strong>{act.context.company?.name ?? 'Sin definir'}</strong>{act.context.company && <span>{act.context.company.code}</span>}</div>
<div><small>Instalaciones con Hallazgos</small><strong>{act.context.installations.length ? act.context.installations.map((item) => item.name).join(' · ') : 'Ninguna'}</strong></div>
<div><small>Subinstalaciones con Hallazgos</small><strong>{act.context.subinstallations.length ? act.context.subinstallations.map((item) => item.name).join(' · ') : 'Ninguna'}</strong></div>
</div>
{act.context.legacyAreaScope && !act.context.yacimiento && <Alert type="info">Esta Acta pertenece a una Inspección histórica creada antes de exigir Yacimiento como alcance. El Área se conserva como fue registrada; no se la presenta como Yacimiento.</Alert>}
</section>}
{act && <section className="panel inspection-act-form">
<div className="responsible-summary">
<div><small>Fecha de inspección</small><strong>{formatDate(act.occurredAt)}</strong></div>
+62 -40
View File
@@ -1,74 +1,96 @@
import { SearchableSelect } from '../components/SearchableSelect';
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router';
import { useAuth } from '../auth/AuthContext';
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
import { Icon } from '../components/Icon';
import { AssetCenterTabs } from '../features/assets/AssetCenterTabs';
import { DhMap } from '../features/map/DhMap';
import {
assetStatusClass,
assetStatusLabel,
ASSET_STATUSES,
} from '../features/assets/assetPresentation';
import { getMapAssets, listAssetTypes } from '../lib/api';
import type {
AssetGeometryType,
AssetInformationStatus,
AssetType,
MapAssetFeatureCollection,
} from '../lib/api';
import { assetStatusClass, assetStatusLabel, ASSET_STATUSES } from '../features/assets/assetPresentation';
import { getMapAssets, getMapDocuments, getMapOperationalContext, listAssetTypes } from '../lib/api';
import type { AssetGeometryType, AssetInformationStatus, AssetType, MapAssetFeatureCollection, MapEntityKind } from '../lib/api';
import { formatDate } from '../lib/format';
const emptyCollection: MapAssetFeatureCollection = {
type: 'FeatureCollection', features: [], meta: { count: 0, truncated: false },
const emptyCollection: MapAssetFeatureCollection = { type: 'FeatureCollection', features: [], meta: { count: 0, truncated: false } };
const layerLabels: Record<MapEntityKind, string> = {
ASSET: 'Inventario GPS', YACIMIENTO: 'Yacimientos', COMPANY: 'Empresas', ACT: 'Actas', FINDING: 'Hallazgos',
};
export function MapPage() {
const { hasPermission } = useAuth();
const canReadDocuments = hasPermission('inspection_acts.read') && hasPermission('inspection_findings.read');
const [types, setTypes] = useState<AssetType[]>([]);
const [data, setData] = useState<MapAssetFeatureCollection>(emptyCollection);
const [assets, setAssets] = useState<MapAssetFeatureCollection>(emptyCollection);
const [context, setContext] = useState<MapAssetFeatureCollection>(emptyCollection);
const [documents, setDocuments] = useState<MapAssetFeatureCollection>(emptyCollection);
const [typeId, setTypeId] = useState('');
const [status, setStatus] = useState<AssetInformationStatus | ''>('');
const [geometryType, setGeometryType] = useState<AssetGeometryType | ''>('');
const [mapSearch, setMapSearch] = useState('');
const [layers, setLayers] = useState<Record<MapEntityKind, boolean>>({ ASSET: true, YACIMIENTO: true, COMPANY: true, ACT: true, FINDING: true });
const [selectedId, setSelectedId] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
listAssetTypes().then(setTypes).catch(() => undefined);
}, []);
useEffect(() => { listAssetTypes().then(setTypes).catch(() => undefined); }, []);
useEffect(() => {
setLoading(true); setError('');
getMapAssets({ typeId, status, geometryType })
.then((result) => {
setData(result);
setSelectedId((current) => result.features.some((item) => item.id === current) ? current : null);
})
.catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false));
}, [typeId, status, geometryType]);
Promise.all([
getMapAssets({ typeId, status, geometryType }),
getMapOperationalContext(),
canReadDocuments ? getMapDocuments() : Promise.resolve(emptyCollection),
]).then(([assetResult, contextResult, documentResult]) => {
setAssets(assetResult); setContext(contextResult); setDocuments(documentResult);
}).catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false));
}, [typeId, status, geometryType, canReadDocuments]);
const selected = useMemo(
() => data.features.find((feature) => feature.id === selectedId) ?? null,
[data, selectedId],
);
const data = useMemo<MapAssetFeatureCollection>(() => {
const directAssets = assets.features.filter((feature) => !['yacimiento', 'empresa'].includes(feature.properties.typeCode?.toLowerCase() ?? ''));
const all = [...directAssets, ...context.features, ...documents.features];
const term = mapSearch.trim().toLocaleLowerCase('es-AR');
const features = all.filter((feature) => {
if (!layers[feature.properties.entityKind ?? 'ASSET']) return false;
if (!term) return true;
const searchable = [
feature.properties.code, feature.properties.name, feature.properties.typeName,
feature.properties.contextLine, feature.properties.departmentName, feature.properties.areaName,
feature.properties.yacimientoName, feature.properties.companyName, feature.properties.actCode,
feature.properties.assetName,
].filter(Boolean).join(' ').toLocaleLowerCase('es-AR');
return searchable.includes(term);
});
return { type: 'FeatureCollection', features, meta: { count: features.length, truncated: assets.meta.truncated } };
}, [assets, context, documents, layers, mapSearch]);
useEffect(() => {
setSelectedId((current) => data.features.some((item) => item.id === current) ? current : null);
}, [data]);
const selected = useMemo(() => data.features.find((feature) => feature.id === selectedId) ?? null, [data, selectedId]);
const toggleLayer = (kind: MapEntityKind) => setLayers((current) => ({ ...current, [kind]: !current[kind] }));
const kind = selected?.properties.entityKind ?? 'ASSET';
return <section>
<div className="page-heading"><div><span className="eyebrow">INVENTARIOS</span><h1>Mapa de inventarios</h1><p>Vista territorial de las ubicaciones registradas.</p></div><span className="map-count"><strong>{data.meta.count}</strong> geometría{data.meta.count === 1 ? '' : 's'}</span></div>
<div className="page-heading"><div><span className="eyebrow">TERRITORIO Y OPERACIÓN</span><h1>Mapa operativo</h1><p>Yacimientos, presencia de Empresas, Actas, Hallazgos e Inventario con ubicación real o derivada de geometrías registradas.</p></div><span className="map-count"><strong>{data.meta.count}</strong> elemento{data.meta.count === 1 ? '' : 's'}</span></div>
<AssetCenterTabs active="map" />
{error && <Alert>{error}</Alert>}
{data.meta.truncated && <Alert type="info">Se muestran los primeros 5000 registros. Aplicá filtros para reducir el resultado.</Alert>}
{data.meta.truncated && <Alert type="info">Se muestran los primeros 5000 registros de Inventario. Aplicá filtros para reducir el resultado.</Alert>}
<div className="map-layout operational-map-layout">
<aside className="filters map-sidebar">
<div><span className="eyebrow">FILTROS</span><h2>Vista territorial</h2></div>
<label className="field"><span>Tipo de elemento</span><SearchableSelect value={typeId} onChange={(event) => setTypeId(event.target.value)}><option value="">Todos</option>{types.map((type) => <option key={type.id} value={type.id}>{type.name}</option>)}</SearchableSelect></label>
<label className="field"><span>Estado de información</span><SearchableSelect value={status} onChange={(event) => setStatus(event.target.value as AssetInformationStatus | '')}><option value="">Todos</option>{ASSET_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect></label>
<label className="field"><span>Geometría</span><SearchableSelect value={geometryType} onChange={(event) => setGeometryType(event.target.value as AssetGeometryType | '')}><option value="">Todas</option><option value="POINT">Puntos</option><option value="LINESTRING">Líneas</option><option value="POLYGON">Polígonos</option></SearchableSelect></label>
<button className="button secondary wide" onClick={() => { setTypeId(''); setStatus(''); setGeometryType(''); }}>Limpiar filtros</button>
<div><span className="eyebrow">CAPAS</span><h2>Qué mostrar</h2></div>
<label className="search-field map-global-search"><Icon name="search" /><input value={mapSearch} onChange={(event) => setMapSearch(event.target.value)} placeholder="Buscar Yacimiento, Empresa, Acta, Hallazgo…" /></label>
<div className="map-layer-buttons">
{(Object.keys(layerLabels) as MapEntityKind[]).map((item) => item === 'ACT' || item === 'FINDING' ? (canReadDocuments && <button key={item} type="button" className={`button compact ${layers[item] ? 'primary' : 'secondary'}`} onClick={() => toggleLayer(item)}>{layerLabels[item]}</button>) : <button key={item} type="button" className={`button compact ${layers[item] ? 'primary' : 'secondary'}`} onClick={() => toggleLayer(item)}>{layerLabels[item]}</button>)}
</div>
<div><span className="eyebrow">FILTROS DE INVENTARIO</span></div>
<label className="field"><span>Tipo de elemento</span><SearchableSelect value={typeId} onChange={(event) => setTypeId(event.target.value)} disabled={!layers.ASSET}><option value="">Todos</option>{types.map((type) => <option key={type.id} value={type.id}>{type.name}</option>)}</SearchableSelect></label>
<label className="field"><span>Estado de información</span><SearchableSelect value={status} onChange={(event) => setStatus(event.target.value as AssetInformationStatus | '')} disabled={!layers.ASSET}><option value="">Todos</option>{ASSET_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect></label>
<label className="field"><span>Geometría</span><SearchableSelect value={geometryType} onChange={(event) => setGeometryType(event.target.value as AssetGeometryType | '')} disabled={!layers.ASSET}><option value="">Todas</option><option value="POINT">Puntos</option><option value="LINESTRING">Líneas</option><option value="POLYGON">Polígonos</option></SearchableSelect></label>
<button className="button secondary wide" onClick={() => { setTypeId(''); setStatus(''); setGeometryType(''); }}>Limpiar filtros de Inventario</button>
{selected && <div className="map-selection"><span className="eyebrow">REGISTRO SELECCIONADO</span><h3>{selected.properties.name}</h3><code>{selected.properties.code}</code><div className="map-selection-meta"><span className="tag">{selected.properties.typeName}</span><span className={`status-badge ${assetStatusClass(selected.properties.informationStatus)}`}>{assetStatusLabel(selected.properties.informationStatus)}</span></div>{selected.properties.parentName && <p>Depende de <strong>{selected.properties.parentName}</strong></p>}<p>{selected.properties.geometryType === 'POINT' ? 'Punto' : selected.properties.geometryType === 'LINESTRING' ? 'Línea' : 'Polígono'} · actualizado {formatDate(selected.properties.updatedAt)}</p>{selected.properties.accuracyM != null && <p>Precisión informada: {selected.properties.accuracyM} m</p>}<Link className="button primary wide" to={`/inventarios/${selected.id}`}>Abrir registro <Icon name="chevron" /></Link></div>}
{selected && <div className="map-selection"><span className="eyebrow">{layerLabels[kind]}</span><h3>{selected.properties.name}</h3><code>{selected.properties.code}</code><div className="map-selection-meta"><span className="tag">{selected.properties.typeName}</span>{kind === 'ASSET' && selected.properties.informationStatus && <span className={`status-badge ${assetStatusClass(selected.properties.informationStatus)}`}>{assetStatusLabel(selected.properties.informationStatus)}</span>}</div>{selected.properties.contextLine && <p>{selected.properties.contextLine}</p>}{selected.properties.departmentName && <p><strong>Departamento:</strong> {selected.properties.departmentName}</p>}{selected.properties.areaName && <p><strong>Área:</strong> {selected.properties.areaName}</p>}{selected.properties.yacimientoName && <p><strong>Yacimiento:</strong> {selected.properties.yacimientoName}</p>}{selected.properties.companyName && <p><strong>Empresa:</strong> {selected.properties.companyName}</p>}{selected.properties.assetName && <p><strong>Elemento:</strong> {selected.properties.assetName}</p>}<p>Ubicación actualizada {formatDate(selected.properties.updatedAt)}</p>{selected.properties.sourceGeometries != null && <p><small>Ubicación territorial derivada de {selected.properties.sourceGeometries} geometría{selected.properties.sourceGeometries === 1 ? '' : 's'} registrada{selected.properties.sourceGeometries === 1 ? '' : 's'} en el Yacimiento.</small></p>}{selected.properties.href && <Link className="button primary wide" to={selected.properties.href}>Abrir {layerLabels[kind].replace(/s$/, '')} <Icon name="chevron" /></Link>}</div>}
</aside>
<div className="map-stage">{loading && <div className="map-loading"><LoadingBlock label="Actualizando mapa…" /></div>}<DhMap data={data} selectedId={selectedId} onSelect={setSelectedId} />{!loading && data.features.length === 0 && <div className="map-empty"><Icon name="map" size={30} /><strong>No hay geometrías para mostrar</strong><span>Agregá una ubicación desde el detalle de un registro.</span></div>}</div>
<div className="map-stage">{loading && <div className="map-loading"><LoadingBlock label="Actualizando mapa…" /></div>}<DhMap data={data} selectedId={selectedId} onSelect={setSelectedId} />{!loading && data.features.length === 0 && <div className="map-empty"><Icon name="map" size={30} /><strong>No hay ubicaciones para mostrar</strong><span>Las capas sólo muestran registros con una geometría real propia o derivable de su Yacimiento.</span></div>}</div>
</div>
</section>;
}
+6
View File
@@ -1436,3 +1436,9 @@ code { color: #5e6677; font-family: ui-monospace, monospace; font-size: 9px; }
.act-other-asset-photos { border-top: 1px solid #e3eaf5; margin-top: 24px; padding-top: 20px; }
.act-other-asset-photos h3 { margin: 0 0 14px; }
/* F6.15 · capas del mapa operativo */
.map-layer-buttons { display: flex; flex-wrap: wrap; gap: 6px; }
.map-layer-buttons .button { flex: 1 1 calc(50% - 6px); justify-content: center; min-width: 104px; }
.map-global-search { width: 100%; margin: 0; }
.act-finding-context { margin: 10px 0 12px; }