From 25ccb432721af8c8b4e4fd08613ad1c598d9866d Mon Sep 17 00:00:00 2001 From: Yannick Le Duc Date: Sun, 21 Sep 2025 21:01:38 +0200 Subject: [PATCH] =?UTF-8?q?dans=20la=20fonction=20"envoyer=20des=20emails"?= =?UTF-8?q?,=20ajoute=20une=20case=20=C3=A0=20cocher=20pour=20n'envoyer=20?= =?UTF-8?q?qu'aux=20participants=20n"ayant=20pas=20vot=C3=A9=20(utilie=20p?= =?UTF-8?q?our=20les=20rappels=20aux=20retardataires)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 35 +++++- package.json | 1 + .../admin/campaigns/[id]/send-emails/page.tsx | 114 +++++++++++++----- src/components/ui/checkbox.tsx | 30 +++++ 4 files changed, 146 insertions(+), 34 deletions(-) create mode 100644 src/components/ui/checkbox.tsx diff --git a/package-lock.json b/package-lock.json index afe58b8..fbe86b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "mes-budgets-participatifs", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mes-budgets-participatifs", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@headlessui/react": "^2.2.7", "@heroicons/react": "^2.2.0", "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-progress": "^1.1.7", @@ -3004,6 +3005,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", diff --git a/package.json b/package.json index 08d7492..ac1ff51 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@headlessui/react": "^2.2.7", "@heroicons/react": "^2.2.0", "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-progress": "^1.1.7", diff --git a/src/app/admin/campaigns/[id]/send-emails/page.tsx b/src/app/admin/campaigns/[id]/send-emails/page.tsx index 3b76d95..30e8a58 100644 --- a/src/app/admin/campaigns/[id]/send-emails/page.tsx +++ b/src/app/admin/campaigns/[id]/send-emails/page.tsx @@ -2,8 +2,8 @@ import { useState, useEffect } from 'react'; import { useParams, useRouter } from 'next/navigation'; -import { Campaign, Participant } from '@/types'; -import { campaignService, participantService, settingsService } from '@/lib/services'; +import { Campaign, Participant, ParticipantWithVoteStatus } from '@/types'; +import { campaignService, participantService, settingsService, voteService } from '@/lib/services'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Textarea } from '@/components/ui/textarea'; @@ -11,6 +11,7 @@ import { Label } from '@/components/ui/label'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Progress } from '@/components/ui/progress'; import { Badge } from '@/components/ui/badge'; +import { Checkbox } from '@/components/ui/checkbox'; import { ArrowLeft, Mail, Send, CheckCircle, XCircle, Clock, Users } from 'lucide-react'; import AuthGuard from '@/components/AuthGuard'; import Footer from '@/components/Footer'; @@ -29,13 +30,14 @@ function SendEmailsPageContent() { 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 [sending, setSending] = useState(false); const [emailProgress, setEmailProgress] = useState([]); const [defaultSubject, setDefaultSubject] = useState(''); const [defaultMessage, setDefaultMessage] = useState(''); const [smtpConfigured, setSmtpConfigured] = useState(false); + const [onlyNonVoters, setOnlyNonVoters] = useState(true); useEffect(() => { loadData(); @@ -46,7 +48,7 @@ function SendEmailsPageContent() { setLoading(true); const [campaignData, participantsData, smtpSettings] = await Promise.all([ campaignService.getById(campaignId), - participantService.getByCampaign(campaignId), + voteService.getParticipantVoteStatus(campaignId), settingsService.getSmtpSettings() ]); @@ -85,15 +87,24 @@ Cordialement,`); } }; + // Fonction pour obtenir les participants filtrés selon l'option sélectionnée + const getFilteredParticipants = () => { + if (onlyNonVoters) { + return participants.filter(participant => !participant.has_voted); + } + return participants; + }; + const handleSendAllEmails = async () => { if (!campaign || !defaultSubject.trim() || !defaultMessage.trim()) { return; } setSending(true); + const filteredParticipants = getFilteredParticipants(); - for (let i = 0; i < participants.length; i++) { - const participant = participants[i]; + for (let i = 0; i < filteredParticipants.length; i++) { + const participant = filteredParticipants[i]; // Mettre à jour le statut à "sending" setEmailProgress(prev => prev.map(p => @@ -157,7 +168,7 @@ Cordialement,`); } // Attendre 1 seconde avant l'email suivant - if (i < participants.length - 1) { + if (i < filteredParticipants.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } @@ -191,9 +202,10 @@ Cordialement,`); } }; + const filteredParticipants = getFilteredParticipants(); const sentCount = emailProgress.filter(p => p.status === 'sent').length; const errorCount = emailProgress.filter(p => p.status === 'error').length; - const progressPercentage = participants.length > 0 ? (sentCount / participants.length) * 100 : 0; + const progressPercentage = filteredParticipants.length > 0 ? (sentCount / filteredParticipants.length) * 100 : 0; if (loading) { return ( @@ -308,7 +320,7 @@ Cordialement,`); {/* Statistiques */} -
+
@@ -321,6 +333,18 @@ Cordialement,`); + + +
+ +
+

Destinataires

+

{filteredParticipants.length}

+
+
+
+
+
@@ -379,14 +403,25 @@ Cordialement,`); />
+
+ setOnlyNonVoters(checked as boolean)} + /> + +
+
@@ -402,7 +437,7 @@ Cordialement,`);
- {sentCount} / {participants.length} emails envoyés + {sentCount} / {filteredParticipants.length} emails envoyés {Math.round(progressPercentage)}% @@ -424,27 +459,42 @@ Cordialement,`);
- {emailProgress.map((progress) => ( -
-
- {getStatusIcon(progress.status)} -
-

- {progress.participant.first_name} {progress.participant.last_name} -

-

- {progress.participant.email} -

- {progress.error && ( -

- {progress.error} -

- )} + {emailProgress + .filter(progress => { + const participant = participants.find(p => p.id === progress.participant.id); + return participant && (!onlyNonVoters || !participant.has_voted); + }) + .map((progress) => { + const participant = participants.find(p => p.id === progress.participant.id); + return ( +
+
+ {getStatusIcon(progress.status)} +
+
+

+ {progress.participant.first_name} {progress.participant.last_name} +

+ {participant?.has_voted && ( + + A voté + + )} +
+

+ {progress.participant.email} +

+ {progress.error && ( +

+ {progress.error} +

+ )} +
+
+ {getStatusBadge(progress.status)}
-
- {getStatusBadge(progress.status)} -
- ))} + ); + })}
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..df61a13 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox }