supersam/src/components/orders/SmsStatusCard.jsx

289 lines
12 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 SmsStatusCard.jsx
* @description SMS status + restart button for a specific delivery group.
* Shows: current notification status, when SMS was sent, countdown to next SMS,
* and a "Restart SMS" button that resets this group's notification status.
*/
import React, { useState, useEffect, useCallback } from "react";
import { Panel } from "../UI/Panel";
import { Badge } from "../UI/Badge";
import { supabase } from "../../supabaseClient";
// ── Status labels ────────────────────────────────────────────────────────────
const NOTIF_LABELS = {
not_started: "Не отправлено",
link_ready: "В очереди на отправку",
sms_sending: "Отправляется…",
first_sms_sent: "1-е SMS отправлено",
second_sms_sending: "2-я SMS отправляется…",
second_sms_sent: "2-е SMS отправлено",
send_failed: "Ошибка отправки",
manual_required: "Требуется ручное управление",
paid_storage_sending: "Отправляется…",
paid_storage_sent: "Платное хранение: отправлено",
draft: "Черновик",
confirmed: "Клиент согласовал дату",
completed: "Завершено",
};
const NOTIF_TONES = {
not_started: "neutral",
link_ready: "warning",
sms_sending: "info",
first_sms_sent: "info",
second_sms_sending: "info",
second_sms_sent: "accent",
send_failed: "danger",
manual_required: "warning",
paid_storage_sending: "info",
paid_storage_sent: "accent",
draft: "neutral",
confirmed: "accent",
completed: "neutral",
};
// ── Helpers ──────────────────────────────────────────────────────────────────
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 fmtCountdown = (targetTs) => {
if (!targetTs) return null;
const diff = new Date(targetTs).getTime() - Date.now();
if (diff <= 0) return "готово к отправке";
const m = Math.floor(diff / 60000);
const s = Math.floor((diff % 60000) / 1000);
if (m >= 60) {
const h = Math.floor(m / 60);
const restM = m % 60;
return `${h}ч ${restM}м`;
}
return `${m}м ${s}с`;
};
// ── Component ────────────────────────────────────────────────────────────────
export const SmsStatusCard = ({ order, userRole }) => {
const [restarting, setRestarting] = useState(false);
const [restartDone, setRestartDone] = useState(false);
const [now, setNow] = useState(Date.now());
const [smsLog, setSmsLog] = useState([]);
const [loadingLog, setLoadingLog] = useState(false);
// Tick every 1s for countdown
useEffect(() => {
const timer = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(timer);
}, []);
// Load SMS log for this group
const loadSmsLog = useCallback(async () => {
if (!order?.id) return;
setLoadingLog(true);
try {
const { data, error } = await supabase
.from("sms_campaign_log")
.select("*")
.eq("order_group_id", order.id)
.order("created_at", { ascending: false })
.limit(10);
if (error) throw error;
setSmsLog(data || []);
} catch (e) {
// Silent fail — not critical
} finally {
setLoadingLog(false);
}
}, [order?.id]);
useEffect(() => { loadSmsLog(); }, [loadSmsLog]);
// Can manage?
const canManage = ["mega_admin", "admin"].includes(userRole);
const notifStatus = order.notificationStatus || order.notification_status || "not_started";
const nextCheck = order.nextNotificationCheckAt || order.next_notification_check_at;
const firstSmsAt = order.firstSmsSentAt || order.first_sms_sent_at;
const secondSmsAt = order.secondSmsSentAt || order.second_sms_sent_at;
const smsSentAt = order.smsSentAt || order.sms_sent_at;
const smsAttempts = order.smsAttempts ?? order.sms_attempts ?? 0;
const lastError = order.lastSmsError || order.last_sms_error;
// Restart: reset this group's notification_status to link_ready
const handleRestart = async () => {
if (!order?.id) return;
setRestarting(true);
try {
// Reset this group to link_ready — restart SMS flow from beginning
const { error: updateError } = await supabase
.from("order_groups")
.update({
notification_status: "link_ready",
sms_sent_at: null,
first_sms_sent_at: null,
second_sms_sent_at: null,
next_notification_check_at: null,
sms_attempts: 0,
last_sms_error: null,
})
.eq("id", order.id);
if (updateError) throw updateError;
setRestartDone(true);
setTimeout(() => setRestartDone(false), 3000);
// Reload log
loadSmsLog();
} catch (e) {
console.error("Restart SMS error:", e);
} finally {
setRestarting(false);
}
};
// Restart second SMS only (keep first SMS sent)
const handleRestartSecond = async () => {
if (!order?.id) return;
setRestarting(true);
try {
const { error: updateError } = await supabase
.from("order_groups")
.update({
notification_status: "first_sms_sent",
second_sms_sent_at: null,
next_notification_check_at: null,
sms_attempts: 0,
last_sms_error: null,
})
.eq("id", order.id);
if (updateError) throw updateError;
setRestartDone(true);
setTimeout(() => setRestartDone(false), 3000);
loadSmsLog();
} catch (e) {
console.error("Restart second SMS error:", e);
} finally {
setRestarting(false);
}
};
const countdown = nextCheck ? fmtCountdown(nextCheck) : null;
// Determine SMS display state
const hasSmsSent = !!smsSentAt; // SMS physically sent (may not be delivered yet)
const hasFirstSms = !!firstSmsAt; // First SMS confirmed delivered (103)
const hasSecondSms = !!secondSmsAt; // Second SMS confirmed delivered (103)
return (
<Panel className="p-4">
<div className="mb-3">
<h3 className="text-sm font-semibold text-[var(--color-text)]">📱 SMS-уведомления</h3>
</div>
{/* Timeline */}
<div className="space-y-2 text-xs">
{/* 1st SMS */}
<div className="flex items-start gap-2">
<span className={`mt-0.5 h-2 w-2 rounded-full ${hasFirstSms ? "bg-[#22c55e]" : hasSmsSent && notifStatus === "sms_sending" ? "bg-[var(--color-accent)]" : notifStatus === "link_ready" || notifStatus === "not_started" ? "bg-[var(--color-warning)]" : "bg-[var(--color-border)]"}`} />
<div className="flex-1">
<div className="text-[var(--color-text)]">1-е SMS</div>
{hasFirstSms ? (
<div className="text-[var(--color-text-muted)]">{fmtTime(firstSmsAt)} получено клиентом</div>
) : hasSmsSent && notifStatus === "sms_sending" ? (
<div className="text-[var(--color-text-muted)]">{fmtTime(smsSentAt)} · отправлено, ждём ответ оператора</div>
) : notifStatus === "link_ready" ? (
<div className="text-[var(--color-text-muted)]">в очереди на отправку</div>
) : (
<div className="text-[var(--color-text-muted)]">не отправлено</div>
)}
</div>
</div>
{/* 2nd SMS */}
<div className="flex items-start gap-2">
<span className={`mt-0.5 h-2 w-2 rounded-full ${hasSecondSms ? "bg-[#22c55e]" : notifStatus === "first_sms_sent" ? "bg-[var(--color-warning)]" : "bg-[var(--color-border)]"}`} />
<div className="flex-1">
<div className="text-[var(--color-text)]">2-е SMS</div>
{hasSecondSms ? (
<div className="text-[var(--color-text-muted)]">{fmtTime(secondSmsAt)} получено клиентом</div>
) : notifStatus === "first_sms_sent" && countdown ? (
<div className="text-[var(--color-text-muted)]">
отправка через <span className="font-mono text-[var(--color-accent)]">{countdown}</span>
</div>
) : (
<div className="text-[var(--color-text-muted)]"></div>
)}
</div>
</div>
{/* Next check countdown */}
{countdown && notifStatus !== "second_sms_sent" && (
<div className="flex items-center gap-2 rounded-lg bg-[var(--color-surface-strong)] px-2 py-1.5">
<span className="text-[var(--color-text-muted)]"> Следующая проверка:</span>
<span className="font-mono text-[var(--color-accent)]">{countdown}</span>
</div>
)}
{/* Error */}
{lastError && (
<div className="rounded-lg bg-[rgba(239,68,68,0.08)] px-2 py-1.5 text-[var(--color-danger)]">
{lastError}
</div>
)}
{/* Attempts */}
{smsAttempts > 0 && (
<div className="text-[var(--color-text-muted)]">Попыток отправки: {smsAttempts}</div>
)}
</div>
{/* SMS log for this group */}
{smsLog.length > 0 && (
<div className="mt-3 border-t border-[var(--color-border)] pt-3">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">История SMS</div>
<div className="space-y-1.5">
{smsLog.map((log) => (
<div key={log.id} className="flex items-center gap-2 text-[11px]">
<span className="text-[var(--color-text-muted)]">{fmtTime(log.created_at)}</span>
<Badge tone={log.status === "delivered" ? "accent" : log.status === "sent" || log.status === "checking" ? "info" : "danger"}>
{log.status === "delivered" ? "доставлено" : log.status === "sent" ? "отправлено" : log.status === "checking" ? "проверка" : log.status === "expired" ? "истёк" : log.status}
</Badge>
{log.sent_to && (
<span className="text-[var(--color-text-muted)]"> {log.sent_to}</span>
)}
{log.sms_code && (
<span className="text-[var(--color-text-muted)]">код: {log.sms_code}</span>
)}
</div>
))}
</div>
</div>
)}
{/* Restart buttons */}
{canManage && (
<div className="mt-3 flex gap-2 border-t border-[var(--color-border)] pt-3">
<button
type="button"
onClick={handleRestart}
disabled={restarting}
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"
>
{restarting ? "Сброс…" : restartDone ? "✓ Сброшено" : "🔄 Перезапустить SMS"}
</button>
{hasFirstSms && !hasSecondSms && (
<button
type="button"
onClick={handleRestartSecond}
disabled={restarting}
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"
>
Только 2-ю SMS
</button>
)}
</div>
)}
</Panel>
);
};