calendar 9
This commit is contained in:
parent
d76af98aec
commit
b2240cb880
@ -9,12 +9,35 @@ import frLocale from "@fullcalendar/core/locales/fr";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Loader2, Plus, Calendar as CalendarIcon, Check, X } from "lucide-react";
|
||||
import {
|
||||
Loader2,
|
||||
Plus,
|
||||
Calendar as CalendarIcon,
|
||||
Check,
|
||||
X,
|
||||
User,
|
||||
Clock,
|
||||
BarChart2,
|
||||
Settings,
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
Bell,
|
||||
Users,
|
||||
MapPin,
|
||||
Tag,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from "lucide-react";
|
||||
import { Calendar, Event } from "@prisma/client";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
// Predefined professional color palette
|
||||
const colorPalette = [
|
||||
@ -35,6 +58,11 @@ const colorPalette = [
|
||||
interface CalendarClientProps {
|
||||
initialCalendars: (Calendar & { events: Event[] })[];
|
||||
userId: string;
|
||||
userProfile: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EventFormData {
|
||||
@ -233,7 +261,136 @@ function CalendarDialog({ open, onClose, onSave, initialData }: CalendarDialogPr
|
||||
);
|
||||
}
|
||||
|
||||
export function CalendarClient({ initialCalendars, userId }: CalendarClientProps) {
|
||||
function StatisticsPanel({ statistics }: {
|
||||
statistics: {
|
||||
totalEvents: number;
|
||||
upcomingEvents: number;
|
||||
completedEvents: number;
|
||||
meetingHours: number;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 p-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-full">
|
||||
<CalendarIcon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Total Événements</p>
|
||||
<p className="text-2xl font-bold">{statistics.totalEvents}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-full">
|
||||
<Clock className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Heures de Réunion</p>
|
||||
<p className="text-2xl font-bold">{statistics.meetingHours}h</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-full">
|
||||
<Bell className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Prochains Événements</p>
|
||||
<p className="text-2xl font-bold">{statistics.upcomingEvents}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-full">
|
||||
<Check className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Événements Terminés</p>
|
||||
<p className="text-2xl font-bold">{statistics.completedEvents}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EventPreview({ event, calendar }: { event: Event; calendar: Calendar }) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Card className="p-4 space-y-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium">{event.title}</h3>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>
|
||||
{new Date(event.start).toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: calendar.color }}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="space-y-4">
|
||||
{event.description && (
|
||||
<p className="text-sm text-gray-600">{event.description}</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{event.location && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<MapPin className="h-4 w-4 text-gray-500" />
|
||||
<span>{event.location}</span>
|
||||
</div>
|
||||
)}
|
||||
{event.calendarId && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Tag className="h-4 w-4 text-gray-500" />
|
||||
<span>{calendar.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
Modifier
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
Supprimer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function CalendarClient({ initialCalendars, userId, userProfile }: CalendarClientProps) {
|
||||
const [calendars, setCalendars] = useState(initialCalendars);
|
||||
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
|
||||
initialCalendars[0]?.id || ""
|
||||
@ -254,6 +411,29 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
||||
location: null,
|
||||
});
|
||||
|
||||
const [selectedEventPreview, setSelectedEventPreview] = useState<Event | null>(null);
|
||||
const [statistics, setStatistics] = useState({
|
||||
totalEvents: initialCalendars.reduce((acc, cal) => acc + cal.events.length, 0),
|
||||
upcomingEvents: initialCalendars.reduce((acc, cal) =>
|
||||
acc + cal.events.filter(e => new Date(e.start) > new Date()).length, 0
|
||||
),
|
||||
completedEvents: initialCalendars.reduce((acc, cal) =>
|
||||
acc + cal.events.filter(e => new Date(e.end) < new Date()).length, 0
|
||||
),
|
||||
meetingHours: initialCalendars.reduce((acc, cal) =>
|
||||
acc + cal.events.reduce((hours, e) => {
|
||||
const duration = (new Date(e.end).getTime() - new Date(e.start).getTime()) / (1000 * 60 * 60);
|
||||
return hours + duration;
|
||||
}, 0)
|
||||
, 0)
|
||||
});
|
||||
|
||||
const upcomingEvents = initialCalendars
|
||||
.flatMap(cal => cal.events)
|
||||
.filter(event => new Date(event.start) > new Date())
|
||||
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
|
||||
.slice(0, 5);
|
||||
|
||||
const calendarRef = useRef<any>(null);
|
||||
|
||||
const handleCalendarSave = async (calendarData: Partial<Calendar>) => {
|
||||
@ -412,114 +592,161 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<div className="p-4 mb-4 text-red-500 bg-red-50 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Calendar management */}
|
||||
<div className="flex flex-wrap justify-between items-center gap-4 mb-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{calendars.map((calendar) => (
|
||||
<Button
|
||||
key={calendar.id}
|
||||
variant={calendar.id === selectedCalendarId ? "default" : "outline"}
|
||||
onClick={() => setSelectedCalendarId(calendar.id)}
|
||||
style={{ backgroundColor: calendar.id === selectedCalendarId ? calendar.color : undefined }}
|
||||
>
|
||||
{calendar.name}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSelectedCalendar(null);
|
||||
setIsCalendarModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Nouveau calendrier
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedEvent(null);
|
||||
setIsEventModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Nouvel événement
|
||||
</Button>
|
||||
</div>
|
||||
<StatisticsPanel statistics={statistics} />
|
||||
|
||||
{/* View selector */}
|
||||
<Tabs value={view} className="w-full">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger
|
||||
value="dayGridMonth"
|
||||
onClick={() => handleViewChange("dayGridMonth")}
|
||||
>
|
||||
Mois
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="timeGridWeek"
|
||||
onClick={() => handleViewChange("timeGridWeek")}
|
||||
>
|
||||
Semaine
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="timeGridDay"
|
||||
onClick={() => handleViewChange("timeGridDay")}
|
||||
>
|
||||
Jour
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-8">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSelectedCalendar(null);
|
||||
setIsCalendarModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Nouveau calendrier
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedEvent(null);
|
||||
setIsEventModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Nouvel événement
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Calendar display */}
|
||||
<Card className="p-4">
|
||||
{loading ? (
|
||||
<div className="h-96 flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<span className="ml-2">Chargement des événements...</span>
|
||||
<Tabs value={view} className="w-auto">
|
||||
<TabsList>
|
||||
<TabsTrigger
|
||||
value="dayGridMonth"
|
||||
onClick={() => handleViewChange("dayGridMonth")}
|
||||
>
|
||||
Mois
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="timeGridWeek"
|
||||
onClick={() => handleViewChange("timeGridWeek")}
|
||||
>
|
||||
Semaine
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="timeGridDay"
|
||||
onClick={() => handleViewChange("timeGridDay")}
|
||||
>
|
||||
Jour
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
) : (
|
||||
<FullCalendar
|
||||
ref={calendarRef}
|
||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||
initialView={view}
|
||||
headerToolbar={{
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: "",
|
||||
}}
|
||||
events={calendars.flatMap(cal =>
|
||||
cal.events.map(event => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
allDay: event.isAllDay,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
calendarId: event.calendarId,
|
||||
originalEvent: event,
|
||||
backgroundColor: cal.color,
|
||||
}))
|
||||
)}
|
||||
locale={frLocale}
|
||||
selectable={true}
|
||||
selectMirror={true}
|
||||
dayMaxEvents={true}
|
||||
weekends={true}
|
||||
select={handleDateSelect}
|
||||
eventClick={handleEventClick}
|
||||
height="auto"
|
||||
aspectRatio={1.8}
|
||||
|
||||
{loading ? (
|
||||
<div className="h-96 flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<span className="ml-2">Chargement des événements...</span>
|
||||
</div>
|
||||
) : (
|
||||
<FullCalendar
|
||||
ref={calendarRef}
|
||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||
initialView={view}
|
||||
headerToolbar={{
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: "",
|
||||
}}
|
||||
events={calendars.flatMap(cal =>
|
||||
cal.events.map(event => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
allDay: event.isAllDay,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
calendarId: event.calendarId,
|
||||
originalEvent: event,
|
||||
backgroundColor: cal.color,
|
||||
textColor: '#ffffff',
|
||||
borderColor: cal.color,
|
||||
}))
|
||||
)}
|
||||
locale={frLocale}
|
||||
selectable={true}
|
||||
selectMirror={true}
|
||||
dayMaxEvents={true}
|
||||
weekends={true}
|
||||
select={handleDateSelect}
|
||||
eventClick={(clickInfo) => {
|
||||
handleEventClick(clickInfo);
|
||||
setSelectedEventPreview(clickInfo.event.extendedProps.originalEvent);
|
||||
}}
|
||||
height="auto"
|
||||
aspectRatio={1.8}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-span-4 space-y-4">
|
||||
{selectedEventPreview ? (
|
||||
<EventPreview
|
||||
event={selectedEventPreview}
|
||||
calendar={calendars.find(c => c.id === selectedEventPreview.calendarId)!}
|
||||
/>
|
||||
) : (
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium">Mini-calendrier</h3>
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
headerToolbar={false}
|
||||
height="auto"
|
||||
aspectRatio={1}
|
||||
events={calendars.flatMap(cal =>
|
||||
cal.events.map(event => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
backgroundColor: cal.color,
|
||||
}))
|
||||
)}
|
||||
locale={frLocale}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
</Tabs>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="font-medium mb-4">Calendriers</h3>
|
||||
<div className="space-y-2">
|
||||
{calendars.map((calendar) => (
|
||||
<Button
|
||||
key={calendar.id}
|
||||
variant={calendar.id === selectedCalendarId ? "secondary" : "ghost"}
|
||||
className="w-full justify-start"
|
||||
onClick={() => setSelectedCalendarId(calendar.id)}
|
||||
>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full mr-2"
|
||||
style={{ backgroundColor: calendar.color }}
|
||||
/>
|
||||
{calendar.name}
|
||||
<Badge variant="outline" className="ml-auto">
|
||||
{calendar.events.length}
|
||||
</Badge>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar dialog */}
|
||||
<CalendarDialog
|
||||
|
||||
Loading…
Reference in New Issue
Block a user