From caed358661f1bc7697a6df33ccb016c186e1682b Mon Sep 17 00:00:00 2001 From: Yannick Le Duc Date: Mon, 25 Aug 2025 17:29:35 +0200 Subject: [PATCH] Ajout page statistiques --- src/app/admin/campaigns/[id]/stats/page.tsx | 338 ++++++++++++++++++++ src/app/admin/page.tsx | 10 +- src/lib/services.ts | 10 + 3 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 src/app/admin/campaigns/[id]/stats/page.tsx diff --git a/src/app/admin/campaigns/[id]/stats/page.tsx b/src/app/admin/campaigns/[id]/stats/page.tsx new file mode 100644 index 0000000..c1b3d43 --- /dev/null +++ b/src/app/admin/campaigns/[id]/stats/page.tsx @@ -0,0 +1,338 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useParams } from 'next/navigation'; +import Link from 'next/link'; +import { Campaign, Proposition, Participant, Vote } from '@/types'; +import { campaignService, propositionService, participantService, voteService } from '@/lib/services'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Progress } from '@/components/ui/progress'; +import Navigation from '@/components/Navigation'; +import AuthGuard from '@/components/AuthGuard'; +import { + BarChart3, + Users, + Vote as VoteIcon, + TrendingUp, + Target, + Award, + FileText, + Calendar, + ArrowLeft +} from 'lucide-react'; + +export const dynamic = 'force-dynamic'; + +interface PropositionStats { + proposition: Proposition; + voteCount: number; + averageAmount: number; + minAmount: number; + maxAmount: number; +} + +function CampaignStatsPageContent() { + const params = useParams(); + const campaignId = params.id as string; + + const [campaign, setCampaign] = useState(null); + const [participants, setParticipants] = useState([]); + const [propositions, setPropositions] = useState([]); + const [votes, setVotes] = useState([]); + const [loading, setLoading] = useState(true); + const [propositionStats, setPropositionStats] = useState([]); + + useEffect(() => { + if (campaignId) { + loadData(); + } + }, [campaignId]); + + const loadData = async () => { + try { + setLoading(true); + const [campaigns, participantsData, propositionsData, votesData] = await Promise.all([ + campaignService.getAll(), + participantService.getByCampaign(campaignId), + propositionService.getByCampaign(campaignId), + voteService.getByCampaign(campaignId) + ]); + + const campaignData = campaigns.find(c => c.id === campaignId); + if (!campaignData) { + throw new Error('Campagne non trouvée'); + } + + setCampaign(campaignData); + setParticipants(participantsData); + setPropositions(propositionsData); + setVotes(votesData); + + // Calculer les statistiques des propositions + const stats = propositionsData.map(proposition => { + const propositionVotes = votesData.filter(vote => vote.proposition_id === proposition.id && vote.amount > 0); + const amounts = propositionVotes.map(vote => vote.amount); + + return { + proposition, + voteCount: propositionVotes.length, + averageAmount: amounts.length > 0 ? Math.round(amounts.reduce((sum, amount) => sum + amount, 0) / amounts.length) : 0, + minAmount: amounts.length > 0 ? Math.min(...amounts) : 0, + maxAmount: amounts.length > 0 ? Math.max(...amounts) : 0 + }; + }); + + setPropositionStats(stats); + } catch (error) { + console.error('Erreur lors du chargement des données:', error); + } finally { + setLoading(false); + } + }; + + const getParticipationRate = () => { + if (participants.length === 0) return 0; + const votedParticipants = participants.filter(p => { + const participantVotes = votes.filter(v => v.participant_id === p.id); + return participantVotes.some(v => v.amount > 0); + }); + return Math.round((votedParticipants.length / participants.length) * 100); + }; + + + + const getAverageVotesPerProposition = () => { + if (propositions.length === 0) return 0; + const totalVotes = votes.filter(v => v.amount > 0).length; + return Math.round(totalVotes / propositions.length); + }; + + if (loading) { + return ( +
+
+ +
+
+
+

Chargement des statistiques...

+
+
+
+
+ ); + } + + if (!campaign) { + return ( +
+
+ + + +
+ +
+

+ Campagne introuvable +

+

+ La campagne que vous recherchez n'existe pas ou a été supprimée. +

+ +
+
+
+
+ ); + } + + const participationRate = getParticipationRate(); + const averageVotesPerProposition = getAverageVotesPerProposition(); + + return ( +
+
+ + + {/* Header */} +
+
+ + + {campaign.status === 'voting' ? 'En cours de vote' : 'Terminée'} + +
+ +
+
+

+ + Statistiques +

+

+ {campaign.title} +

+

+ {campaign.description} +

+
+
+
+ + {/* Overview Stats */} +
+ + +
+
+

Taux de participation

+

{participationRate}%

+
+
+ +
+
+ +

+ {participants.filter(p => votes.some(v => v.participant_id === p.id && v.amount > 0)).length} / {participants.length} participants +

+
+
+ + + +
+
+

Propositions

+

{propositions.length}

+
+
+ +
+
+

+ {averageVotesPerProposition} votes moy. par proposition +

+
+
+
+ + {/* Propositions Stats */} + + + + + Préférences par proposition + + + Statistiques des montants exprimés par les participants pour chaque proposition + + + + {propositionStats.length === 0 ? ( +
+ +

+ Aucune proposition +

+

+ Aucune proposition n'a été soumise pour cette campagne. +

+
+ ) : ( +
+ {propositionStats + .sort((a, b) => b.averageAmount - a.averageAmount) // Trier par moyenne décroissante + .map((stat, index) => ( +
+
+
+
+ + #{index + 1} + +

+ {stat.proposition.title} +

+
+

+ {stat.proposition.description} +

+
+ {index === 0 && stat.averageAmount > 0 && ( + + + Préférée + + )} +
+ +
+
+

+ {stat.voteCount} +

+

+ {stat.voteCount === 1 ? 'Votant' : 'Votants'} +

+
+
+

+ {stat.averageAmount}€ +

+

Moyenne

+
+
+

+ {stat.minAmount}€ +

+

Minimum

+
+
+

+ {stat.maxAmount}€ +

+

Maximum

+
+
+ + {stat.voteCount > 0 && ( +
+
+ Répartition des préférences + {stat.voteCount} {stat.voteCount === 1 ? 'votant' : 'votants'} +
+ +
+ )} +
+ ))} +
+ )} +
+
+
+
+ ); +} + +export default function CampaignStatsPage() { + return ( + + + + ); +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 26176a0..2b8c555 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -13,7 +13,7 @@ import { Input } from '@/components/ui/input'; import { Progress } from '@/components/ui/progress'; import Navigation from '@/components/Navigation'; import AuthGuard from '@/components/AuthGuard'; -import { FolderOpen, Users, FileText, CheckCircle, Clock, Plus } from 'lucide-react'; +import { FolderOpen, Users, FileText, CheckCircle, Clock, Plus, BarChart3 } from 'lucide-react'; export const dynamic = 'force-dynamic'; @@ -323,6 +323,14 @@ function AdminPageContent() { Votants ({campaign.stats.participants}) + {(campaign.status === 'voting' || campaign.status === 'closed') && ( + + )} diff --git a/src/lib/services.ts b/src/lib/services.ts index 62e8ae6..607835b 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -242,6 +242,16 @@ export const voteService = { if (error) throw error; }, + async getByCampaign(campaignId: string): Promise { + const { data, error } = await supabase + .from('votes') + .select('*') + .eq('campaign_id', campaignId); + + if (error) throw error; + return data || []; + }, + async getParticipantVoteStatus(campaignId: string): Promise { const { data: participants, error: participantsError } = await supabase .from('participants')