supersam/src/components/orders/AutocallStatusCard.jsx

191 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @file AutocallStatusCard.jsx
* @description Блок «Автодозвон» в карточке заказа.
* Показывает попытки звонков, слот, кнопки «Позвонить сейчас» / «Не звонить».
*/
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: "✅" },
confirmed_kept_previous:{ label: "Правка не доведена", tone: "warning", icon: "🔁" },
confirmed_silent: { label: "Подтвердил молчанием", tone: "accent", icon: "🤫" },
no_answer: { label: "Не взял трубку", tone: "neutral", icon: "📵" },
no_input_day: { label: "Взял, не нажал (день)", tone: "warning", icon: "🤐" },
no_input_half: { label: "Взял, не нажал (время)", tone: "warning", icon: "🤐" },
operator: { label: "Просил менеджера", tone: "danger", icon: "👤" },
opt_out: { label: "Отказ от звонков", tone: "danger", icon: "🚫" },
invalid_number: { label: "Неверный номер", tone: "danger", icon: "❌" },
error: { label: "Ошибка", tone: "danger", icon: "⚠️" },
};
const DAY_LABEL = { 1: "завтра", 2: "послезавтра" };
const HALF_LABEL = { 1: "первая половина дня", 2: "вторая половина дня" };
const fmtTime = (ts) => {
if (!ts) return "—";
try {
return new Date(ts).toLocaleString("ru-RU", {
day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit",
});
} catch { return ts; }
};
const slotText = (l) => {
if (!l) return null;
const day = DAY_LABEL[l.dtmf_day];
const half = HALF_LABEL[l.dtmf_half];
if (day && half) return `${day}, ${half}`;
if (day) return day;
return null;
};
export const AutocallStatusCard = ({ order, userRole }) => {
const [calls, setCalls] = useState([]);
const [loading, setLoading] = useState(false);
const [toggling, setToggling] = useState(false);
const [queued, setQueued] = useState(false);
const canManage = ["mega_admin", "logistician"].includes(userRole);
const optOut = order.autocall_opt_out ?? order.autocallOptOut ?? false;
const isAutocall = order.delivery_date_source === "autocall" || order.deliveryDateSource === "autocall";
const exclusionReason = order.autocall_exclusion_reason || order.autocallExclusionReason || null;
const loadCalls = useCallback(async () => {
if (!order?.id) return;
setLoading(true);
try {
const { data, error } = await supabase
.from("autocall_log")
.select("*")
.eq("order_group_id", order.id)
.order("created_at", { ascending: false })
.limit(5);
if (error) throw error;
setCalls(data || []);
} catch (e) {
// silent
} finally {
setLoading(false);
}
}, [order?.id]);
useEffect(() => { loadCalls(); }, [loadCalls]);
const handleCallNow = async () => {
if (!order?.id) return;
setQueued(true);
try {
// Пишем в очередь — планировщик подхватит и создаст call-файл (без ssh из edge fn)
const { error } = await supabase.from("autocall_queue").insert({
order_group_id: order.id,
phone: order.customer_phone || order.customerPhone || "",
scenario: "A",
});
if (error) throw error;
setTimeout(() => setQueued(false), 3000);
} catch (e) {
console.error("Queue call error:", e);
setQueued(false);
}
};
const handleToggleOptOut = async () => {
if (!order?.id) return;
setToggling(true);
try {
const { error } = await supabase
.from("order_groups")
.update({ autocall_opt_out: !optOut })
.eq("id", order.id);
if (error) throw error;
// Обновим локально
order.autocall_opt_out = !optOut;
setToggling(false);
} catch (e) {
console.error("Toggle opt-out error:", e);
setToggling(false);
}
};
const lastCall = calls[0];
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">
<Badge tone={statusMeta.tone}>{statusMeta.label}</Badge>
{lastSlot && <span className="text-xs text-[var(--color-text)]">{lastSlot}</span>}
</div>
{/* Пояснение исключения */}
{exclusionReason && !optOut && (
<div className="mb-3 rounded-lg bg-[rgba(191,123,33,0.08)] px-2.5 py-1.5 text-[11px] text-[var(--color-warning)]">
🚫 Робот не звонит на этот номер по настройке: {exclusionReason}
</div>
)}
{/* Попытки */}
{calls.length > 0 ? (
<div className="space-y-1.5">
{calls.map((c) => {
const m = OUTCOME_META[c.outcome] || { label: c.outcome, tone: "neutral", icon: "•" };
const slot = slotText(c);
return (
<div key={c.id} className="flex items-center gap-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] px-2.5 py-1.5 text-[11px]">
<span className="text-[var(--color-text-muted)]">{fmtTime(c.created_at)}</span>
<Badge tone={m.tone}>{m.icon} {m.label}</Badge>
{slot && <span className="text-[var(--color-text)]">{slot}</span>}
{c.was_test_mode && <span className="text-[10px] text-[var(--color-warning)]">🧪 тест</span>}
</div>
);
})}
</div>
) : (
<div className="text-xs text-[var(--color-text-muted)]">Звонков не было</div>
)}
{/* Подсказка о правке */}
{lastCall?.changed && (
<div className="mt-2 rounded-lg bg-[rgba(245,158,11,0.08)] px-2 py-1.5 text-[11px] text-[var(--color-warning)]">
⚠ Клиент правил выбор в звонке. При сомнении — перезвоните.
</div>
)}
{/* Кнопки */}
{canManage && (
<div className="mt-3 flex gap-2 border-t border-[var(--color-border)] pt-3">
<button
type="button"
onClick={handleCallNow}
disabled={queued || optOut}
className="rounded-xl border border-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-[var(--color-accent)] hover:bg-[var(--color-accent-soft)] disabled:opacity-50"
>
{queued ? "✓ В очереди" : "📞 Позвонить сейчас"}
</button>
<button
type="button"
onClick={handleToggleOptOut}
disabled={toggling}
className="rounded-xl border border-[var(--color-border)] px-3 py-1.5 text-xs font-medium text-[var(--color-text)] hover:bg-[var(--color-surface-strong)] disabled:opacity-50"
>
{optOut ? "🔔 Вернуть в автодозвон" : "🚫 Не звонить"}
</button>
</div>
)}
</div>
);
};