import React, { useState, useEffect } from 'react'; import { useUser } from '../context/UserContext'; import EditGroupModal from '../modals/EditGroupModal'; import CreateGroupModal from '../modals/CreateGroupModal'; import EditSubscriberModal from '../modals/EditSubscriberModal'; import CreateSubscriberModal from '../modals/CreateSubscriberModal'; import Paginator from '../components/Paginator'; const PAGE_SIZE = 10; function GroupsPage() { const { token, user } = useUser(); const [groups, setGroups] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [editGroup, setEditGroup] = useState(null); const [editLoading, setEditLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(null); const [createGroup, setCreateGroup] = useState(null); const [createLoading, setCreateLoading] = useState(false); const [selectedGroup, setSelectedGroup] = useState(null); useEffect(() => { if (!selectedGroup) fetchGroups(page); // eslint-disable-next-line }, [page, selectedGroup]); const fetchGroups = async (page) => { setLoading(true); setError(''); try { const offset = (page - 1) * PAGE_SIZE; const res = await fetch(`/api/mail/mailing-groups?limit=${PAGE_SIZE}&offset=${offset}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} }); const data = await res.json(); if (!res.ok) { setError(data.error || 'Ошибка загрузки'); setGroups([]); setTotal(0); } else { setGroups(Array.isArray(data.rows) ? data.rows : []); setTotal(data.count || 0); } } catch (e) { setError('Ошибка сети'); setGroups([]); setTotal(0); } finally { setLoading(false); } }; const handleDelete = async (id) => { if (!window.confirm('Удалить группу?')) return; setDeleteLoading(id); try { const res = await fetch(`/api/mail/mailing-groups/${id}`, { method: 'DELETE', headers: token ? { Authorization: `Bearer ${token}` } : {} }); if (!res.ok) { const data = await res.json(); alert(data.error || 'Ошибка удаления'); } else { fetchGroups(page); } } catch (e) { alert('Ошибка сети'); } finally { setDeleteLoading(null); } }; const handleEdit = (group) => { setEditGroup(group); }; const handleEditSave = async (e) => { e.preventDefault(); setEditLoading(true); try { const res = await fetch(`/api/mail/mailing-groups/${editGroup.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify({ ...editGroup, user_id: user?.id }) }); if (!res.ok) { const data = await res.json(); alert(data.error || 'Ошибка обновления'); } else { setEditGroup(null); fetchGroups(page); } } catch (e) { alert('Ошибка сети'); } finally { setEditLoading(false); } }; const handleCreate = () => { setCreateGroup({ name: '', description: '' }); }; const handleCreateSave = async (e) => { e.preventDefault(); setCreateLoading(true); try { const res = await fetch('/api/mail/mailing-groups', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify({ ...createGroup, user_id: user?.id }) }); if (!res.ok) { const data = await res.json(); alert(data.error || 'Ошибка создания'); } else { setCreateGroup(null); fetchGroups(page); } } catch (e) { alert('Ошибка сети'); } finally { setCreateLoading(false); } }; if (selectedGroup) { return setSelectedGroup(null)} token={token} />; } return (

Группы подписчиков

{loading &&
Загрузка...
} {error &&
{error}
} {!loading && !error && ( <> {groups.map(g => ( setSelectedGroup(g)}> ))} {groups.length === 0 && ( )}
ID Название Описание
{g.id} {g.name} {g.description}
Нет данных
)} {editGroup && ( setEditGroup(null)} group={editGroup} loading={editLoading} onChange={setEditGroup} onSave={handleEditSave} /> )} {createGroup && ( setCreateGroup(null)} group={createGroup} loading={createLoading} onChange={setCreateGroup} onSave={handleCreateSave} /> )}
); } function SubscribersInGroupPage({ group, onBack, token }) { const PAGE_SIZE = 10; const [subs, setSubs] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [editSub, setEditSub] = useState(null); const [editLoading, setEditLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(null); const [createSub, setCreateSub] = useState(null); const [createLoading, setCreateLoading] = useState(false); useEffect(() => { fetchSubs(page); // eslint-disable-next-line }, [page, group.id]); const fetchSubs = async (page) => { setLoading(true); setError(''); try { const offset = (page - 1) * PAGE_SIZE; const res = await fetch(`/api/mail/group-subscribers?limit=${PAGE_SIZE}&offset=${offset}&group_id=${group.id}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} }); const data = await res.json(); if (!res.ok) { setError(data.error || 'Ошибка загрузки'); setSubs([]); setTotal(0); } else { setSubs(Array.isArray(data.rows) ? data.rows : []); setTotal(data.count || 0); } } catch (e) { setError('Ошибка сети'); setSubs([]); setTotal(0); } finally { setLoading(false); } }; const handleDelete = async (id) => { if (!window.confirm('Удалить подписчика?')) return; setDeleteLoading(id); try { const res = await fetch(`/api/mail/group-subscribers/${id}`, { method: 'DELETE', headers: token ? { Authorization: `Bearer ${token}` } : {} }); if (!res.ok) { const data = await res.json(); alert(data.error || 'Ошибка удаления'); } else { fetchSubs(page); } } catch (e) { alert('Ошибка сети'); } finally { setDeleteLoading(null); } }; const handleEdit = (gs) => { setEditSub({ ...gs.Subscriber, groupSubId: gs.id }); }; const handleEditSave = async (e) => { e.preventDefault(); setEditLoading(true); try { const res = await fetch(`/api/mail/subscribers/${editSub.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify(editSub) }); if (!res.ok) { const data = await res.json(); alert(data.error || 'Ошибка обновления'); } else { setEditSub(null); fetchSubs(page); } } catch (e) { alert('Ошибка сети'); } finally { setEditLoading(false); } }; const handleCreate = () => { setCreateSub({ email: '', name: '', status: 'active' }); }; const handleCreateSave = async (e) => { e.preventDefault(); setCreateLoading(true); try { // 1. Создать подписчика const res = await fetch('/api/mail/subscribers', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify(createSub) }); const data = await res.json(); if (!res.ok) { alert(data.error || 'Ошибка создания'); } else { // 2. Привязать к группе const res2 = await fetch('/api/mail/group-subscribers', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify({ group_id: group.id, subscriber_id: data.id }) }); if (!res2.ok) { const data2 = await res2.json(); alert(data2.error || 'Ошибка привязки к группе'); } else { setCreateSub(null); fetchSubs(page); } } } catch (e) { alert('Ошибка сети'); } finally { setCreateLoading(false); } }; return (

Подписчики группы: {group.name}

{loading &&
Загрузка...
} {error &&
{error}
} {!loading && !error && ( <> {subs.map(gs => ( ))} {subs.length === 0 && ( )}
ID Email Имя Статус
{gs.Subscriber?.id || ''} {gs.Subscriber?.email || ''} {gs.Subscriber?.name || ''} {gs.Subscriber?.status || ''}
Нет данных
)} {editSub && ( setEditSub(null)} sub={editSub} loading={editLoading} onChange={setEditSub} onSave={handleEditSave} /> )} {createSub && ( setCreateSub(null)} sub={createSub} loading={createLoading} onChange={setCreateSub} onSave={handleCreateSave} /> )}
); } const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' }; const tdStyle = { padding: '10px 16px', background: '#fff' }; const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 }; const modalOverlayStyle = { position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.18)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }; const modalStyle = { background: '#fff', borderRadius: 14, padding: '32px 28px 24px 28px', minWidth: 340, boxShadow: '0 12px 48px 0 rgba(31,38,135,0.22)', position: 'relative', animation: 'modalIn 0.18s cubic-bezier(.4,1.3,.6,1)'}; const closeBtnStyle = { position: 'absolute', top: 12, right: 16, background: 'none', border: 'none', fontSize: 26, color: '#6366f1', cursor: 'pointer', fontWeight: 700, lineHeight: 1, padding: 0, zIndex: 2, opacity: 0.8, transition: 'opacity 0.2s' }; const labelStyle = { fontWeight: 500, color: '#374151', fontSize: 15, display: 'flex', flexDirection: 'column', gap: 4 }; const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #c7d2fe', fontSize: 16, outline: 'none', background: '#f8fafc', transition: 'border 0.2s' }; const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' }; const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' }; const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' }; export default GroupsPage;