Files
mes-budgets-participatifs/src/app/admin/campaigns/[id]/propositions/page.tsx
Yannick Le Duc 2a2738f5c0 - améliore l'export/import (format de fichiers en paramètres, amélioration de la robustesse, )
- ajout bouton tout effacer des propositions et participants
2025-09-16 15:45:28 +02:00

347 lines
13 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { Campaign, Proposition } from '@/types';
import { campaignService, propositionService } from '@/lib/services';
import AddPropositionModal from '@/components/AddPropositionModal';
import EditPropositionModal from '@/components/EditPropositionModal';
import DeletePropositionModal from '@/components/DeletePropositionModal';
import ImportFileModal from '@/components/ImportFileModal';
import ExportPropositionsButton from '@/components/ExportPropositionsButton';
import ClearAllPropositionsModal from '@/components/ClearAllPropositionsModal';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import Navigation from '@/components/Navigation';
import AuthGuard from '@/components/AuthGuard';
import { FileText, Calendar, Mail, Upload } from 'lucide-react';
import { MarkdownContent } from '@/components/MarkdownContent';
export const dynamic = 'force-dynamic';
function CampaignPropositionsPageContent() {
const params = useParams();
const campaignId = params.id as string;
const [campaign, setCampaign] = useState<Campaign | null>(null);
const [propositions, setPropositions] = useState<Proposition[]>([]);
const [loading, setLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const [showClearAllModal, setShowClearAllModal] = useState(false);
const [selectedProposition, setSelectedProposition] = useState<Proposition | null>(null);
useEffect(() => {
// Vérifier la configuration Supabase
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
// Si pas de configuration ou valeurs par défaut, rediriger vers setup
if (!supabaseUrl || !supabaseAnonKey ||
supabaseUrl === 'https://placeholder.supabase.co' ||
supabaseAnonKey === 'your-anon-key') {
console.log('🔧 Configuration Supabase manquante, redirection vers /setup');
window.location.href = '/setup';
return;
}
loadData();
}, [campaignId]);
const loadData = async () => {
try {
setLoading(true);
const [campaignData, propositionsData] = await Promise.all([
campaignService.getById(campaignId),
propositionService.getByCampaign(campaignId)
]);
setCampaign(campaignData);
setPropositions(propositionsData);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
} finally {
setLoading(false);
}
};
const handlePropositionAdded = () => {
setShowAddModal(false);
loadData();
};
const handlePropositionEdited = () => {
setShowEditModal(false);
loadData();
};
const handlePropositionDeleted = () => {
setShowDeleteModal(false);
loadData();
};
const handleImportPropositions = async (data: any[]) => {
try {
const propositionsToCreate = data.map(row => ({
campaign_id: campaignId,
title: row.Titre || '',
description: row.Description || '',
author_first_name: row.Prénom || 'admin',
author_last_name: row.Nom || 'admin',
author_email: row.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 handleClearAllPropositions = async () => {
try {
await propositionService.deleteAllByCampaign(campaignId);
loadData();
} catch (error) {
console.error('Erreur lors de la suppression des propositions:', error);
throw error;
}
};
const getInitials = (firstName: string, lastName: string) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
};
if (loading) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<div className="container mx-auto px-4 py-8">
<Navigation showBackButton backUrl="/admin" />
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900 dark:border-slate-100 mx-auto mb-4"></div>
<p className="text-slate-600 dark:text-slate-300">Chargement des propositions...</p>
</div>
</div>
</div>
</div>
);
}
if (!campaign) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<div className="container mx-auto px-4 py-8">
<Navigation showBackButton backUrl="/admin" />
<Card className="border-dashed">
<CardContent className="p-12 text-center">
<div className="w-16 h-16 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-2xl"></span>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
Campagne introuvable
</h3>
<p className="text-slate-600 dark:text-slate-300 mb-6">
La campagne que vous recherchez n'existe pas ou a été supprimée.
</p>
<Button asChild>
<Link href="/admin">Retour à l'administration</Link>
</Button>
</CardContent>
</Card>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<div className="container mx-auto px-4 py-8">
<Navigation showBackButton backUrl="/admin" />
{/* Header */}
<div className="mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">
Propositions
</h1>
<p className="text-slate-600 dark:text-slate-300 mt-2">
{campaign.title}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setShowImportModal(true)}>
<Upload className="w-4 h-4 mr-2" />
Importer
</Button>
<ExportPropositionsButton
propositions={propositions}
campaignTitle={campaign.title}
/>
{propositions.length > 0 && (
<Button
variant="outline"
onClick={() => setShowClearAllModal(true)}
className="text-red-600 border-red-200 hover:bg-red-50 hover:border-red-300 dark:text-red-400 dark:border-red-800 dark:hover:bg-red-900/20"
>
<FileText className="w-4 h-4 mr-2" />
Tout effacer
</Button>
)}
<Button onClick={() => setShowAddModal(true)} size="lg">
Nouvelle proposition
</Button>
</div>
</div>
</div>
{/* Propositions List */}
{propositions.length === 0 ? (
<Card className="border-dashed">
<CardContent className="p-12 text-center">
<div className="w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-4">
<FileText className="w-8 h-8 text-slate-400" />
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
Aucune proposition
</h3>
<p className="text-slate-600 dark:text-slate-300 mb-6">
Aucune proposition n'a encore été soumise pour cette campagne.
</p>
<Button onClick={() => setShowAddModal(true)}>
Ajouter une proposition
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-6">
{propositions.map((proposition) => (
<Card key={proposition.id} className="hover:shadow-lg transition-shadow duration-200">
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div className="flex-1">
<CardTitle className="text-xl mb-2">{proposition.title}</CardTitle>
<CardDescription className="text-base">
<MarkdownContent content={proposition.description} />
</CardDescription>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedProposition(proposition);
setShowEditModal(true);
}}
>
✏️ Modifier
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedProposition(proposition);
setShowDeleteModal(true);
}}
>
🗑️ Supprimer
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4 mb-4">
<Avatar className="w-10 h-10">
<AvatarFallback className="bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">
{getInitials(proposition.author_first_name, proposition.author_last_name)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium text-slate-900 dark:text-slate-100">
{proposition.author_first_name} {proposition.author_last_name}
</p>
<div className="flex items-center gap-4 text-sm text-slate-600 dark:text-slate-300">
<div className="flex items-center gap-1">
<Mail className="w-3 h-3" />
{proposition.author_email}
</div>
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{new Date(proposition.created_at).toLocaleDateString('fr-FR')}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* Modals */}
<AddPropositionModal
isOpen={showAddModal}
onClose={() => setShowAddModal(false)}
onSuccess={handlePropositionAdded}
campaignId={campaignId}
/>
{selectedProposition && (
<EditPropositionModal
isOpen={showEditModal}
onClose={() => setShowEditModal(false)}
onSuccess={handlePropositionEdited}
proposition={selectedProposition}
/>
)}
{selectedProposition && (
<DeletePropositionModal
isOpen={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
onSuccess={handlePropositionDeleted}
proposition={selectedProposition}
/>
)}
<ImportFileModal
isOpen={showImportModal}
onClose={() => setShowImportModal(false)}
onImport={handleImportPropositions}
type="propositions"
campaignTitle={campaign?.title}
/>
<ClearAllPropositionsModal
isOpen={showClearAllModal}
onClose={() => setShowClearAllModal(false)}
onConfirm={handleClearAllPropositions}
campaignTitle={campaign?.title}
propositionCount={propositions.length}
/>
</div>
</div>
);
}
export default function CampaignPropositionsPage() {
return (
<AuthGuard>
<CampaignPropositionsPageContent />
</AuthGuard>
);
}