the-tip-top-frontend/app/admin/marketing-data/page.tsx
soufiane f20cf40fff feat: redesign admin panel with blanc cassé theme
- Update sidebar and header with off-white (#faf8f5) background
- Add ticket stats endpoint integration for global counts
- Redesign tirages page with animation and improved layout
- Add red accent color for admin avatar
- Update various button styles and remove unnecessary elements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 19:43:14 +01:00

506 lines
19 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 { API_BASE_URL } from '@/utils/constants';
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');
const response = await fetch(
`${API_BASE_URL}/admin/marketing/stats`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}
);
if (!response.ok) {
let errorMessage = 'Erreur lors du chargement';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorData.message || errorMessage;
} catch {
// Ignore parse error
}
throw new Error(errorMessage);
}
const data = await response.json();
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(
`${API_BASE_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',
'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.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>
);
}
// Filtrer les données vides/non spécifiées pour les graphiques
const filteredGenderData = stats.byGender.filter(
(g) => g.gender && !g.gender.toLowerCase().includes('not_specified') && !g.gender.toLowerCase().includes('non spécifié') && g.count > 0
);
const filteredAgeData = stats.byAge.filter(
(a) => a.range && !a.range.toLowerCase().includes('non spécifié') && a.count > 0
);
const filteredCityData = stats.byCity.filter(
(c) => c.city && c.city.trim() !== '' && !c.city.toLowerCase().includes('non spécifié') && c.count > 0
);
// Vérifier s'il y a des données démographiques valides
const hasDemographicData = filteredGenderData.length > 0 || filteredAgeData.length > 0 || filteredCityData.length > 0;
return (
<div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8">
{/* En-tête */}
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-6 mb-8">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-white/20 rounded-xl flex items-center justify-center">
<Mail className="w-7 h-7 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-white">
Données Marketing
</h1>
<p className="text-indigo-200">
Statistiques et export des données pour vos campagnes d'emailing
</p>
</div>
</div>
</div>
{/* Statistiques globales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-2xl p-6 border border-blue-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-blue-600 font-medium">Total Clients</p>
<p className="text-3xl font-bold text-blue-700 mt-1">
{stats.totalClients.toLocaleString()}
</p>
</div>
<div className="w-14 h-14 bg-blue-200 rounded-xl flex items-center justify-center">
<Users className="w-7 h-7 text-blue-600" />
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-green-100 rounded-2xl p-6 border border-green-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-green-600 font-medium">Participants Actifs</p>
<p className="text-3xl font-bold text-green-700 mt-1">
{stats.activeParticipants.toLocaleString()}
</p>
<p className="text-xs text-green-600 mt-1">
{((stats.activeParticipants / stats.totalClients) * 100).toFixed(1)}% du total
</p>
</div>
<div className="w-14 h-14 bg-green-200 rounded-xl flex items-center justify-center">
<UserCheck className="w-7 h-7 text-green-600" />
</div>
</div>
</div>
<div className="bg-gradient-to-br from-purple-50 to-purple-100 rounded-2xl p-6 border border-purple-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-purple-600 font-medium">Gagnants</p>
<p className="text-3xl font-bold text-purple-700 mt-1">
{stats.winners.toLocaleString()}
</p>
<p className="text-xs text-purple-600 mt-1">
{stats.activeParticipants > 0 ? ((stats.winners / stats.activeParticipants) * 100).toFixed(1) : 0}% de conversion
</p>
</div>
<div className="w-14 h-14 bg-purple-200 rounded-xl flex items-center justify-center">
<Gift className="w-7 h-7 text-purple-600" />
</div>
</div>
</div>
<div className="bg-gradient-to-br from-orange-50 to-orange-100 rounded-2xl p-6 border border-orange-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-orange-600 font-medium">Inactifs</p>
<p className="text-3xl font-bold text-orange-700 mt-1">
{stats.inactiveParticipants.toLocaleString()}
</p>
<p className="text-xs text-orange-600 mt-1">À réactiver</p>
</div>
<div className="w-14 h-14 bg-orange-200 rounded-xl flex items-center justify-center">
<TrendingUp className="w-7 h-7 text-orange-600" />
</div>
</div>
</div>
</div>
{/* Graphiques - Affichés uniquement s'il y a des données valides */}
{hasDemographicData && (
<>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Répartition par genre */}
{filteredGenderData.length > 0 && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
<BarChart3 className="w-5 h-5 text-blue-600" />
</div>
Répartition par Genre
</h2>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={filteredGenderData}
dataKey="count"
nameKey="gender"
cx="50%"
cy="50%"
outerRadius={100}
label={(entry: any) => `${entry.gender}: ${entry.count}`}
>
{filteredGenderData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
)}
{/* Répartition par âge */}
{filteredAgeData.length > 0 && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<div className="w-10 h-10 bg-green-100 rounded-xl flex items-center justify-center">
<BarChart3 className="w-5 h-5 text-green-600" />
</div>
Répartition par Âge
</h2>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={filteredAgeData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="range" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="count" fill="#10b981" name="Participants" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
)}
</div>
{/* Top villes */}
{filteredCityData.length > 0 && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 mb-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
<MapPin className="w-5 h-5 text-purple-600" />
</div>
Top Villes ({filteredCityData.length})
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
{filteredCityData.slice(0, 10).map((city, index) => (
<div
key={index}
className="bg-gradient-to-br from-purple-50 to-indigo-50 p-4 rounded-xl border border-purple-100 hover:shadow-md transition-shadow"
>
<span className="inline-block px-2 py-0.5 bg-purple-200 text-purple-700 text-xs font-bold rounded-full mb-2">#{index + 1}</span>
<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>
</div>
)}
</>
)}
{/* Section Export */}
<div className="bg-[#faf8f5] rounded-2xl border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-bold text-[#1e3a5f] flex items-center gap-3">
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center border border-gray-200">
<Download className="w-5 h-5 text-[#1e3a5f]" />
</div>
Exporter les Données pour Emailing
</h2>
</div>
<div className="p-6 space-y-5">
{/* Sélection du segment */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
Segment à exporter
</label>
<select
value={selectedSegment}
onChange={(e) => setSelectedSegment(e.target.value)}
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl 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 - Affichés uniquement si données disponibles */}
{(filteredCityData.length > 0 || filteredGenderData.length > 0) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredCityData.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Ville</label>
<select
value={filters.city}
onChange={(e) => setFilters({ ...filters, city: e.target.value })}
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Toutes les villes</option>
{filteredCityData.slice(0, 10).map((city) => (
<option key={city.city} value={city.city}>
{city.city} ({city.count})
</option>
))}
</select>
</div>
)}
{filteredGenderData.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Genre</label>
<select
value={filters.gender}
onChange={(e) => setFilters({ ...filters, gender: e.target.value })}
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Tous les genres</option>
{filteredGenderData.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-2">
<Button
onClick={exportSegmentData}
isLoading={exportLoading}
disabled={exportLoading}
className="!bg-white !text-black border border-gray-200 hover:!bg-gray-100"
>
<Download className="w-5 h-5 mr-2" />
Exporter en CSV
</Button>
<Button variant="outline" onClick={loadMarketingStats} className="bg-white">
<FileText className="w-5 h-5 mr-2" />
Actualiser les données
</Button>
</div>
<div className="p-4 bg-white border border-gray-200 rounded-xl">
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0">
<Mail className="w-4 h-4 text-gray-600" />
</div>
<p className="text-sm text-gray-600">
<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>
</div>
</div>
</div>
);
}