131 lines
7.6 KiB
TypeScript
131 lines
7.6 KiB
TypeScript
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 {
|
||
ASSET_VERSION_CHANGES,
|
||
assetVersionChangeLabel,
|
||
assetVersionFieldLabel,
|
||
} from '../features/assets/assetVersionPresentation';
|
||
import {
|
||
getAssetVersion,
|
||
listAssetTypes,
|
||
listAssetVersions,
|
||
} from '../lib/api';
|
||
import type {
|
||
AssetInformationStatus,
|
||
AssetType,
|
||
AssetVersionChangeType,
|
||
AssetVersionDetail,
|
||
AssetVersionSummary,
|
||
PageMeta,
|
||
} from '../lib/api';
|
||
import { formatDate } from '../lib/format';
|
||
|
||
interface HistoryFilters {
|
||
search: string;
|
||
typeId: string;
|
||
status: AssetInformationStatus | '';
|
||
changeType: AssetVersionChangeType | '';
|
||
from: string;
|
||
to: string;
|
||
}
|
||
|
||
const emptyFilters: HistoryFilters = {
|
||
search: '',
|
||
typeId: '',
|
||
status: '',
|
||
changeType: '',
|
||
from: '',
|
||
to: '',
|
||
};
|
||
|
||
export function HistoryPage() {
|
||
const [versions, setVersions] = useState<AssetVersionSummary[]>([]);
|
||
const [types, setTypes] = useState<AssetType[]>([]);
|
||
const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 25, total: 0, totalPages: 0 });
|
||
const [draft, setDraft] = useState<HistoryFilters>(emptyFilters);
|
||
const [filters, setFilters] = useState<HistoryFilters>(emptyFilters);
|
||
const [page, setPage] = useState(1);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState('');
|
||
const [detail, setDetail] = useState<AssetVersionDetail | null>(null);
|
||
const [detailLoading, setDetailLoading] = useState(false);
|
||
|
||
useEffect(() => {
|
||
listAssetTypes().then(setTypes).catch(() => undefined);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
setLoading(true);
|
||
setError('');
|
||
listAssetVersions({
|
||
page,
|
||
pageSize: 25,
|
||
search: filters.search,
|
||
typeId: filters.typeId,
|
||
status: filters.status,
|
||
changeType: filters.changeType,
|
||
from: filters.from ? `${filters.from}T00:00:00.000Z` : undefined,
|
||
to: filters.to ? `${filters.to}T23:59:59.999Z` : undefined,
|
||
})
|
||
.then((response) => {
|
||
setVersions(response.data);
|
||
setMeta(response.meta);
|
||
})
|
||
.catch((requestError) => setError(errorMessage(requestError)))
|
||
.finally(() => setLoading(false));
|
||
}, [filters, page]);
|
||
|
||
const set = <K extends keyof HistoryFilters>(key: K, value: HistoryFilters[K]) => {
|
||
setDraft((current) => ({ ...current, [key]: value }));
|
||
};
|
||
const apply = (event: FormEvent) => {
|
||
event.preventDefault();
|
||
setPage(1);
|
||
setFilters({ ...draft, search: draft.search.trim() });
|
||
};
|
||
const clear = () => {
|
||
setDraft(emptyFilters);
|
||
setFilters(emptyFilters);
|
||
setPage(1);
|
||
};
|
||
const open = async (version: AssetVersionSummary) => {
|
||
setDetail(null);
|
||
setDetailLoading(true);
|
||
setError('');
|
||
try {
|
||
setDetail(await getAssetVersion(version.assetId, version.versionNumber));
|
||
} catch (requestError) {
|
||
setError(errorMessage(requestError));
|
||
} finally {
|
||
setDetailLoading(false);
|
||
}
|
||
};
|
||
|
||
return <section>
|
||
<div className="page-heading"><div><span className="eyebrow">INVENTARIOS</span><h1>Historial</h1><p>Historial completo de cambios realizados sobre los inventarios.</p></div><span className="count-pill large">{meta.total} versiones</span></div>
|
||
<AssetCenterTabs active="history" />
|
||
|
||
<form className="history-filters panel" onSubmit={apply}>
|
||
<label className="search-field"><Icon name="search" /><input value={draft.search} onChange={(event) => set('search', event.target.value)} placeholder="Código, registro o usuario" /></label>
|
||
<label className="field compact-field"><span>Tipo</span><SearchableSelect value={draft.typeId} onChange={(event) => set('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) => set('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>
|
||
<label className="field compact-field"><span>Cambio</span><SearchableSelect value={draft.changeType} onChange={(event) => set('changeType', event.target.value as AssetVersionChangeType | '')}><option value="">Todos</option>{ASSET_VERSION_CHANGES.map((change) => <option key={change.value} value={change.value}>{change.label}</option>)}</SearchableSelect></label>
|
||
<label className="field compact-field"><span>Desde</span><input type="date" value={draft.from} onChange={(event) => set('from', event.target.value)} /></label>
|
||
<label className="field compact-field"><span>Hasta</span><input type="date" value={draft.to} onChange={(event) => set('to', event.target.value)} /></label>
|
||
<div className="filter-actions"><button className="button text" type="button" onClick={clear}>Limpiar</button><button className="button primary">Aplicar filtros</button></div>
|
||
</form>
|
||
|
||
{error && <Alert>{error}</Alert>}
|
||
{loading ? <LoadingBlock label="Consultando versiones…" /> : versions.length === 0 ? <EmptyState title="Sin versiones" text="No hay versiones históricas para los filtros seleccionados." /> : <div className="table-panel history-table"><div className="table-summary"><strong>{meta.total} versiones registradas</strong><span>Página {page} de {Math.max(meta.totalPages, 1)}</span></div><div className="table-scroll"><table><thead><tr><th>Fecha y hora</th><th>Registro</th><th>Versión</th><th>Cambio</th><th>Campos</th><th>Usuario</th><th>Estado</th><th /></tr></thead><tbody>{versions.map((version) => <tr key={version.id}><td>{formatDate(version.occurredAt)}</td><td><Link className="history-asset-link" to={`/inventarios/${version.assetId}`}><strong>{version.assetName}</strong><small>{version.assetCode} · {version.typeName}</small></Link></td><td><span className={`version-badge ${version.isCurrent ? 'current' : ''}`}>v{version.versionNumber}{version.isCurrent ? ' · actual' : ''}</span></td><td><strong>{assetVersionChangeLabel(version.changeType)}</strong><small className="cell-subtext">{version.source}</small></td><td><div className="tag-list">{version.changedFields.slice(0, 3).map((field) => <span className="tag" key={field}>{assetVersionFieldLabel(field)}</span>)}</div></td><td>{version.actorUsername ?? 'Sistema'}</td><td>{assetStatusLabel(version.informationStatus)}</td><td><button type="button" className="icon-button" onClick={() => open(version)} aria-label={`Ver versión ${version.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>;
|
||
}
|