diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 49315e7..522b7b3 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -100,7 +100,6 @@ export default function CalendarApp() { const handleDateSelect = (selectInfo: DateSelectArg) => { setSelectedRange(selectInfo); - console.log(selectInfo); setAnchorEl(document.body); // fallback: we could use selectInfo.jsEvent.target if from a click }; diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 02dd02f..1bf82ba 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -4,7 +4,7 @@ import { CalendarEvent } from "../Events/EventsTypes"; import { getCalendar, getCalendars } from "./CalendarApi"; import getOpenPaasUserId from "../User/userAPI"; import { parseCalendarEvent } from "../Events/eventUtils"; -import { getEvent, putEvent } from "../Events/EventApi"; +import { putEvent } from "../Events/EventApi"; import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils"; export const getCalendarsListAsync = createAsyncThunk< @@ -50,18 +50,28 @@ export const getCalendarDetailAsync = createAsyncThunk< }); export const putEventAsync = createAsyncThunk< - { calId: string; event: CalendarEvent }, // Return type + { calId: string; events: CalendarEvent[] }, // Return type { cal: Calendars; newEvent: CalendarEvent } // Arg type >("calendars/putEvent", async ({ cal, newEvent }) => { const response = await putEvent(cal, newEvent); - const now = new Date(); - const event = (await getCalendar(cal.id, { - start: formatDateToYYYYMMDDTHHMMSS(now), - end: formatDateToYYYYMMDDTHHMMSS(new Date(now.getDate() + 1)), + const calEvents = (await getCalendar(cal.id, { + start: formatDateToYYYYMMDDTHHMMSS(newEvent.start), + end: formatDateToYYYYMMDDTHHMMSS( + new Date(newEvent.start.getTime() + 86400000) + ), })) as Record; + const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap( + (eventdata: any) => { + const vevents = eventdata.data[2] as any[][]; + return vevents.map((vevent: any[]) => { + return parseCalendarEvent(vevent[1], cal.color ?? "", cal.id); + }); + } + ); + return { calId: cal.id, - event: parseCalendarEvent([event[1]], "", cal.id), + events, }; }); @@ -133,7 +143,7 @@ const CalendarSlice = createSlice({ putEventAsync.fulfilled, ( state, - action: PayloadAction<{ calId: string; event: CalendarEvent }> + action: PayloadAction<{ calId: string; events: CalendarEvent[] }> ) => { state.pending = false; if (!state.list[action.payload.calId]) { @@ -142,11 +152,13 @@ const CalendarSlice = createSlice({ events: {}, } as Calendars; } - state.list[action.payload.calId].events[action.payload.event.uid] = - action.payload.event; - state.list[action.payload.calId].events[ - action.payload.event.uid - ].color = state.list[action.payload.calId].color; + action.payload.events.forEach((event) => { + state.list[action.payload.calId].events[event.uid] = event; + }); + Object.keys(state.list[action.payload.calId].events).forEach((id) => { + state.list[action.payload.calId].events[id].color = + state.list[action.payload.calId].color; + }); } ) .addCase(getCalendarDetailAsync.pending, (state) => { diff --git a/src/features/Events/EventApi.ts b/src/features/Events/EventApi.ts index c26ec4b..60bb186 100644 --- a/src/features/Events/EventApi.ts +++ b/src/features/Events/EventApi.ts @@ -1,14 +1,14 @@ import { api } from "../../utils/apiUtils"; import { Calendars } from "../Calendars/CalendarTypes"; import { CalendarEvent } from "./EventsTypes"; -import { calendarEventToICal, calendarEventToJCal } from "./eventUtils"; +import { calendarEventToJCal } from "./eventUtils"; export async function putEvent(cal: Calendars, event: CalendarEvent) { const response = await api( `dav/calendars/${cal.id}/${event.uid.split(".")[0]}.isc`, { method: "PUT", - body: JSON.stringify(calendarEventToICal(cal, event)), + body: JSON.stringify(calendarEventToJCal(event)), headers: { "content-type": "text/calendar; charset=utf-8", }, @@ -16,10 +16,3 @@ export async function putEvent(cal: Calendars, event: CalendarEvent) { ).json(); return response; } - -export async function getEvent(calID: string, eventID: string) { - const response = await api - .get(`dav/calendars/${calID}/${eventID}.ics`) - .json(); - return response; -} diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 14f1ec6..ca894a8 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -62,8 +62,6 @@ function EventPopover({ }, [selectedRange]); const handleSave = async () => { - if (!calendarid) return; // Prevent crash if no calendar is selected - const newEvent: CalendarEvent = { title, start: new Date(start), @@ -86,12 +84,7 @@ function EventPopover({ transp: "OPAQUE", color: userPersonnalCalendars[calendarid]?.color, }; - // dispatch( - // addEvent({ - // calendarUid: userPersonnalCalendars[calendarid]?.id, - // event: newEvent, - // }) - // ); + dispatch( putEventAsync({ cal: userPersonnalCalendars[calendarid], @@ -208,11 +201,7 @@ function EventPopover({ > Cancel - diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index 2ec1fcd..05c2694 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -87,19 +87,14 @@ export function parseCalendarEvent( } export function calendarEventToJCal(event: CalendarEvent): any[] { - const tzid = event.timezone ?? "UTC"; // Fallback to UTC if no timezone provided + const tzid = event.timezone; // Fallback to UTC if no timezone provided const vevent: any[] = [ "vevent", [ ["uid", {}, "text", event.uid], ["transp", {}, "text", event.transp ?? "OPAQUE"], - [ - "dtstart", - { tzid }, - "date-time", - event.start.toISOString().split(".")[0], - ], + ["dtstart", { tzid }, "date-time", formatDateToICal(event.start)], ["class", {}, "text", event.class ?? "PUBLIC"], [ "x-openpaas-videoconference", @@ -117,7 +112,7 @@ export function calendarEventToJCal(event: CalendarEvent): any[] { "dtend", { tzid }, "date-time", - event.start.toISOString().split(".")[0], + formatDateToICal(event.end), ]); } if (event.organizer) { @@ -125,9 +120,15 @@ export function calendarEventToJCal(event: CalendarEvent): any[] { "organizer", { cn: event.organizer.cn }, "cal-address", - `mailto:${event.organizer.cal_address}`, + event.organizer.cal_address, ]); } + if (event.location) { + vevent[1].push(["location", {}, "text", event.location]); + } + if (event.description) { + vevent[1].push(["description", {}, "text", event.description]); + } event.attendee.forEach((att) => { vevent[1].push([ @@ -140,18 +141,19 @@ export function calendarEventToJCal(event: CalendarEvent): any[] { cutype: att.cutype, }, "cal-address", - `mailto:${att.cal_address}`, + att.cal_address, ]); }); - const vtimezone: any[] = ["vtimezone", [["tzid", {}, "text", tzid]], []]; - - return ["vcalendar", [], [vevent, vtimezone]]; + const vtimezone = new ICAL.Timezone({ + component: TIMEZONES.zones[event.timezone].ics, + tzid: event.timezone, + }); + return ["vcalendar", [], [vevent, vtimezone.component.jCal]]; } - -function formatDateToICal(date: Date, tz?: string) { +function formatDateToICal(date: Date) { // Format date like: 20250214T110000 (local time) - // If tz is provided, use TZID param in DTSTART/DTEND + const pad = (n: number) => n.toString().padStart(2, "0"); const year = date.getFullYear(); const month = pad(date.getMonth() + 1); @@ -161,61 +163,3 @@ function formatDateToICal(date: Date, tz?: string) { const seconds = pad(date.getSeconds()); return `${year}${month}${day}T${hours}${minutes}${seconds}`; } - -export function calendarEventToICal( - cal: Calendars, - event: CalendarEvent -): string { - const lines = []; - lines.push("BEGIN:VCALENDAR"); - lines.push("VERSION:2.0"); - lines.push("PRODID:-//Sabre//Sabre VObject 4.1.3//EN"); - if (cal.calscale) lines.push(`CALSCALE:${cal.calscale}`); - if (cal.version) lines.push(`VERSION:${cal.version}`); - - lines.push(TIMEZONES.zones[event.timezone].ics); - lines.push("BEGIN:VEVENT"); - lines.push(`UID:${event.uid}`); - - if (event.transp) lines.push(`TRANSP:${event.transp.toUpperCase()}`); - if (event.start) - lines.push( - `DTSTART${event.timezone ? `;TZID=${event.timezone}` : ""}:` + - formatDateToICal(event.start) - ); - if (event.end) - lines.push( - `DTEND${event.timezone ? `;TZID=${event.timezone}` : ""}:` + - formatDateToICal(event.end) - ); - lines.push(`DTSTAMP:${ICAL.Time.now().toICALString()}`); - - if (event.class) lines.push(`CLASS:${event.class.toUpperCase()}`); - if (event.x_openpass_videoconference !== undefined) - lines.push( - `X-OPENPAAS-VIDEOCONFERENCE:${event.x_openpass_videoconference || ""}` - ); - if (event.title) lines.push(`SUMMARY:${event.title}`); - if (event.description) lines.push(`DESCRIPTION:${event.description}`); - if (event.location) lines.push(`LOCATION:${event.location}`); - - if (event.organizer) { - lines.push( - `ORGANIZER;CN=${event.organizer.cn}:${event.organizer.cal_address}` - ); - } - - event.attendee.forEach((att) => { - lines.push( - `ATTENDEE;PARTSTAT=${att.partstat};RSVP=${att.rsvp};ROLE=${att.role};CUTYPE=${att.cutype};CN=${att.cn}:${att.cal_address}` - ); - }); - - if (event.stamp) lines.push(`DTSTAMP:${formatDateToICal(event.stamp)}Z`); - if (event.sequence !== undefined) lines.push(`SEQUENCE:${event.sequence}`); - - lines.push("END:VEVENT"); - lines.push("END:VCALENDAR\n"); - console.debug(lines.join("\r\n")); - return lines.join("\r\n"); -} diff --git a/src/utils/timezone-data.ts b/src/utils/timezone-data.ts index d239d69..4c2d34f 100644 --- a/src/utils/timezone-data.ts +++ b/src/utils/timezone-data.ts @@ -1,4 +1,17 @@ -export const TIMEZONES = { +export interface TimeZones { + version: string; + aliases: Record; + zones: Record< + string, + { + ics: string; + latitude: string; + longitude: string; + } + >; +} + +export const TIMEZONES: TimeZones = { version: "2.2016c", aliases: { "AUS Central Standard Time": {