fix: hide empty demographic data in marketing page

- Filter out NOT_SPECIFIED and empty values from charts
- Show placeholder message when no demographic data available
- Hide city/gender filters when no valid data
- Remove debug console.log statements

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
soufiane 2025-12-01 15:55:29 +01:00
parent 51ec802131
commit f2d4bb3c5f

View File

@ -66,10 +66,6 @@ export default function MarketingPage() {
setLoading(true); setLoading(true);
const token = localStorage.getItem('auth_token') || localStorage.getItem('token'); const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
console.log('Loading marketing stats...');
console.log('API URL:', API_BASE_URL);
console.log('Token present:', !!token);
const response = await fetch( const response = await fetch(
`${API_BASE_URL}/admin/marketing/stats`, `${API_BASE_URL}/admin/marketing/stats`,
{ {
@ -80,22 +76,18 @@ export default function MarketingPage() {
} }
); );
console.log('Response status:', response.status);
if (!response.ok) { if (!response.ok) {
let errorMessage = 'Erreur lors du chargement'; let errorMessage = 'Erreur lors du chargement';
try { try {
const errorData = await response.json(); const errorData = await response.json();
errorMessage = errorData.error || errorData.message || errorMessage; errorMessage = errorData.error || errorData.message || errorMessage;
console.error('Error from API:', errorData); } catch {
} catch (e) { // Ignore parse error
console.error('Failed to parse error response');
} }
throw new Error(errorMessage); throw new Error(errorMessage);
} }
const data = await response.json(); const data = await response.json();
console.log('Marketing stats loaded:', data);
setStats(data.data); setStats(data.data);
} catch (error: any) { } catch (error: any) {
console.error('Error loading marketing stats:', error); console.error('Error loading marketing stats:', error);
@ -216,6 +208,22 @@ export default function MarketingPage() {
); );
} }
// 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 ( return (
<div className="p-6 max-w-7xl mx-auto"> <div className="p-6 max-w-7xl mx-auto">
{/* En-tête */} {/* En-tête */}
@ -287,73 +295,95 @@ export default function MarketingPage() {
</Card> </Card>
</div> </div>
{/* Graphiques */} {/* Graphiques - Affichés uniquement s'il y a des données valides */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> {hasDemographicData ? (
{/* Répartition par genre */} <>
<Card className="p-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2"> {/* Répartition par genre */}
<BarChart3 className="w-6 h-6 text-blue-600" /> {filteredGenderData.length > 0 && (
Répartition par Genre <Card className="p-6">
</h2> <h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<ResponsiveContainer width="100%" height={300}> <BarChart3 className="w-6 h-6 text-blue-600" />
<PieChart> Répartition par Genre
<Pie </h2>
data={stats.byGender} <ResponsiveContainer width="100%" height={300}>
dataKey="count" <PieChart>
nameKey="gender" <Pie
cx="50%" data={filteredGenderData}
cy="50%" dataKey="count"
outerRadius={100} nameKey="gender"
label={(entry: any) => `${entry.gender}: ${entry.count}`} cx="50%"
> cy="50%"
{stats.byGender.map((entry, index) => ( outerRadius={100}
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> 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>
</Card>
)}
{/* Répartition par âge */}
{filteredAgeData.length > 0 && (
<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={filteredAgeData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="range" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="count" fill="#10b981" name="Participants" />
</BarChart>
</ResponsiveContainer>
</Card>
)}
</div>
{/* Top villes */}
{filteredCityData.length > 0 && (
<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 ({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-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>
))} ))}
</Pie> </div>
<Tooltip /> </Card>
<Legend /> )}
</PieChart> </>
</ResponsiveContainer> ) : (
<Card className="p-6 mb-6">
<div className="text-center py-8">
<BarChart3 className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-600 mb-2">
Données démographiques non disponibles
</h3>
<p className="text-gray-500">
Les informations sur le genre, l'âge et la ville des participants ne sont pas encore renseignées.
</p>
</div>
</Card> </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 */} {/* Section Export */}
<Card className="p-6"> <Card className="p-6">
@ -385,40 +415,46 @@ export default function MarketingPage() {
</select> </select>
</div> </div>
{/* Filtres additionnels */} {/* Filtres additionnels - Affichés uniquement si données disponibles */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {(filteredCityData.length > 0 || filteredGenderData.length > 0) && (
<div> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="block text-sm text-gray-700 mb-1">Ville</label> {filteredCityData.length > 0 && (
<select <div>
value={filters.city} <label className="block text-sm text-gray-700 mb-1">Ville</label>
onChange={(e) => setFilters({ ...filters, city: e.target.value })} <select
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm" value={filters.city}
> onChange={(e) => setFilters({ ...filters, city: e.target.value })}
<option value="">Toutes les villes</option> className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
{stats.byCity.slice(0, 10).map((city) => ( >
<option key={city.city} value={city.city}> <option value="">Toutes les villes</option>
{city.city} ({city.count}) {filteredCityData.slice(0, 10).map((city) => (
</option> <option key={city.city} value={city.city}>
))} {city.city} ({city.count})
</select> </option>
</div> ))}
</select>
</div>
)}
<div> {filteredGenderData.length > 0 && (
<label className="block text-sm text-gray-700 mb-1">Genre</label> <div>
<select <label className="block text-sm text-gray-700 mb-1">Genre</label>
value={filters.gender} <select
onChange={(e) => setFilters({ ...filters, gender: e.target.value })} value={filters.gender}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm" 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 value="">Tous les genres</option>
<option key={g.gender} value={g.gender}> {filteredGenderData.map((g) => (
{g.gender} ({g.count}) <option key={g.gender} value={g.gender}>
</option> {g.gender} ({g.count})
))} </option>
</select> ))}
</select>
</div>
)}
</div> </div>
</div> )}
{/* Boutons d'action */} {/* Boutons d'action */}
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">