Files
mes-budgets-participatifs/src/app/admin/campaigns/[id]/participants/page.tsx
2025-08-25 16:02:57 +02:00

381 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
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';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Input } from '@/components/ui/input';
import Navigation from '@/components/Navigation';
import AuthGuard from '@/components/AuthGuard';
import { Users, User, Calendar, Mail, Vote, Copy, Check } from 'lucide-react';
export const dynamic = 'force-dynamic';
function CampaignParticipantsPageContent() {
const params = useParams();
const campaignId = params.id as string;
const [campaign, setCampaign] = useState<Campaign | null>(null);
const [participants, setParticipants] = useState<ParticipantWithVoteStatus[]>([]);
const [loading, setLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedParticipant, setSelectedParticipant] = useState<Participant | null>(null);
const [copiedParticipantId, setCopiedParticipantId] = useState<string | null>(null);
useEffect(() => {
loadData();
}, [campaignId]);
const loadData = async () => {
try {
setLoading(true);
const [campaigns, participantsWithVoteStatus] = await Promise.all([
campaignService.getAll(),
voteService.getParticipantVoteStatus(campaignId)
]);
const campaignData = campaigns.find(c => c.id === campaignId);
setCampaign(campaignData || null);
setParticipants(participantsWithVoteStatus);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
} finally {
setLoading(false);
}
};
const handleParticipantAdded = () => {
setShowAddModal(false);
loadData();
};
const handleParticipantEdited = () => {
setShowEditModal(false);
loadData();
};
const handleParticipantDeleted = () => {
setShowDeleteModal(false);
loadData();
};
const getInitials = (firstName: string, lastName: string) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
};
const copyVoteLink = (participantId: string) => {
const voteUrl = `${window.location.origin}/campaigns/${campaignId}/vote/${participantId}`;
navigator.clipboard.writeText(voteUrl);
setCopiedParticipantId(participantId);
setTimeout(() => setCopiedParticipantId(null), 2000);
};
if (loading) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<div className="container mx-auto px-4 py-8">
<Navigation showBackButton backUrl="/admin" />
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900 dark:border-slate-100 mx-auto mb-4"></div>
<p className="text-slate-600 dark:text-slate-300">Chargement des participants...</p>
</div>
</div>
</div>
</div>
);
}
if (!campaign) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<div className="container mx-auto px-4 py-8">
<Navigation showBackButton backUrl="/admin" />
<Card className="border-dashed">
<CardContent className="p-12 text-center">
<div className="w-16 h-16 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-2xl"></span>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
Campagne introuvable
</h3>
<p className="text-slate-600 dark:text-slate-300 mb-6">
La campagne que vous recherchez n'existe pas ou a été supprimée.
</p>
<Button asChild>
<Link href="/admin">Retour à l'administration</Link>
</Button>
</CardContent>
</Card>
</div>
</div>
);
}
const votedCount = participants.filter(p => p.has_voted).length;
const totalBudget = participants.reduce((sum, p) => sum + (p.total_voted_amount || 0), 0);
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<div className="container mx-auto px-4 py-8">
<Navigation showBackButton backUrl="/admin" />
{/* Header */}
<div className="mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">
Participants
</h1>
<p className="text-slate-600 dark:text-slate-300 mt-2">
{campaign.title}
</p>
</div>
<Button onClick={() => setShowAddModal(true)} size="lg">
Nouveau participant
</Button>
</div>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Total Participants</p>
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{participants.length}</p>
</div>
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<Users className="w-4 h-4 text-blue-600 dark:text-blue-300" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Ont voté</p>
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{votedCount}</p>
</div>
<div className="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
<Vote className="w-4 h-4 text-green-600 dark:text-green-300" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Taux de participation</p>
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">
{participants.length > 0 ? Math.round((votedCount / participants.length) * 100) : 0}%
</p>
</div>
<div className="w-8 h-8 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
<span className="text-purple-600 dark:text-purple-300">📊</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Budget total voté</p>
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{totalBudget}</p>
</div>
<div className="w-8 h-8 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center">
<span className="text-yellow-600 dark:text-yellow-300">💰</span>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Participants List */}
{participants.length === 0 ? (
<Card className="border-dashed">
<CardContent className="p-12 text-center">
<div className="w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-4">
<Users className="w-8 h-8 text-slate-400" />
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
Aucun participant
</h3>
<p className="text-slate-600 dark:text-slate-300 mb-6">
Aucun participant n'a encore été ajouté à cette campagne.
</p>
<Button onClick={() => setShowAddModal(true)}>
Ajouter un participant
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-6">
{participants.map((participant) => (
<Card key={participant.id} className="hover:shadow-lg transition-shadow duration-200">
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<CardTitle className="text-xl">
{participant.first_name} {participant.last_name}
</CardTitle>
<Badge variant={participant.has_voted ? 'default' : 'secondary'}>
{participant.has_voted ? 'A voté' : 'N\'a pas voté'}
</Badge>
</div>
<CardDescription className="text-base">
{participant.email}
</CardDescription>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedParticipant(participant);
setShowEditModal(true);
}}
>
Modifier
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedParticipant(participant);
setShowDeleteModal(true);
}}
>
🗑 Supprimer
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4 mb-4">
<Avatar className="w-10 h-10">
<AvatarFallback className="bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">
{getInitials(participant.first_name, participant.last_name)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-4 text-sm text-slate-600 dark:text-slate-300">
<div className="flex items-center gap-1">
<Mail className="w-3 h-3" />
{participant.email}
</div>
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{new Date(participant.created_at).toLocaleDateString('fr-FR')}
</div>
{participant.has_voted && participant.total_voted_amount !== undefined && (
<div className="flex items-center gap-1">
<Vote className="w-3 h-3" />
{participant.total_voted_amount} votés
</div>
)}
</div>
</div>
</div>
{/* Vote Link for voting campaigns */}
{campaign.status === 'voting' && (
<Card className="bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">
Lien de vote personnel
</h4>
<div className="flex items-center space-x-2">
<Input
type="text"
readOnly
value={`${window.location.origin}/campaigns/${campaignId}/vote/${participant.id}`}
className="flex-1 text-xs bg-white dark:bg-slate-800 border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300 font-mono"
/>
<Button
variant="outline"
size="sm"
onClick={() => copyVoteLink(participant.id)}
className="text-xs"
>
{copiedParticipantId === participant.id ? (
<>
<Check className="w-3 h-3 mr-1" />
Copié !
</>
) : (
<>
<Copy className="w-3 h-3 mr-1" />
Copier
</>
)}
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
)}
</CardContent>
</Card>
))}
</div>
)}
{/* Modals */}
<AddParticipantModal
isOpen={showAddModal}
onClose={() => setShowAddModal(false)}
onSuccess={handleParticipantAdded}
campaignId={campaignId}
/>
{selectedParticipant && (
<EditParticipantModal
isOpen={showEditModal}
onClose={() => setShowEditModal(false)}
onSuccess={handleParticipantEdited}
participant={selectedParticipant}
/>
)}
{selectedParticipant && (
<DeleteParticipantModal
isOpen={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
onSuccess={handleParticipantDeleted}
participant={selectedParticipant}
/>
)}
</div>
</div>
);
}
export default function CampaignParticipantsPage() {
return (
<AuthGuard>
<CampaignParticipantsPageContent />
</AuthGuard>
);
}