Files
balikci/frontend/src/pages/Templates.jsx
salvacybersec 20191eb35d feat: Mail template management UI and API CRUD
- Added full CRUD endpoints for mail templates (create, update, delete, preview)
- Introduced Joi validators for template create/update/preview
- Updated routes/controller to support ID and type lookups
- Built React Templates page with HTML editor, preview, and clipboard helpers
- Added navigation entry and route for /templates
- Enhanced documentation (README, QUICKSTART, KULLANIM, frontend/backend README)
2025-11-10 17:27:19 +03:00

454 lines
14 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 { useState, useEffect } from 'react';
import {
Box,
Button,
Paper,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Chip,
CircularProgress,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Switch,
FormControlLabel,
Tabs,
Tab,
} from '@mui/material';
import {
Add,
Edit,
Delete,
Preview,
ContentCopy,
} from '@mui/icons-material';
import { templateService } from '../services/templateService';
import { format } from 'date-fns';
const defaultForm = {
name: '',
template_type: '',
subject_template: '',
body_html: '',
description: '',
active: true,
};
function Templates() {
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(true);
const [form, setForm] = useState(defaultForm);
const [activeTab, setActiveTab] = useState('form');
const [dialogOpen, setDialogOpen] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewHtml, setPreviewHtml] = useState('');
const [previewUrl, setPreviewUrl] = useState('');
const [previewLoading, setPreviewLoading] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState(null);
const [companyPlaceholder, setCompanyPlaceholder] = useState('Örnek Şirket');
const [employeePlaceholder, setEmployeePlaceholder] = useState('Ahmet Yılmaz');
useEffect(() => {
loadTemplates();
}, []);
const loadTemplates = async () => {
try {
setLoading(true);
const response = await templateService.getAll();
setTemplates(response.data);
} catch (error) {
console.error('Failed to load templates:', error);
alert('Şablonlar yüklenemedi');
} finally {
setLoading(false);
}
};
const handleOpenCreate = () => {
setSelectedTemplate(null);
setForm(defaultForm);
setActiveTab('form');
setDialogOpen(true);
};
const handleOpenEdit = async (template) => {
setSelectedTemplate(template);
setForm({
name: template.name,
template_type: template.template_type,
subject_template: template.subject_template || '',
body_html: template.body_html,
description: template.description || '',
active: Boolean(template.active),
});
setActiveTab('form');
setDialogOpen(true);
};
const handleCloseDialog = () => {
setDialogOpen(false);
setSelectedTemplate(null);
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
setPreviewUrl('');
}
setPreviewHtml('');
setActiveTab('form');
};
const handleSave = async () => {
try {
if (selectedTemplate) {
await templateService.update(selectedTemplate.id, form);
alert('Şablon güncellendi');
} else {
await templateService.create(form);
alert('Şablon oluşturuldu');
}
handleCloseDialog();
loadTemplates();
} catch (error) {
const message = error.response?.data?.error || 'Şablon kaydedilemedi';
alert(message);
console.error('Failed to save template:', error);
}
};
const handleDelete = async (template) => {
if (!window.confirm(`${template.name} şablonunu silmek istediğinizden emin misiniz?`)) {
return;
}
try {
await templateService.delete(template.id);
alert('Şablon silindi');
loadTemplates();
} catch (error) {
const message = error.response?.data?.error || 'Şablon silinemedi';
alert(message);
console.error('Failed to delete template:', error);
}
};
const handlePreview = async (htmlOverride, options = { openModal: false }) => {
try {
setPreviewLoading(true);
const htmlContent = htmlOverride ?? form.body_html;
if (!htmlContent) {
alert('Önizleme için HTML içeriği gerekli');
return;
}
const response = await templateService.preview({
template_html: htmlContent,
company_name: companyPlaceholder,
employee_name: employeePlaceholder,
});
const renderedHtml = response.data.data.rendered_html;
setPreviewHtml(renderedHtml);
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
const blobUrl = URL.createObjectURL(new Blob([renderedHtml], { type: 'text/html' }));
setPreviewUrl(blobUrl);
setActiveTab('preview');
if (options.openModal) {
setPreviewOpen(true);
}
} catch (error) {
const message = error.response?.data?.error || 'Önizleme oluşturulamadı';
alert(message);
console.error('Failed to preview template:', error);
} finally {
setPreviewLoading(false);
}
};
const copyTemplateType = (value) => {
navigator.clipboard.writeText(value);
alert(`Template Type panoya kopyalandı: ${value}`);
};
useEffect(() => {
return () => {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
};
}, [previewUrl]);
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
<CircularProgress />
</Box>
);
}
return (
<Box>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography variant="h4">Mail Şablonları</Typography>
<Button variant="contained" startIcon={<Add />} onClick={handleOpenCreate}>
Yeni Şablon
</Button>
</Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Şablon Adı</TableCell>
<TableCell>Template Type</TableCell>
<TableCell>Durum</TableCell>
<TableCell>Güncelleme</TableCell>
<TableCell align="right">İşlemler</TableCell>
</TableRow>
</TableHead>
<TableBody>
{templates.map((template) => (
<TableRow key={template.id} hover>
<TableCell>
<Typography fontWeight={600}>{template.name}</Typography>
{template.description && (
<Typography variant="body2" color="textSecondary">
{template.description}
</Typography>
)}
</TableCell>
<TableCell>
<Box display="flex" alignItems="center" gap={1}>
<Chip
label={template.template_type}
color="primary"
size="small"
/>
<Button
size="small"
variant="outlined"
startIcon={<ContentCopy fontSize="small" />}
onClick={() => copyTemplateType(template.template_type)}
>
Kopyala
</Button>
</Box>
</TableCell>
<TableCell>
<Chip
label={template.active ? 'Aktif' : 'Pasif'}
color={template.active ? 'success' : 'default'}
size="small"
/>
</TableCell>
<TableCell>
{format(new Date(template.updated_at), 'dd/MM/yyyy HH:mm')}
</TableCell>
<TableCell align="right">
<Button
size="small"
startIcon={<Preview />}
onClick={() => {
setSelectedTemplate(template);
setForm({
name: template.name,
template_type: template.template_type,
subject_template: template.subject_template || '',
body_html: template.body_html,
description: template.description || '',
active: Boolean(template.active),
});
setCompanyPlaceholder('Örnek Şirket');
setEmployeePlaceholder('Ahmet Yılmaz');
setActiveTab('preview');
handlePreview(template.body_html, { openModal: true });
}}
sx={{ mr: 1 }}
>
Önizleme
</Button>
<Button
size="small"
startIcon={<Edit />}
onClick={() => handleOpenEdit(template)}
sx={{ mr: 1 }}
>
Düzenle
</Button>
<Button
size="small"
color="error"
startIcon={<Delete />}
onClick={() => handleDelete(template)}
>
Sil
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Dialog
open={dialogOpen}
onClose={handleCloseDialog}
maxWidth="md"
fullWidth
>
<DialogTitle>
{selectedTemplate ? 'Şablonu Düzenle' : 'Yeni Şablon Oluştur'}
</DialogTitle>
<DialogContent dividers>
<Tabs
value={activeTab}
onChange={(_, value) => setActiveTab(value)}
sx={{ mb: 2 }}
>
<Tab label="Form" value="form" />
<Tab label="Önizleme" value="preview" />
</Tabs>
{activeTab === 'form' && (
<Box display="flex" flexDirection="column" gap={2}>
<TextField
label="Şablon Adı"
fullWidth
required
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
<TextField
label="Template Type"
fullWidth
required
helperText="Örn: bank, government, internal"
value={form.template_type}
onChange={(e) => setForm({ ...form, template_type: e.target.value.trim().toLowerCase() })}
/>
<TextField
label="Mail Konusu"
fullWidth
value={form.subject_template}
onChange={(e) => setForm({ ...form, subject_template: e.target.value })}
/>
<TextField
label="Açıklama"
fullWidth
multiline
minRows={2}
value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })}
/>
<TextField
label="HTML İçeriği"
fullWidth
multiline
minRows={10}
value={form.body_html}
onChange={(e) => setForm({ ...form, body_html: e.target.value })}
helperText="Handlebars değişkenleri: {{company_name}}, {{employee_name}}, {{tracking_url}}"
/>
<FormControlLabel
control={
<Switch
checked={form.active}
onChange={(e) => setForm({ ...form, active: e.target.checked })}
/>
}
label="Aktif"
/>
</Box>
)}
{activeTab === 'preview' && (
<Box display="flex" flexDirection="column" gap={2}>
<Box display="flex" gap={2}>
<TextField
label="Şirket Adı"
fullWidth
value={companyPlaceholder}
onChange={(e) => setCompanyPlaceholder(e.target.value)}
/>
<TextField
label="Çalışan Adı"
fullWidth
value={employeePlaceholder}
onChange={(e) => setEmployeePlaceholder(e.target.value)}
/>
</Box>
<Button
variant="outlined"
startIcon={<Preview />}
onClick={handlePreview}
disabled={previewLoading || !form.body_html}
>
{previewLoading ? 'Önizleme Oluşturuluyor...' : 'Önizleme Oluştur'}
</Button>
<Paper variant="outlined" sx={{ minHeight: 400 }}>
{previewHtml ? (
<iframe
title="template-preview"
src={previewUrl}
style={{ border: 'none', width: '100%', height: '600px' }}
/>
) : (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="200px"
color="text.secondary"
>
Önizleme için HTML içeriği girin ve \"Önizleme Oluştur\" butonuna basın
</Box>
)}
</Paper>
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>İptal</Button>
<Button onClick={handleSave} variant="contained" disabled={!form.name || !form.template_type || !form.body_html}>
Kaydet
</Button>
</DialogActions>
</Dialog>
<Dialog
open={previewOpen}
onClose={() => setPreviewOpen(false)}
maxWidth="lg"
fullWidth
>
<DialogTitle>Önizleme</DialogTitle>
<DialogContent dividers>
<iframe
title="template-preview-full"
src={previewUrl}
style={{ border: 'none', width: '100%', minHeight: '600px' }}
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setPreviewOpen(false);
}}
>
Kapat
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
export default Templates;