diff --git a/app/api/centrale/[missionId]/route.ts b/app/api/centrale/[missionId]/route.ts
new file mode 100644
index 00000000..cf7189b7
--- /dev/null
+++ b/app/api/centrale/[missionId]/route.ts
@@ -0,0 +1,101 @@
+import { NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from "@/app/api/auth/options";
+import { prisma } from '@/lib/prisma';
+import { deleteMissionLogo } from '@/lib/mission-uploads';
+import { getPublicUrl, S3_CONFIG } from '@/lib/s3';
+import { IntegrationService } from '@/lib/services/integration-service';
+
+// Helper function to check authentication
+async function checkAuth(request: Request) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) {
+ console.error('Unauthorized access attempt:', {
+ url: request.url,
+ method: request.method,
+ headers: Object.fromEntries(request.headers)
+ });
+ return { authorized: false, userId: null };
+ }
+ return { authorized: true, userId: session.user.id };
+}
+
+// GET endpoint to retrieve a mission by ID
+export async function GET(request: Request, props: { params: Promise<{ missionId: string }> }) {
+ const params = await props.params;
+ try {
+ const { authorized, userId } = await checkAuth(request);
+ if (!authorized || !userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { missionId } = params;
+ if (!missionId) {
+ return NextResponse.json({ error: 'Mission ID is required' }, { status: 400 });
+ }
+
+ // Get mission with detailed info
+ const mission = await (prisma as any).mission.findFirst({
+ where: {
+ id: missionId,
+ OR: [
+ { creatorId: userId },
+ { missionUsers: { some: { userId } } }
+ ]
+ },
+ include: {
+ creator: {
+ select: {
+ id: true,
+ email: true
+ }
+ },
+ missionUsers: {
+ select: {
+ id: true,
+ role: true,
+ user: {
+ select: {
+ id: true,
+ email: true
+ }
+ }
+ }
+ },
+ attachments: {
+ select: {
+ id: true,
+ filename: true,
+ filePath: true,
+ fileType: true,
+ fileSize: true,
+ createdAt: true
+ },
+ orderBy: { createdAt: 'desc' }
+ }
+ }
+ });
+
+ if (!mission) {
+ return NextResponse.json({ error: 'Mission not found or access denied' }, { status: 404 });
+ }
+
+ // Add public URLs to mission logo and attachments
+ const missionWithUrls = {
+ ...mission,
+ logoUrl: mission.logo ? `/api/centrale/image/${mission.logo}` : null,
+ attachments: mission.attachments.map((attachment: { id: string; filename: string; filePath: string; fileType: string; fileSize: number; createdAt: Date }) => ({
+ ...attachment,
+ publicUrl: `/api/centrale/image/${attachment.filePath}`
+ }))
+ };
+
+ return NextResponse.json(missionWithUrls);
+ } catch (error) {
+ console.error('Error retrieving mission:', error);
+ return NextResponse.json({
+ error: 'Internal server error',
+ details: error instanceof Error ? error.message : String(error)
+ }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/centrale/all/route.ts b/app/api/centrale/all/route.ts
new file mode 100644
index 00000000..442922c5
--- /dev/null
+++ b/app/api/centrale/all/route.ts
@@ -0,0 +1,109 @@
+import { NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from "@/app/api/auth/options";
+import { prisma } from '@/lib/prisma';
+import { getPublicUrl } from '@/lib/s3';
+import { S3_CONFIG } from '@/lib/s3';
+
+// Helper function to check authentication
+async function checkAuth(request: Request) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) {
+ console.error('Unauthorized access attempt:', {
+ url: request.url,
+ method: request.method,
+ headers: Object.fromEntries(request.headers)
+ });
+ return { authorized: false, userId: null };
+ }
+ return { authorized: true, userId: session.user.id };
+}
+
+// GET endpoint to list all missions (not filtered by user)
+export async function GET(request: Request) {
+ try {
+ const { authorized, userId } = await checkAuth(request);
+ if (!authorized || !userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { searchParams } = new URL(request.url);
+ const limit = Number(searchParams.get('limit') || '100'); // Default to 100 for "all"
+ const offset = Number(searchParams.get('offset') || '0');
+ const search = searchParams.get('search');
+
+ // Build query conditions
+ const where: any = {};
+
+ // Add search filter if provided
+ if (search) {
+ where.OR = [
+ { name: { contains: search, mode: 'insensitive' } },
+ { intention: { contains: search, mode: 'insensitive' } }
+ ];
+ }
+
+ // Get all missions with basic info (no user filtering)
+ const missions = await prisma.mission.findMany({
+ where,
+ skip: offset,
+ take: limit,
+ orderBy: { createdAt: 'desc' },
+ select: {
+ id: true,
+ name: true,
+ logo: true,
+ oddScope: true,
+ niveau: true,
+ missionType: true,
+ projection: true,
+ participation: true,
+ services: true,
+ intention: true,
+ createdAt: true,
+ creator: {
+ select: {
+ id: true,
+ email: true
+ }
+ },
+ missionUsers: {
+ select: {
+ id: true,
+ role: true,
+ user: {
+ select: {
+ id: true,
+ email: true
+ }
+ }
+ }
+ }
+ }
+ });
+
+ // Get total count
+ const totalCount = await prisma.mission.count({ where });
+
+ // Transform logo paths to public URLs
+ const missionsWithPublicUrls = missions.map(mission => ({
+ ...mission,
+ logo: mission.logo ? `/api/centrale/image/${mission.logo}` : null
+ }));
+
+ return NextResponse.json({
+ missions: missionsWithPublicUrls,
+ pagination: {
+ total: totalCount,
+ offset,
+ limit
+ }
+ });
+ } catch (error) {
+ console.error('Error listing all missions:', error);
+ return NextResponse.json({
+ error: 'Internal server error',
+ details: error instanceof Error ? error.message : String(error)
+ }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/centrale/route.ts b/app/api/centrale/route.ts
new file mode 100644
index 00000000..d993b769
--- /dev/null
+++ b/app/api/centrale/route.ts
@@ -0,0 +1,257 @@
+import { NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from "@/app/api/auth/options";
+import { prisma } from '@/lib/prisma';
+import { getPublicUrl } from '@/lib/s3';
+import { S3_CONFIG } from '@/lib/s3';
+import { IntegrationService } from '@/lib/services/integration-service';
+
+// Helper function to check authentication
+async function checkAuth(request: Request) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) {
+ console.error('Unauthorized access attempt:', {
+ url: request.url,
+ method: request.method,
+ headers: Object.fromEntries(request.headers)
+ });
+ return { authorized: false, userId: null };
+ }
+ return { authorized: true, userId: session.user.id };
+}
+
+// GET endpoint to list missions with filters
+export async function GET(request: Request) {
+ try {
+ const { authorized, userId } = await checkAuth(request);
+ if (!authorized || !userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { searchParams } = new URL(request.url);
+ const limit = Number(searchParams.get('limit') || '10');
+ const offset = Number(searchParams.get('offset') || '0');
+ const search = searchParams.get('search');
+
+ // Build query conditions
+ const where: any = {};
+
+ // Add search filter if provided
+ if (search) {
+ where.OR = [
+ { name: { contains: search, mode: 'insensitive' } },
+ { intention: { contains: search, mode: 'insensitive' } }
+ ];
+ }
+
+ // Get missions with basic info
+ const missions = await (prisma as any).mission.findMany({
+ where,
+ skip: offset,
+ take: limit,
+ orderBy: { createdAt: 'desc' },
+ select: {
+ id: true,
+ name: true,
+ logo: true,
+ oddScope: true,
+ niveau: true,
+ missionType: true,
+ projection: true,
+ participation: true,
+ services: true,
+ intention: true,
+ createdAt: true,
+ creator: {
+ select: {
+ id: true,
+ email: true
+ }
+ },
+ missionUsers: {
+ select: {
+ id: true,
+ role: true,
+ user: {
+ select: {
+ id: true,
+ email: true
+ }
+ }
+ }
+ }
+ }
+ });
+
+ // Get total count
+ const totalCount = await (prisma as any).mission.count({ where });
+
+ // Transform logo paths to public URLs
+ const missionsWithFormatting = missions.map((mission: any) => ({
+ ...mission,
+ logo: mission.logo ? `/api/centrale/image/${mission.logo}` : null
+ }));
+
+ return NextResponse.json({
+ missions: missionsWithFormatting,
+ pagination: {
+ total: totalCount,
+ offset,
+ limit
+ }
+ });
+ } catch (error) {
+ console.error('Error listing missions:', error);
+ return NextResponse.json({
+ error: 'Internal server error',
+ details: error instanceof Error ? error.message : String(error)
+ }, { status: 500 });
+ }
+}
+
+// POST endpoint to create a new mission
+export async function POST(request: Request) {
+ try {
+ const { authorized, userId } = await checkAuth(request);
+ if (!authorized || !userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Parse the request body
+ const body = await request.json();
+ const {
+ name,
+ logo,
+ oddScope,
+ niveau,
+ intention,
+ missionType,
+ donneurDOrdre,
+ projection,
+ services,
+ participation,
+ profils,
+ guardians,
+ volunteers
+ } = body;
+
+ // Validate required fields
+ if (!name || !niveau || !intention || !missionType || !donneurDOrdre || !projection) {
+ return NextResponse.json({
+ error: 'Missing required fields',
+ required: {
+ name: true,
+ niveau: true,
+ intention: true,
+ missionType: true,
+ donneurDOrdre: true,
+ projection: true
+ },
+ received: {
+ name: !!name,
+ niveau: !!niveau,
+ intention: !!intention,
+ missionType: !!missionType,
+ donneurDOrdre: !!donneurDOrdre,
+ projection: !!projection
+ }
+ }, { status: 400 });
+ }
+
+ // Wrap the mission creation and integration in a transaction
+ const result = await prisma.$transaction(async (tx: any) => {
+ // Create the mission
+ const mission = await tx.mission.create({
+ data: {
+ name,
+ logo,
+ oddScope: oddScope || [],
+ niveau,
+ intention,
+ missionType,
+ donneurDOrdre,
+ projection,
+ services: services || [],
+ participation,
+ profils: profils || [],
+ creatorId: userId
+ }
+ });
+
+ // Add guardians if provided
+ if (guardians) {
+ const guardianRoles = ['gardien-temps', 'gardien-parole', 'gardien-memoire'];
+ const guardianEntries = Object.entries(guardians)
+ .filter(([role, userId]) => guardianRoles.includes(role) && userId)
+ .map(([role, userId]) => ({
+ role,
+ userId: userId as string,
+ missionId: mission.id
+ }));
+
+ if (guardianEntries.length > 0) {
+ await tx.missionUser.createMany({
+ data: guardianEntries
+ });
+ }
+ }
+
+ // Add volunteers if provided
+ if (volunteers && volunteers.length > 0) {
+ const volunteerEntries = volunteers.map((userId: string) => ({
+ role: 'volontaire',
+ userId,
+ missionId: mission.id
+ }));
+
+ await tx.missionUser.createMany({
+ data: volunteerEntries
+ });
+ }
+
+ return mission;
+ });
+
+ try {
+ // Initialize external integrations after transaction completes
+ const integrationService = new IntegrationService();
+ const integrationResult = await integrationService.setupIntegrationsForMission(result.id);
+
+ if (!integrationResult.success) {
+ // If integration failed, the mission was already deleted in the integration service
+ return NextResponse.json({
+ error: 'Failed to set up external services',
+ details: integrationResult.error
+ }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ mission: {
+ id: result.id,
+ name: result.name,
+ createdAt: result.createdAt
+ },
+ integrations: {
+ status: 'success',
+ data: integrationResult.data
+ }
+ });
+ } catch (integrationError) {
+ // If there's any unhandled error, delete the mission and report failure
+ console.error('Integration error:', integrationError);
+ await (prisma as any).mission.delete({ where: { id: result.id } });
+
+ return NextResponse.json({
+ error: 'Failed to set up external services',
+ details: integrationError instanceof Error ? integrationError.message : String(integrationError)
+ }, { status: 500 });
+ }
+ } catch (error) {
+ console.error('Error creating mission:', error);
+ return NextResponse.json({
+ error: 'Internal server error',
+ details: error instanceof Error ? error.message : String(error)
+ }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/centrale/[missionId]/edit/page.tsx b/app/centrale/[missionId]/edit/page.tsx
new file mode 100644
index 00000000..0389f916
--- /dev/null
+++ b/app/centrale/[missionId]/edit/page.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { MissionsAdminPanel } from "@/components/missions/missions-admin-panel";
+import { Button } from "@/components/ui/button";
+import { ArrowLeft, Home } from "lucide-react";
+
+export default function EditMissionPage({ params }: { params: { missionId: string }}) {
+ const router = useRouter();
+ const { missionId } = params;
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Check if the mission exists
+ useEffect(() => {
+ const checkMission = async () => {
+ try {
+ const response = await fetch(`/api/centrale/${missionId}`);
+ if (!response.ok) {
+ console.error('Mission not found, redirecting to list');
+ router.push('/centrale');
+ }
+ setIsLoading(false);
+ } catch (error) {
+ console.error('Error checking mission:', error);
+ router.push('/centrale');
+ }
+ };
+
+ checkMission();
+ }, [missionId, router]);
+
+ if (isLoading) {
+ return
Loading...
;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/centrale/[missionId]/page.tsx b/app/centrale/[missionId]/page.tsx
new file mode 100644
index 00000000..3536dbcc
--- /dev/null
+++ b/app/centrale/[missionId]/page.tsx
@@ -0,0 +1,422 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { FileIcon, Calendar, Eye, MapPin, Users, Clock, ThumbsUp, Languages, BarChart, Edit, Trash2 } from "lucide-react";
+import { useToast } from "@/components/ui/use-toast";
+import { useParams, useRouter } from "next/navigation";
+
+// Define types for mission details
+interface User {
+ id: string;
+ email: string;
+}
+
+interface Attachment {
+ id: string;
+ filename: string;
+ filePath: string;
+ fileType: string;
+ fileSize: number;
+ publicUrl: string;
+ createdAt: string;
+}
+
+interface Mission {
+ id: string;
+ name: string;
+ logo?: string | null;
+ logoUrl?: string | null;
+ oddScope: string[];
+ niveau: string;
+ missionType: string;
+ projection: string;
+ intention?: string;
+ donneurDOrdre?: string;
+ participation?: string;
+ services?: string[];
+ profils?: string[];
+ attachments?: Attachment[];
+ createdAt: string;
+ creator: User;
+ missionUsers: any[];
+}
+
+export default function MissionDetailPage() {
+ const [mission, setMission] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [deleting, setDeleting] = useState(false);
+ const { toast } = useToast();
+ const params = useParams();
+ const router = useRouter();
+ const missionId = params.missionId as string;
+
+ // Fetch mission details
+ useEffect(() => {
+ const fetchMissionDetails = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch(`/api/centrale/${missionId}`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch mission details');
+ }
+ const data = await response.json();
+ console.log("Mission details:", data);
+ setMission(data.mission);
+ } catch (error) {
+ console.error('Error fetching mission details:', error);
+ toast({
+ title: "Error",
+ description: "Failed to load mission details",
+ variant: "destructive",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (missionId) {
+ fetchMissionDetails();
+ }
+ }, [missionId, toast]);
+
+ // Helper function to format date
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('fr-FR', {
+ day: '2-digit',
+ month: 'long',
+ year: 'numeric'
+ });
+ };
+
+ // Helper functions to get labels
+ const getMissionTypeLabel = (type: string) => {
+ switch(type) {
+ case 'remote': return 'À distance';
+ case 'onsite': return 'Sur site';
+ case 'hybrid': return 'Hybride';
+ default: return type;
+ }
+ };
+
+ const getDurationLabel = (projection: string) => {
+ switch(projection) {
+ case 'short': return '< 1 mois';
+ case 'medium': return '1-3 mois';
+ case 'long': return '> 3 mois';
+ default: return projection;
+ }
+ };
+
+ const getNiveauLabel = (niveau: string) => {
+ switch(niveau) {
+ case 'a': return 'Apprentissage';
+ case 'b': return 'Basique';
+ case 'c': return 'Complexe';
+ case 's': return 'Spécial';
+ default: return niveau;
+ }
+ };
+
+ // Function to get odd info
+ const getODDInfo = (oddScope: string[]) => {
+ const oddCode = oddScope && oddScope.length > 0
+ ? oddScope[0]
+ : null;
+
+ // Extract number from odd code (e.g., "odd-3" -> "3")
+ const oddNumber = oddCode ? oddCode.replace('odd-', '') : null;
+
+ return {
+ number: oddNumber,
+ label: oddNumber ? `ODD ${oddNumber}` : "Non catégorisé",
+ iconPath: oddNumber ? `/F SDG Icons 2019 WEB/F-WEB-Goal-${oddNumber.padStart(2, '0')}.png` : ""
+ };
+ };
+
+ // Handle edit mission
+ const handleEditMission = () => {
+ router.push(`/centrale/${missionId}/edit`);
+ };
+
+ // Handle delete mission
+ const handleDeleteMission = async () => {
+ if (!confirm("Êtes-vous sûr de vouloir supprimer cette mission ? Cette action est irréversible.")) {
+ return;
+ }
+
+ try {
+ setDeleting(true);
+ const response = await fetch(`/api/centrale/${missionId}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to delete mission');
+ }
+
+ toast({
+ title: "Success",
+ description: "Mission deleted successfully",
+ });
+
+ // Redirect back to missions list
+ router.push('/centrale');
+ } catch (error) {
+ console.error('Error deleting mission:', error);
+ toast({
+ title: "Error",
+ description: "Failed to delete mission",
+ variant: "destructive",
+ });
+ } finally {
+ setDeleting(false);
+ }
+ };
+
+ // Loading state
+ if (loading) {
+ return (
+
+ );
+ }
+
+ // Error state if mission not found
+ if (!mission) {
+ return (
+
+
+
Mission non trouvée
+
Cette mission n'existe pas ou a été supprimée.
+
+
+
+ );
+ }
+
+ const oddInfo = getODDInfo(mission.oddScope);
+
+ return (
+
+ {/* Header */}
+
+
+
+
{mission.name}
+
+
+
+ {formatDate(mission.createdAt)}
+
+
+
+ {Math.floor(Math.random() * 100) + 1} Views
+
+
+
+
+ {/* Display logo instead of Participate button */}
+
+ {mission.logoUrl ? (
+

{
+ console.log("Logo failed to load:", mission.logoUrl);
+ // Show placeholder on error
+ (e.currentTarget as HTMLImageElement).style.display = 'none';
+ const parent = e.currentTarget.parentElement;
+ if (parent) {
+ parent.classList.add('bg-gray-100');
+ parent.classList.add('flex');
+ parent.classList.add('items-center');
+ parent.classList.add('justify-center');
+ parent.innerHTML = `
${mission.name.slice(0, 2).toUpperCase()}`;
+ }
+ }}
+ />
+ ) : (
+
+ {mission.name.slice(0, 2).toUpperCase()}
+
+ )}
+
+
+
+
+ {/* Info Grid */}
+
+
+
+
+
+
+
Type de mission
+
{getMissionTypeLabel(mission.missionType)}
+
+
+
+
+
+
+
+
+
Donneur d'ordre
+
{mission.donneurDOrdre || "Non spécifié"}
+
+
+
+
+
+
+
+
+
Durée
+
{getDurationLabel(mission.projection)}
+
+
+
+
+
+
+
+
+
Niveau
+
{getNiveauLabel(mission.niveau)}
+
+
+
+
+
+
+
+
+
Participation
+
{mission.participation || "Non spécifié"}
+
+
+
+ {oddInfo.number && (
+
+
+

+
+
+
Objectif
+
Développement durable
+
+
+ )}
+
+
+ {/* Project Description */}
+
+
Description de la mission
+
+ {mission.intention || "Aucune description disponible pour cette mission."}
+
+
+
+ {/* Attachments Section */}
+ {mission.attachments && mission.attachments.length > 0 && (
+
+ )}
+
+ {/* Skills Required Section */}
+ {mission.profils && mission.profils.length > 0 && (
+
+
Profils recherchés
+
+ {mission.profils.map((profil, index) => (
+
+ {profil}
+
+ ))}
+
+
+ )}
+
+ {/* Services Section */}
+ {mission.services && mission.services.length > 0 && (
+
+
Services
+
+ {mission.services.map((service, index) => (
+
+ {service}
+
+ ))}
+
+
+ )}
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/centrale/layout.tsx b/app/centrale/layout.tsx
new file mode 100644
index 00000000..a5798f92
--- /dev/null
+++ b/app/centrale/layout.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+export default function CentraleLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const pathname = usePathname();
+
+ return (
+
+
+ {/* Sidebar with light pink background */}
+
+ {/* Title section */}
+
+
CAP
+
Centre d'Administration et de Pilotage
+
+
+ {/* Navigation links */}
+
+
+
+ {/* Main content - white background */}
+
+ {children}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/centrale/new/page.tsx b/app/centrale/new/page.tsx
new file mode 100644
index 00000000..537376d5
--- /dev/null
+++ b/app/centrale/new/page.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { MissionsAdminPanel } from "@/components/missions/missions-admin-panel";
+import Link from "next/link";
+import { ChevronRight } from "lucide-react";
+
+export default function NewMissionPage() {
+ return (
+
+ {/* Breadcrumb navigation */}
+
+
+ {/* Mission admin panel for creating a new mission */}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/centrale/page.tsx b/app/centrale/page.tsx
new file mode 100644
index 00000000..13e6dc12
--- /dev/null
+++ b/app/centrale/page.tsx
@@ -0,0 +1,311 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Search } from "lucide-react";
+import { Input } from "@/components/ui/input";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { useToast } from "@/components/ui/use-toast";
+import { getPublicUrl } from "@/lib/s3";
+
+// Define Mission interface
+interface User {
+ id: string;
+ email: string;
+}
+
+interface MissionUser {
+ id: string;
+ role: string;
+ user: User;
+}
+
+interface Mission {
+ id: string;
+ name: string;
+ logo?: string;
+ oddScope: string[];
+ niveau: string;
+ missionType: string;
+ projection: string;
+ participation?: string;
+ services?: string[];
+ createdAt: string;
+ creator: User;
+ missionUsers: MissionUser[];
+ intention?: string;
+}
+
+export default function CentralePage() {
+ const [searchTerm, setSearchTerm] = useState("");
+ const [missions, setMissions] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const { toast } = useToast();
+
+ // Fetch missions from API
+ useEffect(() => {
+ const fetchMissions = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch('/api/centrale');
+ if (!response.ok) {
+ throw new Error('Failed to fetch missions');
+ }
+ const data = await response.json();
+ // Debug log to check mission data structure including intention
+ console.log("Mission data with intention:", data.missions);
+ setMissions(data.missions || []);
+ } catch (error) {
+ console.error('Error fetching missions:', error);
+ toast({
+ title: "Erreur",
+ description: "Impossible de charger les missions",
+ variant: "destructive",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchMissions();
+ }, []);
+
+ // Filter missions based on search term
+ const filteredMissions = missions.filter(mission =>
+ mission.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ mission.niveau.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ mission.missionType.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ mission.oddScope.some(scope => scope.toLowerCase().includes(searchTerm.toLowerCase()))
+ );
+
+ // Function to format date
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('fr-FR', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric'
+ });
+ };
+
+ // Function to get mission category and icon
+ const getODDInfo = (mission: Mission) => {
+ const oddCode = mission.oddScope && mission.oddScope.length > 0
+ ? mission.oddScope[0]
+ : null;
+
+ // Extract number from odd code (e.g., "odd-3" -> "3")
+ const oddNumber = oddCode ? oddCode.replace('odd-', '') : null;
+
+ return {
+ number: oddNumber,
+ label: oddNumber ? `ODD ${oddNumber}` : "Non catégorisé",
+ iconPath: oddNumber ? `/F SDG Icons 2019 WEB/F-WEB-Goal-${oddNumber.padStart(2, '0')}.png` : ""
+ };
+ };
+
+ // Function to get appropriate badge color based on niveau
+ const getNiveauBadgeColor = (niveau: string) => {
+ switch(niveau) {
+ case 'a': return 'bg-green-100 text-green-800';
+ case 'b': return 'bg-blue-100 text-blue-800';
+ case 'c': return 'bg-purple-100 text-purple-800';
+ case 's': return 'bg-amber-100 text-amber-800';
+ default: return 'bg-gray-100 text-gray-800';
+ }
+ };
+
+ // Function to get full niveau label
+ const getNiveauLabel = (niveau: string) => {
+ switch(niveau) {
+ case 'a': return 'A';
+ case 'b': return 'B';
+ case 'c': return 'C';
+ case 's': return 'S';
+ default: return niveau.toUpperCase();
+ }
+ };
+
+ // Function to get mission type label
+ const getMissionTypeLabel = (type: string) => {
+ switch(type) {
+ case 'remote': return 'À distance';
+ case 'onsite': return 'Sur site';
+ case 'hybrid': return 'Hybride';
+ default: return type;
+ }
+ };
+
+ // Function to get participation label
+ const getParticipationLabel = (participation: string | null | undefined) => {
+ console.log("Participation value:", participation); // Debug log
+ if (!participation) return 'Non spécifié';
+ switch(participation) {
+ case 'volontaire': return 'Volontaire';
+ case 'cooptation': return 'Cooptation';
+ default: return participation;
+ }
+ };
+
+ // Function to get mission duration
+ const getDuration = (projection: string) => {
+ switch(projection) {
+ case 'short': return '< 1 mois';
+ case 'medium': return '1-3 mois';
+ case 'long': return '> 3 mois';
+ default: return projection;
+ }
+ };
+
+ return (
+
+
+
+
Gérez vos missions et opportunités de bénévolat
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+ {loading ? (
+
+ ) : filteredMissions.length > 0 ? (
+
+ {/* @ts-ignore */}
+ {(() => {
+ // Debug: Log all mission logos to see what URLs are being used
+ console.log("All mission logos:", filteredMissions.map(m => ({
+ id: m.id,
+ name: m.name,
+ logo: m.logo
+ })));
+ return filteredMissions.map((mission) => {
+ const oddInfo = getODDInfo(mission);
+ const niveauColor = getNiveauBadgeColor(mission.niveau);
+
+ return (
+
+ {/* Card Header with Name and Level */}
+
+
{mission.name}
+
+ {/* ODD scope icon moved next to level badge */}
+ {oddInfo.number && (
+
+

{
+ // Fallback if image fails to load
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+
+ )}
+
+ {getNiveauLabel(mission.niveau)}
+
+
+
+
+ {/* Centered Logo */}
+
+
+ {mission.logo ? (
+

{
+ console.log("Logo failed to load:", mission.logo);
+ console.log("Full URL attempted:", mission.logo);
+ // If the image fails to load, show the fallback
+ (e.currentTarget as HTMLImageElement).style.display = 'none';
+ // Show the fallback div
+ const fallbackDiv = e.currentTarget.parentElement?.querySelector('.logo-fallback');
+ if (fallbackDiv) {
+ (fallbackDiv as HTMLElement).style.display = 'flex';
+ }
+ }}
+ />
+ ) : null}
+
+ {mission.name.slice(0, 2).toUpperCase()}
+
+
+
+
+ {/* Card Content - Services and Description */}
+
+ {/* Services section */}
+ {mission.services && mission.services.length > 0 && (
+
+
Services:
+
+ {mission.services.map(service => (
+
+ {service}
+
+ ))}
+
+
+ )}
+
+ {/* Description text (can be added from mission data) */}
+
+ {mission.intention ?
+ (mission.intention.substring(0, 100) + (mission.intention.length > 100 ? '...' : '')) :
+ 'Pas de description disponible.'}
+
+
+
+ {/* Card Footer */}
+
+
+ Créée le {formatDate(mission.createdAt)}
+
+
+
+
+
+
+
+ );
+ });
+ })()}
+
+ ) : (
+
+
+
+
+
Aucune mission trouvée
+
+ Créez votre première mission pour commencer à organiser vos projets et inviter des participants.
+
+
+
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/missions/attachments-list.tsx b/components/missions/attachments-list.tsx
index 8a4c0425..1fc0d08d 100644
--- a/components/missions/attachments-list.tsx
+++ b/components/missions/attachments-list.tsx
@@ -28,6 +28,7 @@ import {
import { useSession } from 'next-auth/react';
import { toast } from '@/components/ui/use-toast';
import { FileUpload } from './file-upload';
+import Link from 'next/link';
interface Attachment {
id: string;
@@ -73,7 +74,7 @@ export function AttachmentsList({
setIsLoading(true);
try {
- const response = await fetch(`/api/missions/${missionId}/attachments`);
+ const response = await fetch(`/api/centrale/${missionId}/attachments`);
if (!response.ok) {
throw new Error('Failed to fetch attachments');
}
@@ -107,7 +108,7 @@ export function AttachmentsList({
if (!deleteAttachment) return;
try {
- const response = await fetch(`/api/missions/${missionId}/attachments/${deleteAttachment.id}`, {
+ const response = await fetch(`/api/centrale/${missionId}/attachments/${deleteAttachment.id}`, {
method: 'DELETE',
});
@@ -247,12 +248,13 @@ export function AttachmentsList({
asChild
className="text-gray-500 hover:text-gray-700 hover:bg-gray-100"
>
-
-
+
{allowDelete && (
diff --git a/components/missions/missions-admin-panel.tsx b/components/missions/missions-admin-panel.tsx
index 0e2f86f8..3174ae20 100644
--- a/components/missions/missions-admin-panel.tsx
+++ b/components/missions/missions-admin-panel.tsx
@@ -402,7 +402,7 @@ export function MissionsAdminPanel() {
};
// Send to API
- const response = await fetch('/api/missions', {
+ const response = await fetch('/api/centrale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -431,7 +431,7 @@ export function MissionsAdminPanel() {
logoFormData.append('missionId', newMissionId);
logoFormData.append('type', 'logo');
- const logoResponse = await fetch('/api/missions/upload', {
+ const logoResponse = await fetch('/api/centrale/upload', {
method: 'POST',
body: logoFormData
});
@@ -468,7 +468,7 @@ export function MissionsAdminPanel() {
attachmentFormData.append('type', 'attachment');
try {
- const attachmentResponse = await fetch('/api/missions/upload', {
+ const attachmentResponse = await fetch('/api/centrale/upload', {
method: 'POST',
body: attachmentFormData
});
@@ -512,7 +512,7 @@ export function MissionsAdminPanel() {
});
// Redirect to missions list
- router.push('/missions');
+ router.push('/centrale');
} catch (error) {
console.error('Error creating mission:', error);
diff --git a/components/sidebar.tsx b/components/sidebar.tsx
index b9209ffa..4a3d86d3 100644
--- a/components/sidebar.tsx
+++ b/components/sidebar.tsx
@@ -146,9 +146,9 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
iframe: process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL,
},
{
- title: "Missions",
+ title: "Centrale",
icon: Kanban,
- href: "/missions",
+ href: "/centrale",
iframe: process.env.NEXT_PUBLIC_IFRAME_MISSIONSBOARD_URL,
},
{