diff --git a/src/app/admin/campaigns/[id]/participants/page.tsx b/src/app/admin/campaigns/[id]/participants/page.tsx
index aea6958..47021e9 100644
--- a/src/app/admin/campaigns/[id]/participants/page.tsx
+++ b/src/app/admin/campaigns/[id]/participants/page.tsx
@@ -3,8 +3,8 @@
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
-import { Campaign, Participant } from '@/types';
-import { campaignService, participantService } from '@/lib/services';
+import { Campaign, Participant, ParticipantWithVoteStatus } from '@/types';
+import { campaignService, participantService, voteService } from '@/lib/services';
import AddParticipantModal from '@/components/AddParticipantModal';
import EditParticipantModal from '@/components/EditParticipantModal';
import DeleteParticipantModal from '@/components/DeleteParticipantModal';
@@ -17,12 +17,13 @@ export default function CampaignParticipantsPage() {
const campaignId = params.id as string;
const [campaign, setCampaign] = useState(null);
- const [participants, setParticipants] = useState([]);
+ const [participants, setParticipants] = useState([]);
const [loading, setLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedParticipant, setSelectedParticipant] = useState(null);
+ const [copiedParticipantId, setCopiedParticipantId] = useState(null);
useEffect(() => {
if (campaignId) {
@@ -33,13 +34,13 @@ export default function CampaignParticipantsPage() {
const loadData = async () => {
try {
setLoading(true);
- const [campaignData, participantsData] = await Promise.all([
+ const [campaigns, participantsWithVoteStatus] = await Promise.all([
campaignService.getAll().then(campaigns => campaigns.find(c => c.id === campaignId)),
- participantService.getByCampaign(campaignId)
+ voteService.getParticipantVoteStatus(campaignId)
]);
- setCampaign(campaignData || null);
- setParticipants(participantsData);
+ setCampaign(campaigns || null);
+ setParticipants(participantsWithVoteStatus);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
} finally {
@@ -181,9 +182,65 @@ export default function CampaignParticipantsPage() {
-
- Inscrit le {new Date(participant.created_at).toLocaleDateString('fr-FR')}
-
+
+
+ Inscrit le : {new Date(participant.created_at).toLocaleDateString('fr-FR')}
+
+
+ {participant.has_voted ? 'A voté' : 'N\'a pas voté'}
+
+ {participant.has_voted && participant.total_voted_amount && (
+
+ Total voté : {participant.total_voted_amount}€
+
+ )}
+
+
+ {campaign?.status === 'voting' && (
+
+
+
+
Lien de vote personnel
+
+
+
+
+
+
+
+ )}
@@ -115,7 +115,7 @@ export default function EditParticipantModal({
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
- placeholder="Nom du participant"
+ placeholder="Nom"
/>
diff --git a/src/lib/services.ts b/src/lib/services.ts
index aedf008..62e8ae6 100644
--- a/src/lib/services.ts
+++ b/src/lib/services.ts
@@ -1,5 +1,5 @@
import { supabase } from './supabase';
-import { Campaign, Proposition, Participant } from '@/types';
+import { Campaign, Proposition, Participant, Vote, ParticipantWithVoteStatus } from '@/types';
// Services pour les campagnes
export const campaignService = {
@@ -173,3 +173,99 @@ export const participantService = {
if (error) throw error;
}
};
+
+// Services pour les votes
+export const voteService = {
+ async getByParticipant(campaignId: string, participantId: string): Promise {
+ const { data, error } = await supabase
+ .from('votes')
+ .select('*')
+ .eq('campaign_id', campaignId)
+ .eq('participant_id', participantId);
+
+ if (error) throw error;
+ return data || [];
+ },
+
+ async getByProposition(propositionId: string): Promise {
+ const { data, error } = await supabase
+ .from('votes')
+ .select('*')
+ .eq('proposition_id', propositionId);
+
+ if (error) throw error;
+ return data || [];
+ },
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ async create(vote: any): Promise {
+ const { data, error } = await supabase
+ .from('votes')
+ .insert(vote)
+ .select()
+ .single();
+
+ if (error) throw error;
+ return data;
+ },
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ async update(id: string, updates: any): Promise {
+ const { data, error } = await supabase
+ .from('votes')
+ .update(updates)
+ .eq('id', id)
+ .select()
+ .single();
+
+ if (error) throw error;
+ return data;
+ },
+
+ async upsert(vote: { campaign_id: string; participant_id: string; proposition_id: string; amount: number }): Promise {
+ const { data, error } = await supabase
+ .from('votes')
+ .upsert(vote, { onConflict: 'participant_id,proposition_id' })
+ .select()
+ .single();
+
+ if (error) throw error;
+ return data;
+ },
+
+ async delete(id: string): Promise {
+ const { error } = await supabase
+ .from('votes')
+ .delete()
+ .eq('id', id);
+
+ if (error) throw error;
+ },
+
+ async getParticipantVoteStatus(campaignId: string): Promise {
+ const { data: participants, error: participantsError } = await supabase
+ .from('participants')
+ .select('*')
+ .eq('campaign_id', campaignId);
+
+ if (participantsError) throw participantsError;
+
+ const { data: votes, error: votesError } = await supabase
+ .from('votes')
+ .select('*')
+ .eq('campaign_id', campaignId);
+
+ if (votesError) throw votesError;
+
+ return participants.map(participant => {
+ const participantVotes = votes.filter(vote => vote.participant_id === participant.id);
+ const totalVotedAmount = participantVotes.reduce((sum, vote) => sum + vote.amount, 0);
+
+ return {
+ ...participant,
+ has_voted: participantVotes.length > 0,
+ total_voted_amount: totalVotedAmount
+ };
+ });
+ }
+};
diff --git a/src/types/index.ts b/src/types/index.ts
index 194280f..1c46826 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -38,6 +38,25 @@ export interface Participant {
created_at: string;
}
+export interface Vote {
+ id: string;
+ campaign_id: string;
+ participant_id: string;
+ proposition_id: string;
+ amount: number;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface PropositionWithVote extends Proposition {
+ vote?: Vote;
+}
+
+export interface ParticipantWithVoteStatus extends Participant {
+ has_voted: boolean;
+ total_voted_amount?: number;
+}
+
export interface Database {
public: {
Tables: {
diff --git a/supabase-schema.sql b/supabase-schema.sql
index 4632aa3..abecc41 100644
--- a/supabase-schema.sql
+++ b/supabase-schema.sql
@@ -34,12 +34,28 @@ CREATE TABLE participants (
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
+-- Table des votes
+CREATE TABLE votes (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ campaign_id UUID NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
+ participant_id UUID NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
+ proposition_id UUID NOT NULL REFERENCES propositions(id) ON DELETE CASCADE,
+ amount INTEGER NOT NULL CHECK (amount > 0),
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ UNIQUE(participant_id, proposition_id) -- Un seul vote par participant par proposition
+);
+
-- Index pour améliorer les performances
CREATE INDEX idx_propositions_campaign_id ON propositions(campaign_id);
CREATE INDEX idx_participants_campaign_id ON participants(campaign_id);
CREATE INDEX idx_campaigns_status ON campaigns(status);
CREATE INDEX idx_campaigns_created_at ON campaigns(created_at DESC);
+-- Index pour optimiser les requêtes
+CREATE INDEX idx_votes_campaign_participant ON votes(campaign_id, participant_id);
+CREATE INDEX idx_votes_proposition ON votes(proposition_id);
+
-- Trigger pour mettre à jour updated_at automatiquement
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
@@ -54,10 +70,14 @@ CREATE TRIGGER update_campaigns_updated_at
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
+CREATE TRIGGER update_votes_updated_at BEFORE UPDATE ON votes
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
-- Politique RLS (Row Level Security) - Activer pour toutes les tables
ALTER TABLE campaigns ENABLE ROW LEVEL SECURITY;
ALTER TABLE propositions ENABLE ROW LEVEL SECURITY;
ALTER TABLE participants ENABLE ROW LEVEL SECURITY;
+ALTER TABLE votes ENABLE ROW LEVEL SECURITY;
-- Politiques pour permettre l'accès public (à adapter selon vos besoins d'authentification)
CREATE POLICY "Allow public read access to campaigns" ON campaigns FOR SELECT USING (true);
@@ -67,9 +87,16 @@ CREATE POLICY "Allow public delete access to campaigns" ON campaigns FOR DELETE
CREATE POLICY "Allow public read access to propositions" ON propositions FOR SELECT USING (true);
CREATE POLICY "Allow public insert access to propositions" ON propositions FOR INSERT WITH CHECK (true);
+CREATE POLICY "Allow public delete access to propositions" ON propositions FOR DELETE USING (true);
CREATE POLICY "Allow public read access to participants" ON participants FOR SELECT USING (true);
CREATE POLICY "Allow public insert access to participants" ON participants FOR INSERT WITH CHECK (true);
+CREATE POLICY "Allow public delete access to participants" ON participants FOR DELETE USING (true);
+
+CREATE POLICY "Allow public read access to votes" ON votes FOR SELECT USING (true);
+CREATE POLICY "Allow public insert access to votes" ON votes FOR INSERT WITH CHECK (true);
+CREATE POLICY "Allow public update access to votes" ON votes FOR UPDATE USING (true);
+CREATE POLICY "Allow public delete access to votes" ON votes FOR DELETE USING (true);
-- Données d'exemple (optionnel)
INSERT INTO campaigns (title, description, status, budget_per_user, spending_tiers) VALUES