Files
dh-inspeccion-v2/web-v2/src/features/inspections/InspectionActMediaPanel.tsx
T
admin a414d0ed36
DH V2 CI / API · typecheck, tests, build (push) Successful in 36s
Production dependency audit / API · production dependencies (push) Successful in 8s
Production dependency audit / WEB · production dependencies (push) Successful in 9s
DH V2 CI / WEB · typecheck, build (push) Successful in 1m37s
DH V2 CI / Docker / migrations / production images (push) Successful in 1m52s
DH V2 CI / Promote verified main to deploy (push) Successful in 4s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 4m11s
fix(web): clarify act context and operational map
2026-09-16 08:29:19 -03:00

81 lines
4.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../../components/Feedback';
import {
getInspectionFindingEvidenceBlob,
listInspectionFindingEvidence,
listInspectionFindings,
} from '../../lib/api';
import type { InspectionFinding, InspectionFindingEvidence } from '../../lib/api';
import { formatDate } from '../../lib/format';
type FindingWithPhotos = { finding: InspectionFinding; photos: InspectionFindingEvidence[] };
function Photo({ id, title, caption, load }: { id: string; title: string; caption: string; load: (id: string) => Promise<Blob> }) {
const [url, setUrl] = useState('');
useEffect(() => {
let active = true;
let objectUrl = '';
load(id).then((blob) => {
if (!active) return;
objectUrl = URL.createObjectURL(blob);
setUrl(objectUrl);
}).catch(() => undefined);
return () => { active = false; if (objectUrl) URL.revokeObjectURL(objectUrl); };
}, [id, load]);
return <figure className="act-finding-photo">
{url ? <a href={url} target="_blank" rel="noopener noreferrer" aria-label={`Ver foto ${title}`}><img src={url} alt={title} /></a> : <span className="media-preview-loading">Cargando foto</span>}
<figcaption>{caption}</figcaption>
</figure>;
}
function Finding({ item }: { item: FindingWithPhotos }) {
const { finding, photos } = item;
const hierarchy = finding.asset.hierarchy;
return <article className="act-finding-record">
<div className="inspection-finding-heading"><div><span className="eyebrow">{finding.code}</span><h3>{finding.title}</h3><p><strong>Elemento afectado:</strong> {finding.asset.typeName} · {finding.asset.name} · {finding.asset.code}</p></div></div>
{hierarchy && <div className="responsible-summary act-finding-context">
<div><small>Departamento</small><strong>{hierarchy.department?.name ?? '—'}</strong></div>
<div><small>Área</small><strong>{hierarchy.area?.name ?? '—'}</strong></div>
<div><small>Yacimiento</small><strong>{hierarchy.yacimiento?.name ?? '—'}</strong></div>
<div><small>Empresa</small><strong>{hierarchy.company?.name ?? '—'}</strong></div>
<div><small>Instalación</small><strong>{hierarchy.installation?.name ?? 'No corresponde'}</strong></div>
<div><small>Subinstalación</small><strong>{hierarchy.subinstallation?.name ?? 'No corresponde'}</strong></div>
</div>}
<p className="inspection-finding-description"><strong>Constatación:</strong> {finding.description}</p>
{finding.legalBasis && <p><strong>Normativa:</strong> {finding.legalBasis}</p>}
{finding.severity != null && <p>Gravedad {finding.severity}/10</p>}
{photos.length > 0 && <div className="act-finding-photos">
{photos.map((photo) => <Photo key={photo.id} id={photo.id} title={photo.title || finding.title} caption={`Hallazgo · ${formatDate(photo.capturedAt || photo.createdAt)}${photo.latitude != null && photo.longitude != null ? ` · GPS ${photo.latitude.toFixed(6)}, ${photo.longitude.toFixed(6)}` : ''}`} load={getInspectionFindingEvidenceBlob} />)}
</div>}
{photos.length === 0 && <small className="muted">Sin fotografías vinculadas al hallazgo.</small>}
</article>;
}
export function InspectionActMediaPanel({ actId }: { actId: string }) {
const [items, setItems] = useState<FindingWithPhotos[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
let active = true;
setLoading(true); setError('');
listInspectionFindings(actId)
.then(async (findings) => {
const records = await Promise.all(findings.map(async (finding) => ({
finding,
photos: (await listInspectionFindingEvidence(finding.id)).filter((evidence) => evidence.kind === 'PHOTO' && evidence.purpose === 'OBSERVATION'),
})));
if (active) setItems(records);
})
.catch((requestError) => active && setError(errorMessage(requestError)))
.finally(() => active && setLoading(false));
return () => { active = false; };
}, [actId]);
return <section className="panel act-media-panel">
<div className="panel-heading"><div><h2>Hallazgos del Acta</h2><p className="section-copy">El Acta muestra únicamente Hallazgos y la evidencia fotográfica vinculada a cada uno.</p></div><span className="count-pill">{items.length}</span></div>
{error && <Alert>{error}</Alert>}
{loading ? <LoadingBlock label="Cargando hallazgos y fotografías…" /> : items.length ?
<div className="act-finding-list">{items.map((item) => <Finding key={item.finding.id} item={item} />)}</div> :
<EmptyState title="Sin hallazgos" text="Esta Acta no contiene hallazgos sincronizados." />}
</section>;
}