Compare commits

...
9 changed files with 448 additions and 113 deletions
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import test from 'node:test';
function source(path: string): string {
return readFileSync(resolve(process.cwd(), path), 'utf8');
}
test('F6.2 planning uses the shared CSRF-aware API requester', () => {
const page = source('../web-v2/src/pages/InspectionVisitCreateF61Page.tsx');
const api = source('../web-v2/src/lib/api.ts');
assert.match(page, /import \{ apiRequest \} from '\.\.\/lib\/api';/);
assert.doesNotMatch(page, /async function requestJson/);
assert.doesNotMatch(page, /fetch\(`\/api\/v3/);
assert.match(page, /apiRequest<CreatedInspection>\('\/inspection-visits'/);
assert.match(api, /headers\.set\('x-csrf-token', token\)/);
assert.match(api, /readCsrfCookie\(\)/);
});
test('F6.2 removes the duplicated global operational bar and hidden persisted filters', () => {
const layout = source('../web-v2/src/layout/AppLayout.tsx');
const context = source('../web-v2/src/context/OperationalContext.tsx');
assert.doesNotMatch(layout, /OperationalContextBar/);
assert.match(context, /localStorage\.removeItem\(LEGACY_STORAGE_KEY\)/);
assert.doesNotMatch(context, /localStorage\.setItem/);
assert.match(context, /useState\(''\)/);
});
test('F6.2 exposes provisional PDF data contracts for Actas and Informes', () => {
const projection = source('../web-v2/src/components/DocumentPdfProjection.tsx');
const actPage = source('../web-v2/src/pages/InspectionActEditorPage.tsx');
const reportPage = source('../web-v2/src/pages/ReportDetailPage.tsx');
assert.match(projection, /PDF PRELIMINAR · ESTRUCTURA PROYECTADA/);
assert.match(projection, /Contrato de datos estable, renderer reemplazable/);
assert.match(projection, /Identificación y contexto/);
assert.match(projection, /Firmas y manifestaciones/);
assert.match(projection, /GEDO e integridad/);
assert.match(projection, /Anexo final/);
assert.match(actPage, /<DocumentPdfProjection kind="act" \/>/);
assert.match(reportPage, /<DocumentPdfProjection kind="report" \/>/);
});
+90 -62
View File
@@ -1,96 +1,124 @@
# Modelo documental de inspecciones # Modelo documental de inspecciones
Este documento fija el modelo funcional validado a partir del Informe Técnico Este documento fija el modelo funcional vigente para Inspecciones, Actas,
Nº 124/2026, el acta manuscrita adjunta y la planilla de hallazgos entregada. Hallazgos e Informes. La estructura visual definitiva de los documentos todavía
puede cambiar cuando Hidrocarburos entregue las plantillas oficiales; el contrato
de datos, la trazabilidad y las reglas de inmutabilidad deben permanecer estables.
## Canales de operación ## Canales de operación
| Función | APK móvil | Dashboard web | | Función | APK móvil | Dashboard web |
|---|---:|---:| |---|---:|---:|
| Planificar visita, activos y equipo | Consulta | Administra | | Planificar Inspección, alcance y equipo | Consulta | Administra |
| Administrar activos y catálogo de hallazgos | Consulta | Administra | | Administrar Inventarios y catálogo de Hallazgos | Consulta | Administra |
| Iniciar y ejecutar la visita | Inspector | No | | Iniciar y ejecutar la Inspección | Inspector | No |
| Crear o modificar el acta y sus hallazgos | Inspector | Sólo consulta | | Crear Actas y registrar Hallazgos de campo | Inspector | Sólo consulta |
| Incorporar evidencia de campo y GPS | Inspector | Sólo consulta | | Incorporar evidencia de campo y GPS | Inspector | Sólo consulta |
| Identificar responsable, firmar y cerrar | Inspector | Sólo consulta | | Identificar responsable, firmar y sellar Acta | Inspector | Sólo consulta |
| Registrar respuesta empresarial, PDF y próximo control | Consulta | Administra | | Preparar y revisar el INF | Consulta | Administra |
| Consultar acta, hallazgos e informe sincronizados | Sí | Sí | | Registrar oficialización GEDO/IF y PDF oficial | Consulta | Administra |
| Registrar respuesta empresarial y seguimientos | Consulta | Administra |
| Consultar Actas, Hallazgos e Informes sincronizados | Sí | Sí |
La API aplica esta separación además de la interfaz: las escrituras operativas La API aplica esta separación además de la interfaz. Las escrituras operativas de
requieren autenticación bearer de la aplicación móvil y el rol `inspector`. campo se realizan desde la aplicación móvil con las autorizaciones correspondientes;
Una sesión web no puede eludir la restricción llamando directamente al endpoint. el dashboard administra planificación, consulta y circuito documental posterior.
## Cardinalidades ## Cardinalidades vigentes
| Origen | Relación | Destino | | Origen | Relación | Destino |
|---|---:|---| |---|---:|---|
| Inspección o visita | 1 a 1 | Acta de inspección | | Inspección o visita | 1 a muchos | Actas de inspección |
| Acta | 1 a muchos | Hallazgos | | Acta | 1 a muchos | Hallazgos |
| Hallazgo | muchos a 1 | Activo o equipo del Maestro | | Hallazgo | muchos a 1 | Elemento de Inventario |
| Hallazgo | muchos a 1 | Ítem del catálogo configurable | | Hallazgo | muchos a 1 | Ítem del catálogo configurable, o propuesta OTROS |
| Hallazgo | 1 a muchos | Evidencias, principalmente fotografías | | Hallazgo | 1 a muchos | Evidencias y seguimientos |
| Acta | 1 a muchos | Firmas de inspectores | | Acta | 1 a muchos | Firmas de inspectores |
| Acta | 1 a 1 | Firma del responsable que atendió la visita | | Acta | 0 o 1 | Firma/manifestación del responsable de empresa |
| Inspección o visita | 1 a 1 | Informe técnico | | Acta sellada | 0 o 1 | Informe técnico (INF) |
| Informe técnico | incorpora | Acta, hallazgos y evidencias congelados | | Informe técnico | 1 a 1 | Acta fuente |
| Informe técnico | incorpora | Acta, Hallazgos, Inventarios y evidencias congelados |
Una Inspección puede, por lo tanto, contener varias Actas y generar varios INF,
pero cada INF conserva una única Acta fuente. No se mezcla el contenido de dos
Actas dentro de un mismo Informe.
## Acta digital ## Acta digital
El acta debe reunir como mínimo: El Acta debe poder reunir como mínimo:
- número anual oficial, fecha, hora y ubicación; - número y año oficial del Acta;
- empresa, área, yacimiento o instalación inspeccionada; - Inspección vinculada, fecha y hora;
- objeto y resultado de la inspección; - Departamento, Área, Yacimiento, Operadora y ubicación de la actuación;
- todos los hallazgos de la visita y sus plazos de corrección; - objeto, alcance y descripción de lo actuado;
- Instalaciones y Subinstalaciones inspeccionadas;
- todos los Hallazgos del Acta, con descripción, Inventario afectado, base legal,
reincidencia, gravedad y plazos que correspondan;
- inspectores intervinientes; - inspectores intervinientes;
- identidad, DNI, cargo y correo del responsable de la empresa que recibió al - identidad, DNI, cargo y correo del responsable de la empresa que recibió al
equipo; equipo, cuando corresponda;
- firmas manuscritas capturadas en pantalla de inspectores y responsable; - firma de inspectores y manifestación/firma del responsable de empresa, incluida
- versión inmutable, hash, sello de tiempo y token de verificación. conformidad, disidencia, negativa o ausencia según el flujo aplicable;
- versión inmutable, hash, sello de tiempo y mecanismo de verificación documental;
- referencias a fotografías/evidencias cuando el formulario oficial determine que
deben formar parte del documento.
Al finalizar la visita, el acta se cierra y ya no admite edición. Se genera el Al sellarse, el Acta queda inmutable. Los cambios posteriores pertenecen al
PDF verificable y se envía al correo del responsable de la empresa y a una seguimiento y no reescriben el contenido constatado. El sistema conserva la
dirección interna de Hidrocarburos definida por configuración. Cada intento y instantánea y hashes necesarios para que el PDF se pueda regenerar o sustituir su
resultado de entrega debe quedar auditado. renderer sin alterar la evidencia fuente.
Si se requiere corregir un acta cerrada, se emitirá una rectificativa vinculada
sin reemplazar ni borrar el documento original.
## Hallazgos ## Hallazgos
La planilla entregada contiene 170 modelos de hallazgo agrupados en 19 El catálogo de Hallazgos es administrable, importable y versionado. Cada Hallazgo
categorías de equipos, con descripción, normativa y glosario. Se implementará operativo conserva una instantánea de su descripción y base legal para que cambios
como catálogo administrable, importable y versionado. No se copiarán esas posteriores del catálogo no modifiquen documentos ya emitidos.
opciones como constantes en la interfaz.
Cada hallazgo operativo conservará una instantánea de su descripción y base Cada Hallazgo nace abierto. Se distinguen el plazo exigido, la respuesta y fecha
legal para que posteriores cambios del catálogo no alteren documentos ya comprometida por la empresa, la fecha de próximo control y las verificaciones
emitidos. También podrá registrar estado, observación específica, activo, posteriores. La respuesta empresarial no cierra por sí sola un Hallazgo: el cierre
ubicación/GPS, plazo y evidencias. surge del flujo de verificación autorizado.
Cada hallazgo nace abierto. Se distinguen el plazo exigido, la respuesta y ## Informe técnico (INF)
fecha comprometida por la empresa, y la fecha de próximo control fijada por el
organismo. La respuesta empresarial no cierra el hallazgo: sólo una verificación
posterior autorizada puede hacerlo.
## Informe técnico El INF nace desde una Acta sellada y tiene numeración propia. Mientras está en
preparación dispone de un Word editable para el desarrollo técnico; modificar ese
Word o los campos narrativos no modifica el Acta ni sus Hallazgos fuente.
El informe se genera después del cierre del acta, con numeración anual global El contrato proyectado del Informe contempla:
independiente. Siguiendo el modelo entregado, contempla:
- título, número, acta referenciada, empresa, área/yacimiento y autores; - número/año del INF y Acta referenciada;
- objetivos; - Inspección, Empresa, Departamento, Área, Yacimiento y autores;
- antecedentes; - objetivos y antecedentes;
- marco legal; - marco legal;
- resumen ejecutivo; - resumen ejecutivo;
- descripción de la inspección y de todos los hallazgos; - descripción y análisis técnico de la inspección;
- registro fotográfico; - Inventarios inspeccionados;
- conclusión; - detalle de los Hallazgos, reincidencias, gravedad y plazos;
- acta digital completa como anexo final. - registro fotográfico/evidencias seleccionadas;
- conclusiones y recomendaciones;
- trazabilidad e integridad de la fuente;
- Acta digital completa como anexo final.
El informe congela las versiones exactas del acta, hallazgos, activos, Cuando GEDO devuelve el identificador IF y su PDF oficial, ambos se registran de
evidencias y normativa que incorpora. forma inmutable junto con la fecha de oficialización y el hash del archivo. La
carga del IF/PDF oficial no se interpreta por sí sola como notificación a la
empresa ni inicia plazos que dependan de una notificación posterior.
La solicitud de creación del informe corresponde al flujo del inspector en la ## Plantillas PDF provisionales
APK después de cerrar la visita. El backend numera, compone y sella el PDF; el
dashboard sólo lo consulta, descarga y administra su seguimiento documental. Hasta recibir los formularios oficiales, la implementación se divide en dos capas:
1. **Contrato de datos estable.** Define qué información debe estar disponible para
Actas e Informes, de dónde proviene, qué versión se congela y qué hashes prueban
su integridad.
2. **Renderer reemplazable.** Encabezado, logos, tipografías, márgenes, cuadros,
orden visual, QR y anexos podrán adaptarse al modelo institucional definitivo
sin modificar el dominio ni volver a diseñar el circuito documental.
El dashboard muestra una sección **PDF preliminar · estructura proyectada** tanto
en la ficha del Acta como en la del Informe. Esa sección no pretende reemplazar el
formulario oficial: sirve para comprobar ahora que el sistema está capturando toda
la información que después deberá volcarse al documento.
La especificación detallada se mantiene en `docs/PDF_PROVISIONAL_STRUCTURE.md`.
+184
View File
@@ -0,0 +1,184 @@
# Estructura provisional de PDFs · PREVIEW-1
Estado: **provisional hasta recepción de formularios oficiales**.
El objetivo de este documento no es fijar todavía la estética institucional. Fija
el conjunto de datos que DH Inspección debe ser capaz de colocar en los PDFs de
Actas e Informes y separa ese contrato del renderer visual.
## Principio de implementación
El dominio documental no debe depender de coordenadas, cuadros o textos impresos
de una plantilla determinada. Los datos se congelan primero en snapshots
versionados; luego un renderer transforma esa fuente a PDF. Cuando llegue el
formulario oficial se reemplaza o adapta el renderer, no la lógica de negocio.
Se consideran **estables**: identidad del documento, vínculos entre entidades,
contenido constatado, Hallazgos, firmas/manifestaciones, fechas, versiones, hashes
y trazabilidad. Se consideran **reemplazables**: logo, membrete, encabezado,
footer, tipografía, colores, tamaños, posiciones, cantidad de columnas, QR y forma
de los anexos.
## PDF de Acta
### 1. Cabecera institucional
Pendiente de modelo oficial. Debe reservar espacio para organismo, dependencia,
logo/membrete, denominación del documento y numeración.
### 2. Identificación
- número y año del Acta;
- código interno;
- código de Inspección/visita;
- fecha y hora de actuación;
- estado documental y versión.
### 3. Contexto territorial y operativo
- Departamento;
- Área;
- Yacimiento;
- Empresa/Operadora vigente;
- ubicación/GPS cuando corresponda;
- Instalación y Subinstalación relacionadas cuando el Acta tenga ese alcance.
### 4. Personas intervinientes
- Inspectores actuantes;
- responsable de la empresa;
- DNI/identificación;
- cargo o función;
- correo/dato de contacto que exija el formulario.
### 5. Objeto y descripción de lo actuado
- objeto o motivo;
- resumen/constatación;
- observaciones;
- Inventarios inspeccionados con código, nombre, tipo y ruta jerárquica.
### 6. Hallazgos
Por cada Hallazgo:
- código;
- Inventario asociado;
- ítem de catálogo u origen OTROS;
- título/denominación;
- descripción constatada;
- base legal congelada;
- reincidencia y referencia al antecedente, si existe;
- gravedad cuando aplique;
- plazo y tipo de días cuando corresponda;
- referencias a evidencia.
### 7. Firmas y manifestaciones
- firma de cada Inspector interviniente;
- firma/manifestación del responsable de empresa;
- conformidad o disidencia;
- texto de disidencia;
- negativa o ausencia con motivo cuando aplique;
- fecha/hora de cada constancia.
### 8. Integridad
- hash del contenido bloqueado;
- hash final del Acta sellada;
- sello de tiempo;
- versión del esquema;
- espacio para token/QR de verificación si el modelo institucional lo utiliza.
### 9. Anexo de evidencias
El sistema debe estar preparado para incorporar fotografías/evidencias, pero su
inclusión dentro del PDF principal queda pendiente de la plantilla oficial.
## Informe técnico · INF
Cada INF tiene **una sola Acta fuente**. Una Inspección puede contener múltiples
Actas y, por lo tanto, múltiples INF independientes.
### 1. Cabecera e identificación
- número/año y código del INF;
- Acta fuente;
- Inspección;
- fecha de generación;
- autores/Inspector responsable;
- Empresa/Operadora;
- Departamento, Área y Yacimiento.
### 2. Objetivos
Texto editable durante la preparación, si la plantilla oficial lo requiere como
campo separado.
### 3. Antecedentes
Antecedentes relevantes para comprender la inspección y el contexto técnico.
### 4. Marco legal
Normativa aplicable y bases legales congeladas con los Hallazgos del Acta fuente.
### 5. Resumen ejecutivo
Campo narrativo editable en estado de preparación.
### 6. Descripción y análisis técnico
Desarrollo técnico editable que no modifica el contenido del Acta ni de sus
Hallazgos.
### 7. Inventarios inspeccionados
Tabla o listado con código, nombre, tipo y ruta territorial/técnica.
### 8. Hallazgos
Detalle de los Hallazgos del Acta fuente, con descripción, base legal,
reincidencia, gravedad, plazos y vínculo con el Inventario.
### 9. Registro fotográfico
Fotografías/evidencias seleccionadas, identificando Hallazgo/Inventario, fecha y
metadatos disponibles. La maquetación se definirá con el modelo oficial.
### 10. Conclusiones y recomendaciones
Bloque técnico editable durante la preparación del INF.
### 11. GEDO
Al oficializar:
- identificador IF;
- fecha/hora de oficialización;
- nombre del PDF oficial;
- SHA-256 del PDF oficial.
La oficialización no equivale automáticamente a notificación a la empresa.
### 12. Integridad y anexo
- hash del snapshot fuente del INF;
- hash del cierre del Acta;
- versión de Acta utilizada;
- Acta fuente completa como anexo final.
## Reemplazo por la plantilla oficial
Cuando se reciba el Acta o Informe oficial en PDF/Word se deberá:
1. inventariar cada campo del formulario;
2. mapearlo contra este contrato;
3. identificar únicamente los datos faltantes;
4. añadir esos datos al dominio sólo si son información real y no mero diseño;
5. construir el renderer definitivo;
6. comparar visualmente una muestra contra el original;
7. conservar el renderer PREVIEW-1 sólo para pruebas históricas si resultara útil.
No se deben alterar Actas ya selladas para acomodar una nueva plantilla. Los
nuevos renderers deben poder representar snapshots históricos de forma trazable.
@@ -0,0 +1,109 @@
import { Icon } from './Icon';
type ProjectionKind = 'act' | 'report';
interface ProjectionSection {
title: string;
description: string;
}
const actSections: ProjectionSection[] = [
{
title: 'Identificación y contexto',
description: 'Número y año de Acta, Inspección vinculada, fecha y hora, Departamento, Área, Yacimiento, Operadora y ubicación de la actuación.',
},
{
title: 'Intervinientes',
description: 'Inspectores actuantes y responsable de la empresa: nombre, DNI, cargo, correo y demás datos de identificación que exija el modelo oficial.',
},
{
title: 'Alcance y constatación',
description: 'Objeto de la actuación, descripción de lo actuado, observaciones e Inventarios inspeccionados con su ruta Instalación / Subinstalación.',
},
{
title: 'Hallazgos',
description: 'Código, Inventario afectado, descripción, base legal, reincidencia, gravedad y plazos aplicables, conservando la instantánea emitida en campo.',
},
{
title: 'Firmas y manifestaciones',
description: 'Firmas de inspectores y responsable de empresa, conformidad o disidencia, manifestación textual, negativa/ausencia si correspondiera y marcas de tiempo.',
},
{
title: 'Integridad y verificación',
description: 'Versión sellada, hash SHA-256, sello de tiempo y espacio reservado para token o QR de verificación documental.',
},
{
title: 'Anexos',
description: 'Registro fotográfico y evidencias sólo si el formulario oficial exige incorporarlos al Acta; el modelo queda preparado para hacerlo sin alterar el dato fuente.',
},
];
const reportSections: ProjectionSection[] = [
{
title: 'Identificación y contexto',
description: 'Número y año del INF, Acta fuente, Inspección, Empresa, Departamento, Área, Yacimiento, fechas y autores/intervinientes.',
},
{
title: 'Objetivos y antecedentes',
description: 'Motivo de la inspección, antecedentes relevantes y alcance técnico del Informe.',
},
{
title: 'Marco legal',
description: 'Normativa y bases legales aplicables, incluyendo las referencias congeladas de los Hallazgos del Acta fuente.',
},
{
title: 'Resumen ejecutivo y desarrollo técnico',
description: 'Resumen ejecutivo, descripción de la inspección, análisis técnico y consideraciones editables durante la preparación del INF.',
},
{
title: 'Inventarios y Hallazgos',
description: 'Inventarios inspeccionados y detalle de Hallazgos con descripción, reincidencia, gravedad, plazos y trazabilidad con el Acta.',
},
{
title: 'Registro fotográfico',
description: 'Fotografías y evidencias seleccionadas, con referencia al Inventario/Hallazgo, fecha y metadatos disponibles.',
},
{
title: 'Conclusiones',
description: 'Conclusión técnica, recomendaciones y texto final sujeto a revisión antes de la oficialización.',
},
{
title: 'GEDO e integridad',
description: 'Identificador IF, fecha de oficialización, hash del PDF oficial y hash de la instantánea congelada que originó el Informe.',
},
{
title: 'Anexo final',
description: 'Acta fuente completa e inmutable como anexo del Informe, manteniendo la relación exacta INF → Acta.',
},
];
export function DocumentPdfProjection({ kind }: { kind: ProjectionKind }) {
const isAct = kind === 'act';
const sections = isAct ? actSections : reportSections;
return <section className="panel document-pdf-projection">
<div className="panel-heading">
<div>
<span className="eyebrow">PDF PRELIMINAR · ESTRUCTURA PROYECTADA</span>
<h2>{isAct ? 'Contenido previsto del Acta' : 'Contenido previsto del Informe'}</h2>
<p className="section-copy">El diseño institucional definitivo todavía no fue recibido. Esta vista fija qué información debe poder alimentar el documento, sin comprometer encabezados, tipografías, logos ni distribución final.</p>
</div>
<span className="status-badge pending">Plantilla oficial pendiente</span>
</div>
<div className="temporal-notice">
<Icon name="clipboard" />
<p><strong>Contrato de datos estable, renderer reemplazable.</strong> Cuando llegue el PDF/Word oficial se adapta la composición visual sobre estos mismos datos, sin rehacer el circuito de Inspecciones, Actas, Hallazgos ni trazabilidad.</p>
</div>
<div className="dossier-link-list">
{sections.map((section, index) => <div key={section.title}>
<div>
<strong>{index + 1}. {section.title}</strong>
<small>{section.description}</small>
</div>
<span>Previsto</span>
</div>)}
</div>
</section>;
}
+7 -20
View File
@@ -10,31 +10,18 @@ interface OperationalContextValue {
clearContext: () => void; clearContext: () => void;
} }
const STORAGE_KEY = 'dhv2.operational-context'; const LEGACY_STORAGE_KEY = 'dhv2.operational-context';
const OperationalContext = createContext<OperationalContextValue | null>(null); const OperationalContext = createContext<OperationalContextValue | null>(null);
function readStoredContext(): { areaId: string; companyId: string } {
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return { areaId: '', companyId: '' };
const parsed = JSON.parse(raw) as { areaId?: unknown; companyId?: unknown };
return {
areaId: typeof parsed.areaId === 'string' ? parsed.areaId : '',
companyId: typeof parsed.companyId === 'string' ? parsed.companyId : '',
};
} catch {
return { areaId: '', companyId: '' };
}
}
export function OperationalContextProvider({ children }: PropsWithChildren) { export function OperationalContextProvider({ children }: PropsWithChildren) {
const initial = useMemo(readStoredContext, []); const [areaId, setAreaState] = useState('');
const [areaId, setAreaState] = useState(initial.areaId); const [companyId, setCompanyState] = useState('');
const [companyId, setCompanyState] = useState(initial.companyId);
useEffect(() => { useEffect(() => {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ areaId, companyId })); // F6.2 retiró la barra global Área/Operadora. Un contexto persistido sin
}, [areaId, companyId]); // controles visibles podía seguir filtrando pantallas de forma inesperada.
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
}, []);
const value = useMemo<OperationalContextValue>(() => ({ const value = useMemo<OperationalContextValue>(() => ({
areaId, areaId,
-2
View File
@@ -3,7 +3,6 @@ import { NavLink, Outlet, useNavigate } from 'react-router';
import { useAuth } from '../auth/AuthContext'; import { useAuth } from '../auth/AuthContext';
import { APP_PHASE, APP_VERSION } from '../config/version'; import { APP_PHASE, APP_VERSION } from '../config/version';
import { Icon } from '../components/Icon'; import { Icon } from '../components/Icon';
import { OperationalContextBar } from '../components/OperationalContextBar';
import type { IconName } from '../components/Icon'; import type { IconName } from '../components/Icon';
interface NavItem { interface NavItem {
@@ -118,7 +117,6 @@ export function AppLayout() {
<strong>DH Inspección</strong> <strong>DH Inspección</strong>
<span className="mobile-version">v{APP_VERSION}</span> <span className="mobile-version">v{APP_VERSION}</span>
</header> </header>
<OperationalContextBar />
<main className="content"><Outlet /></main> <main className="content"><Outlet /></main>
<footer className="app-footer" aria-label="Versión de la aplicación"> <footer className="app-footer" aria-label="Versión de la aplicación">
<span>DH Inspección V2</span><span>·</span><strong>v{APP_VERSION}</strong><span>·</span><span>{APP_PHASE}</span> <span>DH Inspección V2</span><span>·</span><strong>v{APP_VERSION}</strong><span>·</span><span>{APP_PHASE}</span>
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router'; import { Link, useParams } from 'react-router';
import { DocumentPdfProjection } from '../components/DocumentPdfProjection';
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 { import {
@@ -70,6 +71,7 @@ export function InspectionActEditorPage() {
<div className="inspection-act-asset-grid">{act.assets.map((asset) => <div className="inspection-member selected" key={asset.id}><span><strong>{asset.name}</strong><small>{asset.code} · {asset.typeName}</small></span></div>)}</div> <div className="inspection-act-asset-grid">{act.assets.map((asset) => <div className="inspection-member selected" key={asset.id}><span><strong>{asset.name}</strong><small>{asset.code} · {asset.typeName}</small></span></div>)}</div>
</section>} </section>}
{act && <DocumentPdfProjection kind="act" />}
{act && <InspectionFindingsPanel act={act} />} {act && <InspectionFindingsPanel act={act} />}
{act && <InspectionClosurePanel act={act} />} {act && <InspectionClosurePanel act={act} />}
@@ -4,6 +4,7 @@ import { Link, useNavigate } from 'react-router';
import { Alert, LoadingBlock } from '../components/Feedback'; import { Alert, LoadingBlock } from '../components/Feedback';
import { Icon } from '../components/Icon'; import { Icon } from '../components/Icon';
import { SearchableSelect } from '../components/SearchableSelect'; import { SearchableSelect } from '../components/SearchableSelect';
import { apiRequest } from '../lib/api';
interface Option { interface Option {
id: string; id: string;
@@ -23,28 +24,6 @@ interface CreatedInspection {
code: string; code: string;
} }
async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(`/api/v3${url}`, {
credentials: 'same-origin',
...init,
headers: {
Accept: 'application/json',
...(init?.body ? { 'Content-Type': 'application/json' } : {}),
...(init?.headers ?? {}),
},
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
const message = typeof payload?.message === 'string'
? payload.message
: typeof payload?.error?.message === 'string'
? payload.error.message
: `No se pudo completar la operación (${response.status}).`;
throw new Error(message);
}
return payload as T;
}
function inspectorName(person: Inspector): string { function inspectorName(person: Inspector): string {
return `${person.firstName} ${person.lastName}`.trim() || person.username; return `${person.firstName} ${person.lastName}`.trim() || person.username;
} }
@@ -76,8 +55,8 @@ export function InspectionVisitCreateF61Page() {
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
Promise.all([ Promise.all([
requestJson<{ data: Option[] }>('/inspection-visits/planning-context/departments'), apiRequest<{ data: Option[] }>('/inspection-visits/planning-context/departments'),
requestJson<{ data: Inspector[] }>('/inspection-visits/assignees'), apiRequest<{ data: Inspector[] }>('/inspection-visits/assignees'),
]).then(([departmentResponse, inspectorResponse]) => { ]).then(([departmentResponse, inspectorResponse]) => {
setDepartments(departmentResponse.data); setDepartments(departmentResponse.data);
setInspectors(inspectorResponse.data); setInspectors(inspectorResponse.data);
@@ -94,7 +73,7 @@ export function InspectionVisitCreateF61Page() {
setYacimientos([]); setYacimientos([]);
setOperators([]); setOperators([]);
if (!departmentId) return; if (!departmentId) return;
requestJson<{ data: Option[] }>(`/inspection-visits/planning-context/departments/${departmentId}/areas`) apiRequest<{ data: Option[] }>(`/inspection-visits/planning-context/departments/${departmentId}/areas`)
.then((response) => setAreas(response.data)) .then((response) => setAreas(response.data))
.catch((cause) => setError(cause instanceof Error ? cause.message : String(cause))); .catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)));
}, [departmentId]); }, [departmentId]);
@@ -105,7 +84,7 @@ export function InspectionVisitCreateF61Page() {
setYacimientos([]); setYacimientos([]);
setOperators([]); setOperators([]);
if (!areaId) return; if (!areaId) return;
requestJson<{ data: Option[] }>(`/inspection-visits/planning-context/areas/${areaId}/yacimientos`) apiRequest<{ data: Option[] }>(`/inspection-visits/planning-context/areas/${areaId}/yacimientos`)
.then((response) => setYacimientos(response.data)) .then((response) => setYacimientos(response.data))
.catch((cause) => setError(cause instanceof Error ? cause.message : String(cause))); .catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)));
}, [areaId]); }, [areaId]);
@@ -118,7 +97,7 @@ export function InspectionVisitCreateF61Page() {
if (Number.isNaN(parsedStart.getTime())) return; if (Number.isNaN(parsedStart.getTime())) return;
const at = encodeURIComponent(parsedStart.toISOString()); const at = encodeURIComponent(parsedStart.toISOString());
setLoadingOperators(true); setLoadingOperators(true);
requestJson<{ data: Option[] }>(`/inspection-visits/planning-context/areas/${areaId}/operators?at=${at}`) apiRequest<{ data: Option[] }>(`/inspection-visits/planning-context/areas/${areaId}/operators?at=${at}`)
.then((response) => { .then((response) => {
setOperators(response.data); setOperators(response.data);
if (response.data.length === 1 && response.data[0]) setOperatorId(response.data[0].id); if (response.data.length === 1 && response.data[0]) setOperatorId(response.data[0].id);
@@ -142,7 +121,7 @@ export function InspectionVisitCreateF61Page() {
setBusy(true); setBusy(true);
try { try {
const created = await requestJson<CreatedInspection>('/inspection-visits', { const created = await apiRequest<CreatedInspection>('/inspection-visits', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
operationalAreaId: areaId, operationalAreaId: areaId,
+4 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { Link, useParams } from 'react-router'; import { Link, useParams } from 'react-router';
import { useAuth } from '../auth/AuthContext'; import { useAuth } from '../auth/AuthContext';
import { DocumentPdfProjection } from '../components/DocumentPdfProjection';
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 { SearchableSelect } from '../components/SearchableSelect'; import { SearchableSelect } from '../components/SearchableSelect';
@@ -193,7 +194,7 @@ export function ReportDetailPage() {
{success && <Alert type="success">{success}</Alert>} {success && <Alert type="success">{success}</Alert>}
{report && <> {report && <>
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>Un INF corresponde a una sola Acta.</strong> El Word puede editarse durante la preparación del Informe. El Acta sellada y los Hallazgos que contiene permanecen inmutables.</p></div> <div className="temporal-notice"><Icon name="clipboard" /><p><strong>Un INF corresponde a una sola Acta.</strong> Una Inspección puede contener varias Actas y, por lo tanto, varios INF independientes. El Word puede editarse durante la preparación; el Acta sellada y sus Hallazgos permanecen inmutables.</p></div>
<section className="panel report-summary-panel"> <section className="panel report-summary-panel">
<div className="panel-heading"><div><span className="eyebrow">TRAZABILIDAD DOCUMENTAL</span><h2>{report.code}</h2></div><small className="muted">Versión del Acta: {report.actVersion}</small></div> <div className="panel-heading"><div><span className="eyebrow">TRAZABILIDAD DOCUMENTAL</span><h2>{report.code}</h2></div><small className="muted">Versión del Acta: {report.actVersion}</small></div>
@@ -210,6 +211,8 @@ export function ReportDetailPage() {
</div> </div>
</section> </section>
<DocumentPdfProjection kind="report" />
<section className="panel"> <section className="panel">
<div className="panel-heading"><div><span className="eyebrow">WORD EDITABLE</span><h2>Preparación del INF</h2><p className="section-copy">El Inspector puede revisar y ajustar el texto del Informe antes de incorporarlo a GEDO. Esta edición no altera el Acta fuente.</p></div>{report.wordStatus === 'READY' && <a className="button secondary" href={inspectionReportWordDownloadUrl(report.id)}>Descargar Word</a>}</div> <div className="panel-heading"><div><span className="eyebrow">WORD EDITABLE</span><h2>Preparación del INF</h2><p className="section-copy">El Inspector puede revisar y ajustar el texto del Informe antes de incorporarlo a GEDO. Esta edición no altera el Acta fuente.</p></div>{report.wordStatus === 'READY' && <a className="button secondary" href={inspectionReportWordDownloadUrl(report.id)}>Descargar Word</a>}</div>
<form className="form-section" onSubmit={saveNarrative}> <form className="form-section" onSubmit={saveNarrative}>