From 7b1d8834a1cb7a7a11f29fe98b73788793b38d2f Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Mon, 25 May 2026 15:28:00 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix(admin-ui):=20sidebar=20=C2=AB=D0=9C?= =?UTF-8?q?=D0=BE=D0=B8=20=D1=87=D0=B5=D1=80=D0=BD=D0=BE=D0=B2=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8=C2=BB=20badge=20=E2=80=94=20=D1=81=D1=87=D0=B8=D1=82?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20act?= =?UTF-8?q?ionable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Badge в сайдбаре (desktop + mobile) показывал totalElements от /drafts/me и /admin/schema-drafts/me — это сумма ВСЕХ статусов (PENDING + APPROVED + REJECTED + WITHDRAWN). После approve/reject счётчик не уменьшался, юзер видел «висящие» цифры на драфтах которые уже terminal. Tab-бейдж в ReviewsPage давно делает правильно через classifyRecordStatus / classifySchemaStatus. Подтягиваем sidebar к той же логике: - fetch size 50 вместо 1 (нужны .items[] для filter; TanStack дедуплицирует query — на /reviews этот же ключ уже зовётся, лишних запросов нет) - record → 'pending', schema → 'pending'|'wip' - approved schema всё ещё попадает в pending по дизайну (maker должен publish'нуть; см. classifySchemaStatus комментарий) --- .../src/components/layout/Sidebar.tsx | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/ordinis-admin-ui/src/components/layout/Sidebar.tsx b/ordinis-admin-ui/src/components/layout/Sidebar.tsx index 5d180f1..f44fec5 100644 --- a/ordinis-admin-ui/src/components/layout/Sidebar.tsx +++ b/ordinis-admin-ui/src/components/layout/Sidebar.tsx @@ -28,6 +28,7 @@ import { } from '@/api/queries' import { Sheet, SheetContent, SheetTitle } from '@/ui' import { ThemeSwitch } from '@/components/layout/ThemeSwitch' +import { classifyRecordStatus, classifySchemaStatus } from '@/components/reviews/MyDraftsPanel' import { cn } from '@/lib/utils' /** @@ -86,8 +87,15 @@ export function Sidebar() { // Loaded just for сидбар counter — light request, кэшируется. // Badge "Мои черновики" суммирует record (/drafts/me) + schema // (/admin/schema-drafts/me) — maker не должен помнить разные типы. - const myDrafts = useMyDrafts(0, 1) - const mySchemaDrafts = useMySchemaDrafts(0, 1) + // + // page=0 size=50 (не size=1!) — backend /drafts/me возвращает ВСЕ статусы + // (PENDING + APPROVED + REJECTED + WITHDRAWN), а totalElements не умеет + // filter по статусу. Нужно фильтровать на клиенте: считаем только + // pending/WIP-драфты, чтобы approved/rejected не «висели» в badge. + // Reuse: на /reviews этот же query (size=50) уже зовётся → TanStack + // дедуплицирует по queryKey, лишних запросов нет. + const myDrafts = useMyDrafts(0, 50) + const mySchemaDrafts = useMySchemaDrafts(0, 50) // Reviews badge — суммирует record-reviews + schema-reviews pending queues // через size=1 paged запросы. Gated на authenticated. const reviews = useReviewsBadgeCount(auth.isAuthenticated) @@ -100,8 +108,17 @@ export function Sidebar() { const webhooks = useWebhookSubscriptions(hasInternal) const dictsCount = dictsQuery.data?.length ?? 0 - const myDraftsCount = - (myDrafts.data?.totalElements ?? 0) + (mySchemaDrafts.data?.totalElements ?? 0) + // Считаем только actionable-драфты (то же что tab-badge в ReviewsPage): + // record → 'pending', schema → 'pending'|'wip'. APPROVED/REJECTED/WITHDRAWN + // — терминальны, не считаем. + const myRecordPending = (myDrafts.data?.items ?? []).filter( + (r) => classifyRecordStatus(r.status) === 'pending', + ).length + const mySchemaPending = (mySchemaDrafts.data?.items ?? []).filter((s) => { + const b = classifySchemaStatus(s.status) + return b === 'pending' || b === 'wip' + }).length + const myDraftsCount = myRecordPending + mySchemaPending const reviewsCount = reviews.total // Pending events ждут dispatch. dlq отдельно — alarming, но не блокирует // нормальную работу, можно увидеть на /outbox. Badge показывает только @@ -469,8 +486,10 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () => const canReviewRecord = useCanReviewRecord() const isReviewer = canReviewSchema || canReviewRecord const dictsQuery = useDictionaries() - const myDrafts = useMyDrafts(0, 1) - const mySchemaDrafts = useMySchemaDrafts(0, 1) + // size=50 (не size=1): см. desktop Sidebar — нужен .items[] для filter + // по статусу (totalElements включает APPROVED/REJECTED). + const myDrafts = useMyDrafts(0, 50) + const mySchemaDrafts = useMySchemaDrafts(0, 50) const reviews = useReviewsBadgeCount(auth.isAuthenticated) // Same INTERNAL-scope gate as desktop Sidebar (см. line ~92). const hasInternal = useHasInternalScope() @@ -478,8 +497,17 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () => const webhooks = useWebhookSubscriptions(hasInternal) const dictsCount = dictsQuery.data?.length ?? 0 - const myDraftsCount = - (myDrafts.data?.totalElements ?? 0) + (mySchemaDrafts.data?.totalElements ?? 0) + // Считаем только actionable-драфты (то же что tab-badge в ReviewsPage): + // record → 'pending', schema → 'pending'|'wip'. APPROVED/REJECTED/WITHDRAWN + // — терминальны, не считаем. + const myRecordPending = (myDrafts.data?.items ?? []).filter( + (r) => classifyRecordStatus(r.status) === 'pending', + ).length + const mySchemaPending = (mySchemaDrafts.data?.items ?? []).filter((s) => { + const b = classifySchemaStatus(s.status) + return b === 'pending' || b === 'wip' + }).length + const myDraftsCount = myRecordPending + mySchemaPending const reviewsCount = reviews.total const outboxPending = outboxStats.data?.pending ?? 0 const webhooksActive = From 7a9170d96eefbf5182f49261f7c99e65ffcafbaa Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Mon, 25 May 2026 15:30:04 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(admin-ui):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20badge=20=D1=81=20=C2=AB=D0=A1=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=B8=C2=BB=20=E2=80=94=20?= =?UTF-8?q?total=20count=20=D0=BD=D0=B5=20actionable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Цифра у иконки словарей показывала общее число справочников (~40 на проде). Не меняется в течение работы юзера — не actionable, только визуальный шум. Actionable badges остаются: «Мои черновики», «На ревью», «Outbox», «Webhooks» — там цифра реально что-то означает (надо что-то сделать или разобраться). Заодно убрал unused dictsQuery / dictsCount + import useDictionaries из обоих сайдбаров (desktop + mobile). TSC noUnusedLocals чистый. --- ordinis-admin-ui/src/components/layout/Sidebar.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ordinis-admin-ui/src/components/layout/Sidebar.tsx b/ordinis-admin-ui/src/components/layout/Sidebar.tsx index f44fec5..ca58234 100644 --- a/ordinis-admin-ui/src/components/layout/Sidebar.tsx +++ b/ordinis-admin-ui/src/components/layout/Sidebar.tsx @@ -19,7 +19,6 @@ import { useHasInternalScope, } from '@/auth/usePermissions' import { - useDictionaries, useMyDrafts, useMySchemaDrafts, useOutboxStats, @@ -83,7 +82,6 @@ export function Sidebar() { const canReviewSchema = useCanReviewSchema() const canReviewRecord = useCanReviewRecord() const isReviewer = canReviewSchema || canReviewRecord - const dictsQuery = useDictionaries() // Loaded just for сидбар counter — light request, кэшируется. // Badge "Мои черновики" суммирует record (/drafts/me) + schema // (/admin/schema-drafts/me) — maker не должен помнить разные типы. @@ -107,7 +105,6 @@ export function Sidebar() { const outboxStats = useOutboxStats(hasInternal) const webhooks = useWebhookSubscriptions(hasInternal) - const dictsCount = dictsQuery.data?.length ?? 0 // Считаем только actionable-драфты (то же что tab-badge в ReviewsPage): // record → 'pending', schema → 'pending'|'wip'. APPROVED/REJECTED/WITHDRAWN // — терминальны, не считаем. @@ -140,7 +137,9 @@ export function Sidebar() { to: '/dictionaries', label: t('nav.dictionaries'), icon: Database, - badge: dictsCount > 0 ? dictsCount : undefined, + // Без badge: total count словарей — не actionable, только шумит + // (на проде ~40 dicts, цифра не меняется в течение работы юзера). + // Actionable badges остаются: «Мои черновики», «На ревью», «Outbox». }, { to: '/my-drafts', @@ -485,7 +484,6 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () => const canReviewSchema = useCanReviewSchema() const canReviewRecord = useCanReviewRecord() const isReviewer = canReviewSchema || canReviewRecord - const dictsQuery = useDictionaries() // size=50 (не size=1): см. desktop Sidebar — нужен .items[] для filter // по статусу (totalElements включает APPROVED/REJECTED). const myDrafts = useMyDrafts(0, 50) @@ -496,7 +494,6 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () => const outboxStats = useOutboxStats(hasInternal) const webhooks = useWebhookSubscriptions(hasInternal) - const dictsCount = dictsQuery.data?.length ?? 0 // Считаем только actionable-драфты (то же что tab-badge в ReviewsPage): // record → 'pending', schema → 'pending'|'wip'. APPROVED/REJECTED/WITHDRAWN // — терминальны, не считаем. @@ -522,7 +519,9 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () => to: '/dictionaries', label: t('nav.dictionaries'), icon: Database, - badge: dictsCount > 0 ? dictsCount : undefined, + // Без badge: total count словарей — не actionable, только шумит + // (на проде ~40 dicts, цифра не меняется в течение работы юзера). + // Actionable badges остаются: «Мои черновики», «На ревью», «Outbox». }, { to: '/my-drafts',