feat: manual campaign tab shows order_groups instead of empty sms_campaign_log

- Manual campaign doesn't write to sms_campaign_log, so the tab was always empty
- Now loads order_groups with notification_status='manual_required'
- Shows: customer name, phone, first/second SMS timestamps, last update
- Red 'не отправлена' badge for groups where second SMS was never sent
- Also shows manual count on the campaign card
This commit is contained in:
root 2026-06-30 15:48:52 +00:00
parent 94f534ded3
commit 5e02f61567
1 changed files with 90 additions and 11 deletions

View File

@ -109,6 +109,7 @@ export const SmsCampaignPanel = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [activeCampaign, setActiveCampaign] = useState("first_sms"); const [activeCampaign, setActiveCampaign] = useState("first_sms");
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [manualGroups, setManualGroups] = useState([]);
const [settings, setSettings] = useState(null); const [settings, setSettings] = useState(null);
const [allSettings, setAllSettings] = useState({}); const [allSettings, setAllSettings] = useState({});
const [campaignCounts, setCampaignCounts] = useState({}); const [campaignCounts, setCampaignCounts] = useState({});
@ -152,19 +153,43 @@ export const SmsCampaignPanel = () => {
if (r.status === "sent" || r.status === "checking") counts[r.campaign_type].sent++; if (r.status === "sent" || r.status === "checking") counts[r.campaign_type].sent++;
if (["send_failed", "error", "limit_exceeded"].includes(r.status)) counts[r.campaign_type].errors++; if (["send_failed", "error", "limit_exceeded"].includes(r.status)) counts[r.campaign_type].errors++;
}); });
// Also count manual_required groups for the manual card
const { count: manualCount, error: manualCountErr } = await supabase
.from("order_groups")
.select("id", { count: "exact", head: true })
.eq("notification_status", "manual_required");
if (!manualCountErr && manualCount != null) {
counts["manual"] = counts["manual"] || { total: 0, delivered: 0, sent: 0, errors: 0 };
counts["manual"].total = manualCount;
}
setCampaignCounts(counts); setCampaignCounts(counts);
// Load logs for active campaign // For "manual" campaign: load order_groups with manual_required status
let query = supabase if (activeCampaign === "manual") {
.from("sms_campaign_log") const { data: manualData, error: manualErr } = await supabase
.select("*") .from("order_groups")
.eq("campaign_type", activeCampaign) .select("id, customer_name, customer_phone, notification_status, first_sms_sent_at, second_sms_sent_at, next_notification_check_at, sms_attempts, last_sms_error, delivery_status, delivery_link, created_at, updated_at")
.order("created_at", { ascending: false }) .eq("notification_status", "manual_required")
.limit(200); .order("updated_at", { ascending: false })
if (filter !== "all") query = query.eq("status", filter); .limit(200);
const { data: logData, error: logError } = await query; if (manualErr) throw manualErr;
if (logError) throw logError; setManualGroups(manualData || []);
setLogs(logData || []); setLogs([]); // No SMS logs for manual campaign
} else {
setManualGroups([]);
// Load logs for active campaign
let query = supabase
.from("sms_campaign_log")
.select("*")
.eq("campaign_type", activeCampaign)
.order("created_at", { ascending: false })
.limit(200);
if (filter !== "all") query = query.eq("status", filter);
const { data: logData, error: logError } = await query;
if (logError) throw logError;
setLogs(logData || []);
}
// Load settings for active campaign // Load settings for active campaign
if (HAS_SETTINGS.includes(activeCampaign)) { if (HAS_SETTINGS.includes(activeCampaign)) {
@ -550,6 +575,58 @@ export const SmsCampaignPanel = () => {
</Panel> </Panel>
)} )}
{/* ── Manual campaign: groups table ─────────────────────────────────── */}
{isManualCampaign ? (
<>
<Panel className="p-4">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-[var(--color-text-muted)]">В ручном управлении: {manualGroups.length}</span>
</div>
</Panel>
<Panel className="p-0">
<div className="overflow-x-auto">
<div className="min-w-[800px]">
<div className="grid grid-cols-[minmax(160px,2fr)_minmax(120px,1fr)_minmax(120px,1fr)_minmax(120px,1fr)_minmax(100px,0.8fr)] gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.08em] text-[var(--color-text-muted)]">
<div className="px-3 py-1.5 font-medium">Клиент</div>
<div className="px-3 py-1.5 font-medium">Телефон</div>
<div className="px-3 py-1.5 font-medium">1-я SMS</div>
<div className="px-3 py-1.5 font-medium">2-я SMS</div>
<div className="px-3 py-1.5 font-medium">Обновлён</div>
</div>
{manualGroups.length === 0 ? (
<div className="px-4 py-6 text-xs text-[var(--color-text-muted)]">Нет групп в ручном управлении</div>
) : (
manualGroups.map((group) => (
<div
key={group.id}
onClick={() => handleOpenGroup(group.id)}
className="grid grid-cols-[minmax(160px,2fr)_minmax(120px,1fr)_minmax(120px,1fr)_minmax(120px,1fr)_minmax(100px,0.8fr)] gap-0 border-t border-[var(--color-border)] text-xs hover:bg-[var(--color-accent-soft)] cursor-pointer"
>
<div className="px-3 py-1.5 text-[var(--color-text)]">
<div className="font-medium">{group.customer_name || "—"}</div>
{group.last_sms_error && <div className="mt-0.5 text-[10px] text-[var(--color-danger)]">{group.last_sms_error.slice(0, 60)}</div>}
</div>
<div className="px-3 py-1.5 text-[var(--color-text-muted)]">{fmtPhone(group.customer_phone)}</div>
<div className="px-3 py-1.5 text-[var(--color-text-muted)]">
{group.first_sms_sent_at ? fmtTime(group.first_sms_sent_at) : "—"}
</div>
<div className="px-3 py-1.5 text-[var(--color-text-muted)]">
{group.second_sms_sent_at ? fmtTime(group.second_sms_sent_at) : <span className="text-[var(--color-danger)]">не отправлена</span>}
</div>
<div className="px-3 py-1.5 text-[var(--color-text-muted)]">
{fmtTime(group.updated_at)}
<div className="text-[10px]">{fmtElapsed(group.updated_at)}</div>
</div>
</div>
))
)}
</div>
</div>
</Panel>
</>
) : (
<>
{/* ── Stats ─────────────────────────────────────────────────────────── */} {/* ── Stats ─────────────────────────────────────────────────────────── */}
<Panel className="p-4"> <Panel className="p-4">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
@ -649,6 +726,8 @@ export const SmsCampaignPanel = () => {
Обновить Обновить
</button> </button>
</div> </div>
</>
)}
</div> </div>
); );
}; };