feat(ui): improve dark mode surfaces and planning event actions
This commit is contained in:
parent
421676c3f0
commit
737cd5f073
3 changed files with 1213 additions and 974 deletions
|
|
@ -295,7 +295,7 @@ export function GanttChart({
|
|||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="px-4 py-2 bg-card border border-border rounded-lg min-w-[200px] text-center">
|
||||
<div className="px-4 py-2 bg-card dark:bg-gray-900 border border-border dark:border-gray-700 rounded-lg min-w-[200px] text-center">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{formatDateRange(periodStart, periodEnd)}
|
||||
</span>
|
||||
|
|
@ -334,7 +334,7 @@ export function GanttChart({
|
|||
|
||||
{/* Gantt chart */}
|
||||
<div
|
||||
className="bg-card rounded-xl border border-border overflow-hidden shadow-sm"
|
||||
className="bg-card dark:bg-gray-900 rounded-xl border border-border dark:border-gray-700 overflow-hidden shadow-sm"
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="overflow-x-auto">
|
||||
|
|
@ -441,7 +441,7 @@ export function GanttChart({
|
|||
{/* Title */}
|
||||
<h3
|
||||
className={twMerge(
|
||||
"font-semibold text-foreground leading-tight line-clamp-1",
|
||||
"font-semibold text-gray-900 leading-tight line-clamp-1",
|
||||
isCompact ? "mt-1 text-xs" : "mt-2 text-sm",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,13 @@ import { WebcalModal } from "@ui/components/WebcalModal";
|
|||
import { downloadICSFile, generateICSFromEvents, toast } from "@xtablo/shared";
|
||||
import { EventAndTablo } from "@xtablo/shared/types/events.types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@xtablo/ui/components/dropdown-menu";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -11,10 +18,33 @@ import {
|
|||
SelectValue,
|
||||
} from "@xtablo/ui/components/select";
|
||||
import { TypographyH3, TypographyH4 } from "@xtablo/ui/components/typography";
|
||||
import { ClockIcon, Download, EllipsisVerticalIcon, FolderInputIcon, PlusIcon, RefreshCcw } from "lucide-react";
|
||||
import {
|
||||
ClockIcon,
|
||||
Compass,
|
||||
Download,
|
||||
EllipsisVerticalIcon,
|
||||
Flame,
|
||||
FolderIcon,
|
||||
FolderInputIcon,
|
||||
Gem,
|
||||
Heart,
|
||||
Leaf,
|
||||
PlusIcon,
|
||||
RefreshCcw,
|
||||
Sparkles,
|
||||
Star,
|
||||
Sun,
|
||||
Waves,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Outlet, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
Outlet,
|
||||
useNavigate,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
} from "react-router-dom";
|
||||
import { useDeleteEvent, useEventsByTablo } from "../hooks/events";
|
||||
import { useGetAllTabloAccess, useTablosList } from "../hooks/tablos";
|
||||
import { useIsReadOnlyUser } from "../providers/UserStoreProvider";
|
||||
|
|
@ -22,6 +52,43 @@ import { EventModal } from "../components/EventModal";
|
|||
|
||||
type ViewType = "month" | "week" | "day";
|
||||
|
||||
function getTabloIcon(color: string | null | undefined) {
|
||||
switch (color) {
|
||||
case "bg-blue-500":
|
||||
return Zap;
|
||||
case "bg-green-500":
|
||||
return Leaf;
|
||||
case "bg-purple-500":
|
||||
return Gem;
|
||||
case "bg-red-500":
|
||||
return Flame;
|
||||
case "bg-yellow-500":
|
||||
return Star;
|
||||
case "bg-indigo-500":
|
||||
return Compass;
|
||||
case "bg-pink-500":
|
||||
return Heart;
|
||||
case "bg-teal-500":
|
||||
return Waves;
|
||||
case "bg-orange-500":
|
||||
return Sun;
|
||||
case "bg-cyan-500":
|
||||
return Sparkles;
|
||||
default:
|
||||
return FolderIcon;
|
||||
}
|
||||
}
|
||||
|
||||
function getTabloIconColor(color: string | null | undefined): string {
|
||||
switch (color) {
|
||||
case "bg-yellow-500":
|
||||
case "bg-cyan-500":
|
||||
return "text-gray-700";
|
||||
default:
|
||||
return "text-white";
|
||||
}
|
||||
}
|
||||
|
||||
export const PlanningPage = () => {
|
||||
const { t } = useTranslation(["planning", "common"]);
|
||||
const { tablo_id } = useParams();
|
||||
|
|
@ -33,10 +100,14 @@ export const PlanningPage = () => {
|
|||
// Initialize view from URL search params, default to "month"
|
||||
const viewFromUrl = searchParams.get("view") as ViewType | null;
|
||||
const initialView: ViewType =
|
||||
viewFromUrl && ["month", "week", "day"].includes(viewFromUrl) ? viewFromUrl : "month";
|
||||
viewFromUrl && ["month", "week", "day"].includes(viewFromUrl)
|
||||
? viewFromUrl
|
||||
: "month";
|
||||
const [currentView, setCurrentView] = useState<ViewType>(initialView);
|
||||
|
||||
const [selectedTabloId, setSelectedTabloId] = useState<string>(tablo_id || "all");
|
||||
const [selectedTabloId, setSelectedTabloId] = useState<string>(
|
||||
tablo_id || "all",
|
||||
);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
const [isWebcalModalOpen, setIsWebcalModalOpen] = useState(false);
|
||||
const isReadOnly = useIsReadOnlyUser();
|
||||
|
|
@ -48,9 +119,8 @@ export const PlanningPage = () => {
|
|||
const { data: tablos, isLoading: tablosLoading } = useTablosList();
|
||||
|
||||
// Fetch events for selected tablo or all tablos
|
||||
const { data: tabloEvents = [], isLoading: tabloEventsLoading } = useEventsByTablo(
|
||||
selectedTabloId !== "all" ? selectedTabloId : null
|
||||
);
|
||||
const { data: tabloEvents = [], isLoading: tabloEventsLoading } =
|
||||
useEventsByTablo(selectedTabloId !== "all" ? selectedTabloId : null);
|
||||
// Fetch all tablo accesses
|
||||
const { data: tabloAccess } = useGetAllTabloAccess();
|
||||
|
||||
|
|
@ -61,7 +131,7 @@ export const PlanningPage = () => {
|
|||
if (
|
||||
tabloAccess?.find(
|
||||
(access: { tablo_id: string; is_admin: boolean }) =>
|
||||
access.tablo_id === event.tablo_id && access.is_admin
|
||||
access.tablo_id === event.tablo_id && access.is_admin,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
|
|
@ -84,13 +154,17 @@ export const PlanningPage = () => {
|
|||
return newParams;
|
||||
});
|
||||
},
|
||||
[setSearchParams]
|
||||
[setSearchParams],
|
||||
);
|
||||
|
||||
// Sync view with URL on mount and when URL changes
|
||||
useEffect(() => {
|
||||
const viewParam = searchParams.get("view") as ViewType | null;
|
||||
if (viewParam && ["month", "week", "day"].includes(viewParam) && viewParam !== currentView) {
|
||||
if (
|
||||
viewParam &&
|
||||
["month", "week", "day"].includes(viewParam) &&
|
||||
viewParam !== currentView
|
||||
) {
|
||||
setCurrentView(viewParam);
|
||||
} else if (!viewParam) {
|
||||
// If no view param in URL, set it to current view
|
||||
|
|
@ -100,7 +174,7 @@ export const PlanningPage = () => {
|
|||
newParams.set("view", currentView);
|
||||
return newParams;
|
||||
},
|
||||
{ replace: true }
|
||||
{ replace: true },
|
||||
);
|
||||
}
|
||||
}, [searchParams, currentView, setSearchParams]);
|
||||
|
|
@ -151,7 +225,8 @@ export const PlanningPage = () => {
|
|||
const calendarName =
|
||||
selectedTabloId === "all"
|
||||
? t("planning:allEvents")
|
||||
: tablos?.find((t) => t.id === selectedTabloId)?.name || t("planning:title");
|
||||
: tablos?.find((t) => t.id === selectedTabloId)?.name ||
|
||||
t("planning:title");
|
||||
|
||||
const icsContent = generateICSFromEvents(tabloEvents, calendarName);
|
||||
const filename =
|
||||
|
|
@ -176,6 +251,18 @@ export const PlanningPage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const navigateToEditEvent = (event: EventAndTablo) => {
|
||||
if (!canEditEvent(event) || isReadOnly) return;
|
||||
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("tab", "events");
|
||||
|
||||
navigate({
|
||||
pathname: `/planning/${event.tablo_id}/events/${event.event_id}/edit`,
|
||||
search: `?${params.toString()}`,
|
||||
});
|
||||
};
|
||||
|
||||
const monthNames = [
|
||||
t("planning:months.january"),
|
||||
t("planning:months.february"),
|
||||
|
|
@ -268,7 +355,12 @@ export const PlanningPage = () => {
|
|||
// const nowMinute = now.getMinutes();
|
||||
const nowDay = now.getDate();
|
||||
|
||||
fullDate.setHours(Number(time.split(":")[0]), Number(time.split(":")[1]), 0, 0);
|
||||
fullDate.setHours(
|
||||
Number(time.split(":")[0]),
|
||||
Number(time.split(":")[1]),
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
const hour = fullDate.getHours();
|
||||
// const minute = fullDate.getMinutes();
|
||||
|
|
@ -336,7 +428,8 @@ export const PlanningPage = () => {
|
|||
const daysInMonth = lastDay.getDate();
|
||||
// Adjust for Monday as first day of week
|
||||
const startingDayOfWeek = firstDay.getDay();
|
||||
const mondayStartingDay = startingDayOfWeek === 0 ? 6 : startingDayOfWeek - 1;
|
||||
const mondayStartingDay =
|
||||
startingDayOfWeek === 0 ? 6 : startingDayOfWeek - 1;
|
||||
|
||||
const days = [];
|
||||
for (let i = 0; i < mondayStartingDay; i++) {
|
||||
|
|
@ -373,7 +466,9 @@ export const PlanningPage = () => {
|
|||
if (currentView === "week") {
|
||||
const weekDays = getWeekDays();
|
||||
const weekDateStrings = weekDays.map(formatDate);
|
||||
return tabloEvents.filter((event) => weekDateStrings.includes(event.start_date));
|
||||
return tabloEvents.filter((event) =>
|
||||
weekDateStrings.includes(event.start_date),
|
||||
);
|
||||
} else if (currentView === "day") {
|
||||
const dateString = formatDate(currentDate);
|
||||
return tabloEvents.filter((event) => event.start_date === dateString);
|
||||
|
|
@ -394,7 +489,7 @@ export const PlanningPage = () => {
|
|||
...visibleEvents.map((event) => {
|
||||
const [hour] = event.start_time.split(":").map(Number);
|
||||
return hour;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Return the earlier of 8am or the earliest event hour
|
||||
|
|
@ -418,7 +513,7 @@ export const PlanningPage = () => {
|
|||
}
|
||||
const [hour] = event.end_time.split(":").map(Number);
|
||||
return hour;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Return the later of 7pm or the latest event hour
|
||||
|
|
@ -445,7 +540,7 @@ export const PlanningPage = () => {
|
|||
|
||||
return Array.from(
|
||||
{ length: numSlots },
|
||||
(_, i) => `${(startHour + i).toString().padStart(2, "0")}:00`
|
||||
(_, i) => `${(startHour + i).toString().padStart(2, "0")}:00`,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -473,7 +568,9 @@ export const PlanningPage = () => {
|
|||
className={`min-h-[120px] border-b border-border ${
|
||||
(index + 1) % 7 !== 0 ? "border-r border-border" : ""
|
||||
} ${day ? "cursor-pointer hover:bg-muted" : "bg-muted"} ${
|
||||
day && formatDate(day) === formatDate(new Date()) ? "bg-primary/10" : ""
|
||||
day && formatDate(day) === formatDate(new Date())
|
||||
? "bg-primary/10"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (day) {
|
||||
|
|
@ -491,7 +588,9 @@ export const PlanningPage = () => {
|
|||
<div className="p-2">
|
||||
<div
|
||||
className={`text-sm font-medium mb-1 ${
|
||||
formatDate(day) === formatDate(new Date()) ? "text-primary" : "text-foreground"
|
||||
formatDate(day) === formatDate(new Date())
|
||||
? "text-primary"
|
||||
: "text-foreground"
|
||||
}`}
|
||||
>
|
||||
{day.getDate()}
|
||||
|
|
@ -518,14 +617,18 @@ export const PlanningPage = () => {
|
|||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (canEditEvent(event)) {
|
||||
navigate(`/planning/${event.tablo_id}/events/${event.event_id}/edit`);
|
||||
navigate(
|
||||
`/planning/${event.tablo_id}/events/${event.event_id}/edit`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="truncate">
|
||||
{formatTime(event.start_time)} {event.title}
|
||||
{selectedTabloId === "all" && event.tablo_name && (
|
||||
<span className="opacity-75 ml-1">• {event.tablo_name}</span>
|
||||
<span className="opacity-75 ml-1">
|
||||
• {event.tablo_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{canDeleteEvent(event) && (
|
||||
|
|
@ -573,7 +676,9 @@ export const PlanningPage = () => {
|
|||
</div>
|
||||
<div
|
||||
className={`text-lg font-medium mt-1 ${
|
||||
formatDate(day) === formatDate(new Date()) ? "text-primary" : "text-foreground"
|
||||
formatDate(day) === formatDate(new Date())
|
||||
? "text-primary"
|
||||
: "text-foreground"
|
||||
}`}
|
||||
>
|
||||
{day.getDate()}
|
||||
|
|
@ -615,10 +720,18 @@ export const PlanningPage = () => {
|
|||
)}
|
||||
|
||||
{getEventsForDate(day)
|
||||
.filter((event) => event.start_time.startsWith(time.split(":")[0]))
|
||||
.filter((event) =>
|
||||
event.start_time.startsWith(time.split(":")[0]),
|
||||
)
|
||||
.map((event) => {
|
||||
const eventHeight = calculateEventHeight(event.start_time, event.end_time);
|
||||
const eventOffset = calculateEventOffset(event.start_time, time);
|
||||
const eventHeight = calculateEventHeight(
|
||||
event.start_time,
|
||||
event.end_time,
|
||||
);
|
||||
const eventOffset = calculateEventOffset(
|
||||
event.start_time,
|
||||
time,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={event.event_id}
|
||||
|
|
@ -635,7 +748,7 @@ export const PlanningPage = () => {
|
|||
minHeight: "30px",
|
||||
}}
|
||||
title={`${formatTime(event.start_time)} - ${formatTime(
|
||||
event.end_time
|
||||
event.end_time,
|
||||
)} ${event.title}${
|
||||
selectedTabloId === "all" && event.tablo_name
|
||||
? ` - ${event.tablo_name}`
|
||||
|
|
@ -644,19 +757,24 @@ export const PlanningPage = () => {
|
|||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (canEditEvent(event)) {
|
||||
navigate(`/planning/${event.tablo_id}/events/${event.event_id}/edit`);
|
||||
navigate(
|
||||
`/planning/${event.tablo_id}/events/${event.event_id}/edit`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="text-[10px] font-medium leading-tight">
|
||||
{event.title}
|
||||
{selectedTabloId === "all" && event.tablo_name && (
|
||||
<span className="opacity-75 ml-1">• {event.tablo_name}</span>
|
||||
<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)}
|
||||
{formatTime(event.start_time)} -{" "}
|
||||
{formatTime(event.end_time)}
|
||||
</div>
|
||||
)}
|
||||
{canDeleteEvent(event) && (
|
||||
|
|
@ -689,7 +807,9 @@ export const PlanningPage = () => {
|
|||
<div className="text-sm text-muted-foreground uppercase">
|
||||
{dayNames[currentDate.getDay()]}
|
||||
</div>
|
||||
<div className="text-2xl font-medium text-foreground mt-1">{currentDate.getDate()}</div>
|
||||
<div className="text-2xl font-medium text-foreground mt-1">
|
||||
{currentDate.getDate()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time slots */}
|
||||
|
|
@ -722,10 +842,18 @@ export const PlanningPage = () => {
|
|||
)}
|
||||
|
||||
{getEventsForDate(currentDate)
|
||||
.filter((event) => event.start_time.startsWith(time.split(":")[0]))
|
||||
.filter((event) =>
|
||||
event.start_time.startsWith(time.split(":")[0]),
|
||||
)
|
||||
.map((event) => {
|
||||
const eventHeight = calculateEventHeight(event.start_time, event.end_time);
|
||||
const eventOffset = calculateEventOffset(event.start_time, time);
|
||||
const eventHeight = calculateEventHeight(
|
||||
event.start_time,
|
||||
event.end_time,
|
||||
);
|
||||
const eventOffset = calculateEventOffset(
|
||||
event.start_time,
|
||||
time,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={event.event_id}
|
||||
|
|
@ -742,7 +870,7 @@ export const PlanningPage = () => {
|
|||
minHeight: "30px",
|
||||
}}
|
||||
title={`${formatTime(event.start_time)} - ${formatTime(
|
||||
event.end_time
|
||||
event.end_time,
|
||||
)} ${event.title}${
|
||||
selectedTabloId === "all" && event.tablo_name
|
||||
? ` - ${event.tablo_name}`
|
||||
|
|
@ -751,19 +879,24 @@ export const PlanningPage = () => {
|
|||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (canEditEvent(event)) {
|
||||
navigate(`/planning/${event.tablo_id}/events/${event.event_id}/edit`);
|
||||
navigate(
|
||||
`/planning/${event.tablo_id}/events/${event.event_id}/edit`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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)}
|
||||
{formatTime(event.start_time)} -{" "}
|
||||
{formatTime(event.end_time)}
|
||||
</div>
|
||||
)}
|
||||
{eventHeight >= 75 && event.description && (
|
||||
|
|
@ -799,11 +932,26 @@ export const PlanningPage = () => {
|
|||
today.setHours(0, 0, 0, 0);
|
||||
const filtered = tabloEvents.filter((e) => {
|
||||
if (showAllEvents) return true;
|
||||
const eventDate = e.start_date ? new Date(e.start_date + "T00:00:00") : null;
|
||||
const eventDate = e.start_date
|
||||
? new Date(e.start_date + "T00:00:00")
|
||||
: null;
|
||||
return !eventDate || eventDate >= today;
|
||||
});
|
||||
|
||||
const months = ["JAN", "FÉV", "MAR", "AVR", "MAI", "JUN", "JUL", "AOÛ", "SEP", "OCT", "NOV", "DÉC"];
|
||||
const months = [
|
||||
"JAN",
|
||||
"FÉV",
|
||||
"MAR",
|
||||
"AVR",
|
||||
"MAI",
|
||||
"JUN",
|
||||
"JUL",
|
||||
"AOÛ",
|
||||
"SEP",
|
||||
"OCT",
|
||||
"NOV",
|
||||
"DÉC",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="py-6 px-2">
|
||||
|
|
@ -814,7 +962,9 @@ export const PlanningPage = () => {
|
|||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { if (!isReadOnly) setIsCreateEventOpen(true); }}
|
||||
onClick={() => {
|
||||
if (!isReadOnly) setIsCreateEventOpen(true);
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
className="flex items-center gap-2 px-5 py-3 bg-[#804EEC] hover:bg-[#6f3fd4] text-white rounded-xl transition-colors font-medium shadow-sm disabled:opacity-50"
|
||||
>
|
||||
|
|
@ -852,21 +1002,35 @@ export const PlanningPage = () => {
|
|||
{/* Event cards grid */}
|
||||
{tabloEventsLoading ? (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<img src="/icon.jpg" alt="Loading..." className="animate-spin rounded-full h-8 w-8 object-cover" />
|
||||
<img
|
||||
src="/icon.jpg"
|
||||
alt="Loading..."
|
||||
className="animate-spin rounded-full h-8 w-8 object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium">Aucun événement trouvé</p>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium">
|
||||
Aucun événement trouvé
|
||||
</p>
|
||||
<p className="text-gray-400 dark:text-gray-500 text-sm mt-1">
|
||||
{showAllEvents ? "Aucun événement trouvé" : "Aucun événement à venir"}
|
||||
{showAllEvents
|
||||
? "Aucun événement trouvé"
|
||||
: "Aucun événement à venir"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{filtered.map((event) => {
|
||||
const date = event.start_date ? new Date(event.start_date + "T00:00:00") : null;
|
||||
const date = event.start_date
|
||||
? new Date(event.start_date + "T00:00:00")
|
||||
: null;
|
||||
const monthLabel = date ? months[date.getMonth()] : "";
|
||||
const dayLabel = date ? String(date.getDate()).padStart(2, "0") : "";
|
||||
const dayLabel = date
|
||||
? String(date.getDate()).padStart(2, "0")
|
||||
: "";
|
||||
const TabloIcon = getTabloIcon(event.tablo_color);
|
||||
const iconColor = getTabloIconColor(event.tablo_color);
|
||||
const timeLabel = event.start_time
|
||||
? `${event.start_time.slice(0, 5)}${event.end_time ? ` – ${event.end_time.slice(0, 5)}` : ""}`
|
||||
: null;
|
||||
|
|
@ -874,16 +1038,39 @@ export const PlanningPage = () => {
|
|||
return (
|
||||
<div
|
||||
key={event.event_id}
|
||||
className="bg-white dark:bg-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all hover:border-gray-300 dark:hover:border-gray-600 relative group cursor-pointer"
|
||||
onClick={() => navigate(`/planning/${event.event_id}?tablo_id=${event.tablo_id}`)}
|
||||
className="bg-white dark:bg-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all hover:border-gray-300 dark:hover:border-gray-600 relative group"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); }}
|
||||
className="absolute top-6 right-6 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<EllipsisVerticalIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-6 right-6 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
|
||||
aria-label="Options de l'événement"
|
||||
>
|
||||
<EllipsisVerticalIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigateToEditEvent(event)}
|
||||
disabled={!canEditEvent(event) || isReadOnly}
|
||||
>
|
||||
Replanifier
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => deleteEvent.mutate(event.event_id)}
|
||||
disabled={
|
||||
!canDeleteEvent(event) ||
|
||||
isReadOnly ||
|
||||
deleteEvent.isPending
|
||||
}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
Supprimer
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
{/* Date badge */}
|
||||
|
|
@ -922,11 +1109,13 @@ export const PlanningPage = () => {
|
|||
{event.tablo_name && (
|
||||
<div className="flex items-center gap-2.5 text-[#344054] dark:text-gray-300">
|
||||
<div
|
||||
className={`w-4 h-4 rounded-[3px] flex-shrink-0 flex items-center justify-center text-[7px] font-bold text-white ${event.tablo_color || "bg-gray-400"}`}
|
||||
className={`w-5 h-5 rounded-[5px] flex-shrink-0 flex items-center justify-center ${event.tablo_color || "bg-gray-400"}`}
|
||||
>
|
||||
{event.tablo_name.charAt(0).toUpperCase()}
|
||||
<TabloIcon className={`w-3 h-3 ${iconColor}`} />
|
||||
</div>
|
||||
<span className="text-sm truncate">{event.tablo_name}</span>
|
||||
<span className="text-sm truncate">
|
||||
{event.tablo_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -947,7 +1136,9 @@ export const PlanningPage = () => {
|
|||
mode="create"
|
||||
isOpen={isCreateEventOpen}
|
||||
onClose={() => setIsCreateEventOpen(false)}
|
||||
defaultTabloId={selectedTabloId !== "all" ? selectedTabloId : undefined}
|
||||
defaultTabloId={
|
||||
selectedTabloId !== "all" ? selectedTabloId : undefined
|
||||
}
|
||||
defaultDate={currentDate}
|
||||
/>
|
||||
<Outlet />
|
||||
|
|
@ -968,10 +1159,15 @@ export const PlanningPage = () => {
|
|||
onValueChange={(value) => setSelectedTabloId(value)}
|
||||
disabled={tablosLoading}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label={t("planning:selectTablo")}>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
aria-label={t("planning:selectTablo")}
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
tablosLoading ? t("common:actions.loading") : t("planning:selectTablo")
|
||||
tablosLoading
|
||||
? t("common:actions.loading")
|
||||
: t("planning:selectTablo")
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
|
|
@ -996,15 +1192,17 @@ export const PlanningPage = () => {
|
|||
"Vous êtes en mode lecture seule. Vous ne pouvez pas créer d'événement.",
|
||||
type: "error",
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (selectedTabloId === "all") {
|
||||
navigate(`/planning/create?date=${currentDate.toISOString()}`);
|
||||
navigate(
|
||||
`/planning/create?date=${currentDate.toISOString()}`,
|
||||
);
|
||||
} else {
|
||||
navigate(
|
||||
`/planning/create?tablo_id=${selectedTabloId}&date=${currentDate.toISOString()}`
|
||||
`/planning/create?tablo_id=${selectedTabloId}&date=${currentDate.toISOString()}`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
|
@ -1025,7 +1223,7 @@ export const PlanningPage = () => {
|
|||
"Vous êtes en mode lecture seule. Vous ne pouvez pas importer de calendrier.",
|
||||
type: "error",
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1058,7 +1256,10 @@ export const PlanningPage = () => {
|
|||
</div>
|
||||
<div className="grid grid-cols-7 gap-1 text-xs">
|
||||
{dayNamesShort.map((day) => (
|
||||
<div key={day} className="text-center text-muted-foreground p-1">
|
||||
<div
|
||||
key={day}
|
||||
className="text-center text-muted-foreground p-1"
|
||||
>
|
||||
{day.slice(0, 1)}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -1094,12 +1295,25 @@ export const PlanningPage = () => {
|
|||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<TypographyH3>{t("planning:title")}</TypographyH3>
|
||||
<Button onClick={goToToday} variant="outline" size="sm" className="border-[#804EEC] text-[#804EEC] hover:bg-[#804EEC]/10">
|
||||
<Button
|
||||
onClick={goToToday}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-[#804EEC] text-[#804EEC] hover:bg-[#804EEC]/10"
|
||||
>
|
||||
{t("planning:today")}
|
||||
</Button>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button onClick={() => navigateDate(-1)} className="p-2 hover:bg-muted rounded">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<button
|
||||
onClick={() => navigateDate(-1)}
|
||||
className="p-2 hover:bg-muted rounded"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
|
|
@ -1108,8 +1322,16 @@ export const PlanningPage = () => {
|
|||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onClick={() => navigateDate(1)} className="p-2 hover:bg-muted rounded">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<button
|
||||
onClick={() => navigateDate(1)}
|
||||
className="p-2 hover:bg-muted rounded"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
|
|
@ -1164,7 +1386,9 @@ export const PlanningPage = () => {
|
|||
alt="Loading..."
|
||||
className="animate-spin rounded-full h-8 w-8 object-cover"
|
||||
/>
|
||||
<span className="ml-2 text-muted-foreground">{t("planning:loadingEvents")}</span>
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
{t("planning:loadingEvents")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -1179,9 +1403,14 @@ export const PlanningPage = () => {
|
|||
|
||||
<Outlet />
|
||||
|
||||
{isImportModalOpen && <ImportICSModal onClose={() => setIsImportModalOpen(false)} />}
|
||||
{isImportModalOpen && (
|
||||
<ImportICSModal onClose={() => setIsImportModalOpen(false)} />
|
||||
)}
|
||||
|
||||
<WebcalModal open={isWebcalModalOpen} onOpenChange={setIsWebcalModalOpen} />
|
||||
<WebcalModal
|
||||
open={isWebcalModalOpen}
|
||||
onOpenChange={setIsWebcalModalOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue