import { Link, useLocation } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { Home, Database, FileText, ClipboardList, Inbox, Webhook, ScrollText, Search, GitBranch, type LucideIcon, } from 'lucide-react' import { useAuth } from 'react-oidc-context' import { useCanMutate } from '@/auth/useCanMutate' import { useDictionaries, useMyDrafts } from '@/api/queries' import { cn } from '@/lib/utils' /** * Sidebar — 220px nav rail per handoff design system. * * Structure (top-to-bottom): * - Logo block (ORDINIS wordmark в Tektur) * - Section: navigation rail (Home, Справочники [count], Поиск, Граф) * - Section: admin-only (Мои черновики, На ревью [count], Аудит, Outbox, Webhooks) * - User profile (bottom, sticky) * * Responsive: на <1024px collapse в icon-only rail (56px). Smaller — overlay * drawer (TODO Stage 3.x). Сейчас always-visible — main grid сам адаптирует. */ type NavSection = { /** Optional .cap header above items. */ label?: string items: NavItem[] } type NavItem = { to: string label: string icon: LucideIcon /** Badge content справа (число pending / counts). */ badge?: React.ReactNode /** Только аутентифицированным пользователям. */ authRequired?: boolean } export function Sidebar() { const { t } = useTranslation() const auth = useAuth() const canMutate = useCanMutate() const dictsQuery = useDictionaries() // Loaded just for сидбар counter — light request, кэшируется. const myDrafts = useMyDrafts(0, 1) 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 sections: NavSection[] = [ { items: [ { to: '/', label: t('nav.home'), icon: Home }, { to: '/dictionaries', label: t('nav.dictionaries'), icon: Database, badge: dictsCount > 0 ? dictsCount : undefined, }, { to: '/search', label: t('nav.search'), icon: Search }, // Graph view — Stage 3.3 placeholder. Когда d3-force готов. { to: '/graph', label: t('nav.graph'), icon: GitBranch }, ], }, { label: t('nav.section.workflow'), items: [ { to: '/my-drafts', label: t('nav.myDrafts'), icon: FileText, badge: myDraftsCount > 0 ? myDraftsCount : undefined, authRequired: true, }, { to: '/reviews', label: t('nav.reviews'), icon: ClipboardList, badge: reviewsCount > 0 ? reviewsCount : undefined, authRequired: true, }, ], }, { label: t('nav.section.admin'), items: [ { to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true }, { to: '/outbox', label: t('nav.outbox'), icon: Inbox, authRequired: true }, { to: '/webhooks', label: t('nav.webhooks'), icon: Webhook, authRequired: true }, ], }, ] const visibleSections = sections .map((s) => ({ ...s, items: s.items.filter((i) => !i.authRequired || canMutate), })) .filter((s) => s.items.length > 0) return ( ) } function SidebarLink({ to, label, icon: Icon, badge }: NavItem) { const location = useLocation() const active = to === '/' ? location.pathname === '/' : location.pathname === to || location.pathname.startsWith(to + '/') return ( {label} {badge !== undefined && ( {badge} )} ) }