381 lines
16 KiB
TypeScript
381 lines
16 KiB
TypeScript
'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>
|
||
);
|
||
}
|