Implementa el nuevo núcleo operativo: múltiples Actas por Inspección, un Informe por Acta, cierre de campo independiente y firma de empresa diferida con conformidad/disidencia.
86 lines
6.4 KiB
TypeScript
86 lines
6.4 KiB
TypeScript
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 { formatDate } from '../../lib/format';
|
|
|
|
function signatureStatusLabel(value: InspectionActSignature['status']): string {
|
|
if (value === 'SIGNED') return 'Firmada';
|
|
if (value === 'REFUSED') return 'Se negó a firmar';
|
|
return 'Ausente';
|
|
}
|
|
|
|
function signerTypeLabel(value: InspectionActSignature['signerType']): string {
|
|
return value === 'INSPECTOR' ? 'Inspector/a' : 'Responsable de la empresa';
|
|
}
|
|
|
|
export function InspectionClosurePanel({ act }: { act: InspectionAct }) {
|
|
const { hasPermission } = useAuth();
|
|
const canRead = hasPermission('inspection_closure.read');
|
|
const [closure, setClosure] = useState<InspectionClosure | null>(null);
|
|
const [loading, setLoading] = useState(canRead);
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (!canRead) return;
|
|
setLoading(true);
|
|
getInspectionClosure(act.id)
|
|
.then(setClosure)
|
|
.catch((requestError) => setError(errorMessage(requestError)))
|
|
.finally(() => setLoading(false));
|
|
}, [act.id, canRead]);
|
|
|
|
const inspectorSignatures = useMemo(
|
|
() => closure?.signatures.filter((item) => item.signerType === 'INSPECTOR') ?? [],
|
|
[closure],
|
|
);
|
|
const companyOutcome = useMemo(
|
|
() => closure?.signatures.find((item) => item.signerType === 'COMPANY_RESPONSIBLE') ?? null,
|
|
[closure],
|
|
);
|
|
|
|
const viewSignature = async (signature: InspectionActSignature) => {
|
|
const tab = window.open('about:blank', '_blank');
|
|
if (tab) tab.opener = null;
|
|
try {
|
|
const blob = await getInspectionSignatureBlob(signature.id);
|
|
const url = URL.createObjectURL(blob);
|
|
if (tab) tab.location.href = url;
|
|
else window.open(url, '_blank', 'noopener,noreferrer');
|
|
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
} catch (requestError) {
|
|
tab?.close();
|
|
setError(errorMessage(requestError));
|
|
}
|
|
};
|
|
|
|
if (!canRead) return null;
|
|
if (loading || !closure) return <section className="panel inspection-closure-panel"><LoadingBlock label="Cargando cierre del acta…" /></section>;
|
|
|
|
const constanciasCompletas = inspectorSignatures.length > 0 && Boolean(companyOutcome);
|
|
|
|
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>
|
|
|
|
{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="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>
|
|
|
|
{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.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>}
|
|
</section>;
|
|
}
|