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:
salvacybersec
2025-11-10 17:13:05 +03:00
parent 0e5dffb7fc
commit dc16d0c549
4 changed files with 681 additions and 1 deletions

View 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">
ı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;