diff --git a/tradein-mvp/frontend/src/app/v2/page.tsx b/tradein-mvp/frontend/src/app/v2/page.tsx index 043c7a83..1bc3617e 100644 --- a/tradein-mvp/frontend/src/app/v2/page.tsx +++ b/tradein-mvp/frontend/src/app/v2/page.tsx @@ -677,7 +677,14 @@ export default function TradeInV2Page() { // TopNav «Мои отчёты» badge: per-user estimate count. quota.used is the // authoritative monthly counter; fall back to the loaded history length, then // to undefined (TopNav renders 0) for pre-load states. - const reportsCount = quota.data?.used ?? history.data?.length; + // Счётчик у «Мои отчёты» — МЕСЯЧНАЯ квота, и вместе с use идёт limit: + // «использовано / доступно». Прежний фолбэк на history.length убран — + // это lifetime-число с потолком 50 строк, у него другой смысл, и одна + // цифра с двумя смыслами уже попала в аудит (#3320 п.4). Нет квоты — + // нет цифры: честнее спрятать, чем показать не то. + const quotaBadge = quota.data && !quota.data.unlimited + ? { used: quota.data.used, limit: quota.data.limit } + : undefined; // Real user for the TopNav menu. undefined until /me resolves → TopNav shows // its neutral «Гость» fallback (never the old "Андрей Петров / Брусника"). @@ -1045,7 +1052,7 @@ export default function TradeInV2Page() { logoutMutation.mutate()} showTeamLink={showTeamNavItem} diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/TopNav.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/TopNav.tsx index 70674679..9716c025 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/TopNav.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/v2/TopNav.tsx @@ -20,8 +20,8 @@ import { navLabels } from "./ui-config"; import { useSupportChat } from "./SupportChatContext"; // Real logged-in user identity, derived by the page from useMe() -// ({username, role, brand}). Deliberately excludes `reports` — the «Мои -// отчёты» badge count is a separate prop fed from useQuota().used. +// ({username, role, brand}). Deliberately excludes the quota — the «Мои +// отчёты» badge is a separate prop fed from useQuota(). interface TopNavUser { name: string; org: string; @@ -32,9 +32,12 @@ interface TopNavUser { interface TopNavProps { active: number; onNavigate: (i: number) => void; - // «Мои отчёты» badge count — page feeds it from useQuota().used (per-user - // estimate count). Defaults to 0 for unwired usage. - reports?: number; + // Бейдж «Мои отчёты»: месячная квота «использовано / доступно» из + // useQuota(). undefined → бейджа нет (unlimited-аккаунт или квота не + // загрузилась). Раньше тут было голое число used с фолбэком на длину + // истории — одна цифра с двумя смыслами (#3320 п.4): «0» читался как + // «отчётов нет», а был месячным счётчиком, и остаток не показывался нигде. + quotaBadge?: { used: number; limit: number }; // Real logged-in user. undefined → neutral «Гость» placeholder, NEVER the // old design fixture ("Андрей Петров / Брусника"). user?: TopNavUser; @@ -114,7 +117,7 @@ const menuItemDisabledStyle: CSSProperties = { export default function TopNav({ active, onNavigate, - reports = 0, + quotaBadge, user, onLogout, showTeamLink = false, @@ -460,18 +463,21 @@ export default function TopNav({ /> Мои отчёты{" "} - - {reports} - + {quotaBadge && ( + + {quotaBadge.used} / {quotaBadge.limit} + + )}
({ + useSupportChat: () => ({ openChat: vi.fn() }), +})); + +import TopNav from "../TopNav"; + +const USER = { name: "Тест", org: "Орг", email: "t@t", initials: "ТТ" }; + +describe("TopNav: бейдж квоты", () => { + it("показывает использовано / доступно, а не голое число", () => { + render( + {}} user={USER} + quotaBadge={{ used: 3, limit: 50 }} />, + ); + // Меню выпадающее — бейдж существует только после открытия. + fireEvent.click(screen.getByText("ТТ")); + expect( + screen.getByText((_, el) => el?.textContent === "3 / 50"), + ).toBeTruthy(); + }); + + it("без квоты (unlimited или не загрузилась) бейджа нет — лучше ничего, чем не то", () => { + render( {}} user={USER} />); + fireEvent.click(screen.getByText("ТТ")); + expect( + screen.queryByText((_, el) => /^\d+ \/ \d+$/.test(el?.textContent ?? "")), + ).toBeNull(); + // сам пункт меню на месте + expect(screen.getByText(/Мои отчёты/)).toBeTruthy(); + }); +});