786 lines
31 KiB
TypeScript
786 lines
31 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useSession } from "next-auth/react";
|
|
import { redirect } from "next/navigation";
|
|
import { ResponsiveIframe } from "@/app/components/responsive-iframe";
|
|
import { Users, FolderKanban, Video, ArrowLeft, Loader2, Calendar, Clock, Plus, X } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
interface Group {
|
|
id: string;
|
|
name: string;
|
|
path: string;
|
|
}
|
|
|
|
interface Mission {
|
|
id: string;
|
|
name: string;
|
|
logoUrl?: string | null;
|
|
}
|
|
|
|
interface ScheduledMeeting {
|
|
id: string;
|
|
type: "group" | "mission";
|
|
entityId: string;
|
|
entityName: string;
|
|
date: string; // ISO date string
|
|
time: string; // HH:mm format
|
|
title?: string;
|
|
}
|
|
|
|
type ConferenceType = "group" | "mission" | null;
|
|
|
|
export default function VisionPage() {
|
|
const { data: session, status } = useSession();
|
|
const { toast } = useToast();
|
|
const [groups, setGroups] = useState<Group[]>([]);
|
|
const [missions, setMissions] = useState<Mission[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedConference, setSelectedConference] = useState<{
|
|
type: ConferenceType;
|
|
id: string;
|
|
name: string;
|
|
} | null>(null);
|
|
const [jitsiUrl, setJitsiUrl] = useState<string | null>(null);
|
|
const [scheduledMeetings, setScheduledMeetings] = useState<ScheduledMeeting[]>([]);
|
|
const [showMeetingDialog, setShowMeetingDialog] = useState(false);
|
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(new Date());
|
|
const [meetingForm, setMeetingForm] = useState<{
|
|
type: "group" | "mission" | "";
|
|
entityId: string;
|
|
entityName: string;
|
|
date: string;
|
|
time: string;
|
|
title: string;
|
|
}>({
|
|
type: "",
|
|
entityId: "",
|
|
entityName: "",
|
|
date: "",
|
|
time: "",
|
|
title: "",
|
|
});
|
|
|
|
// Redirect if not authenticated
|
|
useEffect(() => {
|
|
if (status === "unauthenticated") {
|
|
redirect("/signin");
|
|
}
|
|
}, [status]);
|
|
|
|
// Load scheduled meetings from localStorage
|
|
useEffect(() => {
|
|
if (status === "authenticated" && session?.user?.id) {
|
|
const stored = localStorage.getItem(`meetings_${session.user.id}`);
|
|
if (stored) {
|
|
try {
|
|
setScheduledMeetings(JSON.parse(stored));
|
|
} catch (e) {
|
|
console.error("Error loading meetings:", e);
|
|
}
|
|
}
|
|
}
|
|
}, [session, status]);
|
|
|
|
// Save scheduled meetings to localStorage
|
|
useEffect(() => {
|
|
if (status === "authenticated" && session?.user?.id && scheduledMeetings.length >= 0) {
|
|
localStorage.setItem(`meetings_${session.user.id}`, JSON.stringify(scheduledMeetings));
|
|
}
|
|
}, [scheduledMeetings, session, status]);
|
|
|
|
// Fetch user groups and missions
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
if (status !== "authenticated" || !session?.user?.id) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
const userId = session.user.id;
|
|
|
|
// Fetch user groups
|
|
const groupsRes = await fetch(`/api/users/${userId}/groups`);
|
|
if (groupsRes.ok) {
|
|
const groupsData = await groupsRes.json();
|
|
setGroups(Array.isArray(groupsData) ? groupsData : []);
|
|
} else {
|
|
console.error("Failed to fetch groups");
|
|
}
|
|
|
|
// Fetch all missions and filter for user's missions
|
|
const missionsRes = await fetch("/api/missions?limit=1000");
|
|
if (missionsRes.ok) {
|
|
const missionsData = await missionsRes.json();
|
|
const allMissions = missionsData.missions || [];
|
|
|
|
// Filter missions where user is creator or member
|
|
const userMissions = allMissions.filter((mission: any) => {
|
|
const isCreator = mission.creator?.id === userId;
|
|
const isMember = mission.missionUsers?.some(
|
|
(mu: any) => mu.user?.id === userId
|
|
);
|
|
return isCreator || isMember;
|
|
});
|
|
|
|
setMissions(userMissions);
|
|
} else {
|
|
console.error("Failed to fetch missions");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching data:", error);
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Impossible de charger les données",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, [session, status, toast]);
|
|
|
|
// Handle conference selection
|
|
const handleConferenceClick = (type: ConferenceType, id: string, name: string) => {
|
|
const baseUrl = process.env.NEXT_PUBLIC_IFRAME_CONFERENCE_URL || 'https://vision.slm-lab.net';
|
|
|
|
let url: string;
|
|
if (type === "group") {
|
|
// URL format: https://vision.slm-lab.net/Groupe-{groupId}
|
|
url = `${baseUrl}/Groupe-${id}`;
|
|
} else {
|
|
// URL format: https://vision.slm-lab.net/{missionId}
|
|
url = `${baseUrl}/${id}`;
|
|
}
|
|
|
|
setSelectedConference({ type, id, name });
|
|
setJitsiUrl(url);
|
|
};
|
|
|
|
// Handle back to list
|
|
const handleBack = () => {
|
|
setSelectedConference(null);
|
|
setJitsiUrl(null);
|
|
};
|
|
|
|
// Get today's meetings
|
|
const getTodayMeetings = (): ScheduledMeeting[] => {
|
|
const today = new Date();
|
|
const todayStr = today.toISOString().split('T')[0];
|
|
return scheduledMeetings.filter(meeting => meeting.date === todayStr);
|
|
};
|
|
|
|
// Get meetings for a specific date
|
|
const getMeetingsForDate = (date: Date): ScheduledMeeting[] => {
|
|
const dateStr = date.toISOString().split('T')[0];
|
|
return scheduledMeetings.filter(meeting => meeting.date === dateStr);
|
|
};
|
|
|
|
// Handle open meeting dialog
|
|
const handleOpenMeetingDialog = (type: "group" | "mission", id: string, name: string) => {
|
|
setMeetingForm({
|
|
type,
|
|
entityId: id,
|
|
entityName: name,
|
|
date: selectedDate ? selectedDate.toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
|
time: "",
|
|
title: "",
|
|
});
|
|
setShowMeetingDialog(true);
|
|
};
|
|
|
|
// Handle save meeting
|
|
const handleSaveMeeting = () => {
|
|
if (!meetingForm.type || !meetingForm.entityId || !meetingForm.date || !meetingForm.time) {
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Veuillez remplir tous les champs requis",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const newMeeting: ScheduledMeeting = {
|
|
id: `${Date.now()}-${Math.random()}`,
|
|
type: meetingForm.type,
|
|
entityId: meetingForm.entityId,
|
|
entityName: meetingForm.entityName,
|
|
date: meetingForm.date,
|
|
time: meetingForm.time,
|
|
title: meetingForm.title || `${meetingForm.type === "group" ? "Groupe" : "Mission"}: ${meetingForm.entityName}`,
|
|
};
|
|
|
|
setScheduledMeetings([...scheduledMeetings, newMeeting]);
|
|
setShowMeetingDialog(false);
|
|
setMeetingForm({
|
|
type: "",
|
|
entityId: "",
|
|
entityName: "",
|
|
date: "",
|
|
time: "",
|
|
title: "",
|
|
});
|
|
toast({
|
|
title: "Succès",
|
|
description: "Réunion planifiée avec succès",
|
|
});
|
|
};
|
|
|
|
// Handle delete meeting
|
|
const handleDeleteMeeting = (meetingId: string) => {
|
|
setScheduledMeetings(scheduledMeetings.filter(m => m.id !== meetingId));
|
|
toast({
|
|
title: "Succès",
|
|
description: "Réunion supprimée",
|
|
});
|
|
};
|
|
|
|
// Format date for display
|
|
const formatDate = (dateStr: string): string => {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' });
|
|
};
|
|
|
|
// Show loading state
|
|
if (status === "loading" || loading) {
|
|
return (
|
|
<main className="w-full h-screen bg-gray-50 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<Loader2 className="h-12 w-12 animate-spin text-blue-600 mx-auto mb-4" />
|
|
<p className="text-gray-600">Chargement...</p>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
// Show Jitsi iframe if conference is selected
|
|
if (selectedConference && jitsiUrl) {
|
|
return (
|
|
<main className="w-full h-screen bg-black flex flex-col">
|
|
{/* Header with back button */}
|
|
<div className="bg-white border-b border-gray-200 px-4 py-3 flex items-center gap-4">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleBack}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Retour
|
|
</Button>
|
|
<div className="flex-1">
|
|
<h1 className="text-sm font-medium text-gray-900">
|
|
{selectedConference.type === "group" ? "Groupe" : "Mission"}: {selectedConference.name}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Jitsi iframe */}
|
|
<div className="flex-1 overflow-hidden">
|
|
<ResponsiveIframe
|
|
src={jitsiUrl}
|
|
allow="camera; microphone; fullscreen; display-capture; autoplay; clipboard-write; encrypted-media"
|
|
/>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
// Show list of groups and missions
|
|
const todayMeetings = getTodayMeetings();
|
|
|
|
return (
|
|
<main className="w-full h-screen bg-gray-50 overflow-auto">
|
|
<div className="w-full px-4 pt-20 pb-4">
|
|
<div className="max-w-6xl mx-auto">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
|
Espaces de réunion
|
|
</h1>
|
|
<p className="text-gray-600">
|
|
Sélectionnez un groupe ou une mission pour accéder à son espace de réunion Jitsi
|
|
</p>
|
|
</div>
|
|
|
|
{/* Today's Meetings Section */}
|
|
{todayMeetings.length > 0 && (
|
|
<div className="mb-8 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Calendar className="h-5 w-5 text-blue-600" />
|
|
<h2 className="text-lg font-semibold text-blue-900">
|
|
Réunions du jour ({todayMeetings.length})
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{todayMeetings.map((meeting) => (
|
|
<div
|
|
key={meeting.id}
|
|
className="bg-white rounded-lg border border-blue-200 p-3 flex items-center justify-between"
|
|
>
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
|
|
<Clock className="h-5 w-5 text-blue-600" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<h3 className="font-medium text-gray-900">
|
|
{meeting.title || `${meeting.type === "group" ? "Groupe" : "Mission"}: ${meeting.entityName}`}
|
|
</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{meeting.time} - {formatDate(meeting.date)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
size="sm"
|
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
|
onClick={() => handleConferenceClick(meeting.type, meeting.entityId, meeting.entityName)}
|
|
>
|
|
<Video className="h-4 w-4 mr-1" />
|
|
Rejoindre
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDeleteMeeting(meeting.id)}
|
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Calendar Section */}
|
|
<div className="mb-8">
|
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Calendar className="h-5 w-5 text-purple-600" />
|
|
<h2 className="text-xl font-semibold text-gray-900">
|
|
Calendrier des réunions
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="flex flex-col lg:flex-row gap-6">
|
|
{/* Colonne calendrier (étroite) */}
|
|
<div id="vision-calendar" className="w-full lg:w-80 lg:flex-shrink-0">
|
|
<style dangerouslySetInnerHTML={{__html: `
|
|
#vision-calendar {
|
|
color: #111827 !important;
|
|
}
|
|
#vision-calendar .rdp {
|
|
--rdp-cell-size: 2.5rem;
|
|
--rdp-accent-color: #2563eb;
|
|
--rdp-background-color: white;
|
|
--rdp-accent-color-dark: #1d4ed8;
|
|
--rdp-background-color-dark: #f3f4f6;
|
|
--rdp-outline: 2px solid var(--rdp-accent-color);
|
|
--rdp-outline-selected: 2px solid var(--rdp-accent-color);
|
|
margin: 0;
|
|
}
|
|
#vision-calendar .rdp-caption {
|
|
color: #111827 !important;
|
|
background-color: white !important;
|
|
}
|
|
#vision-calendar .rdp-caption_label {
|
|
color: #111827 !important;
|
|
font-weight: 600 !important;
|
|
font-size: 0.875rem !important;
|
|
background-color: white !important;
|
|
}
|
|
#vision-calendar .rdp-nav {
|
|
background-color: white !important;
|
|
}
|
|
#vision-calendar .rdp-nav_button {
|
|
color: #374151 !important;
|
|
background-color: white !important;
|
|
border: 1px solid #e5e7eb !important;
|
|
}
|
|
#vision-calendar .rdp-nav_button:hover {
|
|
color: #111827 !important;
|
|
background-color: #f9fafb !important;
|
|
}
|
|
#vision-calendar .rdp-nav_button svg,
|
|
#vision-calendar .rdp-nav_button path {
|
|
color: #374151 !important;
|
|
stroke: #374151 !important;
|
|
fill: #374151 !important;
|
|
}
|
|
#vision-calendar .rdp-nav_button:hover svg,
|
|
#vision-calendar .rdp-nav_button:hover path {
|
|
color: #111827 !important;
|
|
stroke: #111827 !important;
|
|
fill: #111827 !important;
|
|
}
|
|
#vision-calendar .rdp-head_cell {
|
|
color: #6b7280 !important;
|
|
font-weight: 500 !important;
|
|
background-color: white !important;
|
|
}
|
|
#vision-calendar .rdp-day {
|
|
color: #1f2937 !important;
|
|
background-color: white !important;
|
|
width: 2.5rem !important;
|
|
height: 2.5rem !important;
|
|
font-size: 0.875rem !important;
|
|
border: 1px solid transparent !important;
|
|
}
|
|
#vision-calendar .rdp-day:hover:not(.rdp-day_disabled):not(.rdp-day_selected):not(.hasMeeting) {
|
|
background-color: #f3f4f6 !important;
|
|
color: #111827 !important;
|
|
}
|
|
#vision-calendar .rdp-day_today {
|
|
background-color: #DBEAFE !important;
|
|
color: #1d4ed8 !important;
|
|
border: 2px solid #3b82f6 !important;
|
|
font-weight: 600 !important;
|
|
}
|
|
#vision-calendar .rdp-day_today:hover {
|
|
background-color: #bfdbfe !important;
|
|
color: #1e40af !important;
|
|
}
|
|
#vision-calendar .rdp-day_selected {
|
|
background-color: #2563eb !important;
|
|
color: white !important;
|
|
font-weight: 600 !important;
|
|
}
|
|
#vision-calendar .rdp-day_selected:hover {
|
|
background-color: #1d4ed8 !important;
|
|
color: white !important;
|
|
}
|
|
#vision-calendar .rdp-day_outside {
|
|
color: #9ca3af !important;
|
|
background-color: white !important;
|
|
}
|
|
#vision-calendar .rdp-day_outside:hover {
|
|
background-color: #f9fafb !important;
|
|
color: #6b7280 !important;
|
|
}
|
|
#vision-calendar .rdp-day.hasMeeting {
|
|
background-color: #2563eb !important;
|
|
color: white !important;
|
|
font-weight: 600 !important;
|
|
}
|
|
#vision-calendar .rdp-day.hasMeeting:hover {
|
|
background-color: #1d4ed8 !important;
|
|
color: white !important;
|
|
}
|
|
#vision-calendar .rdp-day.hasMeeting.rdp-day_today {
|
|
background-color: #1d4ed8 !important;
|
|
color: white !important;
|
|
border: 2px solid #1e40af !important;
|
|
}
|
|
#vision-calendar .rdp-day.hasMeeting.rdp-day_selected {
|
|
background-color: #1e40af !important;
|
|
color: white !important;
|
|
}
|
|
`}} />
|
|
<CalendarComponent
|
|
mode="single"
|
|
selected={selectedDate}
|
|
onSelect={setSelectedDate}
|
|
className="rounded-md border"
|
|
modifiers={{
|
|
hasMeeting: (date) => {
|
|
if (!date) return false;
|
|
return getMeetingsForDate(date).length > 0;
|
|
},
|
|
}}
|
|
modifiersClassNames={{
|
|
hasMeeting: "hasMeeting",
|
|
}}
|
|
/>
|
|
</div>
|
|
{/* Colonne invisible pour espacer calendrier et liste des réunions */}
|
|
<div className="hidden lg:block w-8" />
|
|
<div className="flex-1">
|
|
{selectedDate && (
|
|
<div>
|
|
<h3 className="font-semibold text-gray-900 mb-3">
|
|
Réunions du {selectedDate.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' })}
|
|
</h3>
|
|
{getMeetingsForDate(selectedDate).length === 0 ? (
|
|
<p className="text-gray-500 text-sm">Aucune réunion planifiée</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{getMeetingsForDate(selectedDate).map((meeting) => (
|
|
<div
|
|
key={meeting.id}
|
|
className="bg-gray-50 rounded-lg border border-gray-200 p-3 flex items-center justify-between"
|
|
>
|
|
<div>
|
|
<h4 className="font-medium text-gray-900">
|
|
{meeting.title || `${meeting.type === "group" ? "Groupe" : "Mission"}: ${meeting.entityName}`}
|
|
</h4>
|
|
<p className="text-sm text-gray-500">{meeting.time}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handleConferenceClick(meeting.type, meeting.entityId, meeting.entityName)}
|
|
>
|
|
<Video className="h-4 w-4 mr-1" />
|
|
Rejoindre
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDeleteMeeting(meeting.id)}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Groups Section */}
|
|
<div className="mb-8">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Users className="h-5 w-5 text-blue-600" />
|
|
<h2 className="text-xl font-semibold text-gray-900">
|
|
Groupes ({groups.length})
|
|
</h2>
|
|
</div>
|
|
|
|
{groups.length === 0 ? (
|
|
<div className="bg-white rounded-lg border border-gray-200 p-8 text-center">
|
|
<Users className="h-12 w-12 text-gray-400 mx-auto mb-3" />
|
|
<p className="text-gray-500">Aucun groupe disponible</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{groups.map((group) => (
|
|
<div
|
|
key={group.id}
|
|
className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow cursor-pointer"
|
|
onClick={() => handleConferenceClick("group", group.id, group.name)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
|
|
<Users className="h-5 w-5 text-blue-600" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-medium text-gray-900 truncate">
|
|
{group.name}
|
|
</h3>
|
|
<p className="text-sm text-gray-500 truncate">
|
|
{group.path}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 ml-2">
|
|
<Button
|
|
size="sm"
|
|
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleConferenceClick("group", group.id, group.name);
|
|
}}
|
|
>
|
|
<Video className="h-4 w-4" />
|
|
Réunion
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-2"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setSelectedDate(new Date());
|
|
handleOpenMeetingDialog("group", group.id, group.name);
|
|
}}
|
|
title="Planifier une réunion"
|
|
>
|
|
<Calendar className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Missions Section */}
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<FolderKanban className="h-5 w-5 text-purple-600" />
|
|
<h2 className="text-xl font-semibold text-gray-900">
|
|
Missions ({missions.length})
|
|
</h2>
|
|
</div>
|
|
|
|
{missions.length === 0 ? (
|
|
<div className="bg-white rounded-lg border border-gray-200 p-8 text-center">
|
|
<FolderKanban className="h-12 w-12 text-gray-400 mx-auto mb-3" />
|
|
<p className="text-gray-500">Aucune mission disponible</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{missions.map((mission) => (
|
|
<div
|
|
key={mission.id}
|
|
className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow cursor-pointer"
|
|
onClick={() => handleConferenceClick("mission", mission.id, mission.name)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3 flex-1">
|
|
{mission.logoUrl ? (
|
|
<div className="h-10 w-10 rounded-md overflow-hidden flex-shrink-0">
|
|
<img
|
|
src={mission.logoUrl}
|
|
alt={mission.name}
|
|
className="h-full w-full object-cover"
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).style.display = 'none';
|
|
}}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="h-10 w-10 rounded-md bg-purple-100 flex items-center justify-center flex-shrink-0">
|
|
<FolderKanban className="h-5 w-5 text-purple-600" />
|
|
</div>
|
|
)}
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-medium text-gray-900 truncate">
|
|
{mission.name}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 ml-2">
|
|
<Button
|
|
size="sm"
|
|
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleConferenceClick("mission", mission.id, mission.name);
|
|
}}
|
|
>
|
|
<Video className="h-4 w-4" />
|
|
Réunion
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-2"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setSelectedDate(new Date());
|
|
handleOpenMeetingDialog("mission", mission.id, mission.name);
|
|
}}
|
|
title="Planifier une réunion"
|
|
>
|
|
<Calendar className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Meeting Dialog */}
|
|
<Dialog open={showMeetingDialog} onOpenChange={setShowMeetingDialog}>
|
|
<DialogContent className="sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Planifier une réunion</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div>
|
|
<Label htmlFor="meeting-title">Titre (optionnel)</Label>
|
|
<Input
|
|
id="meeting-title"
|
|
value={meetingForm.title}
|
|
onChange={(e) => setMeetingForm({ ...meetingForm, title: e.target.value })}
|
|
placeholder={`Réunion ${meetingForm.type === "group" ? "du groupe" : "de la mission"}`}
|
|
className="mt-1 bg-black text-white border-gray-700 placeholder-gray-400"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="meeting-date">Date *</Label>
|
|
<Input
|
|
id="meeting-date"
|
|
type="date"
|
|
value={meetingForm.date}
|
|
onChange={(e) => setMeetingForm({ ...meetingForm, date: e.target.value })}
|
|
className="mt-1 bg-black text-white border-gray-700"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="meeting-time">Heure *</Label>
|
|
<Input
|
|
id="meeting-time"
|
|
type="time"
|
|
value={meetingForm.time}
|
|
onChange={(e) => setMeetingForm({ ...meetingForm, time: e.target.value })}
|
|
className="mt-1 bg-black text-white border-gray-700"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="bg-gray-50 p-3 rounded-lg">
|
|
<p className="text-sm text-gray-600">
|
|
<strong>{meetingForm.type === "group" ? "Groupe" : "Mission"}:</strong> {meetingForm.entityName}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
onClick={() => {
|
|
setShowMeetingDialog(false);
|
|
setMeetingForm({
|
|
type: "",
|
|
entityId: "",
|
|
entityName: "",
|
|
date: "",
|
|
time: "",
|
|
title: "",
|
|
});
|
|
}}
|
|
className="bg-black text-white hover:bg-gray-900 border border-black"
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
onClick={handleSaveMeeting}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
|
>
|
|
Planifier
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</main>
|
|
);
|
|
}
|