diff --git a/.DS_Store b/.DS_Store index 354e791..03adda7 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/MISSIONS_CODE_REVIEW.md b/MISSIONS_CODE_REVIEW.md new file mode 100644 index 0000000..cf5d38a --- /dev/null +++ b/MISSIONS_CODE_REVIEW.md @@ -0,0 +1,503 @@ +# Analyse approfondie du systĂšme Missions - Code Review Senior + +## 📋 Vue d'ensemble + +Ce document prĂ©sente une analyse complĂšte du systĂšme de gestion des missions, incluant la page de liste, les dĂ©tails de mission, et l'architecture backend associĂ©e. + +--- + +## đŸ—ïž Architecture gĂ©nĂ©rale + +### Structure des fichiers + +``` +app/ +├── missions/ +│ ├── page.tsx # Page principale de liste des missions +│ ├── layout.tsx # Layout avec sidebar CAP +│ ├── new/ +│ │ └── page.tsx # CrĂ©ation de nouvelle mission +│ └── [missionId]/ +│ ├── page.tsx # Page de dĂ©tails de mission +│ └── edit/ +│ └── page.tsx # Édition de mission +│ +├── api/ +│ └── missions/ +│ ├── route.ts # GET/POST missions +│ ├── [missionId]/ +│ │ ├── route.ts # GET/PUT/DELETE mission spĂ©cifique +│ │ ├── close/route.ts # ClĂŽture de mission +│ │ └── generate-plan/ # GĂ©nĂ©ration plan d'action IA +│ └── ... +│ +components/ +└── missions/ + ├── missions-frame.tsx # Iframe wrapper + ├── missions-admin-panel.tsx # Panel de crĂ©ation/Ă©dition + └── ... +``` + +--- + +## 📄 Page de liste des missions (`app/missions/page.tsx`) + +### Points forts ✅ + +1. **Interface utilisateur claire** + - Design en grille responsive (1/2/3 colonnes) + - Cartes de mission bien structurĂ©es + - Indicateurs visuels pour missions clĂŽturĂ©es + - Recherche en temps rĂ©el + +2. **Gestion d'Ă©tat** + - Utilisation appropriĂ©e de `useState` et `useEffect` + - Gestion des Ă©tats de chargement + - Filtrage cĂŽtĂ© client efficace + +3. **Affichage des donnĂ©es** + - Logos avec fallback gracieux + - Badges ODD avec icĂŽnes + - Affichage conditionnel des services + - Formatage des dates en français + +### Points d'amĂ©lioration 🔧 + +1. **Performance** + ```typescript + // ❌ ProblĂšme: Filtrage cĂŽtĂ© client uniquement + const filteredMissions = missions.filter(mission => + mission.name.toLowerCase().includes(searchTerm.toLowerCase()) || ... + ); + + // ✅ Suggestion: Pagination et recherche cĂŽtĂ© serveur + // Utiliser les query params dans l'API + ``` + +2. **Gestion d'erreurs** + ```typescript + // ⚠ Actuel: Toast gĂ©nĂ©rique + toast({ + title: "Erreur", + description: "Impossible de charger les missions", + variant: "destructive", + }); + + // ✅ Suggestion: Messages d'erreur plus spĂ©cifiques + // + Retry automatique pour erreurs rĂ©seau + ``` + +3. **Console.log en production** + ```typescript + // ❌ Lignes 59, 199-203: console.log en production + console.log("Mission data with intention:", data.missions); + + // ✅ Suggestion: Utiliser un logger conditionnel + if (process.env.NODE_ENV === 'development') { + console.log(...); + } + ``` + +4. **AccessibilitĂ©** + - Manque d'attributs ARIA sur les cartes + - Navigation clavier non optimisĂ©e + - Pas de skip links + +--- + +## 📄 Page de dĂ©tails de mission (`app/missions/[missionId]/page.tsx`) + +### Points forts ✅ + +1. **Architecture en onglets** + - Organisation claire: GĂ©nĂ©ral, Plan d'actions, Équipe, Ressources + - Compteurs visuels sur les onglets (Ă©quipe, documents) + - Navigation intuitive + +2. **FonctionnalitĂ©s avancĂ©es** + - GĂ©nĂ©ration de plan d'action par IA (N8N) + - Édition inline du plan avec sauvegarde + - Gestion des gardiens de l'intention + - ClĂŽture de mission avec intĂ©gration N8N + +3. **Gestion d'Ă©tat complexe** + - Suivi des modifications non sauvegardĂ©es + - États de chargement multiples (generating, saving, deleting, closing) + - Synchronisation avec le backend + +### Points d'amĂ©lioration critiques 🔮 + +1. **SĂ©curitĂ© - Validation cĂŽtĂ© client uniquement** + ```typescript + // ⚠ Ligne 192: Confirmation simple avec confirm() + if (!confirm("Êtes-vous sĂ»r de vouloir supprimer cette mission ?")) { + return; + } + + // ✅ Suggestion: Modal de confirmation avec dĂ©tails + // + VĂ©rification des permissions cĂŽtĂ© serveur (dĂ©jĂ  fait ✅) + ``` + +2. **Gestion des erreurs rĂ©seau** + ```typescript + // ⚠ Pas de retry automatique + // Pas de gestion des timeouts + // Pas de fallback si l'API est down + + // ✅ Suggestion: ImplĂ©menter retry avec exponential backoff + // + Cache local pour donnĂ©es critiques + ``` + +3. **Performance - Re-renders inutiles** + ```typescript + // ⚠ Ligne 112-116: useEffect qui se dĂ©clenche Ă  chaque changement + useEffect(() => { + if (mission) { + setIsPlanModified(editedPlan !== (mission.actionPlan || "")); + } + }, [editedPlan, mission]); + + // ✅ Suggestion: Utiliser useMemo pour Ă©viter recalculs + const isPlanModified = useMemo(() => { + return mission ? editedPlan !== (mission.actionPlan || "") : false; + }, [editedPlan, mission?.actionPlan]); + ``` + +4. **Textarea auto-resize - Code fragile** + ```typescript + // ⚠ Lignes 676-688: Manipulation directe du DOM + e.target.style.height = 'auto'; + e.target.style.height = e.target.scrollHeight + 'px'; + + // ✅ Suggestion: Utiliser une librairie dĂ©diĂ©e (react-textarea-autosize) + // ou un hook personnalisĂ© rĂ©utilisable + ``` + +5. **Duplication de code** + ```typescript + // ⚠ Fonctions helper dupliquĂ©es entre page.tsx et [missionId]/page.tsx + // getMissionTypeLabel, getDurationLabel, getNiveauLabel, etc. + + // ✅ Suggestion: Extraire dans lib/mission-helpers.ts + ``` + +--- + +## 🔌 API Routes - Analyse Backend + +### `app/api/missions/route.ts` (GET/POST) + +#### Points forts ✅ + +1. **SĂ©curitĂ©** + - VĂ©rification d'authentification systĂ©matique + - Validation des champs requis + - Gestion des permissions + +2. **Gestion des fichiers** + - Upload vers Minio/S3 bien structurĂ© + - VĂ©rification d'existence des fichiers avant N8N + - Cleanup en cas d'erreur (lignes 460-474) + +3. **IntĂ©gration N8N** + - Workflow asynchrone pour crĂ©ation + - Gestion des erreurs non-bloquantes + - Logging dĂ©taillĂ© + +#### Points d'amĂ©lioration 🔧 + +1. **Transaction database** + ```typescript + // ⚠ Pas de transaction Prisma + const mission = await prisma.mission.create({...}); + await prisma.missionUser.createMany({...}); + + // ✅ Suggestion: Utiliser $transaction pour atomicitĂ© + await prisma.$transaction(async (tx) => { + const mission = await tx.mission.create({...}); + await tx.missionUser.createMany({...}); + return mission; + }); + ``` + +2. **Validation des donnĂ©es** + ```typescript + // ⚠ Validation basique (lignes 230-235) + if (!body.name || !body.oddScope) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + } + + // ✅ Suggestion: Utiliser Zod ou Yup pour validation stricte + const MissionSchema = z.object({ + name: z.string().min(3).max(100), + oddScope: z.array(z.string().regex(/^odd-\d+$/)), + // ... + }); + ``` + +3. **Gestion des erreurs N8N** + ```typescript + // ⚠ Ligne 439: Erreur N8N bloque la crĂ©ation + if (!workflowResult.success) { + throw new Error(workflowResult.error || 'N8N workflow failed'); + } + + // ✅ Suggestion: Mode "best effort" - crĂ©er la mission mĂȘme si N8N Ă©choue + // + Queue de retry pour N8N (BullMQ, etc.) + ``` + +### `app/api/missions/[missionId]/route.ts` (GET/PUT/DELETE) + +#### Points forts ✅ + +1. **DELETE bien implĂ©mentĂ©** + - Cleanup Minio avant suppression DB + - IntĂ©gration N8N pour rollback + - Gestion des erreurs non-bloquantes + +2. **Permissions granulaires** + - VĂ©rification crĂ©ateur/admin pour DELETE + - Gardiens peuvent modifier (PUT) + +#### Points d'amĂ©lioration 🔧 + +1. **GET - Performance** + ```typescript + // ⚠ Ligne 38: findFirst au lieu de findUnique + const mission = await (prisma as any).mission.findFirst({ + where: { + id: missionId, + OR: [ + { creatorId: userId }, + { missionUsers: { some: { userId } } } + ] + }, + // ... + }); + + // ✅ Suggestion: findUnique + vĂ©rification permissions sĂ©parĂ©e + // Plus performant avec index sur id + ``` + +2. **PUT - Validation partielle** + ```typescript + // ⚠ Pas de validation des donnĂ©es mises Ă  jour + // Pas de vĂ©rification de cohĂ©rence (ex: oddScope doit ĂȘtre array) + + // ✅ Suggestion: Validation stricte avec schĂ©ma + ``` + +--- + +## 🎹 Composants UI + +### `components/missions/missions-admin-panel.tsx` + +#### Points forts ✅ + +1. **Interface complĂšte** + - Formulaire multi-onglets bien organisĂ© + - Gestion des gardiens et volontaires + - Upload de fichiers intĂ©grĂ© + +2. **UX soignĂ©e** + - Validation en temps rĂ©el + - Indicateurs visuels de progression + - Messages d'erreur contextuels + +#### Points d'amĂ©lioration critiques 🔮 + +1. **Fichier trop volumineux (1570 lignes)** + ```typescript + // ❌ Un seul composant fait tout + // Difficile Ă  maintenir, tester, et rĂ©utiliser + + // ✅ Suggestion: DĂ©couper en sous-composants + // - MissionGeneralForm + // - MissionDetailsForm + // - MissionAttachmentsForm + // - MissionMembersForm + // - MissionSkillsForm + ``` + +2. **Gestion d'Ă©tat complexe** + ```typescript + // ⚠ Trop de useState (15+) + const [selectedServices, setSelectedServices] = useState([]); + const [selectedProfils, setSelectedProfils] = useState([]); + // ... 13 autres + + // ✅ Suggestion: Utiliser useReducer ou Zustand + const [state, dispatch] = useReducer(missionReducer, initialState); + ``` + +3. **Logique mĂ©tier dans le composant** + ```typescript + // ⚠ Lignes 400-408: Conversion base64 dans le composant + const convertFileToBase64 = (file: File): Promise => { + // ... + }; + + // ✅ Suggestion: Extraire dans lib/file-utils.ts + ``` + +4. **Console.log en production** + ```typescript + // ❌ Lignes 412, 422, 428, 451, 465, 492, 504, 514, 541, 559 + // Trop de logs de debug + + // ✅ Suggestion: Logger conditionnel ou supprimer + ``` + +--- + +## 🔄 Flux de donnĂ©es + +### CrĂ©ation de mission + +``` +1. User remplit formulaire (missions-admin-panel.tsx) + ↓ +2. POST /api/missions + ↓ +3. CrĂ©ation DB (Prisma) + ↓ +4. Upload fichiers (Minio) + ↓ +5. VĂ©rification fichiers + ↓ +6. Trigger N8N workflow + ↓ +7. N8N crĂ©e intĂ©grations (Gitea, Leantime, etc.) + ↓ +8. Callback /api/missions/mission-created + ↓ +9. Mise Ă  jour mission avec IDs externes +``` + +**ProblĂšme potentiel**: Si N8N Ă©choue aprĂšs crĂ©ation DB, la mission existe sans intĂ©grations. + +**Solution**: Queue de retry ou mode "best effort" avec notification. + +### Affichage de mission + +``` +1. GET /api/missions/[missionId] + ↓ +2. Prisma query avec includes + ↓ +3. GĂ©nĂ©ration URLs publiques (logo, attachments) + ↓ +4. Affichage dans page.tsx +``` + +**Optimisation possible**: Cache Redis pour missions frĂ©quemment consultĂ©es. + +--- + +## 🐛 Bugs potentiels identifiĂ©s + +1. **Race condition sur plan d'action** + ```typescript + // Si l'utilisateur modifie pendant la gĂ©nĂ©ration + // Les modifications peuvent ĂȘtre Ă©crasĂ©es + ``` + +2. **Memory leak potentiel** + ```typescript + // Textarea auto-resize avec ref callback + // Pas de cleanup dans useEffect + ``` + +3. **Type safety** + ```typescript + // Utilisation de (prisma as any) dans plusieurs endroits + // Indique que le schema Prisma n'est pas Ă  jour + ``` + +--- + +## 📊 MĂ©triques de code + +### ComplexitĂ© cyclomatique + +- `missions-admin-panel.tsx`: **TrĂšs Ă©levĂ©e** (>50) +- `[missionId]/page.tsx`: **ÉlevĂ©e** (~30) +- `page.tsx`: **Moyenne** (~15) + +### Taille des fichiers + +- `missions-admin-panel.tsx`: **1570 lignes** ⚠ +- `[missionId]/page.tsx`: **920 lignes** ⚠ +- `route.ts` (POST): **480 lignes** ⚠ + +**Recommandation**: DĂ©couper les fichiers >500 lignes. + +--- + +## ✅ Recommandations prioritaires + +### 🔮 Critique (À faire immĂ©diatement) + +1. **SĂ©curitĂ©** + - Ajouter validation stricte avec Zod + - ImplĂ©menter rate limiting sur API + - Ajouter CSRF protection + +2. **Performance** + - ImplĂ©menter pagination cĂŽtĂ© serveur + - Ajouter cache Redis + - Optimiser les requĂȘtes Prisma (select spĂ©cifiques) + +3. **MaintenabilitĂ©** + - DĂ©couper `missions-admin-panel.tsx` + - Extraire helpers dans lib/ + - Supprimer console.log de production + +### 🟡 Important (À planifier) + +1. **Tests** + - Unit tests pour helpers + - Integration tests pour API routes + - E2E tests pour flux critiques + +2. **Documentation** + - JSDoc pour fonctions complexes + - Diagrammes de sĂ©quence pour flux N8N + - Guide de contribution + +3. **Monitoring** + - Sentry pour erreurs frontend + - Logging structurĂ© backend + - MĂ©triques de performance + +### 🟱 AmĂ©lioration (Nice to have) + +1. **UX** + - Optimistic updates + - Skeleton loaders + - Animations de transition + +2. **AccessibilitĂ©** + - ARIA labels complets + - Navigation clavier + - Support lecteurs d'Ă©cran + +--- + +## 🎯 Conclusion + +Le systĂšme de missions est **fonctionnel et bien structurĂ©** avec une architecture claire. Les principales amĂ©liorations Ă  apporter concernent: + +1. **MaintenabilitĂ©**: DĂ©coupage des gros composants +2. **Performance**: Optimisation des requĂȘtes et pagination +3. **Robustesse**: Meilleure gestion d'erreurs et retry logic +4. **SĂ©curitĂ©**: Validation stricte et rate limiting + +Le code montre une bonne comprĂ©hension de Next.js, Prisma, et des patterns React modernes. Avec les amĂ©liorations suggĂ©rĂ©es, le systĂšme sera prĂȘt pour la production Ă  grande Ă©chelle. + +--- + +**Date de review**: $(date) +**Reviewer**: Senior Developer +**Version analysĂ©e**: Current codebase diff --git a/components/missions/mission-members-panel.tsx b/components/missions/mission-members-panel.tsx new file mode 100644 index 0000000..c86a1f6 --- /dev/null +++ b/components/missions/mission-members-panel.tsx @@ -0,0 +1,620 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { logger } from '@/lib/logger'; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; +import { Badge } from "../ui/badge"; +import { X, Search, UserPlus, Users, PlusCircle, AlertCircle, Check } from "lucide-react"; +import { toast } from "../ui/use-toast"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "../ui/dropdown-menu"; + +// Define interfaces for user and group data +export interface User { + id: string; + username: string; + firstName: string; + lastName: string; + email: string; + roles?: string[]; + enabled?: boolean; +} + +export interface Group { + id: string; + name: string; + path: string; + membersCount: number; +} + +// User role types in mission +export type GuardienRole = 'temps' | 'parole' | 'memoire'; +export type UserRole = GuardienRole | 'volontaire'; + +interface MissionMembersPanelProps { + gardienDuTemps: string | null; + gardienDeLaParole: string | null; + gardienDeLaMemoire: string | null; + volontaires: string[]; + onGardienDuTempsChange: (userId: string | null) => void; + onGardienDeLaParoleChange: (userId: string | null) => void; + onGardienDeLaMemoireChange: (userId: string | null) => void; + onVolontairesChange: (userIds: string[]) => void; +} + +export function MissionMembersPanel({ + gardienDuTemps, + gardienDeLaParole, + gardienDeLaMemoire, + volontaires, + onGardienDuTempsChange, + onGardienDeLaParoleChange, + onGardienDeLaMemoireChange, + onVolontairesChange, +}: MissionMembersPanelProps) { + const [searchTerm, setSearchTerm] = useState(""); + const [selectedTab, setSelectedTab] = useState<'users' | 'groups'>('users'); + const [users, setUsers] = useState([]); + const [groups, setGroups] = useState([]); + 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) { + logger.error("Error fetching data", { error: error instanceof Error ? error.message : String(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) { + logger.error("Error fetching users", { error: error instanceof Error ? error.message : String(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) { + logger.error(`Error fetching members for group ${group.id}`, { error: error instanceof Error ? error.message : String(error) }); + return {...group, membersCount: 0}; + } + }) + ); + + setGroups(groupsWithCounts); + } catch (error) { + logger.error("Error fetching groups", { error: error instanceof Error ? error.message : String(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; + }; + + // 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)) { + onVolontairesChange(volontaires.filter(id => id !== userId)); + } + + // Assign to new role + if (role === 'temps') { + onGardienDuTempsChange(userId); + } else if (role === 'parole') { + onGardienDeLaParoleChange(userId); + } else if (role === 'memoire') { + onGardienDeLaMemoireChange(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 + onVolontairesChange([...volontaires, 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) onGardienDuTempsChange(null); + if (gardienDeLaParole === userId) onGardienDeLaParoleChange(null); + if (gardienDeLaMemoire === userId) onGardienDeLaMemoireChange(null); + if (volontaires.includes(userId)) { + onVolontairesChange(volontaires.filter(id => id !== userId)); + } + }; + + // 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) { + logger.error(`Error fetching members for group ${groupId}`, { error: error instanceof Error ? error.message : String(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) { + logger.error("Error handling group members", { error: error instanceof Error ? error.message : String(error) }); + toast({ + title: "Erreur", + description: "Erreur lors de l'affichage des membres du groupe", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + // Helper component for Guardian card + const GuardianCard = ({ + title, + userId, + onRemove + }: { + title: string; + userId: string | null; + onRemove: () => void; + }) => { + const user = userId ? users.find(u => u.id === userId) : null; + + return ( +
+
+

