feat(records): scheduled records empty-state hint

Fixes UX bug: пользователь создаёт запись с validFrom на будущую дату
(e.g. 18.05), открывает catalog 15.05 — пустая таблица, никаких хинтов
о том что запись существует.

Backend (read-api):
- New endpoint GET /api/v1/{dict}/records/scheduled-summary
- Returns {count, nearestValidFrom} для записей с validFrom > now AND
  validTo > now в текущем scope view
- Single-aggregate JPA query (COUNT + MIN), respect scope filter same way
  as listActive — INTERNAL scheduled не учитывается для PUBLIC consumer'а

Frontend:
- New useScheduledSummary hook + ScheduledRecordsSummary type
- Empty-state в dictionaries.$name now shows: "Запланировано N, ближайшая
  на DD.MM [Перейти к этой дате]" с CTA который set'ает time-travel на
  nearestValidFrom
- i18n keys для ru/en plural forms

Связан с MR !219 (TimeTravelPicker 30-day future window) — теперь slider
позволяет дойти до даты записи, и empty-state hint позволяет узнать что
запись существует.
This commit is contained in:
Zimin A.N.
2026-05-15 20:17:07 +03:00
parent 4c3327b05c
commit 234184645f
8 changed files with 205 additions and 1 deletions
@@ -178,4 +178,29 @@ public interface DictionaryRecordRepository extends JpaRepository<DictionaryReco
@Param("from") OffsetDateTime from,
@Param("to") OffsetDateTime to,
@Param("granularity") String granularity);
/**
* Scheduled records summary: записи которые ещё не активны (validFrom > now),
* но запланированы на будущее (validTo > now — т.е. не сразу закрытые).
* Используется empty-state hint в admin-UI: «У словаря есть N запланированных
* на DD.MM» — fixes UX bug когда пользователь создаёт запись с validFrom
* на будущую дату и не видит её в списке.
*
* <p>Single query → returns {@code [count Long, nearestValidFrom OffsetDateTime]}.
* Если scheduled нет — count=0, nearest=null.
*
* <p>Filter по scope тот же что в read-side listActive: respects user's
* view permissions. Anonymous (только PUBLIC) → INTERNAL scheduled не учитывается.
*/
@Query("""
SELECT COUNT(r), MIN(r.validFrom) FROM DictionaryRecord r
WHERE r.dictionaryId = :dictionaryId
AND r.dataScope IN :allowedScopes
AND r.validFrom > :now
AND r.validTo > :now
""")
List<Object[]> findScheduledSummary(
@Param("dictionaryId") UUID dictionaryId,
@Param("allowedScopes") Collection<DataScope> allowedScopes,
@Param("now") OffsetDateTime now);
}