diff --git a/src/components/orders/OrderHistoryTimeline.jsx b/src/components/orders/OrderHistoryTimeline.jsx index ecd7096..eddff77 100644 --- a/src/components/orders/OrderHistoryTimeline.jsx +++ b/src/components/orders/OrderHistoryTimeline.jsx @@ -103,6 +103,32 @@ const SMS_STATUS_LABELS = { pending: "в очереди", }; +// ─── Автодозвон: outcome → человеческий статус ───────────────────── +const AUTOCALL_OUTCOME_LABELS = { + confirmed: { label: "Автодозвон: клиент согласовал слот", tone: "ok", icon: "🤖" }, + confirmed_silent: { label: "Автодозвон: подтвердил молчанием", tone: "ok", icon: "🤖" }, + confirmed_kept_previous:{ label: "Автодозвон: клиент правил выбор", tone: "warning", icon: "🤖" }, + no_answer: { label: "Автодозвон: не взял трубку", tone: "danger", icon: "🤖" }, + no_input_day: { label: "Автодозвон: не нажал (день)", tone: "warning", icon: "🤖" }, + no_input_half: { label: "Автодозвон: не нажал (время)", tone: "warning", icon: "🤖" }, + operator: { label: "Автодозвон: клиент попросил менеджера", tone: "warning", icon: "🤖" }, + opt_out: { label: "Автодозвон: клиент отказался от звонков", tone: "danger", icon: "🤖" }, + invalid_number: { label: "Автодозвон: неверный номер", tone: "danger", icon: "🤖" }, + error: { label: "Автодозвон: ошибка звонка", tone: "danger", icon: "🤖" }, +}; + +const AUTOCALL_DAY_LABEL = { 1: "завтра", 2: "послезавтра" }; +const AUTOCALL_HALF_LABEL = { 1: "первая половина дня", 2: "вторая половина дня" }; + +const autocallSlotText = (l) => { + if (!l) return null; + const day = AUTOCALL_DAY_LABEL[l.dtmf_day]; + const half = AUTOCALL_HALF_LABEL[l.dtmf_half]; + if (day && half) return `${day}, ${half}`; + if (day) return day; + return null; +}; + const fmtTimestamp = (ts) => { if (!ts) return "—"; try { @@ -172,7 +198,7 @@ const STAGES_PICKUP = [ { key: "picked_up", icon: "📦", label: "Вывезено" }, ]; -const getActiveStageIndex = (order, smsLogs = []) => { +const getActiveStageIndex = (order, smsLogs = [], autocallLogs = []) => { const deliveryStatus = order?.deliveryStatus || order?.delivery_status || ""; const notifStatus = order?.notificationStatus || order?.notification_status || ""; const firstSms = order?.firstSmsSentAt || order?.first_sms_sent_at; @@ -197,6 +223,12 @@ const getActiveStageIndex = (order, smsLogs = []) => { return 2; } + // Автодозвон: если робот согласовал слот — это уже «Согласовано» + const hasAutocallConfirmed = autocallLogs.some((l) => ["confirmed", "confirmed_silent", "confirmed_kept_previous"].includes(l.outcome)); + if (hasAutocallConfirmed) { + return 2; + } + // SMS stage counts as done when any first_sms record exists (even failed), not just when delivered const hasFirstSmsLog = smsLogs.some((l) => l.campaign_type === "first_sms"); if (firstSms || hasFirstSmsLog || ["first_sms_sent", "second_sms_sent", "confirmed"].includes(notifStatus)) { @@ -206,10 +238,10 @@ const getActiveStageIndex = (order, smsLogs = []) => { return 0; }; -const ProgressStepper = ({ order, smsLogs = [] }) => { +const ProgressStepper = ({ order, smsLogs = [], autocallLogs = [] }) => { const isPickup = (order?.deliveryType || order?.delivery_type) === "pickup"; const stages = isPickup ? STAGES_PICKUP : STAGES_DELIVERY; - const activeIdx = getActiveStageIndex(order, smsLogs); + const activeIdx = getActiveStageIndex(order, smsLogs, autocallLogs); if (activeIdx === -1) { return ( @@ -276,7 +308,7 @@ const ProgressStepper = ({ order, smsLogs = [] }) => { }; // ─── Current Status Summary (compact card above timeline) ─────────── -const StatusSummary = ({ order, smsLogs = [] }) => { +const StatusSummary = ({ order, smsLogs = [], autocallLogs = [] }) => { const isPickup = (order?.deliveryType || order?.delivery_type) === "pickup"; const deliveryStatus = order?.deliveryStatus || order?.delivery_status || ""; const notifStatus = order?.notificationStatus || order?.notification_status || ""; @@ -350,6 +382,22 @@ const StatusSummary = ({ order, smsLogs = [] }) => { items.push({ icon: "📞", label: "Звонок", value: "не звонили", tone: "muted" }); } + // Автодозвон — итог по последнему звонку робота + const lastAutocall = autocallLogs && autocallLogs.length > 0 ? autocallLogs[0] : null; + if (lastAutocall) { + const acMeta = AUTOCALL_OUTCOME_LABELS[lastAutocall.outcome] || { label: lastAutocall.outcome || "—", tone: "muted" }; + const acSlot = autocallSlotText(lastAutocall); + const acValue = acSlot ? `${acMeta.label} · ${acSlot}` : acMeta.label; + items.push({ + icon: "🤖", + label: "Автодозвон", + value: `${acValue}${lastAutocall.was_test_mode ? " · 🧪 тест" : ""}`, + tone: acMeta.tone, + }); + } else { + items.push({ icon: "🤖", label: "Автодозвон", value: "не звонили", tone: "muted" }); + } + // Driver (delivery only) if (!isPickup) { if (driverName) { @@ -408,6 +456,7 @@ const StatusSummary = ({ order, smsLogs = [] }) => { export const OrderHistoryTimeline = ({ order, userRole }) => { const [history, setHistory] = useState(null); const [smsLogs, setSmsLogs] = useState([]); + const [autocallLogs, setAutocallLogs] = useState([]); const [pushLogs, setPushLogs] = useState([]); const [pushSubscriptions, setPushSubscriptions] = useState([]); const [cityProfile, setCityProfile] = useState(null); @@ -440,6 +489,18 @@ export const OrderHistoryTimeline = ({ order, userRole }) => { if (!smsError) setSmsLogs(smsData || []); } catch { if (!cancelled) setSmsLogs([]); } + // Автодозвон — лог робо-звонков + try { + const { data: acData, error: acError } = await supabase + .from("autocall_log") + .select("id, outcome, attempt_no, scenario, dtmf_day, dtmf_half, changed, was_test_mode, sip_code, duration_sec, created_at") + .eq("order_group_id", order.id) + .order("created_at", { ascending: false }) + .limit(50); + if (cancelled) return; + if (!acError) setAutocallLogs(acData || []); + } catch { if (!cancelled) setAutocallLogs([]); } + // Push is stored on the client phone, so it is available for every current and future order. const phoneNormalized = String(order?.customerPhoneNormalized || order?.customer_phone_normalized || order?.customerPhone || order?.customer_phone || "").replace(/\D/g, "").replace(/^8/, "7"); if (phoneNormalized) { @@ -561,6 +622,26 @@ export const OrderHistoryTimeline = ({ order, userRole }) => { } } + if (autocallLogs && autocallLogs.length > 0) { + for (const ac of autocallLogs) { + const meta = AUTOCALL_OUTCOME_LABELS[ac.outcome] || { label: `Автодозвон: ${ac.outcome || "—"}`, tone: "muted", icon: "🤖" }; + const slot = autocallSlotText(ac); + events.push({ + id: `autocall-${ac.id}`, + created_at: ac.created_at, + type: "autocall", + label: meta.label, + tone: meta.tone, + slot, + attemptNo: ac.attempt_no, + wasTest: ac.was_test_mode, + changed: ac.changed, + sipCode: ac.sip_code, + durationSec: ac.duration_sec, + }); + } + } + if (pushLogs && pushLogs.length > 0) { for (const push of pushLogs) { events.push({ @@ -609,7 +690,7 @@ export const OrderHistoryTimeline = ({ order, userRole }) => { events.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); return events; - }, [history, smsLogs, pushLogs, invitation, cityProfile]); + }, [history, smsLogs, autocallLogs, pushLogs, invitation, cityProfile]); const activePushSubscriptions = pushSubscriptions.filter((sub) => sub.is_active); const latestPush = pushLogs[0] || null; @@ -618,8 +699,8 @@ export const OrderHistoryTimeline = ({ order, userRole }) => { return (
+ + {ev.tone === "ok" ? "успешно" : ev.tone === "danger" ? "неуспешно" : ev.tone === "warning" ? "требует внимания" : "—"} + +
+ {ev.slot &&Слот: {ev.slot}
} + {ev.changed && ( +⚠ Клиент правил выбор в звонке
+ )} ++ Попытка {ev.attemptNo || "—"} + {ev.durationSec ? ` · длительность ${ev.durationSec}с` : ""} + {ev.sipCode ? ` · SIP ${ev.sipCode}` : ""} + {ev.wasTest ? " · 🧪 тест" : ""} +
+ > + )} + {ev.type === "reason" && (