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)
This commit is contained in:
11
KULLANIM.md
11
KULLANIM.md
@@ -123,6 +123,17 @@
|
|||||||
|
|
||||||
## 📧 Mail Şablonları
|
## 📧 Mail Şablonları
|
||||||
|
|
||||||
|
### Şablon Paneli (Yeni)
|
||||||
|
|
||||||
|
1. **Menü:** Sol menüde `Mail Şablonları` sekmesine tıklayın
|
||||||
|
2. **Liste:** Aktif/pasif tüm şablonları görün, güncelleme tarihlerini takip edin
|
||||||
|
3. **Oluştur:** `Yeni Şablon` butonu ile HTML içeriği girebilir, template type tanımlayabilirsiniz
|
||||||
|
4. **Düzenle:** Her satırdaki `Düzenle` butonu ile şablon bilgilerini güncelleyin
|
||||||
|
5. **Önizleme:** `Önizleme` butonu ile şablonu gerçek verilerle render edip tam ekran görüntüleyin
|
||||||
|
6. **Sil:** Gereksiz şablonları `Sil` butonu ile kaldırın (tokenlarda kullanılanları değiştirmeyi unutmayın)
|
||||||
|
7. **Template Type Kopyalama:** Token oluştururken kullanmak için `Template Type` değerini kopyalayın
|
||||||
|
8. **Handlebars Değişkenleri:** Editörde `{{company_name}}`, `{{employee_name}}`, `{{tracking_url}}` değişkenlerini kullanın
|
||||||
|
|
||||||
### Mevcut Şablonlar
|
### Mevcut Şablonlar
|
||||||
|
|
||||||
**1. Banka Bildirimi (`bank`)**
|
**1. Banka Bildirimi (`bank`)**
|
||||||
|
|||||||
@@ -95,6 +95,14 @@ Artık sistemin tüm özellikleri kullanıma hazır:
|
|||||||
- ✅ Telegram bildirimleri
|
- ✅ Telegram bildirimleri
|
||||||
- ✅ Detaylı istatistikler
|
- ✅ Detaylı istatistikler
|
||||||
|
|
||||||
|
## ✉️ Şablonları Özelleştir
|
||||||
|
|
||||||
|
1. **Mail Şablonları** sayfasına git
|
||||||
|
2. `Yeni Şablon` ile kendi senaryonu oluştur
|
||||||
|
3. HTML alanında `{{company_name}}`, `{{employee_name}}`, `{{tracking_url}}` değişkenlerini kullan
|
||||||
|
4. `Önizleme` butonuyla gerçek örnek verilerle testi gör
|
||||||
|
5. Token oluştururken yeni template type'ı seçmeyi unutma
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Daha Fazla Bilgi
|
## 📚 Daha Fazla Bilgi
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Güvenlik farkındalık eğitimleri için basit ve etkili phishing test yönetim
|
|||||||
- 📊 **Detaylı İstatistikler** - IP, konum, cihaz bilgileri
|
- 📊 **Detaylı İstatistikler** - IP, konum, cihaz bilgileri
|
||||||
- 💾 **SQLite** - Tek dosya, kolay yedekleme
|
- 💾 **SQLite** - Tek dosya, kolay yedekleme
|
||||||
- 🎨 **Modern UI** - React ile responsive admin paneli
|
- 🎨 **Modern UI** - React ile responsive admin paneli
|
||||||
|
- ✉️ **Mail Şablonları** - HTML editör ve önizleme paneli
|
||||||
|
|
||||||
## 🚀 Hızlı Başlangıç
|
## 🚀 Hızlı Başlangıç
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ oltalama/
|
|||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── services/ ✅ 5 servis (auth, company, token, stats, template)
|
│ │ ├── services/ ✅ 5 servis (auth, company, token, stats, template)
|
||||||
│ │ ├── context/ ✅ Auth context
|
│ │ ├── context/ ✅ Auth context
|
||||||
│ │ ├── pages/ ✅ 5 sayfa (Login, Dashboard, Companies, Tokens, Settings)
|
│ │ ├── pages/ ✅ 6 sayfa (Login, Dashboard, Companies, Tokens, Templates, Settings)
|
||||||
│ │ └── components/ ✅ Layout + Navigation
|
│ │ └── components/ ✅ Layout + Navigation
|
||||||
└── devpan.md ✅ Detaylı plan
|
└── devpan.md ✅ Detaylı plan
|
||||||
```
|
```
|
||||||
@@ -163,8 +164,9 @@ curl http://localhost:3000/api/stats/dashboard
|
|||||||
**Core Pages:**
|
**Core Pages:**
|
||||||
- ✅ Login (Session-based auth)
|
- ✅ Login (Session-based auth)
|
||||||
- ✅ Dashboard (Stats, recent clicks)
|
- ✅ Dashboard (Stats, recent clicks)
|
||||||
- ✅ Companies (CRUD, grid view)
|
- ✅ Companies (CRUD, grid view + detail)
|
||||||
- ✅ Tokens (Create & send, table view)
|
- ✅ Tokens (Create & send, detail & history)
|
||||||
|
- ✅ Mail Şablonları (HTML editör + önizleme)
|
||||||
- ✅ Settings (Gmail, Telegram config)
|
- ✅ Settings (Gmail, Telegram config)
|
||||||
|
|
||||||
**Components:**
|
**Components:**
|
||||||
|
|||||||
@@ -65,9 +65,13 @@ GET /t/:token - Tracking endpoint (IP, GeoIP, Telegram)
|
|||||||
|
|
||||||
### Templates
|
### Templates
|
||||||
```
|
```
|
||||||
GET /api/templates - Tüm şablonlar
|
GET /api/templates - Tüm şablonlar
|
||||||
GET /api/templates/:type - Şablon detay
|
POST /api/templates - Yeni şablon oluştur
|
||||||
POST /api/templates/preview - Önizleme
|
GET /api/templates/:id - Şablon detay (ID)
|
||||||
|
PUT /api/templates/:id - Şablon güncelle
|
||||||
|
DELETE /api/templates/:id - Şablon sil
|
||||||
|
GET /api/templates/type/:type - Şablon (type ile)
|
||||||
|
POST /api/templates/preview - Önizleme
|
||||||
```
|
```
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
const { MailTemplate } = require('../models');
|
const { MailTemplate } = require('../models');
|
||||||
const mailService = require('../services/mail.service');
|
const mailService = require('../services/mail.service');
|
||||||
|
|
||||||
|
const buildTemplateResponse = (template) => ({
|
||||||
|
success: true,
|
||||||
|
data: template,
|
||||||
|
});
|
||||||
|
|
||||||
// Get all templates
|
// Get all templates
|
||||||
exports.getAllTemplates = async (req, res, next) => {
|
exports.getAllTemplates = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@@ -17,6 +22,25 @@ exports.getAllTemplates = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get template by ID
|
||||||
|
exports.getTemplateById = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const template = await MailTemplate.findByPk(id);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Template not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(buildTemplateResponse(template));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Get template by type
|
// Get template by type
|
||||||
exports.getTemplateByType = async (req, res, next) => {
|
exports.getTemplateByType = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@@ -33,9 +57,71 @@ exports.getTemplateByType = async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.json(buildTemplateResponse(template));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create template
|
||||||
|
exports.createTemplate = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const template = await MailTemplate.create(req.body);
|
||||||
|
res.status(201).json(buildTemplateResponse(template));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Bu template_type değeri zaten kullanılıyor.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update template
|
||||||
|
exports.updateTemplate = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const template = await MailTemplate.findByPk(id);
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Template not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await template.update(req.body);
|
||||||
|
res.json(buildTemplateResponse(template));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Bu template_type değeri zaten kullanılıyor.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete template
|
||||||
|
exports.deleteTemplate = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const template = await MailTemplate.findByPk(id);
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Template not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await template.destroy();
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: template,
|
message: 'Template deleted successfully',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -69,4 +155,3 @@ exports.previewTemplate = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = exports;
|
module.exports = exports;
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,22 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const templateController = require('../controllers/template.controller');
|
const templateController = require('../controllers/template.controller');
|
||||||
const { requireAuth } = require('../middlewares/auth');
|
const { requireAuth } = require('../middlewares/auth');
|
||||||
|
const {
|
||||||
|
validateCreateTemplate,
|
||||||
|
validateUpdateTemplate,
|
||||||
|
validatePreviewTemplate,
|
||||||
|
} = require('../validators/template.validator');
|
||||||
|
|
||||||
// All template routes require authentication
|
// All template routes require authentication
|
||||||
router.use(requireAuth);
|
router.use(requireAuth);
|
||||||
|
|
||||||
router.get('/', templateController.getAllTemplates);
|
router.get('/', templateController.getAllTemplates);
|
||||||
router.get('/:type', templateController.getTemplateByType);
|
router.post('/', validateCreateTemplate, templateController.createTemplate);
|
||||||
router.post('/preview', templateController.previewTemplate);
|
router.get('/:id(\\d+)', templateController.getTemplateById);
|
||||||
|
router.put('/:id(\\d+)', validateUpdateTemplate, templateController.updateTemplate);
|
||||||
|
router.delete('/:id(\\d+)', templateController.deleteTemplate);
|
||||||
|
router.get('/type/:type', templateController.getTemplateByType);
|
||||||
|
router.post('/preview', validatePreviewTemplate, templateController.previewTemplate);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
63
backend/src/validators/template.validator.js
Normal file
63
backend/src/validators/template.validator.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const Joi = require('joi');
|
||||||
|
|
||||||
|
const baseSchema = {
|
||||||
|
name: Joi.string().max(255).required(),
|
||||||
|
template_type: Joi.string().max(50).required(),
|
||||||
|
subject_template: Joi.string().max(500).allow('', null),
|
||||||
|
body_html: Joi.string().required(),
|
||||||
|
description: Joi.string().allow('', null),
|
||||||
|
active: Joi.boolean().default(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSchema = Joi.object(baseSchema);
|
||||||
|
|
||||||
|
const updateSchema = Joi.object({
|
||||||
|
...baseSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewSchema = Joi.object({
|
||||||
|
template_html: Joi.string().required(),
|
||||||
|
company_name: Joi.string().allow('', null),
|
||||||
|
employee_name: Joi.string().allow('', null),
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.validateCreateTemplate = (req, res, next) => {
|
||||||
|
const { error, value } = createSchema.validate(req.body, { abortEarly: false });
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: error.details.map((detail) => detail.message),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
req.body = value;
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.validateUpdateTemplate = (req, res, next) => {
|
||||||
|
const { error, value } = updateSchema.validate(req.body, { abortEarly: false });
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: error.details.map((detail) => detail.message),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
req.body = value;
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.validatePreviewTemplate = (req, res, next) => {
|
||||||
|
const { error, value } = previewSchema.validate(req.body, { abortEarly: false });
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: error.details.map((detail) => detail.message),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
req.body = value;
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ Modern ve responsive phishing test yönetim paneli frontend'i.
|
|||||||
- ⚡ **Vite** - Hızlı development server
|
- ⚡ **Vite** - Hızlı development server
|
||||||
- 🔐 **Session Auth** - Context-based authentication
|
- 🔐 **Session Auth** - Context-based authentication
|
||||||
- 📱 **Responsive** - Mobile-first tasarım
|
- 📱 **Responsive** - Mobile-first tasarım
|
||||||
- 🎯 **5 Sayfa** - Login, Dashboard, Companies, Tokens, Settings
|
- 🎯 **6 Sayfa** - Login, Dashboard, Companies, Tokens, Templates, Settings
|
||||||
|
|
||||||
## 🚀 Kurulum
|
## 🚀 Kurulum
|
||||||
|
|
||||||
@@ -47,6 +47,14 @@ npm run dev
|
|||||||
- Token oluştur + mail gönder
|
- Token oluştur + mail gönder
|
||||||
- Durum badge'leri (Tıklandı/Bekliyor)
|
- Durum badge'leri (Tıklandı/Bekliyor)
|
||||||
- Tıklama sayısı tracking
|
- Tıklama sayısı tracking
|
||||||
|
- Detay sayfası (tıklama geçmişi, IP & cihaz)
|
||||||
|
|
||||||
|
### ✉️ Mail Şablonları
|
||||||
|
- Tüm şablonların listesi (aktif/pasif)
|
||||||
|
- HTML editörü + Handlebars değişkenleri
|
||||||
|
- Önizleme (iframe) & tam ekran preview
|
||||||
|
- Template type kopyalama, aktif/pasif toggle
|
||||||
|
- Oluştur / Düzenle / Sil işlemleri
|
||||||
|
|
||||||
### ⚙️ Settings
|
### ⚙️ Settings
|
||||||
- Gmail yapılandırması
|
- Gmail yapılandırması
|
||||||
@@ -139,7 +147,8 @@ src/
|
|||||||
- ✅ Protected routes
|
- ✅ Protected routes
|
||||||
- ✅ Dashboard with stats
|
- ✅ Dashboard with stats
|
||||||
- ✅ Company management (CRUD)
|
- ✅ Company management (CRUD)
|
||||||
- ✅ Token management (CRUD + send)
|
- ✅ Token management (CRUD + send + detay)
|
||||||
|
- ✅ Mail templates (CRUD + preview)
|
||||||
- ✅ Settings (Gmail + Telegram)
|
- ✅ Settings (Gmail + Telegram)
|
||||||
- ✅ Responsive layout
|
- ✅ Responsive layout
|
||||||
- ✅ Material-UI theming
|
- ✅ Material-UI theming
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Companies from './pages/Companies';
|
|||||||
import CompanyDetail from './pages/CompanyDetail';
|
import CompanyDetail from './pages/CompanyDetail';
|
||||||
import Tokens from './pages/Tokens';
|
import Tokens from './pages/Tokens';
|
||||||
import TokenDetail from './pages/TokenDetail';
|
import TokenDetail from './pages/TokenDetail';
|
||||||
|
import Templates from './pages/Templates';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
@@ -56,6 +57,7 @@ function App() {
|
|||||||
<Route path="companies/:id" element={<CompanyDetail />} />
|
<Route path="companies/:id" element={<CompanyDetail />} />
|
||||||
<Route path="tokens" element={<Tokens />} />
|
<Route path="tokens" element={<Tokens />} />
|
||||||
<Route path="tokens/:id" element={<TokenDetail />} />
|
<Route path="tokens/:id" element={<TokenDetail />} />
|
||||||
|
<Route path="templates" element={<Templates />} />
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<Settings />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Logout,
|
Logout,
|
||||||
AccountCircle,
|
AccountCircle,
|
||||||
|
Email,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ const menuItems = [
|
|||||||
{ text: 'Dashboard', icon: <Dashboard />, path: '/' },
|
{ text: 'Dashboard', icon: <Dashboard />, path: '/' },
|
||||||
{ text: 'Şirketler', icon: <Business />, path: '/companies' },
|
{ text: 'Şirketler', icon: <Business />, path: '/companies' },
|
||||||
{ text: 'Tokenlar', icon: <TokenIcon />, path: '/tokens' },
|
{ text: 'Tokenlar', icon: <TokenIcon />, path: '/tokens' },
|
||||||
|
{ text: 'Mail Şablonları', icon: <Email />, path: '/templates' },
|
||||||
{ text: 'Ayarlar', icon: <Settings />, path: '/settings' },
|
{ text: 'Ayarlar', icon: <Settings />, path: '/settings' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
453
frontend/src/pages/Templates.jsx
Normal file
453
frontend/src/pages/Templates.jsx
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user