the-tip-top-frontend/app/admin/marketing-data/page.tsx
2025-11-17 23:38:02 +01:00

466 lines
15 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Card } from '@/components/ui/Card';
import Button from '@/components/Button';
import toast from 'react-hot-toast';
import {
Mail,
Download,
Users,
TrendingUp,
MapPin,
UserCheck,
Gift,
BarChart3,
Filter,
FileText,
} from 'lucide-react';
import {
PieChart,
Pie,
Cell,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
interface MarketingStats {
totalClients: number;
activeParticipants: number;
inactiveParticipants: number;
winners: number;
nonWinners: number;
byCity: Array<{ city: string; count: number }>;
byAge: Array<{ range: string; count: number }>;
byGender: Array<{ gender: string; count: number }>;
}
export default function MarketingPage() {
const [stats, setStats] = useState<MarketingStats | null>(null);
const [loading, setLoading] = useState(true);
const [exportLoading, setExportLoading] = useState(false);
// Filtres d'export
const [selectedSegment, setSelectedSegment] = useState<string>('all');
const [filters, setFilters] = useState({
verified: false,
city: '',
gender: '',
});
useEffect(() => {
loadMarketingStats();
}, []);
const loadMarketingStats = async () => {
try {
setLoading(true);
const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
console.log('Loading marketing stats...');
console.log('API URL:', process.env.NEXT_PUBLIC_API_URL);
console.log('Token present:', !!token);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/admin/marketing/stats`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}
);
console.log('Response status:', response.status);
if (!response.ok) {
let errorMessage = 'Erreur lors du chargement';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorData.message || errorMessage;
console.error('Error from API:', errorData);
} catch (e) {
console.error('Failed to parse error response');
}
throw new Error(errorMessage);
}
const data = await response.json();
console.log('Marketing stats loaded:', data);
setStats(data.data);
} catch (error: any) {
console.error('Error loading marketing stats:', error);
toast.error(error.message || 'Erreur lors du chargement des statistiques marketing');
} finally {
setLoading(false);
}
};
const exportSegmentData = async () => {
try {
setExportLoading(true);
const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
// Construire les critères selon le segment sélectionné
const criteria: any = {};
if (selectedSegment === 'active') {
criteria.hasPlayed = true;
} else if (selectedSegment === 'inactive') {
criteria.hasPlayed = false;
} else if (selectedSegment === 'winners') {
criteria.hasWon = true;
} else if (selectedSegment === 'non-winners') {
criteria.hasPlayed = true;
criteria.hasWon = false;
}
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/admin/marketing/export`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
criteria,
filters: {
verified: filters.verified || undefined,
city: filters.city || undefined,
gender: filters.gender || undefined,
},
}),
}
);
if (!response.ok) throw new Error('Erreur lors de l\'export');
const data = await response.json();
const users = data.data;
// Créer le CSV
const csvContent = [
[
'Email',
'Prénom',
'Nom',
'Téléphone',
'Ville',
'Code Postal',
'Genre',
'Âge',
'Vérifié',
'A joué',
'A gagné',
'Nombre de tickets',
],
...users.map((user: any) => [
user.email,
user.first_name || '',
user.last_name || '',
user.phone || '',
user.city || '',
user.postal_code || '',
user.gender || '',
user.age || '',
user.is_verified ? 'Oui' : 'Non',
user.has_played ? 'Oui' : 'Non',
user.has_won ? 'Oui' : 'Non',
user.ticket_count || 0,
]),
]
.map((row) => row.join(','))
.join('\n');
// Télécharger le fichier
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `export-marketing-${selectedSegment}-${new Date().toISOString().split('T')[0]}.csv`;
link.click();
toast.success(`${users.length} contacts exportés avec succès!`);
} catch (error: any) {
toast.error(error.message || 'Erreur lors de l\'export');
} finally {
setExportLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Chargement des données marketing...</p>
</div>
</div>
);
}
if (!stats) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-800">
Erreur lors du chargement des statistiques marketing
</div>
</div>
);
}
return (
<div className="p-6 max-w-7xl mx-auto">
{/* En-tête */}
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2 flex items-center gap-3">
<Mail className="w-10 h-10 text-blue-600" />
Données Marketing
</h1>
<p className="text-gray-600">
Statistiques et export des données pour vos campagnes d'emailing
</p>
</div>
{/* Statistiques globales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Total Clients</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
{stats.totalClients.toLocaleString()}
</p>
</div>
<Users className="w-12 h-12 text-blue-500" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Participants Actifs</p>
<p className="text-3xl font-bold text-green-600 mt-1">
{stats.activeParticipants.toLocaleString()}
</p>
<p className="text-xs text-gray-500 mt-1">
{((stats.activeParticipants / stats.totalClients) * 100).toFixed(1)}% du total
</p>
</div>
<UserCheck className="w-12 h-12 text-green-500" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Gagnants</p>
<p className="text-3xl font-bold text-purple-600 mt-1">
{stats.winners.toLocaleString()}
</p>
<p className="text-xs text-gray-500 mt-1">
{((stats.winners / stats.activeParticipants) * 100).toFixed(1)}% de conversion
</p>
</div>
<Gift className="w-12 h-12 text-purple-500" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Inactifs</p>
<p className="text-3xl font-bold text-orange-600 mt-1">
{stats.inactiveParticipants.toLocaleString()}
</p>
<p className="text-xs text-gray-500 mt-1">À réactiver</p>
</div>
<TrendingUp className="w-12 h-12 text-orange-500" />
</div>
</Card>
</div>
{/* Graphiques */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Répartition par genre */}
<Card className="p-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<BarChart3 className="w-6 h-6 text-blue-600" />
Répartition par Genre
</h2>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={stats.byGender}
dataKey="count"
nameKey="gender"
cx="50%"
cy="50%"
outerRadius={100}
label={(entry) => `${entry.gender}: ${entry.count}`}
>
{stats.byGender.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</Card>
{/* Répartition par âge */}
<Card className="p-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<BarChart3 className="w-6 h-6 text-green-600" />
Répartition par Âge
</h2>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={stats.byAge}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="range" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="count" fill="#10b981" name="Participants" />
</BarChart>
</ResponsiveContainer>
</Card>
</div>
{/* Top villes */}
<Card className="p-6 mb-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<MapPin className="w-6 h-6 text-purple-600" />
Top Villes ({stats.byCity.length})
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
{stats.byCity.slice(0, 10).map((city, index) => (
<div
key={index}
className="bg-gradient-to-br from-purple-50 to-blue-50 p-4 rounded-lg border border-purple-200"
>
<p className="text-sm text-gray-600">#{index + 1}</p>
<p className="font-bold text-gray-900 truncate">{city.city}</p>
<p className="text-2xl font-bold text-purple-600">{city.count}</p>
</div>
))}
</div>
</Card>
{/* Section Export */}
<Card className="p-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<Download className="w-6 h-6 text-blue-600" />
Exporter les Données pour Emailing
</h2>
<div className="space-y-4">
{/* Sélection du segment */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Segment à exporter
</label>
<select
value={selectedSegment}
onChange={(e) => setSelectedSegment(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tous les clients ({stats.totalClients})</option>
<option value="active">
Participants actifs ({stats.activeParticipants})
</option>
<option value="inactive">
Participants inactifs ({stats.inactiveParticipants})
</option>
<option value="winners">Gagnants ({stats.winners})</option>
<option value="non-winners">Non-gagnants ({stats.nonWinners})</option>
</select>
</div>
{/* Filtres additionnels */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={filters.verified}
onChange={(e) => setFilters({ ...filters, verified: e.target.checked })}
className="w-4 h-4"
/>
<span className="text-sm text-gray-700">Emails vérifiés uniquement</span>
</label>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Ville</label>
<select
value={filters.city}
onChange={(e) => setFilters({ ...filters, city: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="">Toutes les villes</option>
{stats.byCity.slice(0, 10).map((city) => (
<option key={city.city} value={city.city}>
{city.city} ({city.count})
</option>
))}
</select>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Genre</label>
<select
value={filters.gender}
onChange={(e) => setFilters({ ...filters, gender: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="">Tous les genres</option>
{stats.byGender.map((g) => (
<option key={g.gender} value={g.gender}>
{g.gender} ({g.count})
</option>
))}
</select>
</div>
</div>
{/* Boutons d'action */}
<div className="flex gap-3 pt-4">
<Button
onClick={exportSegmentData}
isLoading={exportLoading}
disabled={exportLoading}
className="bg-blue-600 hover:bg-blue-700"
>
<Download className="w-5 h-5 mr-2" />
Exporter en CSV
</Button>
<Button variant="outline" onClick={loadMarketingStats}>
<FileText className="w-5 h-5 mr-2" />
Actualiser les données
</Button>
</div>
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Note:</strong> L'export générera un fichier CSV avec les emails, noms,
prénoms et autres données de contact. Utilisez ce fichier pour vos campagnes
d'emailing avec votre outil préféré (Mailchimp, SendGrid, etc.).
</p>
</div>
</div>
</Card>
</div>
);
}