missions mission pages

This commit is contained in:
alma 2025-05-06 21:05:35 +02:00
parent ff1b5244e6
commit f45efbedb5
12 changed files with 105 additions and 286 deletions

View File

@ -1,101 +0,0 @@
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 });
}
}

View File

@ -88,7 +88,7 @@ export async function GET(request: Request) {
// Transform logo paths to public URLs
const missionsWithPublicUrls = missions.map(mission => ({
...mission,
logo: mission.logo ? `/api/centrale/image/${mission.logo}` : null
logo: mission.logo ? `/api/missions/image/${mission.logo}` : null
}));
return NextResponse.json({

View File

@ -1,70 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/options';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { S3_CONFIG } from '@/lib/s3';
// Initialize S3 client
const s3Client = new S3Client({
region: S3_CONFIG.region,
endpoint: S3_CONFIG.endpoint,
credentials: {
accessKeyId: S3_CONFIG.accessKey || '',
secretAccessKey: S3_CONFIG.secretKey || ''
},
forcePathStyle: true // Required for MinIO
});
// This endpoint serves mission images from Minio using the server's credentials
export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) {
try {
// Check if path is provided
if (!params.path || params.path.length === 0) {
console.error('Missing path parameter');
return new NextResponse('Path is required', { status: 400 });
}
// Reconstruct the full path from path segments
const filePath = params.path.join('/');
console.log('Fetching centrale image:', filePath);
console.log('S3 bucket being used:', S3_CONFIG.bucket);
// Create S3 command to get the object
const command = new GetObjectCommand({
Bucket: S3_CONFIG.bucket, // Use the pages bucket
Key: filePath,
});
// Get the object from S3/Minio
console.log('Sending S3 request...');
const response = await s3Client.send(command);
console.log('S3 response received');
if (!response.Body) {
console.error('File not found in Minio:', filePath);
return new NextResponse('File not found', { status: 404 });
}
// Log success information
console.log('File found, content type:', response.ContentType);
console.log('File size:', response.ContentLength, 'bytes');
// Get the readable web stream directly
const stream = response.Body.transformToWebStream();
// Determine content type
const contentType = response.ContentType || 'application/octet-stream';
// Create and return a new response with the file stream
return new NextResponse(stream as ReadableStream, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=31536000', // Cache for 1 year
},
});
} catch (error) {
console.error('Error serving centrale image:', error);
console.error('Error details:', JSON.stringify(error, null, 2));
return new NextResponse('Internal Server Error', { status: 500 });
}
}

View File

