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}
+
+ }
+ onClick={() => setEditDialog(true)}
+ sx={{ mr: 1 }}
+ >
+ Düzenle
+
+ }
+ color="error"
+ onClick={() => setDeleteDialog(true)}
+ >
+ Sil
+
+
+
+ {/* 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 */}
+
+
+ {/* Delete Confirmation */}
+
+
+ );
+}
+
+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'}
+
+
+ }
+ onClick={handleSendMail}
+ disabled={sendingMail}
+ sx={{ mr: 1 }}
+ >
+ {sendingMail ? 'Gönderiliyor...' : 'Mail Gönder'}
+
+ }
+ color="error"
+ onClick={() => setDeleteDialog(true)}
+ >
+ Sil
+
+
+
+ {/* 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 */}
+
+
+ );
+}
+
+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 || '-'}