NeahStable/components/missions/missions-admin-panel.tsx
2026-01-12 14:46:22 +01:00

902 lines
42 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { logger } from '@/lib/logger';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger
} from "../ui/tabs";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import { Checkbox } from "../ui/checkbox";
import {
Card,
CardContent
} from "../ui/card";
import { Badge } from "../ui/badge";
import { X, UploadCloud, File } from "lucide-react";
import { toast } from "../ui/use-toast";
import { FileUpload } from "./file-upload";
import { AttachmentsList } from "./attachments-list";
import { MissionMembersPanel } from "./mission-members-panel";
import { useRouter } from "next/navigation";
interface MissionData {
name?: string;
logo?: {
data: string;
name: string;
type: string;
};
oddScope?: string[];
niveau?: string;
intention?: string;
missionType?: string;
donneurDOrdre?: string;
projection?: string;
services?: string[];
profils?: string[];
participation?: string;
}
export function MissionsAdminPanel() {
const router = useRouter();
const [selectedServices, setSelectedServices] = useState<string[]>([]);
const [selectedProfils, setSelectedProfils] = useState<string[]>([]);
const [gardienDuTemps, setGardienDuTemps] = useState<string | null>(null);
const [gardienDeLaParole, setGardienDeLaParole] = useState<string | null>(null);
const [gardienDeLaMemoire, setGardienDeLaMemoire] = useState<string | null>(null);
const [volontaires, setVolontaires] = useState<string[]>([]);
const [missionId, setMissionId] = useState<string>("");
const [activeTab, setActiveTab] = useState<string>("general");
const [isSubmitting, setIsSubmitting] = useState(false);
const [selectedLogoFile, setSelectedLogoFile] = useState<File | null>(null);
const [selectedAttachments, setSelectedAttachments] = useState<File[]>([]);
const [missionData, setMissionData] = useState<MissionData>({});
// Check if mission is valid (has all required guardiens)
const isMissionValid = gardienDuTemps !== null && gardienDeLaParole !== null && gardienDeLaMemoire !== null;
// Add a draft mission ID for uploads
const [draftMissionId, setDraftMissionId] = useState<string>("");
const [uploadedLogoPath, setUploadedLogoPath] = useState<string | null>(null);
const [uploadedAttachmentPaths, setUploadedAttachmentPaths] = useState<string[]>([]);
// Generate a draft mission ID on mount
useEffect(() => {
if (!draftMissionId) {
const userId = typeof window !== 'undefined' && window.localStorage.getItem('userId');
const id = `draft-${userId || 'nouser'}-${Date.now()}`;
setDraftMissionId(id);
}
}, [draftMissionId]);
// Function to navigate to the next tab
const goToNextTab = () => {
const tabOrder = ["general", "details", "attachments", "skills", "membres"];
const currentIndex = tabOrder.indexOf(activeTab);
if (currentIndex < tabOrder.length - 1) {
const nextTab = tabOrder[currentIndex + 1];
setActiveTab(nextTab);
}
};
// Function to navigate to the previous tab
const goToPreviousTab = () => {
const tabOrder = ["general", "details", "attachments", "skills", "membres"];
const currentIndex = tabOrder.indexOf(activeTab);
if (currentIndex > 0) {
const prevTab = tabOrder[currentIndex - 1];
setActiveTab(prevTab);
}
};
// Check if we're on the last tab
const isLastTab = () => {
return activeTab === "membres";
};
// Check if we're on the first tab
const isFirstTab = () => {
return activeTab === "general";
};
// Validate all required fields
const validateMission = () => {
const requiredFields = {
name: !!missionData.name,
oddScope: Array.isArray(missionData.oddScope) && missionData.oddScope.length > 0,
niveau: !!missionData.niveau,
intention: !!missionData.intention,
missionType: !!missionData.missionType,
donneurDOrdre: !!missionData.donneurDOrdre,
projection: !!missionData.projection,
participation: !!missionData.participation,
gardiens: gardienDuTemps !== null && gardienDeLaParole !== null && gardienDeLaMemoire !== null
};
const missingFields = Object.entries(requiredFields)
.filter(([_, value]) => value === false)
.map(([key]) => key);
if (missingFields.length > 0) {
toast({
title: "Champs obligatoires manquants",
description: `Veuillez remplir tous les champs obligatoires: ${missingFields.join(", ")}`,
variant: "destructive",
});
return false;
}
return true;
};
// Helper function to convert File to base64
const convertFileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = error => reject(error);
});
};
// Handle mission submission
const handleSubmitMission = async () => {
console.log('[handleSubmitMission] Function called', {
activeTab,
isLastTab: activeTab === "membres"
});
logger.debug('handleSubmitMission called', {
activeTab,
isLastTab: activeTab === "membres"
});
if (!validateMission()) {
console.log('[handleSubmitMission] Validation failed, returning early');
logger.debug('Validation failed, returning early');
return;
}
console.log('[handleSubmitMission] Validation passed, setting isSubmitting to true');
logger.debug('Validation passed, setting isSubmitting to true');
setIsSubmitting(true);
try {
// Prepare the mission data
const guardians = {
"gardien-temps": gardienDuTemps,
"gardien-parole": gardienDeLaParole,
"gardien-memoire": gardienDeLaMemoire
};
// Convert attachments to base64
const attachmentsBase64 = await Promise.all(
selectedAttachments.map(async (file) => {
const base64Data = await convertFileToBase64(file);
return {
data: base64Data,
name: file.name,
type: file.type
};
})
);
console.log('[handleSubmitMission] Converted attachments to base64', {
count: attachmentsBase64.length
});
const missionSubmitData = {
...missionData,
services: selectedServices,
profils: selectedProfils,
guardians,
volunteers: volontaires,
logo: missionData.logo, // Ensure logo data is included
attachments: attachmentsBase64 // Include attachments as base64
};
console.log('[handleSubmitMission] Prepared mission data', {
hasName: !!missionSubmitData.name,
hasOddScope: !!missionSubmitData.oddScope,
hasLogo: !!missionSubmitData.logo,
logoType: missionSubmitData.logo ? typeof missionSubmitData.logo : 'null',
servicesCount: missionSubmitData.services?.length || 0,
profilsCount: missionSubmitData.profils?.length || 0,
volunteersCount: missionSubmitData.volunteers?.length || 0,
attachmentsCount: missionSubmitData.attachments?.length || 0,
hasGuardians: !!(missionSubmitData.guardians && Object.keys(missionSubmitData.guardians).length > 0)
});
logger.debug('Prepared mission data', {
hasName: !!missionSubmitData.name,
hasOddScope: !!missionSubmitData.oddScope,
hasLogo: !!missionSubmitData.logo,
logoType: missionSubmitData.logo ? typeof missionSubmitData.logo : 'null',
servicesCount: missionSubmitData.services?.length || 0,
profilsCount: missionSubmitData.profils?.length || 0,
volunteersCount: missionSubmitData.volunteers?.length || 0,
attachmentsCount: missionSubmitData.attachments?.length || 0,
hasGuardians: !!(missionSubmitData.guardians && Object.keys(missionSubmitData.guardians).length > 0)
});
// Test JSON serialization before sending
let jsonString;
try {
jsonString = JSON.stringify(missionSubmitData);
console.log('[handleSubmitMission] JSON.stringify successful', { jsonLength: jsonString.length });
logger.debug('JSON.stringify successful', { jsonLength: jsonString.length });
} catch (jsonError) {
console.error('[handleSubmitMission] JSON.stringify failed', jsonError);
logger.error('JSON.stringify failed', {
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
missionDataKeys: Object.keys(missionSubmitData)
});
throw new Error(`Failed to serialize mission data: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`);
}
console.log('[handleSubmitMission] Sending fetch request to /api/missions');
logger.debug('Sending fetch request to /api/missions');
// Send to API
const response = await fetch('/api/missions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: jsonString,
});
console.log('[handleSubmitMission] Fetch response received', {
status: response.status,
ok: response.ok,
statusText: response.statusText
});
logger.debug('Fetch response received', {
status: response.status,
ok: response.ok,
statusText: response.statusText
});
if (!response.ok) {
const errorData = await response.json();
console.error('[handleSubmitMission] API returned error response', {
status: response.status,
error: errorData.error,
errorData
});
logger.error('API returned error response', {
status: response.status,
error: errorData.error,
errorData
});
throw new Error(errorData.error || 'Failed to create mission');
}
const data = await response.json();
console.log('[handleSubmitMission] Mission created successfully', {
missionId: data.mission?.id,
success: data.success
});
logger.debug('Mission created successfully', {
missionId: data.mission?.id,
success: data.success
});
toast({
title: "Mission créée avec succès",
description: "Tous les gardiens ont été assignés et la mission a été enregistrée.",
});
// Redirect to missions list
router.push('/missions');
} catch (error) {
console.error('[handleSubmitMission] Error creating mission', {
error: error instanceof Error ? error.message : String(error),
errorType: error instanceof Error ? error.constructor.name : typeof error,
errorStack: error instanceof Error ? error.stack : undefined,
missionData: {
hasName: !!missionData.name,
hasOddScope: !!missionData.oddScope,
hasLogo: !!missionData.logo,
servicesCount: selectedServices.length,
profilsCount: selectedProfils.length,
volunteersCount: volontaires.length,
hasGuardians: !!(gardienDuTemps && gardienDeLaParole && gardienDeLaMemoire)
}
});
logger.error('Error creating mission', {
error: error instanceof Error ? error.message : String(error),
errorType: error instanceof Error ? error.constructor.name : typeof error,
missionData: {
hasName: !!missionData.name,
hasOddScope: !!missionData.oddScope,
hasLogo: !!missionData.logo,
servicesCount: selectedServices.length,
profilsCount: selectedProfils.length,
volunteersCount: volontaires.length,
hasGuardians: !!(gardienDuTemps && gardienDeLaParole && gardienDeLaMemoire)
}
});
toast({
title: "Erreur",
description: error instanceof Error ? error.message : "Une erreur est survenue lors de la création de la mission",
variant: "destructive",
});
} finally {
console.log('[handleSubmitMission] Finally block - setting isSubmitting to false');
setIsSubmitting(false);
}
};
// Function to handle input changes
const handleInputChange = (field: string, value: any) => {
setMissionData(prev => ({
...prev,
[field]: value
}));
};
return (
<div className="w-full">
<Card className="border shadow-sm bg-white">
<CardContent className="pt-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="mb-4 bg-gray-100">
<TabsTrigger value="general" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white">Général</TabsTrigger>
<TabsTrigger value="details" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white">Détails</TabsTrigger>
<TabsTrigger value="attachments" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white">Documents</TabsTrigger>
<TabsTrigger value="skills" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white">Compétences</TabsTrigger>
<TabsTrigger value="membres" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white">Membres</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-6">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Nom de la Mission<span className="text-red-500">*</span></label>
<Input
placeholder="Nom de la mission"
className="bg-white border-gray-300"
value={missionData.name || ''}
onChange={(e) => handleInputChange('name', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Logo</label>
<FileUpload
type="logo"
isNewMission={true}
onFileSelect={(fileData) => {
setMissionData(prev => ({
...prev,
logo: fileData
}));
}}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">ODD scope<span className="text-red-500">*</span></label>
<Select onValueChange={(value) => handleInputChange('oddScope', [value])}>
<SelectTrigger className="bg-white border-gray-300">
<SelectValue placeholder="Choisir le scope" />
</SelectTrigger>
<SelectContent>
<SelectItem value="odd-1">1. Pas de pauvreté</SelectItem>
<SelectItem value="odd-2">2. Faim "zéro"</SelectItem>
<SelectItem value="odd-3">3. Bonne santé et bien-être</SelectItem>
<SelectItem value="odd-4">4. Éducation de qualité</SelectItem>
<SelectItem value="odd-5">5. Égalité entre les sexes</SelectItem>
<SelectItem value="odd-6">6. Eau propre et assainissement</SelectItem>
<SelectItem value="odd-7">7. Énergie propre et d'un coût abordable</SelectItem>
<SelectItem value="odd-8">8. Travail décent et croissance économique</SelectItem>
<SelectItem value="odd-9">9. Industrie, innovation et infrastructure</SelectItem>
<SelectItem value="odd-10">10. Inégalités réduites</SelectItem>
<SelectItem value="odd-11">11. Villes et communautés durables</SelectItem>
<SelectItem value="odd-12">12. Consommation et production responsables</SelectItem>
<SelectItem value="odd-13">13. Mesures relatives à la lutte contre les changements climatiques</SelectItem>
<SelectItem value="odd-14">14. Vie aquatique</SelectItem>
<SelectItem value="odd-15">15. Vie terrestre</SelectItem>
<SelectItem value="odd-16">16. Paix, justice et institutions efficaces</SelectItem>
<SelectItem value="odd-17">17. Partenariats pour la réalisation des objectifs</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Niveau<span className="text-red-500">*</span></label>
<Select onValueChange={(value) => handleInputChange('niveau', value)}>
<SelectTrigger className="bg-white border-gray-300">
<SelectValue placeholder="Choisir un niveau" />
</SelectTrigger>
<SelectContent>
<SelectItem value="a">A- Apprentissage</SelectItem>
<SelectItem value="b">B- Basique</SelectItem>
<SelectItem value="c">C- Complexe</SelectItem>
<SelectItem value="s">S- Spécial</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Intention<span className="text-red-500">*</span></label>
<div className="border rounded-md border-gray-300">
<div className="bg-gray-50 p-2 border-b flex items-center space-x-2">
<span className="text-gray-700">Paragraphe</span>
<div className="flex items-center space-x-1">
<Button variant="ghost" size="icon" className="h-8 w-8 text-gray-700">B</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-gray-700">I</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-gray-700">•</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-gray-700">1.</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-gray-700">"</Button>
</div>
</div>
<Textarea
className="min-h-[200px] border-0 bg-white"
value={missionData.intention || ''}
onChange={(e) => handleInputChange('intention', e.target.value)}
/>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="details" className="space-y-6">
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Type de mission<span className="text-red-500">*</span></label>
<Select onValueChange={(value) => handleInputChange('missionType', value)}>
<SelectTrigger className="bg-white border-gray-300">
<SelectValue placeholder="Type de location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="remote">Remote</SelectItem>
<SelectItem value="onsite">Sur Site</SelectItem>
<SelectItem value="hybrid">Hybride</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Donneur d'ordre<span className="text-red-500">*</span></label>
<Select onValueChange={(value) => handleInputChange('donneurDOrdre', value)}>
<SelectTrigger className="bg-white border-gray-300">
<SelectValue placeholder="Sélectionner" />
</SelectTrigger>
<SelectContent>
<SelectItem value="individual">Individu</SelectItem>
<SelectItem value="group">ONG</SelectItem>
<SelectItem value="organization">Start-ups</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Projection<span className="text-red-500">*</span></label>
<Select onValueChange={(value) => handleInputChange('projection', value)}>
<SelectTrigger className="bg-white border-gray-300">
<SelectValue placeholder="Sélectionner" />
</SelectTrigger>
<SelectContent>
<SelectItem value="short">Court Terme</SelectItem>
<SelectItem value="medium">Moyen Terme</SelectItem>
<SelectItem value="long">Long Terme</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Services</label>
<div className="space-y-2">
<div className="flex flex-wrap gap-1 mb-2">
{selectedServices.map((service) => (
<Badge key={service} className="bg-blue-100 text-blue-800 hover:bg-blue-200 px-2 py-1">
{service}
<button
type="button"
onClick={() => setSelectedServices(selectedServices.filter(s => s !== service))}
className="ml-1 text-blue-600 hover:text-blue-800"
>
<X size={14} />
</button>
</Badge>
))}
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="gite"
checked={selectedServices.includes('Gite')}
onCheckedChange={(checked) => {
if (checked) {
setSelectedServices([...selectedServices, 'Gite']);
} else {
setSelectedServices(selectedServices.filter(s => s !== 'Gite'));
}
}}
className="border-gray-300"
/>
<label htmlFor="gite" className="text-sm text-gray-700">Gite</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="artlab"
checked={selectedServices.includes('ArtLab')}
onCheckedChange={(checked) => {
if (checked) {
setSelectedServices([...selectedServices, 'ArtLab']);
} else {
setSelectedServices(selectedServices.filter(s => s !== 'ArtLab'));
}
}}
className="border-gray-300"
/>
<label htmlFor="artlab" className="text-sm text-gray-700">ArtLab</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="calcul"
checked={selectedServices.includes('Calcul')}
onCheckedChange={(checked) => {
if (checked) {
setSelectedServices([...selectedServices, 'Calcul']);
} else {
setSelectedServices(selectedServices.filter(s => s !== 'Calcul'));
}
}}
className="border-gray-300"
/>
<label htmlFor="calcul" className="text-sm text-gray-700">Calcul</label>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Participation<span className="text-red-500">*</span></label>
<Select onValueChange={(value) => handleInputChange('participation', value)}>
<SelectTrigger className="bg-white border-gray-300">
<SelectValue placeholder="Sélectionner" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ouvert">Ouverte</SelectItem>
<SelectItem value="cooptation">Cooptation</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Location</label>
<Input placeholder="Enter location" className="bg-white border-gray-300" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Language</label>
<Input placeholder="Enter language" className="bg-white border-gray-300" />
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Profils</label>
<div className="space-y-2">
<div className="flex flex-wrap gap-1 mb-2">
{selectedProfils.map((profil) => (
<Badge key={profil} className="bg-blue-100 text-blue-800 hover:bg-blue-200 px-2 py-1">
{profil}
<button
type="button"
onClick={() => setSelectedProfils(selectedProfils.filter(p => p !== profil))}
className="ml-1 text-blue-600 hover:text-blue-800"
>
<X size={14} />
</button>
</Badge>
))}
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="dataintelligence"
checked={selectedProfils.includes('DataIntelligence')}
onCheckedChange={(checked) => {
if (checked) {
setSelectedProfils([...selectedProfils, 'DataIntelligence']);
// Auto-check Gite in services
if (!selectedServices.includes('Gite')) {
setSelectedServices([...selectedServices, 'Gite']);
}
} else {
setSelectedProfils(selectedProfils.filter(p => p !== 'DataIntelligence'));
}
}}
className="border-gray-300"
/>
<label htmlFor="dataintelligence" className="text-sm text-gray-700">DataIntelligence</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="expression"
checked={selectedProfils.includes('Expression')}
onCheckedChange={(checked) => {
if (checked) {
setSelectedProfils([...selectedProfils, 'Expression']);
} else {
setSelectedProfils(selectedProfils.filter(p => p !== 'Expression'));
}
}}
className="border-gray-300"
/>
<label htmlFor="expression" className="text-sm text-gray-700">Expression</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="mediation"
checked={selectedProfils.includes('Mediation')}
onCheckedChange={(checked) => {
if (checked) {
setSelectedProfils([...selectedProfils, 'Mediation']);
} else {
setSelectedProfils(selectedProfils.filter(p => p !== 'Mediation'));
}
}}
className="border-gray-300"
/>
<label htmlFor="mediation" className="text-sm text-gray-700">Mediation</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="investigation"
checked={selectedProfils.includes('Investigation')}
onCheckedChange={(checked) => {
if (checked) {
setSelectedProfils([...selectedProfils, 'Investigation']);
} else {
setSelectedProfils(selectedProfils.filter(p => p !== 'Investigation'));
}
}}
className="border-gray-300"
/>
<label htmlFor="investigation" className="text-sm text-gray-700">Investigation</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="coding"
checked={selectedProfils.includes('Coding')}
onCheckedChange={(checked) => {
if (checked) {
setSelectedProfils([...selectedProfils, 'Coding']);
// Auto-check Gite in services
if (!selectedServices.includes('Gite')) {
setSelectedServices([...selectedServices, 'Gite']);
}
} else {
setSelectedProfils(selectedProfils.filter(p => p !== 'Coding'));
}
}}
className="border-gray-300"
/>
<label htmlFor="coding" className="text-sm text-gray-700">Coding</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="lean"
checked={selectedProfils.includes('Lean')}
onCheckedChange={(checked) => {
if (checked) {
setSelectedProfils([...selectedProfils, 'Lean']);
} else {
setSelectedProfils(selectedProfils.filter(p => p !== 'Lean'));
}
}}
className="border-gray-300"
/>
<label htmlFor="lean" className="text-sm text-gray-700">Lean</label>
</div>
</div>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="attachments" className="space-y-6">
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Documents</label>
{/* For new missions */}
{!missionId ? (
<div className="space-y-4">
<div className="border rounded-md p-4 bg-white">
<h4 className="text-sm font-medium mb-3 text-gray-700">Ajouter des documents</h4>
<p className="text-xs text-gray-500 mb-3">Les documents seront téléchargés lors de la sauvegarde de la mission.</p>
{selectedAttachments.length > 0 && (
<div className="mb-4 border rounded-md p-3 bg-gray-50">
<h5 className="text-sm font-medium mb-2 text-gray-700">Selected Files ({selectedAttachments.length})</h5>
<ul className="divide-y divide-gray-200">
{selectedAttachments.map((file, index) => (
<li key={index} className="py-2 flex items-center justify-between">
<div className="flex items-center">
<File className="h-4 w-4 text-gray-500 mr-2" />
<span className="text-sm text-gray-700">{file.name}</span>
<span className="text-xs text-gray-500 ml-2">({(file.size / 1024).toFixed(1)} KB)</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedAttachments(prev => prev.filter((_, i) => i !== index));
}}
className="text-red-500 hover:text-red-700 hover:bg-red-50 h-7 w-7 p-0"
>
<X className="h-4 w-4" />
</Button>
</li>
))}
</ul>
</div>
)}
<div className="flex items-center justify-center w-full">
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<UploadCloud className="w-8 h-8 mb-3 text-gray-400" />
<p className="mb-2 text-sm text-gray-500">
<span className="font-semibold">Click to upload</span> or drag and drop
</p>
<p className="text-xs text-gray-500">PDF, DOC, DOCX, XLS, XLSX, JPG, JPEG, PNG</p>
</div>
<input
type="file"
className="hidden"
onChange={async (e) => {
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
// Add file to selectedAttachments for display
setSelectedAttachments(prev => [...prev, file]);
// Reset input so same file can be selected again
e.target.value = '';
}
}}
/>
</label>
</div>
</div>
</div>
) : (
/* For existing missions */
<AttachmentsList
missionId={missionId || ""}
allowUpload={true}
allowDelete={true}
/>
)}
</div>
</TabsContent>
<TabsContent value="skills" className="space-y-6">
<div>
<div className="flex justify-between mb-4">
<h3 className="text-lg font-medium text-gray-700">Compétences</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Checkbox id="skill-fullstack" className="border-gray-300" />
<label htmlFor="skill-fullstack" className="text-sm text-gray-700">Developper FullStack</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="skill-devops" className="border-gray-300" />
<label htmlFor="skill-devops" className="text-sm text-gray-700">DevOps</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="skill-design" className="border-gray-300" />
<label htmlFor="skill-design" className="text-sm text-gray-700">Design</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="skill-investigation" className="border-gray-300" />
<label htmlFor="skill-investigation" className="text-sm text-gray-700">Investigation & Documentation</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="skill-ml-ai" className="border-gray-300" />
<label htmlFor="skill-ml-ai" className="text-sm text-gray-700">Machine Learning & AI</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="skill-communication" className="border-gray-300" />
<label htmlFor="skill-communication" className="text-sm text-gray-700">Communication</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="skill-organisation" className="border-gray-300" />
<label htmlFor="skill-organisation" className="text-sm text-gray-700">Organisation</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="skill-support" className="border-gray-300" />
<label htmlFor="skill-support" className="text-sm text-gray-700">Support</label>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="membres" className="space-y-6">
<MissionMembersPanel
gardienDuTemps={gardienDuTemps}
gardienDeLaParole={gardienDeLaParole}
gardienDeLaMemoire={gardienDeLaMemoire}
volontaires={volontaires}
onGardienDuTempsChange={setGardienDuTemps}
onGardienDeLaParoleChange={setGardienDeLaParole}
onGardienDeLaMemoireChange={setGardienDeLaMemoire}
onVolontairesChange={setVolontaires}
/>
</TabsContent>
</Tabs>
<div className="mt-8 flex justify-between">
{!isFirstTab() && (
<Button
variant="outline"
className="text-gray-700 bg-white hover:bg-gray-50 border border-gray-300"
onClick={goToPreviousTab}
disabled={isSubmitting}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<polyline points="19 12 5 12"></polyline>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Retour
</Button>
)}
{isFirstTab() && <div></div>}
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
onClick={() => {
console.log('[BUTTON CLICK] Button clicked', {
activeTab,
isLastTab: activeTab === "membres",
isSubmitting
});
if (isLastTab()) {
console.log('[BUTTON CLICK] Calling handleSubmitMission');
handleSubmitMission();
} else {
console.log('[BUTTON CLICK] Calling goToNextTab instead');
goToNextTab();
}
}}
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<span className="animate-spin mr-2"></span>
{isLastTab() ? "Enregistrement..." : "Chargement..."}
</>
) : (
<>
{isLastTab() ? "Enregistrer" : "Suivant"}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="ml-2">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</>
)}
</Button>
</div>
</CardContent>
</Card>
</div>
);
}