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

96 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 } from 'react-router';
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback';
import { Icon } from '../components/Icon';
import { AssetCenterTabs } from '../features/assets/AssetCenterTabs';
import { assetStatusLabel, ASSET_STATUSES } from '../features/assets/assetPresentation';
import { AssetVersionDrawer } from '../features/assets/AssetVersionDrawer';
import { assetVersionChangeLabel } from '../features/assets/assetVersionPresentation';
import { getTemporalAsset, listAssetTypes, listTemporalAssets } from '../lib/api';
import type {
AssetInformationStatus,
AssetType,
PageMeta,
TemporalAssetDetail,
TemporalAssetSummary,
} from '../lib/api';
import { formatDate } from '../lib/format';
function localDateTime(date: Date): string {
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
return local.toISOString().slice(0, 16);
}
interface TemporalFilters {
search: string;
typeId: string;
status: AssetInformationStatus | '';
}
const emptyFilters: TemporalFilters = { search: '', typeId: '', status: '' };
export function TemporalAssetsPage() {
const initialDate = new Date();
const [draftAt, setDraftAt] = useState(localDateTime(initialDate));
const [at, setAt] = useState(initialDate.toISOString());
const [draft, setDraft] = useState<TemporalFilters>(emptyFilters);
const [filters, setFilters] = useState<TemporalFilters>(emptyFilters);
const [assets, setAssets] = useState<TemporalAssetSummary[]>([]);
const [types, setTypes] = useState<AssetType[]>([]);
const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 25, total: 0, totalPages: 0 });
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [detail, setDetail] = useState<TemporalAssetDetail | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
useEffect(() => { listAssetTypes().then(setTypes).catch(() => undefined); }, []);
useEffect(() => {
setLoading(true); setError('');
listTemporalAssets({ at, page, pageSize: 25, ...filters })
.then((response) => { setAssets(response.data); setMeta(response.meta); })
.catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false));
}, [at, filters, page]);
const apply = (event: FormEvent) => {
event.preventDefault();
const selected = new Date(draftAt);
if (Number.isNaN(selected.getTime())) { setError('Seleccioná una fecha y hora válida.'); return; }
setPage(1);
setAt(selected.toISOString());
setFilters({ ...draft, search: draft.search.trim() });
};
const reset = () => {
const now = new Date();
setDraftAt(localDateTime(now)); setAt(now.toISOString());
setDraft(emptyFilters); setFilters(emptyFilters); setPage(1);
};
const open = async (asset: TemporalAssetSummary) => {
setDetail(null); setDetailLoading(true); setError('');
try { setDetail(await getTemporalAsset(asset.assetId, at)); }
catch (requestError) { setError(errorMessage(requestError)); }
finally { setDetailLoading(false); }
};
return <section>
<div className="page-heading"><div><span className="eyebrow">INVENTARIOS</span><h1>Registros</h1><p>Reconstruí cómo se encontraban los inventarios en una fecha y hora.</p></div><span className="count-pill large">{meta.total} registros</span></div>
<AssetCenterTabs active="temporal" />
<div className="temporal-notice"><Icon name="history" /><p><strong>Vista histórica exacta.</strong> Los resultados provienen de snapshots inmutables; no reemplazan ni modifican los datos actuales.</p></div>
<form className="history-filters panel temporal-filters" onSubmit={apply}>
<label className="field temporal-date"><span>Fecha y hora de consulta</span><input type="datetime-local" value={draftAt} onChange={(event) => setDraftAt(event.target.value)} required /></label>
<label className="search-field"><Icon name="search" /><input value={draft.search} onChange={(event) => setDraft((current) => ({ ...current, search: event.target.value }))} placeholder="Código o nombre histórico" /></label>
<label className="field compact-field"><span>Tipo</span><SearchableSelect value={draft.typeId} onChange={(event) => setDraft((current) => ({ ...current, typeId: event.target.value }))}><option value="">Todos</option>{types.map((type) => <option key={type.id} value={type.id}>{type.name}</option>)}</SearchableSelect></label>
<label className="field compact-field"><span>Estado</span><SearchableSelect value={draft.status} onChange={(event) => setDraft((current) => ({ ...current, status: event.target.value as AssetInformationStatus | '' }))}><option value="">Todos</option>{ASSET_STATUSES.map((status) => <option key={status.value} value={status.value}>{status.label}</option>)}</SearchableSelect></label>
<div className="filter-actions"><button className="button text" type="button" onClick={reset}>Volver al presente</button><button className="button primary"><Icon name="history" />Reconstruir</button></div>
</form>
<div className="temporal-result-heading"><strong>Inventarios reconstruidos al {formatDate(at)}</strong><span>Las vigencias terminan cuando se registra la versión siguiente.</span></div>
{error && <Alert>{error}</Alert>}
{loading ? <LoadingBlock label="Reconstruyendo inventarios…" /> : assets.length === 0 ? <EmptyState title="Sin registros en esa fecha" text="No existen versiones históricas que coincidan con la fecha y los filtros seleccionados." /> : <div className="table-panel temporal-table"><div className="table-summary"><strong>{meta.total} registro{meta.total === 1 ? '' : 's'} reconstruido{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>Registro histórico</th><th>Tipo</th><th>Estado</th><th>Versión aplicable</th><th>Vigente desde</th><th>Vigente hasta</th><th>Cambio</th><th /></tr></thead><tbody>{assets.map((asset) => <tr key={asset.assetId}><td><Link className="history-asset-link" to={`/inventarios/${asset.assetId}`}><strong>{asset.assetName}</strong><small>{asset.assetCode}</small></Link></td><td><span className="tag">{asset.typeName}</span></td><td>{assetStatusLabel(asset.informationStatus)}</td><td><span className={`version-badge ${asset.isCurrent ? 'current' : ''}`}>v{asset.versionNumber}{asset.isCurrent ? ' · actual' : ''}</span></td><td>{formatDate(asset.occurredAt)}</td><td>{asset.effectiveUntil ? formatDate(asset.effectiveUntil) : 'Continúa vigente'}</td><td>{assetVersionChangeLabel(asset.changeType)}</td><td><button type="button" className="icon-button" onClick={() => open(asset)} aria-label={`Ver versión ${asset.versionNumber}`}><Icon name="chevron" /></button></td></tr>)}</tbody></table></div><div className="pagination"><button type="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 type="button" className="button secondary" disabled={page >= meta.totalPages} onClick={() => setPage(page + 1)}>Siguiente</button></div></div>}
{(detailLoading || detail) && <AssetVersionDrawer detail={detail} loading={detailLoading} onClose={() => setDetail(null)} />}
</section>;
}