dans la fonction "envoyer des emails", ajoute une case à cocher pour n'envoyer qu'aux participants n"ayant pas voté (utilie pour les rappels aux retardataires)
This commit is contained in:
35
package-lock.json
generated
35
package-lock.json
generated
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "mes-budgets-participatifs",
|
"name": "mes-budgets-participatifs",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mes-budgets-participatifs",
|
"name": "mes-budgets-participatifs",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.7",
|
"@headlessui/react": "^2.2.7",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-progress": "^1.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": {
|
"node_modules/@radix-ui/react-collection": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@headlessui/react": "^2.2.7",
|
"@headlessui/react": "^2.2.7",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { Campaign, Participant } from '@/types';
|
import { Campaign, Participant, ParticipantWithVoteStatus } from '@/types';
|
||||||
import { campaignService, participantService, settingsService } from '@/lib/services';
|
import { campaignService, participantService, settingsService, voteService } from '@/lib/services';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@@ -11,6 +11,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { ArrowLeft, Mail, Send, CheckCircle, XCircle, Clock, Users } from 'lucide-react';
|
import { ArrowLeft, Mail, Send, CheckCircle, XCircle, Clock, Users } from 'lucide-react';
|
||||||
import AuthGuard from '@/components/AuthGuard';
|
import AuthGuard from '@/components/AuthGuard';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
@@ -29,13 +30,14 @@ function SendEmailsPageContent() {
|
|||||||
const campaignId = params.id as string;
|
const campaignId = params.id as string;
|
||||||
|
|
||||||
const [campaign, setCampaign] = useState<Campaign | null>(null);
|
const [campaign, setCampaign] = useState<Campaign | null>(null);
|
||||||
const [participants, setParticipants] = useState<Participant[]>([]);
|
const [participants, setParticipants] = useState<ParticipantWithVoteStatus[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [emailProgress, setEmailProgress] = useState<EmailProgress[]>([]);
|
const [emailProgress, setEmailProgress] = useState<EmailProgress[]>([]);
|
||||||
const [defaultSubject, setDefaultSubject] = useState('');
|
const [defaultSubject, setDefaultSubject] = useState('');
|
||||||
const [defaultMessage, setDefaultMessage] = useState('');
|
const [defaultMessage, setDefaultMessage] = useState('');
|
||||||
const [smtpConfigured, setSmtpConfigured] = useState(false);
|
const [smtpConfigured, setSmtpConfigured] = useState(false);
|
||||||
|
const [onlyNonVoters, setOnlyNonVoters] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
@@ -46,7 +48,7 @@ function SendEmailsPageContent() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [campaignData, participantsData, smtpSettings] = await Promise.all([
|
const [campaignData, participantsData, smtpSettings] = await Promise.all([
|
||||||
campaignService.getById(campaignId),
|
campaignService.getById(campaignId),
|
||||||
participantService.getByCampaign(campaignId),
|
voteService.getParticipantVoteStatus(campaignId),
|
||||||
settingsService.getSmtpSettings()
|
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 () => {
|
const handleSendAllEmails = async () => {
|
||||||
if (!campaign || !defaultSubject.trim() || !defaultMessage.trim()) {
|
if (!campaign || !defaultSubject.trim() || !defaultMessage.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSending(true);
|
setSending(true);
|
||||||
|
const filteredParticipants = getFilteredParticipants();
|
||||||
|
|
||||||
for (let i = 0; i < participants.length; i++) {
|
for (let i = 0; i < filteredParticipants.length; i++) {
|
||||||
const participant = participants[i];
|
const participant = filteredParticipants[i];
|
||||||
|
|
||||||
// Mettre à jour le statut à "sending"
|
// Mettre à jour le statut à "sending"
|
||||||
setEmailProgress(prev => prev.map(p =>
|
setEmailProgress(prev => prev.map(p =>
|
||||||
@@ -157,7 +168,7 @@ Cordialement,`);
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attendre 1 seconde avant l'email suivant
|
// Attendre 1 seconde avant l'email suivant
|
||||||
if (i < participants.length - 1) {
|
if (i < filteredParticipants.length - 1) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
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 sentCount = emailProgress.filter(p => p.status === 'sent').length;
|
||||||
const errorCount = emailProgress.filter(p => p.status === 'error').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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -308,7 +320,7 @@ Cordialement,`);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Statistiques */}
|
{/* Statistiques */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -321,6 +333,18 @@ Cordialement,`);
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Users className="h-8 w-8 text-blue-600 dark:text-blue-400 mr-3" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">Destinataires</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-600">{filteredParticipants.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -379,14 +403,25 @@ Cordialement,`);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="only-non-voters"
|
||||||
|
checked={onlyNonVoters}
|
||||||
|
onCheckedChange={(checked) => setOnlyNonVoters(checked as boolean)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="only-non-voters" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
|
N'envoyer qu'aux participants n'ayant pas encore voté
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSendAllEmails}
|
onClick={handleSendAllEmails}
|
||||||
disabled={sending || !defaultSubject.trim() || !defaultMessage.trim() || participants.length === 0}
|
disabled={sending || !defaultSubject.trim() || !defaultMessage.trim() || filteredParticipants.length === 0}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
{sending ? 'Envoi en cours...' : 'Envoyer à tous'}
|
{sending ? 'Envoi en cours...' : `Envoyer à ${filteredParticipants.length} participant${filteredParticipants.length > 1 ? 's' : ''}`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -402,7 +437,7 @@ Cordialement,`);
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
{sentCount} / {participants.length} emails envoyés
|
{sentCount} / {filteredParticipants.length} emails envoyés
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
{Math.round(progressPercentage)}%
|
{Math.round(progressPercentage)}%
|
||||||
@@ -424,27 +459,42 @@ Cordialement,`);
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{emailProgress.map((progress) => (
|
{emailProgress
|
||||||
<div key={progress.participant.id} className="flex items-center justify-between p-3 border border-slate-200 dark:border-slate-700 rounded-lg">
|
.filter(progress => {
|
||||||
<div className="flex items-center space-x-3">
|
const participant = participants.find(p => p.id === progress.participant.id);
|
||||||
{getStatusIcon(progress.status)}
|
return participant && (!onlyNonVoters || !participant.has_voted);
|
||||||
<div>
|
})
|
||||||
<p className="font-medium text-slate-900 dark:text-slate-100">
|
.map((progress) => {
|
||||||
{progress.participant.first_name} {progress.participant.last_name}
|
const participant = participants.find(p => p.id === progress.participant.id);
|
||||||
</p>
|
return (
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
<div key={progress.participant.id} className="flex items-center justify-between p-3 border border-slate-200 dark:border-slate-700 rounded-lg">
|
||||||
{progress.participant.email}
|
<div className="flex items-center space-x-3">
|
||||||
</p>
|
{getStatusIcon(progress.status)}
|
||||||
{progress.error && (
|
<div>
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">
|
<div className="flex items-center gap-2">
|
||||||
{progress.error}
|
<p className="font-medium text-slate-900 dark:text-slate-100">
|
||||||
</p>
|
{progress.participant.first_name} {progress.participant.last_name}
|
||||||
)}
|
</p>
|
||||||
|
{participant?.has_voted && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
A voté
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
{progress.participant.email}
|
||||||
|
</p>
|
||||||
|
{progress.error && (
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{progress.error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{getStatusBadge(progress.status)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
{getStatusBadge(progress.status)}
|
})}
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal file
@@ -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<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className={cn("flex items-center justify-center text-current")}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
Reference in New Issue
Block a user