ads-marketing/frontend/src/pages/GroupsPage.js
2025-08-02 16:19:00 +05:00

486 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

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';
import styles from '../styles/Common.module.css';
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 <SubscribersInGroupPage group={selectedGroup} onBack={() => setSelectedGroup(null)} token={token} />;
}
return (
<div className={styles.container}>
<div className={styles.pageHeader}>
<h2 className={styles.pageTitle}>Подписчики и группы</h2>
<div className={styles.pageActions}>
<button
onClick={handleCreate}
className={`${styles.button} ${styles.buttonGradient}`}
>
+ Добавить группу
</button>
</div>
</div>
{loading && <div className={styles.loading}>Загрузка...</div>}
{error && <div className={styles.error}>{error}</div>}
{!loading && !error && (
<>
<table className={styles.table}>
<thead className={styles.tableHeader}>
<tr>
<th className={styles.tableHeaderCell}>ID</th>
<th className={styles.tableHeaderCell}>Название</th>
<th className={styles.tableHeaderCell}>Описание</th>
<th className={styles.tableHeaderCell}>Действия</th>
</tr>
</thead>
<tbody>
{groups.map(g => (
<tr key={g.id} className={styles.tableRow} style={{ cursor: 'pointer' }} onClick={() => setSelectedGroup(g)}>
<td className={styles.tableCell}>{g.id}</td>
<td className={styles.tableCell}>{g.name}</td>
<td className={styles.tableCell}>{g.description}</td>
<td className={styles.tableCellActions}>
<button
onClick={(e) => { e.stopPropagation(); handleEdit(g); }}
className={`${styles.button} ${styles.buttonPrimary}`}
style={{ marginRight: '8px' }}
>
Редактировать
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDelete(g.id); }}
className={`${styles.button} ${styles.buttonDanger}`}
disabled={deleteLoading === g.id}
>
{deleteLoading === g.id ? 'Удаление...' : 'Удалить'}
</button>
</td>
</tr>
))}
{groups.length === 0 && (
<tr>
<td colSpan={4} className={styles.emptyState}>
<div className={styles.emptyStateIcon}>👥</div>
<div className={styles.emptyStateTitle}>Нет групп</div>
<div className={styles.emptyStateText}>Создайте первую группу для организации подписчиков</div>
</td>
</tr>
)}
</tbody>
</table>
<Paginator
page={page}
total={total}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
{editGroup && (
<EditGroupModal
isOpen={!!editGroup}
onClose={() => setEditGroup(null)}
group={editGroup}
loading={editLoading}
onChange={setEditGroup}
onSave={handleEditSave}
/>
)}
{createGroup && (
<CreateGroupModal
isOpen={!!createGroup}
onClose={() => setCreateGroup(null)}
group={createGroup}
loading={createLoading}
onChange={setCreateGroup}
onSave={handleCreateSave}
/>
)}
</div>
);
}
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 (
<div className={styles.container}>
<button
onClick={onBack}
className={`${styles.button} ${styles.buttonSecondary}`}
style={{ marginBottom: 18 }}
>
Назад к группам
</button>
<div className={styles.pageHeader}>
<h2 className={styles.pageTitle}>Подписчики группы: {group.name}</h2>
<div className={styles.pageActions}>
<button
onClick={handleCreate}
className={`${styles.button} ${styles.buttonGradient}`}
>
+ Добавить подписчика
</button>
</div>
</div>
{loading && <div className={styles.loading}>Загрузка...</div>}
{error && <div className={styles.error}>{error}</div>}
{!loading && !error && (
<>
<table className={styles.table}>
<thead className={styles.tableHeader}>
<tr>
<th className={styles.tableHeaderCell}>ID</th>
<th className={styles.tableHeaderCell}>Email</th>
<th className={styles.tableHeaderCell}>Имя</th>
<th className={styles.tableHeaderCell}>Статус</th>
<th className={styles.tableHeaderCell}>Действия</th>
</tr>
</thead>
<tbody>
{subs.map(gs => (
<tr key={gs.id} className={styles.tableRow}>
<td className={styles.tableCell}>{gs.Subscriber?.id || ''}</td>
<td className={styles.tableCell}>{gs.Subscriber?.email || ''}</td>
<td className={styles.tableCell}>{gs.Subscriber?.name || ''}</td>
<td className={styles.tableCell}>{gs.Subscriber?.status || ''}</td>
<td className={styles.tableCellActions}>
<button
onClick={() => handleEdit(gs)}
className={`${styles.button} ${styles.buttonPrimary}`}
style={{ marginRight: '8px' }}
>
Редактировать
</button>
<button
onClick={() => handleDelete(gs.id)}
className={`${styles.button} ${styles.buttonDanger}`}
disabled={deleteLoading === gs.id}
>
{deleteLoading === gs.id ? 'Удаление...' : 'Удалить'}
</button>
</td>
</tr>
))}
{subs.length === 0 && (
<tr>
<td colSpan={5} className={styles.emptyState}>
<div className={styles.emptyStateIcon}>📧</div>
<div className={styles.emptyStateTitle}>Нет подписчиков</div>
<div className={styles.emptyStateText}>В этой группе пока нет подписчиков</div>
</td>
</tr>
)}
</tbody>
</table>
<Paginator
page={page}
total={total}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
{editSub && (
<EditSubscriberModal
isOpen={!!editSub}
onClose={() => setEditSub(null)}
sub={editSub}
loading={editLoading}
onChange={setEditSub}
onSave={handleEditSave}
/>
)}
{createSub && (
<CreateSubscriberModal
isOpen={!!createSub}
onClose={() => setCreateSub(null)}
sub={createSub}
loading={createLoading}
onChange={setCreateSub}
onSave={handleCreateSave}
/>
)}
</div>
);
}
export default GroupsPage;