{title}

+ {userId && ( + + )} +
+ {loading ? ( +
+
+
+
+
+
+
+
+
+ ) : user ? ( +
+
+
+ {user.firstName?.[0] || ""}{user.lastName?.[0] || ""} +
+
+
{user.firstName} {user.lastName}
+
{user.email}
+
+
+
+ ) : ( +
+
+ +
+ Aucun utilisateur sélectionné +
+ )} +
+ ); + }; + + return ( +
+
+
+
+

Les Gardiens de l'Intention

+ +
+ {!isMissionValid && ( +
+ + Les 3 gardiens doivent ĂȘtre assignĂ©s +
+ )} + {isMissionValid && ( +
+ + Tous les gardiens sont assignés +
+ )} +
+
+ +
+ removeUserFromAllRoles(gardienDuTemps!)} + /> + + removeUserFromAllRoles(gardienDeLaParole!)} + /> + + removeUserFromAllRoles(gardienDeLaMemoire!)} + /> +
+
+ +
+
+
+

Sélectionner des membres

+
+ + +
+
+ +
+
Volontaires ({volontaires.length})
+ {volontaires.length > 0 ? ( +
+ {volontaires.map(userId => { + const user = users.find(u => u.id === userId); + if (!user) return null; + return ( + + {user.firstName} {user.lastName} + + + ); + })} +
+ ) : ( +
Aucun volontaire assigné
+ )} +
+ +
+ + setSearchTerm(e.target.value)} + className="pl-9 bg-white text-gray-900 border-gray-300" + disabled={loading} + /> +
+ + {loading ? ( +
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+ ))} +
+
+ ) : ( +
+ {selectedTab === 'users' ? ( + filteredUsers.length > 0 ? ( +
+ {filteredUsers.map(user => ( +
+
+
+ {user.firstName?.[0] || ""}{user.lastName?.[0] || ""} +
+
+
{user.firstName} {user.lastName}
+
{user.email}
+ {isUserAssigned(user.id) && ( +
+ {getUserRoles(user.id).map((role) => ( + + {getRoleDisplayName(role)} + + ))} +
+ )} +
+
+ + {/* User role controls always show dropdown */} +
+ + + + + + assignGuardienRole(user.id, 'temps')} + className="cursor-pointer" + > + Gardien du Temps + + assignGuardienRole(user.id, 'parole')} + className="cursor-pointer" + > + Gardien de la Parole + + assignGuardienRole(user.id, 'memoire')} + className="cursor-pointer" + > + Gardien de la Mémoire + + + + + + + {isUserAssigned(user.id) && ( + + )} +
+
+ ))} +
+ ) : ( +
+ Aucun utilisateur trouvé +
+ ) + ) : ( + filteredGroups.length > 0 ? ( +
+ {filteredGroups.map(group => ( +
+
+
+ +
+
+
{group.name}
+
{group.membersCount} membres
+
+
+ +
+ ))} +
+ ) : ( +
+ Aucun groupe trouvé +
+ ) + )} +
+ )} +
+
+
+
+ ); +} diff --git a/components/missions/missions-admin-panel.bak.tsx b/components/missions/missions-admin-panel.bak.tsx deleted file mode 100644 index 1c71590..0000000 --- a/components/missions/missions-admin-panel.bak.tsx +++ /dev/null @@ -1,1333 +0,0 @@ -"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 } 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([]); - const [selectedProfils, setSelectedProfils] = useState([]); - const [searchTerm, setSearchTerm] = useState(""); - const [selectedTab, setSelectedTab] = useState<'users' | 'groups'>('users'); - const [gardienDuTemps, setGardienDuTemps] = useState(null); - const [gardienDeLaParole, setGardienDeLaParole] = useState(null); - const [gardienDeLaMemoire, setGardienDeLaMemoire] = useState(null); - const [volontaires, setVolontaires] = useState([]); - const [missionId, setMissionId] = useState(""); - const [activeTab, setActiveTab] = useState("general"); - const [isSubmitting, setIsSubmitting] = useState(false); - 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([]); - const [groups, setGroups] = useState([]); - 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) => { - // No longer removing user from existing roles to allow multiple roles - // 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); - // Here you would typically open a dialog to show members - // For this implementation, we'll just show a toast with the count - toast({ - title: `Membres de ${groupName}`, - description: `${members.length} membres trouvés dans ce groupe`, - }); - } catch (error) { - console.error("Error handling group members:", error); - } 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 isValid = Object.values(requiredFields).every(field => field === true); - - if (!isValid) { - const missingFields = Object.entries(requiredFields) - .filter(([_, value]) => value === false) - .map(([key, _]) => key); - - toast({ - title: "Champs obligatoires manquants", - description: `Veuillez remplir tous les champs obligatoires: ${missingFields.join(", ")}`, - variant: "destructive", - }); - } - - return isValid; - }; - - // Handle mission submission - const handleSubmitMission = async () => { - if (!validateMission()) { - return; - } - - setIsSubmitting(true); - - try { - // Prepare the mission data - const guardians = { - "gardien-temps": gardienDuTemps, - "gardien-parole": gardienDeLaParole, - "gardien-memoire": gardienDeLaMemoire - }; - - const missionSubmitData = { - ...missionData, - services: selectedServices, - profils: selectedProfils, - guardians, - volunteers: volontaires - }; - - // Send to API - const response = await fetch('/api/missions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(missionSubmitData), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to create mission'); - } - - const data = await response.json(); - - 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('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 ( -
- - - - - General - Details - Attachments - Skills - Membres - - - -
-
- - handleInputChange('name', e.target.value)} - /> -
- -
- - { - // Handle logo upload complete - if (data?.filePath) { - setMissionData(prev => ({ - ...prev, - logo: data.filePath - })); - } - }} - /> -
- -
-
- - -
- -
- - -
-
- -
- -
-
- Paragraphe -
- - - - - -
-
-