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 (
+
+ );
+}
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 }