@ -87,13 +87,13 @@ export async function GET(request: Request) {
const totalCount = await (prisma as any).mission.count({ where });
// Transform logo paths to public URLs
const missionsWithFormatting = missions.map((mission: any) => ({
const missionsWithPublicUrls = missions.map((mission: any) => ({
...mission,
logo: mission.logo ? `/api/centrale/image/${mission.logo}` : null
logo: mission.logo ? `/api/missions/image/${mission.logo}` : null
}));
return NextResponse.json({
missions: missionsWithFormatting,
missions: missionsWithPublicUrls,
pagination: {
total: totalCount,
offset,

View File

@ -1,67 +1,58 @@
"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";
import { useToast } from "@/components/ui/use-toast";
import { useParams, useRouter } from "next/navigation";
export default function EditMissionPage({ params }: { params: { missionId: string }}) {
export default function EditMissionPage() {
const [loading, setLoading] = useState(true);
const { toast } = useToast();
const params = useParams();
const router = useRouter();
const { missionId } = params;
const [isLoading, setIsLoading] = useState(true);
// Check if the mission exists
const missionId = params.missionId as string;
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');
}
};
toast({
title: "Fonctionnalité en développement",
description: "L'édition de mission sera bientôt disponible.",
variant: "default",
});
checkMission();
}, [missionId, router]);
if (isLoading) {
return <div className="p-8 text-center">Loading...</div>;
}
setLoading(false);
}, [toast]);
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 className="bg-gray-50 min-h-screen p-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-100 mb-6 p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Modifier la mission</h1>
{loading ? (
<div className="flex justify-center my-12">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-600"></div>
</div>
) : (
<div className="space-y-6">
<p className="text-gray-500">
La fonctionnalité d'édition de mission est en cours de développement et sera disponible prochainement.
</p>
<div className="flex gap-4">
<Button
onClick={() => router.push(`/missions/${missionId}`)}
variant="outline"
>
Retour à la mission
</Button>
<Button
onClick={() => router.push("/missions")}
>
Voir toutes les missions
</Button>
</div>
</div>
)}
</div>
</div>
);

View File

@ -56,18 +56,18 @@ export default function MissionDetailPage() {
const fetchMissionDetails = async () => {
try {
setLoading(true);
const response = await fetch(`/api/centrale/${missionId}`);
const response = await fetch(`/api/missions/${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);
setMission(data);
} catch (error) {
console.error('Error fetching mission details:', error);
toast({
title: "Error",
description: "Failed to load mission details",
title: "Erreur",
description: "Impossible de charger les détails de la mission",
variant: "destructive",
});
} finally {
@ -137,7 +137,7 @@ export default function MissionDetailPage() {
// Handle edit mission
const handleEditMission = () => {
router.push(`/centrale/${missionId}/edit`);
router.push(`/missions/${missionId}/edit`);
};
// Handle delete mission
@ -148,7 +148,7 @@ export default function MissionDetailPage() {
try {
setDeleting(true);
const response = await fetch(`/api/centrale/${missionId}`, {
const response = await fetch(`/api/missions/${missionId}`, {
method: 'DELETE',
});
@ -157,17 +157,17 @@ export default function MissionDetailPage() {
}
toast({
title: "Success",
description: "Mission deleted successfully",
title: "Mission supprimée",
description: "La mission a été supprimée avec succès",
});
// Redirect back to missions list
router.push('/centrale');
router.push('/missions');
} catch (error) {
console.error('Error deleting mission:', error);
toast({
title: "Error",
description: "Failed to delete mission",
title: "Erreur",
description: "Impossible de supprimer la mission",
variant: "destructive",
});
} finally {

View File

@ -4,7 +4,7 @@ import React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
export default function CentraleLayout({
export default function MissionsLayout({
children,
}: {
children: React.ReactNode;
@ -24,13 +24,13 @@ export default function CentraleLayout({
{/* 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>
<Link href="/missions" passHref>
<div className={`px-6 py-[10px] ${pathname === "/missions" ? "bg-white" : ""} hover:bg-white`}>
<span className="text-sm font-normal text-gray-700">Mes Missions</span>
</div>
</Link>
<Link href="/centrale/new" passHref>
<div className={`px-6 py-[10px] ${pathname === "/centrale/new" ? "bg-white" : ""} hover:bg-white`}>
<Link href="/missions/new" passHref>
<div className={`px-6 py-[10px] ${pathname === "/missions/new" ? "bg-white" : ""} hover:bg-white`}>
<span className="text-sm font-normal text-gray-700">Nouvelle Mission</span>
</div>
</Link>

View File

@ -1,27 +1,30 @@
"use client";
import { useState } from "react";
import { MissionsAdminPanel } from "@/components/missions/missions-admin-panel";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
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>
<div className="flex flex-col h-full w-full bg-white">
<div className="bg-white border-b border-gray-100 py-2 px-4">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/missions">Missions</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink>Poster une Mission</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
{/* Mission admin panel for creating a new mission */}
<div className="flex-1 overflow-auto">
<div className="flex-1 overflow-auto p-4 bg-white">
<MissionsAdminPanel />
</div>
</div>

View File

@ -36,7 +36,7 @@ interface Mission {
intention?: string;
}
export default function CentralePage() {
export default function MissionsPage() {
const [searchTerm, setSearchTerm] = useState("");
const [missions, setMissions] = useState<Mission[]>([]);
const [loading, setLoading] = useState(true);
@ -47,7 +47,7 @@ export default function CentralePage() {
const fetchMissions = async () => {
try {
setLoading(true);
const response = await fetch('/api/centrale');
const response = await fetch('/api/missions');
if (!response.ok) {
throw new Error('Failed to fetch missions');
}
@ -224,16 +224,14 @@ export default function CentralePage() {
<div className="w-48 h-48 relative">
{mission.logo ? (
<img
src={mission.logo}
src={mission.logo || ''}
alt={mission.name}
className="w-full h-full object-cover rounded-md"
onError={(e) => {
console.error("Logo failed to load:", mission.logo);
console.error("Full URL attempted:", window.location.origin + 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) {
@ -280,7 +278,7 @@ export default function CentralePage() {
Créée le {formatDate(mission.createdAt)}
</span>
<Link href={`/centrale/${mission.id}`}>
<Link href={`/missions/${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>
@ -300,7 +298,7 @@ export default function CentralePage() {
<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">
<Link href="/missions/new">
<Button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2">
Créer une nouvelle mission
</Button>

View File

@ -28,7 +28,6 @@ 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;
@ -74,7 +73,7 @@ export function AttachmentsList({
setIsLoading(true);
try {
const response = await fetch(`/api/centrale/${missionId}/attachments`);
const response = await fetch(`/api/missions/${missionId}/attachments`);
if (!response.ok) {
throw new Error('Failed to fetch attachments');
}
@ -108,7 +107,7 @@ export function AttachmentsList({
if (!deleteAttachment) return;
try {
const response = await fetch(`/api/centrale/${missionId}/attachments/${deleteAttachment.id}`, {
const response = await fetch(`/api/missions/${missionId}/attachments/${deleteAttachment.id}`, {
method: 'DELETE',
});
@ -248,13 +247,12 @@ export function AttachmentsList({
asChild
className="text-gray-500 hover:text-gray-700 hover:bg-gray-100"
>
<Link
href={`/api/centrale/${missionId}/attachments/download/${attachment.id}`}
target="_blank"
className="text-blue-600 hover:text-blue-800"
<a
href={`/api/missions/${missionId}/attachments/download/${attachment.id}`}
download={attachment.filename}
>
<Download className="h-4 w-4" />
</Link>
</a>
</Button>
{allowDelete && (

View File

@ -402,7 +402,7 @@ export function MissionsAdminPanel() {
};
// Send to API
const response = await fetch('/api/centrale', {
const response = await fetch('/api/missions', {
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/centrale/upload', {
const logoResponse = await fetch('/api/missions/upload', {
method: 'POST',
body: logoFormData
});
@ -468,7 +468,7 @@ export function MissionsAdminPanel() {
attachmentFormData.append('type', 'attachment');
try {
const attachmentResponse = await fetch('/api/centrale/upload', {
const attachmentResponse = await fetch('/api/missions/upload', {
method: 'POST',
body: attachmentFormData
});
@ -512,7 +512,7 @@ export function MissionsAdminPanel() {
});
// Redirect to missions list
router.push('/centrale');
router.push('/missions');
} catch (error) {
console.error('Error creating mission:', error);

View File

@ -146,9 +146,9 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
iframe: process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL,
},
{
title: "Mes Missions",
title: "Missions",
icon: Kanban,
href: "/centrale",
href: "/missions",
iframe: process.env.NEXT_PUBLIC_IFRAME_MISSIONSBOARD_URL,
},
{