From 64bcda8eed91c40b6a776dc8236e764c8b851e90 Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 8 Sep 2026 13:04:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(autocall):=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D1=83=D1=81=20=D0=B0=D0=B2=D1=82=D0=BE=D0=B4=D0=BE=D0=B7=D0=B2?= =?UTF-8?q?=D0=BE=D0=BD=D0=B0=20=D0=B2=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B5=20(=D0=BA=D0=B0=D0=BA=20=D1=83=20SMS)=20+=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=C2=AB=D0=9F=D1=80=D0=BE?= =?UTF-8?q?=D0=B7=D0=B2=D0=BE=D0=BD=D0=B8=D1=82=D1=8C=C2=BB=20=D0=B2=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B5=20+=20=D0=B2=D1=8B?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BD=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logistics/LogisticsReadinessBoard.jsx | 3 +- src/components/orders/AutocallStatusCard.jsx | 19 ++++----- src/components/orders/OrderFilters.jsx | 22 +++++++++-- src/services/orderGroupViews.js | 39 +++++++++++++++++++ src/services/supabase/orderGroupRepository.js | 8 +++- 5 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/components/logistics/LogisticsReadinessBoard.jsx b/src/components/logistics/LogisticsReadinessBoard.jsx index 6424f38..2dfecfa 100644 --- a/src/components/logistics/LogisticsReadinessBoard.jsx +++ b/src/components/logistics/LogisticsReadinessBoard.jsx @@ -299,6 +299,7 @@ const isFilterActive = (filters) => { if (filters.deliveryStatus && filters.deliveryStatus !== "all") return true; if (filters.deliveryHalfDay && filters.deliveryHalfDay !== "all") return true; if (filters.smsStatus && filters.smsStatus !== "all") return true; + if (filters.callStatus && filters.callStatus !== "all") return true; if (filters.dateFrom) return true; if (filters.dateTo) return true; return false; @@ -404,7 +405,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], totalGroups: totalGr // Reset to page 1 when filters change React.useEffect(() => { setPage(1); - }, [filters.query, filters.displayStatus, filters.city, filters.deliveryStatus, filters.deliveryHalfDay, filters.smsStatus, filters.dateFrom, filters.dateTo]); + }, [filters.query, filters.displayStatus, filters.city, filters.deliveryStatus, filters.deliveryHalfDay, filters.smsStatus, filters.callStatus, filters.dateFrom, filters.dateTo]); const totalGroups = rankedGroups.length; const totalPages = Math.max(1, Math.ceil(totalGroups / PAGE_SIZE)); diff --git a/src/components/orders/AutocallStatusCard.jsx b/src/components/orders/AutocallStatusCard.jsx index 5275116..24133af 100644 --- a/src/components/orders/AutocallStatusCard.jsx +++ b/src/components/orders/AutocallStatusCard.jsx @@ -6,6 +6,7 @@ import React, { useState, useEffect, useCallback } from "react"; import { Badge } from "../UI/Badge"; import { supabase } from "../../supabaseClient"; +import { resolveAutocallStatus, AUTOCALL_STATUS_META } from "../../services/orderGroupViews"; const OUTCOME_META = { confirmed: { label: "Согласовал", tone: "accent", icon: "✅" }, @@ -113,25 +114,19 @@ export const AutocallStatusCard = ({ order, userRole }) => { const lastMeta = lastCall ? OUTCOME_META[lastCall.outcome] : null; const lastSlot = lastCall ? slotText(lastCall) : null; + // Единый человеческий статус (по аналогии со статусом SMS) + const statusKey = resolveAutocallStatus(calls, { optOut, exclusionReason }); + const statusMeta = AUTOCALL_STATUS_META[statusKey] || AUTOCALL_STATUS_META.not_called; + return (

📞 Автодозвон

- {/* Итог */} + {/* Единый статус — как у SMS */}
- {optOut ? ( - 🚫 Отказ от звонков - ) : exclusionReason ? ( - 🚫 Не звоним (исключение) - ) : isAutocall ? ( - 🤖 Согласовано роботом - ) : calls.length > 0 ? ( - {lastMeta?.icon} {lastMeta?.label} - ) : ( - Не звонили - )} + {statusMeta.label} {lastSlot && {lastSlot}}
diff --git a/src/components/orders/OrderFilters.jsx b/src/components/orders/OrderFilters.jsx index 366ac4e..32321c2 100644 --- a/src/components/orders/OrderFilters.jsx +++ b/src/components/orders/OrderFilters.jsx @@ -127,6 +127,13 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities = { value: "problem_not_delivered", label: "SMS не доставлено" }, ]; + const callStatusOptions = [ + { value: "all", label: "Прозвонить" }, + { value: "need_call", label: "Нужно прозвонить (SMS/автодозвон)" }, + { value: "sms_problem", label: "Проблема с SMS" }, + { value: "autocall_problem", label: "Проблема с автодозвоном" }, + ]; + return (
@@ -205,10 +212,10 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities = />
- {/* Row 2: Date range + SMS problems (width matches its text) + hide-completed toggle. - Align by the bottom edge of the input fields, not by center — dates have a - label above the field, so items-center would lift SMS/toggle above them. */} -
+ {/* Row 2: Date range + SMS problems + прозвонить + hide-completed toggle. + Выравнивание по верхней линии полей ввода — чтобы переключатель + «Скрыть завершённые» стоял на уровне лейблов, а не прижимался к низу. */} +
updateFilter("dateFrom", v)} @@ -240,6 +247,13 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities = placeholder="Проблемы SMS" /> + updateFilter("callStatus", v)} + placeholder="Прозвонить" + /> + {typeof onToggleHideCompleted === "function" && ( { return "ok"; }; +// ─── Автодозвон: единый человеческий статус (по аналогии со статусом SMS) ── +// Принимает массив autocall_log (отсортированный по created_at desc) или null. +export const AUTOCALL_STATUS_META = { + not_called: { label: "Не звонили", tone: "neutral" }, + confirmed: { label: "Согласовано", tone: "accent" }, + no_answer: { label: "Не дозвонились", tone: "danger" }, + no_input: { label: "Не нажал в меню", tone: "warning" }, + manual: { label: "Требует ручного управления", tone: "warning" }, + opt_out: { label: "Отказ от звонков", tone: "danger" }, + excluded: { label: "Не звоним (исключение)", tone: "warning" }, +}; + +export const resolveAutocallStatus = (calls = [], { optOut = false, exclusionReason = null } = {}) => { + if (optOut) return "opt_out"; + if (exclusionReason) return "excluded"; + if (!Array.isArray(calls) || calls.length === 0) return "not_called"; + const last = calls[0]; + if (["confirmed", "confirmed_silent", "confirmed_kept_previous"].includes(last.outcome)) return "confirmed"; + if (last.outcome === "no_answer") return "no_answer"; + if (["no_input_day", "no_input_half"].includes(last.outcome)) return "no_input"; + if (["operator", "error", "invalid_number"].includes(last.outcome)) return "manual"; + return "not_called"; +}; + export const filterOrderGroups = (groups, filters = {}) => { const query = normalizeDate(filters.query).trim().toLowerCase(); const status = filters.status || "all"; @@ -270,6 +294,7 @@ export const filterOrderGroups = (groups, filters = {}) => { const dateTo = normalizeDate(filters.dateTo); const deliveryHalfDay = normalizeDate(filters.deliveryHalfDay || filters.timeSlot || "all"); const smsStatus = filters.smsStatus || "all"; + const callStatus = filters.callStatus || "all"; const isWithinDateRange = (group) => { const deliveryDate = parseGroupDate(getDeliveryDate(group)); @@ -367,6 +392,20 @@ export const filterOrderGroups = (groups, filters = {}) => { if (smsStatus === "problem_not_delivered" && tone !== "not_delivered") return false; } + // Фильтр «прозвонить»: SMS + автодозвон + if (callStatus !== "all") { + const smsTone = getSmsProblemTone(group); + const autocallStatus = group.autocall_status || group.autocallStatus || resolveAutocallStatus([], { + optOut: group.autocall_opt_out, + exclusionReason: group.autocall_exclusion_reason, + }); + const smsNotDone = smsTone === "not_sent" || smsTone === "not_delivered"; + const callNotDone = ["not_called", "no_answer", "no_input", "manual"].includes(autocallStatus); + if (callStatus === "need_call" && !(smsNotDone || callNotDone)) return false; + if (callStatus === "sms_problem" && !smsNotDone) return false; + if (callStatus === "autocall_problem" && !callNotDone) return false; + } + if (!query) { return true; } diff --git a/src/services/supabase/orderGroupRepository.js b/src/services/supabase/orderGroupRepository.js index 148865c..f308348 100644 --- a/src/services/supabase/orderGroupRepository.js +++ b/src/services/supabase/orderGroupRepository.js @@ -231,6 +231,12 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => { deliveryDate, deliveryTime, deliveryDateSource: row.delivery_date_source || null, + autocallOptOut: !!row.autocall_opt_out, + autocall_opt_out: !!row.autocall_opt_out, + autocallStatus: row.autocall_status || null, + autocall_status: row.autocall_status || null, + autocallExclusionReason: row.autocall_exclusion_reason || null, + autocall_exclusion_reason: row.autocall_exclusion_reason || null, deliveryType: effectiveDeliveryType, pickupDate: row.pickup_date || null, pickupTimeSlot: row.pickup_time_slot || null, @@ -294,7 +300,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => { }; const ORDER_GROUP_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, source_orders, order_list, order_list_structured, delivery_invitation_id, delivery_link, delivery_link_code, notification_status, sms_attempts, first_sms_sent_at, second_sms_sent_at, last_sms_error, next_notification_check_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, autocall_opt_out, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, delivery_invitation:delivery_invitations!order_groups_delivery_invitation_id_fkey(opened_at, access_count, last_accessed_at), called, called_at, called_comment, order_history(id, action, old_status, new_status, user_id, metadata, created_at)`; -export const ORDER_GROUP_LIST_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, notification_status, sms_attempts, last_sms_error, first_sms_sent_at, second_sms_sent_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, autocall_opt_out, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, called, called_at, called_comment, autocall_exclusion_reason`; +export const ORDER_GROUP_LIST_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, notification_status, sms_attempts, last_sms_error, first_sms_sent_at, second_sms_sent_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, autocall_opt_out, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, called, called_at, called_comment, autocall_exclusion_reason, autocall_status`; // Delivery statuses treated as "finished" — hidden by default in the board. const COMPLETED_DELIVERY_STATUSES = ["delivered", "picked_up", "cancelled"];