[#31]added two function to setup data to be sent and put function
This commit is contained in:
Generated
+7
@@ -23,6 +23,7 @@
|
|||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"i18next": "^23.7.9",
|
"i18next": "^23.7.9",
|
||||||
|
"ical.js": "^2.2.0",
|
||||||
"ky": "^1.8.1",
|
"ky": "^1.8.1",
|
||||||
"openid-client": "^6.5.3",
|
"openid-client": "^6.5.3",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
@@ -7450,6 +7451,12 @@
|
|||||||
"i18next-resources-for-ts": "bin/i18next-resources-for-ts.js"
|
"i18next-resources-for-ts": "bin/i18next-resources-for-ts.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ical.js": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-P8gjWkTEd5M/SEEvBVPPO/KC+V+HRNRZh3xfCDTVWmUTEfVbL8JaK5GTWS2MJ55aLMhfXhbh7kYzd0nrBARjsA==",
|
||||||
|
"license": "MPL-2.0"
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"i18next": "^23.7.9",
|
"i18next": "^23.7.9",
|
||||||
|
"ical.js": "^2.2.0",
|
||||||
"ky": "^1.8.1",
|
"ky": "^1.8.1",
|
||||||
"openid-client": "^6.5.3",
|
"openid-client": "^6.5.3",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||||
import userReducer from "../features/User/userSlice";
|
import userReducer from "../features/User/userSlice";
|
||||||
import eventsReducer from "../features/Events/EventsSlice";
|
|
||||||
import eventsCalendar from "../features/Calendars/CalendarSlice";
|
import eventsCalendar from "../features/Calendars/CalendarSlice";
|
||||||
import { createReduxHistoryContext } from "redux-first-history";
|
import { createReduxHistoryContext } from "redux-first-history";
|
||||||
import { createBrowserHistory } from "history";
|
import { createBrowserHistory } from "history";
|
||||||
@@ -11,7 +10,6 @@ const { createReduxHistory, routerMiddleware, routerReducer } =
|
|||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
router: routerReducer,
|
router: routerReducer,
|
||||||
user: userReducer,
|
user: userReducer,
|
||||||
events: eventsReducer,
|
|
||||||
calendars: eventsCalendar,
|
calendars: eventsCalendar,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export default function CalendarApp() {
|
|||||||
|
|
||||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
||||||
setSelectedRange(selectInfo);
|
setSelectedRange(selectInfo);
|
||||||
|
console.log(selectInfo);
|
||||||
setAnchorEl(document.body); // fallback: we could use selectInfo.jsEvent.target if from a click
|
setAnchorEl(document.body); // fallback: we could use selectInfo.jsEvent.target if from a click
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { CalendarEvent } from "../Events/EventsTypes";
|
|||||||
import { getCalendar, getCalendars } from "./CalendarApi";
|
import { getCalendar, getCalendars } from "./CalendarApi";
|
||||||
import getOpenPaasUserId from "../User/userAPI";
|
import getOpenPaasUserId from "../User/userAPI";
|
||||||
import { parseCalendarEvent } from "../Events/eventUtils";
|
import { parseCalendarEvent } from "../Events/eventUtils";
|
||||||
|
import { getEvent, putEvent } from "../Events/EventApi";
|
||||||
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
|
||||||
|
|
||||||
export const getCalendarsListAsync = createAsyncThunk<
|
export const getCalendarsListAsync = createAsyncThunk<
|
||||||
Record<string, Calendars> // Return type
|
Record<string, Calendars> // Return type
|
||||||
@@ -47,6 +49,22 @@ export const getCalendarDetailAsync = createAsyncThunk<
|
|||||||
return { calId, events };
|
return { calId, events };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const putEventAsync = createAsyncThunk<
|
||||||
|
{ calId: string; event: 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)),
|
||||||
|
})) as Record<string, any>;
|
||||||
|
return {
|
||||||
|
calId: cal.id,
|
||||||
|
event: parseCalendarEvent([event[1]], "", cal.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const CalendarSlice = createSlice({
|
const CalendarSlice = createSlice({
|
||||||
name: "calendars",
|
name: "calendars",
|
||||||
initialState: { list: {} as Record<string, Calendars>, pending: false },
|
initialState: { list: {} as Record<string, Calendars>, pending: false },
|
||||||
@@ -111,11 +129,34 @@ const CalendarSlice = createSlice({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.addCase(
|
||||||
|
putEventAsync.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ calId: string; event: CalendarEvent }>
|
||||||
|
) => {
|
||||||
|
state.pending = false;
|
||||||
|
if (!state.list[action.payload.calId]) {
|
||||||
|
state.list[action.payload.calId] = {
|
||||||
|
id: action.payload.calId,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
)
|
||||||
.addCase(getCalendarDetailAsync.pending, (state) => {
|
.addCase(getCalendarDetailAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
})
|
})
|
||||||
.addCase(getCalendarsListAsync.pending, (state) => {
|
.addCase(getCalendarsListAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
|
})
|
||||||
|
.addCase(putEventAsync.pending, (state) => {
|
||||||
|
state.pending = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { api } from "../../utils/apiUtils";
|
||||||
|
import { Calendars } from "../Calendars/CalendarTypes";
|
||||||
|
import { CalendarEvent } from "./EventsTypes";
|
||||||
|
import { calendarEventToICal, 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)),
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/calendar; charset=utf-8",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).json();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEvent(calID: string, eventID: string) {
|
||||||
|
const response = await api
|
||||||
|
.get(`dav/calendars/${calID}/${eventID}.ics`)
|
||||||
|
.json();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { addEvent } from "../Calendars/CalendarSlice";
|
import { addEvent, putEventAsync } from "../Calendars/CalendarSlice";
|
||||||
import { CalendarEvent } from "./EventsTypes";
|
import { CalendarEvent } from "./EventsTypes";
|
||||||
import { DateSelectArg } from "@fullcalendar/core";
|
import { DateSelectArg } from "@fullcalendar/core";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
@@ -12,7 +12,12 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { Calendars } from "../Calendars/CalendarTypes";
|
||||||
|
import { putEvent } from "./EventApi";
|
||||||
|
import { TIMEZONES } from "../../utils/timezone-data";
|
||||||
|
|
||||||
function EventPopover({
|
function EventPopover({
|
||||||
anchorEl,
|
anchorEl,
|
||||||
@@ -28,30 +33,46 @@ function EventPopover({
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const organizer = useAppSelector((state) => state.user.organiserData);
|
const organizer = useAppSelector((state) => state.user.organiserData);
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
||||||
|
const userPersonnalCalendars: Calendars[] = useAppSelector((state) =>
|
||||||
|
Object.keys(state.calendars.list).map((id) => {
|
||||||
|
if (id.split("/")[0] === userId) {
|
||||||
|
return state.calendars.list[id];
|
||||||
|
}
|
||||||
|
return {} as Calendars;
|
||||||
|
})
|
||||||
|
).filter((calendar) => calendar.id);
|
||||||
|
const timezones = TIMEZONES.aliases;
|
||||||
|
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [location, setLocation] = useState("");
|
const [location, setLocation] = useState("");
|
||||||
const [start, setStart] = useState("");
|
const [start, setStart] = useState("");
|
||||||
const [end, setEnd] = useState("");
|
const [end, setEnd] = useState("");
|
||||||
const [calendarid, setCalendarid] = useState("");
|
const [calendarid, setCalendarid] = useState(0);
|
||||||
|
const [timezone, setTimezone] = useState(
|
||||||
|
Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedRange) {
|
if (selectedRange) {
|
||||||
setStart(selectedRange.startStr);
|
setStart(selectedRange ? formatLocalDateTime(selectedRange.start) : "");
|
||||||
setEnd(selectedRange.endStr ?? "");
|
setEnd(selectedRange ? formatLocalDateTime(selectedRange.end) : "");
|
||||||
}
|
}
|
||||||
}, [selectedRange]);
|
}, [selectedRange]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
|
if (!calendarid) return; // Prevent crash if no calendar is selected
|
||||||
|
|
||||||
const newEvent: CalendarEvent = {
|
const newEvent: CalendarEvent = {
|
||||||
title,
|
title,
|
||||||
start: new Date(start ?? ""),
|
start: new Date(start),
|
||||||
end: new Date(end ?? ""),
|
end: new Date(end),
|
||||||
uid: Date.now().toString(36),
|
uid: crypto.randomUUID(),
|
||||||
description,
|
description,
|
||||||
location,
|
location,
|
||||||
organizer,
|
organizer,
|
||||||
|
timezone,
|
||||||
attendee: [
|
attendee: [
|
||||||
{
|
{
|
||||||
cn: organizer.cn,
|
cn: organizer.cn,
|
||||||
@@ -63,15 +84,27 @@ function EventPopover({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
transp: "OPAQUE",
|
transp: "OPAQUE",
|
||||||
color: calendars[calendarid].color,
|
color: userPersonnalCalendars[calendarid]?.color,
|
||||||
};
|
};
|
||||||
dispatch(addEvent({ calendarUid: calendarid, event: newEvent }));
|
// dispatch(
|
||||||
|
// addEvent({
|
||||||
|
// calendarUid: userPersonnalCalendars[calendarid]?.id,
|
||||||
|
// event: newEvent,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
dispatch(
|
||||||
|
putEventAsync({
|
||||||
|
cal: userPersonnalCalendars[calendarid],
|
||||||
|
newEvent,
|
||||||
|
})
|
||||||
|
);
|
||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setLocation("");
|
setLocation("");
|
||||||
|
setCalendarid(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -79,29 +112,35 @@ function EventPopover({
|
|||||||
open={open}
|
open={open}
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
anchorOrigin={{
|
anchorOrigin={{ vertical: "top", horizontal: "left" }}
|
||||||
vertical: "top",
|
transformOrigin={{ vertical: "top", horizontal: "left" }}
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box p={2} width={300}>
|
<Box p={2} width={500}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
Create Event
|
Create Event
|
||||||
</Typography>
|
</Typography>
|
||||||
<Select
|
|
||||||
onChange={(e: SelectChangeEvent) => setCalendarid(e.target.value)}
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
>
|
<InputLabel id="calendar-select-label">Calendar</InputLabel>
|
||||||
{Object.keys(calendars).map((calendar) => (
|
<Select
|
||||||
<MenuItem value={calendar}>{calendars[calendar].name}</MenuItem>
|
labelId="calendar-select-label"
|
||||||
))}
|
value={calendarid.toString()}
|
||||||
</Select>
|
label="Calendar"
|
||||||
|
onChange={(e: SelectChangeEvent) =>
|
||||||
|
setCalendarid(Number(e.target.value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Object.keys(userPersonnalCalendars).map((value, calendar) => (
|
||||||
|
<MenuItem key={calendar} value={calendar}>
|
||||||
|
{userPersonnalCalendars[calendar].name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="title"
|
label="Title"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -145,6 +184,23 @@ function EventPopover({
|
|||||||
size="small"
|
size="small"
|
||||||
margin="dense"
|
margin="dense"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<InputLabel id="timezone-select-label">Time Zone</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="timezone-select-label"
|
||||||
|
value={timezone}
|
||||||
|
label="Time Zone"
|
||||||
|
onChange={(e: SelectChangeEvent) => setTimezone(e.target.value)}
|
||||||
|
>
|
||||||
|
{Object.keys(timezones).map((key) => (
|
||||||
|
<MenuItem key={key} value={timezones[key].aliasTo}>
|
||||||
|
{key}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
|
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -152,7 +208,11 @@ function EventPopover({
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="contained" onClick={handleSave}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!(calendarid ?? false) || !title}
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -162,3 +222,10 @@ function EventPopover({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default EventPopover;
|
export default EventPopover;
|
||||||
|
|
||||||
|
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())}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|
||||||
import { CalendarEvent } from "./EventsTypes";
|
|
||||||
|
|
||||||
const eventsSlice = createSlice({
|
|
||||||
name: "events",
|
|
||||||
initialState: [] as CalendarEvent[],
|
|
||||||
reducers: {
|
|
||||||
addEvent: (state, action: PayloadAction<CalendarEvent>) => {
|
|
||||||
state.push(action.payload);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const { addEvent } = eventsSlice.actions;
|
|
||||||
export default eventsSlice.reducer;
|
|
||||||
@@ -18,4 +18,5 @@ export interface CalendarEvent {
|
|||||||
allday?: Boolean;
|
allday?: Boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
timezone: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { Calendars } from "../Calendars/CalendarTypes";
|
||||||
import { userAttendee } from "../User/userDataTypes";
|
import { userAttendee } from "../User/userDataTypes";
|
||||||
import { CalendarEvent } from "./EventsTypes";
|
import { CalendarEvent } from "./EventsTypes";
|
||||||
|
import ICAL from "ical.js";
|
||||||
|
import { TIMEZONES } from "../../utils/timezone-data";
|
||||||
type RawEntry = [string, Record<string, string>, string, any];
|
type RawEntry = [string, Record<string, string>, string, any];
|
||||||
|
|
||||||
export function parseCalendarEvent(
|
export function parseCalendarEvent(
|
||||||
@@ -83,3 +85,137 @@ export function parseCalendarEvent(
|
|||||||
|
|
||||||
return event as CalendarEvent;
|
return event as CalendarEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calendarEventToJCal(event: CalendarEvent): any[] {
|
||||||
|
const tzid = event.timezone ?? "UTC"; // 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],
|
||||||
|
],
|
||||||
|
["class", {}, "text", event.class ?? "PUBLIC"],
|
||||||
|
[
|
||||||
|
"x-openpaas-videoconference",
|
||||||
|
{},
|
||||||
|
"unknown",
|
||||||
|
event.x_openpass_videoconference ?? null,
|
||||||
|
],
|
||||||
|
["summary", {}, "text", event.title ?? ""],
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (event.end) {
|
||||||
|
vevent[1].push([
|
||||||
|
"dtend",
|
||||||
|
{ tzid },
|
||||||
|
"date-time",
|
||||||
|
event.start.toISOString().split(".")[0],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (event.organizer) {
|
||||||
|
vevent[1].push([
|
||||||
|
"organizer",
|
||||||
|
{ cn: event.organizer.cn },
|
||||||
|
"cal-address",
|
||||||
|
`mailto:${event.organizer.cal_address}`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.attendee.forEach((att) => {
|
||||||
|
vevent[1].push([
|
||||||
|
"attendee",
|
||||||
|
{
|
||||||
|
cn: att.cn,
|
||||||
|
partstat: att.partstat,
|
||||||
|
rsvp: att.rsvp,
|
||||||
|
role: att.role,
|
||||||
|
cutype: att.cutype,
|
||||||
|
},
|
||||||
|
"cal-address",
|
||||||
|
`mailto:${att.cal_address}`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const vtimezone: any[] = ["vtimezone", [["tzid", {}, "text", tzid]], []];
|
||||||
|
|
||||||
|
return ["vcalendar", [], [vevent, vtimezone]];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateToICal(date: Date, tz?: string) {
|
||||||
|
// 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);
|
||||||
|
const day = pad(date.getDate());
|
||||||
|
const hours = pad(date.getHours());
|
||||||
|
const minutes = pad(date.getMinutes());
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
|||||||
|
// timezone.ts
|
||||||
|
|
||||||
|
// Ensure ICAL is imported before using this module.
|
||||||
|
import ICAL from "ical.js";
|
||||||
|
|
||||||
|
// TIMEZONES data must be imported or defined separately.
|
||||||
|
import { TIMEZONES } from "./timezone-data";
|
||||||
|
|
||||||
|
// Core timezone registration functionality
|
||||||
|
export function registerTimezones() {
|
||||||
|
for (const [key, data] of Object.entries(TIMEZONES.zones)) {
|
||||||
|
ICAL.TimezoneService.register(key, buildTimezone(key, data.ics));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, data] of Object.entries(TIMEZONES.aliases)) {
|
||||||
|
ICAL.TimezoneService.register(key, findTimezone(data.aliasTo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTimezone(tzid: string, ics: string): any {
|
||||||
|
return (
|
||||||
|
ICAL.TimezoneService.get(tzid) ||
|
||||||
|
new ICAL.Timezone(new ICAL.Component(ICAL.parse(ics)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTimezone(tzid: string): any {
|
||||||
|
if (TIMEZONES.zones[tzid]) {
|
||||||
|
return buildTimezone(tzid, TIMEZONES.zones[tzid].ics);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alias = TIMEZONES.aliases[tzid];
|
||||||
|
if (alias && alias.aliasTo) {
|
||||||
|
return findTimezone(alias.aliasTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown timezone alias: ${tzid}`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user