fix(логистика): бейдж обзвона не висит на согласованной заявке
Заявку согласовали — логист проставил дату вручную (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.
This commit is contained in:
parent
50c9a68e9f
commit
34463bbc4a
|
|
@ -1,7 +1,7 @@
|
||||||
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
|
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
|
||||||
|
|
||||||
if (!isLocalhost) {
|
if (!isLocalhost) {
|
||||||
const STATIC_CACHE = "construction-delivery-static-v135";
|
const STATIC_CACHE = "construction-delivery-static-v136";
|
||||||
const RUNTIME_CACHE = "construction-delivery-runtime-v119";
|
const RUNTIME_CACHE = "construction-delivery-runtime-v119";
|
||||||
const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
|
const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@
|
||||||
* «Не звонили» показываем только по заказам, которые ещё ждут решения
|
* «Не звонили» показываем только по заказам, которые ещё ждут решения
|
||||||
* (pending_confirmation / manual_confirmation_required): в уже доставленных
|
* (pending_confirmation / manual_confirmation_required): в уже доставленных
|
||||||
* заказах такая плашка — просто шум.
|
* заказах такая плашка — просто шум.
|
||||||
|
*
|
||||||
|
* То же правило действует для исходов «клиент не согласовал» (не дозвонились,
|
||||||
|
* не выбрал день/время, нужен менеджер): если заявку уже согласовали — логист
|
||||||
|
* проставил дату вручную или клиент подтвердил по ссылке — бейдж обзвона
|
||||||
|
* скрываем. Иначе рядом со «Согласовано» висело «📅 не выбрал день», то есть
|
||||||
|
* итог старого звонка читался как состояние заявки.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
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 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 = {
|
const STYLES = {
|
||||||
confirmed: "bg-[rgba(18,128,92,0.14)] text-[var(--color-accent)]",
|
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)]",
|
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);
|
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 — показывать нечего. */
|
/** Что показать в строке: null — показывать нечего. */
|
||||||
export const resolveCallBadge = (group) => {
|
export const resolveCallBadge = (group) => {
|
||||||
if (!group) return null;
|
if (!group) return null;
|
||||||
const status = group.autocallStatus || group.autocall_status || "not_called";
|
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];
|
const meta = AUTOCALL_STATUS_META[status];
|
||||||
if (!meta) return null;
|
if (!meta) return null;
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect } from "vitest";
|
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";
|
import { AUTOCALL_STATUS_META, AUTOCALL_STATUS_OPTIONS } from "../../services/orderGroupViews";
|
||||||
|
|
||||||
const group = (over = {}) => ({ id: "g1", deliveryStatus: "pending_confirmation", ...over });
|
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", () => {
|
describe("isAwaitingDecision", () => {
|
||||||
it("ждёт решения только ожидающий подтверждения заказ", () => {
|
it("ждёт решения только ожидающий подтверждения заказ", () => {
|
||||||
expect(isAwaitingDecision({ deliveryStatus: "pending_confirmation" })).toBe(true);
|
expect(isAwaitingDecision({ deliveryStatus: "pending_confirmation" })).toBe(true);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue