From 139c895290d97898cd68d5f6c53e22fc5f37595e Mon Sep 17 00:00:00 2001 From: Hermes Date: Fri, 11 Sep 2026 11:59:44 +0200 Subject: [PATCH] =?UTF-8?q?feat(autocall):=20=D0=BB=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=E2=80=94=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B0=D1=82=D0=B5=D0=BB=D0=B8=20=D0=B7=D0=B2?= =?UTF-8?q?=D0=BE=D0=BD=D0=BA=D0=BE=D0=B2=20=D0=BF=D0=BE=20=D0=B3=D0=BE?= =?UTF-8?q?=D1=80=D0=BE=D0=B4=D0=B0=D0=BC=20=D0=B2=20=D0=B0=D0=B4=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=BA=D0=B5=20(=D0=B0=D0=B2=D1=82=D0=BE-=D0=B4?= =?UTF-8?q?=D0=BE=D0=B7=D0=B2=D0=BE=D0=BD),=20=D0=B3=D0=B5=D0=BE-=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=20=D0=BE=D0=B1=D1=89=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=81=20SMS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/autocall_scheduler.py | 14 ++ .../admin/autocall/AutocallChannelTab.jsx | 4 + .../admin/autocall/AutocallLocationsPanel.jsx | 151 ++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 src/components/admin/autocall/AutocallLocationsPanel.jsx diff --git a/scripts/autocall_scheduler.py b/scripts/autocall_scheduler.py index 83cf518..0602109 100644 --- a/scripts/autocall_scheduler.py +++ b/scripts/autocall_scheduler.py @@ -135,6 +135,11 @@ def get_groups_to_call(conn, settings): SELECT 1 FROM sms_city_profiles p WHERE p.city = og.town AND p.sms_enabled = false ) + -- Локация выключена для звонков в админке автодозвона + AND NOT EXISTS ( + SELECT 1 FROM autocall_city_rules c + WHERE c.city = og.town AND c.calls_enabled = false + ) -- Самовывоз звоним только если разрешено настройкой AND (COALESCE(og.delivery_type, 'delivery') <> 'pickup' OR %s) ORDER BY og.sms_sent_at ASC @@ -202,6 +207,15 @@ def get_city_exclusion(conn, city): "source": "city_profile", "reason": (profile.get("note") or "").strip() or f"город {city}: канал выключен", } + # Только звонки: переключатель локации в админке автодозвона + # (на SMS не влияет — уведомления по городу продолжают уходить). + cur.execute("SELECT calls_enabled, note FROM autocall_city_rules WHERE city = %s LIMIT 1", (city,)) + calls_rule = cur.fetchone() + if calls_rule is not None and calls_rule.get("calls_enabled") is False: + return { + "source": "city_calls_off", + "reason": (calls_rule.get("note") or "").strip() or f"звонки по {city} выключены в админке", + } return None diff --git a/src/components/admin/autocall/AutocallChannelTab.jsx b/src/components/admin/autocall/AutocallChannelTab.jsx index d0cdbea..0d507da 100644 --- a/src/components/admin/autocall/AutocallChannelTab.jsx +++ b/src/components/admin/autocall/AutocallChannelTab.jsx @@ -11,6 +11,7 @@ import { supabase } from "../../../supabaseClient"; import { useAuth } from "../../../context/AuthContext"; import { TestCallPanel } from "./TestCallPanel"; import { AutocallExclusionsPanel } from "./AutocallExclusionsPanel"; +import { AutocallLocationsPanel } from "./AutocallLocationsPanel"; // ── Кампании звонков ───────────────────────────────────────────────────────── const CAMPAIGNS = [ @@ -368,6 +369,9 @@ export const AutocallChannelTab = () => { {/* ── Тестовый звонок (только мегаадмин) ───────────────────────────── */} {isMegaAdmin && } + {/* ── Локации: где звонит робот ──────────────────────────────────── */} + + {/* ── Исключения автодозвона ─────────────────────────────────────── */} diff --git a/src/components/admin/autocall/AutocallLocationsPanel.jsx b/src/components/admin/autocall/AutocallLocationsPanel.jsx new file mode 100644 index 0000000..9013c6d --- /dev/null +++ b/src/components/admin/autocall/AutocallLocationsPanel.jsx @@ -0,0 +1,151 @@ +/** + * @file AutocallLocationsPanel.jsx + * @description Панель «Локации» в настройках автодозвона. + * Логист/админ выключает звонки по конкретным городам (например, по Ялте звонки + * не делаем — там работают SMS-уведомления). Правила хранятся в общих городских + * настройках рассылок, но только для звонков (campaign_type='autocall_slot'), + * поэтому SMS по этому городу продолжают уходить. + */ +import React, { useState, useEffect, useCallback } from "react"; +import { Panel } from "../../UI/Panel"; +import { Badge } from "../../UI/Badge"; +import { supabase } from "../../../supabaseClient"; +import { useAuth } from "../../../context/AuthContext"; + +export const AutocallLocationsPanel = () => { + const { user } = useAuth(); + const canManage = ["mega_admin", "admin", "logistician"].includes(user?.role); + const [cities, setCities] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const { data, error } = await supabase.rpc("autocall_city_options"); + if (error) throw error; + setCities(data || []); + } catch (e) { + setError(e.message || String(e)); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { load(); }, [load]); + + const handleToggle = async (city) => { + const next = !city.calls_enabled; + setBusy(city.city); + setError(null); + try { + // Пишем в таблицу переключателей звонков: у SMS по этому городу + // своя настройка, и она не меняется. + const { error } = await supabase + .from("autocall_city_rules") + .upsert( + { + city: city.city, + calls_enabled: next, + note: next ? "" : `Звонки по ${city.city} выключены`, + updated_by: user?.email || null, + updated_at: new Date().toISOString(), + }, + { onConflict: "city" } + ); + if (error) throw error; + setCities((prev) => + prev.map((c) => (c.city === city.city ? { ...c, calls_enabled: next } : c)) + ); + } catch (e) { + setError(e.message || String(e)); + } finally { + setBusy(null); + } + }; + + return ( + +
+
+

📍 Локации

+

+ Где звонит робот. Выключенный город звонки не получает — на SMS это не влияет, + уведомления продолжают уходить. +

+
+ {canManage && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
Загрузка…
+ ) : cities.length === 0 ? ( +
+ Городов пока нет. +
+ ) : ( +
+ {cities.map((c) => ( +
+ +
+
+ {c.city} + + {c.calls_enabled ? "звоним" : "не звоним"} + + {c.global_blocked && ( + город исключён полностью + )} +
+ {c.note && ( +
{c.note}
+ )} + {c.global_blocked && ( +
+ В правилах города стоит полное исключение — SMS по нему тоже не уходят. +
+ )} +
+
+ ))} +
+ )} +
+ ); +};