v2: бейдж «Мои отчёты» показывает «использовано / доступно» — остаток квоты был не виден нигде #3326
3 changed files with 75 additions and 20 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue