@@ -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"];