- EventModal now accepts optional isOpen/onClose/defaultTabloId/defaultDate props; when provided it works as a controlled dialog without needing to navigate to /planning/create - When used as a route child (existing behavior), falls back to URL params and navigate(-1) as before - Events view "Créer un événement" button opens EventModal inline instead of navigating, passing the current tablo and date as defaults Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
291 lines
10 KiB
TypeScript
291 lines
10 KiB
TypeScript
import { getLocalTimeZone, parseDate, today } from "@internationalized/date";
|
|
import { toast } from "@xtablo/shared";
|
|
import { Event, EventInsert } from "@xtablo/shared/types/events.types";
|
|
import { Button } from "@xtablo/ui/components/button";
|
|
import { DatePicker } from "@xtablo/ui/components/date-picker";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@xtablo/ui/components/dialog";
|
|
import { Input } from "@xtablo/ui/components/input";
|
|
import { Label } from "@xtablo/ui/components/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@xtablo/ui/components/select";
|
|
import { Textarea } from "@xtablo/ui/components/textarea";
|
|
import { TimeInput } from "@xtablo/ui/components/time-input";
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
import { useCreateEvents, useEvent, useUpdateEvent } from "../hooks/events";
|
|
import { useTablosList } from "../hooks/tablos";
|
|
import { useIsReadOnlyUser, useUser } from "../providers/UserStoreProvider";
|
|
|
|
export const EventModal = ({
|
|
mode,
|
|
isOpen,
|
|
onClose: onCloseProp,
|
|
defaultTabloId,
|
|
defaultDate,
|
|
}: {
|
|
mode: "create" | "edit";
|
|
isOpen?: boolean;
|
|
onClose?: () => void;
|
|
defaultTabloId?: string;
|
|
defaultDate?: Date;
|
|
}) => {
|
|
const { t, i18n } = useTranslation("components");
|
|
const { event_id } = useParams();
|
|
const { data: event } = useEvent(event_id as string);
|
|
|
|
const user = useUser();
|
|
const isReadOnly = useIsReadOnlyUser();
|
|
const [searchParams] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
|
|
// When used standalone (isOpen prop provided), ignore URL params and use props instead
|
|
const isStandalone = isOpen !== undefined;
|
|
const tablo_id = isStandalone ? (defaultTabloId ?? "") : (searchParams.get("tablo_id") ?? "");
|
|
const dateFromParams = isStandalone ? null : searchParams.get("date");
|
|
const date = defaultDate ?? (dateFromParams ? new Date(dateFromParams) : new Date());
|
|
|
|
const { data: tablos, isLoading: tablosLoading } = useTablosList();
|
|
const createEvents = useCreateEvents();
|
|
const updateEvent = useUpdateEvent();
|
|
|
|
const onClose = () => {
|
|
if (onCloseProp) {
|
|
onCloseProp();
|
|
} else {
|
|
navigate(-1);
|
|
}
|
|
};
|
|
|
|
// 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}`;
|
|
};
|
|
|
|
// Format time from Date to HH:MM string
|
|
const formatTimeFromDate = (date: Date, addMinutes: number = 0): string => {
|
|
const hours = date.getHours();
|
|
const minutes = date.getMinutes() + addMinutes;
|
|
const totalMinutes = hours * 60 + minutes;
|
|
const finalHours = Math.floor(totalMinutes / 60) % 24;
|
|
const finalMinutes = totalMinutes % 60;
|
|
return `${finalHours.toString().padStart(2, "0")}:${finalMinutes.toString().padStart(2, "0")}`;
|
|
};
|
|
|
|
const [formEvent, setFormEvent] = useState<EventInsert>({
|
|
start_date: date ? getLocalDateString(date) : "",
|
|
start_time: date ? formatTimeFromDate(date) : "",
|
|
end_time: date ? formatTimeFromDate(date, 30) : "",
|
|
tablo_id: tablo_id || "",
|
|
title: "",
|
|
created_by: user.id,
|
|
});
|
|
|
|
// Initialize form data when in edit mode
|
|
useEffect(() => {
|
|
if (mode === "edit" && event) {
|
|
setFormEvent({
|
|
start_date: event.start_date,
|
|
start_time: event.start_time || "",
|
|
end_time: event.end_time || "",
|
|
tablo_id: event.tablo_id,
|
|
title: event.title,
|
|
description: event.description || "",
|
|
created_by: event.created_by,
|
|
});
|
|
}
|
|
}, [mode, event]);
|
|
|
|
return (
|
|
<Dialog open={isStandalone ? isOpen : true} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{mode === "edit" ? t("eventModal.title.edit") : t("eventModal.title.create")}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{mode === "edit" && event
|
|
? new Date(event.start_date).toLocaleDateString(i18n.language, {
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
})
|
|
: date.toLocaleDateString(i18n.language, {
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
})}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* Title Input */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="event-title">{t("eventModal.labels.title")}</Label>
|
|
<Input
|
|
id="event-title"
|
|
type="text"
|
|
value={formEvent?.title}
|
|
onChange={(e) =>
|
|
setFormEvent({
|
|
...formEvent,
|
|
title: e.target.value,
|
|
} as Event)
|
|
}
|
|
placeholder={t("eventModal.placeholders.title")}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
{/* Tablo Selection */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="event-tablo">{t("eventModal.labels.tablo")}</Label>
|
|
<Select
|
|
value={formEvent?.tablo_id}
|
|
onValueChange={(value) =>
|
|
setFormEvent({
|
|
...formEvent,
|
|
tablo_id: value,
|
|
} as Event)
|
|
}
|
|
disabled={tablosLoading}
|
|
>
|
|
<SelectTrigger id="event-tablo" className="w-full">
|
|
<SelectValue placeholder={t("eventModal.placeholders.tablo")} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tablos?.map((tablo) => (
|
|
<SelectItem key={tablo.id} value={tablo.id}>
|
|
{tablo.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="event-date">{t("eventModal.labels.date")}</Label>
|
|
<DatePicker
|
|
aria-label={t("eventModal.labels.date")}
|
|
value={formEvent?.start_date ? parseDate(formEvent?.start_date) : undefined}
|
|
minValue={today(getLocalTimeZone())}
|
|
onChange={(date) => {
|
|
if (date) {
|
|
// Convert Date to YYYY-MM-DD format
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
setFormEvent({
|
|
...formEvent,
|
|
start_date: `${year}-${month}-${day}`,
|
|
});
|
|
}
|
|
}}
|
|
buttonClassName="h-10 w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="event-start-time">{t("eventModal.labels.startTime")}</Label>
|
|
<TimeInput
|
|
value={formEvent?.start_time || undefined}
|
|
onChange={(value) => {
|
|
setFormEvent({
|
|
...formEvent,
|
|
start_time: value,
|
|
});
|
|
}}
|
|
className="w-full"
|
|
id="event-start-time"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="event-end-time">{t("eventModal.labels.endTime")}</Label>
|
|
<TimeInput
|
|
value={formEvent?.end_time || undefined}
|
|
onChange={(value) => {
|
|
setFormEvent({
|
|
...formEvent,
|
|
end_time: value,
|
|
});
|
|
}}
|
|
className="w-full"
|
|
id="event-end-time"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="event-description">{t("eventModal.labels.description")}</Label>
|
|
<Textarea
|
|
id="event-description"
|
|
value={formEvent?.description ?? ""}
|
|
onChange={(e) =>
|
|
setFormEvent({
|
|
...formEvent,
|
|
description: e.target.value,
|
|
} as Event)
|
|
}
|
|
rows={3}
|
|
placeholder={t("eventModal.placeholders.description")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={onClose}>
|
|
{t("eventModal.buttons.cancel")}
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
if (isReadOnly) {
|
|
toast.add(
|
|
{
|
|
title: t("eventModal.errors.readOnly"),
|
|
description:
|
|
"Vous êtes en mode lecture seule. Vous ne pouvez pas modifier ou créer d'événement.",
|
|
type: "error",
|
|
},
|
|
{ timeout: 5000 }
|
|
);
|
|
return;
|
|
}
|
|
const eventName = formEvent?.title.trim() || t("eventModal.untitled");
|
|
if (mode === "edit" && event) {
|
|
updateEvent.mutate(
|
|
{ id: event.id, ...formEvent, title: eventName },
|
|
{ onSuccess: () => onClose() }
|
|
);
|
|
} else {
|
|
createEvents({ ...formEvent, title: eventName }, { onSuccess: () => onClose() });
|
|
}
|
|
}}
|
|
disabled={!formEvent?.tablo_id || isReadOnly}
|
|
>
|
|
{mode === "edit" ? t("eventModal.buttons.edit") : t("eventModal.buttons.save")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|