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

127 lines
7.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 { SearchableSelect } from '../components/SearchableSelect';
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 { inspectionActStatusClass, inspectionActStatusLabel } from '../features/inspections/inspectionActPresentation';
import { OperationalFilters } from '../features/inspections/OperationalFilters';
import type { PageMeta } from '../lib/api';
import {
listInspectionActsGlobalF4,
type InspectionActListItemF4,
type InspectionActStatusF4,
} from '../lib/inspectionActF4Api';
import { formatDate } from '../lib/format';
const statuses: Array<{ value: InspectionActStatusF4 | ''; label: string }> = [
{ value: '', label: 'Todos los estados' },
{ value: 'DRAFT', label: 'Borrador' },
{ value: 'LOCKED', label: 'Bloqueada · esperando manifestación' },
{ value: 'SEALED', label: 'Sellada' },
{ value: 'CANCELLED', label: 'Cancelada' },
{ value: 'READY', label: 'Lista para cerrar · legado' },
{ value: 'CLOSED', label: 'Cerrada · legado' },
{ value: 'RECTIFIED', label: 'Rectificada · legado' },
];
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}`;
}
export function ActsPage() {
const [params, setParams] = useSearchParams();
const operationalContext = useOperationalContext();
const [items, setItems] = useState<InspectionActListItemF4[]>([]);
const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 25, total: 0, totalPages: 0 });
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const search = params.get('search') ?? '';
const rawStatus = params.get('status') ?? '';
const status = statuses.some((item) => item.value === rawStatus)
? rawStatus as InspectionActStatusF4 | ''
: '';
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);
useEffect(() => {
setLoading(true);
setError('');
listInspectionActsGlobalF4({ page, pageSize: 25, search, status, year: year ? Number(year) : '', companyId, areaId, inspectorId, dateFrom, dateTo })
.then((response) => {
setItems(response.data);
setMeta(response.meta);
})
.catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false));
}, [page, search, status, 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);
};
return <section>
<div className="page-heading">
<div><span className="eyebrow">CENTRO DOCUMENTAL</span><h1>Actas</h1><p>Consulta global de actas con acceso a la inspección, hallazgos, inventario e informe relacionado.</p></div>
</div>
<DocumentCenterTabs />
<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 acta, inspección, empresa, área o elemento" /><button>Buscar</button></label>
<label className="select-field"><span>Estado</span><SearchableSelect value={status} onChange={(event) => setFilter('status', event.target.value)}>{statuses.map((item) => <option key={item.value || 'all'} value={item.value}>{item.label}</option>)}</SearchableSelect></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>
{error && <Alert>{error}</Alert>}
{loading ? <LoadingBlock label="Cargando actas…" /> : items.length === 0 ? <EmptyState title="Sin actas" text="No hay actas para los filtros seleccionados." /> : <div className="table-panel document-table">
<div className="table-summary"><strong>{meta.total} acta{meta.total === 1 ? '' : 's'}</strong><span>Página {page} de {Math.max(meta.totalPages, 1)}</span></div>
<div className="table-scroll"><table><thead><tr><th>Acta</th><th>Empresa / área</th><th>Inspección</th><th>Hallazgos</th><th>Estado</th><th>Informe</th><th /></tr></thead><tbody>{items.map((act) => <tr key={act.id}>
<td><div className="document-primary"><strong>{act.code}</strong><small>{act.title} · {formatDate(act.occurredAt)}</small></div></td>
<td><div className="document-primary"><strong>{contextLabel(act.companies, 'Empresa sin asignar')}</strong><small>{contextLabel(act.areas, 'Área sin asignar')}</small></div></td>
<td><Link className="text-link" to={`/inspecciones/${act.visitId}`}>{act.visit.code}</Link></td>
<td><Link className="text-link" to={`/hallazgos?search=${encodeURIComponent(act.code)}`}>{act.findingCount}</Link></td>
<td><span className={`status-badge ${inspectionActStatusClass(act.status)}`}>{inspectionActStatusLabel(act.status)}</span></td>
<td>{act.report ? <Link className="text-link" to={`/informes/${act.report.id}`}>{act.report.code}<small className="block-muted">{act.report.pdfStatus === 'READY' ? 'PDF disponible' : 'PDF pendiente'}</small></Link> : ['SEALED', 'CLOSED'].includes(act.status) ? <span className="status-badge pending">Pendiente de emisión</span> : <span className="muted"></span>}</td>
<td className="action-cell"><Link className="icon-button" to={`/inspecciones/actas/${act.id}`} aria-label={`Abrir ${act.code}`}><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>;
}