F4: align report list with INF GEDO workflow
This commit is contained in:
@@ -6,8 +6,15 @@ import { Icon } from '../components/Icon';
|
||||
import { useOperationalContext } from '../context/OperationalContext';
|
||||
import { DocumentCenterTabs } from '../features/documents/DocumentCenterTabs';
|
||||
import { OperationalFilters } from '../features/inspections/OperationalFilters';
|
||||
import { listInspectionReports, listPendingInspectionReports } from '../lib/api';
|
||||
import type { InspectionReportListItem, PageMeta, PendingInspectionReportItem } from '../lib/api';
|
||||
import {
|
||||
listInspectionReportsF4,
|
||||
listPendingInspectionReportsF4,
|
||||
} from '../lib/reportWorkflowApi';
|
||||
import type {
|
||||
InspectionReportDetailF4,
|
||||
PageMetaF4,
|
||||
PendingInspectionReportF4,
|
||||
} from '../lib/reportWorkflowApi';
|
||||
import { formatDate } from '../lib/format';
|
||||
|
||||
function contextLabel(items: Array<{ name: string }>, empty: string): string {
|
||||
@@ -18,16 +25,16 @@ function contextLabel(items: Array<{ name: string }>, empty: string): string {
|
||||
return `${first.name} +${items.length - 1}`;
|
||||
}
|
||||
|
||||
|
||||
function reviewLabel(value: InspectionReportListItem['reviewStatus']): string {
|
||||
if (value === 'SIGNED') return 'Firmado';
|
||||
if (value === 'APPROVED') return 'Aprobado';
|
||||
return 'Pendiente';
|
||||
function statusLabel(report: InspectionReportDetailF4): string {
|
||||
if (report.status === 'OFFICIALIZED') return report.gedoIfIdentifier ? `GEDO · ${report.gedoIfIdentifier}` : 'Oficializado en GEDO';
|
||||
if (report.status === 'WORKING') return 'En preparación';
|
||||
if (report.status === 'CANCELLED') return 'Cancelado';
|
||||
return 'Legado congelado';
|
||||
}
|
||||
|
||||
function reviewClass(value: InspectionReportListItem['reviewStatus']): string {
|
||||
if (value === 'SIGNED') return 'active';
|
||||
if (value === 'APPROVED') return 'observed';
|
||||
function statusClass(report: InspectionReportDetailF4): string {
|
||||
if (report.status === 'OFFICIALIZED') return 'active';
|
||||
if (report.status === 'CANCELLED') return 'danger';
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
@@ -44,9 +51,9 @@ export function ReportsPage() {
|
||||
const dateTo = params.get('dateTo') ?? '';
|
||||
const page = Math.max(1, Number(params.get('page') ?? 1) || 1);
|
||||
const [draftSearch, setDraftSearch] = useState(search);
|
||||
const [issued, setIssued] = useState<InspectionReportListItem[]>([]);
|
||||
const [pending, setPending] = useState<PendingInspectionReportItem[]>([]);
|
||||
const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 25, total: 0, totalPages: 0 });
|
||||
const [issued, setIssued] = useState<InspectionReportDetailF4[]>([]);
|
||||
const [pending, setPending] = useState<PendingInspectionReportF4[]>([]);
|
||||
const [meta, setMeta] = useState<PageMetaF4>({ page: 1, pageSize: 25, total: 0, totalPages: 0 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -54,16 +61,27 @@ export function ReportsPage() {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
const query = {
|
||||
page,
|
||||
pageSize: 25,
|
||||
search,
|
||||
year: year ? Number(year) : undefined,
|
||||
companyId,
|
||||
areaId,
|
||||
inspectorId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
};
|
||||
const load = async () => {
|
||||
try {
|
||||
if (view === 'issued') {
|
||||
const response = await listInspectionReports({ page, pageSize: 25, search, year: year ? Number(year) : '', companyId, areaId, inspectorId, dateFrom, dateTo });
|
||||
const response = await listInspectionReportsF4(query);
|
||||
if (cancelled) return;
|
||||
setIssued(response.data);
|
||||
setPending([]);
|
||||
setMeta(response.meta);
|
||||
} else {
|
||||
const response = await listPendingInspectionReports({ page, pageSize: 25, search, year: year ? Number(year) : '', companyId, areaId, inspectorId, dateFrom, dateTo });
|
||||
const response = await listPendingInspectionReportsF4(query);
|
||||
if (cancelled) return;
|
||||
setPending(response.data);
|
||||
setIssued([]);
|
||||
@@ -110,39 +128,39 @@ export function ReportsPage() {
|
||||
|
||||
return <section>
|
||||
<div className="page-heading">
|
||||
<div><span className="eyebrow">CENTRO DOCUMENTAL</span><h1>Informes</h1><p>Registro global de informes de inspección generados automáticamente al cerrar cada visita.</p></div>
|
||||
<div><span className="eyebrow">CENTRO DOCUMENTAL</span><h1>Informes</h1><p>Cada Acta sellada genera un único INF editable. Luego el IF y PDF oficial de GEDO completan su trazabilidad institucional.</p></div>
|
||||
</div>
|
||||
<DocumentCenterTabs />
|
||||
|
||||
<div className="document-view-tabs">
|
||||
<button type="button" className={view === 'issued' ? 'active' : ''} onClick={() => setFilter('view', 'issued')}>Informes emitidos</button>
|
||||
<button type="button" className={view === 'pending' ? 'active' : ''} onClick={() => setFilter('view', 'pending')}>Pendientes de emisión</button>
|
||||
<button type="button" className={view === 'issued' ? 'active' : ''} onClick={() => setFilter('view', 'issued')}>Informes</button>
|
||||
<button type="button" className={view === 'pending' ? 'active' : ''} onClick={() => setFilter('view', 'pending')}>Actas selladas sin INF</button>
|
||||
</div>
|
||||
|
||||
<form className="toolbar survey-toolbar" onSubmit={applySearch}>
|
||||
<label className="search-field"><Icon name="search" /><input value={draftSearch} onChange={(event) => setDraftSearch(event.target.value)} placeholder="Buscar informe, acta, inspección, empresa o área" /><button>Buscar</button></label>
|
||||
<label className="search-field"><Icon name="search" /><input value={draftSearch} onChange={(event) => setDraftSearch(event.target.value)} placeholder="Buscar INF, IF GEDO, acta, inspección, empresa o área" /><button>Buscar</button></label>
|
||||
<label className="select-field"><span>Año</span><input inputMode="numeric" value={year} onChange={(event) => setFilter('year', event.target.value.replace(/\D/g, '').slice(0, 4))} placeholder="Todos" /></label>
|
||||
<OperationalFilters inspectorId={inspectorId} dateFrom={dateFrom} dateTo={dateTo} onChange={setFilter} />
|
||||
</form>
|
||||
|
||||
{view === 'pending' && <div className="temporal-notice"><Icon name="clipboard" /><p><strong>Recuperación documental.</strong> Las actas históricas sin informe aparecen aquí. Desde D5.3.20 el informe se numera y congela automáticamente al cerrar la visita.</p></div>}
|
||||
{view === 'pending' && <div className="temporal-notice"><Icon name="clipboard" /><p><strong>Actas listas para generar INF.</strong> Esta bandeja sólo muestra Actas selladas que todavía no tienen su Informe 1 a 1.</p></div>}
|
||||
{error && <Alert>{error}</Alert>}
|
||||
{loading ? <LoadingBlock label={view === 'issued' ? 'Cargando informes…' : 'Cargando pendientes…'} /> : empty ? <EmptyState title={view === 'issued' ? 'Sin informes emitidos' : 'Sin informes pendientes'} text={view === 'issued' ? 'Todavía no hay informes emitidos para los filtros seleccionados.' : 'Todas las actas cerradas tienen su informe emitido.'} /> : <div className="table-panel document-table">
|
||||
<div className="table-summary"><strong>{meta.total} {view === 'issued' ? `informe${meta.total === 1 ? '' : 's'}` : `pendiente${meta.total === 1 ? '' : 's'}`}</strong><span>Página {page} de {Math.max(meta.totalPages, 1)}</span></div>
|
||||
<div className="table-scroll"><table><thead>{view === 'issued' ? <tr><th>Informe</th><th>Empresa / área</th><th>Acta</th><th>Hallazgos</th><th>Word</th><th>Revisión</th><th /></tr> : <tr><th>Acta cerrada</th><th>Empresa / área</th><th>Inspección</th><th>Hallazgos</th><th>Cierre</th><th /></tr>}</thead><tbody>{view === 'issued' ? issued.map((report) => <tr key={report.id}>
|
||||
{loading ? <LoadingBlock label={view === 'issued' ? 'Cargando informes…' : 'Cargando actas pendientes…'} /> : empty ? <EmptyState title={view === 'issued' ? 'Sin informes' : 'Sin actas pendientes'} text={view === 'issued' ? 'Todavía no hay informes para los filtros seleccionados.' : 'Todas las Actas selladas tienen su INF correspondiente.'} /> : <div className="table-panel document-table">
|
||||
<div className="table-summary"><strong>{meta.total} {view === 'issued' ? `informe${meta.total === 1 ? '' : 's'}` : `acta${meta.total === 1 ? '' : 's'} pendiente${meta.total === 1 ? '' : 's'}`}</strong><span>Página {page} de {Math.max(meta.totalPages, 1)}</span></div>
|
||||
<div className="table-scroll"><table><thead>{view === 'issued' ? <tr><th>Informe</th><th>Empresa / área</th><th>Acta</th><th>Hallazgos</th><th>Word</th><th>Estado</th><th /></tr> : <tr><th>Acta sellada</th><th>Empresa / área</th><th>Inspección</th><th>Hallazgos</th><th>Sellado</th><th /></tr>}</thead><tbody>{view === 'issued' ? issued.map((report) => <tr key={report.id}>
|
||||
<td><div className="document-primary"><strong>{report.code}</strong><small>{report.title} · {formatDate(report.generatedAt)}</small></div></td>
|
||||
<td><div className="document-primary"><strong>{contextLabel(report.companies, 'Empresa sin asignar')}</strong><small>{contextLabel(report.areas, 'Área sin asignar')}</small></div></td>
|
||||
<td><Link className="text-link" to={`/inspecciones/actas/${report.actId}`}>{report.act.code}<small className="block-muted">{report.act.title}</small></Link></td>
|
||||
<td><Link className="text-link" to={`/hallazgos?search=${encodeURIComponent(report.act.code)}`}>{report.findingCount}</Link></td>
|
||||
<td><span className={`status-badge ${report.wordStatus === 'READY' ? 'active' : report.wordStatus === 'FAILED' ? 'danger' : 'pending'}`}>{report.wordStatus === 'READY' ? 'Word disponible' : report.wordStatus === 'FAILED' ? 'Word con error' : 'Word pendiente'}</span></td>
|
||||
<td><span className={`status-badge ${reviewClass(report.reviewStatus)}`}>{reviewLabel(report.reviewStatus)}</span></td>
|
||||
<td><span className={`status-badge ${report.wordStatus === 'READY' ? 'active' : report.wordStatus === 'FAILED' ? 'danger' : 'pending'}`}>{report.wordStatus === 'READY' ? 'Disponible' : report.wordStatus === 'FAILED' ? 'Error' : 'Pendiente'}</span></td>
|
||||
<td><span className={`status-badge ${statusClass(report)}`}>{statusLabel(report)}</span></td>
|
||||
<td className="action-cell"><Link className="icon-button" to={`/informes/${report.id}`} aria-label={`Abrir ${report.code}`}><Icon name="chevron" /></Link></td>
|
||||
</tr>) : pending.map((item) => <tr key={item.actId}>
|
||||
<td><div className="document-primary"><strong>{item.actCode}</strong><small>{item.actTitle} · {formatDate(item.occurredAt)}</small></div></td>
|
||||
<td><div className="document-primary"><strong>{contextLabel(item.companies, 'Empresa sin asignar')}</strong><small>{contextLabel(item.areas, 'Área sin asignar')}</small></div></td>
|
||||
<td><Link className="text-link" to={`/inspecciones/${item.visitId}`}>{item.visitCode}</Link></td>
|
||||
<td><Link className="text-link" to={`/inspecciones/${item.visitId}`}>{item.visitCode}<small className="block-muted">{item.visitTitle}</small></Link></td>
|
||||
<td><Link className="text-link" to={`/hallazgos?search=${encodeURIComponent(item.actCode)}`}>{item.findingCount}</Link></td>
|
||||
<td>{formatDate(item.closedAt)}</td>
|
||||
<td>{formatDate(item.sealedAt)}</td>
|
||||
<td className="action-cell"><Link className="icon-button" to={`/inspecciones/actas/${item.actId}`} aria-label={`Abrir ${item.actCode}`}><Icon name="chevron" /></Link></td>
|
||||
</tr>)}</tbody></table></div>
|
||||
<div className="pagination"><button className="button secondary" disabled={page <= 1} onClick={() => setPage(page - 1)}>Anterior</button><span>{meta.total ? `${(page - 1) * meta.pageSize + 1}–${Math.min(page * meta.pageSize, meta.total)} de ${meta.total}` : '0 resultados'}</span><button className="button secondary" disabled={page >= meta.totalPages} onClick={() => setPage(page + 1)}>Siguiente</button></div>
|
||||
|
||||
Reference in New Issue
Block a user