version
This commit is contained in:
parent
6e89deb3f5
commit
63eb19fd02
47
frontend/src/modals/CreateTemplateVersionModal.js
Normal file
47
frontend/src/modals/CreateTemplateVersionModal.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Modal from './Modal';
|
||||
import styles from '../styles/TemplateModal.module.css';
|
||||
|
||||
export default function CreateTemplateVersionModal({ isOpen, onClose, loading, onSave, initial }) {
|
||||
const [subject, setSubject] = useState(initial?.subject || '');
|
||||
const [bodyHtml, setBodyHtml] = useState(initial?.body_html || '');
|
||||
const [bodyText, setBodyText] = useState(initial?.body_text || '');
|
||||
|
||||
useEffect(() => {
|
||||
if (initial) {
|
||||
setSubject(initial.subject || '');
|
||||
setBodyHtml(initial.body_html || '');
|
||||
setBodyText(initial.body_text || '');
|
||||
} else {
|
||||
setSubject('');
|
||||
setBodyHtml('');
|
||||
setBodyText('');
|
||||
}
|
||||
}, [initial, isOpen]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSave({ subject, body_html: bodyHtml, body_text: bodyText });
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
|
||||
<h3 className={styles.title}>{initial ? 'Редактировать версию' : 'Добавить версию шаблона'}</h3>
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<label className={styles.label}>Тема письма
|
||||
<input type="text" value={subject} onChange={e => setSubject(e.target.value)} required className={styles.input} />
|
||||
</label>
|
||||
<label className={styles.label}>HTML-версия
|
||||
<textarea value={bodyHtml} onChange={e => setBodyHtml(e.target.value)} className={styles.input} rows={4} placeholder="<h1>Заголовок</h1>..." />
|
||||
</label>
|
||||
<label className={styles.label}>Текстовая версия
|
||||
<textarea value={bodyText} onChange={e => setBodyText(e.target.value)} className={styles.input} rows={4} placeholder="Текст письма..." />
|
||||
</label>
|
||||
<div className={styles.actions}>
|
||||
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? (initial ? 'Сохранение...' : 'Создание...') : (initial ? 'Сохранить' : 'Создать')}</button>
|
||||
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
207
frontend/src/pages/TemplateVersionsPage.js
Normal file
207
frontend/src/pages/TemplateVersionsPage.js
Normal file
@ -0,0 +1,207 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import CreateTemplateVersionModal from '../modals/CreateTemplateVersionModal';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export default function TemplateVersionsPage({ template, onBack, token }) {
|
||||
const [versions, setVersions] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [createVersionModal, setCreateVersionModal] = useState(false);
|
||||
const [createVersionLoading, setCreateVersionLoading] = useState(false);
|
||||
const [editVersion, setEditVersion] = useState(null);
|
||||
const [editVersionLoading, setEditVersionLoading] = useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = useState(null);
|
||||
|
||||
const fetchVersions = useCallback(async (page) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const offset = (page - 1) * PAGE_SIZE;
|
||||
const res = await fetch(`/api/mail/email-template-versions?template_id=${template.id}&limit=${PAGE_SIZE}&offset=${offset}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||
});
|
||||
const data = await res.json();
|
||||
let rows = [];
|
||||
let count = 0;
|
||||
if (res.ok && Array.isArray(data)) {
|
||||
rows = data.filter(v => v.template_id === template.id);
|
||||
count = rows.length;
|
||||
} else if (res.ok && Array.isArray(data.rows)) {
|
||||
rows = data.rows.filter(v => v.template_id === template.id);
|
||||
count = data.count || rows.length;
|
||||
}
|
||||
setVersions(rows);
|
||||
setTotal(count);
|
||||
} catch (e) {
|
||||
setError('Ошибка сети');
|
||||
setVersions([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [template.id, token]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchVersions(page);
|
||||
}, [fetchVersions, page]);
|
||||
|
||||
const handleAddVersion = () => setCreateVersionModal(true);
|
||||
const handleCreateVersionSave = async (versionData) => {
|
||||
setCreateVersionLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/mail/email-template-versions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||
},
|
||||
body: JSON.stringify({ ...versionData, template_id: template.id })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
alert(data.error || 'Ошибка создания версии');
|
||||
} else {
|
||||
setCreateVersionModal(false);
|
||||
fetchVersions(page);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка сети');
|
||||
} finally {
|
||||
setCreateVersionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Edit version ---
|
||||
const handleEditVersion = (version) => setEditVersion(version);
|
||||
const handleEditVersionSave = async (data) => {
|
||||
setEditVersionLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/mail/email-template-versions/${editVersion.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject: data.subject,
|
||||
body_html: data.body_html,
|
||||
body_text: data.body_text
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
const resp = await res.json();
|
||||
alert(resp.error || 'Ошибка обновления версии');
|
||||
} else {
|
||||
setEditVersion(null);
|
||||
fetchVersions(page);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка сети');
|
||||
} finally {
|
||||
setEditVersionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Delete version ---
|
||||
const handleDeleteVersion = async (id) => {
|
||||
if (!window.confirm('Удалить эту версию шаблона?')) return;
|
||||
setDeleteLoading(id);
|
||||
try {
|
||||
const res = await fetch(`/api/mail/email-template-versions/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
alert(data.error || 'Ошибка удаления');
|
||||
} else {
|
||||
fetchVersions(page);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка сети');
|
||||
} finally {
|
||||
setDeleteLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={onBack} style={{ marginBottom: 18, background: 'none', border: 'none', color: '#6366f1', fontWeight: 600, fontSize: 16, cursor: 'pointer' }}>← Назад к шаблонам</button>
|
||||
<h2>Версии шаблона: {template.name}</h2>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 12 }}>
|
||||
<button onClick={handleAddVersion} style={addBtnStyle}>+ Добавить версию</button>
|
||||
</div>
|
||||
{loading && <div>Загрузка...</div>}
|
||||
{error && <div style={{ color: 'red' }}>{error}</div>}
|
||||
{!loading && !error && (
|
||||
<>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: 16, background: '#fff', borderRadius: 8, overflow: 'hidden' }}>
|
||||
<thead style={{ background: '#f3f4f6' }}>
|
||||
<tr>
|
||||
<th style={thStyle}>ID</th>
|
||||
<th style={thStyle}>Версия</th>
|
||||
<th style={thStyle}>Тема</th>
|
||||
<th style={thStyle}>HTML</th>
|
||||
<th style={thStyle}>Текст</th>
|
||||
<th style={thStyle}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versions.map(v => (
|
||||
<tr key={v.id} style={{ borderBottom: '1px solid #e5e7eb' }}>
|
||||
<td style={tdStyle}>{v.id}</td>
|
||||
<td style={tdStyle}>{v.version_number}</td>
|
||||
<td style={tdStyle}>{v.subject}</td>
|
||||
<td style={tdStyle}>{v.body_html ? <span style={{ color: '#10b981' }}>Есть</span> : <span style={{ color: '#ef4444' }}>Нет</span>}</td>
|
||||
<td style={tdStyle}>{v.body_text ? <span style={{ color: '#10b981' }}>Есть</span> : <span style={{ color: '#ef4444' }}>Нет</span>}</td>
|
||||
<td style={tdStyle}>
|
||||
<button onClick={() => handleEditVersion(v)} style={btnStyle}>Редактировать</button>
|
||||
<button onClick={() => handleDeleteVersion(v.id)} style={btnStyle} disabled={deleteLoading === v.id}>
|
||||
{deleteLoading === v.id ? 'Удаление...' : 'Удалить'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{versions.length === 0 && (
|
||||
<tr><td colSpan={6} style={{ textAlign: 'center', color: '#9ca3af', padding: 24 }}>Нет версий</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Пагинация если нужно */}
|
||||
{total > PAGE_SIZE && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1} style={btnStyle}>Назад</button>
|
||||
<span style={{ margin: '0 12px' }}>Страница {page}</span>
|
||||
<button onClick={() => setPage(p => p + 1)} disabled={page * PAGE_SIZE >= total} style={btnStyle}>Вперёд</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{createVersionModal && (
|
||||
<CreateTemplateVersionModal
|
||||
isOpen={createVersionModal}
|
||||
onClose={() => setCreateVersionModal(false)}
|
||||
loading={createVersionLoading}
|
||||
onSave={handleCreateVersionSave}
|
||||
/>
|
||||
)}
|
||||
{editVersion && (
|
||||
<CreateTemplateVersionModal
|
||||
isOpen={!!editVersion}
|
||||
onClose={() => setEditVersion(null)}
|
||||
loading={editVersionLoading}
|
||||
onSave={handleEditVersionSave}
|
||||
initial={editVersion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 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' };
|
||||
@ -25,9 +25,19 @@ app.use('/api/mail', routes);
|
||||
}
|
||||
})();
|
||||
|
||||
let isQueueFilling = false;
|
||||
|
||||
// Периодически заполняем очередь для scheduled кампаний
|
||||
setInterval(() => {
|
||||
processScheduledCampaigns().catch(err => console.error('Queue fill error:', err));
|
||||
setInterval(async () => {
|
||||
if (isQueueFilling) return;
|
||||
isQueueFilling = true;
|
||||
try {
|
||||
await processScheduledCampaigns();
|
||||
} catch (err) {
|
||||
console.error('Queue fill error:', err);
|
||||
} finally {
|
||||
isQueueFilling = false;
|
||||
}
|
||||
}, 60 * 1000); // раз в минуту
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user