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 { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
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 { Calendar, Event } from "@prisma/client";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Label } from "@/components/ui/label";
|
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
|
// Predefined professional color palette
|
||||||
const colorPalette = [
|
const colorPalette = [
|
||||||
@ -35,6 +58,11 @@ const colorPalette = [
|
|||||||
interface CalendarClientProps {
|
interface CalendarClientProps {
|
||||||
initialCalendars: (Calendar & { events: Event[] })[];
|
initialCalendars: (Calendar & { events: Event[] })[];
|
||||||
userId: string;
|
userId: string;
|
||||||
|
userProfile: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
avatar?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventFormData {
|
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 [calendars, setCalendars] = useState(initialCalendars);
|
||||||
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
|
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
|
||||||
initialCalendars[0]?.id || ""
|
initialCalendars[0]?.id || ""
|
||||||
@ -254,6 +411,29 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
location: null,
|
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 calendarRef = useRef<any>(null);
|
||||||
|
|
||||||
const handleCalendarSave = async (calendarData: Partial<Calendar>) => {
|
const handleCalendarSave = async (calendarData: Partial<Calendar>) => {
|
||||||
@ -412,25 +592,13 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{error && (
|
<StatisticsPanel statistics={statistics} />
|
||||||
<div className="p-4 mb-4 text-red-500 bg-red-50 rounded-md">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Calendar management */}
|
<div className="grid grid-cols-12 gap-4">
|
||||||
<div className="flex flex-wrap justify-between items-center gap-4 mb-4">
|
<div className="col-span-8">
|
||||||
<div className="flex flex-wrap gap-2">
|
<Card className="p-4">
|
||||||
{calendars.map((calendar) => (
|
<div className="flex items-center justify-between mb-4">
|
||||||
<Button
|
<div className="flex items-center gap-4">
|
||||||
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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -441,7 +609,6 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Nouveau calendrier
|
Nouveau calendrier
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEvent(null);
|
setSelectedEvent(null);
|
||||||
@ -453,9 +620,8 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View selector */}
|
<Tabs value={view} className="w-auto">
|
||||||
<Tabs value={view} className="w-full">
|
<TabsList>
|
||||||
<TabsList className="mb-4">
|
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="dayGridMonth"
|
value="dayGridMonth"
|
||||||
onClick={() => handleViewChange("dayGridMonth")}
|
onClick={() => handleViewChange("dayGridMonth")}
|
||||||
@ -475,9 +641,9 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
Jour
|
Jour
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Calendar display */}
|
|
||||||
<Card className="p-4">
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="h-96 flex items-center justify-center">
|
<div className="h-96 flex items-center justify-center">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
@ -505,6 +671,8 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
calendarId: event.calendarId,
|
calendarId: event.calendarId,
|
||||||
originalEvent: event,
|
originalEvent: event,
|
||||||
backgroundColor: cal.color,
|
backgroundColor: cal.color,
|
||||||
|
textColor: '#ffffff',
|
||||||
|
borderColor: cal.color,
|
||||||
}))
|
}))
|
||||||
)}
|
)}
|
||||||
locale={frLocale}
|
locale={frLocale}
|
||||||
@ -513,13 +681,72 @@ export function CalendarClient({ initialCalendars, userId }: CalendarClientProps
|
|||||||
dayMaxEvents={true}
|
dayMaxEvents={true}
|
||||||
weekends={true}
|
weekends={true}
|
||||||
select={handleDateSelect}
|
select={handleDateSelect}
|
||||||
eventClick={handleEventClick}
|
eventClick={(clickInfo) => {
|
||||||
|
handleEventClick(clickInfo);
|
||||||
|
setSelectedEventPreview(clickInfo.event.extendedProps.originalEvent);
|
||||||
|
}}
|
||||||
height="auto"
|
height="auto"
|
||||||
aspectRatio={1.8}
|
aspectRatio={1.8}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Tabs>
|
</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 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 */}
|
{/* Calendar dialog */}
|
||||||
<CalendarDialog
|
<CalendarDialog
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user