fix(f4): present locked and sealed act lifecycle
This commit is contained in:
@@ -2,8 +2,13 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useAuth } from '../../auth/AuthContext';
|
||||
import { Alert, LoadingBlock, errorMessage } from '../../components/Feedback';
|
||||
import { Icon } from '../../components/Icon';
|
||||
import { getInspectionClosure, getInspectionSignatureBlob } from '../../lib/api';
|
||||
import type { InspectionAct, InspectionActSignature, InspectionClosure } from '../../lib/api';
|
||||
import { getInspectionSignatureBlob } from '../../lib/api';
|
||||
import type { InspectionActSignature } from '../../lib/api';
|
||||
import {
|
||||
getInspectionClosureF4,
|
||||
type InspectionActF4,
|
||||
type InspectionClosureF4,
|
||||
} from '../../lib/inspectionActF4Api';
|
||||
import { formatDate } from '../../lib/format';
|
||||
|
||||
function signatureStatusLabel(value: InspectionActSignature['status']): string {
|
||||
@@ -16,17 +21,17 @@ function signerTypeLabel(value: InspectionActSignature['signerType']): string {
|
||||
return value === 'INSPECTOR' ? 'Inspector/a' : 'Responsable de la empresa';
|
||||
}
|
||||
|
||||
export function InspectionClosurePanel({ act }: { act: InspectionAct }) {
|
||||
export function InspectionClosurePanel({ act }: { act: InspectionActF4 }) {
|
||||
const { hasPermission } = useAuth();
|
||||
const canRead = hasPermission('inspection_closure.read');
|
||||
const [closure, setClosure] = useState<InspectionClosure | null>(null);
|
||||
const [closure, setClosure] = useState<InspectionClosureF4 | null>(null);
|
||||
const [loading, setLoading] = useState(canRead);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!canRead) return;
|
||||
setLoading(true);
|
||||
getInspectionClosure(act.id)
|
||||
getInspectionClosureF4(act.id)
|
||||
.then(setClosure)
|
||||
.catch((requestError) => setError(errorMessage(requestError)))
|
||||
.finally(() => setLoading(false));
|
||||
@@ -60,26 +65,43 @@ export function InspectionClosurePanel({ act }: { act: InspectionAct }) {
|
||||
if (loading || !closure) return <section className="panel inspection-closure-panel"><LoadingBlock label="Cargando cierre del acta…" /></section>;
|
||||
|
||||
const constanciasCompletas = inspectorSignatures.length > 0 && Boolean(companyOutcome);
|
||||
const isSealed = act.status === 'SEALED' || act.status === 'CLOSED';
|
||||
const isLocked = act.status === 'LOCKED' || act.status === 'READY';
|
||||
const lifecycleLabel = isSealed
|
||||
? 'Acta sellada'
|
||||
: isLocked
|
||||
? 'Esperando manifestación'
|
||||
: act.status === 'CANCELLED'
|
||||
? 'Acta cancelada'
|
||||
: 'Borrador en campo';
|
||||
const lifecycleClass = isSealed ? 'active' : isLocked ? 'observed' : act.status === 'CANCELLED' ? 'inactive' : 'pending';
|
||||
|
||||
return <section className="panel inspection-closure-panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">CIERRE DEL ACTA · SÓLO LECTURA</span><h2>Responsable, firmas y sellado</h2><p className="section-copy">La preparación y firma del acta se realizan desde la APK. La inspección puede continuar con otras actas.</p></div><span className={`status-badge large ${act.status === 'CLOSED' ? 'active' : act.status === 'READY' ? 'observed' : 'pending'}`}>{act.status === 'CLOSED' ? 'Cierre sellado' : act.status === 'READY' ? 'Esperando firmas' : 'Pendiente en APK'}</span></div>
|
||||
<div className="panel-heading"><div><span className="eyebrow">CIERRE DEL ACTA · SÓLO LECTURA</span><h2>Responsable, firmas y sellado</h2><p className="section-copy">Al bloquearse, el contenido del Acta queda inmutable. La Inspección puede continuar y generar otras Actas.</p></div><span className={`status-badge large ${lifecycleClass}`}>{lifecycleLabel}</span></div>
|
||||
|
||||
{error && <Alert>{error}</Alert>}
|
||||
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>Operación exclusiva en APK.</strong> El dashboard muestra las constancias sincronizadas, pero no permite identificar al responsable, firmar, reabrir ni cerrar el acta.</p></div>
|
||||
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>El dashboard no modifica el Acta.</strong> El bloqueo y la firma del Inspector se realizan desde la APK. La manifestación de la empresa puede completarse en campo o mediante el enlace seguro posterior.</p></div>
|
||||
|
||||
<div className="closure-step-grid">
|
||||
<div className={closure.responsible ? 'complete' : ''}><span>1</span><strong>Responsable</strong><small>{closure.responsible ? 'Registrado' : 'Pendiente'}</small></div>
|
||||
<div className={closure.closure?.isCurrent ? 'complete' : ''}><span>2</span><strong>Congelado</strong><small>{closure.closure?.isCurrent ? 'SHA-256 listo' : 'Pendiente'}</small></div>
|
||||
<div className={constanciasCompletas ? 'complete' : ''}><span>3</span><strong>Constancias</strong><small>{inspectorSignatures.length} inspector · {companyOutcome ? 'empresa OK' : 'empresa pendiente'}</small></div>
|
||||
<div className={act.status === 'CLOSED' ? 'complete' : ''}><span>4</span><strong>Cierre</strong><small>{act.status === 'CLOSED' ? 'Sellado' : 'Pendiente'}</small></div>
|
||||
<div className={closure.closure?.isCurrent ? 'complete' : ''}><span>2</span><strong>Bloqueo</strong><small>{closure.closure?.isCurrent ? 'Contenido inmutable' : 'Pendiente'}</small></div>
|
||||
<div className={constanciasCompletas ? 'complete' : ''}><span>3</span><strong>Manifestaciones</strong><small>{inspectorSignatures.length} inspector · {companyOutcome ? 'empresa resuelta' : 'empresa pendiente'}</small></div>
|
||||
<div className={isSealed ? 'complete' : ''}><span>4</span><strong>Sellado</strong><small>{isSealed ? 'Definitivo' : 'Pendiente'}</small></div>
|
||||
</div>
|
||||
|
||||
<div className="responsible-summary">
|
||||
<div><small>Urgencia</small><strong>{closure.act.urgency === 'URGENT' ? 'Urgente' : 'No urgente'}</strong></div>
|
||||
<div><small>Plazo configurado</small><strong>{closure.act.deadlineDays ? `${closure.act.deadlineDays} días ${closure.act.deadlineDayType === 'BUSINESS' ? 'hábiles' : 'corridos'}` : 'Pendiente'}</strong></div>
|
||||
<div><small>Inicio del plazo</small><strong>{closure.act.deadlineBaseAt ? formatDate(closure.act.deadlineBaseAt) : 'Pendiente de evento válido'}</strong></div>
|
||||
<div><small>Vencimiento</small><strong>{closure.act.deadlineAt ? formatDate(closure.act.deadlineAt) : 'Todavía no iniciado'}</strong></div>
|
||||
</div>
|
||||
|
||||
{closure.responsible && <div className="responsible-summary"><div><small>Situación</small><strong>{closure.responsible.attendanceStatus === 'PRESENT' ? 'Presente' : 'Ausente'}</strong></div><div><small>Responsable</small><strong>{closure.responsible.fullName ?? 'No estuvo presente'}</strong></div><div><small>Documento / cargo</small><strong>{closure.responsible.documentNumber ? `${closure.responsible.documentType} ${closure.responsible.documentNumber}` : 'No informado'}{closure.responsible.position ? ` · ${closure.responsible.position}` : ''}</strong></div><div><small>Contacto</small><strong>{closure.responsible.email ?? closure.responsible.phone ?? 'No informado'}</strong></div></div>}
|
||||
|
||||
{closure.closure?.isCurrent && <div className="closure-hash-card"><div><span className="eyebrow">CONTENIDO CONGELADO</span><strong>{closure.closure.schemaVersion}</strong><small>Preparado {formatDate(closure.closure.preparedAt)}</small></div><code>{closure.closure.preparedSha256}</code></div>}
|
||||
{closure.closure?.isCurrent && <div className="closure-hash-card"><div><span className="eyebrow">CONTENIDO BLOQUEADO</span><strong>{closure.closure.schemaVersion}</strong><small>Bloqueado {formatDate(closure.act.lockedAt ?? closure.closure.preparedAt)}</small></div><code>{closure.act.lockedSha256 ?? closure.closure.preparedSha256}</code></div>}
|
||||
|
||||
{closure.signatures.length > 0 && <div className="signature-records">{closure.signatures.map((signature) => <article key={signature.id}><div><span className={`status-badge ${signature.status === 'SIGNED' ? 'active' : 'observed'}`}>{signatureStatusLabel(signature.status)}</span><strong>{signature.signerName}</strong><small>{signerTypeLabel(signature.signerType)} · {formatDate(signature.createdAt)}</small></div><code title={signature.signaturePayloadSha256}>{signature.signaturePayloadSha256}</code>{signature.status === 'SIGNED' ? <><button type="button" className="button secondary" onClick={() => viewSignature(signature)}>Ver firma</button>{signature.signerType === 'COMPANY_RESPONSIBLE' && <p><strong>{signature.companyManifestation === 'DISSENT' ? 'Firma en disidencia' : 'Firma en conformidad'}</strong>{signature.companyStatement ? ` · ${signature.companyStatement}` : ''}</p>}</> : <p>{signature.reason}</p>}</article>)}</div>}
|
||||
|
||||
{act.status === 'CLOSED' && closure.closure?.finalSha256 && <div className="closed-seal"><Icon name="check" /><div><span className="eyebrow">ACTA CERRADA</span><strong>{formatDate(closure.closure.serverClosedAt)}</strong><p>El acta quedó sellada. La inspección y las demás actas tienen un ciclo independiente.</p><code>{closure.closure.finalSha256}</code></div></div>}
|
||||
{isSealed && closure.closure?.finalSha256 && <div className="closed-seal"><Icon name="check" /><div><span className="eyebrow">ACTA SELLADA</span><strong>{formatDate(closure.act.sealedAt ?? closure.closure.serverClosedAt)}</strong><p>El Acta quedó sellada e inmutable. La Inspección y sus demás Actas continúan con ciclo independiente.</p><code>{closure.closure.finalSha256}</code></div></div>}
|
||||
</section>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user