347 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|