1433 lines
68 KiB
TypeScript
1433 lines
68 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
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, Search, UserPlus, Users, PlusCircle, AlertCircle, Check, UploadCloud, File } from "lucide-react";
|
|
import { toast } from "../ui/use-toast";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger
|
|
} from "../ui/dropdown-menu";
|
|
import { FileUpload } from "./file-upload";
|
|
import { AttachmentsList } from "./attachments-list";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
// Define interfaces for user and group data
|
|
interface User {
|
|
id: string;
|
|
username: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string;
|
|
roles?: string[];
|
|
enabled?: boolean;
|
|
}
|
|
|
|
interface Group {
|
|
id: string;
|
|
name: string;
|
|
path: string;
|
|
membersCount: number;
|
|
}
|
|
|
|
// User role types in mission
|
|
type GuardienRole = 'temps' | 'parole' | 'memoire';
|
|
type UserRole = GuardienRole | 'volontaire';
|
|
|
|
export function MissionsAdminPanel() {
|
|
const router = useRouter();
|
|
const [selectedServices, setSelectedServices] = useState<string[]>([]);
|
|
const [selectedProfils, setSelectedProfils] = useState<string[]>([]);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [selectedTab, setSelectedTab] = useState<'users' | 'groups'>('users');
|
|
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<{
|
|
name?: string;
|
|
logo?: string;
|
|
oddScope?: string[];
|
|
niveau?: string;
|
|
intention?: string;
|
|
missionType?: string;
|
|
donneurDOrdre?: string;
|
|
projection?: string;
|
|
services?: string[];
|
|
participation?: string;
|
|
profils?: string[];
|
|
}>({});
|
|
|
|
// State for storing fetched data
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [groups, setGroups] = useState<Group[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Check if mission is valid (has all required guardiens)
|
|
const isMissionValid = gardienDuTemps !== null && gardienDeLaParole !== null && gardienDeLaMemoire !== null;
|
|
|
|
// Fetch users and groups on component mount
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
await Promise.all([fetchUsers(), fetchGroups()]);
|
|
} catch (error) {
|
|
console.error("Error fetching data:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, []);
|
|
|
|
// Function to fetch users from API
|
|
const fetchUsers = async () => {
|
|
try {
|
|
const response = await fetch("/api/users");
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch users");
|
|
}
|
|
const data = await response.json();
|
|
setUsers(data);
|
|
} catch (error) {
|
|
console.error("Error fetching users:", error);
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Erreur lors de la récupération des utilisateurs",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
// Function to fetch groups from API
|
|
const fetchGroups = async () => {
|
|
try {
|
|
const response = await fetch("/api/groups");
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch groups");
|
|
}
|
|
const data = await response.json();
|
|
|
|
// Fetch member counts for groups
|
|
const groupsWithCounts = await Promise.all(
|
|
(Array.isArray(data) ? data : []).map(async (group) => {
|
|
try {
|
|
const membersResponse = await fetch(`/api/groups/${group.id}/members`);
|
|
if (membersResponse.ok) {
|
|
const members = await membersResponse.json();
|
|
return {
|
|
...group,
|
|
membersCount: Array.isArray(members) ? members.length : 0
|
|
};
|
|
}
|
|
return {...group, membersCount: 0};
|
|
} catch (error) {
|
|
console.error(`Error fetching members for group ${group.id}:`, error);
|
|
return {...group, membersCount: 0};
|
|
}
|
|
})
|
|
);
|
|
|
|
setGroups(groupsWithCounts);
|
|
} catch (error) {
|
|
console.error("Error fetching groups:", error);
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Erreur lors de la récupération des groupes",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
// Filtered users based on search term
|
|
const filteredUsers = users.filter(user =>
|
|
(user.username?.toLowerCase() || "").includes(searchTerm.toLowerCase()) ||
|
|
(user.email?.toLowerCase() || "").includes(searchTerm.toLowerCase()) ||
|
|
(user.firstName?.toLowerCase() || "").includes(searchTerm.toLowerCase()) ||
|
|
(user.lastName?.toLowerCase() || "").includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
// Filtered groups based on search term
|
|
const filteredGroups = groups.filter(group =>
|
|
(group.name?.toLowerCase() || "").includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
// Function to check if a user is already assigned to any role
|
|
const isUserAssigned = (userId: string) => {
|
|
return gardienDuTemps === userId ||
|
|
gardienDeLaParole === userId ||
|
|
gardienDeLaMemoire === userId ||
|
|
volontaires.includes(userId);
|
|
};
|
|
|
|
// Function to get user's roles (can now have multiple)
|
|
const getUserRoles = (userId: string): UserRole[] => {
|
|
const roles: UserRole[] = [];
|
|
if (gardienDuTemps === userId) roles.push('temps');
|
|
if (gardienDeLaParole === userId) roles.push('parole');
|
|
if (gardienDeLaMemoire === userId) roles.push('memoire');
|
|
if (volontaires.includes(userId)) roles.push('volontaire');
|
|
return roles;
|
|
};
|
|
|
|
// For backwards compatibility with existing code
|
|
const getUserRole = (userId: string): UserRole | null => {
|
|
const roles = getUserRoles(userId);
|
|
return roles.length > 0 ? roles[0] : null;
|
|
};
|
|
|
|
// Function to get role display name
|
|
const getRoleDisplayName = (role: UserRole | null): string => {
|
|
switch(role) {
|
|
case 'temps': return "Gardien du Temps";
|
|
case 'parole': return "Gardien de la Parole";
|
|
case 'memoire': return "Gardien de la Mémoire";
|
|
case 'volontaire': return "Volontaire";
|
|
default: return "";
|
|
}
|
|
};
|
|
|
|
// Function to assign a user to a specific guardian role
|
|
const assignGuardienRole = (userId: string, role: GuardienRole) => {
|
|
// Only remove from volunteers if they're currently a volunteer
|
|
if (volontaires.includes(userId)) {
|
|
setVolontaires(prev => prev.filter(id => id !== userId));
|
|
}
|
|
|
|
// Assign to new role
|
|
if (role === 'temps') {
|
|
setGardienDuTemps(userId);
|
|
} else if (role === 'parole') {
|
|
setGardienDeLaParole(userId);
|
|
} else if (role === 'memoire') {
|
|
setGardienDeLaMemoire(userId);
|
|
}
|
|
|
|
toast({
|
|
title: "Rôle assigné",
|
|
description: `L'utilisateur a été assigné comme ${getRoleDisplayName(role)}`,
|
|
});
|
|
};
|
|
|
|
// Function to assign a user as volunteer
|
|
const assignVolontaire = (userId: string) => {
|
|
// Remove from any existing role first
|
|
removeUserFromAllRoles(userId);
|
|
|
|
// Add to volunteers
|
|
setVolontaires(prev => [...prev, userId]);
|
|
|
|
toast({
|
|
title: "Rôle assigné",
|
|
description: "L'utilisateur a été assigné comme Volontaire",
|
|
});
|
|
};
|
|
|
|
// Function to remove a user from all roles
|
|
const removeUserFromAllRoles = (userId: string) => {
|
|
if (gardienDuTemps === userId) setGardienDuTemps(null);
|
|
if (gardienDeLaParole === userId) setGardienDeLaParole(null);
|
|
if (gardienDeLaMemoire === userId) setGardienDeLaMemoire(null);
|
|
if (volontaires.includes(userId)) {
|
|
setVolontaires(prev => prev.filter(id => id !== userId));
|
|
}
|
|
};
|
|
|
|
// Check if all guardian roles are filled
|
|
const areAllGuardiensFilled = (): boolean => {
|
|
return gardienDuTemps !== null && gardienDeLaParole !== null && gardienDeLaMemoire !== null;
|
|
};
|
|
|
|
// Function to fetch group members
|
|
const fetchGroupMembers = async (groupId: string) => {
|
|
try {
|
|
const response = await fetch(`/api/groups/${groupId}/members`);
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch group members");
|
|
}
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error(`Error fetching members for group ${groupId}:`, error);
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Erreur lors de la récupération des membres du groupe",
|
|
variant: "destructive",
|
|
});
|
|
return [];
|
|
}
|
|
};
|
|
|
|
// Handler for viewing group members
|
|
const handleViewGroupMembers = async (groupId: string, groupName: string) => {
|
|
try {
|
|
setLoading(true);
|
|
const members = await fetchGroupMembers(groupId);
|
|
|
|
// Update the users list with the group members and switch to users tab
|
|
if (Array.isArray(members) && members.length > 0) {
|
|
setUsers(members);
|
|
setSelectedTab('users');
|
|
setSearchTerm(''); // Clear any existing search
|
|
|
|
toast({
|
|
title: `Membres de ${groupName}`,
|
|
description: `${members.length} membres trouvés et affichés ci-dessous`,
|
|
});
|
|
} else {
|
|
toast({
|
|
title: `Membres de ${groupName}`,
|
|
description: "Aucun membre trouvé dans ce groupe",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Error handling group members:", error);
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Erreur lors de l'affichage des membres du groupe",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
};
|
|
|
|
// Handle mission submission
|
|
const handleSubmitMission = async () => {
|
|
console.log('Starting mission submission...');
|
|
console.log('Current mission data:', JSON.stringify(missionData, null, 2));
|
|
console.log('Selected services:', selectedServices);
|
|
console.log('Guardians:', {
|
|
gardienDuTemps,
|
|
gardienDeLaParole,
|
|
gardienDeLaMemoire
|
|
});
|
|
console.log('Volunteers:', volontaires);
|
|
|
|
if (!validateMission()) {
|
|
console.log('Mission validation failed');
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
// Format the data before sending
|
|
const formattedData = {
|
|
name: missionData.name,
|
|
oddScope: Array.isArray(missionData.oddScope) ? missionData.oddScope : [missionData.oddScope],
|
|
niveau: missionData.niveau,
|
|
intention: missionData.intention,
|
|
missionType: missionData.missionType,
|
|
donneurDOrdre: missionData.donneurDOrdre,
|
|
projection: missionData.projection,
|
|
participation: missionData.participation,
|
|
services: selectedServices,
|
|
profils: selectedProfils,
|
|
guardians: {
|
|
'gardien-temps': gardienDuTemps,
|
|
'gardien-parole': gardienDeLaParole,
|
|
'gardien-memoire': gardienDeLaMemoire
|
|
},
|
|
volunteers: volontaires,
|
|
logo: selectedLogoFile,
|
|
attachments: selectedAttachments
|
|
};
|
|
|
|
console.log('Submitting mission data:', JSON.stringify(formattedData, null, 2));
|
|
|
|
const response = await fetch('/api/missions', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(formattedData),
|
|
});
|
|
|
|
console.log('Response status:', response.status);
|
|
const data = await response.json();
|
|
console.log('Response data:', JSON.stringify(data, null, 2));
|
|
|
|
if (!response.ok) {
|
|
console.error('Error response:', data);
|
|
throw new Error(data.error || 'Failed to create mission');
|
|
}
|
|
|
|
toast({
|
|
title: "Mission créée avec succès",
|
|
description: "La mission a été créée et les intégrations sont en cours.",
|
|
});
|
|
|
|
router.push('/missions');
|
|
} catch (error) {
|
|
console.error('Error creating mission:', error);
|
|
toast({
|
|
title: "Erreur",
|
|
description: error instanceof Error ? error.message : "Une erreur est survenue lors de la création de la mission",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
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">General</TabsTrigger>
|
|
<TabsTrigger value="details" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white">Details</TabsTrigger>
|
|
<TabsTrigger value="attachments" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white">Attachments</TabsTrigger>
|
|
<TabsTrigger value="skills" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white">Skills</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"
|
|
missionId={missionId || ""}
|
|
isNewMission={!missionId}
|
|
onFileSelect={(file) => {
|
|
// Store the selected file in state for later upload
|
|
setSelectedLogoFile(file);
|
|
console.log('Logo file selected for later upload:', file.name);
|
|
}}
|
|
onUploadComplete={(data) => {
|
|
// Handle logo upload complete
|
|
if (data?.filePath) {
|
|
setMissionData(prev => ({
|
|
...prev,
|
|
logo: data.filePath
|
|
}));
|
|
}
|
|
}}
|
|
/>
|
|
</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="Select duration" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="short">Short Term (< 1 month)</SelectItem>
|
|
<SelectItem value="medium">Medium Term (1-3 months)</SelectItem>
|
|
<SelectItem value="long">Long Term (> 3 months)</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="Select participation" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="volontaire">Volontaire</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']);
|
|
} 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']);
|
|
} 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">Attachements</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">Add Attachments</h4>
|
|
<p className="text-xs text-gray-500 mb-3">Attachments will be uploaded when you save the 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={(e) => {
|
|
if (e.target.files && e.target.files.length > 0) {
|
|
const file = e.target.files[0];
|
|
setSelectedAttachments(prev => [...prev, file]);
|
|
}
|
|
}}
|
|
/>
|
|
</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">Skills</h3>
|
|
<Button variant="outline" size="sm" className="bg-white text-gray-700 border-gray-300 hover:bg-gray-50">Select / Deselect All</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="photoshop" className="border-gray-300" />
|
|
<label htmlFor="photoshop" className="text-sm text-gray-700">Adobe Photoshop</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="xd" className="border-gray-300" />
|
|
<label htmlFor="xd" className="text-sm text-gray-700">Adobe XD</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="android" className="border-gray-300" />
|
|
<label htmlFor="android" className="text-sm text-gray-700">Android Developer</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="artist" className="border-gray-300" />
|
|
<label htmlFor="artist" className="text-sm text-gray-700">Artist</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="computer" className="border-gray-300" />
|
|
<label htmlFor="computer" className="text-sm text-gray-700">Computer</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="developer" className="border-gray-300" />
|
|
<label htmlFor="developer" className="text-sm text-gray-700">Developer</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="frontend" className="border-gray-300" />
|
|
<label htmlFor="frontend" className="text-sm text-gray-700">Front end Developer</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="ios" className="border-gray-300" />
|
|
<label htmlFor="ios" className="text-sm text-gray-700">iOS Developer</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="support" className="border-gray-300" />
|
|
<label htmlFor="support" className="text-sm text-gray-700">Support Agent</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox id="writer" className="border-gray-300" />
|
|
<label htmlFor="writer" className="text-sm text-gray-700">Writer</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="membres" className="space-y-6">
|
|
<div>
|
|
<div className="mb-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg font-medium text-gray-700">Les Gardiens de l'Intention</h3>
|
|
|
|
<div className="flex items-center">
|
|
{!isMissionValid && (
|
|
<div className="flex items-center text-amber-600 mr-3 text-sm">
|
|
<AlertCircle size={16} className="mr-1" />
|
|
Les 3 gardiens doivent être assignés
|
|
</div>
|
|
)}
|
|
{isMissionValid && (
|
|
<div className="flex items-center text-green-600 mr-3 text-sm">
|
|
<Check size={16} className="mr-1" />
|
|
Tous les gardiens sont assignés
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{/* Gardien du Temps */}
|
|
<div className="border rounded-md p-4 bg-white">
|
|
<div className="flex justify-between items-center mb-2">
|
|
<h4 className="font-medium text-gray-800">Gardien du Temps</h4>
|
|
{gardienDuTemps && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => removeUserFromAllRoles(gardienDuTemps)}
|
|
className="text-red-600 hover:bg-red-50 hover:text-red-700 border-red-200 h-8 bg-white"
|
|
disabled={loading}
|
|
>
|
|
<X size={16} className="mr-1" />
|
|
Supprimer
|
|
</Button>
|
|
)}
|
|
</div>
|
|
{loading ? (
|
|
<div className="flex items-center py-2 px-3 bg-gray-50 border border-gray-200 rounded-md">
|
|
<div className="animate-pulse w-full flex items-center">
|
|
<div className="h-10 w-10 bg-gray-300 rounded-full mr-3"></div>
|
|
<div className="flex-1 space-y-2">
|
|
<div className="h-3 bg-gray-300 rounded w-1/3"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
gardienDuTemps ? (
|
|
<div className="bg-blue-50 border border-blue-100 rounded-md p-3">
|
|
{(() => {
|
|
const user = users.find(u => u.id === gardienDuTemps);
|
|
return user ? (
|
|
<div className="flex items-center">
|
|
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-medium mr-3">
|
|
{user.firstName?.[0] || ""}{user.lastName?.[0] || ""}
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{user.firstName} {user.lastName}</div>
|
|
<div className="text-sm text-gray-500">{user.email}</div>
|
|
</div>
|
|
</div>
|
|
) : "Utilisateur non trouvé";
|
|
})()}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center text-gray-500 bg-gray-50 border border-gray-200 rounded-md py-2 px-3">
|
|
<div className="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center mr-3">
|
|
<Users size={14} className="text-gray-400" />
|
|
</div>
|
|
<span className="text-sm">Aucun utilisateur sélectionné</span>
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
|
|
{/* Gardien de la Parole */}
|
|
<div className="border rounded-md p-4 bg-white">
|
|
<div className="flex justify-between items-center mb-2">
|
|
<h4 className="font-medium text-gray-800">Gardien de la Parole</h4>
|
|
{gardienDeLaParole && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => removeUserFromAllRoles(gardienDeLaParole)}
|
|
className="text-red-600 hover:bg-red-50 hover:text-red-700 border-red-200 h-8 bg-white"
|
|
disabled={loading}
|
|
>
|
|
<X size={16} className="mr-1" />
|
|
Supprimer
|
|
</Button>
|
|
)}
|
|
</div>
|
|
{loading ? (
|
|
<div className="flex items-center py-2 px-3 bg-gray-50 border border-gray-200 rounded-md">
|
|
<div className="animate-pulse w-full flex items-center">
|
|
<div className="h-10 w-10 bg-gray-300 rounded-full mr-3"></div>
|
|
<div className="flex-1 space-y-2">
|
|
<div className="h-3 bg-gray-300 rounded w-1/3"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
gardienDeLaParole ? (
|
|
<div className="bg-blue-50 border border-blue-100 rounded-md p-3">
|
|
{(() => {
|
|
const user = users.find(u => u.id === gardienDeLaParole);
|
|
return user ? (
|
|
<div className="flex items-center">
|
|
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-medium mr-3">
|
|
{user.firstName?.[0] || ""}{user.lastName?.[0] || ""}
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{user.firstName} {user.lastName}</div>
|
|
<div className="text-sm text-gray-500">{user.email}</div>
|
|
</div>
|
|
</div>
|
|
) : "Utilisateur non trouvé";
|
|
})()}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center text-gray-500 bg-gray-50 border border-gray-200 rounded-md py-2 px-3">
|
|
<div className="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center mr-3">
|
|
<Users size={14} className="text-gray-400" />
|
|
</div>
|
|
<span className="text-sm">Aucun utilisateur sélectionné</span>
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
|
|
{/* Gardien de la Mémoire */}
|
|
<div className="border rounded-md p-4 bg-white">
|
|
<div className="flex justify-between items-center mb-2">
|
|
<h4 className="font-medium text-gray-800">Gardien de la Mémoire</h4>
|
|
{gardienDeLaMemoire && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => removeUserFromAllRoles(gardienDeLaMemoire)}
|
|
className="text-red-600 hover:bg-red-50 hover:text-red-700 border-red-200 h-8 bg-white"
|
|
disabled={loading}
|
|
>
|
|
<X size={16} className="mr-1" />
|
|
Supprimer
|
|
</Button>
|
|
)}
|
|
</div>
|
|
{loading ? (
|
|
<div className="flex items-center py-2 px-3 bg-gray-50 border border-gray-200 rounded-md">
|
|
<div className="animate-pulse w-full flex items-center">
|
|
<div className="h-10 w-10 bg-gray-300 rounded-full mr-3"></div>
|
|
<div className="flex-1 space-y-2">
|
|
<div className="h-3 bg-gray-300 rounded w-1/3"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
gardienDeLaMemoire ? (
|
|
<div className="bg-blue-50 border border-blue-100 rounded-md p-3">
|
|
{(() => {
|
|
const user = users.find(u => u.id === gardienDeLaMemoire);
|
|
return user ? (
|
|
<div className="flex items-center">
|
|
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-medium mr-3">
|
|
{user.firstName?.[0] || ""}{user.lastName?.[0] || ""}
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{user.firstName} {user.lastName}</div>
|
|
<div className="text-sm text-gray-500">{user.email}</div>
|
|
</div>
|
|
</div>
|
|
) : "Utilisateur non trouvé";
|
|
})()}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center text-gray-500 bg-gray-50 border border-gray-200 rounded-md py-2 px-3">
|
|
<div className="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center mr-3">
|
|
<Users size={14} className="text-gray-400" />
|
|
</div>
|
|
<span className="text-sm">Aucun utilisateur sélectionné</span>
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white border rounded-md p-4">
|
|
<div className="mb-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h4 className="font-medium text-gray-800">Sélectionner des membres</h4>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
variant={selectedTab === 'users' ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setSelectedTab('users')}
|
|
className={selectedTab === 'users'
|
|
? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
: 'text-gray-700 bg-white hover:bg-gray-50 border border-gray-300'}
|
|
disabled={loading}
|
|
>
|
|
<Users size={16} className="mr-1" />
|
|
Utilisateurs
|
|
</Button>
|
|
<Button
|
|
variant={selectedTab === 'groups' ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setSelectedTab('groups')}
|
|
className={selectedTab === 'groups'
|
|
? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
: 'text-gray-700 bg-white hover:bg-gray-50 border border-gray-300'}
|
|
disabled={loading}
|
|
>
|
|
<Users size={16} className="mr-1" />
|
|
Groupes
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<h5 className="text-sm font-medium text-gray-700 mb-2">Volontaires ({volontaires.length})</h5>
|
|
{volontaires.length > 0 ? (
|
|
<div className="flex flex-wrap gap-2 mb-3">
|
|
{volontaires.map(userId => {
|
|
const user = users.find(u => u.id === userId);
|
|
if (!user) return null;
|
|
return (
|
|
<Badge key={userId} className="bg-gray-100 text-gray-800 hover:bg-gray-200 px-2 py-1 flex items-center">
|
|
{user.firstName} {user.lastName}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeUserFromAllRoles(userId)}
|
|
className="ml-1 h-5 w-5 p-0 text-gray-500 hover:text-red-600 hover:bg-transparent"
|
|
>
|
|
<X size={12} />
|
|
</Button>
|
|
</Badge>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-sm text-gray-500 mb-3">Aucun volontaire assigné</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="relative mb-4">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
|
|
<Input
|
|
type="text"
|
|
placeholder={`Rechercher ${selectedTab === 'users' ? 'un utilisateur' : 'un groupe'}...`}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-9 bg-white text-gray-900 border-gray-300"
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="border rounded-md p-6 flex flex-col items-center justify-center text-gray-500">
|
|
<div className="animate-pulse space-y-4 w-full">
|
|
{[1, 2, 3].map((i) => (
|
|
<div key={i} className="flex items-center py-3 px-4">
|
|
<div className="h-10 w-10 bg-gray-300 rounded-full mr-3"></div>
|
|
<div className="flex-1 space-y-2">
|
|
<div className="h-3 bg-gray-300 rounded w-1/3"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
|
|
</div>
|
|
<div className="h-7 w-16 bg-gray-300 rounded"></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="border rounded-md max-h-[300px] overflow-y-auto">
|
|
{selectedTab === 'users' ? (
|
|
filteredUsers.length > 0 ? (
|
|
<div className="divide-y divide-gray-200">
|
|
{filteredUsers.map(user => (
|
|
<div key={user.id} className="p-3 hover:bg-gray-50 flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<div className="h-10 w-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 font-medium mr-3">
|
|
{user.firstName?.[0] || ""}{user.lastName?.[0] || ""}
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{user.firstName} {user.lastName}</div>
|
|
<div className="text-sm text-gray-500">{user.email}</div>
|
|
{isUserAssigned(user.id) && (
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
{getUserRoles(user.id).map((role) => (
|
|
<Badge
|
|
key={role}
|
|
className={`px-1.5 py-0.5 text-xs ${
|
|
role === 'volontaire'
|
|
? 'bg-gray-100 text-gray-800'
|
|
: 'bg-blue-100 text-blue-800'
|
|
}`}
|
|
>
|
|
{getRoleDisplayName(role)}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* User role controls always show dropdown */}
|
|
<div className="flex items-center">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-blue-600 bg-white hover:bg-blue-50 hover:text-blue-700 border-blue-200 h-8 mr-2"
|
|
disabled={loading}
|
|
>
|
|
<UserPlus size={16} className="mr-1" />
|
|
Ajouter rôle
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="bg-white border border-gray-200">
|
|
<DropdownMenuItem
|
|
onClick={() => assignGuardienRole(user.id, 'temps')}
|
|
className="cursor-pointer"
|
|
>
|
|
Gardien du Temps
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => assignGuardienRole(user.id, 'parole')}
|
|
className="cursor-pointer"
|
|
>
|
|
Gardien de la Parole
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => assignGuardienRole(user.id, 'memoire')}
|
|
className="cursor-pointer"
|
|
>
|
|
Gardien de la Mémoire
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => assignVolontaire(user.id)}
|
|
className="text-gray-700 bg-white hover:bg-gray-50 border-gray-300 h-8"
|
|
disabled={loading}
|
|
>
|
|
<PlusCircle size={16} className="mr-1" />
|
|
Volontaire
|
|
</Button>
|
|
|
|
{isUserAssigned(user.id) && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => removeUserFromAllRoles(user.id)}
|
|
className="ml-2 text-red-600 hover:bg-red-50 hover:text-red-700 border-red-200 h-8 bg-white"
|
|
disabled={loading}
|
|
>
|
|
<X size={14} className="mr-1" />
|
|
Supprimer
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="p-4 text-center text-gray-500">
|
|
Aucun utilisateur trouvé
|
|
</div>
|
|
)
|
|
) : (
|
|
filteredGroups.length > 0 ? (
|
|
<div className="divide-y divide-gray-200">
|
|
{filteredGroups.map(group => (
|
|
<div key={group.id} className="p-3 hover:bg-gray-50 flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<div className="h-10 w-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 font-medium mr-3">
|
|
<Users size={16} />
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{group.name}</div>
|
|
<div className="text-sm text-gray-500">{group.membersCount} membres</div>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleViewGroupMembers(group.id, group.name)}
|
|
className="text-blue-600 bg-white hover:bg-blue-50 hover:text-blue-700 border-blue-200 h-8"
|
|
disabled={loading}
|
|
>
|
|
<Users size={16} className="mr-1" />
|
|
Voir membres
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="p-4 text-center text-gray-500">
|
|
Aucun groupe trouvé
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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={() => {
|
|
if (isLastTab()) {
|
|
handleSubmitMission();
|
|
} else {
|
|
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>
|
|
);
|
|
}
|