feat: Add detail pages for Companies and Tokens
- Created CompanyDetail page with stats, info, and tokens list - Created TokenDetail page with click history and full tracking info - Added routes for /companies/:id and /tokens/:id - Made table rows clickable to navigate to detail pages - Added edit, delete, and mail resend functionality - Shows IP addresses, GeoIP location, device and browser info in click logs
This commit is contained in:
@@ -5,7 +5,9 @@ import Layout from './components/Layout/Layout';
|
|||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import Companies from './pages/Companies';
|
import Companies from './pages/Companies';
|
||||||
|
import CompanyDetail from './pages/CompanyDetail';
|
||||||
import Tokens from './pages/Tokens';
|
import Tokens from './pages/Tokens';
|
||||||
|
import TokenDetail from './pages/TokenDetail';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
@@ -51,7 +53,9 @@ function App() {
|
|||||||
>
|
>
|
||||||
<Route index element={<Dashboard />} />
|
<Route index element={<Dashboard />} />
|
||||||
<Route path="companies" element={<Companies />} />
|
<Route path="companies" element={<Companies />} />
|
||||||
|
<Route path="companies/:id" element={<CompanyDetail />} />
|
||||||
<Route path="tokens" element={<Tokens />} />
|
<Route path="tokens" element={<Tokens />} />
|
||||||
|
<Route path="tokens/:id" element={<TokenDetail />} />
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<Settings />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
333
frontend/src/pages/CompanyDetail.jsx
Normal file
333
frontend/src/pages/CompanyDetail.jsx
Normal file
@@ -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 (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* Header */}
|
||||||
|
<Box display="flex" alignItems="center" mb={3}>
|
||||||
|
<IconButton onClick={() => navigate('/companies')} sx={{ mr: 2 }}>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h4" sx={{ flexGrow: 1 }}>
|
||||||
|
{company.name}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
startIcon={<Edit />}
|
||||||
|
onClick={() => setEditDialog(true)}
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
Düzenle
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<Delete />}
|
||||||
|
color="error"
|
||||||
|
onClick={() => setDeleteDialog(true)}
|
||||||
|
>
|
||||||
|
Sil
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography color="textSecondary" variant="body2">
|
||||||
|
Toplam Token
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4">{stats.total_tokens}</Typography>
|
||||||
|
</Box>
|
||||||
|
<TokenIcon color="primary" sx={{ fontSize: 40 }} />
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography color="textSecondary" variant="body2">
|
||||||
|
Toplam Tıklama
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4">{stats.total_clicks}</Typography>
|
||||||
|
</Box>
|
||||||
|
<CheckCircle color="success" sx={{ fontSize: 40 }} />
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography color="textSecondary" variant="body2">
|
||||||
|
Başarı Oranı
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4">{stats.click_rate}%</Typography>
|
||||||
|
</Box>
|
||||||
|
<TrendingUp
|
||||||
|
color={stats.click_rate > 30 ? 'error' : 'warning'}
|
||||||
|
sx={{ fontSize: 40 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Company Info */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Şirket Bilgileri
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Sektör
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{company.industry || 'Belirtilmemiş'}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Oluşturulma Tarihi
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{format(new Date(company.created_at), 'dd/MM/yyyy HH:mm')}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
{company.description && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Açıklama
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">{company.description}</Typography>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Tokens Table */}
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Tokenlar ({tokens.length})
|
||||||
|
</Typography>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Email</TableCell>
|
||||||
|
<TableCell>Çalışan</TableCell>
|
||||||
|
<TableCell>Durum</TableCell>
|
||||||
|
<TableCell align="right">Tıklama</TableCell>
|
||||||
|
<TableCell>Tarih</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{tokens.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} align="center">
|
||||||
|
<Typography color="textSecondary">
|
||||||
|
Henüz token oluşturulmamış
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
tokens.map((token) => (
|
||||||
|
<TableRow key={token.id} hover>
|
||||||
|
<TableCell>{token.target_email}</TableCell>
|
||||||
|
<TableCell>{token.employee_name || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
label={token.clicked ? 'Tıklandı' : 'Bekliyor'}
|
||||||
|
color={token.clicked ? 'success' : 'default'}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">{token.click_count}×</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{format(new Date(token.created_at), 'dd/MM/yyyy HH:mm')}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Edit Dialog */}
|
||||||
|
<Dialog open={editDialog} onClose={() => setEditDialog(false)} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>Şirket Düzenle</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="Şirket Adı"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Açıklama"
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Sektör"
|
||||||
|
fullWidth
|
||||||
|
value={formData.industry}
|
||||||
|
onChange={(e) => setFormData({ ...formData, industry: e.target.value })}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setEditDialog(false)}>İptal</Button>
|
||||||
|
<Button onClick={handleUpdate} variant="contained">
|
||||||
|
Güncelle
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation */}
|
||||||
|
<Dialog open={deleteDialog} onClose={() => setDeleteDialog(false)}>
|
||||||
|
<DialogTitle>Şirketi Sil?</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography>
|
||||||
|
<strong>{company.name}</strong> şirketini silmek istediğinizden emin misiniz?
|
||||||
|
Bu işlem geri alınamaz.
|
||||||
|
</Typography>
|
||||||
|
{tokens.length > 0 && (
|
||||||
|
<Typography color="error" sx={{ mt: 2 }}>
|
||||||
|
⚠️ Bu şirkete ait {tokens.length} token var. Önce tokenları silmelisiniz.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteDialog(false)}>İptal</Button>
|
||||||
|
<Button onClick={handleDelete} color="error" variant="contained">
|
||||||
|
Sil
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CompanyDetail;
|
||||||
|
|
||||||
336
frontend/src/pages/TokenDetail.jsx
Normal file
336
frontend/src/pages/TokenDetail.jsx
Normal file
@@ -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 (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* Header */}
|
||||||
|
<Box display="flex" alignItems="center" mb={3}>
|
||||||
|
<IconButton onClick={() => navigate('/tokens')} sx={{ mr: 2 }}>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Typography variant="h5">{token.target_email}</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
{token.company?.name} - {token.employee_name || 'İsimsiz'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
startIcon={<Send />}
|
||||||
|
onClick={handleSendMail}
|
||||||
|
disabled={sendingMail}
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
{sendingMail ? 'Gönderiliyor...' : 'Mail Gönder'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<Delete />}
|
||||||
|
color="error"
|
||||||
|
onClick={() => setDeleteDialog(true)}
|
||||||
|
>
|
||||||
|
Sil
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||||
|
<Grid item xs={12} sm={3}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
{token.clicked ? (
|
||||||
|
<CheckCircle color="success" sx={{ fontSize: 40 }} />
|
||||||
|
) : (
|
||||||
|
<Cancel color="disabled" sx={{ fontSize: 40 }} />
|
||||||
|
)}
|
||||||
|
<Box>
|
||||||
|
<Typography color="textSecondary" variant="body2">
|
||||||
|
Durum
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
{token.clicked ? 'Tıklandı' : 'Bekliyor'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={3}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
<AccessTime color="primary" sx={{ fontSize: 40 }} />
|
||||||
|
<Box>
|
||||||
|
<Typography color="textSecondary" variant="body2">
|
||||||
|
Tıklama Sayısı
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6">{token.click_count}×</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={3}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box>
|
||||||
|
<Typography color="textSecondary" variant="body2" gutterBottom>
|
||||||
|
Oluşturulma
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{format(new Date(token.created_at), 'dd/MM/yyyy')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
{format(new Date(token.created_at), 'HH:mm')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={3}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box>
|
||||||
|
<Typography color="textSecondary" variant="body2" gutterBottom>
|
||||||
|
İlk Tıklama
|
||||||
|
</Typography>
|
||||||
|
{token.first_clicked_at ? (
|
||||||
|
<>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{format(new Date(token.first_clicked_at), 'dd/MM/yyyy')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
{format(new Date(token.first_clicked_at), 'HH:mm')}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body1" color="textSecondary">
|
||||||
|
Henüz tıklanmadı
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Token Info */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Token Bilgileri
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Token
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ fontFamily: 'monospace' }}>
|
||||||
|
{token.token}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Tracking URL
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{ fontFamily: 'monospace', wordBreak: 'break-all' }}
|
||||||
|
>
|
||||||
|
{`${window.location.origin.replace('5173', '3000')}/t/${token.token}`}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Şablon Tipi
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{token.template_type || 'Belirtilmemiş'}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Şirket
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">{token.company?.name}</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Click History */}
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Tıklama Geçmişi ({clicks.length})
|
||||||
|
</Typography>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Zaman</TableCell>
|
||||||
|
<TableCell>IP Adresi</TableCell>
|
||||||
|
<TableCell>Konum</TableCell>
|
||||||
|
<TableCell>Cihaz</TableCell>
|
||||||
|
<TableCell>Tarayıcı</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{clicks.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} align="center">
|
||||||
|
<Typography color="textSecondary">
|
||||||
|
Henüz tıklama kaydı yok
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
clicks.map((click) => (
|
||||||
|
<TableRow key={click.id} hover>
|
||||||
|
<TableCell>
|
||||||
|
{format(new Date(click.clicked_at), 'dd/MM/yyyy HH:mm:ss')}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||||||
|
{click.ip_address}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Box display="flex" alignItems="center" gap={0.5}>
|
||||||
|
<LocationOn fontSize="small" color="action" />
|
||||||
|
{click.city && click.country
|
||||||
|
? `${click.city}, ${click.country}`
|
||||||
|
: 'Bilinmiyor'}
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Box display="flex" alignItems="center" gap={0.5}>
|
||||||
|
<Computer fontSize="small" color="action" />
|
||||||
|
{click.device || 'Bilinmiyor'}
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{click.browser || 'Bilinmiyor'}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Delete Confirmation */}
|
||||||
|
<Dialog open={deleteDialog} onClose={() => setDeleteDialog(false)}>
|
||||||
|
<DialogTitle>Token'ı Sil?</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography>
|
||||||
|
<strong>{token.target_email}</strong> için oluşturulan bu token'ı silmek
|
||||||
|
istediğinizden emin misiniz? Bu işlem geri alınamaz.
|
||||||
|
</Typography>
|
||||||
|
{clicks.length > 0 && (
|
||||||
|
<Typography color="warning.main" sx={{ mt: 2 }}>
|
||||||
|
⚠️ Bu token'a ait {clicks.length} tıklama kaydı da silinecek.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteDialog(false)}>İptal</Button>
|
||||||
|
<Button onClick={handleDelete} color="error" variant="contained">
|
||||||
|
Sil
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TokenDetail;
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -26,6 +27,7 @@ import { templateService } from '../services/templateService';
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
function Tokens() {
|
function Tokens() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [tokens, setTokens] = useState([]);
|
const [tokens, setTokens] = useState([]);
|
||||||
const [companies, setCompanies] = useState([]);
|
const [companies, setCompanies] = useState([]);
|
||||||
const [templates, setTemplates] = useState([]);
|
const [templates, setTemplates] = useState([]);
|
||||||
@@ -107,7 +109,12 @@ function Tokens() {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{tokens.map((token) => (
|
{tokens.map((token) => (
|
||||||
<TableRow key={token.id} hover sx={{ cursor: 'pointer' }}>
|
<TableRow
|
||||||
|
key={token.id}
|
||||||
|
hover
|
||||||
|
sx={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => navigate(`/tokens/${token.id}`)}
|
||||||
|
>
|
||||||
<TableCell>{token.target_email}</TableCell>
|
<TableCell>{token.target_email}</TableCell>
|
||||||
<TableCell>{token.company?.name}</TableCell>
|
<TableCell>{token.company?.name}</TableCell>
|
||||||
<TableCell>{token.employee_name || '-'}</TableCell>
|
<TableCell>{token.employee_name || '-'}</TableCell>
|
||||||
|
|||||||
Reference in New Issue
Block a user