Files
workavia-calendar-front/src/features/Events/EventModal.tsx
T
2025-09-09 11:53:12 +02:00

362 lines
12 KiB
TypeScript

import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import {
Box,
Button,
Card,
CardActions,
CardContent,
CardHeader,
FormControl,
InputLabel,
MenuItem,
Popover,
Select,
SelectChangeEvent,
TextField,
Typography,
} from "@mui/material";
import React, { useEffect, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
import { TIMEZONES } from "../../utils/timezone-data";
import { putEventAsync } from "../Calendars/CalendarSlice";
import { Calendars } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { createSelector } from "@reduxjs/toolkit";
import RepeatEvent from "../../components/Event/EventRepeat";
function EventPopover({
anchorEl,
open,
onClose,
selectedRange,
setSelectedRange,
calendarRef,
}: {
anchorEl: HTMLElement | null;
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
selectedRange: DateSelectArg | null;
setSelectedRange: Function;
calendarRef: React.RefObject<CalendarApi | null>;
}) {
const dispatch = useAppDispatch();
const organizer = useAppSelector((state) => state.user.organiserData);
const userId = useAppSelector((state) => state.user.userData.openpaasId);
const selectPersonnalCalendars = createSelector(
(state) => state.calendars,
(calendars) =>
Object.keys(calendars.list)
.map((id) => {
if (id.split("/")[0] === userId) {
return calendars.list[id];
}
return {} as Calendars;
})
.filter((calendar) => calendar.id)
);
const userPersonnalCalendars: Calendars[] = useAppSelector(
selectPersonnalCalendars
);
const timezones = TIMEZONES.aliases;
const [showMore, setShowMore] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [location, setLocation] = useState("");
const [start, setStart] = useState("");
const [end, setEnd] = useState("");
const [calendarid, setCalendarid] = useState(0);
const [allday, setAllDay] = useState(false);
const [repetition, setRepetition] = useState<RepetitionObject>(
{} as RepetitionObject
);
const [attendees, setAttendees] = useState<userAttendee[]>([]);
const [alarm, setAlarm] = useState("");
const [eventClass, setEventClass] = useState("PUBLIC");
const [busy, setBusy] = useState("");
const [timezone, setTimezone] = useState(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
useEffect(() => {
if (selectedRange) {
setStart(selectedRange ? formatLocalDateTime(selectedRange.start) : "");
setEnd(selectedRange ? formatLocalDateTime(selectedRange.end) : "");
}
}, [selectedRange]);
const handleSave = async () => {
const newEventUID = crypto.randomUUID();
const newEvent: CalendarEvent = {
calId: userPersonnalCalendars[calendarid].id,
title,
URL: `/calendars/${userPersonnalCalendars[calendarid].id}/${newEventUID}.ics`,
start: new Date(start),
allday,
uid: newEventUID,
description,
location,
class: eventClass,
repetition,
organizer,
timezone,
attendee: [
{
cn: organizer.cn,
cal_address: organizer.cal_address,
partstat: "ACCEPTED",
rsvp: "FALSE",
role: "CHAIR",
cutype: "INDIVIDUAL",
},
],
transp: "OPAQUE",
color: userPersonnalCalendars[calendarid]?.color,
alarm: { trigger: alarm, action: "EMAIL" },
};
if (end) {
newEvent.end = new Date(end);
}
if (attendees.length > 0) {
newEvent.attendee = newEvent.attendee.concat(attendees);
}
dispatch(
putEventAsync({
cal: userPersonnalCalendars[calendarid],
newEvent,
})
);
onClose({}, "backdropClick");
// Reset
setTitle("");
setDescription("");
setLocation("");
setCalendarid(0);
};
return (
<Popover
open={open}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{
vertical: "center",
horizontal: "center",
}}
transformOrigin={{
vertical: "center",
horizontal: "center",
}}
>
<Card>
<CardHeader title="Create Event" />
<CardContent
sx={{ maxHeight: "85vh", maxWidth: "40vw", overflow: "auto" }}
>
<TextField
fullWidth
label="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
size="small"
margin="dense"
/>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="calendar-select-label">Calendar</InputLabel>
<Select
labelId="calendar-select-label"
value={calendarid.toString()}
label="Calendar"
onChange={(e: SelectChangeEvent) =>
setCalendarid(Number(e.target.value))
}
>
{Object.keys(userPersonnalCalendars).map((calendar, index) => (
<MenuItem key={index} value={index}>
{userPersonnalCalendars[index].name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
fullWidth
label="Start"
type={allday ? "date" : "datetime-local"}
value={allday ? start.split("T")[0] : start}
onChange={(e) => {
const newStart = e.target.value;
setStart(newStart);
const newRange = {
...selectedRange,
start: new Date(newStart),
startStr: newStart,
allDay: allday,
};
setSelectedRange(newRange);
calendarRef.current?.select(newRange);
}}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<TextField
fullWidth
label="End"
type={allday ? "date" : "datetime-local"}
value={allday ? end.split("T")[0] : end}
onChange={(e) => {
const newEnd = e.target.value;
setEnd(newEnd);
const newRange = {
...selectedRange,
end: new Date(newEnd),
endStr: newEnd,
allDay: allday,
};
setSelectedRange(newRange);
calendarRef.current?.select(newRange);
}}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<label>
<input
type="checkbox"
checked={allday}
onChange={() => {
const endDate = new Date(end);
const startDate = new Date(start);
setAllDay(!allday);
if (endDate.getDate() === startDate.getDate()) {
endDate.setDate(startDate.getDate() + 1);
setEnd(formatLocalDateTime(endDate));
}
const newRange = {
...selectedRange,
startStr: allday ? start.split("T")[0] : start,
endStr: allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString(),
start: new Date(allday ? start.split("T")[0] : start),
end: new Date(
allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString()
),
allDay: allday,
};
setSelectedRange(newRange);
}}
/>
All day
</label>
<TextField
fullWidth
label="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
margin="dense"
multiline
rows={2}
/>
<TextField
fullWidth
label="Location"
value={location}
onChange={(e) => setLocation(e.target.value)}
size="small"
margin="dense"
/>
<AttendeeSelector attendees={attendees} setAttendees={setAttendees} />
{/* Extended options */}
{showMore && (
<>
<RepeatEvent
repetition={repetition}
eventStart={selectedRange?.start ?? new Date()}
setRepetition={setRepetition}
/>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="alarm">Alarm</InputLabel>
<Select
labelId="alarm"
value={alarm}
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
>
<MenuItem value={""}>No Alarm</MenuItem>
<MenuItem value={"-PT1M"}>1 minute</MenuItem>
<MenuItem value={"-PT5M"}>2 minutes</MenuItem>
<MenuItem value={"-PT10M"}>10 minutes</MenuItem>
<MenuItem value={"-PT15M"}>15 minutes</MenuItem>
<MenuItem value={"-PT30M"}>30 minutes</MenuItem>
<MenuItem value={"-PT1H"}>1 hours</MenuItem>
<MenuItem value={"-PT2H"}>2 hours</MenuItem>
<MenuItem value={"-PT5H"}>5 hours</MenuItem>
<MenuItem value={"-PT12H"}>12 hours</MenuItem>
<MenuItem value={"-PT1D"}>1 day</MenuItem>
<MenuItem value={"-PT2D"}>2 days</MenuItem>
<MenuItem value={"-PT1W"}>1 week</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="Visibility">Visibility</InputLabel>
<Select
labelId="Visibility"
label="Visibility"
value={eventClass}
onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value)
}
>
<MenuItem value={"PUBLIC"}>Public</MenuItem>
<MenuItem value={"CONFIDENTIAL"}>Show time only</MenuItem>
<MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select>
</FormControl>
</>
)}
</CardContent>
<CardActions>
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
onClick={() => onClose({}, "backdropClick")}
>
Cancel
</Button>
<Button size="small" onClick={() => setShowMore(!showMore)}>
{showMore ? "Show Less" : "Show More"}
</Button>
<Button variant="contained" onClick={handleSave} disabled={!title}>
Save
</Button>
</Box>
</CardActions>
</Card>
</Popover>
);
}
export default EventPopover;
export function formatLocalDateTime(date: Date): string {
const pad = (n: number) => n.toString().padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
date.getDate()
)}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}