diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ef93d82..93c6158 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,7 +5,9 @@ import Layout from './components/Layout/Layout'; import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import Companies from './pages/Companies'; +import CompanyDetail from './pages/CompanyDetail'; import Tokens from './pages/Tokens'; +import TokenDetail from './pages/TokenDetail'; import Settings from './pages/Settings'; const theme = createTheme({ @@ -51,7 +53,9 @@ function App() { > } /> } /> + } /> } /> + } /> } /> diff --git a/frontend/src/pages/CompanyDetail.jsx b/frontend/src/pages/CompanyDetail.jsx new file mode 100644 index 0000000..436f098 --- /dev/null +++ b/frontend/src/pages/CompanyDetail.jsx @@ -0,0 +1,333 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Box, + Button, + Paper, + Typography, + Grid, + Card, + CardContent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Chip, + CircularProgress, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + IconButton, +} from '@mui/material'; +import { + ArrowBack, + Edit, + Delete, + Token as TokenIcon, + CheckCircle, + TrendingUp, +} from '@mui/icons-material'; +import { companyService } from '../services/companyService'; +import { format } from 'date-fns'; + +function CompanyDetail() { + const { id } = useParams(); + const navigate = useNavigate(); + const [company, setCompany] = useState(null); + const [tokens, setTokens] = useState([]); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [editDialog, setEditDialog] = useState(false); + const [deleteDialog, setDeleteDialog] = useState(false); + const [formData, setFormData] = useState({ + name: '', + description: '', + industry: '', + }); + + useEffect(() => { + loadData(); + }, [id]); + + const loadData = async () => { + try { + const [companyData, tokensData, statsData] = await Promise.all([ + companyService.getById(id), + companyService.getTokens(id), + companyService.getStats(id), + ]); + setCompany(companyData.data); + setTokens(tokensData.data); + setStats(statsData.data); + setFormData({ + name: companyData.data.name, + description: companyData.data.description || '', + industry: companyData.data.industry || '', + }); + } catch (error) { + console.error('Failed to load company:', error); + alert('Şirket yüklenemedi'); + navigate('/companies'); + } finally { + setLoading(false); + } + }; + + const handleUpdate = async () => { + try { + await companyService.update(id, formData); + setEditDialog(false); + loadData(); + } catch (error) { + console.error('Failed to update company:', error); + alert('Şirket güncellenemedi'); + } + }; + + const handleDelete = async () => { + try { + await companyService.delete(id); + navigate('/companies'); + } catch (error) { + console.error('Failed to delete company:', error); + alert('Şirket silinemedi: Önce tokenları silmelisiniz'); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + {/* Header */} + + navigate('/companies')} sx={{ mr: 2 }}> + + + + {company.name} + + + + + + {/* Stats Cards */} + + + + + + + + Toplam Token + + {stats.total_tokens} + + + + + + + + + + + + + Toplam Tıklama + + {stats.total_clicks} + + + + + + + + + + + + + Başarı Oranı + + {stats.click_rate}% + + 30 ? 'error' : 'warning'} + sx={{ fontSize: 40 }} + /> + + + + + + + {/* Company Info */} + + + Şirket Bilgileri + + + + + Sektör + + + {company.industry || 'Belirtilmemiş'} + + + + + Oluşturulma Tarihi + + + {format(new Date(company.created_at), 'dd/MM/yyyy HH:mm')} + + + {company.description && ( + + + Açıklama + + {company.description} + + )} + + + + {/* Tokens Table */} + + + Tokenlar ({tokens.length}) + + + + + + Email + Çalışan + Durum + Tıklama + Tarih + + + + {tokens.length === 0 ? ( + + + + Henüz token oluşturulmamış + + + + ) : ( + tokens.map((token) => ( + + {token.target_email} + {token.employee_name || '-'} + + + + {token.click_count}× + + {format(new Date(token.created_at), 'dd/MM/yyyy HH:mm')} + + + )) + )} + +
+
+
+ + {/* Edit Dialog */} + setEditDialog(false)} maxWidth="sm" fullWidth> + Şirket Düzenle + + setFormData({ ...formData, name: e.target.value })} + /> + setFormData({ ...formData, description: e.target.value })} + /> + setFormData({ ...formData, industry: e.target.value })} + /> + + + + + + + + {/* Delete Confirmation */} + setDeleteDialog(false)}> + Şirketi Sil? + + + {company.name} şirketini silmek istediğinizden emin misiniz? + Bu işlem geri alınamaz. + + {tokens.length > 0 && ( + + ⚠️ Bu şirkete ait {tokens.length} token var. Önce tokenları silmelisiniz. + + )} + + + + + + +
+ ); +} + +export default CompanyDetail; + diff --git a/frontend/src/pages/TokenDetail.jsx b/frontend/src/pages/TokenDetail.jsx new file mode 100644 index 0000000..627372a --- /dev/null +++ b/frontend/src/pages/TokenDetail.jsx @@ -0,0 +1,336 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Box, + Button, + Paper, + Typography, + Grid, + Card, + CardContent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Chip, + CircularProgress, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from '@mui/material'; +import { + ArrowBack, + Delete, + Send, + LocationOn, + Computer, + AccessTime, + CheckCircle, + Cancel, +} from '@mui/icons-material'; +import { tokenService } from '../services/tokenService'; +import { format } from 'date-fns'; + +function TokenDetail() { + const { id } = useParams(); + const navigate = useNavigate(); + const [token, setToken] = useState(null); + const [clicks, setClicks] = useState([]); + const [loading, setLoading] = useState(true); + const [deleteDialog, setDeleteDialog] = useState(false); + const [sendingMail, setSendingMail] = useState(false); + + useEffect(() => { + loadData(); + }, [id]); + + const loadData = async () => { + try { + const [tokenData, clicksData] = await Promise.all([ + tokenService.getById(id), + tokenService.getClicks(id), + ]); + setToken(tokenData.data); + setClicks(clicksData.data); + } catch (error) { + console.error('Failed to load token:', error); + alert('Token yüklenemedi'); + navigate('/tokens'); + } finally { + setLoading(false); + } + }; + + const handleSendMail = async () => { + setSendingMail(true); + try { + await tokenService.sendMail(id); + alert('Mail başarıyla gönderildi!'); + } catch (error) { + console.error('Failed to send mail:', error); + alert('Mail gönderilemedi: ' + (error.response?.data?.error || error.message)); + } finally { + setSendingMail(false); + } + }; + + const handleDelete = async () => { + try { + await tokenService.delete(id); + navigate('/tokens'); + } catch (error) { + console.error('Failed to delete token:', error); + alert('Token silinemedi'); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + {/* Header */} + + navigate('/tokens')} sx={{ mr: 2 }}> + + + + {token.target_email} + + {token.company?.name} - {token.employee_name || 'İsimsiz'} + + + + + + + {/* Stats Cards */} + + + + + + {token.clicked ? ( + + ) : ( + + )} + + + Durum + + + {token.clicked ? 'Tıklandı' : 'Bekliyor'} + + + + + + + + + + + + + + Tıklama Sayısı + + {token.click_count}× + + + + + + + + + + + Oluşturulma + + + {format(new Date(token.created_at), 'dd/MM/yyyy')} + + + {format(new Date(token.created_at), 'HH:mm')} + + + + + + + + + + + İlk Tıklama + + {token.first_clicked_at ? ( + <> + + {format(new Date(token.first_clicked_at), 'dd/MM/yyyy')} + + + {format(new Date(token.first_clicked_at), 'HH:mm')} + + + ) : ( + + Henüz tıklanmadı + + )} + + + + + + + {/* Token Info */} + + + Token Bilgileri + + + + + Token + + + {token.token} + + + + + Tracking URL + + + {`${window.location.origin.replace('5173', '3000')}/t/${token.token}`} + + + + + Şablon Tipi + + + {token.template_type || 'Belirtilmemiş'} + + + + + Şirket + + {token.company?.name} + + + + + {/* Click History */} + + + Tıklama Geçmişi ({clicks.length}) + + + + + + Zaman + IP Adresi + Konum + Cihaz + Tarayıcı + + + + {clicks.length === 0 ? ( + + + + Henüz tıklama kaydı yok + + + + ) : ( + clicks.map((click) => ( + + + {format(new Date(click.clicked_at), 'dd/MM/yyyy HH:mm:ss')} + + + {click.ip_address} + + + + + {click.city && click.country + ? `${click.city}, ${click.country}` + : 'Bilinmiyor'} + + + + + + {click.device || 'Bilinmiyor'} + + + {click.browser || 'Bilinmiyor'} + + )) + )} + +
+
+
+ + {/* Delete Confirmation */} + setDeleteDialog(false)}> + Token'ı Sil? + + + {token.target_email} için oluşturulan bu token'ı silmek + istediğinizden emin misiniz? Bu işlem geri alınamaz. + + {clicks.length > 0 && ( + + ⚠️ Bu token'a ait {clicks.length} tıklama kaydı da silinecek. + + )} + + + + + + +
+ ); +} + +export default TokenDetail; + diff --git a/frontend/src/pages/Tokens.jsx b/frontend/src/pages/Tokens.jsx index 046be91..b07ceb2 100644 --- a/frontend/src/pages/Tokens.jsx +++ b/frontend/src/pages/Tokens.jsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Box, Button, @@ -26,6 +27,7 @@ import { templateService } from '../services/templateService'; import { format } from 'date-fns'; function Tokens() { + const navigate = useNavigate(); const [tokens, setTokens] = useState([]); const [companies, setCompanies] = useState([]); const [templates, setTemplates] = useState([]); @@ -107,7 +109,12 @@ function Tokens() { {tokens.map((token) => ( - + navigate(`/tokens/${token.id}`)} + > {token.target_email} {token.company?.name} {token.employee_name || '-'}