From 012d98205fb0f10b670a805d77766b6d263303b9 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 2 Sep 2026 12:25:14 +0500 Subject: [PATCH] =?UTF-8?q?v2:=20=D0=B1=D0=B5=D0=B9=D0=B4=D0=B6=20=C2=AB?= =?UTF-8?q?=D0=9C=D0=BE=D0=B8=20=D0=BE=D1=82=D1=87=D1=91=D1=82=D1=8B=C2=BB?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5=D1=82?= =?UTF-8?q?=20=D0=BA=D0=B2=D0=BE=D1=82=D1=83=20=C2=AB=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE=20/=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=BD=D0=BE=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Владелец выдал покупателю 50 оценок и не смог найти, где виден остаток. Ответ был: нигде. Бэкенд отдаёт limit/used/remaining/unlimited целиком, а фронт рендерил голое used с фолбэком на длину истории — «0» у свежего аккаунта читался как «отчётов нет», а был месячным счётчиком (#3320 п.4: одна цифра с двумя смыслами). Теперь: «3 / 50» + title с расшифровкой. Фолбэк на history.length убран — lifetime-число с потолком 50 строк смыслово другое; нет квоты (unlimited или не загрузилась) — нет бейджа: лучше ничего, чем не то. Тест по значению, с открытием меню; фальсификация: возврат голого used красит «Unable to find 3 / 50». 204 теста (v2 + mera-public) зелёные. --- tradein-mvp/frontend/src/app/v2/page.tsx | 11 ++++- .../src/components/trade-in/v2/TopNav.tsx | 42 +++++++++++-------- .../v2/__tests__/TopNav.quota.test.tsx | 42 +++++++++++++++++++ 3 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 tradein-mvp/frontend/src/components/trade-in/v2/__tests__/TopNav.quota.test.tsx 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(); + }); +}); -- 2.45.3