supersam/src/components/admin/SmsCityProfilesPanel.jsx

348 lines
18 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 SmsCityProfilesPanel.jsx
* @description Профили 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 THEME_STYLES = `
.sms-card {
background: #e8f0ec;
border: 1px solid #c8d8d2;
border-radius: 16px;
padding: 16px;
}
[data-theme="dark"] .sms-card {
background: #0d1a16;
border: 1px solid rgba(236,245,242,0.12);
}
.sms-inner {
background: #ffffff;
border: 1px solid #d0dad6;
border-radius: 10px;
padding: 12px;
}
[data-theme="dark"] .sms-inner {
background: #142822;
border: 1px solid rgba(236,245,242,0.08);
}
.sms-input {
background: #f4f7f5;
border: 1px solid #c8d8d2;
border-radius: 10px;
padding: 8px 12px;
color: #10211b;
width: 100%;
font-size: 14px;
font-family: inherit;
}
[data-theme="dark"] .sms-input {
background: #09110f;
border: 1px solid rgba(236,245,242,0.12);
color: #ecf5f2;
}
.sms-input::placeholder { color: #5d6d66; }
[data-theme="dark"] .sms-input::placeholder { color: #9eb0aa; }
.sms-label { font-size: 12px; font-weight: 500; color: #5d6d66; }
[data-theme="dark"] .sms-label { color: #9eb0aa; }
.sms-city-name { font-size: 16px; font-weight: 700; color: #10211b; }
[data-theme="dark"] .sms-city-name { color: #ecf5f2; }
.sms-section-title { font-size: 14px; font-weight: 600; color: #10211b; }
[data-theme="dark"] .sms-section-title { color: #ecf5f2; }
.sms-muted { font-size: 12px; color: #5d6d66; }
[data-theme="dark"] .sms-muted { color: #9eb0aa; }
.sms-text { font-size: 13px; color: #10211b; }
[data-theme="dark"] .sms-text { color: #ecf5f2; }
.sms-italic { font-size: 12px; color: #5d6d66; font-style: italic; line-height: 1.4; margin-top: 6px; }
[data-theme="dark"] .sms-italic { color: #9eb0aa; }
.sms-toggle-text { font-size: 13px; color: #10211b; }
[data-theme="dark"] .sms-toggle-text { color: #ecf5f2; }
.sms-btn {
border-radius: 12px; padding: 8px 16px; font-size: 12px; font-weight: 600;
color: #fff; border: none; cursor: pointer; background: #12805c;
}
[data-theme="dark"] .sms-btn { background: #57d8a9; color: #06120d; }
.sms-btn-sm {
border-radius: 8px; border: 1px solid #c8d8d2; padding: 4px 10px;
font-size: 12px; font-weight: 500; color: #10211b; cursor: pointer; background: transparent;
}
[data-theme="dark"] .sms-btn-sm { border: 1px solid rgba(236,245,242,0.12); color: #ecf5f2; }
.sms-btn-del {
border-radius: 8px; padding: 4px 8px; font-size: 14px;
color: #5d6d66; cursor: pointer; background: transparent; border: none;
}
[data-theme="dark"] .sms-btn-del { color: #9eb0aa; }
.sms-mode-btn-active-g {
flex: 1; border-radius: 10px; border: 1px solid #22c55e;
background: rgba(34,197,94,0.1); color: #22c55e;
padding: 8px 12px; font-size: 14px; font-weight: 500; cursor: pointer;
}
.sms-mode-btn-inactive {
flex: 1; border-radius: 10px; border: 1px solid #c8d8d2;
background: transparent; color: #5d6d66;
padding: 8px 12px; font-size: 14px; font-weight: 500; cursor: pointer;
}
[data-theme="dark"] .sms-mode-btn-inactive {
border: 1px solid rgba(236,245,242,0.12); color: #9eb0aa;
}
.sms-mode-btn-active-b {
flex: 1; border-radius: 10px; border: 1px solid #3b82f6;
background: rgba(59,130,246,0.1); color: #3b82f6;
padding: 8px 12px; font-size: 14px; font-weight: 500; cursor: pointer;
}
`;
export const SmsCityProfilesPanel = () => {
const [profiles, setProfiles] = useState([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState("");
const [formCity, setFormCity] = useState("Ялта");
const [formDeliveryMode, setFormDeliveryMode] = useState("notification");
const [formDeliveryText, setFormDeliveryText] = useState("");
const [formPickupMode, setFormPickupMode] = useState("notification");
const [formPickupText, setFormPickupText] = useState("");
const [formSecondSms, setFormSecondSms] = useState(true);
const [formPaidStorage, setFormPaidStorage] = useState(true);
const [formSmsEnabled, setFormSmsEnabled] = useState(true);
const [formNote, setFormNote] = useState("");
const [editingId, setEditingId] = useState(null);
const fetchProfiles = useCallback(async () => {
setLoading(true);
try {
const { data, error } = await supabase
.from("sms_city_profiles")
.select("*")
.order("city", { ascending: true });
if (error) throw error;
setProfiles(data || []);
} catch (e) {
console.error("fetch sms_city_profiles error", e);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchProfiles(); }, [fetchProfiles]);
const resetForm = () => {
setFormCity("Ялта"); setFormDeliveryMode("notification"); setFormDeliveryText("");
setFormPickupMode("notification"); setFormPickupText("");
setFormSecondSms(true); setFormPaidStorage(true); setFormSmsEnabled(true);
setFormNote(""); setEditingId(null);
};
const handleSave = async () => {
setSaving(true);
try {
const payload = {
city: formCity, delivery_mode: formDeliveryMode, delivery_text: formDeliveryText || null,
pickup_mode: formPickupMode, pickup_text: formPickupText || null,
second_sms_enabled: formSecondSms, paid_storage_enabled: formPaidStorage,
sms_enabled: formSmsEnabled, note: formNote || "",
};
if (editingId) {
const { error } = await supabase.from("sms_city_profiles").update(payload).eq("id", editingId);
if (error) throw error;
setMessage("✅ Профиль обновлён");
} else {
const { error } = await supabase.from("sms_city_profiles").insert(payload);
if (error) throw error;
setMessage("✅ Профиль добавлен");
}
setShowForm(false); resetForm(); fetchProfiles();
setTimeout(() => setMessage(""), 3000);
} catch (e) {
setMessage("❌ Ошибка: " + e.message);
} finally { setSaving(false); }
};
const handleEdit = (p) => {
setEditingId(p.id); setFormCity(p.city);
setFormDeliveryMode(p.delivery_mode || "link"); setFormDeliveryText(p.delivery_text || "");
setFormPickupMode(p.pickup_mode || "link"); setFormPickupText(p.pickup_text || "");
setFormSecondSms(p.second_sms_enabled ?? true); setFormPaidStorage(p.paid_storage_enabled ?? true);
setFormSmsEnabled(p.sms_enabled ?? true); setFormNote(p.note || "");
setShowForm(true);
};
const handleDelete = async (id) => {
try { const { error } = await supabase.from("sms_city_profiles").delete().eq("id", id); if (error) throw error; fetchProfiles(); }
catch (e) { console.error("delete error", e); }
};
const ModeBadge = ({ mode }) => mode === "notification"
? <span style={{ backgroundColor: "rgba(59,130,246,0.15)", color: "#3b82f6", borderRadius: "6px", padding: "2px 8px", fontSize: "10px", fontWeight: 600 }}>Уведомление</span>
: <span style={{ backgroundColor: "rgba(34,197,94,0.15)", color: "#22c55e", borderRadius: "6px", padding: "2px 8px", fontSize: "10px", fontWeight: 600 }}>Ссылка</span>;
const Toggle = ({ checked, onChange, label }) => (
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<button type="button" onClick={onChange} style={{
position: "relative", height: "20px", width: "40px", borderRadius: "9999px",
transition: "all 0.2s", flexShrink: 0, border: "none", cursor: "pointer",
backgroundColor: checked ? "#22c55e" : "#c8d8d2",
}}>
<span style={{ position: "absolute", top: "2px", height: "16px", width: "16px", borderRadius: "9999px", backgroundColor: "#fff", transition: "all 0.2s", left: checked ? "20px" : "2px" }} />
</button>
{label && <span className="sms-toggle-text">{label}</span>}
</div>
);
return (
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
<style>{THEME_STYLES}</style>
{/* Header */}
<Panel className="p-5">
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<span style={{ fontSize: "20px" }}>🏙</span>
<div>
<h2 className="sms-section-title" style={{ textTransform: "uppercase", letterSpacing: "0.16em" }}>
Профили SMS по городам
</h2>
<p className="sms-muted" style={{ marginTop: "4px" }}>
Настройка поведения SMS для каждого города.
</p>
</div>
</div>
<button type="button" className="sms-btn" onClick={() => { setShowForm(!showForm); if (!showForm) resetForm(); }}>
{showForm ? "Отмена" : "+ Профиль"}
</button>
</div>
{message && <span className="sms-text" style={{ display: "block", marginTop: "8px" }}>{message}</span>}
</Panel>
{/* Form */}
{showForm && (
<Panel className="p-5" style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
<h3 className="sms-section-title">{editingId ? "Редактировать профиль" : "Новый профиль города"}</h3>
<div style={{ display: "grid", gap: "16px", gridTemplateColumns: "1fr 1fr" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
<label className="sms-label">Город</label>
<select value={formCity} onChange={(e) => setFormCity(e.target.value)} disabled={!!editingId}
className="sms-input" style={{ opacity: editingId ? 0.5 : 1 }}>
{CRIMEAN_CITIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div className="sms-inner">
<Toggle checked={formSmsEnabled} onChange={() => setFormSmsEnabled(!formSmsEnabled)} label="SMS включены для города" />
{!formSmsEnabled && <p style={{ fontSize: "12px", color: "#c93d3d", marginTop: "8px" }}> Все SMS в этот город отключены</p>}
</div>
</div>
{/* Delivery */}
<div className="sms-inner">
<div className="sms-muted" style={{ fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: "12px" }}>📦 Доставка</div>
<div style={{ display: "flex", gap: "8px" }}>
<button type="button" className={formDeliveryMode === "link" ? "sms-mode-btn-active-g" : "sms-mode-btn-inactive"} onClick={() => setFormDeliveryMode("link")}>🔗 Ссылка</button>
<button type="button" className={formDeliveryMode === "notification" ? "sms-mode-btn-active-b" : "sms-mode-btn-inactive"} onClick={() => setFormDeliveryMode("notification")}>📢 Уведомление</button>
</div>
{formDeliveryMode === "notification" && (
<textarea value={formDeliveryText} onChange={(e) => setFormDeliveryText(e.target.value)} placeholder="Ваш заказ готов. Доставим по графику." rows={2} className="sms-input" style={{ resize: "none", marginTop: "8px" }} />
)}
<p className="sms-muted" style={{ marginTop: "8px" }}>
{formDeliveryMode === "link" ? "Клиент получит ссылку для выбора даты" : "Уведомление без ссылки, без 2-й SMS"}
</p>
</div>
{/* Pickup */}
<div className="sms-inner">
<div className="sms-muted" style={{ fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: "12px" }}>🏪 Самовывоз</div>
<div style={{ display: "flex", gap: "8px" }}>
<button type="button" className={formPickupMode === "link" ? "sms-mode-btn-active-g" : "sms-mode-btn-inactive"} onClick={() => setFormPickupMode("link")}>🔗 Ссылка</button>
<button type="button" className={formPickupMode === "notification" ? "sms-mode-btn-active-b" : "sms-mode-btn-inactive"} onClick={() => setFormPickupMode("notification")}>📢 Уведомление</button>
</div>
{formPickupMode === "notification" && (
<textarea value={formPickupText} onChange={(e) => setFormPickupText(e.target.value)} placeholder="Ваш заказ готов. Заберите со склада." rows={2} className="sms-input" style={{ resize: "none", marginTop: "8px" }} />
)}
</div>
<div style={{ display: "grid", gap: "12px", gridTemplateColumns: "1fr 1fr" }}>
<div className="sms-inner"><Toggle checked={formSecondSms} onChange={() => setFormSecondSms(!formSecondSms)} label="2-я SMS (повтор)" /></div>
<div className="sms-inner"><Toggle checked={formPaidStorage} onChange={() => setFormPaidStorage(!formPaidStorage)} label="SMS: платное хранение" /></div>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
<label className="sms-label">Комментарий</label>
<input type="text" value={formNote} onChange={(e) => setFormNote(e.target.value)} placeholder="Например: Ялта — доставка без согласования" className="sms-input" />
</div>
<button type="button" disabled={saving} onClick={handleSave} className="sms-btn" style={{ padding: "10px 20px", fontSize: "14px", opacity: saving ? 0.4 : 1 }}>
{saving ? "Сохранение..." : editingId ? "Обновить профиль" : "Сохранить профиль"}
</button>
</Panel>
)}
{/* Profiles list */}
<Panel className="p-5" style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
<h2 className="sms-section-title" style={{ textTransform: "uppercase", letterSpacing: "0.16em" }}>
Профили ({profiles.length})
</h2>
{loading ? (
<p className="sms-muted">Загрузка...</p>
) : profiles.length === 0 ? (
<div style={{ border: "1px dashed #c8d8d2", borderRadius: "12px", padding: "24px", textAlign: "center" }}>
<p className="sms-muted">Профилей нет. Города без профиля используют стандартный флоу (ссылка на согласование).</p>
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
{profiles.map((p) => (
<div key={p.id} className="sms-card">
{/* Header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "12px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<span className="sms-city-name">{p.city}</span>
{!p.sms_enabled
? <span style={{ backgroundColor: "rgba(239,68,68,0.15)", color: "#ef4444", borderRadius: "6px", padding: "2px 8px", fontSize: "10px", fontWeight: 600 }}>SMS OFF</span>
: <span style={{ backgroundColor: "rgba(34,197,94,0.15)", color: "#22c55e", borderRadius: "6px", padding: "2px 8px", fontSize: "10px", fontWeight: 600 }}>SMS ON</span>}
</div>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<button type="button" className="sms-btn-sm" onClick={() => handleEdit(p)}> Изменить</button>
<button type="button" className="sms-btn-del" onClick={() => handleDelete(p.id)} title="Удалить"></button>
</div>
</div>
{/* Config */}
<div style={{ display: "grid", gap: "8px", gridTemplateColumns: "1fr 1fr", marginBottom: "12px" }}>
<div className="sms-inner">
<div style={{ display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" }}>
<span className="sms-text" style={{ fontSize: "12px", fontWeight: 500 }}>📦 Доставка</span>
<ModeBadge mode={p.delivery_mode} />
</div>
{p.delivery_text && <p className="sms-italic">"{p.delivery_text}"</p>}
</div>
<div className="sms-inner">
<div style={{ display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" }}>
<span className="sms-text" style={{ fontSize: "12px", fontWeight: 500 }}>🏪 Самовывоз</span>
<ModeBadge mode={p.pickup_mode} />
</div>
{p.pickup_text && <p className="sms-italic">"{p.pickup_text}"</p>}
</div>
</div>
{/* Toggles */}
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "16px", paddingTop: "8px", borderTop: "1px solid #c8d8d2" }}>
<span className="sms-text" style={{ fontSize: "12px", fontWeight: 500, opacity: p.second_sms_enabled ? 1 : 0.5 }}>
2-я SMS: {p.second_sms_enabled ? "✅" : "❌"}
</span>
<span className="sms-text" style={{ fontSize: "12px", fontWeight: 500, opacity: p.paid_storage_enabled ? 1 : 0.5 }}>
Платное хранение: {p.paid_storage_enabled ? "✅" : "❌"}
</span>
</div>
{p.note && <p className="sms-muted" style={{ paddingTop: "8px" }}>💬 {p.note}</p>}
</div>
))}
</div>
)}
</Panel>
</div>
);
};