feat(autocall): статус автодозвона в карточке (как у SMS) + фильтр «Прозвонить» в таблице + выравнивание переключателя
This commit is contained in:
parent
1552c21d99
commit
64bcda8eed
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="p-4">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-semibold text-[var(--color-text)]">📞 Автодозвон</h3>
|
||||
</div>
|
||||
|
||||
{/* Итог */}
|
||||
{/* Единый статус — как у SMS */}
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
{optOut ? (
|
||||
<Badge tone="danger">🚫 Отказ от звонков</Badge>
|
||||
) : exclusionReason ? (
|
||||
<Badge tone="warning">🚫 Не звоним (исключение)</Badge>
|
||||
) : isAutocall ? (
|
||||
<Badge tone="accent">🤖 Согласовано роботом</Badge>
|
||||
) : calls.length > 0 ? (
|
||||
<Badge tone={lastMeta?.tone || "neutral"}>{lastMeta?.icon} {lastMeta?.label}</Badge>
|
||||
) : (
|
||||
<Badge tone="neutral">Не звонили</Badge>
|
||||
)}
|
||||
<Badge tone={statusMeta.tone}>{statusMeta.label}</Badge>
|
||||
{lastSlot && <span className="text-xs text-[var(--color-text)]">{lastSlot}</span>}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Panel className="p-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
|
|
@ -205,10 +212,10 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 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. */}
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
{/* Row 2: Date range + SMS problems + прозвонить + hide-completed toggle.
|
||||
Выравнивание по верхней линии полей ввода — чтобы переключатель
|
||||
«Скрыть завершённые» стоял на уровне лейблов, а не прижимался к низу. */}
|
||||
<div className="flex flex-wrap items-start gap-3">
|
||||
<DatePicker
|
||||
value={filters.dateFrom || ""}
|
||||
onChange={(v) => updateFilter("dateFrom", v)}
|
||||
|
|
@ -240,6 +247,13 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
|||
placeholder="Проблемы SMS"
|
||||
/>
|
||||
|
||||
<DropdownSelect
|
||||
value={filters.callStatus || "all"}
|
||||
options={callStatusOptions}
|
||||
onChange={(v) => updateFilter("callStatus", v)}
|
||||
placeholder="Прозвонить"
|
||||
/>
|
||||
|
||||
{typeof onToggleHideCompleted === "function" && (
|
||||
<Switch
|
||||
checked={hideCompleted}
|
||||
|
|
|
|||
|
|
@ -261,6 +261,30 @@ export const getSmsProblemTone = (group) => {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
Loading…
Reference in New Issue