diff --git a/ordinis-admin-ui/src/api/queries.ts b/ordinis-admin-ui/src/api/queries.ts index 4affe22..efa4022 100644 --- a/ordinis-admin-ui/src/api/queries.ts +++ b/ordinis-admin-ui/src/api/queries.ts @@ -622,6 +622,40 @@ export const reviewQueueQuery = (page = 0, size = 50) => export const useReviewQueue = (page = 0, size = 50) => useQuery(reviewQueueQuery(page, size)) +/** + * Lightweight count для sidebar badge — fetch only metadata (size=1). + * + *
Combines record-reviews + schema-reviews queues. Возвращает сумму pending + * drafts обоих видов. Поллится каждые 30s в синк с queue queries — reviewer + * увидит свежий counter при возврате на app даже если /reviews не открыт. + * + *
Gated на authenticated — anonymous не должен дёргать admin endpoint'ы + * (вернут 401, шум в логах). Когда Phase 1d Keycloak roles подключатся, + * этот gate можно сузить до approver-role (useCanReviewSchema). + */ +export const useReviewsBadgeCount = (enabled = true) => { + const recordQ = useQuery({ + ...reviewQueueQuery(0, 1), + enabled, + // Override interval: badge ОК с 60s — это не deep view, lower frequency + // снижает background load когда tab idle. + refetchInterval: 60_000, + }) + const schemaQ = useQuery({ + ...schemaReviewQueueQuery(0, 1), + enabled, + refetchInterval: 60_000, + }) + const recordCount = recordQ.data?.totalElements ?? 0 + const schemaCount = schemaQ.data?.total ?? 0 + return { + total: recordCount + schemaCount, + recordCount, + schemaCount, + isLoading: recordQ.isLoading || schemaQ.isLoading, + } +} + /** * Pending drafts на конкретный dict — для бейджа "На review" в records list. * Лёгкий poll каждые 30s. Backend возвращает только PENDING (фильтрация в SQL). diff --git a/ordinis-admin-ui/src/components/layout/Sidebar.tsx b/ordinis-admin-ui/src/components/layout/Sidebar.tsx index 4bf6ef4..84fa3be 100644 --- a/ordinis-admin-ui/src/components/layout/Sidebar.tsx +++ b/ordinis-admin-ui/src/components/layout/Sidebar.tsx @@ -14,7 +14,7 @@ import { } from 'lucide-react' import { useAuth } from 'react-oidc-context' import { useCanMutate } from '@/auth/useCanMutate' -import { useDictionaries, useMyDrafts } from '@/api/queries' +import { useDictionaries, useMyDrafts, useReviewsBadgeCount } from '@/api/queries' import { Sheet, SheetContent } from '@/ui' import { ThemeSwitch } from '@/components/layout/ThemeSwitch' import { cn } from '@/lib/utils' @@ -55,13 +55,14 @@ export function Sidebar() { const dictsQuery = useDictionaries() // Loaded just for сидбар counter — light request, кэшируется. const myDrafts = useMyDrafts(0, 1) + // Reviews badge — суммирует record-reviews + schema-reviews pending queues + // через size=1 paged запросы (cheap: одна запись + metadata). Gated на + // authenticated, чтобы anonymous не пытался admin endpoint'ы. + const reviews = useReviewsBadgeCount(auth.isAuthenticated) const dictsCount = dictsQuery.data?.length ?? 0 const myDraftsCount = myDrafts.data?.totalElements ?? 0 - // Reviews count бэйдж — TODO когда будет cheap endpoint /drafts/pending/count. - // Сейчас GET /drafts/pending возвращает page, paged запрос на каждый mount - // sidebar'а — overkill. Skip badge. - const reviewsCount = 0 + const reviewsCount = reviews.total // Flat list per redesign/compact.html — no section labels, /graph removed // (now reachable via catalog toolbar "Граф ⇄" button). Order: Главная → @@ -392,10 +393,11 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () => const canMutate = useCanMutate() const dictsQuery = useDictionaries() const myDrafts = useMyDrafts(0, 1) + const reviews = useReviewsBadgeCount(auth.isAuthenticated) const dictsCount = dictsQuery.data?.length ?? 0 const myDraftsCount = myDrafts.data?.totalElements ?? 0 - const reviewsCount = 0 + const reviewsCount = reviews.total // Flat list per redesign — same as desktop Sidebar above. const sections: NavSection[] = [