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:
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;
|
||||
|
||||
Reference in New Issue
Block a user