Files
mes-budgets-participatifs/src/app/admin/page.tsx
2025-08-25 17:29:35 +02:00

361 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}