chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../../components/Feedback';
|
||||
import { Icon } from '../../components/Icon';
|
||||
import { formatDate, formatDateOnly } from '../../lib/format';
|
||||
import { getAssetDossier } from '../../lib/api';
|
||||
import type { AssetDossier, AssetDossierTimelineEvent } from '../../lib/api';
|
||||
|
||||
type View = 'timeline' | 'findings' | 'documents';
|
||||
|
||||
function findingStatusLabel(status: string) {
|
||||
if (status === 'OPEN') return 'Abierto';
|
||||
if (status === 'CLOSED') return 'Cerrado';
|
||||
return 'Anulado';
|
||||
}
|
||||
|
||||
function inspectionStatusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
DRAFT: 'Borrador', PLANNED: 'Planificada', IN_PROGRESS: 'En curso', CLOSED: 'Cerrada', CANCELLED: 'Cancelada',
|
||||
};
|
||||
return labels[status] ?? status;
|
||||
}
|
||||
|
||||
function actStatusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
DRAFT: 'Borrador', READY: 'Lista', CLOSED: 'Cerrada', CANCELLED: 'Cancelada', RECTIFIED: 'Rectificada',
|
||||
};
|
||||
return labels[status] ?? status;
|
||||
}
|
||||
|
||||
function timelineLabel(event: AssetDossierTimelineEvent) {
|
||||
const labels: Record<AssetDossierTimelineEvent['kind'], string> = {
|
||||
INVENTORY_CHANGE: 'Inventario', INSPECTION: 'Inspección', ACT: 'Acta', FINDING: 'Hallazgo', FINDING_CLOSED: 'Cierre',
|
||||
COMMUNICATION: 'Seguimiento', PHOTO: 'Fotografía', DOCUMENT: 'Documento', REPORT: 'Informe', SOURCE_DOCUMENT: 'Documento fuente', VERIFICATION: 'Verificación',
|
||||
};
|
||||
return labels[event.kind];
|
||||
}
|
||||
|
||||
function timelineIcon(event: AssetDossierTimelineEvent) {
|
||||
if (event.kind === 'FINDING' || event.kind === 'FINDING_CLOSED') return 'alert' as const;
|
||||
if (event.kind === 'INSPECTION' || event.kind === 'VERIFICATION') return 'calendar' as const;
|
||||
if (event.kind === 'INVENTORY_CHANGE') return 'history' as const;
|
||||
return 'clipboard' as const;
|
||||
}
|
||||
|
||||
export function AssetDossierPanel({ assetId }: { assetId: string }) {
|
||||
const [dossier, setDossier] = useState<AssetDossier | null>(null);
|
||||
const [view, setView] = useState<View>('timeline');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
getAssetDossier(assetId)
|
||||
.then(setDossier)
|
||||
.catch((requestError) => setError(errorMessage(requestError)))
|
||||
.finally(() => setLoading(false));
|
||||
}, [assetId]);
|
||||
|
||||
const pendingVerification = useMemo(() => dossier?.findings.filter((finding) => finding.status === 'OPEN' && finding.companyResponseReceivedOn && finding.nextControlOn).length ?? 0, [dossier]);
|
||||
|
||||
if (loading) return <div className="panel"><LoadingBlock label="Armando expediente técnico…" /></div>;
|
||||
if (!dossier) return <Alert>{error || 'No se pudo cargar el expediente.'}</Alert>;
|
||||
|
||||
return <div className="asset-dossier-stack">
|
||||
{error && <Alert>{error}</Alert>}
|
||||
|
||||
<article className="panel dossier-overview">
|
||||
<div className="panel-heading">
|
||||
<div><span className="eyebrow">EXPEDIENTE TÉCNICO</span><h2>{dossier.asset.name}</h2><p className="section-copy">{dossier.asset.code}{dossier.asset.commonName ? ` · Nombre habitual: ${dossier.asset.commonName}` : ''} · Inspecciones, actas, hallazgos, respuestas, evidencias, documentos y cambios reunidos en un solo lugar.</p></div>
|
||||
</div>
|
||||
<div className="dossier-metrics">
|
||||
<div><small>Inspecciones</small><strong>{dossier.counters.inspections}</strong></div>
|
||||
<div><small>Actas</small><strong>{dossier.counters.acts}</strong></div>
|
||||
<div className={dossier.counters.openFindings ? 'attention' : ''}><small>Hallazgos abiertos</small><strong>{dossier.counters.openFindings}</strong></div>
|
||||
<div><small>Hallazgos cerrados</small><strong>{dossier.counters.closedFindings}</strong></div>
|
||||
<div><small>Fotos</small><strong>{dossier.counters.photos}</strong></div>
|
||||
<div><small>Informes</small><strong>{dossier.counters.reports}</strong></div>
|
||||
<div><small>Verificaciones</small><strong>{dossier.counters.verifications}</strong></div>
|
||||
</div>
|
||||
{pendingVerification > 0 && <div className="temporal-notice"><Icon name="calendar" /><p><strong>{pendingVerification} hallazgo{pendingVerification === 1 ? '' : 's'} con verificación programada.</strong> Las fechas operativas pueden utilizarse para planificar próximos controles.</p></div>}
|
||||
</article>
|
||||
|
||||
<nav className="dossier-subtabs" aria-label="Vistas del expediente">
|
||||
<button type="button" className={view === 'timeline' ? 'active' : ''} onClick={() => setView('timeline')}>Cronología</button>
|
||||
<button type="button" className={view === 'findings' ? 'active' : ''} onClick={() => setView('findings')}>Inspecciones y hallazgos</button>
|
||||
<button type="button" className={view === 'documents' ? 'active' : ''} onClick={() => setView('documents')}>Documentos y evidencias</button>
|
||||
</nav>
|
||||
|
||||
{view === 'timeline' && <article className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">CRONOLOGÍA</span><h2>Línea de tiempo</h2><p className="section-copy">Cada evento conserva su origen y enlaza con el registro que lo generó cuando corresponde.</p></div><span className="count-pill">{dossier.timeline.length}</span></div>
|
||||
{dossier.timeline.length === 0 ? <EmptyState title="Sin actividad histórica" text="Los eventos aparecerán a medida que el elemento sea inspeccionado o actualizado." /> : <div className="dossier-timeline">
|
||||
{dossier.timeline.map((event) => <div className="dossier-timeline-item" key={event.id}>
|
||||
<span className="dossier-timeline-icon"><Icon name={timelineIcon(event)} size={16} /></span>
|
||||
<div className="dossier-timeline-body">
|
||||
<div className="dossier-timeline-head"><span>{timelineLabel(event)}</span><time>{formatDate(event.occurredAt)}</time></div>
|
||||
{event.href ? <Link to={event.href}><strong>{event.title}</strong></Link> : <strong>{event.title}</strong>}
|
||||
{event.description && <p>{event.description}</p>}
|
||||
</div>
|
||||
</div>)}
|
||||
</div>}
|
||||
</article>}
|
||||
|
||||
{view === 'findings' && <div className="asset-dossier-grid">
|
||||
<article className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">INSPECCIONES</span><h2>Visitas relacionadas</h2></div><span className="count-pill">{dossier.visits.length}</span></div>
|
||||
{dossier.visits.length === 0 ? <EmptyState title="Sin inspecciones" text="Todavía no hay visitas asociadas a este elemento." /> : <div className="dossier-link-list">{dossier.visits.map((visit) => <Link key={visit.id} to={`/inspecciones/${visit.id}`}><div><strong>{visit.code}</strong><small>{inspectionStatusLabel(visit.status)}</small></div><span>{formatDate(visit.actualStartedAt ?? visit.plannedStartAt ?? visit.createdAt)}</span><Icon name="chevron" size={15} /></Link>)}</div>}
|
||||
</article>
|
||||
|
||||
<article className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">ACTAS</span><h2>Actas relacionadas</h2></div><span className="count-pill">{dossier.acts.length}</span></div>
|
||||
{dossier.acts.length === 0 ? <EmptyState title="Sin actas" text="Todavía no hay actas asociadas a este elemento." /> : <div className="dossier-link-list">{dossier.acts.map((act) => <Link key={act.id} to={`/inspecciones/actas/${act.id}`}><div><strong>{act.code}</strong><small>{act.title} · {actStatusLabel(act.status)}</small></div><span>{formatDate(act.occurredAt)}</span><Icon name="chevron" size={15} /></Link>)}</div>}
|
||||
</article>
|
||||
|
||||
<article className="panel dossier-wide">
|
||||
<div className="panel-heading"><div><span className="eyebrow">HALLAZGOS</span><h2>Seguimiento del elemento</h2></div><Link className="text-link" to="/hallazgos">Ver bandeja general <Icon name="chevron" size={14} /></Link></div>
|
||||
{dossier.findings.length === 0 ? <EmptyState title="Sin hallazgos" text="Este elemento todavía no registra hallazgos." /> : <div className="table-scroll"><table><thead><tr><th>Hallazgo</th><th>Estado</th><th>Acta</th><th>Vencimiento empresa</th><th>Verificación</th><th /></tr></thead><tbody>{dossier.findings.map((finding) => <tr key={finding.id}><td><strong>{finding.title}</strong><small className="block-muted">{finding.code}</small></td><td><span className={`status-badge ${finding.status === 'OPEN' ? 'observed' : 'active'}`}>{findingStatusLabel(finding.status)}</span></td><td><Link className="text-link" to={`/inspecciones/actas/${finding.actId}`}>{finding.actCode}</Link></td><td>{formatDateOnly(finding.correctionDueOn)}</td><td>{formatDateOnly(finding.nextControlOn)}</td><td className="action-cell"><Link className="icon-button" to={`/hallazgos/${finding.id}`} aria-label={`Abrir ${finding.code}`}><Icon name="chevron" /></Link></td></tr>)}</tbody></table></div>}
|
||||
</article>
|
||||
</div>}
|
||||
|
||||
{view === 'documents' && <div className="asset-dossier-grid">
|
||||
<article className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">DOCUMENTOS</span><h2>Documentos vinculados</h2></div><span className="count-pill">{dossier.documents.length}</span></div>
|
||||
{dossier.documents.length === 0 ? <EmptyState title="Sin documentos fuente" text="No hay documentos fuente vinculados directamente a este registro." /> : <div className="dossier-document-list">{dossier.documents.map((document) => <div key={document.id}><Icon name="clipboard" /><div><strong>{document.title}</strong><small>{document.documentNumber ?? document.documentType} · {formatDateOnly(document.documentDate)}</small></div></div>)}</div>}
|
||||
</article>
|
||||
|
||||
<article className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">EVIDENCIAS</span><h2>Fotos y archivos de hallazgos</h2></div><span className="count-pill">{dossier.evidence.length}</span></div>
|
||||
{dossier.evidence.length === 0 ? <EmptyState title="Sin evidencias de inspección" text="Las fotos y documentos de hallazgos aparecerán aquí en orden histórico." /> : <div className="dossier-link-list">{dossier.evidence.map((item) => <Link key={item.id} to={`/hallazgos/${item.findingId}`}><div><strong>{item.title ?? item.originalName}</strong><small>{item.findingCode} · {item.kind === 'PHOTO' ? 'Fotografía' : 'Documento'}</small></div><span>{formatDate(item.capturedAt ?? item.createdAt)}</span><Icon name="chevron" size={15} /></Link>)}</div>}
|
||||
</article>
|
||||
|
||||
<article className="panel dossier-wide">
|
||||
<div className="panel-heading"><div><span className="eyebrow">INFORMES DE INSPECCIÓN</span><h2>Informes relacionados</h2><p className="section-copy">Informes emitidos desde inspecciones que incluyeron este elemento o un hallazgo asociado.</p></div><Link className="text-link" to="/informes">Ver centro de informes <Icon name="chevron" size={14} /></Link></div>
|
||||
{dossier.inspectionReports.length === 0 ? <EmptyState title="Sin informes de inspección" text="Todavía no hay informes emitidos para este elemento." /> : <div className="dossier-link-list">{dossier.inspectionReports.map((report) => <Link key={report.id} to={`/informes/${report.id}`}><div><strong>{report.code}</strong><small>{report.title} · {report.pdfStatus === 'READY' ? 'PDF disponible' : 'PDF pendiente'}</small></div><span>{formatDate(report.generatedAt)}</span><Icon name="chevron" size={15} /></Link>)}</div>}
|
||||
</article>
|
||||
|
||||
<article className="panel dossier-wide">
|
||||
<div className="panel-heading"><div><span className="eyebrow">DOCUMENTACIÓN TÉCNICA FUENTE</span><h2>Informes técnicos vinculados</h2></div><span className="count-pill">{dossier.reports.length}</span></div>
|
||||
{dossier.reports.length === 0 ? <EmptyState title="Sin informes técnicos fuente" text="No hay documentos fuente clasificados como informe técnico para este elemento." /> : <div className="dossier-document-list">{dossier.reports.map((report) => <div key={report.id}><Icon name="clipboard" /><div><strong>{report.title}</strong><small>{report.documentNumber ?? 'Informe técnico'} · {formatDateOnly(report.documentDate)}</small></div></div>)}</div>}
|
||||
</article>
|
||||
</div>}
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user