Files
balikci/frontend/src/pages/Templates.jsx
salvacybersec eb2205e4ff fix: Correct preview response data access
Fixed double .data access in template preview:
- Changed response.data.data.rendered_html
- To response.data.rendered_html

templateService already returns response.data, no need to access .data twice.
2025-11-11 02:46:35 +03:00

702 lines
22 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,
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.rendered_html; // Fixed: removed extra .data
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;