From 205ff1bab8ec2a4ef1576743a58306aa05d7f9cc Mon Sep 17 00:00:00 2001 From: enlineawork Date: Sat, 5 Sep 2026 17:19:42 -0300 Subject: [PATCH] =?UTF-8?q?F1.3=20=C2=B7=20Vista=20web=20de=20preparaci?= =?UTF-8?q?=C3=B3n=20de=20campo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completa F1.3 con la vista web imprimible de preparación de campo, selector buscable y lectura Acta → Hallazgos → Inventario. --- web-v2/package.json | 2 +- web-v2/src/app/App.tsx | 2 + web-v2/src/config/version.ts | 4 +- web-v2/src/layout/AppLayout.tsx | 1 + web-v2/src/pages/FieldBriefingsPage.tsx | 265 ++++++++++++++++++++++++ 5 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 web-v2/src/pages/FieldBriefingsPage.tsx diff --git a/web-v2/package.json b/web-v2/package.json index d5e2351..431bb57 100644 --- a/web-v2/package.json +++ b/web-v2/package.json @@ -1,6 +1,6 @@ { "name": "dhv2-web", - "version": "0.20.0-2", + "version": "0.20.0-3", "private": true, "type": "module", "engines": { diff --git a/web-v2/src/app/App.tsx b/web-v2/src/app/App.tsx index 546bef8..dab7bfd 100644 --- a/web-v2/src/app/App.tsx +++ b/web-v2/src/app/App.tsx @@ -34,6 +34,7 @@ import { ReportDetailPage } from '../pages/ReportDetailPage'; import { VerificationPlanningPage } from '../pages/VerificationPlanningPage'; import { ActAdministrationPage } from '../pages/ActAdministrationPage'; import { ActAdministrationDetailPage } from '../pages/ActAdministrationDetailPage'; +import { FieldBriefingsPage } from '../pages/FieldBriefingsPage'; const MapPage = lazy(() => import('../pages/MapPage').then((module) => ({ default: module.MapPage }))); @@ -65,6 +66,7 @@ export function App() { }> } /> } /> + } /> }>} /> }> diff --git a/web-v2/src/config/version.ts b/web-v2/src/config/version.ts index 0c59f2d..18098f7 100644 --- a/web-v2/src/config/version.ts +++ b/web-v2/src/config/version.ts @@ -1,2 +1,2 @@ -export const APP_VERSION = '0.20.0-2'; -export const APP_PHASE = 'Fase F1.2 · Seguimiento administrativo por Acta'; +export const APP_VERSION = '0.20.0-3'; +export const APP_PHASE = 'Fase F1.3 · Planificación inteligente de pendientes'; diff --git a/web-v2/src/layout/AppLayout.tsx b/web-v2/src/layout/AppLayout.tsx index 5e804f5..72bdae1 100644 --- a/web-v2/src/layout/AppLayout.tsx +++ b/web-v2/src/layout/AppLayout.tsx @@ -16,6 +16,7 @@ interface NavItem { const operational: NavItem[] = [ { to: '/', label: 'Inicio', icon: 'home', permission: 'dashboard.read' }, { to: '/inspecciones', label: 'Inspecciones', icon: 'clipboard', permission: 'inspections.read' }, + { to: '/preparacion-campo', label: 'Preparación de campo', icon: 'clipboard', permission: 'inspections.read' }, ]; const followUp: NavItem[] = [ diff --git a/web-v2/src/pages/FieldBriefingsPage.tsx b/web-v2/src/pages/FieldBriefingsPage.tsx new file mode 100644 index 0000000..f85ff0d --- /dev/null +++ b/web-v2/src/pages/FieldBriefingsPage.tsx @@ -0,0 +1,265 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Link } from 'react-router'; +import { SearchableSelect } from '../components/SearchableSelect'; +import { Alert, EmptyState, LoadingBlock } from '../components/Feedback'; +import { formatDate, formatDateOnly } from '../lib/format'; + +type AdminState = + | 'ACT_RESPONSE_OVERDUE' + | 'ACT_RESPONSE_DUE_SOON' + | 'WAITING_RESPONSE' + | 'COMPANY_COMMITMENT_OVERDUE' + | 'VERIFICATION_PENDING' + | 'RESPONSE_RECEIVED'; + +interface VisitOption { + id: string; + code: string; + status: string; + plannedStartAt: string | null; + operationalArea: { name: string } | null; + operatorCompany: { name: string } | null; +} + +interface BriefingFinding { + id: string; + code: string; + title: string; + status: string; + severity: number | null; + nextControlOn: string | null; + asset: { + id: string; + code: string; + name: string; + typeName: string; + }; +} + +interface BriefingAct { + actId: string; + actCode: string; + occurredAt: string; + adminState: AdminState; + responseDueOn: string | null; + responseReceivedOn: string | null; + committedCorrectionOn: string | null; + latestResponseId: string | null; + findings: BriefingFinding[]; +} + +interface FieldBriefing { + inspection: { + id: string; + code: string; + status: string; + plannedStartAt: string | null; + area: { id: string; code: string | null; name: string | null } | null; + operatorCompany: { id: string; code: string | null; name: string | null } | null; + }; + plannedOn: string; + summary: { + acts: number; + findings: number; + inventoryItems: number; + responseOverdue: number; + commitmentOverdue: number; + verificationPending: number; + }; + acts: BriefingAct[]; +} + +async function getJson(url: string): Promise { + const response = await fetch(url, { + credentials: 'same-origin', + headers: { Accept: 'application/json' }, + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) { + const message = typeof payload?.message === 'string' + ? payload.message + : 'No se pudo preparar el paquete de campo.'; + throw new Error(message); + } + return payload as T; +} + +function adminStateLabel(state: AdminState): string { + const labels: Record = { + ACT_RESPONSE_OVERDUE: 'Respuesta del Acta vencida', + ACT_RESPONSE_DUE_SOON: 'Respuesta próxima a vencer', + WAITING_RESPONSE: 'Esperando respuesta', + COMPANY_COMMITMENT_OVERDUE: 'Compromiso de empresa vencido', + VERIFICATION_PENDING: 'Verificación pendiente', + RESPONSE_RECEIVED: 'Respuesta recibida', + }; + return labels[state]; +} + +function findingAction(finding: BriefingFinding, act: BriefingAct, plannedOn: string) { + if (finding.nextControlOn) { + if (finding.nextControlOn <= plannedOn) { + return { label: 'Revisar en campo', detail: 'Control programado o vencido', tone: 'danger' }; + } + return { label: 'Próximo control', detail: `Previsto ${formatDateOnly(finding.nextControlOn)}`, tone: 'pending' }; + } + if (act.adminState === 'VERIFICATION_PENDING') { + return { label: 'Revisar en campo', detail: 'Respuesta recibida sin control programado', tone: 'danger' }; + } + if (act.adminState === 'COMPANY_COMMITMENT_OVERDUE') { + return { label: 'Revisar en campo', detail: 'Regularización comprometida vencida', tone: 'danger' }; + } + if (act.adminState === 'RESPONSE_RECEIVED') { + return { label: 'Antecedente', detail: 'Respuesta de empresa recibida', tone: 'pending' }; + } + return { label: 'Antecedente', detail: adminStateLabel(act.adminState), tone: 'pending' }; +} + +export function FieldBriefingsPage() { + const [visits, setVisits] = useState([]); + const [visitId, setVisitId] = useState(''); + const [briefing, setBriefing] = useState(null); + const [loadingVisits, setLoadingVisits] = useState(true); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + setLoadingVisits(true); + Promise.all([ + getJson<{ data: VisitOption[] }>('/api/v3/inspection-visits?status=PLANNED&page=1&pageSize=100'), + getJson<{ data: VisitOption[] }>('/api/v3/inspection-visits?status=DRAFT&page=1&pageSize=100'), + ]).then(([planned, drafts]) => { + const all = [...planned.data, ...drafts.data] + .sort((a, b) => String(a.plannedStartAt ?? '').localeCompare(String(b.plannedStartAt ?? ''))); + setVisits(all); + const onlyVisit = all[0]; + if (all.length === 1 && onlyVisit) setVisitId(onlyVisit.id); + }).catch((cause) => { + setError(cause instanceof Error ? cause.message : String(cause)); + }).finally(() => setLoadingVisits(false)); + }, []); + + useEffect(() => { + if (!visitId) { + setBriefing(null); + return; + } + setLoading(true); + setError(''); + getJson(`/api/v3/inspection-visits/${visitId}/field-briefing`) + .then(setBriefing) + .catch((cause) => setError(cause instanceof Error ? cause.message : String(cause))) + .finally(() => setLoading(false)); + }, [visitId]); + + const selected = useMemo( + () => visits.find((visit) => visit.id === visitId) ?? null, + [visits, visitId], + ); + + return
+
+
+ ANTES DE SALIR A CAMPO +

Preparación de campo

+

Seleccioná la inspección y revisá los pendientes de Actas anteriores del mismo Área/Yacimiento y Operadora.

+
+ {briefing && } +
+ +
+ + {loadingVisits ? : setVisitId(event.target.value)} + searchPlaceholder="Buscar por código, Área u Operadora…" + > + + {visits.map((visit) => )} + } + {selected?.plannedStartAt && Salida prevista: {formatDate(selected.plannedStartAt)}} +
+ + {error && {error}} + {loading && } + + {!loading && !briefing && !error && } + + {!loading && briefing && <> +
+
+ {briefing.inspection.code} +

{briefing.inspection.area?.name ?? 'Área sin definir'} · {briefing.inspection.operatorCompany?.name ?? 'Operadora sin definir'}

+

Fecha de referencia: {formatDateOnly(briefing.plannedOn)}

+
+ + Abrir planificación + +
+ +
+ Actas {briefing.summary.acts} + Hallazgos {briefing.summary.findings} + Inventario {briefing.summary.inventoryItems} + Respuestas vencidas {briefing.summary.responseOverdue} + Compromisos vencidos {briefing.summary.commitmentOverdue} + Verificaciones pendientes {briefing.summary.verificationPending} +
+ + {briefing.acts.length === 0 ? : briefing.acts.map((act) =>
+
+
+ {act.actCode} + {formatDate(act.occurredAt)} · {adminStateLabel(act.adminState)} +
+
+ Plazo del Acta + {act.responseDueOn ? formatDateOnly(act.responseDueOn) : 'Sin definir'} +
+
+ Respuesta empresa + {act.responseReceivedOn ? formatDateOnly(act.responseReceivedOn) : 'Pendiente'} +
+
+ Compromiso empresa + {act.committedCorrectionOn ? formatDateOnly(act.committedCorrectionOn) : 'Sin fecha'} +
+
+ +
+ + + {act.findings.map((finding) => { + const action = findingAction(finding, act, briefing.plannedOn); + return + + + + + ; + })} +
Qué hacerHallazgoInventarioControl
+ {action.label} + {action.detail} + + {finding.code} + {finding.title} + + {finding.asset.code} + {finding.asset.name} · {finding.asset.typeName} + {finding.nextControlOn ? formatDateOnly(finding.nextControlOn) : 'Sin fecha'}
+
+
)} + } +
; +}