feat: SMS filter no_sms in queue + SW v55

This commit is contained in:
root 2026-07-03 13:26:13 +00:00
parent 6a34f36621
commit e93215f837
2 changed files with 165 additions and 50 deletions

View File

@ -1,8 +1,8 @@
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1"; const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
if (!isLocalhost) { if (!isLocalhost) {
const STATIC_CACHE = "construction-delivery-static-v54"; const STATIC_CACHE = "construction-delivery-static-v55";
const RUNTIME_CACHE = "construction-delivery-runtime-v54"; const RUNTIME_CACHE = "construction-delivery-runtime-v55";
const APP_SHELL_URLS = ["/", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"]; const APP_SHELL_URLS = ["/", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {

View File

@ -184,6 +184,15 @@ const passesLinkFilter = (isOpened, lf) => {
return true; return true;
}; };
// smsFilter: "no_sms" = group has no sms_sent_at and no first/second_sms_sent_at
const passesSmsFilter = (group, sf) => {
if (sf === "all" || !sf) return true;
if (sf === "no_sms") {
return !group.sms_sent_at && !group.first_sms_sent_at && !group.second_sms_sent_at;
}
return true;
};
const fmtCountdown = (ts) => { const fmtCountdown = (ts) => {
if (!ts) return "—"; if (!ts) return "—";
const diff = new Date(ts).getTime() - Date.now(); const diff = new Date(ts).getTime() - Date.now();
@ -332,10 +341,10 @@ const MiniToggle = ({ on, onClick, disabled }) => (
// KPI Tile // KPI Tile
const KpiTile = ({ icon, value, label, color }) => ( const KpiTile = ({ icon, value, label, color }) => (
<div className="rounded-xl bg-[var(--color-surface-strong)] border border-[var(--color-border)] px-3 py-2.5 text-center transition hover:shadow-sm"> <div className="rounded-xl bg-[var(--color-surface-strong)] border border-[var(--color-border)] px-3 py-3 text-center transition hover:shadow-sm">
<div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)] mb-1">{label}</div> <div className="text-xs uppercase tracking-wide text-[var(--color-text-muted)] mb-1.5">{label}</div>
<div className="text-base font-bold" style={{ color: color || "var(--color-text)" }}> <div className="text-lg font-bold" style={{ color: color || "var(--color-text)" }}>
{icon && <span className="mr-1 text-sm">{icon}</span>}{value} {icon && <span className="mr-1 text-base">{icon}</span>}{value}
</div> </div>
</div> </div>
); );
@ -372,13 +381,14 @@ const CardButton = ({ onClick, disabled, tone, children }) => {
const tones = { const tones = {
warning: "border-[var(--color-warning)] text-[var(--color-warning)] hover:bg-[var(--color-accent-soft)]", warning: "border-[var(--color-warning)] text-[var(--color-warning)] hover:bg-[var(--color-accent-soft)]",
accent: "border-[var(--color-accent)] text-[var(--color-accent)] hover:bg-[var(--color-accent-soft)]", accent: "border-[var(--color-accent)] text-[var(--color-accent)] hover:bg-[var(--color-accent-soft)]",
danger: "border-[var(--color-danger)] text-[var(--color-danger)] hover:bg-[rgba(239,68,68,0.08)]",
}; };
return ( return (
<button <button
type="button" type="button"
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
className={`rounded-xl border px-3 py-1.5 text-[11px] font-medium transition-all duration-200 disabled:opacity-40 ${tones[tone] || tones.accent}`} className={`rounded-xl border px-4 py-2 text-xs font-medium transition-all duration-200 disabled:opacity-40 ${tones[tone] || tones.accent}`}
> >
{children} {children}
</button> </button>
@ -434,6 +444,8 @@ export const SmsCampaignPanel = () => {
const [campaignCounts, setCampaignCounts] = useState({}); const [campaignCounts, setCampaignCounts] = useState({});
const [queueData, setQueueData] = useState([]); const [queueData, setQueueData] = useState([]);
const [queueCounts, setQueueCounts] = useState({}); const [queueCounts, setQueueCounts] = useState({});
const [lastSmsByCampaign, setLastSmsByCampaign] = useState({});
const [runningNow, setRunningNow] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [filter, setFilter] = useState("all"); const [filter, setFilter] = useState("all");
@ -446,6 +458,7 @@ export const SmsCampaignPanel = () => {
const [showSettingsPanel, setShowSettingsPanel] = useState(false); const [showSettingsPanel, setShowSettingsPanel] = useState(false);
const [dateFilter, setDateFilter] = useState("all"); // all|today|yesterday|7d const [dateFilter, setDateFilter] = useState("all"); // all|today|yesterday|7d
const [linkFilter, setLinkFilter] = useState("all"); // all|opened|not_opened const [linkFilter, setLinkFilter] = useState("all"); // all|opened|not_opened
const [smsFilter, setSmsFilter] = useState("all"); // all|no_sms
const [selectedIds, setSelectedIds] = useState(new Set()); const [selectedIds, setSelectedIds] = useState(new Set());
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const [showSmsPreview, setShowSmsPreview] = useState(false); const [showSmsPreview, setShowSmsPreview] = useState(false);
@ -487,6 +500,21 @@ export const SmsCampaignPanel = () => {
}); });
setCampaignCounts(counts); setCampaignCounts(counts);
// Load last SMS timestamp per campaign (non-test only)
const { data: lastSmsData, error: lastSmsErr } = await supabase
.from("sms_campaign_log")
.select("campaign_type, created_at")
.eq("was_test_mode", false)
.order("created_at", { ascending: false })
.limit(200);
const lastSmsMap = {};
if (!lastSmsErr && lastSmsData) {
lastSmsData.forEach(r => {
if (!lastSmsMap[r.campaign_type]) lastSmsMap[r.campaign_type] = r.created_at;
});
}
setLastSmsByCampaign(lastSmsMap);
const { data: queueCountsData, error: qErr } = await supabase const { data: queueCountsData, error: qErr } = await supabase
.from("order_groups") .from("order_groups")
.select("notification_status, delivery_status") .select("notification_status, delivery_status")
@ -705,6 +733,7 @@ export const SmsCampaignPanel = () => {
const [restarting, setRestarting] = useState(null); const [restarting, setRestarting] = useState(null);
const handleRestart = async (campaignKey) => { const handleRestart = async (campaignKey) => {
// Reset all groups in this campaign to beginning
const s = allSettings[campaignKey]; const s = allSettings[campaignKey];
if (!s) return; if (!s) return;
setRestarting(campaignKey); setRestarting(campaignKey);
@ -720,6 +749,24 @@ export const SmsCampaignPanel = () => {
finally { setTimeout(() => setRestarting(null), 3000); } finally { setTimeout(() => setRestarting(null), 3000); }
}; };
const handleRunNow = async (campaignKey) => {
// Trigger immediate run via run_requested flag
const s = allSettings[campaignKey];
if (!s) return;
setRunningNow(campaignKey);
try {
const { error: updateError } = await supabase
.from("sms_campaign_settings")
.update({ run_requested: true, updated_at: new Date().toISOString() })
.eq("id", s.id);
if (updateError) throw updateError;
// Quick refresh to show updated status
setTimeout(() => loadData(), 3000);
setTimeout(() => loadData(), 10000);
} catch (e) { setError(`Ошибка: ${e.message}`); }
finally { setTimeout(() => setRunningNow(null), 3000); }
};
const handleRecheck = async (logId) => { const handleRecheck = async (logId) => {
setCheckingIds(prev => new Set([...prev, logId])); setCheckingIds(prev => new Set([...prev, logId]));
try { try {
@ -807,16 +854,43 @@ export const SmsCampaignPanel = () => {
const isPaidStorageCampaign = activeCampaign === "paid_storage"; const isPaidStorageCampaign = activeCampaign === "paid_storage";
const showSmsFields = !isManualCampaign; const showSmsFields = !isManualCampaign;
const getRunnerStatus = (s) => { const getRunnerStatus = (s, lastSmsAt, queueCnt, timerActive) => {
if (!s) return { label: "—", color: "var(--color-text-muted)" }; if (!s) return { label: "—", color: "var(--color-text-muted)" };
if (!s.enabled) return { label: "⏸ Остановлена", color: "var(--color-text-muted)" }; if (!s.enabled) return { label: "⏸ Остановлена", color: "var(--color-text-muted)" };
if (!s.last_run_at) return { label: "⚠️ Не запускалась", color: "var(--color-danger)" }; // If timer active and queue is 0 all good, nothing to send
const lastRun = new Date(s.last_run_at).getTime(); if (timerActive && queueCnt === 0) {
const diffMin = Math.floor((Date.now() - lastRun) / 60000); const lastRunDiff = s.last_run_at ? Math.floor((Date.now() - new Date(s.last_run_at).getTime()) / 60000) : null;
if (diffMin < 10) return { label: `🟢 Работает (${diffMin}м назад)`, color: "#22c55e" }; if (lastRunDiff !== null && lastRunDiff < 10)
if (diffMin < 60) return { label: `🟡 ${diffMin}м назад`, color: "var(--color-warning)" }; return { label: `✅ Очереди нет (тик ${lastRunDiff}м)`, color: "#22c55e" };
if (diffMin < 1440) return { label: `🔴 ${Math.floor(diffMin/60)}ч назад`, color: "var(--color-danger)" }; if (lastRunDiff !== null && lastRunDiff < 60)
return { label: `🔴 ${Math.floor(diffMin/1440)}д назад`, color: "var(--color-danger)" }; return { label: `✅ Очереди нет (тик ${lastRunDiff}м назад)`, color: "#22c55e" };
return { label: "✅ Очереди нет", color: "#22c55e" };
}
// If timer active and queue > 0 waiting for delay, show calm status
if (timerActive && queueCnt > 0) {
if (lastSmsAt) {
const diffMin = Math.floor((Date.now() - new Date(lastSmsAt).getTime()) / 60000);
if (diffMin < 10) return { label: `🟢 SMS ${diffMin}м назад · ${queueCnt} в очереди`, color: "#22c55e" };
if (diffMin < 60) return { label: `⏳ SMS ${diffMin}м назад · ${queueCnt} в очереди`, color: "var(--color-warning)" };
if (diffMin < 1440) return { label: `⏳ SMS ${Math.floor(diffMin/60)}ч назад · ${queueCnt} ждут`, color: "var(--color-warning)" };
return { label: `${queueCnt} в очереди · SMS ${Math.floor(diffMin/1440)}д назад`, color: "var(--color-warning)" };
}
// No SMS in log but queue has items waiting to send
const lastRunDiff = s.last_run_at ? Math.floor((Date.now() - new Date(s.last_run_at).getTime()) / 60000) : null;
if (lastRunDiff !== null && lastRunDiff < 10)
return { label: `🟢 Запуск ${lastRunDiff}м назад · ${queueCnt} ждут`, color: "#22c55e" };
return { label: `${queueCnt} в очереди · ждём задержку`, color: "var(--color-warning)" };
}
// Timer NOT active show warning
if (!timerActive) return { label: "⏸ Таймер выключен", color: "var(--color-danger)" };
// Fallback
if (lastSmsAt) {
const diffMin = Math.floor((Date.now() - new Date(lastSmsAt).getTime()) / 60000);
if (diffMin < 10) return { label: `🟢 SMS ${diffMin}м назад`, color: "#22c55e" };
if (diffMin < 60) return { label: `🟡 SMS ${diffMin}м назад`, color: "var(--color-warning)" };
return { label: `🔴 SMS ${Math.floor(diffMin/1440)}д назад`, color: "var(--color-danger)" };
}
return { label: "⚠️ Не запускалась", color: "var(--color-danger)" };
}; };
const balance = allSettings.first_sms?.last_balance const balance = allSettings.first_sms?.last_balance
@ -861,43 +935,43 @@ export const SmsCampaignPanel = () => {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* ── Balance banner ─────────────────────────────────────────────────── */} {/* ── Balance banner ─────────────────────────────────────────────────── */}
<Panel className="p-4"> <Panel className="p-5">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-lg shrink-0"> <div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[var(--color-accent-soft)] text-xl shrink-0">
💰 💰
</div> </div>
<div> <div>
<div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Баланс sms.ru</div> <div className="text-xs uppercase tracking-wide text-[var(--color-text-muted)]">Баланс sms.ru</div>
<div className="text-base font-bold text-[var(--color-text)]"> <div className="text-xl font-bold text-[var(--color-text)]">
{balance != null ? `${Number(balance).toLocaleString("ru-RU")}` : "—"} {balance != null ? `${Number(balance).toLocaleString("ru-RU")}` : "—"}
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-lg shrink-0"> <div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[var(--color-accent-soft)] text-xl shrink-0">
📊 📊
</div> </div>
<div className="text-right"> <div>
<div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">SMS отправлено (боевые)</div> <div className="text-xs uppercase tracking-wide text-[var(--color-text-muted)]">SMS отправлено</div>
<div className="text-base font-bold text-[var(--color-text)]"> <div className="text-xl font-bold text-[var(--color-text)]">
{Object.values(campaignCounts).reduce((a, c) => a + (c?.total || 0), 0)} {Object.values(campaignCounts).reduce((a, c) => a + (c?.total || 0), 0)}
</div> </div>
</div> </div>
</div> </div>
{linkStats.total > 0 && ( {linkStats.total > 0 ? (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-lg shrink-0"> <div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[var(--color-accent-soft)] text-xl shrink-0">
👁 👁
</div> </div>
<div className="text-right"> <div>
<div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Открыли ссылку</div> <div className="text-xs uppercase tracking-wide text-[var(--color-text-muted)]">Открыли ссылку</div>
<div className="text-base font-bold text-[var(--color-text)]"> <div className="text-xl font-bold text-[var(--color-text)]">
{linkStats.opened} / {linkStats.total} {linkStats.opened} / {linkStats.total}
</div> </div>
</div> </div>
</div> </div>
)} ) : null}
</div> </div>
</Panel> </Panel>
@ -911,7 +985,7 @@ export const SmsCampaignPanel = () => {
const timerActive = s?.timer_active ?? false; const timerActive = s?.timer_active ?? false;
const cnt = campaignCounts[c.key] || { total: 0, delivered: 0, sent: 0, errors: 0 }; const cnt = campaignCounts[c.key] || { total: 0, delivered: 0, sent: 0, errors: 0 };
const queueCnt = queueCounts[c.key] || 0; const queueCnt = queueCounts[c.key] || 0;
const runner = getRunnerStatus(s); const runner = getRunnerStatus(s, lastSmsByCampaign[c.key], queueCnt, timerActive);
const isToggling = togglingCampaign === c.key; const isToggling = togglingCampaign === c.key;
const isTimerToggling = togglingTimer === c.key; const isTimerToggling = togglingTimer === c.key;
const isTestSending = testSending === c.key; const isTestSending = testSending === c.key;
@ -928,22 +1002,25 @@ export const SmsCampaignPanel = () => {
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2.5">
<span className="text-xl">{c.icon}</span> <span className="text-2xl">{c.icon}</span>
<span className="text-sm font-semibold text-[var(--color-text)]">{c.label}</span> <div>
<div className="text-sm font-semibold text-[var(--color-text)]">{c.label}</div>
<div className="text-xs text-[var(--color-text-muted)] mt-0.5">{c.desc}</div>
</div>
</div> </div>
<ToggleSwitch <ToggleSwitch
on={isEnabled} on={isEnabled}
onClick={() => toggleCampaignEnabled(c.key)} onClick={() => toggleCampaignEnabled(c.key)}
disabled={isToggling} disabled={isToggling}
size="sm" size="md"
/> />
</div> </div>
{/* Status badges */} {/* Status row */}
<div className="mt-2.5 flex flex-wrap items-center gap-1.5"> <div className="mt-3 flex flex-wrap items-center gap-2">
<span <span
className="rounded-full border px-2 py-0.5 text-[9px] font-semibold transition" className="rounded-full border px-2.5 py-1 text-xs font-semibold transition"
style={{ style={{
borderColor: !isEnabled ? "var(--color-border)" : isManual ? "var(--color-accent)" : isTest ? "var(--color-warning)" : "#22c55e", borderColor: !isEnabled ? "var(--color-border)" : isManual ? "var(--color-accent)" : isTest ? "var(--color-warning)" : "#22c55e",
color: !isEnabled ? "var(--color-text-muted)" : isManual ? "var(--color-accent)" : isTest ? "var(--color-warning)" : "#22c55e", color: !isEnabled ? "var(--color-text-muted)" : isManual ? "var(--color-accent)" : isTest ? "var(--color-warning)" : "#22c55e",
@ -951,19 +1028,42 @@ export const SmsCampaignPanel = () => {
> >
{!isEnabled ? "⏸ Выкл" : isManual ? "🔧 Ручное" : isTest ? "🧪 Тест" : "🚀 Боевой"} {!isEnabled ? "⏸ Выкл" : isManual ? "🔧 Ручное" : isTest ? "🧪 Тест" : "🚀 Боевой"}
</span> </span>
<span className="text-[9px] font-medium" style={{ color: runner.color }}>{runner.label}</span> <span className="text-xs font-medium" style={{ color: runner.color }}>{runner.label}</span>
</div> </div>
{/* Action buttons */} {/* Timer status bar */}
<div className="mt-3 flex flex-wrap items-center gap-1.5" onClick={(e) => e.stopPropagation()}> <div className={`mt-2.5 flex items-center gap-2 rounded-xl px-3 py-2 text-xs ${
timerActive
? queueCnt > 0
? "bg-[rgba(34,197,94,0.06)]"
: "bg-[var(--color-surface-strong)]"
: "bg-[rgba(239,68,68,0.06)]"
}`}>
{timerActive ? (
<>
<span className={`h-2 w-2 rounded-full ${queueCnt > 0 ? "bg-[#22c55e] animate-pulse" : "bg-[#22c55e]"} shrink-0`} />
<span className="text-[var(--color-text)]">
Таймер активен{queueCnt > 0 ? ` · ${queueCnt} в очереди` : " · очереди нет"}
</span>
</>
) : (
<>
<span className="h-2 w-2 rounded-full bg-[var(--color-danger)] shrink-0" />
<span className="text-[var(--color-danger)]">Таймер выключен SMS не отправляются</span>
</>
)}
</div>
{/* Action buttons — uniform size, full width row */}
<div className="mt-3 flex flex-wrap items-center gap-2" onClick={(e) => e.stopPropagation()}>
<button <button
type="button" type="button"
onClick={() => toggleTimer(c.key)} onClick={() => toggleTimer(c.key)}
disabled={isTimerToggling} disabled={isTimerToggling}
className={`flex items-center gap-1.5 rounded-xl px-3 py-1.5 text-[11px] font-medium transition-all duration-200 ${ /* bigger touch target */ className={`flex items-center gap-2 rounded-xl px-4 py-2 text-xs font-medium transition-all duration-200 ${
timerActive timerActive
? "bg-[rgba(34,197,94,0.12)] text-[#22c55e]" ? "bg-[rgba(34,197,94,0.12)] text-[#22c55e] border border-[rgba(34,197,94,0.2)]"
: "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]" : "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)] border border-[var(--color-border)]"
}`} }`}
> >
<MiniToggle on={timerActive} onClick={() => {}} disabled={isTimerToggling} /> <MiniToggle on={timerActive} onClick={() => {}} disabled={isTimerToggling} />
@ -978,10 +1078,17 @@ export const SmsCampaignPanel = () => {
{isTestSending ? "…" : "🧪 Тест"} {isTestSending ? "…" : "🧪 Тест"}
</CardButton> </CardButton>
)} )}
<CardButton
onClick={() => handleRunNow(c.key)}
disabled={runningNow === c.key}
tone="accent"
>
{runningNow === c.key ? "…" : "▶ Запустить"}
</CardButton>
<CardButton <CardButton
onClick={() => handleRestart(c.key)} onClick={() => handleRestart(c.key)}
disabled={restarting === c.key} disabled={restarting === c.key}
tone="accent" tone="danger"
> >
{restarting === c.key ? "…" : "🔄 Сброс"} {restarting === c.key ? "…" : "🔄 Сброс"}
</CardButton> </CardButton>
@ -1024,6 +1131,7 @@ export const SmsCampaignPanel = () => {
<FilterButton active={viewMode === "queue"} onClick={() => setViewMode("queue")}> Очередь отправки</FilterButton> <FilterButton active={viewMode === "queue"} onClick={() => setViewMode("queue")}> Очередь отправки</FilterButton>
</div> </div>
{/* ── Queue view ────────────────────────────────────────────────────── */} {/* ── Queue view ────────────────────────────────────────────────────── */}
{viewMode === "queue" && ( {viewMode === "queue" && (
<> <>
@ -1047,6 +1155,13 @@ export const SmsCampaignPanel = () => {
].map(opt => ( ].map(opt => (
<FilterButton key={opt.v} active={linkFilter === opt.v} onClick={() => setLinkFilter(opt.v)}>{opt.l}</FilterButton> <FilterButton key={opt.v} active={linkFilter === opt.v} onClick={() => setLinkFilter(opt.v)}>{opt.l}</FilterButton>
))} ))}
<span className="ml-3 text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">📨 SMS:</span>
{[
{ v: "all", l: "Все" },
{ v: "no_sms", l: "Без SMS" },
].map(opt => (
<FilterButton key={opt.v} active={smsFilter === opt.v} onClick={() => setSmsFilter(opt.v)}>{opt.l}</FilterButton>
))}
</div> </div>
{/* Desktop table */} {/* Desktop table */}
@ -1061,7 +1176,7 @@ export const SmsCampaignPanel = () => {
queueData.filter((g) => { queueData.filter((g) => {
const inv = g._invitation; const inv = g._invitation;
const opened = inv && inv.opened_at; const opened = inv && inv.opened_at;
return passesDateFilter(g.created_at, dateFilter) && passesLinkFilter(opened, linkFilter); return passesDateFilter(g.created_at, dateFilter) && passesLinkFilter(opened, linkFilter) && passesSmsFilter(g, smsFilter);
}).slice((queuePage - 1) * PAGE_SIZE, queuePage * PAGE_SIZE).map((g) => { }).slice((queuePage - 1) * PAGE_SIZE, queuePage * PAGE_SIZE).map((g) => {
const nextSend = calcNextSendTime(g, settings, activeCampaign); const nextSend = calcNextSendTime(g, settings, activeCampaign);
const inv = g._invitation; const inv = g._invitation;
@ -1176,7 +1291,7 @@ export const SmsCampaignPanel = () => {
queueData.filter((g) => { queueData.filter((g) => {
const inv = g._invitation; const inv = g._invitation;
const opened = inv && inv.opened_at; const opened = inv && inv.opened_at;
return passesDateFilter(g.created_at, dateFilter) && passesLinkFilter(opened, linkFilter); return passesDateFilter(g.created_at, dateFilter) && passesLinkFilter(opened, linkFilter) && passesSmsFilter(g, smsFilter);
}).map((g) => { }).map((g) => {
const nextSend = calcNextSendTime(g, settings, activeCampaign); const nextSend = calcNextSendTime(g, settings, activeCampaign);
const inv = g._invitation; const inv = g._invitation;
@ -1252,7 +1367,7 @@ export const SmsCampaignPanel = () => {
const filteredQueue = queueData.filter((g) => { const filteredQueue = queueData.filter((g) => {
const inv = g._invitation; const inv = g._invitation;
const opened = inv && inv.opened_at; const opened = inv && inv.opened_at;
return passesDateFilter(g.created_at, dateFilter) && passesLinkFilter(opened, linkFilter); return passesDateFilter(g.created_at, dateFilter) && passesLinkFilter(opened, linkFilter) && passesSmsFilter(g, smsFilter);
}); });
const totalPages = Math.ceil(filteredQueue.length / PAGE_SIZE); const totalPages = Math.ceil(filteredQueue.length / PAGE_SIZE);
if (totalPages <= 1) return null; if (totalPages <= 1) return null;
@ -1271,7 +1386,7 @@ export const SmsCampaignPanel = () => {
<button <button
type="button" type="button"
onClick={() => setShowSettingsPanel(!showSettingsPanel)} onClick={() => setShowSettingsPanel(!showSettingsPanel)}
className="flex w-full items-center justify-between rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-5 py-3 text-left transition hover:bg-[var(--color-surface-strong)]" className="flex w-full items-center justify-between rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-5 py-4 text-left transition hover:bg-[var(--color-surface-strong)]"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-semibold text-[var(--color-text)]"> <span className="text-sm font-semibold text-[var(--color-text)]">
@ -1775,7 +1890,7 @@ export const SmsCampaignPanel = () => {
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
onClick={loadData} onClick={loadData}
className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-2 text-xs font-medium text-[var(--color-text)] transition hover:bg-[var(--color-surface-strong)] hover:border-[var(--color-text-muted)]" className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-5 py-2.5 text-sm font-medium text-[var(--color-text)] transition hover:bg-[var(--color-surface-strong)] hover:border-[var(--color-text-muted)]"
> >
Обновить Обновить
</button> </button>