Avnyr 6cc6506e6f refactor: add explicit any types and improve readability in group state handling
- Updated `setProject` function in `page.tsx` to include explicit `(prev: any)` and `(group: any)` type annotations for better readability.
- Added `"use client";` directive to `notifications.tsx` for React server-client compatibility.
- Improved structural consistency and clarity in group and person state updates.
2025-05-16 17:05:07 +02:00

421 lines
14 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
PlusCircle,
Users,
Wand2,
ArrowLeft,
Loader2,
RefreshCw
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { useSocket } from "@/lib/socket-context";
// Mock project data
const getProjectData = (id: string) => {
return {
id: parseInt(id),
name: "Projet Formation Dev Web",
description: "Création de groupes pour la formation développement web",
date: "2025-05-15",
groups: [
{
id: 1,
name: "Groupe A",
persons: [
{ id: 1, name: "Jean Dupont", tags: ["Frontend", "React", "Junior"] },
{ id: 2, name: "Marie Martin", tags: ["Backend", "Node.js", "Senior"] },
{ id: 3, name: "Pierre Durand", tags: ["Fullstack", "JavaScript", "Medior"] },
{ id: 4, name: "Sophie Lefebvre", tags: ["UX/UI", "Design", "Senior"] },
]
},
{
id: 2,
name: "Groupe B",
persons: [
{ id: 5, name: "Thomas Bernard", tags: ["Backend", "Java", "Senior"] },
{ id: 6, name: "Julie Petit", tags: ["Frontend", "Vue", "Junior"] },
{ id: 7, name: "Nicolas Moreau", tags: ["DevOps", "Docker", "Medior"] },
{ id: 8, name: "Emma Dubois", tags: ["Frontend", "Angular", "Junior"] },
]
},
{
id: 3,
name: "Groupe C",
persons: [
{ id: 9, name: "Lucas Leroy", tags: ["Backend", "Python", "Medior"] },
{ id: 10, name: "Camille Roux", tags: ["Fullstack", "TypeScript", "Senior"] },
{ id: 11, name: "Hugo Fournier", tags: ["Frontend", "React", "Medior"] },
{ id: 12, name: "Léa Girard", tags: ["UX/UI", "Figma", "Junior"] },
]
},
{
id: 4,
name: "Groupe D",
persons: [
{ id: 13, name: "Mathis Bonnet", tags: ["Backend", "PHP", "Junior"] },
{ id: 14, name: "Chloé Lambert", tags: ["Frontend", "CSS", "Senior"] },
{ id: 15, name: "Nathan Mercier", tags: ["DevOps", "Kubernetes", "Senior"] },
{ id: 16, name: "Zoé Faure", tags: ["Fullstack", "MERN", "Medior"] },
]
}
],
persons: [
{ id: 1, name: "Jean Dupont", tags: ["Frontend", "React", "Junior"] },
{ id: 2, name: "Marie Martin", tags: ["Backend", "Node.js", "Senior"] },
{ id: 3, name: "Pierre Durand", tags: ["Fullstack", "JavaScript", "Medior"] },
{ id: 4, name: "Sophie Lefebvre", tags: ["UX/UI", "Design", "Senior"] },
{ id: 5, name: "Thomas Bernard", tags: ["Backend", "Java", "Senior"] },
{ id: 6, name: "Julie Petit", tags: ["Frontend", "Vue", "Junior"] },
{ id: 7, name: "Nicolas Moreau", tags: ["DevOps", "Docker", "Medior"] },
{ id: 8, name: "Emma Dubois", tags: ["Frontend", "Angular", "Junior"] },
{ id: 9, name: "Lucas Leroy", tags: ["Backend", "Python", "Medior"] },
{ id: 10, name: "Camille Roux", tags: ["Fullstack", "TypeScript", "Senior"] },
{ id: 11, name: "Hugo Fournier", tags: ["Frontend", "React", "Medior"] },
{ id: 12, name: "Léa Girard", tags: ["UX/UI", "Figma", "Junior"] },
{ id: 13, name: "Mathis Bonnet", tags: ["Backend", "PHP", "Junior"] },
{ id: 14, name: "Chloé Lambert", tags: ["Frontend", "CSS", "Senior"] },
{ id: 15, name: "Nathan Mercier", tags: ["DevOps", "Kubernetes", "Senior"] },
{ id: 16, name: "Zoé Faure", tags: ["Fullstack", "MERN", "Medior"] },
]
};
};
export default function ProjectGroupsPage() {
const params = useParams();
const router = useRouter();
const projectId = params.id as string;
const [project, setProject] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [activeTab, setActiveTab] = useState("existing");
// Socket connection for real-time updates
const { isConnected, joinProject, leaveProject, onGroupCreated, onGroupUpdated, onPersonAddedToGroup, onPersonRemovedFromGroup } = useSocket();
// Fetch project data from API
const fetchProject = async () => {
setLoading(true);
try {
// Use the API service to get project and groups data
const { projectsAPI, groupsAPI } = await import('@/lib/api');
const projectData = await projectsAPI.getProject(projectId);
const groupsData = await groupsAPI.getGroups(projectId);
// Combine project data with groups data
const data = {
...projectData,
groups: groupsData || []
};
setProject(data);
} catch (error) {
console.error("Error fetching project:", error);
toast.error("Erreur lors du chargement du projet");
// Fallback to mock data for development
const data = getProjectData(projectId);
setProject(data);
} finally {
setLoading(false);
setRefreshing(false);
}
};
// Initial fetch
useEffect(() => {
fetchProject();
}, [projectId]);
// Join project room for real-time updates when connected
useEffect(() => {
if (!isConnected) return;
// Join the project room to receive updates
joinProject(projectId);
// Clean up when component unmounts
return () => {
leaveProject(projectId);
};
}, [isConnected, joinProject, leaveProject, projectId]);
// Listen for group created events
useEffect(() => {
if (!isConnected) return;
const unsubscribe = onGroupCreated((data) => {
console.log("Group created:", data);
if (data.action === "created" && data.group) {
// Add the new group to the list
setProject((prev: any) => ({
...prev,
groups: [...prev.groups, data.group]
}));
toast.success(`Nouveau groupe créé: ${data.group.name}`);
}
});
return () => {
unsubscribe();
};
}, [isConnected, onGroupCreated]);
// Listen for group updated events
useEffect(() => {
if (!isConnected) return;
const unsubscribe = onGroupUpdated((data) => {
console.log("Group updated:", data);
if (data.action === "updated" && data.group) {
// Update the group in the list
setProject((prev: any) => ({
...prev,
groups: prev.groups.map((group: any) =>
group.id === data.group.id ? data.group : group
)
}));
toast.info(`Groupe mis à jour: ${data.group.name}`);
} else if (data.action === "deleted" && data.group) {
// Remove the group from the list
setProject((prev: any) => ({
...prev,
groups: prev.groups.filter((group: any) => group.id !== data.group.id)
}));
toast.info(`Groupe supprimé: ${data.group.name}`);
}
});
return () => {
unsubscribe();
};
}, [isConnected, onGroupUpdated]);
// Listen for person added to group events
useEffect(() => {
if (!isConnected) return;
const unsubscribe = onPersonAddedToGroup((data) => {
console.log("Person added to group:", data);
if (data.group && data.person) {
// Update the group with the new person
setProject((prev: any) => ({
...prev,
groups: prev.groups.map((group: any) => {
if (group.id === data.group.id) {
return {
...group,
persons: [...group.persons, data.person]
};
}
return group;
})
}));
toast.success(`${data.person.name} a été ajouté au groupe ${data.group.name}`);
}
});
return () => {
unsubscribe();
};
}, [isConnected, onPersonAddedToGroup]);
// Listen for person removed from group events
useEffect(() => {
if (!isConnected) return;
const unsubscribe = onPersonRemovedFromGroup((data) => {
console.log("Person removed from group:", data);
if (data.group && data.person) {
// Update the group by removing the person
setProject((prev: any) => ({
...prev,
groups: prev.groups.map((group: any) => {
if (group.id === data.group.id) {
return {
...group,
persons: group.persons.filter((person: any) => person.id !== data.person.id)
};
}
return group;
})
}));
toast.info(`${data.person.name} a été retiré du groupe ${data.group.name}`);
}
});
return () => {
unsubscribe();
};
}, [isConnected, onPersonRemovedFromGroup]);
const handleCreateGroups = () => {
router.push(`/projects/${projectId}/groups/create`);
};
const handleAutoCreateGroups = () => {
router.push(`/projects/${projectId}/groups/auto-create`);
};
if (loading) {
return (
<div className="flex h-[50vh] items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
}
if (!project) {
return (
<div className="flex h-[50vh] flex-col items-center justify-center">
<p className="text-lg font-medium">Projet non trouvé</p>
<Button asChild className="mt-4">
<Link href="/projects">Retour aux projets</Link>
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" asChild>
<Link href={`/projects/${projectId}`}>
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<h1 className="text-3xl font-bold">{project.name} - Groupes</h1>
</div>
<Button
variant="outline"
size="icon"
onClick={() => {
setRefreshing(true);
fetchProject();
}}
disabled={loading || refreshing}
>
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
<span className="sr-only">Rafraîchir</span>
</Button>
</div>
<Tabs defaultValue="existing" className="space-y-4" onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="existing">Groupes existants</TabsTrigger>
<TabsTrigger value="create">Créer des groupes</TabsTrigger>
</TabsList>
<TabsContent value="existing" className="space-y-4">
{project.groups.length === 0 ? (
<Card>
<CardHeader>
<CardTitle>Aucun groupe</CardTitle>
<CardDescription>
Ce projet ne contient pas encore de groupes. Créez-en un maintenant.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-center py-6">
<Button onClick={handleCreateGroups}>
<PlusCircle className="mr-2 h-4 w-4" />
Créer un groupe
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2">
{project.groups.map((group: any) => (
<Card key={group.id}>
<CardHeader>
<CardTitle>{group.name}</CardTitle>
<CardDescription>
{group.persons.length} personnes
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{group.persons.map((person: any) => (
<div key={person.id} className="flex items-center justify-between border-b pb-2 last:border-0 last:pb-0">
<div>
<p className="font-medium">{person.name}</p>
<div className="flex flex-wrap gap-1 mt-1">
{person.tags.map((tag: string, index: number) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
<TabsContent value="create" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Création manuelle</CardTitle>
<CardDescription>
Créez des groupes manuellement en glissant-déposant les personnes
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center justify-center py-6">
<Users className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-center text-muted-foreground mb-4">
Utilisez l'interface de glisser-déposer pour créer vos groupes selon vos critères
</p>
<Button onClick={handleCreateGroups}>
<PlusCircle className="mr-2 h-4 w-4" />
Créer manuellement
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Création automatique</CardTitle>
<CardDescription>
Laissez l'assistant créer des groupes équilibrés automatiquement
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center justify-center py-6">
<Wand2 className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-center text-muted-foreground mb-4">
L'assistant prendra en compte les tags et niveaux pour créer des groupes équilibrés
</p>
<Button onClick={handleAutoCreateGroups}>
<Wand2 className="mr-2 h-4 w-4" />
Utiliser l'assistant
</Button>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
);
}