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 ? ( + {mission.name} { + 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 && ( +
+
+ {oddInfo.label} +
+
+

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 && ( +
+

Documents

+
+ {mission.attachments.map((attachment) => ( + +
+ +
+
+

{attachment.filename}

+

+ {attachment.fileType.split('/')[1]?.toUpperCase() || 'Fichier'} +

+
+
+ ))} +
+
+ )} + + {/* 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 && ( +
+ {oddInfo.label} { + // Fallback if image fails to load + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> +
+ )} + + {getNiveauLabel(mission.niveau)} + +
+
+ + {/* Centered Logo */} +
+
+ {mission.logo ? ( + {mission.name} { + 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, }, {