chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
+95
View File
@@ -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>;
}