Pages corrections pages missions
This commit is contained in:
parent
b543c87e42
commit
eee7fb06b5
97
app/api/missions/[missionId]/files/folder/route.ts
Normal file
97
app/api/missions/[missionId]/files/folder/route.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from "@/app/api/auth/options";
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
// Use the exact same S3 client configuration as mission-uploads.ts
|
||||||
|
const missionsS3Client = new S3Client({
|
||||||
|
region: 'us-east-1',
|
||||||
|
endpoint: 'https://dome-api.slm-lab.net',
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.MINIO_ACCESS_KEY || '4aBT4CMb7JIMMyUtp4Pl',
|
||||||
|
secretAccessKey: process.env.MINIO_SECRET_KEY || 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg'
|
||||||
|
},
|
||||||
|
forcePathStyle: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const MISSIONS_BUCKET = 'missions';
|
||||||
|
|
||||||
|
// Helper function to check if user can manage files (creator or gardien)
|
||||||
|
async function checkCanManage(userId: string, missionId: string): Promise<boolean> {
|
||||||
|
const mission = await prisma.mission.findFirst({
|
||||||
|
where: { id: missionId },
|
||||||
|
select: {
|
||||||
|
creatorId: true,
|
||||||
|
missionUsers: {
|
||||||
|
where: { userId },
|
||||||
|
select: { role: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mission) return false;
|
||||||
|
|
||||||
|
// Creator can always manage
|
||||||
|
if (mission.creatorId === userId) return true;
|
||||||
|
|
||||||
|
// Gardiens can manage
|
||||||
|
const userRole = mission.missionUsers[0]?.role;
|
||||||
|
return userRole === 'gardien-temps' || userRole === 'gardien-parole' || userRole === 'gardien-memoire';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ missionId: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { missionId } = await params;
|
||||||
|
const userId = session.user.id;
|
||||||
|
|
||||||
|
// Check if user can manage files
|
||||||
|
const canManage = await checkCanManage(userId, missionId);
|
||||||
|
if (!canManage) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden: You do not have permission to create folders' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { path } = body;
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return NextResponse.json({ error: 'Path is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the S3 key for the folder marker
|
||||||
|
// Files are stored in MinIO without the "missions/" prefix
|
||||||
|
const s3Key = path.endsWith('/') ? `${path}.placeholder` : `${path}/.placeholder`;
|
||||||
|
|
||||||
|
// Create folder marker in S3
|
||||||
|
await missionsS3Client.send(new PutObjectCommand({
|
||||||
|
Bucket: MISSIONS_BUCKET,
|
||||||
|
Key: s3Key,
|
||||||
|
Body: Buffer.alloc(0),
|
||||||
|
ContentType: 'application/octet-stream'
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
folder: {
|
||||||
|
type: 'folder',
|
||||||
|
name: path.split('/').pop() || path,
|
||||||
|
path: `missions/${path}`,
|
||||||
|
key: `missions/${path}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error creating folder:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to create folder', details: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,6 +33,29 @@ async function checkMissionAccess(userId: string, missionId: string): Promise<bo
|
|||||||
return !!mission;
|
return !!mission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to check if user can manage files (creator or gardien)
|
||||||
|
async function checkCanManage(userId: string, missionId: string): Promise<boolean> {
|
||||||
|
const mission = await prisma.mission.findFirst({
|
||||||
|
where: { id: missionId },
|
||||||
|
select: {
|
||||||
|
creatorId: true,
|
||||||
|
missionUsers: {
|
||||||
|
where: { userId },
|
||||||
|
select: { role: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mission) return false;
|
||||||
|
|
||||||
|
// Creator can always manage
|
||||||
|
if (mission.creatorId === userId) return true;
|
||||||
|
|
||||||
|
// Gardiens can manage
|
||||||
|
const userRole = mission.missionUsers[0]?.role;
|
||||||
|
return userRole === 'gardien-temps' || userRole === 'gardien-parole' || userRole === 'gardien-memoire';
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to stream to string
|
// Helper function to stream to string
|
||||||
async function streamToString(stream: Readable): Promise<string> {
|
async function streamToString(stream: Readable): Promise<string> {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
@ -285,3 +308,67 @@ export async function PUT(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DELETE endpoint to delete a file or folder
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ missionId: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { missionId } = await params;
|
||||||
|
const userId = session.user.id;
|
||||||
|
|
||||||
|
// Check if user can manage files
|
||||||
|
const canManage = await checkCanManage(userId, missionId);
|
||||||
|
if (!canManage) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden: You do not have permission to delete files' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { key } = body;
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return NextResponse.json({ error: 'File key is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the key is within the mission folder
|
||||||
|
if (!key.startsWith(`missions/${missionId}/`)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid file path' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove missions/ prefix for MinIO (files are stored without it)
|
||||||
|
const minioKey = key.replace(/^missions\//, '');
|
||||||
|
|
||||||
|
// Delete from S3
|
||||||
|
await missionsS3Client.send(new DeleteObjectCommand({
|
||||||
|
Bucket: MISSIONS_BUCKET,
|
||||||
|
Key: minioKey
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Try to delete from database if it's an attachment
|
||||||
|
try {
|
||||||
|
await prisma.attachment.deleteMany({
|
||||||
|
where: {
|
||||||
|
missionId: missionId,
|
||||||
|
filePath: key
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (dbError) {
|
||||||
|
// Ignore database errors (file might not be in DB)
|
||||||
|
console.warn('Could not delete attachment from database:', dbError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error deleting file:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to delete file', details: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
118
app/api/missions/[missionId]/files/upload/route.ts
Normal file
118
app/api/missions/[missionId]/files/upload/route.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from "@/app/api/auth/options";
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
// Use the exact same S3 client configuration as mission-uploads.ts
|
||||||
|
const missionsS3Client = new S3Client({
|
||||||
|
region: 'us-east-1',
|
||||||
|
endpoint: 'https://dome-api.slm-lab.net',
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.MINIO_ACCESS_KEY || '4aBT4CMb7JIMMyUtp4Pl',
|
||||||
|
secretAccessKey: process.env.MINIO_SECRET_KEY || 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg'
|
||||||
|
},
|
||||||
|
forcePathStyle: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const MISSIONS_BUCKET = 'missions';
|
||||||
|
|
||||||
|
// Helper function to check if user can manage files (creator or gardien)
|
||||||
|
async function checkCanManage(userId: string, missionId: string): Promise<boolean> {
|
||||||
|
const mission = await prisma.mission.findFirst({
|
||||||
|
where: { id: missionId },
|
||||||
|
select: {
|
||||||
|
creatorId: true,
|
||||||
|
missionUsers: {
|
||||||
|
where: { userId },
|
||||||
|
select: { role: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mission) return false;
|
||||||
|
|
||||||
|
// Creator can always manage
|
||||||
|
if (mission.creatorId === userId) return true;
|
||||||
|
|
||||||
|
// Gardiens can manage
|
||||||
|
const userRole = mission.missionUsers[0]?.role;
|
||||||
|
return userRole === 'gardien-temps' || userRole === 'gardien-parole' || userRole === 'gardien-memoire';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ missionId: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { missionId } = await params;
|
||||||
|
const userId = session.user.id;
|
||||||
|
|
||||||
|
// Check if user can manage files
|
||||||
|
const canManage = await checkCanManage(userId, missionId);
|
||||||
|
if (!canManage) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden: You do not have permission to upload files' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
const file = formData.get('file') as File;
|
||||||
|
const path = formData.get('path') as string || 'attachments';
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return NextResponse.json({ error: 'File is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the S3 key
|
||||||
|
// Files are stored in MinIO without the "missions/" prefix
|
||||||
|
const s3Key = path ? `${missionId}/${path}/${file.name}` : `${missionId}/${file.name}`;
|
||||||
|
const filePath = `missions/${s3Key}`; // Full path for database
|
||||||
|
|
||||||
|
// Convert File to Buffer
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
// Upload to S3
|
||||||
|
await missionsS3Client.send(new PutObjectCommand({
|
||||||
|
Bucket: MISSIONS_BUCKET,
|
||||||
|
Key: s3Key,
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: file.type || 'application/octet-stream',
|
||||||
|
ACL: 'public-read'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create attachment record in database
|
||||||
|
const attachment = await prisma.attachment.create({
|
||||||
|
data: {
|
||||||
|
filename: file.name,
|
||||||
|
filePath: filePath,
|
||||||
|
fileType: file.type || 'application/octet-stream',
|
||||||
|
fileSize: file.size,
|
||||||
|
missionId: missionId,
|
||||||
|
uploaderId: userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
type: 'file',
|
||||||
|
name: file.name,
|
||||||
|
path: filePath,
|
||||||
|
key: filePath,
|
||||||
|
size: file.size,
|
||||||
|
lastModified: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error uploading file:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to upload file', details: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,40 +2,23 @@ import { NextResponse } from 'next/server';
|
|||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from "@/app/api/auth/options";
|
import { authOptions } from "@/app/api/auth/options";
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { deleteMissionLogo, deleteMissionAttachment, getMissionFileUrl } from '@/lib/mission-uploads';
|
|
||||||
import { getPublicUrl, S3_CONFIG } from '@/lib/s3';
|
|
||||||
import { N8nService } from '@/lib/services/n8n-service';
|
|
||||||
import { logger } from '@/lib/logger';
|
|
||||||
|
|
||||||
// Helper function to check authentication
|
// GET endpoint to get mission details including creator and missionUsers
|
||||||
async function checkAuth(request: Request) {
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ missionId: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
logger.error('Unauthorized access attempt', {
|
|
||||||
url: request.url,
|
|
||||||
method: request.method
|
|
||||||
});
|
|
||||||
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 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { missionId } = params;
|
const { missionId } = await params;
|
||||||
if (!missionId) {
|
const userId = session.user.id;
|
||||||
return NextResponse.json({ error: 'Mission ID is required' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get mission with detailed info
|
// Find mission and check access
|
||||||
const mission = await (prisma as any).mission.findFirst({
|
const mission = await prisma.mission.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: missionId,
|
id: missionId,
|
||||||
OR: [
|
OR: [
|
||||||
@ -43,10 +26,14 @@ export async function GET(request: Request, props: { params: Promise<{ missionId
|
|||||||
{ missionUsers: { some: { userId } } }
|
{ missionUsers: { some: { userId } } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
include: {
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
creatorId: true,
|
||||||
creator: {
|
creator: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
name: true,
|
||||||
email: true
|
email: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -54,24 +41,15 @@ export async function GET(request: Request, props: { params: Promise<{ missionId
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
role: true,
|
role: true,
|
||||||
|
userId: true,
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
name: true,
|
||||||
email: true
|
email: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
attachments: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
filename: true,
|
|
||||||
filePath: true,
|
|
||||||
fileType: true,
|
|
||||||
fileSize: true,
|
|
||||||
createdAt: true
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -80,363 +58,11 @@ export async function GET(request: Request, props: { params: Promise<{ missionId
|
|||||||
return NextResponse.json({ error: 'Mission not found or access denied' }, { status: 404 });
|
return NextResponse.json({ error: 'Mission not found or access denied' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add public URLs to mission logo and attachments
|
return NextResponse.json(mission);
|
||||||
const missionWithUrls = {
|
} catch (error: any) {
|
||||||
...mission,
|
console.error('Error fetching mission:', error);
|
||||||
logoUrl: mission.logo ? getMissionFileUrl(mission.logo) : null,
|
|
||||||
logo: mission.logo,
|
|
||||||
attachments: mission.attachments.map((attachment: { id: string; filename: string; filePath: string; fileType: string; fileSize: number; createdAt: Date }) => ({
|
|
||||||
...attachment,
|
|
||||||
publicUrl: getMissionFileUrl(attachment.filePath)
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug('Mission data with URLs', {
|
|
||||||
missionId: mission.id,
|
|
||||||
hasLogo: !!mission.logo,
|
|
||||||
attachmentCount: mission.attachments.length
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(missionWithUrls);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error retrieving mission', {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
missionId: params.missionId
|
|
||||||
});
|
|
||||||
return NextResponse.json({
|
|
||||||
error: 'Internal server error',
|
|
||||||
details: error instanceof Error ? error.message : String(error)
|
|
||||||
}, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT endpoint to update a mission
|
|
||||||
export async function PUT(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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if mission exists and user has access to modify it
|
|
||||||
const existingMission = await (prisma as any).mission.findFirst({
|
|
||||||
where: {
|
|
||||||
id: missionId,
|
|
||||||
OR: [
|
|
||||||
{ creatorId: userId },
|
|
||||||
{ missionUsers: { some: { userId, role: { in: ['gardien-temps', 'gardien-parole'] } } } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingMission) {
|
|
||||||
return NextResponse.json({ error: 'Mission not found or not authorized to update' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the request body
|
|
||||||
const body = await request.json();
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
logo,
|
|
||||||
oddScope,
|
|
||||||
niveau,
|
|
||||||
intention,
|
|
||||||
missionType,
|
|
||||||
donneurDOrdre,
|
|
||||||
projection,
|
|
||||||
services,
|
|
||||||
participation,
|
|
||||||
profils,
|
|
||||||
guardians,
|
|
||||||
volunteers
|
|
||||||
} = body;
|
|
||||||
|
|
||||||
// Process logo URL to get relative path
|
|
||||||
let logoPath = logo;
|
|
||||||
if (logo && typeof logo === 'string') {
|
|
||||||
try {
|
|
||||||
// If it's a full URL, extract the path
|
|
||||||
if (logo.startsWith('http')) {
|
|
||||||
const url = new URL(logo);
|
|
||||||
logoPath = url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname;
|
|
||||||
}
|
|
||||||
// If it's already a relative path, ensure it starts with 'missions/'
|
|
||||||
else if (!logo.startsWith('missions/')) {
|
|
||||||
logoPath = `missions/${logo}`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error processing logo URL', {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
missionId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the mission data
|
|
||||||
const updatedMission = await (prisma as any).mission.update({
|
|
||||||
where: { id: missionId },
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
logo: logoPath,
|
|
||||||
oddScope: oddScope || undefined,
|
|
||||||
niveau,
|
|
||||||
intention,
|
|
||||||
missionType,
|
|
||||||
donneurDOrdre,
|
|
||||||
projection,
|
|
||||||
services: services || undefined,
|
|
||||||
participation,
|
|
||||||
profils: profils || undefined
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update guardians if provided
|
|
||||||
if (guardians) {
|
|
||||||
// Get current guardians
|
|
||||||
const currentGuardians = await (prisma as any).missionUser.findMany({
|
|
||||||
where: {
|
|
||||||
missionId,
|
|
||||||
role: { in: ['gardien-temps', 'gardien-parole', 'gardien-memoire'] }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete all guardians
|
|
||||||
if (currentGuardians.length > 0) {
|
|
||||||
await (prisma as any).missionUser.deleteMany({
|
|
||||||
where: {
|
|
||||||
missionId,
|
|
||||||
role: { in: ['gardien-temps', 'gardien-parole', 'gardien-memoire'] }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new 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
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (guardianEntries.length > 0) {
|
|
||||||
await (prisma as any).missionUser.createMany({
|
|
||||||
data: guardianEntries
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update volunteers if provided
|
|
||||||
if (volunteers && Array.isArray(volunteers)) {
|
|
||||||
// Get current volunteers
|
|
||||||
const currentVolunteers = await (prisma as any).missionUser.findMany({
|
|
||||||
where: {
|
|
||||||
missionId,
|
|
||||||
role: 'volontaire'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete all volunteers
|
|
||||||
if (currentVolunteers.length > 0) {
|
|
||||||
await (prisma as any).missionUser.deleteMany({
|
|
||||||
where: {
|
|
||||||
missionId,
|
|
||||||
role: 'volontaire'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new volunteers
|
|
||||||
if (volunteers.length > 0) {
|
|
||||||
const volunteerEntries = volunteers.map((userId: string) => ({
|
|
||||||
role: 'volontaire',
|
|
||||||
userId,
|
|
||||||
missionId
|
|
||||||
}));
|
|
||||||
|
|
||||||
await (prisma as any).missionUser.createMany({
|
|
||||||
data: volunteerEntries
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
mission: {
|
|
||||||
id: updatedMission.id,
|
|
||||||
name: updatedMission.name,
|
|
||||||
updatedAt: updatedMission.updatedAt
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error updating mission', {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
missionId: params.missionId
|
|
||||||
});
|
|
||||||
return NextResponse.json({
|
|
||||||
error: 'Internal server error',
|
|
||||||
details: error instanceof Error ? error.message : String(error)
|
|
||||||
}, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE endpoint to remove a mission
|
|
||||||
export async function DELETE(
|
|
||||||
request: Request,
|
|
||||||
props: { params: Promise<{ missionId: string }> }
|
|
||||||
) {
|
|
||||||
const params = await props.params;
|
|
||||||
try {
|
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
if (!session?.user) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const mission = await prisma.mission.findUnique({
|
|
||||||
where: { id: params.missionId },
|
|
||||||
include: {
|
|
||||||
missionUsers: {
|
|
||||||
include: {
|
|
||||||
user: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!mission) {
|
|
||||||
return NextResponse.json({ error: 'Mission not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is mission creator or admin
|
|
||||||
const isCreator = mission.creatorId === session.user.id;
|
|
||||||
const userRoles = Array.isArray(session.user.role) ? session.user.role : [];
|
|
||||||
const isAdmin = userRoles.includes('admin') || userRoles.includes('ADMIN');
|
|
||||||
if (!isCreator && !isAdmin) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get attachments before deletion (needed for Minio cleanup)
|
|
||||||
const attachments = await prisma.attachment.findMany({
|
|
||||||
where: { missionId: params.missionId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Step 1: Trigger N8N workflow for deletion (rollback external integrations)
|
|
||||||
logger.debug('Starting N8N deletion workflow');
|
|
||||||
const n8nService = new N8nService();
|
|
||||||
|
|
||||||
// Extract repo name from giteaRepositoryUrl if present
|
|
||||||
// Format: https://gite.slm-lab.net/alma/repo-name or https://gite.slm-lab.net/api/v1/repos/alma/repo-name
|
|
||||||
let repoName = '';
|
|
||||||
if (mission.giteaRepositoryUrl) {
|
|
||||||
try {
|
|
||||||
const url = new URL(mission.giteaRepositoryUrl);
|
|
||||||
// Extract repo name from path (last segment)
|
|
||||||
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
||||||
repoName = pathParts[pathParts.length - 1] || '';
|
|
||||||
logger.debug('Extracted repo name from URL', { repoName });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error extracting repo name from URL', {
|
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
// If URL parsing fails, try to extract from the string directly
|
|
||||||
const match = mission.giteaRepositoryUrl.match(/\/([^\/]+)\/?$/);
|
|
||||||
repoName = match ? match[1] : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare data according to N8N workflow expectations
|
|
||||||
// The workflow expects: repoName, leantimeProjectId, documentationCollectionId, rocketchatChannelId
|
|
||||||
const n8nDeletionData = {
|
|
||||||
missionId: mission.id,
|
|
||||||
name: mission.name,
|
|
||||||
repoName: repoName, // N8N expects repoName, not giteaRepositoryUrl
|
|
||||||
leantimeProjectId: mission.leantimeProjectId || 0,
|
|
||||||
documentationCollectionId: mission.outlineCollectionId || '', // N8N expects documentationCollectionId
|
|
||||||
rocketchatChannelId: mission.rocketChatChannelId || '', // N8N expects rocketchatChannelId (lowercase 'c')
|
|
||||||
// Keep original fields for reference
|
|
||||||
giteaRepositoryUrl: mission.giteaRepositoryUrl,
|
|
||||||
outlineCollectionId: mission.outlineCollectionId,
|
|
||||||
rocketChatChannelId: mission.rocketChatChannelId,
|
|
||||||
penpotProjectId: mission.penpotProjectId,
|
|
||||||
config: {
|
|
||||||
N8N_API_KEY: process.env.N8N_API_KEY,
|
|
||||||
MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL || 'https://hub.slm-lab.net'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug('Sending deletion data to N8N', {
|
|
||||||
missionId: n8nDeletionData.missionId,
|
|
||||||
name: n8nDeletionData.name,
|
|
||||||
hasRepoName: !!n8nDeletionData.repoName
|
|
||||||
});
|
|
||||||
|
|
||||||
const n8nResult = await n8nService.triggerMissionDeletion(n8nDeletionData);
|
|
||||||
logger.debug('N8N deletion workflow result', {
|
|
||||||
success: n8nResult.success,
|
|
||||||
hasError: !!n8nResult.error
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!n8nResult.success) {
|
|
||||||
logger.error('N8N deletion workflow failed, but continuing with mission deletion', {
|
|
||||||
error: n8nResult.error
|
|
||||||
});
|
|
||||||
// Continue with deletion even if N8N fails (non-blocking)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Delete files from Minio AFTER N8N confirmation
|
|
||||||
// Delete logo if exists
|
|
||||||
if (mission.logo) {
|
|
||||||
try {
|
|
||||||
await deleteMissionLogo(params.missionId, mission.logo);
|
|
||||||
logger.debug('Logo deleted successfully from Minio');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error deleting mission logo from Minio', {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
missionId: params.missionId
|
|
||||||
});
|
|
||||||
// Continue deletion even if logo deletion fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete attachments from Minio
|
|
||||||
if (attachments.length > 0) {
|
|
||||||
logger.debug(`Deleting ${attachments.length} attachment(s) from Minio`);
|
|
||||||
for (const attachment of attachments) {
|
|
||||||
try {
|
|
||||||
await deleteMissionAttachment(attachment.filePath);
|
|
||||||
logger.debug('Attachment deleted successfully', { filename: attachment.filename });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error deleting attachment from Minio', {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
filename: attachment.filename
|
|
||||||
});
|
|
||||||
// Continue deletion even if one attachment fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Delete the mission from database (CASCADE will delete MissionUsers and Attachments)
|
|
||||||
await prisma.mission.delete({
|
|
||||||
where: { id: params.missionId }
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug('Mission deleted successfully from database', { missionId: params.missionId });
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error deleting mission', {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
missionId: params.missionId
|
|
||||||
});
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Failed to delete mission' },
|
{ error: 'Failed to fetch mission', details: error.message },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { useMediaQuery } from "@/hooks/use-media-query";
|
|||||||
import { ContactsView } from '@/components/carnet/contacts-view';
|
import { ContactsView } from '@/components/carnet/contacts-view';
|
||||||
import { MissionsView } from '@/components/carnet/missions-view';
|
import { MissionsView } from '@/components/carnet/missions-view';
|
||||||
import { MissionFilesView } from '@/components/carnet/mission-files-view';
|
import { MissionFilesView } from '@/components/carnet/mission-files-view';
|
||||||
|
import { MissionFilesManager } from '@/components/carnet/mission-files-manager';
|
||||||
import { X, Menu } from "lucide-react";
|
import { X, Menu } from "lucide-react";
|
||||||
import { ContactDetails } from '@/components/carnet/contact-details';
|
import { ContactDetails } from '@/components/carnet/contact-details';
|
||||||
import { parse as parseVCard, format as formatVCard } from 'vcard-parser';
|
import { parse as parseVCard, format as formatVCard } from 'vcard-parser';
|
||||||
@ -697,8 +698,26 @@ export default function CarnetPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMissionSelect = (mission: { id: string; name: string }) => {
|
const handleMissionSelect = async (mission: { id: string; name: string }) => {
|
||||||
|
// Fetch full mission details including creator and missionUsers
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/missions/${mission.id}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const missionData = await response.json();
|
||||||
|
setSelectedMission({
|
||||||
|
id: missionData.id,
|
||||||
|
name: missionData.name,
|
||||||
|
creatorId: missionData.creatorId || missionData.creator?.id,
|
||||||
|
missionUsers: missionData.missionUsers || []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback to basic mission data
|
||||||
setSelectedMission(mission);
|
setSelectedMission(mission);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching mission details:', error);
|
||||||
|
setSelectedMission(mission);
|
||||||
|
}
|
||||||
setSelectedMissionFile(null);
|
setSelectedMissionFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1120,7 +1139,7 @@ export default function CarnetPage() {
|
|||||||
onDelete={handleContactDelete}
|
onDelete={handleContactDelete}
|
||||||
/>
|
/>
|
||||||
) : selectedFolder === 'Missions' ? (
|
) : selectedFolder === 'Missions' ? (
|
||||||
selectedMission ? (
|
selectedMission && session?.user?.id ? (
|
||||||
selectedMissionFile ? (
|
selectedMissionFile ? (
|
||||||
<Editor
|
<Editor
|
||||||
note={{
|
note={{
|
||||||
@ -1138,11 +1157,17 @@ export default function CarnetPage() {
|
|||||||
currentFolder="Missions"
|
currentFolder="Missions"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MissionFilesView
|
<MissionFilesManager
|
||||||
missionId={selectedMission.id}
|
mission={{
|
||||||
|
id: selectedMission.id,
|
||||||
|
name: selectedMission.name,
|
||||||
|
creatorId: selectedMission.creatorId || '',
|
||||||
|
missionUsers: selectedMission.missionUsers || []
|
||||||
|
}}
|
||||||
|
currentUserId={session.user.id}
|
||||||
|
currentPath="attachments"
|
||||||
onFileSelect={handleMissionFileSelect}
|
onFileSelect={handleMissionFileSelect}
|
||||||
selectedFileKey={selectedMissionFile?.key}
|
selectedFileKey={selectedMissionFile?.key}
|
||||||
initialPath="attachments"
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
300
components/carnet/mission-files-manager.tsx
Normal file
300
components/carnet/mission-files-manager.tsx
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Folder, FileText, Upload, FolderPlus, Trash2, Loader2, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
interface MissionFile {
|
||||||
|
type: 'folder' | 'file';
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
key: string;
|
||||||
|
size?: number;
|
||||||
|
lastModified?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MissionUser {
|
||||||
|
role: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Mission {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
creatorId: string;
|
||||||
|
missionUsers?: MissionUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MissionFilesManagerProps {
|
||||||
|
mission: Mission;
|
||||||
|
currentUserId: string;
|
||||||
|
currentPath?: string;
|
||||||
|
onFileSelect?: (file: MissionFile) => void;
|
||||||
|
selectedFileKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MissionFilesManager: React.FC<MissionFilesManagerProps> = ({
|
||||||
|
mission,
|
||||||
|
currentUserId,
|
||||||
|
currentPath = 'attachments',
|
||||||
|
onFileSelect,
|
||||||
|
selectedFileKey
|
||||||
|
}) => {
|
||||||
|
const [files, setFiles] = useState<MissionFile[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [newFolderName, setNewFolderName] = useState('');
|
||||||
|
const [showNewFolder, setShowNewFolder] = useState(false);
|
||||||
|
|
||||||
|
// Check user permissions
|
||||||
|
const isCreator = mission.creatorId === currentUserId;
|
||||||
|
const userRole = mission.missionUsers?.find(mu => mu.userId === currentUserId)?.role;
|
||||||
|
const isGardien = userRole === 'gardien-temps' || userRole === 'gardien-parole' || userRole === 'gardien-memoire';
|
||||||
|
const canManage = isCreator || isGardien;
|
||||||
|
|
||||||
|
const fetchFiles = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const url = `/api/missions/${mission.id}/files${currentPath ? `?path=${encodeURIComponent(currentPath)}` : ''}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch files');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
// API returns { folders: [], files: [] } or { files: [] }
|
||||||
|
setFiles(data.files || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching mission files:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load files');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mission.id) return;
|
||||||
|
fetchFiles();
|
||||||
|
}, [mission.id, currentPath]);
|
||||||
|
|
||||||
|
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file || !canManage) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsUploading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('missionId', mission.id);
|
||||||
|
formData.append('path', currentPath);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/missions/${mission.id}/files/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to upload file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh file list
|
||||||
|
await fetchFiles();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error uploading file:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to upload file');
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
// Reset input
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateFolder = async () => {
|
||||||
|
if (!newFolderName.trim() || !canManage) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const folderPath = currentPath ? `${currentPath}/${newFolderName}` : newFolderName;
|
||||||
|
|
||||||
|
const response = await fetch(`/api/missions/${mission.id}/files/folder`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ path: folderPath })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to create folder');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh file list
|
||||||
|
await fetchFiles();
|
||||||
|
|
||||||
|
setNewFolderName('');
|
||||||
|
setShowNewFolder(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating folder:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to create folder');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (file: MissionFile) => {
|
||||||
|
if (!canManage || !confirm(`Êtes-vous sûr de vouloir supprimer ${file.name} ?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/missions/${mission.id}/files`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ key: file.key })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh file list
|
||||||
|
await fetchFiles();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting file:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to delete');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-carnet-text-muted" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-red-500 mb-2">Erreur</p>
|
||||||
|
<p className="text-carnet-text-muted text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full bg-carnet-bg">
|
||||||
|
<div className="p-4 border-b border-carnet-border">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Folder className="h-5 w-5 text-carnet-text-primary" />
|
||||||
|
<h2 className="text-lg font-semibold text-carnet-text-primary">
|
||||||
|
{currentPath || 'Fichiers'}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
{canManage && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewFolder(!showNewFolder)}
|
||||||
|
className="p-2 text-carnet-text-primary hover:bg-carnet-hover rounded-md"
|
||||||
|
title="Créer un dossier"
|
||||||
|
>
|
||||||
|
<FolderPlus className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<label className="p-2 text-carnet-text-primary hover:bg-carnet-hover rounded-md cursor-pointer">
|
||||||
|
<Upload className="h-5 w-5" />
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showNewFolder && canManage && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newFolderName}
|
||||||
|
onChange={(e) => setNewFolderName(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleCreateFolder()}
|
||||||
|
placeholder="Nom du dossier"
|
||||||
|
className="flex-1 px-3 py-2 border border-carnet-border rounded-md text-sm text-carnet-text-primary bg-white focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleCreateFolder}
|
||||||
|
className="px-4 py-2 bg-primary text-white rounded-md text-sm hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Créer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowNewFolder(false);
|
||||||
|
setNewFolderName('');
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 border border-carnet-border rounded-md text-sm text-carnet-text-primary hover:bg-carnet-hover"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{files.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<p className="text-carnet-text-muted">Aucun fichier</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className="divide-y divide-carnet-border">
|
||||||
|
{files.map((file) => {
|
||||||
|
const Icon = file.type === 'folder' ? Folder : FileText;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={file.key}
|
||||||
|
className={`p-4 hover:bg-carnet-hover flex items-center justify-between group ${
|
||||||
|
selectedFileKey === file.key ? 'bg-carnet-hover' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center space-x-2 flex-1 cursor-pointer"
|
||||||
|
onClick={() => onFileSelect?.(file)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4 text-carnet-text-muted" />
|
||||||
|
<span className="text-sm text-carnet-text-primary">{file.name}</span>
|
||||||
|
{file.type === 'folder' && (
|
||||||
|
<ChevronRight className="h-4 w-4 text-carnet-text-muted" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{canManage && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(file)}
|
||||||
|
className="p-2 text-red-500 hover:bg-red-50 rounded-md opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isUploading && (
|
||||||
|
<div className="p-4 border-t border-carnet-border">
|
||||||
|
<div className="flex items-center space-x-2 text-carnet-text-muted">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
<span className="text-sm">Upload en cours...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user