xtablo-source/apps/main/src/components/EventModal.tsx
Arthur Belleville c8699964fb
Make EventModal usable standalone without routing
- 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>
2026-02-21 21:12:15 +01:00

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>
);
};