chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user