supersam/src/components/admin/SmsCityRulesPanel.jsx

337 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 SmsCityRulesPanel.jsx
* @description Управление SMS-отправкой по городам.
* Админ может:
* - Включить/выключить SMS для города × кампания (постоянно)
* - Указать период (с/по дату) — временно
* - Добавить комментарий
* - «Все города» (*) или «Все кампании» (*) — массовое правило
*/
import React, { useState, useEffect, useCallback } from "react";
import { Panel } from "../UI/Panel";
import { supabase } from "../../supabaseClient";
import { CRIMEAN_CITIES } from "../../constants/cities.js";
const CAMPAIGN_LABELS = {
first_sms: "1-е SMS",
second_sms: "2-е SMS",
paid_storage: "Платное хранение",
"*": "Все кампании",
};
const CITY_ALL = "*";
export const SmsCityRulesPanel = () => {
const [rules, setRules] = useState([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState("");
// Form state
const [formCity, setFormCity] = useState("Ялта");
const [formCampaign, setFormCampaign] = useState("*");
const [formEnabled, setFormEnabled] = useState(false);
const [formFrom, setFormFrom] = useState("");
const [formUntil, setFormUntil] = useState("");
const [formNote, setFormNote] = useState("");
const fetchRules = useCallback(async () => {
setLoading(true);
try {
const { data, error } = await supabase
.from("sms_city_rules")
.select("*")
.order("city", { ascending: true })
.order("campaign_type", { ascending: true });
if (error) throw error;
setRules(data || []);
} catch (e) {
console.error("fetch sms_city_rules error", e);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchRules(); }, [fetchRules]);
const handleSave = async () => {
setSaving(true);
try {
const { error } = await supabase.from("sms_city_rules").insert({
city: formCity,
campaign_type: formCampaign,
enabled: formEnabled,
valid_from: formFrom || null,
valid_until: formUntil || null,
note: formNote || "",
});
if (error) throw error;
setShowForm(false);
setFormNote("");
setFormFrom("");
setFormUntil("");
setMessage("✅ Правило добавлено");
fetchRules();
setTimeout(() => setMessage(""), 3000);
} catch (e) {
setMessage("❌ Ошибка: " + e.message);
} finally {
setSaving(false);
}
};
const handleToggle = async (rule) => {
try {
const { error } = await supabase
.from("sms_city_rules")
.update({ enabled: !rule.enabled })
.eq("id", rule.id);
if (error) throw error;
fetchRules();
} catch (e) {
console.error("toggle error", e);
}
};
const handleDelete = async (id) => {
try {
const { error } = await supabase
.from("sms_city_rules")
.delete()
.eq("id", id);
if (error) throw error;
fetchRules();
} catch (e) {
console.error("delete error", e);
}
};
const fmtDate = (d) => {
if (!d) return "—";
try {
return new Date(d).toLocaleDateString("ru-RU", { day: "2-digit", month: "short", year: "numeric" });
} catch { return d; }
};
return (
<div className="space-y-4">
{/* Header */}
<Panel className="p-5 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-xl">🏙</span>
<div>
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-text)]">
SMS по городам
</h2>
<p className="text-xs text-[var(--color-text-muted)] mt-1">
Управление отправкой SMS по городам и кампаниям. Выключенное правило = SMS не отправляется.
</p>
</div>
</div>
<button
type="button"
onClick={() => setShowForm(!showForm)}
className="rounded-xl bg-[var(--color-accent)] px-4 py-2 text-xs font-semibold text-white transition hover:opacity-90"
>
{showForm ? "Отмена" : "+ Правило"}
</button>
</div>
{message && <span className="text-sm text-[var(--color-text-muted)]">{message}</span>}
</Panel>
{/* Add rule form */}
{showForm && (
<Panel className="p-5 space-y-4">
<h3 className="text-sm font-semibold text-[var(--color-text)]">Новое правило</h3>
<div className="grid gap-3 sm:grid-cols-2">
{/* City */}
<div className="space-y-1">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Город</label>
<select
value={formCity}
onChange={(e) => setFormCity(e.target.value)}
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm text-[var(--color-text)]"
>
<option value={CITY_ALL}> Все города</option>
{CRIMEAN_CITIES.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
{/* Campaign */}
<div className="space-y-1">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Кампания</label>
<select
value={formCampaign}
onChange={(e) => setFormCampaign(e.target.value)}
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm text-[var(--color-text)]"
>
<option value="*"> Все кампании</option>
<option value="first_sms">1-е SMS</option>
<option value="second_sms">2-е SMS</option>
<option value="paid_storage">Платное хранение</option>
</select>
</div>
{/* Enabled toggle */}
<div className="space-y-1">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Статус</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => setFormEnabled(true)}
className={["rounded-xl border px-4 py-2 text-sm font-medium transition",
formEnabled ? "border-[#22c55e] bg-[rgba(34,197,94,0.12)] text-[#22c55e]" : "border-[var(--color-border)] text-[var(--color-text-muted)]"].join(" ")}
>
Включить
</button>
<button
type="button"
onClick={() => setFormEnabled(false)}
className={["rounded-xl border px-4 py-2 text-sm font-medium transition",
!formEnabled ? "border-[#ef4444] bg-[rgba(239,68,68,0.12)] text-[#ef4444]" : "border-[var(--color-border)] text-[var(--color-text-muted)]"].join(" ")}
>
Выключить
</button>
</div>
</div>
{/* Period */}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<label className="text-xs font-medium text-[var(--color-text-muted)]">С даты</label>
<input
type="date"
value={formFrom}
onChange={(e) => setFormFrom(e.target.value)}
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm text-[var(--color-text)]"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-[var(--color-text-muted)]">По дату</label>
<input
type="date"
value={formUntil}
onChange={(e) => setFormUntil(e.target.value)}
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm text-[var(--color-text)]"
/>
</div>
</div>
</div>
{/* Note */}
<div className="space-y-1">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Комментарий</label>
<input
type="text"
value={formNote}
onChange={(e) => setFormNote(e.target.value)}
placeholder="Например: Ялта временно отключена"
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm text-[var(--color-text)] placeholder:text-[var(--color-text-muted)]"
/>
</div>
<p className="text-xs text-[var(--color-text-muted)]">
Пустые даты = правило действует постоянно. Указав даты, можно временно отключить SMS на период.
</p>
<button
type="button"
disabled={saving}
onClick={handleSave}
className="rounded-xl bg-[var(--color-accent)] px-5 py-2.5 text-sm font-semibold text-white transition hover:opacity-90 disabled:opacity-40"
>
{saving ? "Сохранение..." : "Сохранить правило"}
</button>
</Panel>
)}
{/* Rules table */}
<Panel className="p-5 space-y-3">
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-text)]">
Правила ({rules.length})
</h2>
{loading ? (
<p className="text-sm text-[var(--color-text-muted)]">Загрузка...</p>
) : rules.length === 0 ? (
<div className="rounded-xl border border-dashed border-[var(--color-border)] p-6 text-center">
<p className="text-sm text-[var(--color-text-muted)]">
Правил пока нет. SMS отправляются во все города.
</p>
<p className="text-xs text-[var(--color-text-muted)] mt-1">
Нажмите «+ Правило» чтобы отключить SMS для конкретного города.
</p>
</div>
) : (
<div className="space-y-2">
{rules.map((rule) => {
const isActive = rule.enabled &&
(!rule.valid_from || new Date(rule.valid_from) <= new Date()) &&
(!rule.valid_until || new Date(rule.valid_until) >= new Date());
return (
<div
key={rule.id}
className="flex items-center justify-between gap-3 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-4"
>
<div className="flex items-center gap-3 flex-1">
{/* Toggle */}
<button
type="button"
onClick={() => handleToggle(rule)}
className={[
"relative h-6 w-11 rounded-full transition shrink-0",
rule.enabled ? "bg-[#22c55e]" : "bg-[var(--color-border)]"
].join(" ")}
>
<span className={[
"absolute top-0.5 h-5 w-5 rounded-full bg-white transition-all",
rule.enabled ? "left-[22px]" : "left-0.5"
].join(" ")} />
</button>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-[var(--color-text)]">
{rule.city === CITY_ALL ? "⭐ Все города" : rule.city}
</span>
<span className="text-xs text-[var(--color-text-muted)]">·</span>
<span className="text-xs font-medium text-[var(--color-text-muted)]">
{CAMPAIGN_LABELS[rule.campaign_type] || rule.campaign_type}
</span>
<span
className="rounded-lg px-2 py-0.5 text-[10px] font-semibold uppercase"
style={{
backgroundColor: isActive ? "rgba(34,197,94,0.12)" : "rgba(239,68,68,0.12)",
color: isActive ? "#22c55e" : "#ef4444",
}}
>
{isActive ? "Активно" : rule.enabled ? "Запланировано" : "Выключено"}
</span>
</div>
{(rule.valid_from || rule.valid_until) && (
<div className="text-xs text-[var(--color-text-muted)] mt-1">
📅 {fmtDate(rule.valid_from)} {fmtDate(rule.valid_until)}
</div>
)}
{rule.note && (
<div className="text-xs text-[var(--color-text-muted)] mt-0.5">
💬 {rule.note}
</div>
)}
</div>
</div>
{/* Delete */}
<button
type="button"
onClick={() => handleDelete(rule.id)}
className="rounded-lg p-1.5 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)] hover:text-[var(--color-danger)] transition shrink-0"
title="Удалить правило"
>
</button>
</div>
);
})}
</div>
)}
</Panel>
</div>
);
};