From 8b0ce4cf45abadf7219248bf43acb6df9d8e276c Mon Sep 17 00:00:00 2001 From: vladtechno Date: Tue, 18 Aug 2026 15:46:35 +1000 Subject: [PATCH] =?UTF-8?q?fix(pwa):=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=B1=D0=B0=D0=BB=D0=B0=D0=BD=D1=81=D0=B0=20=D0=BF=D0=BE=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8F=D0=B5=D1=82=20=D0=BA=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=BE=D1=87=D0=BA=D1=83=20CRM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Итог берётся с сервера, а не складывается из строк (1С округляет статьи до рублей, итог держит с копейками: −8 976 против −8 976,25). Статья, в которую платёж провести нечем, показывается, но не притворяется кнопкой — «Страховка». Пустой снимок 1С даёт «Уточняется» вместо нулей. Co-Authored-By: Claude Opus 5 --- driver-pwa/frontend/src/api/driver.ts | 12 ++++- .../frontend/src/pages/BalancePage.test.tsx | 25 +++++++--- driver-pwa/frontend/src/pages/BalancePage.tsx | 47 ++++++++++++++----- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/driver-pwa/frontend/src/api/driver.ts b/driver-pwa/frontend/src/api/driver.ts index 0b5529b..aa4c5f3 100644 --- a/driver-pwa/frontend/src/api/driver.ts +++ b/driver-pwa/frontend/src/api/driver.ts @@ -25,7 +25,17 @@ export type AuthRequest = z.infer; export const VerifySchema = z.object({ token: z.string(), driver_id: z.number() }); export const BalanceSchema = z.object({ - accounts: z.array(z.object({ bucket: z.string(), balance: z.number() })), + accounts: z.array(z.object({ + bucket: z.string(), + balance: z.number(), + // Можно ли платить в эту статью. «Страховка» приходит из 1С, но платежом не + // закрывается — строку показываем, нажатие не предлагаем. + payable: z.boolean().default(true), + })), + // Итог берём с сервера, а не суммой строк: 1С округляет статьи до рублей, а итог + // держит с копейками, и сумма строк с ним не сходится (−8 976 против −8 976,25). + total: z.number().nullable().default(null), + synced_at: z.string().nullable().default(null), // Принято нами, но ещё не проведено в 1С. В сумму баланса не входит. pending_topup: z.number().default(0), }); diff --git a/driver-pwa/frontend/src/pages/BalancePage.test.tsx b/driver-pwa/frontend/src/pages/BalancePage.test.tsx index 9128eaa..0c0fd9e 100644 --- a/driver-pwa/frontend/src/pages/BalancePage.test.tsx +++ b/driver-pwa/frontend/src/pages/BalancePage.test.tsx @@ -3,12 +3,18 @@ import { render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +// Ответ как у настоящего водителя: шесть статей 1С, итог отдельным числом. vi.mock("@/api/driver", () => ({ getBalance: vi.fn(async () => ({ accounts: [ - { bucket: "Долг аренда", balance: -3200 }, - { bucket: "Депозит", balance: 5000 }, + { bucket: "Долг аренда", balance: -3050, payable: true }, + { bucket: "Долг по штрафам", balance: -5156, payable: true }, + { bucket: "Прочее", balance: -770, payable: true }, + { bucket: "Страховка", balance: 0, payable: false }, ], + total: -8976.25, + synced_at: null, + pending_topup: 0, })), })); @@ -24,15 +30,22 @@ function wrap() { } describe("BalancePage", () => { - it("shows hero total (sum of negatives) and account rows", async () => { + it("итог берётся с сервера, а не складывается из строк", async () => { wrap(); expect(await screen.findByText("Долг аренда")).toBeInTheDocument(); - expect(screen.getByText("Депозит")).toBeInTheDocument(); - // Appears in both the hero total and the single debt account row. - expect(screen.getAllByText("−3 200 ₽").length).toBeGreaterThan(0); + // Сумма статей дала бы −8 976, а в CRM у водителя −8 976,25. Показываем её. + expect(screen.getByText("−8 976,25 ₽")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Пополнить/i })).toBeInTheDocument(); }); + it("статья, в которую платить нечем, не притворяется кнопкой", async () => { + wrap(); + await screen.findByText("Страховка"); + const buckets = screen.getAllByRole("button").map((b) => b.textContent ?? ""); + expect(buckets.some((t) => t.includes("Долг аренда"))).toBe(true); + expect(buckets.some((t) => t.includes("Страховка"))).toBe(false); + }); + it("shows an error state when balance fails to load", async () => { const driver = await import("@/api/driver"); (driver.getBalance as any).mockRejectedValueOnce(new Error("boom")); diff --git a/driver-pwa/frontend/src/pages/BalancePage.tsx b/driver-pwa/frontend/src/pages/BalancePage.tsx index 0007836..a5fdc25 100644 --- a/driver-pwa/frontend/src/pages/BalancePage.tsx +++ b/driver-pwa/frontend/src/pages/BalancePage.tsx @@ -13,15 +13,24 @@ export function BalancePage() { const { data, isLoading, isError, refetch } = useQuery({ queryKey: ["balance"], queryFn: getBalance }); const accounts = data?.accounts ?? []; - const total = accounts.reduce((s, a) => s + a.balance, 0); + // Итог — с сервера (это ровно то число, что видит сотрудник в CRM). Сумма строк + // тут не годится: 1С округляет статьи до рублей, а итог держит с копейками. + const total = data?.total ?? accounts.reduce((s, a) => s + a.balance, 0); const pending = data?.pending_topup ?? 0; + const noData = !isLoading && !isError && accounts.length === 0; return (

Баланс

-

{formatMoney(total)}

+ {noData ? ( +

Уточняется

+ ) : ( +

+ {formatMoney(total)} +

+ )} {pending > 0 && ( // Платёж принят нами, но 1С его ещё не провела. В сумму баланса он не // входит намеренно: сложить оба источника значило бы показать @@ -41,16 +50,30 @@ export function BalancePage() {
) : (
- {accounts.map((a) => ( - - ))} + {noData && ( +

+ Баланс ещё не пришёл из бухгалтерии. Если он нужен прямо сейчас — спросите в парке. +

+ )} + {accounts.map((a) => { + const row = ( + <> + {a.bucket} + {formatMoney(a.balance)} + + ); + const cls = "w-full flex justify-between items-center py-3 border-b border-line last:border-0 text-left"; + // В «Страховку» платёж провести нечем — строку показываем, нажатие не + // предлагаем: кнопка, ведущая в отказ, хуже отсутствующей кнопки. + return a.payable ? ( + + ) : ( +
{row}
+ ); + })}
)}