From 34463bbc4af621750c32d53374835ab94b755974 Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 5 Oct 2026 15:00:30 +0200 Subject: [PATCH] =?UTF-8?q?fix(=D0=BB=D0=BE=D0=B3=D0=B8=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0):=20=D0=B1=D0=B5=D0=B9=D0=B4=D0=B6=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=B7=D0=B2=D0=BE=D0=BD=D0=B0=20=D0=BD=D0=B5=20=D0=B2=D0=B8?= =?UTF-8?q?=D1=81=D0=B8=D1=82=20=D0=BD=D0=B0=20=D1=81=D0=BE=D0=B3=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B9=20=D0=B7?= =?UTF-8?q?=D0=B0=D1=8F=D0=B2=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Заявку согласовали — логист проставил дату вручную (delivery_date_source=manual) или клиент подтвердил по ссылке. Планировщик такие заявки уже не обзванивает, но бейдж последнего звонка («не дозвонились», «не выбрал день», «нужен менеджер») оставался и читался как состояние заявки рядом со «Согласовано». resolveCallBadge: исходы no_answer/no_input_day/no_input_half/no_input_both/manual скрываются, если заявка согласована (есть дата доставки/самовызова, notification_status=confirmed или статус ушёл в логистику). Запреты (opt_out/excluded/city_disabled) и подтверждение по звонку робота показываются как раньше. +7 тестов, SW v136. --- public/service-worker.js | 2 +- src/components/UI/CallStatusBadge.jsx | 42 ++++++++++++- src/components/UI/CallStatusBadge.test.jsx | 69 +++++++++++++++++++++- 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index e449abd..b09ac31 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,7 +1,7 @@ const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1"; if (!isLocalhost) { - const STATIC_CACHE = "construction-delivery-static-v135"; + const STATIC_CACHE = "construction-delivery-static-v136"; const RUNTIME_CACHE = "construction-delivery-runtime-v119"; const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"]; diff --git a/src/components/UI/CallStatusBadge.jsx b/src/components/UI/CallStatusBadge.jsx index 6133530..a3e1191 100644 --- a/src/components/UI/CallStatusBadge.jsx +++ b/src/components/UI/CallStatusBadge.jsx @@ -10,6 +10,12 @@ * «Не звонили» показываем только по заказам, которые ещё ждут решения * (pending_confirmation / manual_confirmation_required): в уже доставленных * заказах такая плашка — просто шум. + * + * То же правило действует для исходов «клиент не согласовал» (не дозвонились, + * не выбрал день/время, нужен менеджер): если заявку уже согласовали — логист + * проставил дату вручную или клиент подтвердил по ссылке — бейдж обзвона + * скрываем. Иначе рядом со «Согласовано» висело «📅 не выбрал день», то есть + * итог старого звонка читался как состояние заявки. */ import React from "react"; @@ -18,6 +24,20 @@ import { AUTOCALL_STATUS_META } from "../../services/orderGroupViews"; /** Статусы, при которых заказ ещё ждёт решения клиента. */ const AWAITING_DELIVERY_STATUSES = ["pending_confirmation", "manual_confirmation_required"]; +/** Статусы доставки: решение уже принято, заказ пошёл в логистику. */ +const AGREED_DELIVERY_STATUSES = [ + "agreed", + "driver_assigned", + "loaded", + "on_route", + "delivered", + "pickup", + "picked_up", +]; + +/** Исходы звонка «клиент не согласовал» — след истории, а не состояние заявки. */ +const HISTORICAL_CALL_STATUSES = ["no_answer", "no_input_day", "no_input_half", "no_input_both", "manual"]; + const STYLES = { confirmed: "bg-[rgba(18,128,92,0.14)] text-[var(--color-accent)]", no_answer: "bg-[rgba(239,68,68,0.14)] text-[var(--color-danger)]", @@ -51,12 +71,32 @@ export const isAwaitingDecision = (group) => { return AWAITING_DELIVERY_STATUSES.includes(status); }; +/** + * Согласована ли заявка — тогда роботу по ней звонить нечего и бейдж обзвона + * не показываем. Признаки те же, по которым планировщик + * (scripts/autocall_scheduler.py) исключает заявку из обзвона: есть дата + * доставки или самовывоза, клиент подтвердил по ссылке (notification_status) + * либо заказ уже ушёл в логистику. + */ +export const isCallingStopped = (group) => { + if (!group) return false; + const notification = String(group.notificationStatus || group.notification_status || "").toLowerCase(); + if (notification === "confirmed") return true; + if (group.deliveryDate || group.delivery_date || group.pickupDate || group.pickup_date) return true; + const status = String(group.deliveryStatus || group.delivery_status || "").toLowerCase(); + return AGREED_DELIVERY_STATUSES.includes(status); +}; + /** Что показать в строке: null — показывать нечего. */ export const resolveCallBadge = (group) => { if (!group) return null; const status = group.autocallStatus || group.autocall_status || "not_called"; // Запрет и исключение важны всегда, независимо от стадии заказа. - if (status === "not_called" && !isAwaitingDecision(group)) return null; + if (status === "not_called" && (!isAwaitingDecision(group) || isCallingStopped(group))) return null; + // Заявку уже согласовали — исход последнего звонка «не дозвонились / не выбрал + // день / нужен менеджер» стал историей: рядом со «Согласовано» он противоречит + // статусу заявки. + if (HISTORICAL_CALL_STATUSES.includes(status) && isCallingStopped(group)) return null; const meta = AUTOCALL_STATUS_META[status]; if (!meta) return null; return { diff --git a/src/components/UI/CallStatusBadge.test.jsx b/src/components/UI/CallStatusBadge.test.jsx index eb913dd..badae55 100644 --- a/src/components/UI/CallStatusBadge.test.jsx +++ b/src/components/UI/CallStatusBadge.test.jsx @@ -6,7 +6,7 @@ */ import { describe, it, expect } from "vitest"; -import { CallStatusBadge, isAwaitingDecision, resolveCallBadge } from "./CallStatusBadge"; +import { CallStatusBadge, isAwaitingDecision, isCallingStopped, resolveCallBadge } from "./CallStatusBadge"; import { AUTOCALL_STATUS_META, AUTOCALL_STATUS_OPTIONS } from "../../services/orderGroupViews"; const group = (over = {}) => ({ id: "g1", deliveryStatus: "pending_confirmation", ...over }); @@ -76,6 +76,73 @@ describe("resolveCallBadge — что видно в строке", () => { }); }); +describe("заявка уже согласована — бейджа обзвона нет", () => { + it("дату поставил логист вручную: «не выбрал день» больше не висит", () => { + expect(resolveCallBadge(group({ + autocallStatus: "no_input_day", + deliveryStatus: "agreed", + deliveryDate: "2026-10-08", + deliveryDateSource: "manual", + notificationStatus: "confirmed", + }))).toBeNull(); + }); + + it("клиент подтвердил по ссылке — исходы «не дозвонились» и «нужен менеджер» тоже скрыты", () => { + const over = { notificationStatus: "confirmed", deliveryStatus: "pending_confirmation" }; + expect(resolveCallBadge(group({ ...over, autocallStatus: "no_answer" }))).toBeNull(); + expect(resolveCallBadge(group({ ...over, autocallStatus: "manual" }))).toBeNull(); + expect(resolveCallBadge(group({ ...over, autocallStatus: "no_input_half" }))).toBeNull(); + expect(resolveCallBadge(group({ ...over, autocallStatus: "no_input_both" }))).toBeNull(); + }); + + it("заказ ушёл в логистику (доставлен, самовывоз, в пути) — плашка не висит", () => { + for (const deliveryStatus of ["delivered", "picked_up", "on_route", "pickup"]) { + expect(resolveCallBadge(group({ autocallStatus: "no_answer", deliveryStatus }))).toBeNull(); + } + }); + + it("дата есть, а статус ещё «ждёт подтверждения» — это не повод показывать «день не выбрал»", () => { + expect(resolveCallBadge(group({ + autocallStatus: "no_input_day", + deliveryStatus: "pending_confirmation", + deliveryDate: "2026-10-08", + }))).toBeNull(); + }); + + it("не согласовано — подписи остаются на месте", () => { + const badge = resolveCallBadge(group({ autocallStatus: "no_input_day", deliveryStatus: "pending_confirmation" })); + expect(badge.label).toContain("не выбрал день"); + }); + + it("запрет и исключение важнее согласования — их не скрываем", () => { + const agreed = { deliveryStatus: "delivered", deliveryDate: "2026-10-01" }; + expect(resolveCallBadge(group({ ...agreed, autocallStatus: "opt_out" })).label).toContain("не звонить"); + expect(resolveCallBadge(group({ ...agreed, autocallStatus: "excluded" })).label).toContain("исключение"); + expect(resolveCallBadge(group({ ...agreed, autocallStatus: "city_disabled" })).label).toContain("не звоним"); + }); + + it("согласие по звонку робота видно и после отгрузки — оно не противоречит статусу", () => { + const badge = resolveCallBadge(group({ autocallStatus: "confirmed", deliveryStatus: "delivered" })); + expect(badge.label).toContain("согласовано"); + }); +}); + +describe("isCallingStopped", () => { + it("согласование видно по дате, подтверждению клиента или уходу в логистику", () => { + expect(isCallingStopped({ deliveryDate: "2026-10-08" })).toBe(true); + expect(isCallingStopped({ pickupDate: "2026-10-08" })).toBe(true); + expect(isCallingStopped({ notificationStatus: "confirmed" })).toBe(true); + expect(isCallingStopped({ delivery_status: "driver_assigned" })).toBe(true); + expect(isCallingStopped({ deliveryStatus: "agreed" })).toBe(true); + }); + + it("пустая и необработанная заявка не считается согласованной", () => { + expect(isCallingStopped(null)).toBe(false); + expect(isCallingStopped(group({}))).toBe(false); + expect(isCallingStopped(group({ deliveryStatus: "pending_confirmation", notificationStatus: "link_ready" }))).toBe(false); + }); +}); + describe("isAwaitingDecision", () => { it("ждёт решения только ожидающий подтверждения заказ", () => { expect(isAwaitingDecision({ deliveryStatus: "pending_confirmation" })).toBe(true);