361 lines
15 KiB
TypeScript
361 lines
15 KiB
TypeScript
'use client';
|
||
import { useState, useEffect } from 'react';
|
||
import Link from 'next/link';
|
||
import { Campaign, CampaignWithStats } from '@/types';
|
||
import { campaignService } from '@/lib/services';
|
||
import CreateCampaignModal from '@/components/CreateCampaignModal';
|
||
import EditCampaignModal from '@/components/EditCampaignModal';
|
||
import DeleteCampaignModal from '@/components/DeleteCampaignModal';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Progress } from '@/components/ui/progress';
|
||
import Navigation from '@/components/Navigation';
|
||
import AuthGuard from '@/components/AuthGuard';
|
||
import { FolderOpen, Users, FileText, CheckCircle, Clock, Plus, BarChart3 } from 'lucide-react';
|
||
|
||
export const dynamic = 'force-dynamic';
|
||
|
||
function AdminPageContent() {
|
||
const [campaigns, setCampaigns] = useState<CampaignWithStats[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||
const [showEditModal, setShowEditModal] = useState(false);
|
||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||
const [selectedCampaign, setSelectedCampaign] = useState<Campaign | null>(null);
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
|
||
useEffect(() => {
|
||
loadCampaigns();
|
||
}, []);
|
||
|
||
const loadCampaigns = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const campaignsData = await campaignService.getAll();
|
||
const campaignsWithStats = await Promise.all(
|
||
campaignsData.map(async (campaign) => {
|
||
const stats = await campaignService.getStats(campaign.id);
|
||
return {
|
||
...campaign,
|
||
stats
|
||
};
|
||
})
|
||
);
|
||
setCampaigns(campaignsWithStats);
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement des campagnes:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleCampaignCreated = () => {
|
||
setShowCreateModal(false);
|
||
loadCampaigns();
|
||
};
|
||
|
||
const handleCampaignEdited = () => {
|
||
setShowEditModal(false);
|
||
loadCampaigns();
|
||
};
|
||
|
||
const handleCampaignDeleted = () => {
|
||
setShowDeleteModal(false);
|
||
loadCampaigns();
|
||
};
|
||
|
||
const getStatusBadge = (status: string) => {
|
||
switch (status) {
|
||
case 'deposit':
|
||
return <Badge variant="secondary">Dépôt de propositions</Badge>;
|
||
case 'voting':
|
||
return <Badge variant="default">En cours de vote</Badge>;
|
||
case 'closed':
|
||
return <Badge variant="outline">Terminée</Badge>;
|
||
default:
|
||
return <Badge variant="outline">{status}</Badge>;
|
||
}
|
||
};
|
||
|
||
const getSpendingTiersDisplay = (tiers: string) => {
|
||
return tiers.split(',').map(tier => `${tier.trim()}€`).join(', ');
|
||
};
|
||
|
||
const filteredCampaigns = campaigns.filter(campaign =>
|
||
campaign.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
campaign.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||
);
|
||
|
||
const stats = {
|
||
total: campaigns.length,
|
||
deposit: campaigns.filter(c => c.status === 'deposit').length,
|
||
voting: campaigns.filter(c => c.status === 'voting').length,
|
||
closed: campaigns.filter(c => c.status === 'closed').length,
|
||
};
|
||
|
||
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 />
|
||
<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 campagnes...</p>
|
||
</div>
|
||
</div>
|
||
</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 />
|
||
|
||
{/* 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">Administration</h1>
|
||
<p className="text-slate-600 dark:text-slate-300 mt-2">Gérez vos campagnes de budget participatif</p>
|
||
</div>
|
||
<Button onClick={() => setShowCreateModal(true)} size="lg">
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
Nouvelle campagne
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats Overview */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||
<Card>
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Total Campagnes</p>
|
||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{stats.total}</p>
|
||
</div>
|
||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
|
||
<FolderOpen className="w-4 h-4 text-blue-600 dark:text-blue-300" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">En cours</p>
|
||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{stats.voting}</p>
|
||
</div>
|
||
<div className="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
|
||
<Clock className="w-4 h-4 text-green-600 dark:text-green-300" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Dépôt</p>
|
||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{stats.deposit}</p>
|
||
</div>
|
||
<div className="w-8 h-8 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center">
|
||
<FileText className="w-4 h-4 text-yellow-600 dark:text-yellow-300" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">Terminées</p>
|
||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{stats.closed}</p>
|
||
</div>
|
||
<div className="w-8 h-8 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
|
||
<CheckCircle className="w-4 h-4 text-purple-600 dark:text-purple-300" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Search */}
|
||
<div className="mb-6">
|
||
<Input
|
||
type="text"
|
||
placeholder="Rechercher une campagne..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="max-w-md"
|
||
/>
|
||
</div>
|
||
|
||
{/* Campaigns List */}
|
||
{filteredCampaigns.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">
|
||
<FolderOpen className="w-8 h-8 text-slate-400" />
|
||
</div>
|
||
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
|
||
{searchTerm ? 'Aucune campagne trouvée' : 'Aucune campagne'}
|
||
</h3>
|
||
<p className="text-slate-600 dark:text-slate-300 mb-6">
|
||
{searchTerm
|
||
? 'Aucune campagne ne correspond à votre recherche.'
|
||
: 'Commencez par créer votre première campagne de budget participatif.'
|
||
}
|
||
</p>
|
||
{!searchTerm && (
|
||
<Button onClick={() => setShowCreateModal(true)}>
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
Créer une campagne
|
||
</Button>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
) : (
|
||
<div className="grid gap-6">
|
||
{filteredCampaigns.map((campaign) => (
|
||
<Card key={campaign.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">
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<CardTitle className="text-xl">{campaign.title}</CardTitle>
|
||
{getStatusBadge(campaign.status)}
|
||
</div>
|
||
<CardDescription className="text-base">{campaign.description}</CardDescription>
|
||
</div>
|
||
<div className="flex flex-col sm:flex-row gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
setSelectedCampaign(campaign);
|
||
setShowEditModal(true);
|
||
}}
|
||
>
|
||
✏️ Modifier
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
setSelectedCampaign(campaign);
|
||
setShowDeleteModal(true);
|
||
}}
|
||
>
|
||
🗑️ Supprimer
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
|
||
<CardContent>
|
||
{/* <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||
<div className="text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
|
||
<p className="text-sm text-slate-600 dark:text-slate-300">Propositions</p>
|
||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">{campaign.stats.propositions}</p>
|
||
</div>
|
||
<div className="text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
|
||
<p className="text-sm text-slate-600 dark:text-slate-300">Participants</p>
|
||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">{campaign.stats.participants}</p>
|
||
</div>
|
||
<div className="text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
|
||
<p className="text-sm text-slate-600 dark:text-slate-300">Budget/participant</p>
|
||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">{campaign.budget_per_user}€</p>
|
||
</div>
|
||
<div className="text-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
|
||
<p className="text-sm text-slate-600 dark:text-slate-300">Paliers</p>
|
||
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">{getSpendingTiersDisplay(campaign.spending_tiers)}</p>
|
||
</div>
|
||
</div> */}
|
||
|
||
{/* Public URL for deposit campaigns */}
|
||
{campaign.status === 'deposit' && (
|
||
<div className="mb-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||
Lien public pour le dépôt de propositions :
|
||
</h4>
|
||
<div className="flex items-center space-x-2">
|
||
<Input
|
||
type="text"
|
||
readOnly
|
||
value={`${window.location.origin}/campaigns/${campaign.id}/propose`}
|
||
className="flex-1 text-sm bg-white dark:bg-slate-800 border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300 font-mono"
|
||
/>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
navigator.clipboard.writeText(`${window.location.origin}/campaigns/${campaign.id}/propose`);
|
||
}}
|
||
className="text-xs"
|
||
>
|
||
Copier
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Action Buttons */}
|
||
<div className="flex flex-col sm:flex-row gap-2 mt-4">
|
||
<Button asChild variant="outline" className="flex-1">
|
||
<Link href={`/admin/campaigns/${campaign.id}/propositions`}>
|
||
<FileText className="w-4 h-4 mr-2" />
|
||
Propositions ({campaign.stats.propositions})
|
||
</Link>
|
||
</Button>
|
||
<Button asChild variant="outline" className="flex-1">
|
||
<Link href={`/admin/campaigns/${campaign.id}/participants`}>
|
||
<Users className="w-4 h-4 mr-2" />
|
||
Votants ({campaign.stats.participants})
|
||
</Link>
|
||
</Button>
|
||
{(campaign.status === 'voting' || campaign.status === 'closed') && (
|
||
<Button asChild variant="default" className="flex-1">
|
||
<Link href={`/admin/campaigns/${campaign.id}/stats`}>
|
||
<BarChart3 className="w-4 h-4 mr-2" />
|
||
Statistiques
|
||
</Link>
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Modals */}
|
||
<CreateCampaignModal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} onSuccess={handleCampaignCreated} />
|
||
{selectedCampaign && (
|
||
<EditCampaignModal isOpen={showEditModal} onClose={() => setShowEditModal(false)} onSuccess={handleCampaignEdited} campaign={selectedCampaign} />
|
||
)}
|
||
{selectedCampaign && (
|
||
<DeleteCampaignModal isOpen={showDeleteModal} onClose={() => setShowDeleteModal(false)} onSuccess={handleCampaignDeleted} campaign={selectedCampaign} />
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function AdminPage() {
|
||
return (
|
||
<AuthGuard>
|
||
<AdminPageContent />
|
||
</AuthGuard>
|
||
);
|
||
}
|