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:
parent
51ec802131
commit
f2d4bb3c5f
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user