Improve planning a lot
This commit is contained in:
parent
34d700f2f4
commit
6b4710a902
4 changed files with 465 additions and 140 deletions
|
|
@ -3,6 +3,17 @@ import { useState } from "react";
|
|||
import { useTablosList } from "@ui/hooks/tablos";
|
||||
import { useCreateEvent } from "@ui/hooks/events";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import {
|
||||
Select,
|
||||
SelectButton,
|
||||
SelectPopover,
|
||||
SelectListBox,
|
||||
SelectListItem,
|
||||
} from "@ui/ui-library/select";
|
||||
import { useTimePicker } from "@ui/ui-library/time-picker";
|
||||
import { DatePicker, DatePickerButton } from "@ui/ui-library/date-picker";
|
||||
import { Group } from "react-aria-components";
|
||||
import { getLocalTimeZone, parseDate, today } from "@internationalized/date";
|
||||
|
||||
interface EventModalProps {
|
||||
date: Date;
|
||||
|
|
@ -13,9 +24,43 @@ export const CreateEventModal = ({ date, onClose }: EventModalProps) => {
|
|||
const user = useUser();
|
||||
const { data: tablos, isLoading: tablosLoading } = useTablosList();
|
||||
const createEvent = useCreateEvent();
|
||||
const timeOptions = useTimePicker({ intervalInMinute: 15 });
|
||||
|
||||
// Get the local date string without timezone conversion
|
||||
const getLocalDateString = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// Find the nearest time option to the selected date
|
||||
const getNearestTimeOption = (date: Date, type: "start" | "end") => {
|
||||
const dateMinutes = date.getHours() * 60 + date.getMinutes();
|
||||
|
||||
let nearestOption = timeOptions[0];
|
||||
let smallestDiff = Infinity;
|
||||
|
||||
for (const option of timeOptions) {
|
||||
const optionMinutes = option.hour * 60 + option.minute;
|
||||
const diff =
|
||||
type === "start"
|
||||
? Math.abs(dateMinutes - optionMinutes)
|
||||
: dateMinutes + 30 - optionMinutes;
|
||||
|
||||
if (0 <= diff && diff < smallestDiff) {
|
||||
smallestDiff = diff;
|
||||
nearestOption = option;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestOption?.id || "";
|
||||
};
|
||||
|
||||
const [createdEvent, setCreatedEvent] = useState<EventInsert>({
|
||||
start_date: date?.toISOString().split("T")[0] || "",
|
||||
start_time: date?.toISOString().split("T")[1] || "",
|
||||
start_date: date ? getLocalDateString(date) : "",
|
||||
start_time: date ? getNearestTimeOption(date, "start") : "",
|
||||
end_time: date ? getNearestTimeOption(date, "end") : "",
|
||||
tablo_id: "",
|
||||
title: "",
|
||||
created_by: user.id,
|
||||
|
|
@ -31,6 +76,7 @@ export const CreateEventModal = ({ date, onClose }: EventModalProps) => {
|
|||
<button
|
||||
onClick={onClose}
|
||||
className="text-white hover:text-gray-200 transition-colors"
|
||||
aria-label="Fermer le modal"
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
|
|
@ -72,6 +118,7 @@ export const CreateEventModal = ({ date, onClose }: EventModalProps) => {
|
|||
}
|
||||
className="w-full text-lg font-medium border-none outline-none bg-transparent text-gray-900 dark:text-white placeholder-gray-400 focus:ring-0 px-0"
|
||||
placeholder="Ajouter un titre"
|
||||
aria-label="Titre de l'événement"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="border-b border-gray-200 dark:border-gray-700"></div>
|
||||
|
|
@ -82,61 +129,121 @@ export const CreateEventModal = ({ date, onClose }: EventModalProps) => {
|
|||
<label className="block text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Tablo *
|
||||
</label>
|
||||
<select
|
||||
value={createdEvent?.tablo_id}
|
||||
onChange={(e) =>
|
||||
<Select
|
||||
placeholder="Sélectionner un tablo"
|
||||
selectedKey={createdEvent?.tablo_id}
|
||||
onSelectionChange={(key) =>
|
||||
setCreatedEvent({
|
||||
...createdEvent,
|
||||
tablo_id: e.target.value,
|
||||
tablo_id: key as string,
|
||||
} as Event)
|
||||
}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white transition-all"
|
||||
disabled={tablosLoading}
|
||||
className="w-full"
|
||||
aria-label="Sélectionner un tablo"
|
||||
isDisabled={tablosLoading}
|
||||
>
|
||||
<option value="">Sélectionner un tablo</option>
|
||||
{tablos?.map((tablo) => (
|
||||
<option key={tablo.id} value={tablo.id}>
|
||||
{tablo.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectButton className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white transition-all text-left" />
|
||||
<SelectPopover>
|
||||
<SelectListBox>
|
||||
{tablos?.map((tablo) => (
|
||||
<SelectListItem key={tablo.id} id={tablo.id}>
|
||||
{tablo.name}
|
||||
</SelectListItem>
|
||||
))}
|
||||
</SelectListBox>
|
||||
</SelectPopover>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Time Selection */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
<Group className="flex flex-row gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Date
|
||||
</label>
|
||||
<DatePicker
|
||||
aria-label="Date de l'événement"
|
||||
value={
|
||||
createdEvent?.start_date
|
||||
? parseDate(createdEvent?.start_date)
|
||||
: null
|
||||
}
|
||||
minValue={today(getLocalTimeZone())}
|
||||
onChange={(value) => {
|
||||
if (value === null) {
|
||||
return;
|
||||
}
|
||||
setCreatedEvent({
|
||||
...createdEvent,
|
||||
start_date: value.toString(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DatePickerButton className="h-[36px]" />
|
||||
</DatePicker>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Début
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={createdEvent?.start_time}
|
||||
onChange={(e) =>
|
||||
setCreatedEvent({
|
||||
...createdEvent,
|
||||
start_time: e.target.value,
|
||||
} as Event)
|
||||
}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white transition-all"
|
||||
/>
|
||||
<Select
|
||||
aria-label="Heure de début"
|
||||
className="min-w-[110px]"
|
||||
selectedKey={createdEvent?.start_time}
|
||||
onSelectionChange={(value) => {
|
||||
const option = timeOptions.find(
|
||||
(option) => option.id === value
|
||||
);
|
||||
if (option) {
|
||||
setCreatedEvent({
|
||||
...createdEvent,
|
||||
start_time: value.toString(),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectButton />
|
||||
<SelectPopover className="w-36">
|
||||
<SelectListBox items={timeOptions}>
|
||||
{(item) => {
|
||||
return <SelectListItem>{item.value}</SelectListItem>;
|
||||
}}
|
||||
</SelectListBox>
|
||||
</SelectPopover>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Fin
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={createdEvent?.end_time ?? ""}
|
||||
onChange={(e) =>
|
||||
setCreatedEvent({
|
||||
...createdEvent,
|
||||
end_time: e.target.value,
|
||||
} as Event)
|
||||
}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white transition-all"
|
||||
/>
|
||||
<Select
|
||||
aria-label="Heure de fin"
|
||||
className="min-w-[110px]"
|
||||
selectedKey={createdEvent?.end_time}
|
||||
onSelectionChange={(value) => {
|
||||
const option = timeOptions.find(
|
||||
(option) => option.id === value
|
||||
);
|
||||
if (option) {
|
||||
setCreatedEvent({
|
||||
...createdEvent,
|
||||
end_time: value.toString(),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectButton />
|
||||
<SelectPopover className="w-36">
|
||||
<SelectListBox items={timeOptions}>
|
||||
{(item) => {
|
||||
return <SelectListItem>{item.value}</SelectListItem>;
|
||||
}}
|
||||
</SelectListBox>
|
||||
</SelectPopover>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
|
|
@ -154,6 +261,7 @@ export const CreateEventModal = ({ date, onClose }: EventModalProps) => {
|
|||
rows={3}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white resize-none transition-all"
|
||||
placeholder="Ajouter une description (optionnel)"
|
||||
aria-label="Description de l'événement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -164,16 +272,22 @@ export const CreateEventModal = ({ date, onClose }: EventModalProps) => {
|
|||
type="button"
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
onClick={onClose}
|
||||
aria-label="Annuler la création d'événement"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-6 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm hover:shadow-md"
|
||||
onClick={() =>
|
||||
createEvent(createdEvent, { onSuccess: () => onClose() })
|
||||
}
|
||||
disabled={!createdEvent?.title.trim() || !createdEvent?.tablo_id}
|
||||
onClick={() => {
|
||||
const eventName = createdEvent?.title.trim() || "(Sans titre)";
|
||||
createEvent(
|
||||
{ ...createdEvent, title: eventName },
|
||||
{ onSuccess: () => onClose() }
|
||||
);
|
||||
}}
|
||||
disabled={!createdEvent?.tablo_id}
|
||||
aria-label="Enregistrer l'événement"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,20 @@ import {
|
|||
} from "@ui/types/events.types";
|
||||
|
||||
// Fetch events for a specific tablo
|
||||
export const useEventsByTablo = (tabloId: string) => {
|
||||
export const useEventsByTablo = (tabloId: string | null) => {
|
||||
return useQuery({
|
||||
queryKey: ["events", tabloId],
|
||||
queryFn: async () => {
|
||||
if (!tabloId) {
|
||||
const { data, error } = await supabase
|
||||
.from("events_and_tablos")
|
||||
.select("*")
|
||||
.order("start_date", { ascending: true })
|
||||
.order("start_time", { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
return data as EventAndTablo[];
|
||||
}
|
||||
const { data, error } = await supabase
|
||||
.from("events_and_tablos")
|
||||
.select("*")
|
||||
|
|
@ -24,7 +34,6 @@ export const useEventsByTablo = (tabloId: string) => {
|
|||
if (error) throw error;
|
||||
return data as EventAndTablo[];
|
||||
},
|
||||
enabled: !!tabloId,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -92,8 +101,8 @@ export const useCreateEvent = () => {
|
|||
if (error) throw error;
|
||||
return data as Event;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["events", data.tablo_id] });
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["events"] });
|
||||
toast.add(
|
||||
{
|
||||
title: "Événement créé",
|
||||
|
|
|
|||
|
|
@ -1,29 +1,33 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTablosList } from "@ui/hooks/tablos";
|
||||
import { useEventsByTablo, useDeleteEvent } from "@ui/hooks/events";
|
||||
import { CreateEventModal } from "@ui/components/CreateEventModal";
|
||||
import {
|
||||
Select,
|
||||
SelectButton,
|
||||
SelectPopover,
|
||||
SelectListBox,
|
||||
SelectListItem,
|
||||
} from "@ui/ui-library/select";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
type ViewType = "month" | "week" | "day";
|
||||
|
||||
export const PlanningPage = () => {
|
||||
const { tablo_id } = useParams();
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
const [currentView, setCurrentView] = useState<ViewType>("month");
|
||||
const [selectedTabloId, setSelectedTabloId] = useState<string>("");
|
||||
const [selectedTabloId, setSelectedTabloId] = useState<string>(
|
||||
tablo_id || "all"
|
||||
);
|
||||
|
||||
// Fetch tablos
|
||||
const { data: tablos, isLoading: tablosLoading } = useTablosList();
|
||||
|
||||
// Set default tablo if none selected
|
||||
useEffect(() => {
|
||||
if (tablos && tablos.length > 0 && !selectedTabloId) {
|
||||
setSelectedTabloId(tablos[0].id);
|
||||
}
|
||||
}, [tablos, selectedTabloId]);
|
||||
|
||||
// Fetch events for selected tablo
|
||||
const { data: events = [], isLoading: eventsLoading } =
|
||||
useEventsByTablo(selectedTabloId);
|
||||
// Fetch events for selected tablo or all tablos
|
||||
const { data: tabloEvents = [], isLoading: tabloEventsLoading } =
|
||||
useEventsByTablo(selectedTabloId !== "all" ? selectedTabloId : null);
|
||||
|
||||
const deleteEvent = useDeleteEvent();
|
||||
|
||||
|
|
@ -64,9 +68,68 @@ export const PlanningPage = () => {
|
|||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
// Remove seconds from time string (HH:MM:SS -> HH:MM)
|
||||
return time.substring(0, 5);
|
||||
};
|
||||
|
||||
const calculateEventHeight = (startTime: string, endTime: string) => {
|
||||
if (!endTime) return 60; // Default height if no end time
|
||||
|
||||
const [startHour, startMinute] = startTime.split(":").map(Number);
|
||||
const [endHour, endMinute] = endTime.split(":").map(Number);
|
||||
|
||||
const startTotalMinutes = startHour * 60 + startMinute;
|
||||
const endTotalMinutes = endHour * 60 + endMinute;
|
||||
|
||||
const durationMinutes = endTotalMinutes - startTotalMinutes;
|
||||
|
||||
// Each hour is 60px, so calculate proportional height
|
||||
return Math.max(durationMinutes, 30); // Minimum 30px height
|
||||
};
|
||||
|
||||
const calculateEventOffset = (startTime: string, slotTime: string) => {
|
||||
const [startHour, startMinute] = startTime.split(":").map(Number);
|
||||
const [slotHour] = slotTime.split(":").map(Number);
|
||||
|
||||
if (startHour === slotHour) {
|
||||
// Calculate offset within the hour slot
|
||||
return startMinute; // Each minute is 1px
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const getEventsForDate = (date: Date) => {
|
||||
const dateString = formatDate(date);
|
||||
return events.filter((event) => event.start_date === dateString);
|
||||
return tabloEvents.filter((event) => event.start_date === dateString);
|
||||
};
|
||||
|
||||
const getCurrentTimePosition = () => {
|
||||
const now = new Date();
|
||||
const minutes = now.getMinutes();
|
||||
// Position within the current hour slot (0-60px based on minutes)
|
||||
return minutes;
|
||||
};
|
||||
|
||||
const isWithinCurrentHour = (fullDate: Date, time: string) => {
|
||||
const now = new Date();
|
||||
const nowHour = now.getHours();
|
||||
// const nowMinute = now.getMinutes();
|
||||
const nowDay = now.getDate();
|
||||
|
||||
fullDate.setHours(
|
||||
Number(time.split(":")[0]),
|
||||
Number(time.split(":")[1]),
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
const hour = fullDate.getHours();
|
||||
// const minute = fullDate.getMinutes();
|
||||
const day = fullDate.getDate();
|
||||
|
||||
return nowDay === day && nowHour === hour;
|
||||
};
|
||||
|
||||
const navigateDate = (direction: number) => {
|
||||
|
|
@ -220,20 +283,31 @@ export const PlanningPage = () => {
|
|||
.map((event) => (
|
||||
<div
|
||||
key={event.event_id}
|
||||
className={`text-xs px-2 py-1 rounded text-white ${event.tablo_color} truncate cursor-pointer hover:opacity-80 group relative`}
|
||||
title={`${event.start_time} ${event.title}`}
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded text-white ${event.tablo_color} truncate cursor-pointer hover:opacity-80 group relative leading-tight`}
|
||||
title={`${formatTime(event.start_time)} ${event.title}${
|
||||
selectedTabloId === "all" && event.tablo_name
|
||||
? ` - ${event.tablo_name}`
|
||||
: ""
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsEventModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
<div className="truncate">
|
||||
{formatTime(event.start_time)} {event.title}
|
||||
{selectedTabloId === "all" && event.tablo_name && (
|
||||
<span className="opacity-75 ml-1">
|
||||
• {event.tablo_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteEvent.mutate(event.event_id);
|
||||
}}
|
||||
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity text-xs flex items-center justify-center hover:bg-red-600"
|
||||
className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all text-sm flex items-center justify-center hover:bg-red-600 hover:scale-110 shadow-sm"
|
||||
title="Supprimer l'événement"
|
||||
>
|
||||
×
|
||||
|
|
@ -299,25 +373,78 @@ export const PlanningPage = () => {
|
|||
index === 6 ? "border-r-0" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedDate(day);
|
||||
const [hour] = time.split(":").map(Number);
|
||||
const dateWithTime = new Date(day);
|
||||
dateWithTime.setHours(hour, 0, 0, 0);
|
||||
setSelectedDate(dateWithTime);
|
||||
setIsEventModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{/* Current time indicator for today */}
|
||||
{isWithinCurrentHour(day, time) && (
|
||||
<div
|
||||
className="absolute left-0 right-0 h-0.5 bg-red-500 z-20 pointer-events-none"
|
||||
style={{
|
||||
top: `${getCurrentTimePosition()}px`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute -left-1 -top-1 w-2 h-2 bg-red-500 rounded-full"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{getEventsForDate(day)
|
||||
.filter((event) =>
|
||||
event.start_time.startsWith(time.split(":")[0])
|
||||
)
|
||||
.map((event) => (
|
||||
<div
|
||||
key={event.event_id}
|
||||
className={`absolute left-1 right-1 top-1 p-1 rounded text-white ${event.tablo_color} text-xs`}
|
||||
>
|
||||
<div className="font-medium truncate">{event.title}</div>
|
||||
<div className="opacity-75">
|
||||
{event.start_time} - {event.end_time}
|
||||
.map((event) => {
|
||||
const eventHeight = calculateEventHeight(
|
||||
event.start_time,
|
||||
event.end_time
|
||||
);
|
||||
const eventOffset = calculateEventOffset(
|
||||
event.start_time,
|
||||
time
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={event.event_id}
|
||||
className={`absolute left-1 right-1 p-0.5 rounded text-white ${event.tablo_color} text-xs overflow-hidden z-10 group cursor-pointer hover:opacity-90`}
|
||||
style={{
|
||||
top: `${eventOffset}px`,
|
||||
height: `${eventHeight}px`,
|
||||
minHeight: "30px",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="text-[10px] font-medium truncate leading-tight">
|
||||
{event.title}
|
||||
{selectedTabloId === "all" && event.tablo_name && (
|
||||
<span className="opacity-75 ml-1">
|
||||
• {event.tablo_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{eventHeight >= 30 && (
|
||||
<div className="text-[9px] opacity-75 leading-tight">
|
||||
{formatTime(event.start_time)} -{" "}
|
||||
{formatTime(event.end_time)}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteEvent.mutate(event.event_id);
|
||||
}}
|
||||
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all text-xs flex items-center justify-center hover:bg-red-600 hover:scale-110 shadow-sm z-30"
|
||||
title="Supprimer l'événement"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -345,7 +472,10 @@ export const PlanningPage = () => {
|
|||
key={time}
|
||||
className="flex border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer relative min-h-[60px]"
|
||||
onClick={() => {
|
||||
setSelectedDate(currentDate);
|
||||
const [hour] = time.split(":").map(Number);
|
||||
const dateWithTime = new Date(currentDate);
|
||||
dateWithTime.setHours(hour, 0, 0, 0);
|
||||
setSelectedDate(dateWithTime);
|
||||
setIsEventModalOpen(true);
|
||||
}}
|
||||
>
|
||||
|
|
@ -353,26 +483,76 @@ export const PlanningPage = () => {
|
|||
{time}
|
||||
</div>
|
||||
<div className="flex-1 p-2 relative">
|
||||
{/* Current time indicator for today */}
|
||||
{isWithinCurrentHour(new Date(currentDate), time) && (
|
||||
<div
|
||||
className="absolute left-0 right-0 h-0.5 bg-red-500 z-20 pointer-events-none"
|
||||
style={{
|
||||
top: `${getCurrentTimePosition()}px`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute -left-1 -top-1 w-2 h-2 bg-red-500 rounded-full"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{getEventsForDate(currentDate)
|
||||
.filter((event) =>
|
||||
event.start_time.startsWith(time.split(":")[0])
|
||||
)
|
||||
.map((event) => (
|
||||
<div
|
||||
key={event.event_id}
|
||||
className={`p-2 rounded text-white ${event.tablo_color} mb-1`}
|
||||
>
|
||||
<div className="font-medium">{event.title}</div>
|
||||
<div className="text-sm opacity-75">
|
||||
{event.start_time} - {event.end_time}
|
||||
</div>
|
||||
{event.description && (
|
||||
<div className="text-sm mt-1 opacity-75">
|
||||
{event.description}
|
||||
.map((event) => {
|
||||
const eventHeight = calculateEventHeight(
|
||||
event.start_time,
|
||||
event.end_time
|
||||
);
|
||||
const eventOffset = calculateEventOffset(
|
||||
event.start_time,
|
||||
time
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={event.event_id}
|
||||
className={`absolute left-2 right-2 p-1 rounded text-white ${event.tablo_color} overflow-hidden z-10 group cursor-pointer hover:opacity-90`}
|
||||
style={{
|
||||
top: `${eventOffset}px`,
|
||||
height: `${eventHeight}px`,
|
||||
minHeight: "30px",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="text-[10px] font-medium truncate leading-tight">
|
||||
{event.title}
|
||||
{selectedTabloId === "all" && event.tablo_name && (
|
||||
<span className="opacity-75 ml-1">
|
||||
• {event.tablo_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{eventHeight >= 30 && (
|
||||
<div className="text-[9px] opacity-75 leading-tight">
|
||||
{formatTime(event.start_time)} -{" "}
|
||||
{formatTime(event.end_time)}
|
||||
</div>
|
||||
)}
|
||||
{eventHeight >= 75 && event.description && (
|
||||
<div className="text-[8px] mt-0.5 opacity-75 leading-tight">
|
||||
{event.description}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteEvent.mutate(event.event_id);
|
||||
}}
|
||||
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all text-xs flex items-center justify-center hover:bg-red-600 hover:scale-110 shadow-sm z-30"
|
||||
title="Supprimer l'événement"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -391,27 +571,32 @@ export const PlanningPage = () => {
|
|||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Tablo
|
||||
</label>
|
||||
<select
|
||||
value={selectedTabloId}
|
||||
onChange={(e) => setSelectedTabloId(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
disabled={tablosLoading}
|
||||
<Select
|
||||
placeholder={
|
||||
tablosLoading ? "Chargement..." : "Sélectionner un tablo"
|
||||
}
|
||||
selectedKey={selectedTabloId}
|
||||
onSelectionChange={(key) => setSelectedTabloId(key as string)}
|
||||
className="w-full"
|
||||
isDisabled={tablosLoading}
|
||||
>
|
||||
<option value="">
|
||||
{tablosLoading ? "Chargement..." : "Sélectionner un tablo"}
|
||||
</option>
|
||||
{tablos?.map((tablo) => (
|
||||
<option key={tablo.id} value={tablo.id}>
|
||||
{tablo.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectButton className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white text-left" />
|
||||
<SelectPopover>
|
||||
<SelectListBox>
|
||||
<SelectListItem id="all">Tous les tablos</SelectListItem>
|
||||
{tablos?.map((tablo) => (
|
||||
<SelectListItem key={tablo.id} id={tablo.id}>
|
||||
{tablo.name}
|
||||
</SelectListItem>
|
||||
))}
|
||||
</SelectListBox>
|
||||
</SelectPopover>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIsEventModalOpen(true)}
|
||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!selectedTabloId}
|
||||
>
|
||||
+ Créer un événement
|
||||
</button>
|
||||
|
|
@ -541,21 +726,13 @@ export const PlanningPage = () => {
|
|||
|
||||
{/* Calendar Views */}
|
||||
<div className="flex-1 p-4">
|
||||
{eventsLoading && selectedTabloId ? (
|
||||
{tabloEventsLoading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Chargement des événements...
|
||||
</span>
|
||||
</div>
|
||||
) : !selectedTabloId ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Sélectionnez un tablo pour voir les événements
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{currentView === "month" && renderMonthView()}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
export type TimeOption = {
|
||||
hour: number;
|
||||
|
|
@ -9,35 +9,60 @@ export type TimeOption = {
|
|||
|
||||
export function useTimePicker({
|
||||
intervalInMinute,
|
||||
format = "24h",
|
||||
}: {
|
||||
intervalInMinute: 15 | 30;
|
||||
format?: "12h" | "24h";
|
||||
}): Array<TimeOption> {
|
||||
return React.useMemo(() => {
|
||||
const options = [];
|
||||
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const period = hour >= 12 ? 'PM' : 'AM';
|
||||
let hourIn12Format = hour % 12;
|
||||
if (format === "12h") {
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const period = hour >= 12 ? "PM" : "AM";
|
||||
let hourIn12Format = hour % 12;
|
||||
|
||||
if (hourIn12Format === 0) {
|
||||
hourIn12Format = 12;
|
||||
if (hourIn12Format === 0) {
|
||||
hourIn12Format = 12;
|
||||
}
|
||||
|
||||
for (
|
||||
let interval = 0;
|
||||
interval < Math.floor(60 / intervalInMinute);
|
||||
interval++
|
||||
) {
|
||||
const minutes = interval * intervalInMinute;
|
||||
options.push({
|
||||
hour,
|
||||
minute: minutes,
|
||||
value: `${hourIn12Format}:${
|
||||
minutes === 0 ? "00" : minutes
|
||||
} ${period}`,
|
||||
id: `${hourIn12Format}:${minutes === 0 ? "00" : minutes} ${period}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (
|
||||
let interval = 0;
|
||||
interval < Math.floor(60 / intervalInMinute);
|
||||
interval++
|
||||
) {
|
||||
const minutes = interval * intervalInMinute;
|
||||
options.push({
|
||||
hour,
|
||||
minute: minutes,
|
||||
value: `${hourIn12Format}:${minutes === 0 ? '00' : minutes} ${period}`,
|
||||
id: `${hourIn12Format}:${minutes === 0 ? '00' : minutes} ${period}`,
|
||||
});
|
||||
} else {
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (
|
||||
let interval = 0;
|
||||
interval < Math.floor(60 / intervalInMinute);
|
||||
interval++
|
||||
) {
|
||||
const minutes = interval * intervalInMinute;
|
||||
options.push({
|
||||
hour,
|
||||
minute: minutes,
|
||||
value: `${hour.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}`,
|
||||
id: `${hour.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [intervalInMinute]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue