diff --git a/src/app/admin/campaigns/[id]/participants/page.tsx b/src/app/admin/campaigns/[id]/participants/page.tsx index 5fc52fc..5304256 100644 --- a/src/app/admin/campaigns/[id]/participants/page.tsx +++ b/src/app/admin/campaigns/[id]/participants/page.tsx @@ -7,6 +7,7 @@ import { campaignService, participantService, voteService } from '@/lib/services import AddParticipantModal from '@/components/AddParticipantModal'; import EditParticipantModal from '@/components/EditParticipantModal'; import DeleteParticipantModal from '@/components/DeleteParticipantModal'; +import ImportCSVModal from '@/components/ImportCSVModal'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -14,7 +15,7 @@ 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'; +import { Users, User, Calendar, Mail, Vote, Copy, Check, Upload } from 'lucide-react'; export const dynamic = 'force-dynamic'; @@ -27,6 +28,7 @@ function CampaignParticipantsPageContent() { const [showAddModal, setShowAddModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showImportModal, setShowImportModal] = useState(false); const [selectedParticipant, setSelectedParticipant] = useState(null); const [copiedParticipantId, setCopiedParticipantId] = useState(null); @@ -67,6 +69,26 @@ function CampaignParticipantsPageContent() { loadData(); }; + const handleImportParticipants = async (data: any[]) => { + try { + const participantsToCreate = data.map(row => ({ + campaign_id: campaignId, + first_name: row.first_name || '', + last_name: row.last_name || '', + email: row.email || '' + })); + + // Créer les participants un par un + for (const participant of participantsToCreate) { + await participantService.create(participant); + } + + loadData(); + } catch (error) { + console.error('Erreur lors de l\'import des participants:', error); + } + }; + const getInitials = (firstName: string, lastName: string) => { return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); }; @@ -139,9 +161,15 @@ function CampaignParticipantsPageContent() { {campaign.title}

- +
+ + +
@@ -366,6 +394,14 @@ function CampaignParticipantsPageContent() { participant={selectedParticipant} /> )} + + setShowImportModal(false)} + onImport={handleImportParticipants} + type="participants" + campaignTitle={campaign?.title} + /> ); diff --git a/src/app/admin/campaigns/[id]/propositions/page.tsx b/src/app/admin/campaigns/[id]/propositions/page.tsx index 37bd16d..dfcf09a 100644 --- a/src/app/admin/campaigns/[id]/propositions/page.tsx +++ b/src/app/admin/campaigns/[id]/propositions/page.tsx @@ -7,13 +7,14 @@ import { campaignService, propositionService } from '@/lib/services'; import AddPropositionModal from '@/components/AddPropositionModal'; import EditPropositionModal from '@/components/EditPropositionModal'; import DeletePropositionModal from '@/components/DeletePropositionModal'; +import ImportCSVModal from '@/components/ImportCSVModal'; 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 Navigation from '@/components/Navigation'; import AuthGuard from '@/components/AuthGuard'; -import { FileText, User, Calendar, Mail } from 'lucide-react'; +import { FileText, User, Calendar, Mail, Upload } from 'lucide-react'; export const dynamic = 'force-dynamic'; @@ -26,6 +27,7 @@ function CampaignPropositionsPageContent() { const [showAddModal, setShowAddModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showImportModal, setShowImportModal] = useState(false); const [selectedProposition, setSelectedProposition] = useState(null); useEffect(() => { @@ -65,6 +67,28 @@ function CampaignPropositionsPageContent() { loadData(); }; + const handleImportPropositions = async (data: any[]) => { + try { + const propositionsToCreate = data.map(row => ({ + campaign_id: campaignId, + title: row.title || '', + description: row.description || '', + author_first_name: row.author_first_name || 'admin', + author_last_name: row.author_last_name || 'admin', + author_email: row.author_email || 'admin@example.com' + })); + + // Créer les propositions une par une + for (const proposition of propositionsToCreate) { + await propositionService.create(proposition); + } + + loadData(); + } catch (error) { + console.error('Erreur lors de l\'import des propositions:', error); + } + }; + const getInitials = (firstName: string, lastName: string) => { return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); }; @@ -127,9 +151,15 @@ function CampaignPropositionsPageContent() { {campaign.title}

- +
+ + +
@@ -292,6 +322,14 @@ function CampaignPropositionsPageContent() { proposition={selectedProposition} /> )} + + setShowImportModal(false)} + onImport={handleImportPropositions} + type="propositions" + campaignTitle={campaign?.title} + /> ); diff --git a/src/components/ImportCSVModal.tsx b/src/components/ImportCSVModal.tsx new file mode 100644 index 0000000..fd0e3ba --- /dev/null +++ b/src/components/ImportCSVModal.tsx @@ -0,0 +1,239 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Upload, FileText, Download, AlertCircle } from 'lucide-react'; + +interface ImportCSVModalProps { + isOpen: boolean; + onClose: () => void; + onImport: (data: any[]) => void; + type: 'propositions' | 'participants'; + campaignTitle?: string; +} + +export default function ImportCSVModal({ + isOpen, + onClose, + onImport, + type, + campaignTitle +}: ImportCSVModalProps) { + const [file, setFile] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [preview, setPreview] = useState([]); + + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + if (selectedFile.type !== 'text/csv' && !selectedFile.name.endsWith('.csv')) { + setError('Veuillez sélectionner un fichier CSV valide.'); + return; + } + setFile(selectedFile); + setError(''); + parseCSV(selectedFile); + } + }; + + const parseCSV = (file: File) => { + const reader = new FileReader(); + reader.onload = (e) => { + const text = e.target?.result as string; + const lines = text.split('\n').filter(line => line.trim()); + + if (lines.length < 2) { + setError('Le fichier CSV doit contenir au moins un en-tête et une ligne de données.'); + return; + } + + const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, '')); + const data = lines.slice(1).map(line => { + const values = line.split(',').map(v => v.trim().replace(/"/g, '')); + const row: any = {}; + headers.forEach((header, index) => { + row[header] = values[index] || ''; + }); + return row; + }); + + setPreview(data.slice(0, 5)); // Afficher les 5 premières lignes + }; + reader.readAsText(file); + }; + + const handleImport = async () => { + if (!file) return; + + setLoading(true); + try { + const reader = new FileReader(); + reader.onload = (e) => { + const text = e.target?.result as string; + const lines = text.split('\n').filter(line => line.trim()); + const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, '')); + const data = lines.slice(1).map(line => { + const values = line.split(',').map(v => v.trim().replace(/"/g, '')); + const row: any = {}; + headers.forEach((header, index) => { + row[header] = values[index] || ''; + }); + return row; + }); + + onImport(data); + onClose(); + setFile(null); + setPreview([]); + }; + reader.readAsText(file); + } catch (error) { + setError('Erreur lors de l\'import du fichier.'); + } finally { + setLoading(false); + } + }; + + const getExpectedColumns = () => { + if (type === 'propositions') { + return ['title', 'description', 'author_first_name', 'author_last_name', 'author_email']; + } else { + return ['first_name', 'last_name', 'email']; + } + }; + + const downloadTemplate = () => { + const columns = getExpectedColumns(); + const csvContent = columns.join(',') + '\n'; + const blob = new Blob([csvContent], { type: 'text/csv' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `template_${type}.csv`; + a.click(); + window.URL.revokeObjectURL(url); + }; + + return ( + + + + + + Importer des {type === 'propositions' ? 'propositions' : 'participants'} depuis CSV + + + Importez en masse des {type === 'propositions' ? 'propositions' : 'participants'} depuis un fichier CSV. + {campaignTitle && ( + + Campagne : {campaignTitle} + + )} + + + +
+ {/* Template download */} +
+
+ + + Téléchargez le modèle CSV + +
+ +
+ + {/* Expected columns */} +
+

+ Colonnes attendues : +

+
+ {getExpectedColumns().join(', ')} +
+
+ + {/* File upload */} +
+ + +
+ + {/* Error message */} + {error && ( + + + {error} + + )} + + {/* Preview */} + {preview.length > 0 && ( +
+ +
+ + + + {Object.keys(preview[0] || {}).map((header) => ( + + ))} + + + + {preview.map((row, index) => ( + + {Object.values(row).map((value, cellIndex) => ( + + ))} + + ))} + +
+ {header} +
+ {String(value)} +
+
+
+ )} +
+ + + + + +
+
+ ); +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription }