Merge pull request #9 from enlineawork/feature/f1-4-dashboard-briefing
F1.4 · Briefing contextual antes de salir
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dhv2-web",
|
||||
"version": "0.20.0-3",
|
||||
"version": "0.20.0-4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const APP_VERSION = '0.20.0-3';
|
||||
export const APP_PHASE = 'Fase F1.3 · Planificación inteligente de pendientes';
|
||||
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`);
|
||||
}
|
||||
Reference in New Issue
Block a user