127 lines
7.5 KiB
TypeScript
127 lines
7.5 KiB
TypeScript
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>;
|
||
}
|