fix(admin-ui): корректные бейджи в истории — future/active/expired по реальному времени

Раньше любую запись с validTo<9999 помечал 'закрыта'. Но запись с validTo в
будущем — не закрытая, а либо ЗАПЛАНИРОВАННАЯ (validFrom > now), либо АКТИВНАЯ
(validFrom <= now < validTo). User видел 'закрыта' на v3 которая ещё не настала.

Теперь:
- future:  validFrom > now → бейдж 'запланирована' (info), точка orbit/horizon
- active:  validFrom <= now < validTo → бейдж 'активна' (success) если не latest
- expired: validTo <= now → 'закрыта' (neutral), точка пустая
- latest active: 'текущая' (primary), точка ultramarain

Бессрочная запись (validTo year>=9999) больше не показывает 'действует до:' —
у бессрочной нет конечной даты, отображение его смущало. i18n: новые ключи
history.active / history.scheduled (RU/EN).
This commit is contained in:
Zimin A.N.
2026-05-04 04:27:44 +03:00
parent 303a5f0402
commit 14d32d93c3
2 changed files with 32 additions and 11 deletions
@@ -35,33 +35,50 @@ export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey
<ol className="relative border-l-2 border-regolith pl-5 space-y-4">
{data.map((rec, idx) => {
const isLatest = idx === 0
const closed = new Date(rec.validTo).getFullYear() < 9999
// Нумерация по позиции в timeline: total..1, новая запись = total.
// rec.version (JPA optimistic lock) растёт на UPDATE и у нескольких
// закрытых может совпасть → дубли в UI. Index — стабильный 1-based счётчик.
const displayVersion = data.length - idx
const now = new Date()
const from = new Date(rec.validFrom)
const to = new Date(rec.validTo)
const farFuture = to.getFullYear() >= 9999
// Статус по реальному времени:
// future — validFrom ещё впереди (запланирована)
// active — сейчас в её диапазоне
// expired — validTo уже прошла (закрыта)
const status = from > now
? 'future'
: !farFuture && to <= now
? 'expired'
: 'active'
return (
<li key={rec.id} className="relative">
<span
className={`absolute -left-[27px] top-1 h-3 w-3 rounded-full border-2 ${
isLatest
isLatest && status === 'active'
? 'bg-ultramarain border-ultramarain'
: closed
? 'bg-white border-carbon/40'
: 'bg-aurora border-aurora'
: status === 'future'
? 'bg-orbit border-horizon'
: status === 'expired'
? 'bg-white border-carbon/40'
: 'bg-aurora border-aurora'
}`}
/>
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-primary text-ultramarain">v{displayVersion}</span>
{isLatest && <Badge variant="primary">{t('history.current')}</Badge>}
{!isLatest && closed && <Badge variant="neutral">{t('history.closed')}</Badge>}
{isLatest && status === 'active' && (
<Badge variant="primary">{t('history.current')}</Badge>
)}
{status === 'future' && <Badge variant="info">{t('history.scheduled')}</Badge>}
{status === 'expired' && <Badge variant="neutral">{t('history.closed')}</Badge>}
{status === 'active' && !isLatest && (
<Badge variant="success">{t('history.active')}</Badge>
)}
</div>
<div className="text-2xs text-carbon/70 space-y-0.5">
<div>
<span className="font-mono">{t('history.validFrom')}:</span>{' '}
{new Date(rec.validFrom).toLocaleString()}
</div>
{closed && (
{!farFuture && (
<div>
<span className="font-mono">{t('history.validTo')}:</span>{' '}
{new Date(rec.validTo).toLocaleString()}