Files
balikci/frontend/src/pages/Templates.jsx

702 lines
22 KiB
React
Raw Normal View History

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,
AutoAwesome,
Send as SendIcon,
} from '@mui/icons-material';
import { templateService } from '../services/templateService';
import { format } from 'date-fns';
import axios from 'axios';
const API_URL = import.meta.env.VITE_API_URL;
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');
const [aiDialogOpen, setAiDialogOpen] = useState(false);
const [aiGenerating, setAiGenerating] = useState(false);
const [aiForm, setAiForm] = useState({
company_name: '',
scenario: '',
employee_info: '',
custom_prompt: '',
template_name: '',
template_type: '',
});
const [testMailDialogOpen, setTestMailDialogOpen] = useState(false);
const [testMailAddress, setTestMailAddress] = useState('');
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');
setTemplates([]);
} 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 handleGenerateWithAI = async () => {
if (!aiForm.company_name || !aiForm.scenario || !aiForm.template_name || !aiForm.template_type) {
alert('Lütfen tüm zorunlu alanları doldurun');
return;
}
setAiGenerating(true);
try {
const response = await axios.post(
`${API_URL}/api/ollama/generate-template`,
aiForm,
{ withCredentials: true }
);
alert(response.data.message);
setAiDialogOpen(false);
setAiForm({
company_name: '',
scenario: '',
employee_info: '',
custom_prompt: '',
template_name: '',
template_type: '',
});
loadTemplates();
} catch (error) {
const message = error.response?.data?.error || 'AI ile şablon oluşturulamadı';
alert(message);
console.error('AI generation failed:', error);
} finally {
setAiGenerating(false);
}
};
const handleSendTestMail = async () => {
if (!testMailAddress || !selectedTemplate) {
alert('Lütfen mail adresi girin');
return;
}
try {
await axios.post(
`${API_URL}/api/ollama/send-test-mail`,
{
test_email: testMailAddress,
subject: selectedTemplate.subject_template,
body: selectedTemplate.body_html,
company_name: companyPlaceholder,
employee_name: employeePlaceholder,
},
{ withCredentials: true }
);
alert('Test maili gönderildi!');
setTestMailDialogOpen(false);
setTestMailAddress('');
} catch (error) {
const message = error.response?.data?.error || 'Test maili gönderilemedi';
alert(message);
console.error('Test mail failed:', 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>
<Box display="flex" gap={2}>
<Button
variant="outlined"
startIcon={<AutoAwesome />}
onClick={() => setAiDialogOpen(true)}
color="secondary"
>
AI ile Oluştur
</Button>
<Button variant="contained" startIcon={<Add />} onClick={handleOpenCreate}>
Yeni Şablon
</Button>
</Box>
</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"
color="info"
startIcon={<SendIcon />}
onClick={() => {
setSelectedTemplate(template);
setTestMailDialogOpen(true);
}}
sx={{ mr: 1 }}
>
Test Mail
</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>
{/* AI Generation Dialog */}
<Dialog open={aiDialogOpen} onClose={() => setAiDialogOpen(false)} maxWidth="md" fullWidth>
<DialogTitle>🤖 AI ile Mail Şablonu Oluştur</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" gutterBottom>
Ollama AI kullanarak otomatik mail şablonu oluşturun. Aşağıdaki bilgileri girin:
</Typography>
<TextField
fullWidth
margin="normal"
label="Şablon Adı"
required
value={aiForm.template_name}
onChange={(e) => setAiForm({ ...aiForm, template_name: e.target.value })}
helperText="Şablona verilecek isim"
/>
<TextField
fullWidth
margin="normal"
label="Template Type"
required
value={aiForm.template_type}
onChange={(e) => setAiForm({ ...aiForm, template_type: e.target.value })}
helperText="Örn: bank, hr, it_support, management"
/>
<TextField
fullWidth
margin="normal"
label="Şirket Adı"
required
value={aiForm.company_name}
onChange={(e) => setAiForm({ ...aiForm, company_name: e.target.value })}
helperText="Hedef şirket adı (örn: Acme Corporation)"
/>
<TextField
fullWidth
margin="normal"
label="Senaryo"
required
multiline
rows={3}
value={aiForm.scenario}
onChange={(e) => setAiForm({ ...aiForm, scenario: e.target.value })}
helperText="Mail senaryosu (örn: 'Şifre sıfırlama maili', 'Yeni güvenlik politikası', 'Ödül programı duyurusu')"
/>
<TextField
fullWidth
margin="normal"
label="Çalışan Bilgisi (Opsiyonel)"
value={aiForm.employee_info}
onChange={(e) => setAiForm({ ...aiForm, employee_info: e.target.value })}
helperText="Hedef çalışan hakkında bilgi (örn: 'İK departmanı çalışanı', 'Yönetici')"
/>
<TextField
fullWidth
margin="normal"
label="Ek Talimatlar (Opsiyonel)"
multiline
rows={2}
value={aiForm.custom_prompt}
onChange={(e) => setAiForm({ ...aiForm, custom_prompt: e.target.value })}
helperText="AI'ya özel talimatlar (örn: 'Resmi dil kullan', 'Aciliyet vurgusu yap')"
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setAiDialogOpen(false)} disabled={aiGenerating}>
İptal
</Button>
<Button
onClick={handleGenerateWithAI}
variant="contained"
disabled={aiGenerating}
startIcon={aiGenerating ? <CircularProgress size={20} /> : <AutoAwesome />}
>
{aiGenerating ? 'Oluşturuluyor...' : 'Oluştur'}
</Button>
</DialogActions>
</Dialog>
{/* Test Mail Dialog */}
<Dialog open={testMailDialogOpen} onClose={() => setTestMailDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>📧 Test Maili Gönder</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" gutterBottom>
Seçili şablon için test maili gönderin.
</Typography>
{selectedTemplate && (
<Box my={2} p={2} bgcolor="grey.100" borderRadius={1}>
<Typography variant="body2" fontWeight="bold">
Şablon: {selectedTemplate.name}
</Typography>
<Typography variant="body2" color="text.secondary">
Tip: {selectedTemplate.template_type}
</Typography>
</Box>
)}
<TextField
fullWidth
margin="normal"
label="Test Mail Adresi"
type="email"
required
value={testMailAddress}
onChange={(e) => setTestMailAddress(e.target.value)}
helperText="Test mailinin gönderileceği adres"
/>
<TextField
fullWidth
margin="normal"
label="Şirket Adı (Placeholder)"
value={companyPlaceholder}
onChange={(e) => setCompanyPlaceholder(e.target.value)}
helperText="{{company_name}} yerine kullanılacak"
/>
<TextField
fullWidth
margin="normal"
label="Çalışan Adı (Placeholder)"
value={employeePlaceholder}
onChange={(e) => setEmployeePlaceholder(e.target.value)}
helperText="{{employee_name}} yerine kullanılacak"
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setTestMailDialogOpen(false)}>
İptal
</Button>
<Button
onClick={handleSendTestMail}
variant="contained"
startIcon={<SendIcon />}
>
Gönder
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
export default Templates;