diff --git a/public/service-worker.js b/public/service-worker.js
index a374282..965fabf 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-v128";
+ const STATIC_CACHE = "construction-delivery-static-v129";
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/scripts/sql/2026-09-29_autocall_status_city_disabled.sql b/scripts/sql/2026-09-29_autocall_status_city_disabled.sql
new file mode 100644
index 0000000..f74a4e2
--- /dev/null
+++ b/scripts/sql/2026-09-29_autocall_status_city_disabled.sql
@@ -0,0 +1,57 @@
+-- Заказ из локации, где звонки выключены (Настройки -> Коммуникации -> Автодозвон -> Локации),
+-- помечается статусом city_disabled = «Не звоним (город)». На SMS это не влияет.
+-- Порядок: ручной запрет -> локация выключена -> исключение по номеру -> журнал звонков.
+
+CREATE OR REPLACE FUNCTION public.autocall_status(og public.order_groups)
+ RETURNS text
+ LANGUAGE sql
+ STABLE SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+ select case
+ when og.autocall_opt_out then 'opt_out'
+ when exists (
+ select 1 from public.autocall_city_rules c
+ where btrim(c.city) = btrim(coalesce(og.town, ''))
+ and c.calls_enabled = false
+ ) then 'city_disabled'
+ when exists (
+ select 1 from public.autocall_exclusions e
+ where e.active
+ and (
+ (e.rule_type = 'phone' and coalesce(og.customer_phone_normalized, regexp_replace(coalesce(og.customer_phone,''),'\D','','g')) = e.value)
+ or
+ (e.rule_type = 'prefix' and coalesce(og.customer_phone_normalized, regexp_replace(coalesce(og.customer_phone,''),'\D','','g')) like e.value || '%')
+ )
+ ) then 'excluded'
+ when not exists (select 1 from public.autocall_log l where l.order_group_id = og.id) then 'not_called'
+ when exists (
+ select 1 from public.autocall_log l
+ where l.order_group_id = og.id
+ and l.outcome in ('confirmed','confirmed_silent','confirmed_kept_previous')
+ ) then 'confirmed'
+ else (
+ select case
+ when l.outcome = 'no_answer' then 'no_answer'
+ when l.outcome in ('no_input_day','no_input_half') then
+ case
+ when exists (select 1 from public.autocall_log l2
+ where l2.order_group_id = og.id and l2.outcome = 'no_input_day')
+ and exists (select 1 from public.autocall_log l3
+ where l3.order_group_id = og.id and l3.outcome = 'no_input_half')
+ then 'no_input_both'
+ when l.outcome = 'no_input_half' then 'no_input_half'
+ else 'no_input_day'
+ end
+ when l.outcome in ('operator','error','invalid_number') then 'manual'
+ else 'not_called'
+ end
+ from public.autocall_log l
+ where l.order_group_id = og.id
+ order by l.created_at desc
+ limit 1
+ )
+ end;
+$function$;
+
+NOTIFY pgrst, 'reload schema';
diff --git a/src/components/UI/CallStatusBadge.jsx b/src/components/UI/CallStatusBadge.jsx
index ba4f6b4..6133530 100644
--- a/src/components/UI/CallStatusBadge.jsx
+++ b/src/components/UI/CallStatusBadge.jsx
@@ -27,6 +27,7 @@ const STYLES = {
manual: "bg-[rgba(245,158,11,0.14)] text-[var(--color-warning)]",
opt_out: "bg-[rgba(239,68,68,0.14)] text-[var(--color-danger)]",
excluded: "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]",
+ city_disabled: "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]",
not_called: "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]",
};
@@ -39,6 +40,7 @@ const TITLES = {
manual: "Нужен менеджер: неверный номер или ошибка звонка",
opt_out: "Звонки по этому клиенту запрещены вручную",
excluded: "Исключение: по этому номеру или городу не звоним",
+ city_disabled: "Локация выключена для звонков: робот по этому городу не набирает (SMS продолжают уходить)",
not_called: "Автодозвона по заказу ещё не было",
};
diff --git a/src/components/UI/CallStatusBadge.test.jsx b/src/components/UI/CallStatusBadge.test.jsx
index 0b6665d..eb913dd 100644
--- a/src/components/UI/CallStatusBadge.test.jsx
+++ b/src/components/UI/CallStatusBadge.test.jsx
@@ -57,6 +57,14 @@ describe("resolveCallBadge — что видно в строке", () => {
expect(excluded.title).toContain("МТС-диапазон");
});
+ it("выключенная локация помечена «не звоним» и видна в завершённом заказе", () => {
+ const badge = resolveCallBadge(group({ autocallStatus: "city_disabled", town: "Ялта", deliveryStatus: "delivered" }));
+ expect(badge.label).toContain("не звоним");
+ expect(badge.title).toContain("Локация выключена для звонков");
+ expect(AUTOCALL_STATUS_META.city_disabled.label).toBe("Не звоним (город)");
+ expect(AUTOCALL_STATUS_OPTIONS.map((o) => o.value)).toContain("city_disabled");
+ });
+
it("неизвестный статус не рисуем", () => {
expect(resolveCallBadge(group({ autocallStatus: "какой-то-старый" }))).toBeNull();
});
diff --git a/src/components/orders/AutocallStatusCard.jsx b/src/components/orders/AutocallStatusCard.jsx
index 6120438..8dd62f9 100644
--- a/src/components/orders/AutocallStatusCard.jsx
+++ b/src/components/orders/AutocallStatusCard.jsx
@@ -6,7 +6,7 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Badge } from "../UI/Badge";
import { supabase } from "../../supabaseClient";
-import { resolveAutocallStatus, AUTOCALL_STATUS_META } from "../../services/orderGroupViews";
+import { resolveAutocallStatus, AUTOCALL_STATUS_META, isCityCallsDisabled } from "../../services/orderGroupViews";
import { describeCallProcess, isOrderAgreed } from "../../services/autocallProcess";
import { isAutocallPossible } from "../../services/autocallAvailability";
import { autocallSlotText } from "./autocallSlot";
@@ -43,6 +43,10 @@ export const AutocallStatusCard = ({ order, userRole }) => {
const optOut = order.autocall_opt_out ?? order.autocallOptOut ?? false;
const isAutocall = order.delivery_date_source === "autocall" || order.deliveryDateSource === "autocall";
const exclusionReason = order.autocall_exclusion_reason || order.autocallExclusionReason || null;
+ // Локация выключена для звонков (админка автодозвона) — робот по ней не набирает,
+ // ни массово, ни по заявке «Позвонить сейчас». Клиент получает SMS со ссылкой.
+ const cityDisabled = isCityCallsDisabled(order);
+ const town = order.town || order.city || "";
const loadCalls = useCallback(async () => {
if (!order?.id) return;
@@ -230,7 +234,7 @@ export const AutocallStatusCard = ({ order, userRole }) => {
const lastSlot = lastCall ? autocallSlotText(lastCall, order) : null;
// Единый человеческий статус (по аналогии со статусом SMS)
- const statusKey = resolveAutocallStatus(calls, { optOut, exclusionReason });
+ const statusKey = resolveAutocallStatus(calls, { optOut, exclusionReason, cityDisabled });
const statusMeta = AUTOCALL_STATUS_META[statusKey] || AUTOCALL_STATUS_META.not_called;
return (
@@ -252,6 +256,14 @@ export const AutocallStatusCard = ({ order, userRole }) => {
)}
+ {/* Локация выключена для звонков — почему звонка не было и не будет */}
+ {cityDisabled && (
+
+ 🚫 Не звоним{town ? ` — ${town}` : ""}: локация выключена для звонков. Уведомления по ней уходят
+ SMS со ссылкой. Включается в Настройки → Коммуникации → Автодозвон → 📍 Локации.
+
+ )}
+
{/* Счётчик попыток — как у SMS «Попыток отправки: N» */}
{calls.length > 0 && (
@@ -356,13 +368,15 @@ export const AutocallStatusCard = ({ order, userRole }) => {