Files
dh-inspeccion-v2/web-v2/src/pages/InspectionVisitsPage.tsx
T
admin 35d4630581 F5 · Inventario operativo, territorio y catálogo autorizado (#25)
* fix(web): simplify inventory administration menu

* fix(web): remove legacy imports and function catalog routes

* fix(web): remove redundant inspections lifecycle legend

* feat(inventory): distinguish physical instances from structural records

* fix(dashboard): align inventory and act follow-up metrics

* fix(web): align dashboard summary contract

* fix(web): clarify dashboard act and report concepts

* feat(inventory): mark field-created records as real instances

* feat(inventory): map physical instance flag on asset entity

* fix(inventory): keep field yacimientos structural

* feat(inventory): classify future concrete instances at database level

* fix(inventory): count only installation and subinstallation instances

* feat(inventory): add authoritative F5 source snapshot

* feat(inventory): preload authoritative territory model

* feat(inventory): preload authoritative technical catalog

* fix(findings): use only authoritative F5 family catalog

* fix(inventory): preserve non-hierarchical operator snapshot compatibility

* feat(inventory): add inventory-only asset filter

* feat(inventory): add inventory-only tree filter

* feat(inventory): add inventory browser query contract

* feat(inventory): add area-owned inventory browser

* feat(inventory): expose area-owned inventory browser

* refactor(inventory): remove function catalog and add inventory browser

* fix(inventory): make operator relation temporal and non-owning

* feat(web): add inventory browser API client

* feat(inventory): extend inventory browser filters

* feat(inventory): add real inventory list endpoint logic

* feat(inventory): expose real inventory list

* feat(web): add real inventory list client

* refactor(web): make inventory hierarchy area-owned

* fix(web): show only real inventory instances

* fix(web): style act follow-up tabs and F5 inventory context

* fix(web): load F5 flow styles

* fix(inventory): apply area-owned operational guard on F5 up

* fix(inventory): treat company on asset as non-owning creation snapshot

* fix(inventory): resolve field inventory by area hierarchy, not company ownership

* fix(inventory): preserve custom catalog and apply authoritative universal findings

* fix(inventory): harden authoritative catalog migration checks

* fix(inventory): make authoritative territory preload safely reversible

* feat(inventory): allow independent company master creation

* fix(inventory): make guided creation area-owned and support companies

* feat(web): expose independent company master in inventory setup

* feat(web): create companies independently from physical inventory hierarchy

* fix(inventory): merge by physical area and preserve sealed documents

* test(inventory): lock F5 authoritative model and merge invariants

* feat(inventory): add family administration DTOs

* feat(inventory): administer installation and subinstallation classifications

* feat(inventory): expose family classification administration

* feat(web): add inventory classification administration API

* fix(web): configure finding applicability by inventory classification

* fix(web): redefine inventory configuration around hierarchy classifications and columns

* chore(release): identify F5 inventory model

* chore(release): bump API for F5 inventory model

* test(release): expect F5 health metadata

* chore(release): align WEB package with F5 inventory cut

* chore(release): expose F5 WEB phase

* test(dashboard): expect inspector activity and act follow-up metrics

* test(dashboard): route F5 summary query mocks explicitly

* ci: rehearse all migrations on clean PostGIS before merge

* ci: prove F5 migrations revert and reapply cleanly

* test(f5): align operational navigation contract

* test(f5): align operator lifecycle with area-owned inventory

* test(f5): make merge compatibility area-based

* test(f5): distinguish literal and normalized yacimiento counts

* test(f5): model normalized yacimiento collision explicitly

* ci(f5): bootstrap historical admin prerequisite in clean migration rehearsal

* ci(f5): bypass irreversible historical reset in clean rehearsal

* fix(f5): make territory SQL parameter types explicit

* fix(f5): guarantee canonical inventory hierarchy before territory preload

* ci(f5): include canonical hierarchy migration in rollback gate

* fix(f5): type relation backup markers explicitly

* fix(f5): make catalog SQL text parameter types explicit
2026-09-08 23:18:15 -03:00

112 lines
7.2 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 { PermissionGate } from '../auth/PermissionGate';
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback';
import { Icon } from '../components/Icon';
import { useOperationalContext } from '../context/OperationalContext';
import { OperationalFilters } from '../features/inspections/OperationalFilters';
import {
INSPECTION_VISIT_STATUSES,
inspectionStatusClass,
inspectionVisitStatusLabel,
} from '../features/inspections/inspectionPresentation';
import { listInspectionVisits } from '../lib/api';
import type {
InspectionVisitListItem,
InspectionVisitStatus,
PageMeta,
} from '../lib/api';
import { formatDate } from '../lib/format';
const statusTabs: Array<{ value: InspectionVisitStatus | ''; label: string }> = [
{ value: '', label: 'Todas' },
...INSPECTION_VISIT_STATUSES,
];
export function InspectionVisitsPage() {
const [urlParams, setUrlParams] = useSearchParams();
const context = useOperationalContext();
const [visits, setVisits] = useState<InspectionVisitListItem[]>([]);
const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 25, total: 0, totalPages: 0 });
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [draftSearch, setDraftSearch] = useState(urlParams.get('search') ?? '');
const search = urlParams.get('search') ?? '';
const rawStatus = urlParams.get('status') ?? '';
const status = INSPECTION_VISIT_STATUSES.some((item) => item.value === rawStatus)
? rawStatus as InspectionVisitStatus
: '';
const companyId = context.companyId || urlParams.get('companyId') || '';
const areaId = context.areaId || urlParams.get('areaId') || '';
const inspectorId = urlParams.get('inspectorId') ?? '';
const dateFrom = urlParams.get('dateFrom') ?? '';
const dateTo = urlParams.get('dateTo') ?? '';
const page = Math.max(1, Number(urlParams.get('page') ?? 1) || 1);
useEffect(() => {
setLoading(true);
setError('');
listInspectionVisits({ page, pageSize: 25, search, status, companyId, areaId, inspectorId, dateFrom, dateTo })
.then((response) => {
setVisits(response.data);
setMeta(response.meta);
})
.catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false));
}, [page, search, status, companyId, areaId, inspectorId, dateFrom, dateTo]);
const updateFilter = (key: string, value: string) => {
const next = new URLSearchParams(urlParams);
if (key === 'areaId') {
context.setAreaId(value);
next.delete('areaId');
next.delete('companyId');
} else if (key === 'companyId') {
context.setCompanyId(value);
next.delete('companyId');
} else {
value ? next.set(key, value) : next.delete(key);
}
next.delete('page');
setUrlParams(next);
};
const applySearch = (event: FormEvent) => {
event.preventDefault();
updateFilter('search', draftSearch.trim());
};
const setPage = (value: number) => {
const next = new URLSearchParams(urlParams);
value > 1 ? next.set('page', String(value)) : next.delete('page');
setUrlParams(next);
};
const statusHref = (value: InspectionVisitStatus | '') => {
const next = new URLSearchParams(urlParams);
value ? next.set('status', value) : next.delete('status');
next.delete('page');
return `/inspecciones${next.size ? `?${next}` : ''}`;
};
return <section>
<div className="page-heading">
<div><span className="eyebrow">OPERACIÓN</span><h1>Inspecciones</h1><p>Planificá, seguí y consultá todo el ciclo de una inspección desde un único lugar.</p></div>
<PermissionGate permission="inspections.manage"><PermissionGate permission="inspections.assign"><Link className="button primary" to="/inspecciones/nueva"><Icon name="plus" />Planificar inspección</Link></PermissionGate></PermissionGate>
</div>
<nav className="inspection-status-tabs" aria-label="Estados de inspección">
{statusTabs.map((item) => <Link key={item.value || 'all'} className={status === item.value ? 'active' : ''} to={statusHref(item.value)}>{item.label}</Link>)}
</nav>
<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 por código" /><button>Buscar</button></label>
<OperationalFilters inspectorId={inspectorId} dateFrom={dateFrom} dateTo={dateTo} onChange={updateFilter} />
</form>
{error && <Alert>{error}</Alert>}
{loading ? <LoadingBlock label="Cargando inspecciones…" /> : visits.length === 0 ? <EmptyState title="Sin inspecciones" text="No hay inspecciones que coincidan con el estado y contexto seleccionados." /> : <div className="table-panel"><div className="table-summary"><strong>{meta.total} inspección{meta.total === 1 ? '' : 'es'}</strong><span>Página {page} de {Math.max(meta.totalPages, 1)}</span></div><div className="table-scroll"><table><thead><tr><th>Inspección</th><th>Estado</th><th>Área / Operadora</th><th>Responsable</th><th>Fecha prevista</th><th>Inventario</th><th /></tr></thead><tbody>{visits.map((visit) => <tr key={visit.id}><td><div className="asset-cell"><span className="asset-symbol"><Icon name="clipboard" size={17} /></span><div><strong>{visit.code}</strong><small>{visit.operationalArea?.name ?? 'Sin Área'} · {visit.operatorCompany?.name ?? 'Sin Operadora'}</small></div></div></td><td><span className={`status-badge ${inspectionStatusClass(visit.status)}`}>{inspectionVisitStatusLabel(visit.status)}</span></td><td>{visit.operationalArea || visit.operatorCompany ? <span><strong className="table-primary">{visit.operationalArea?.name ?? 'Sin Área'}</strong><small className="cell-subtext">{visit.operatorCompany?.name ?? 'Sin Operadora'}</small></span> : <span className="muted">Sin contexto</span>}</td><td>{visit.leadInspector ? `${visit.leadInspector.firstName} ${visit.leadInspector.lastName}` : 'Sin asignar'}</td><td><span className="survey-date-range">{formatDate(visit.plannedStartAt)}<small>inicio planificado</small></span></td><td>{visit.scopeAsset ? <Link className="text-link" to={`/inventarios/${visit.scopeAsset.id}`}>{visit.scopeAsset.name}<small className="block-muted">{visit.scopeAsset.code}</small></Link> : <><strong>{visit.assetCount} registro{visit.assetCount === 1 ? '' : 's'}</strong><small className="block-muted">{visit.memberCount} integrante{visit.memberCount === 1 ? '' : 's'}</small></>}</td><td className="action-cell"><Link className="icon-button" to={`/inspecciones/${visit.id}`} aria-label={`Abrir ${visit.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>;
}