feat: SMS filter no_sms in queue + SW v55
This commit is contained in:
parent
6a34f36621
commit
e93215f837
|
|
@ -1,8 +1,8 @@
|
|||
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
|
||||
|
||||
if (!isLocalhost) {
|
||||
const STATIC_CACHE = "construction-delivery-static-v54";
|
||||
const RUNTIME_CACHE = "construction-delivery-runtime-v54";
|
||||
const STATIC_CACHE = "construction-delivery-static-v55";
|
||||
const RUNTIME_CACHE = "construction-delivery-runtime-v55";
|
||||
const APP_SHELL_URLS = ["/", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
|
|
|
|||
|
|
@ -184,6 +184,15 @@ const passesLinkFilter = (isOpened, lf) => {
|
|||
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) => {
|
||||
if (!ts) return "—";
|
||||
const diff = new Date(ts).getTime() - Date.now();
|
||||
|
|
@ -332,10 +341,10 @@ const MiniToggle = ({ on, onClick, disabled }) => (
|
|||
|
||||
// ── KPI Tile ─────────────────────────────────────────────────────────────────
|
||||
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="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)] mb-1">{label}</div>
|
||||
<div className="text-base font-bold" style={{ color: color || "var(--color-text)" }}>
|
||||
{icon && <span className="mr-1 text-sm">{icon}</span>}{value}
|
||||
<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-xs uppercase tracking-wide text-[var(--color-text-muted)] mb-1.5">{label}</div>
|
||||
<div className="text-lg font-bold" style={{ color: color || "var(--color-text)" }}>
|
||||
{icon && <span className="mr-1 text-base">{icon}</span>}{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -372,13 +381,14 @@ const CardButton = ({ onClick, disabled, tone, children }) => {
|
|||
const tones = {
|
||||
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)]",
|
||||
danger: "border-[var(--color-danger)] text-[var(--color-danger)] hover:bg-[rgba(239,68,68,0.08)]",
|
||||
};
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
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}
|
||||
</button>
|
||||
|
|
@ -434,6 +444,8 @@ export const SmsCampaignPanel = () => {
|
|||
const [campaignCounts, setCampaignCounts] = useState({});
|
||||
const [queueData, setQueueData] = useState([]);
|
||||
const [queueCounts, setQueueCounts] = useState({});
|
||||
const [lastSmsByCampaign, setLastSmsByCampaign] = useState({});
|
||||
const [runningNow, setRunningNow] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
|
@ -446,6 +458,7 @@ export const SmsCampaignPanel = () => {
|
|||
const [showSettingsPanel, setShowSettingsPanel] = useState(false);
|
||||
const [dateFilter, setDateFilter] = useState("all"); // all|today|yesterday|7d
|
||||
const [linkFilter, setLinkFilter] = useState("all"); // all|opened|not_opened
|
||||
const [smsFilter, setSmsFilter] = useState("all"); // all|no_sms
|
||||
const [selectedIds, setSelectedIds] = useState(new Set());
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [showSmsPreview, setShowSmsPreview] = useState(false);
|
||||
|
|
@ -487,6 +500,21 @@ export const SmsCampaignPanel = () => {
|
|||
});
|
||||
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
|
||||
.from("order_groups")
|
||||
.select("notification_status, delivery_status")
|
||||
|
|
@ -705,6 +733,7 @@ export const SmsCampaignPanel = () => {
|
|||
|
||||
const [restarting, setRestarting] = useState(null);
|
||||
const handleRestart = async (campaignKey) => {
|
||||
// Reset all groups in this campaign to beginning
|
||||
const s = allSettings[campaignKey];
|
||||
if (!s) return;
|
||||
setRestarting(campaignKey);
|
||||
|
|
@ -720,6 +749,24 @@ export const SmsCampaignPanel = () => {
|
|||
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) => {
|
||||
setCheckingIds(prev => new Set([...prev, logId]));
|
||||
try {
|
||||
|
|
@ -807,16 +854,43 @@ export const SmsCampaignPanel = () => {
|
|||
const isPaidStorageCampaign = activeCampaign === "paid_storage";
|
||||
const showSmsFields = !isManualCampaign;
|
||||
|
||||
const getRunnerStatus = (s) => {
|
||||
const getRunnerStatus = (s, lastSmsAt, queueCnt, timerActive) => {
|
||||
if (!s) 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)" };
|
||||
const lastRun = new Date(s.last_run_at).getTime();
|
||||
const diffMin = Math.floor((Date.now() - lastRun) / 60000);
|
||||
if (diffMin < 10) return { label: `🟢 Работает (${diffMin}м назад)`, color: "#22c55e" };
|
||||
if (diffMin < 60) return { label: `🟡 ${diffMin}м назад`, color: "var(--color-warning)" };
|
||||
if (diffMin < 1440) return { label: `🔴 ${Math.floor(diffMin/60)}ч назад`, color: "var(--color-danger)" };
|
||||
return { label: `🔴 ${Math.floor(diffMin/1440)}д назад`, color: "var(--color-danger)" };
|
||||
// If timer active and queue is 0 — all good, nothing to send
|
||||
if (timerActive && queueCnt === 0) {
|
||||
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}м)`, color: "#22c55e" };
|
||||
if (lastRunDiff !== null && lastRunDiff < 60)
|
||||
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
|
||||
|
|
@ -861,43 +935,43 @@ export const SmsCampaignPanel = () => {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
{/* ── Balance banner ─────────────────────────────────────────────────── */}
|
||||
<Panel className="p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<Panel className="p-5">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-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-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Баланс sms.ru</div>
|
||||
<div className="text-base font-bold text-[var(--color-text)]">
|
||||
<div className="text-xs uppercase tracking-wide text-[var(--color-text-muted)]">Баланс sms.ru</div>
|
||||
<div className="text-xl font-bold text-[var(--color-text)]">
|
||||
{balance != null ? `${Number(balance).toLocaleString("ru-RU")} ₽` : "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 className="text-right">
|
||||
<div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">SMS отправлено (боевые)</div>
|
||||
<div className="text-base font-bold text-[var(--color-text)]">
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wide text-[var(--color-text-muted)]">SMS отправлено</div>
|
||||
<div className="text-xl font-bold text-[var(--color-text)]">
|
||||
{Object.values(campaignCounts).reduce((a, c) => a + (c?.total || 0), 0)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{linkStats.total > 0 && (
|
||||
{linkStats.total > 0 ? (
|
||||
<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 className="text-right">
|
||||
<div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Открыли ссылку</div>
|
||||
<div className="text-base font-bold text-[var(--color-text)]">
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wide text-[var(--color-text-muted)]">Открыли ссылку</div>
|
||||
<div className="text-xl font-bold text-[var(--color-text)]">
|
||||
{linkStats.opened} / {linkStats.total}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
|
|
@ -911,7 +985,7 @@ export const SmsCampaignPanel = () => {
|
|||
const timerActive = s?.timer_active ?? false;
|
||||
const cnt = campaignCounts[c.key] || { total: 0, delivered: 0, sent: 0, errors: 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 isTimerToggling = togglingTimer === c.key;
|
||||
const isTestSending = testSending === c.key;
|
||||
|
|
@ -928,22 +1002,25 @@ export const SmsCampaignPanel = () => {
|
|||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{c.icon}</span>
|
||||
<span className="text-sm font-semibold text-[var(--color-text)]">{c.label}</span>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="text-2xl">{c.icon}</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>
|
||||
<ToggleSwitch
|
||||
on={isEnabled}
|
||||
onClick={() => toggleCampaignEnabled(c.key)}
|
||||
disabled={isToggling}
|
||||
size="sm"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status badges */}
|
||||
<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
|
||||
{/* Status row */}
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
<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={{
|
||||
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",
|
||||
|
|
@ -951,19 +1028,42 @@ export const SmsCampaignPanel = () => {
|
|||
>
|
||||
{!isEnabled ? "⏸ Выкл" : isManual ? "🔧 Ручное" : isTest ? "🧪 Тест" : "🚀 Боевой"}
|
||||
</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>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="mt-3 flex flex-wrap items-center gap-1.5" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Timer status bar */}
|
||||
<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
|
||||
type="button"
|
||||
onClick={() => toggleTimer(c.key)}
|
||||
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
|
||||
? "bg-[rgba(34,197,94,0.12)] text-[#22c55e]"
|
||||
: "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]"
|
||||
? "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)] border border-[var(--color-border)]"
|
||||
}`}
|
||||
>
|
||||
<MiniToggle on={timerActive} onClick={() => {}} disabled={isTimerToggling} />
|
||||
|
|
@ -978,10 +1078,17 @@ export const SmsCampaignPanel = () => {
|
|||
{isTestSending ? "…" : "🧪 Тест"}
|
||||
</CardButton>
|
||||
)}
|
||||
<CardButton
|
||||
onClick={() => handleRunNow(c.key)}
|
||||
disabled={runningNow === c.key}
|
||||
tone="accent"
|
||||
>
|
||||
{runningNow === c.key ? "…" : "▶ Запустить"}
|
||||
</CardButton>
|
||||
<CardButton
|
||||
onClick={() => handleRestart(c.key)}
|
||||
disabled={restarting === c.key}
|
||||
tone="accent"
|
||||
tone="danger"
|
||||
>
|
||||
{restarting === c.key ? "…" : "🔄 Сброс"}
|
||||
</CardButton>
|
||||
|
|
@ -1024,6 +1131,7 @@ export const SmsCampaignPanel = () => {
|
|||
<FilterButton active={viewMode === "queue"} onClick={() => setViewMode("queue")}>⏳ Очередь отправки</FilterButton>
|
||||
</div>
|
||||
|
||||
|
||||
{/* ── Queue view ────────────────────────────────────────────────────── */}
|
||||
{viewMode === "queue" && (
|
||||
<>
|
||||
|
|
@ -1047,6 +1155,13 @@ export const SmsCampaignPanel = () => {
|
|||
].map(opt => (
|
||||
<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>
|
||||
|
||||
{/* Desktop table */}
|
||||
|
|
@ -1061,7 +1176,7 @@ export const SmsCampaignPanel = () => {
|
|||
queueData.filter((g) => {
|
||||
const inv = g._invitation;
|
||||
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) => {
|
||||
const nextSend = calcNextSendTime(g, settings, activeCampaign);
|
||||
const inv = g._invitation;
|
||||
|
|
@ -1176,7 +1291,7 @@ export const SmsCampaignPanel = () => {
|
|||
queueData.filter((g) => {
|
||||
const inv = g._invitation;
|
||||
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) => {
|
||||
const nextSend = calcNextSendTime(g, settings, activeCampaign);
|
||||
const inv = g._invitation;
|
||||
|
|
@ -1252,7 +1367,7 @@ export const SmsCampaignPanel = () => {
|
|||
const filteredQueue = queueData.filter((g) => {
|
||||
const inv = g._invitation;
|
||||
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);
|
||||
if (totalPages <= 1) return null;
|
||||
|
|
@ -1271,7 +1386,7 @@ export const SmsCampaignPanel = () => {
|
|||
<button
|
||||
type="button"
|
||||
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">
|
||||
<span className="text-sm font-semibold text-[var(--color-text)]">
|
||||
|
|
@ -1775,7 +1890,7 @@ export const SmsCampaignPanel = () => {
|
|||
<div className="flex justify-end">
|
||||
<button
|
||||
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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue