Evite les doublons dans les emails lors d'import de participants

Set version to 0.2.0 (et affiche le en footer)
This commit is contained in:
Yannick Le Duc
2025-09-16 16:02:49 +02:00
parent 2a2738f5c0
commit b20c88b05d
8 changed files with 73 additions and 11 deletions

View File

@@ -218,9 +218,11 @@ DECLARE
counter INTEGER := 0; counter INTEGER := 0;
max_attempts INTEGER := 10; max_attempts INTEGER := 10;
BEGIN BEGIN
-- Convertir le titre en slug (minuscules, remplacer espaces par tirets, supprimer caractères spéciaux) -- Convertir le titre en slug (minuscules, supprimer accents, remplacer espaces par tirets, supprimer caractères spéciaux)
base_slug := lower(regexp_replace(title, '[^a-zA-Z0-9\s]', '', 'g')); base_slug := lower(unaccent(title));
base_slug := regexp_replace(base_slug, '[^a-z0-9\s-]', '', 'g');
base_slug := regexp_replace(base_slug, '\s+', '-', 'g'); base_slug := regexp_replace(base_slug, '\s+', '-', 'g');
base_slug := regexp_replace(base_slug, '-+', '-', 'g');
base_slug := trim(both '-' from base_slug); base_slug := trim(both '-' from base_slug);
-- Si le slug est vide, utiliser un slug par défaut -- Si le slug est vide, utiliser un slug par défaut

View File

@@ -1,6 +1,6 @@
{ {
"name": "mes-budgets-participatifs", "name": "mes-budgets-participatifs",
"version": "0.1.0", "version": "0.2.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",

View File

@@ -87,6 +87,10 @@ function CampaignParticipantsPageContent() {
const handleImportParticipants = async (data: any[]) => { const handleImportParticipants = async (data: any[]) => {
try { try {
// Récupérer les participants existants pour vérifier les emails
const existingParticipants = await participantService.getByCampaign(campaignId);
const existingEmails = new Set(existingParticipants.map(p => p.email.toLowerCase()));
const participantsToCreate = data.map(row => ({ const participantsToCreate = data.map(row => ({
campaign_id: campaignId, campaign_id: campaignId,
first_name: row.Prénom || '', first_name: row.Prénom || '',
@@ -94,11 +98,24 @@ function CampaignParticipantsPageContent() {
email: row.Email || '' email: row.Email || ''
})); }));
// Créer les participants un par un // Filtrer les participants pour éviter les doublons d'email
for (const participant of participantsToCreate) { const newParticipants = participantsToCreate.filter(participant => {
const email = participant.email.toLowerCase();
return email && !existingEmails.has(email);
});
const skippedCount = participantsToCreate.length - newParticipants.length;
// Créer les nouveaux participants un par un
for (const participant of newParticipants) {
await participantService.create(participant); await participantService.create(participant);
} }
// Afficher un message informatif si des participants ont été ignorés
if (skippedCount > 0) {
alert(`${skippedCount} participant(s) ignoré(s) car leur email existe déjà dans la campagne.`);
}
loadData(); loadData();
} catch (error) { } catch (error) {
console.error('Erreur lors de l\'import des participants:', error); console.error('Erreur lors de l\'import des participants:', error);

View File

@@ -15,6 +15,7 @@ import { Badge } from '@/components/ui/badge';
import AuthGuard from '@/components/AuthGuard'; import AuthGuard from '@/components/AuthGuard';
import Footer from '@/components/Footer'; import Footer from '@/components/Footer';
import VersionDisplay from '@/components/VersionDisplay';
import { FolderOpen, Users, FileText, Plus, BarChart3, Settings, Check, Copy, Mail, Share2 } from 'lucide-react'; import { FolderOpen, Users, FileText, Plus, BarChart3, Settings, Check, Copy, Mail, Share2 } from 'lucide-react';
import StatusSwitch from '@/components/StatusSwitch'; import StatusSwitch from '@/components/StatusSwitch';
import { MarkdownContent } from '@/components/MarkdownContent'; import { MarkdownContent } from '@/components/MarkdownContent';
@@ -499,6 +500,9 @@ function AdminPageContent() {
{/* Footer */} {/* Footer */}
<Footer /> <Footer />
{/* Version Display */}
<VersionDisplay />
</div> </div>
</div> </div>
); );

View File

@@ -8,6 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { PROJECT_CONFIG } from '@/lib/project.config'; import { PROJECT_CONFIG } from '@/lib/project.config';
import Footer from '@/components/Footer'; import Footer from '@/components/Footer';
import VersionDisplay from '@/components/VersionDisplay';
export default function HomePage() { export default function HomePage() {
const router = useRouter(); const router = useRouter();
@@ -196,6 +197,9 @@ export default function HomePage() {
{/* Footer */} {/* Footer */}
<Footer variant="home" /> <Footer variant="home" />
{/* Version Display */}
<VersionDisplay />
</div> </div>
</div> </div>
); );

View File

@@ -225,9 +225,11 @@ DECLARE
counter INTEGER := 0; counter INTEGER := 0;
max_attempts INTEGER := 10; max_attempts INTEGER := 10;
BEGIN BEGIN
-- Convertir le titre en slug (minuscules, remplacer espaces par tirets, supprimer caractères spéciaux) -- Convertir le titre en slug (minuscules, supprimer accents, remplacer espaces par tirets, supprimer caractères spéciaux)
base_slug := lower(regexp_replace(title, '[^a-zA-Z0-9\s]', '', 'g')); base_slug := lower(unaccent(title));
base_slug := regexp_replace(base_slug, '[^a-z0-9\s-]', '', 'g');
base_slug := regexp_replace(base_slug, '\s+', '-', 'g'); base_slug := regexp_replace(base_slug, '\s+', '-', 'g');
base_slug := regexp_replace(base_slug, '-+', '-', 'g');
base_slug := trim(both '-' from base_slug); base_slug := trim(both '-' from base_slug);
-- Si le slug est vide, utiliser un slug par défaut -- Si le slug est vide, utiliser un slug par défaut

View File

@@ -0,0 +1,28 @@
'use client';
import { useEffect, useState } from 'react';
export default function VersionDisplay() {
const [version, setVersion] = useState<string>('');
useEffect(() => {
// Récupérer la version depuis package.json
fetch('/package.json')
.then(response => response.json())
.then(data => setVersion(data.version))
.catch(() => {
// Fallback si le fichier n'est pas accessible
setVersion('0.2.0');
});
}, []);
if (!version) return null;
return (
<div className="text-center py-2">
<span className="text-xs text-slate-400 dark:text-slate-500">
v{version}
</span>
</div>
);
}

View File

@@ -5,10 +5,15 @@ import { emailService } from './email';
// Fonction utilitaire pour générer un slug côté client // Fonction utilitaire pour générer un slug côté client
function generateSlugClient(title: string): string { function generateSlugClient(title: string): string {
// Convertir en minuscules et remplacer les caractères spéciaux // Convertir en minuscules, supprimer les accents et remplacer les caractères spéciaux
let slug = title.toLowerCase() let slug = title
.replace(/[^a-z0-9\s]/g, '') .toLowerCase()
.replace(/\s+/g, '-') .normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // Supprime les accents
.replace(/[^a-z0-9\s-]/g, '') // Garde seulement lettres, chiffres, espaces et tirets
.replace(/\s+/g, '-') // Remplace les espaces par des tirets
.replace(/-+/g, '-') // Remplace les tirets multiples par un seul
.replace(/^-+|-+$/g, '') // Supprime les tirets en début et fin
.trim(); .trim();
// Si le slug est vide, utiliser 'campagne' // Si le slug est vide, utiliser 'campagne'