Files
dh-inspeccion-v2/web-v2/src/pages/ReportsPage.tsx
T

170 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import type { FormEvent } from 'react';
import { Link, useSearchParams } from 'react-router';
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback';
import { Icon } from '../components/Icon';
import { useOperationalContext } from '../context/OperationalContext';
import { DocumentCenterTabs } from '../features/documents/DocumentCenterTabs';
import { OperationalFilters } from '../features/inspections/OperationalFilters';
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 {
if (items.length === 0) return empty;
const first = items[0];
if (!first) return empty;
if (items.length === 1) return first.name;
return `${first.name} +${items.length - 1}`;
}
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 statusClass(report: InspectionReportDetailF4): string {
if (report.status === 'OFFICIALIZED') return 'active';
if (report.status === 'CANCELLED') return 'danger';
return 'pending';
}
export function ReportsPage() {
const [params, setParams] = useSearchParams();
const operationalContext = useOperationalContext();
const view = params.get('view') === 'pending' ? 'pending' : 'issued';
const search = params.get('search') ?? '';
const year = params.get('year') ?? '';
const companyId = operationalContext.companyId || params.get('companyId') || '';
const areaId = operationalContext.areaId || params.get('areaId') || '';
const inspectorId = params.get('inspectorId') ?? '';
const dateFrom = params.get('dateFrom') ?? '';
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<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('');
useEffect(() => {
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 listInspectionReportsF4(query);
if (cancelled) return;
setIssued(response.data);
setPending([]);
setMeta(response.meta);
} else {
const response = await listPendingInspectionReportsF4(query);
if (cancelled) return;
setPending(response.data);
setIssued([]);
setMeta(response.meta);
}
} catch (requestError) {
if (!cancelled) setError(errorMessage(requestError));
} finally {
if (!cancelled) setLoading(false);
}
};
void load();
return () => { cancelled = true; };
}, [page, search, view, year, companyId, areaId, inspectorId, dateFrom, dateTo]);
const setFilter = (key: string, value: string) => {
const next = new URLSearchParams(params);
if (key === 'areaId') {
operationalContext.setAreaId(value);
next.delete('areaId');
next.delete('companyId');
} else if (key === 'companyId') {
operationalContext.setCompanyId(value);
next.delete('companyId');
} else {
value ? next.set(key, value) : next.delete(key);
}
next.delete('page');
setParams(next);
};
const applySearch = (event: FormEvent) => {
event.preventDefault();
setFilter('search', draftSearch.trim());
};
const setPage = (value: number) => {
const next = new URLSearchParams(params);
value > 1 ? next.set('page', String(value)) : next.delete('page');
setParams(next);
};
const empty = view === 'issued' ? issued.length === 0 : pending.length === 0;
return <section>
<div className="page-heading">
<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</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 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>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 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' ? '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={`/hallazgos?search=${encodeURIComponent(item.actCode)}`}>{item.findingCount}</Link></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>
</div>}
</section>;
}