Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c309036cb | ||
|
|
6bda0f68fc | ||
|
|
6a86f67eba | ||
|
|
37795e6586 | ||
|
|
3cd9cdd104 | ||
|
|
205ff1bab8 | ||
|
|
d954ec135f |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dhv2-web",
|
||||
"version": "0.20.0-2",
|
||||
"version": "0.20.0-4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -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() {
|
||||
<Route element={<PermissionRoute permission="inspections.read" />}>
|
||||
<Route path="/inspecciones" element={<InspectionVisitsPage />} />
|
||||
<Route path="/inspecciones/:id" element={<InspectionVisitEditorPage />} />
|
||||
<Route path="/preparacion-campo" element={<FieldBriefingsPage />} />
|
||||
</Route>
|
||||
<Route element={<PermissionRoute permission="inspections.manage" />}><Route path="/inspecciones/nueva" element={<InspectionVisitEditorPage />} /></Route>
|
||||
<Route element={<PermissionRoute permission="inspection_acts.read" />}>
|
||||
|
||||
@@ -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-4';
|
||||
export const APP_PHASE = 'Fase F1.4 · Briefing contextual antes de salir';
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../../components/Feedback';
|
||||
import { Icon } from '../../components/Icon';
|
||||
import type { InspectionVisit } from '../../lib/api';
|
||||
import { formatDate, formatDateOnly } from '../../lib/format';
|
||||
import {
|
||||
getInspectionFieldBriefing,
|
||||
type FieldBriefing,
|
||||
type FieldBriefingActState,
|
||||
} from './fieldBriefingApi';
|
||||
|
||||
function stateLabel(state: FieldBriefingActState): string {
|
||||
if (state === 'ACT_RESPONSE_OVERDUE') return 'Respuesta del Acta vencida';
|
||||
if (state === 'ACT_RESPONSE_DUE_SOON') return 'Respuesta próxima a vencer';
|
||||
if (state === 'COMPANY_COMMITMENT_OVERDUE') return 'Compromiso de empresa vencido';
|
||||
if (state === 'VERIFICATION_PENDING') return 'Verificación pendiente';
|
||||
if (state === 'RESPONSE_RECEIVED') return 'Respuesta recibida';
|
||||
return 'Esperando respuesta';
|
||||
}
|
||||
|
||||
function stateClass(state: FieldBriefingActState): string {
|
||||
if (state === 'ACT_RESPONSE_OVERDUE' || state === 'COMPANY_COMMITMENT_OVERDUE') return 'danger';
|
||||
if (state === 'ACT_RESPONSE_DUE_SOON' || state === 'VERIFICATION_PENDING') return 'warning';
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
export function FieldBriefingPanel({ visit }: { visit: InspectionVisit }) {
|
||||
const [briefing, setBriefing] = useState<FieldBriefing | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
getInspectionFieldBriefing(visit.id)
|
||||
.then((value) => { if (active) setBriefing(value); })
|
||||
.catch((requestError) => { if (active) setError(errorMessage(requestError)); })
|
||||
.finally(() => { if (active) setLoading(false); });
|
||||
return () => { active = false; };
|
||||
}, [visit.id, visit.updatedAt]);
|
||||
|
||||
return <section className="panel inspection-checklist-panel">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<span className="eyebrow">ANTES DE CONFIRMAR LA SALIDA</span>
|
||||
<h2>Pendientes para revisar en campo</h2>
|
||||
<p className="section-copy">Actas anteriores del mismo Área y Operadora que requieren atención durante esta inspección. El plazo administrativo pertenece al Acta y los hallazgos se mantienen dentro de su expediente de origen.</p>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<Link className="button secondary" to="/preparacion-campo"><Icon name="clipboard" />Vista imprimible</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <Alert>{error}</Alert>}
|
||||
{loading ? <LoadingBlock label="Preparando pendientes de campo…" /> : briefing && <>
|
||||
<div className="inspection-checklist-metrics">
|
||||
<div><strong>{briefing.summary.acts}</strong><span>actas con pendientes</span></div>
|
||||
<div><strong>{briefing.summary.findings}</strong><span>hallazgos a revisar</span></div>
|
||||
<div><strong>{briefing.summary.inventoryItems}</strong><span>registros involucrados</span></div>
|
||||
<div><strong>{briefing.summary.responseOverdue}</strong><span>respuestas vencidas</span></div>
|
||||
<div><strong>{briefing.summary.commitmentOverdue}</strong><span>compromisos vencidos</span></div>
|
||||
<div><strong>{briefing.summary.verificationPending}</strong><span>verificaciones pendientes</span></div>
|
||||
</div>
|
||||
|
||||
<div className="temporal-notice">
|
||||
<Icon name="clipboard" />
|
||||
<p><strong>Contexto de salida:</strong> {briefing.inspection.area?.name ?? 'Área sin definir'} · {briefing.inspection.operatorCompany?.name ?? 'Operadora sin definir'} · {formatDateOnly(briefing.plannedOn)}.</p>
|
||||
</div>
|
||||
|
||||
{briefing.acts.length === 0
|
||||
? <EmptyState title="Sin pendientes anteriores" text="No hay Actas anteriores con acciones de campo pendientes para este Área y Operadora." />
|
||||
: <div>{briefing.acts.map((act) => <div className="table-panel" key={act.actId}>
|
||||
<div className="table-summary">
|
||||
<div><strong>{act.actCode}</strong><span>{formatDate(act.occurredAt)} · {act.findings.length} hallazgo{act.findings.length === 1 ? '' : 's'}</span></div>
|
||||
<span className={`status-badge ${stateClass(act.adminState)}`}>{stateLabel(act.adminState)}</span>
|
||||
</div>
|
||||
<div className="table-scroll"><table>
|
||||
<thead><tr><th>Hallazgo</th><th>Inventario</th><th>Gravedad</th><th>Control</th><th /></tr></thead>
|
||||
<tbody>{act.findings.map((finding) => <tr key={finding.id}>
|
||||
<td><Link className="history-asset-link" to={`/hallazgos/${finding.id}`}><strong>{finding.title}</strong><small>{finding.code} · {finding.status}</small></Link></td>
|
||||
<td><Link className="history-asset-link" to={`/inventarios/${finding.asset.id}`}><strong>{finding.asset.name}</strong><small>{finding.asset.code} · {finding.asset.typeName}</small></Link></td>
|
||||
<td>{finding.severity ?? '—'}</td>
|
||||
<td>{finding.nextControlOn ? formatDateOnly(finding.nextControlOn) : 'A definir'}</td>
|
||||
<td className="action-cell"><Link className="button secondary compact" to={`/seguimiento-actas/${act.actId}`}>Ver Acta</Link></td>
|
||||
</tr>)}</tbody>
|
||||
</table></div>
|
||||
<div className="table-summary">
|
||||
<span>Plazo empresa: <strong>{act.responseDueOn ? formatDateOnly(act.responseDueOn) : 'sin definir'}</strong></span>
|
||||
<span>Respuesta: <strong>{act.responseReceivedOn ? formatDateOnly(act.responseReceivedOn) : 'pendiente'}</strong></span>
|
||||
<span>Compromiso: <strong>{act.committedCorrectionOn ? formatDateOnly(act.committedCorrectionOn) : '—'}</strong></span>
|
||||
</div>
|
||||
</div>)}</div>}
|
||||
</>}
|
||||
</section>;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { Icon } from '../../components/Icon';
|
||||
import { listInspectionActs } from '../../lib/api';
|
||||
import type { InspectionActListItem, InspectionVisit } from '../../lib/api';
|
||||
import { formatDate } from '../../lib/format';
|
||||
import { FieldBriefingPanel } from './FieldBriefingPanel';
|
||||
import { inspectionActStatusClass, inspectionActStatusLabel } from './inspectionActPresentation';
|
||||
|
||||
export function InspectionActsPanel({ visit }: { visit: InspectionVisit }) {
|
||||
@@ -25,17 +26,18 @@ export function InspectionActsPanel({ visit }: { visit: InspectionVisit }) {
|
||||
.finally(() => setLoading(false));
|
||||
}, [canRead, visit.id]);
|
||||
|
||||
if (!canRead) return null;
|
||||
|
||||
return <section className="inspection-acts-panel">
|
||||
<div className="panel-heading">
|
||||
<div><span className="eyebrow">ACTAS DE LA INSPECCIÓN</span><h2>Actas generadas</h2><p className="section-copy">Una inspección puede generar múltiples actas. Cada acta agrupa sus propios hallazgos y tendrá un único informe.</p></div>
|
||||
<span className="status-badge pending">Sólo lectura</span>
|
||||
</div>
|
||||
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>Las actas se crean en la APK.</strong> El dashboard permite consultar cada acta, sus hallazgos, firmas e informe después de la sincronización.</p></div>
|
||||
{error && <Alert>{error}</Alert>}
|
||||
{loading ? <LoadingBlock label="Cargando actas…" /> : acts.length === 0
|
||||
? <EmptyState title="Sin actas sincronizadas" text={visit.status === 'IN_PROGRESS' ? 'El inspector puede crear la primera acta desde la APK.' : 'Aparecerán aquí a medida que el inspector genere actas desde la APK.'} />
|
||||
: <div className="table-panel inspection-acts-table"><div className="table-summary"><strong>Actas de la inspección</strong><span>Numeración oficial global por año</span></div><div className="table-scroll"><table><thead><tr><th>Acta</th><th>Estado</th><th>Fecha</th><th>Registros</th><th>Hallazgos</th><th>Versión</th><th /></tr></thead><tbody>{acts.map((act) => <tr key={act.id}><td><div className="asset-cell"><span className="asset-symbol"><Icon name="clipboard" size={17} /></span><div><strong>{act.title}</strong><small>{act.code}</small></div></div></td><td><span className={`status-badge ${inspectionActStatusClass(act.status)}`}>{inspectionActStatusLabel(act.status)}</span></td><td>{formatDate(act.occurredAt)}</td><td>{act.assetCount}</td><td>{act.findingCount}</td><td>v{act.currentVersion}</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>}
|
||||
</section>;
|
||||
return <>
|
||||
<FieldBriefingPanel visit={visit} />
|
||||
{canRead && <section className="inspection-acts-panel">
|
||||
<div className="panel-heading">
|
||||
<div><span className="eyebrow">ACTAS DE LA INSPECCIÓN</span><h2>Actas generadas</h2><p className="section-copy">Una inspección puede generar múltiples actas. Cada acta agrupa sus propios hallazgos y tendrá un único informe.</p></div>
|
||||
<span className="status-badge pending">Sólo lectura</span>
|
||||
</div>
|
||||
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>Las actas se crean en la APK.</strong> El dashboard permite consultar cada acta, sus hallazgos, firmas e informe después de la sincronización.</p></div>
|
||||
{error && <Alert>{error}</Alert>}
|
||||
{loading ? <LoadingBlock label="Cargando actas…" /> : acts.length === 0
|
||||
? <EmptyState title="Sin actas sincronizadas" text={visit.status === 'IN_PROGRESS' ? 'El inspector puede crear la primera acta desde la APK.' : 'Aparecerán aquí a medida que el inspector genere actas desde la APK.'} />
|
||||
: <div className="table-panel inspection-acts-table"><div className="table-summary"><strong>Actas de la inspección</strong><span>Numeración oficial global por año</span></div><div className="table-scroll"><table><thead><tr><th>Acta</th><th>Estado</th><th>Fecha</th><th>Registros</th><th>Hallazgos</th><th>Versión</th><th /></tr></thead><tbody>{acts.map((act) => <tr key={act.id}><td><div className="asset-cell"><span className="asset-symbol"><Icon name="clipboard" size={17} /></span><div><strong>{act.title}</strong><small>{act.code}</small></div></div></td><td><span className={`status-badge ${inspectionActStatusClass(act.status)}`}>{inspectionActStatusLabel(act.status)}</span></td><td>{formatDate(act.occurredAt)}</td><td>{act.assetCount}</td><td>{act.findingCount}</td><td>v{act.currentVersion}</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>}
|
||||
</section>}
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { apiRequest } from '../../lib/api';
|
||||
|
||||
export type FieldBriefingActState =
|
||||
| 'ACT_RESPONSE_OVERDUE'
|
||||
| 'ACT_RESPONSE_DUE_SOON'
|
||||
| 'WAITING_RESPONSE'
|
||||
| 'COMPANY_COMMITMENT_OVERDUE'
|
||||
| 'VERIFICATION_PENDING'
|
||||
| 'RESPONSE_RECEIVED';
|
||||
|
||||
export interface FieldBriefingFinding {
|
||||
id: string;
|
||||
code: string;
|
||||
title: string;
|
||||
status: string;
|
||||
severity: number | null;
|
||||
nextControlOn: string | null;
|
||||
asset: { id: string; code: string; name: string; typeName: string };
|
||||
}
|
||||
|
||||
export interface FieldBriefingAct {
|
||||
actId: string;
|
||||
actCode: string;
|
||||
occurredAt: string;
|
||||
adminState: FieldBriefingActState;
|
||||
responseDueOn: string | null;
|
||||
responseReceivedOn: string | null;
|
||||
committedCorrectionOn: string | null;
|
||||
latestResponseId: string | null;
|
||||
findings: FieldBriefingFinding[];
|
||||
}
|
||||
|
||||
export interface FieldBriefing {
|
||||
inspection: {
|
||||
id: string;
|
||||
code: string;
|
||||
status: string;
|
||||
plannedStartAt: string | null;
|
||||
area: { id: string; code: string; name: string } | null;
|
||||
operatorCompany: { id: string; code: string; name: string } | null;
|
||||
};
|
||||
plannedOn: string;
|
||||
summary: {
|
||||
acts: number;
|
||||
findings: number;
|
||||
inventoryItems: number;
|
||||
responseOverdue: number;
|
||||
commitmentOverdue: number;
|
||||
verificationPending: number;
|
||||
};
|
||||
acts: FieldBriefingAct[];
|
||||
}
|
||||
|
||||
export function getInspectionFieldBriefing(visitId: string) {
|
||||
return apiRequest<FieldBriefing>(`/inspection-visits/${visitId}/field-briefing`);
|
||||
}
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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<T>(url: string): Promise<T> {
|
||||
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<AdminState, string> = {
|
||||
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<VisitOption[]>([]);
|
||||
const [visitId, setVisitId] = useState('');
|
||||
const [briefing, setBriefing] = useState<FieldBriefing | null>(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<FieldBriefing>(`/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 <section className="field-briefing-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<span className="eyebrow">ANTES DE SALIR A CAMPO</span>
|
||||
<h1>Preparación de campo</h1>
|
||||
<p>Seleccioná la inspección y revisá los pendientes de Actas anteriores del mismo Área/Yacimiento y Operadora.</p>
|
||||
</div>
|
||||
{briefing && <button className="button secondary no-print" type="button" onClick={() => window.print()}>
|
||||
Imprimir preparación
|
||||
</button>}
|
||||
</div>
|
||||
|
||||
<div className="form-card no-print">
|
||||
<label>Inspección planificada</label>
|
||||
{loadingVisits ? <LoadingBlock label="Cargando inspecciones…" /> : <SearchableSelect
|
||||
value={visitId}
|
||||
onChange={(event) => setVisitId(event.target.value)}
|
||||
searchPlaceholder="Buscar por código, Área u Operadora…"
|
||||
>
|
||||
<option value="">Seleccionar inspección…</option>
|
||||
{visits.map((visit) => <option key={visit.id} value={visit.id}>
|
||||
{visit.code} · {visit.operationalArea?.name ?? 'Sin Área'} · {visit.operatorCompany?.name ?? 'Sin Operadora'}
|
||||
</option>)}
|
||||
</SearchableSelect>}
|
||||
{selected?.plannedStartAt && <small className="block-muted">Salida prevista: {formatDate(selected.plannedStartAt)}</small>}
|
||||
</div>
|
||||
|
||||
{error && <Alert>{error}</Alert>}
|
||||
{loading && <LoadingBlock label="Armando paquete de campo…" />}
|
||||
|
||||
{!loading && !briefing && !error && <EmptyState
|
||||
title="Seleccioná una inspección"
|
||||
text="El sistema reunirá automáticamente los pendientes de inspecciones anteriores del mismo contexto."
|
||||
/>}
|
||||
|
||||
{!loading && briefing && <>
|
||||
<div className="page-heading compact">
|
||||
<div>
|
||||
<span className="eyebrow">{briefing.inspection.code}</span>
|
||||
<h2>{briefing.inspection.area?.name ?? 'Área sin definir'} · {briefing.inspection.operatorCompany?.name ?? 'Operadora sin definir'}</h2>
|
||||
<p>Fecha de referencia: {formatDateOnly(briefing.plannedOn)}</p>
|
||||
</div>
|
||||
<Link className="button secondary no-print" to={`/inspecciones/${briefing.inspection.id}`}>
|
||||
Abrir planificación
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="status-tabs briefing-summary">
|
||||
<span>Actas <strong>{briefing.summary.acts}</strong></span>
|
||||
<span>Hallazgos <strong>{briefing.summary.findings}</strong></span>
|
||||
<span>Inventario <strong>{briefing.summary.inventoryItems}</strong></span>
|
||||
<span>Respuestas vencidas <strong>{briefing.summary.responseOverdue}</strong></span>
|
||||
<span>Compromisos vencidos <strong>{briefing.summary.commitmentOverdue}</strong></span>
|
||||
<span>Verificaciones pendientes <strong>{briefing.summary.verificationPending}</strong></span>
|
||||
</div>
|
||||
|
||||
{briefing.acts.length === 0 ? <EmptyState
|
||||
title="Sin pendientes anteriores"
|
||||
text="No hay Actas anteriores con hallazgos abiertos que requieran atención para esta inspección."
|
||||
/> : briefing.acts.map((act) => <article className="table-panel" key={act.actId}>
|
||||
<div className="table-summary">
|
||||
<div>
|
||||
<strong>{act.actCode}</strong>
|
||||
<small className="block-muted">{formatDate(act.occurredAt)} · {adminStateLabel(act.adminState)}</small>
|
||||
</div>
|
||||
<div>
|
||||
<small>Plazo del Acta</small>
|
||||
<strong>{act.responseDueOn ? formatDateOnly(act.responseDueOn) : 'Sin definir'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<small>Respuesta empresa</small>
|
||||
<strong>{act.responseReceivedOn ? formatDateOnly(act.responseReceivedOn) : 'Pendiente'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<small>Compromiso empresa</small>
|
||||
<strong>{act.committedCorrectionOn ? formatDateOnly(act.committedCorrectionOn) : 'Sin fecha'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-scroll">
|
||||
<table>
|
||||
<thead><tr><th>Qué hacer</th><th>Hallazgo</th><th>Inventario</th><th>Control</th></tr></thead>
|
||||
<tbody>{act.findings.map((finding) => {
|
||||
const action = findingAction(finding, act, briefing.plannedOn);
|
||||
return <tr key={finding.id}>
|
||||
<td>
|
||||
<span className={`status-badge ${action.tone}`}>{action.label}</span>
|
||||
<small className="block-muted">{action.detail}</small>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{finding.code}</strong>
|
||||
<small className="block-muted">{finding.title}</small>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{finding.asset.code}</strong>
|
||||
<small className="block-muted">{finding.asset.name} · {finding.asset.typeName}</small>
|
||||
</td>
|
||||
<td>{finding.nextControlOn ? formatDateOnly(finding.nextControlOn) : 'Sin fecha'}</td>
|
||||
</tr>;
|
||||
})}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>)}
|
||||
</>}
|
||||
</section>;
|
||||
}
|
||||
Reference in New Issue
Block a user