Merge pull request 'v2: бейдж «Мои отчёты» показывает «использовано / доступно» — остаток квоты был не виден нигде' (#3326) from fix/v2-quota-remaining-visible into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Successful in 2m5s
Deploy Trade-In / deploy (push) Successful in 57s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped

This commit is contained in:
bot-backend 2026-09-02 07:28:24 +00:00
commit 1c533c8a98
3 changed files with 75 additions and 20 deletions

View file

@ -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() {
<TopNav
active={nav}
onNavigate={setNav}
reports={reportsCount ?? 0}
quotaBadge={quotaBadge}
user={topNavUser}
onLogout={() => logoutMutation.mutate()}
showTeamLink={showTeamNavItem}

View file

@ -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({
/>
</svg>
Мои отчёты{" "}
<span
style={{
marginLeft: "auto",
fontFamily: tokens.font.mono,
fontSize: "10px",
// accentDeep (not accent): 10px counter text; accent #2e8bff
// on the white menu is ~2.9:1, accentDeep clears AA (#2264 C4).
color: tokens.accentDeep,
}}
>
{reports}
</span>
{quotaBadge && (
<span
style={{
marginLeft: "auto",
fontFamily: tokens.font.mono,
fontSize: "10px",
// accentDeep (not accent): 10px counter text; accent #2e8bff
// on the white menu is ~2.9:1, accentDeep clears AA (#2264 C4).
color: tokens.accentDeep,
}}
title={`Оценок в этом месяце: ${quotaBadge.used} из ${quotaBadge.limit}`}
>
{quotaBadge.used} / {quotaBadge.limit}
</span>
)}
</div>
<div

View file

@ -0,0 +1,42 @@
/**
* Бейдж «Мои отчёты» обязан показывать МЕСЯЧНУЮ квоту «использовано / доступно».
*
* До 02.09.2026 рендерилось голое `used` с фолбэком на длину истории: «0» у
* свежего аккаунта читался как «отчётов нет», остаток не показывался нигде
* владелец, выдав покупателю 50 оценок, сам не смог найти, где их видно
* (#3320 п.4).
*/
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
vi.mock("../SupportChatContext", () => ({
useSupportChat: () => ({ openChat: vi.fn() }),
}));
import TopNav from "../TopNav";
const USER = { name: "Тест", org: "Орг", email: "t@t", initials: "ТТ" };
describe("TopNav: бейдж квоты", () => {
it("показывает использовано / доступно, а не голое число", () => {
render(
<TopNav active={0} onNavigate={() => {}} user={USER}
quotaBadge={{ used: 3, limit: 50 }} />,
);
// Меню выпадающее — бейдж существует только после открытия.
fireEvent.click(screen.getByText("ТТ"));
expect(
screen.getByText((_, el) => el?.textContent === "3 / 50"),
).toBeTruthy();
});
it("без квоты (unlimited или не загрузилась) бейджа нет — лучше ничего, чем не то", () => {
render(<TopNav active={0} onNavigate={() => {}} user={USER} />);
fireEvent.click(screen.getByText("ТТ"));
expect(
screen.queryByText((_, el) => /^\d+ \/ \d+$/.test(el?.textContent ?? "")),
).toBeNull();
// сам пункт меню на месте
expect(screen.getByText(/Мои отчёты/)).toBeTruthy();
});
});