missions mission pages
This commit is contained in:
parent
87bffe35db
commit
6d3827bd6c
101
app/api/centrale/[missionId]/route.ts
Normal file
101
app/api/centrale/[missionId]/route.ts
Normal file
@ -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 });
|
||||
}
|
||||
}
|
||||
109
app/api/centrale/all/route.ts
Normal file
109
app/api/centrale/all/route.ts
Normal file
@ -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 });
|
||||
}
|
||||
}
|
||||
257
app/api/centrale/route.ts
Normal file
257
app/api/centrale/route.ts
Normal file
@ -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 });
|
||||
}
|
||||
}
|
||||
68
app/centrale/[missionId]/edit/page.tsx
Normal file
68
app/centrale/[missionId]/edit/page.tsx
Normal file
@ -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 <div className="p-8 text-center">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full bg-white">
|
||||
<div className="bg-white border-b border-gray-100 py-3 px-6 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/centrale/${missionId}`)}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Retour aux détails
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.push("/centrale")}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<Home className="h-4 w-4 mr-2" />
|
||||
Liste des missions
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto bg-white">
|
||||
<MissionsAdminPanel missionId={missionId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
422
app/centrale/[missionId]/page.tsx
Normal file
422
app/centrale/[missionId]/page.tsx
Normal file
@ -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<Mission | null>(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 (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state if mission not found
|
||||
if (!mission) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 px-4">
|
||||
<div className="text-center bg-white p-8 rounded-lg shadow-sm border border-gray-200 max-w-md">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-2">Mission non trouvée</h2>
|
||||
<p className="text-gray-600 mb-6">Cette mission n'existe pas ou a été supprimée.</p>
|
||||
<Button
|
||||
onClick={() => window.history.back()}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
Retour aux missions
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const oddInfo = getODDInfo(mission.oddScope);
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 min-h-screen p-6">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-100 mb-6 p-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">{mission.name}</h1>
|
||||
<div className="flex items-center text-gray-500 text-sm gap-4">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="h-4 w-4 mr-1" />
|
||||
{formatDate(mission.createdAt)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
{Math.floor(Math.random() * 100) + 1} Views
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Display logo instead of Participate button */}
|
||||
<div className="w-24 h-24 rounded-md overflow-hidden flex-shrink-0">
|
||||
{mission.logoUrl ? (
|
||||
<img
|
||||
src={mission.logoUrl}
|
||||
alt={mission.name}
|
||||
className="w-full h-full object-cover rounded-md"
|
||||
onError={(e) => {
|
||||
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 = `<span class="text-2xl font-medium text-gray-400">${mission.name.slice(0, 2).toUpperCase()}</span>`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
|
||||
<span className="text-2xl font-medium text-gray-400">{mission.name.slice(0, 2).toUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-white rounded-lg p-4 flex items-center shadow-sm border border-gray-100">
|
||||
<div className="bg-amber-50 p-3 rounded-full mr-3">
|
||||
<MapPin className="h-5 w-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 font-medium">Type de mission</p>
|
||||
<p className="text-gray-800 font-medium">{getMissionTypeLabel(mission.missionType)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-4 flex items-center shadow-sm border border-gray-100">
|
||||
<div className="bg-blue-50 p-3 rounded-full mr-3">
|
||||
<Users className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 font-medium">Donneur d'ordre</p>
|
||||
<p className="text-gray-800 font-medium">{mission.donneurDOrdre || "Non spécifié"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-4 flex items-center shadow-sm border border-gray-100">
|
||||
<div className="bg-green-50 p-3 rounded-full mr-3">
|
||||
<Clock className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 font-medium">Durée</p>
|
||||
<p className="text-gray-800 font-medium">{getDurationLabel(mission.projection)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-4 flex items-center shadow-sm border border-gray-100">
|
||||
<div className="bg-purple-50 p-3 rounded-full mr-3">
|
||||
<ThumbsUp className="h-5 w-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 font-medium">Niveau</p>
|
||||
<p className="text-gray-800 font-medium">{getNiveauLabel(mission.niveau)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-4 flex items-center shadow-sm border border-gray-100">
|
||||
<div className="bg-indigo-50 p-3 rounded-full mr-3">
|
||||
<Languages className="h-5 w-5 text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 font-medium">Participation</p>
|
||||
<p className="text-gray-800 font-medium">{mission.participation || "Non spécifié"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{oddInfo.number && (
|
||||
<div className="bg-white rounded-lg p-4 flex items-center shadow-sm border border-gray-100">
|
||||
<div className="bg-red-50 p-3 rounded-full mr-3 flex items-center justify-center">
|
||||
<img
|
||||
src={oddInfo.iconPath}
|
||||
alt={oddInfo.label}
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 font-medium">Objectif</p>
|
||||
<p className="text-gray-800 font-medium">Développement durable</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Project Description */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-100 mb-6 p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">Description de la mission</h2>
|
||||
<div className="text-gray-700 whitespace-pre-wrap">
|
||||
{mission.intention || "Aucune description disponible pour cette mission."}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Attachments Section */}
|
||||
{mission.attachments && mission.attachments.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-100 mb-6 p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">Documents</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{mission.attachments.map((attachment) => (
|
||||
<a
|
||||
key={attachment.id}
|
||||
href={attachment.publicUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-green-50 p-4 rounded-lg flex flex-col hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<div className="text-green-700 mb-2">
|
||||
<FileIcon className="h-10 w-10" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 mb-1 truncate">{attachment.filename}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{attachment.fileType.split('/')[1]?.toUpperCase() || 'Fichier'}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Skills Required Section */}
|
||||
{mission.profils && mission.profils.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-100 mb-6 p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">Profils recherchés</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mission.profils.map((profil, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-red-50 text-red-800 px-3 py-2 rounded-full text-sm"
|
||||
>
|
||||
{profil}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Services Section */}
|
||||
{mission.services && mission.services.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-100 mb-6 p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">Services</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mission.services.map((service, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-blue-50 text-blue-800 px-3 py-2 rounded-full text-sm"
|
||||
>
|
||||
{service}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-4 mb-8">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 border-blue-600 text-blue-600 hover:bg-blue-50"
|
||||
onClick={handleEditMission}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
Modifier
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 border-red-600 text-red-600 hover:bg-red-50"
|
||||
onClick={handleDeleteMission}
|
||||
disabled={deleting}
|
||||
>
|
||||
{deleting ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-red-600"></div>
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
Supprimer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
app/centrale/layout.tsx
Normal file
47
app/centrale/layout.tsx
Normal file
@ -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 (
|
||||
<main className="w-full h-screen bg-white">
|
||||
<div className="w-full h-full px-4 pt-12 pb-4 flex">
|
||||
{/* Sidebar with light pink background */}
|
||||
<div className="w-[234px] min-w-[234px] bg-pink-50 border-r border-gray-100 overflow-y-auto">
|
||||
{/* Title section */}
|
||||
<div className="bg-pink-50 py-4 px-6 border-b border-pink-100">
|
||||
<h2 className="text-lg font-medium text-gray-800">CAP</h2>
|
||||
<p className="text-xs text-gray-600">Centre d'Administration et de Pilotage</p>
|
||||
</div>
|
||||
|
||||
{/* Navigation links */}
|
||||
<nav className="mt-4">
|
||||
<Link href="/centrale" passHref>
|
||||
<div className={`px-6 py-[10px] ${pathname === "/centrale" ? "bg-white" : ""} hover:bg-white`}>
|
||||
<span className="text-sm font-normal text-gray-700">Centrale</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href="/centrale/new" passHref>
|
||||
<div className={`px-6 py-[10px] ${pathname === "/centrale/new" ? "bg-white" : ""} hover:bg-white`}>
|
||||
<span className="text-sm font-normal text-gray-700">Nouvelle Mission</span>
|
||||
</div>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Main content - white background */}
|
||||
<div className="flex-1 overflow-auto bg-white">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
29
app/centrale/new/page.tsx
Normal file
29
app/centrale/new/page.tsx
Normal file
@ -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 (
|
||||
<div className="w-full h-full bg-white flex flex-col">
|
||||
{/* Breadcrumb navigation */}
|
||||
<nav className="flex px-6 py-3 text-sm text-gray-500 border-b border-gray-100">
|
||||
<ol className="flex items-center space-x-2">
|
||||
<li>
|
||||
<Link href="/centrale">Centrale</Link>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<ChevronRight className="h-4 w-4 mx-1" />
|
||||
<span className="text-gray-900">Nouvelle mission</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{/* Mission admin panel for creating a new mission */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<MissionsAdminPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
311
app/centrale/page.tsx
Normal file
311
app/centrale/page.tsx
Normal file
@ -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<Mission[]>([]);
|
||||
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 (
|
||||
<div className="flex flex-col h-full w-full bg-white">
|
||||
<div className="bg-white border-b border-gray-100 py-3 px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-gray-800 text-base font-medium">Gérez vos missions et opportunités de bénévolat</h1>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="Rechercher une mission..."
|
||||
className="h-9 pl-9 pr-3 py-2 text-sm bg-white text-gray-800 border-gray-200 rounded-md w-60"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto bg-gray-50 p-6">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-40">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
) : filteredMissions.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* @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 (
|
||||
<div key={mission.id} className="bg-white shadow-sm hover:shadow-md transition-shadow duration-200 border border-gray-200 overflow-hidden h-full rounded-lg flex flex-col">
|
||||
{/* Card Header with Name and Level */}
|
||||
<div className="px-5 pt-4 pb-3 flex justify-between items-center border-b border-gray-100">
|
||||
<h2 className="text-base font-medium text-gray-900 line-clamp-2 flex-1">{mission.name}</h2>
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
{/* ODD scope icon moved next to level badge */}
|
||||
{oddInfo.number && (
|
||||
<div className="flex items-center bg-gray-100 p-1 rounded-md">
|
||||
<img
|
||||
src={oddInfo.iconPath}
|
||||
alt={oddInfo.label}
|
||||
className="w-8 h-8"
|
||||
onError={(e) => {
|
||||
// Fallback if image fails to load
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span className={`flex-shrink-0 text-sm font-bold px-2.5 py-1.5 rounded-md ${niveauColor}`}>
|
||||
{getNiveauLabel(mission.niveau)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Centered Logo */}
|
||||
<div className="flex justify-center items-center p-6 flex-grow">
|
||||
<div className="w-48 h-48 relative">
|
||||
{mission.logo ? (
|
||||
<img
|
||||
src={mission.logo || ''}
|
||||
alt={mission.name}
|
||||
className="w-full h-full object-cover rounded-md"
|
||||
onError={(e) => {
|
||||
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}
|
||||
<div
|
||||
className={`logo-fallback w-full h-full flex items-center justify-center bg-gray-100 rounded-md text-gray-500 text-4xl font-medium ${mission.logo ? 'hidden' : ''}`}
|
||||
>
|
||||
{mission.name.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Content - Services and Description */}
|
||||
<div className="px-5 pb-3">
|
||||
{/* Services section */}
|
||||
{mission.services && mission.services.length > 0 && (
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700 block mb-1">Services:</span>
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
{mission.services.map(service => (
|
||||
<span key={service} className="bg-blue-50 text-blue-700 px-2 py-1 rounded-md text-xs font-medium">
|
||||
{service}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description text (can be added from mission data) */}
|
||||
<div className="mt-2 text-sm text-gray-600 line-clamp-2">
|
||||
{mission.intention ?
|
||||
(mission.intention.substring(0, 100) + (mission.intention.length > 100 ? '...' : '')) :
|
||||
'Pas de description disponible.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Footer */}
|
||||
<div className="mt-auto px-5 py-3 border-t border-gray-100 bg-gray-50 flex justify-between items-center">
|
||||
<span className="text-xs text-gray-500">
|
||||
Créée le {formatDate(mission.createdAt)}
|
||||
</span>
|
||||
|
||||
<Link href={`/centrale/${mission.id}`}>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white text-xs px-3 py-1 h-7 rounded-md">
|
||||
Voir détails
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16 px-6 bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Search className="h-8 w-8 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Aucune mission trouvée</h3>
|
||||
<p className="text-gray-500 mb-6 max-w-md mx-auto">
|
||||
Créez votre première mission pour commencer à organiser vos projets et inviter des participants.
|
||||
</p>
|
||||
<Link href="/centrale/new">
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2">
|
||||
Créer une nouvelle mission
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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"
|
||||
>
|
||||
<a
|
||||
href={`/api/missions/${missionId}/attachments/download/${attachment.id}`}
|
||||
download={attachment.filename}
|
||||
<Link
|
||||
href={`/api/centrale/${missionId}/attachments/download/${attachment.id}`}
|
||||
target="_blank"
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</a>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{allowDelete && (
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user