Merge pull request #54 from linagora/4-story-manage-attendance

4 story manage attendance
This commit is contained in:
Camille Moussu
2025-08-25 10:10:37 +02:00
committed by GitHub
18 changed files with 772 additions and 84 deletions
+35 -15
View File
@@ -2,11 +2,10 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Calendars } from "./CalendarTypes";
import { CalendarEvent } from "../Events/EventsTypes";
import { getCalendar, getCalendars } from "./CalendarApi";
import getOpenPaasUser from "../User/userAPI";
import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
import { parseCalendarEvent } from "../Events/eventUtils";
import { deleteEvent, putEvent } from "../Events/EventApi";
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
import { responsiveFontSizes } from "@mui/material";
export const getCalendarsListAsync = createAsyncThunk<
Record<string, Calendars> // Return type
@@ -19,14 +18,28 @@ export const getCalendarsListAsync = createAsyncThunk<
for (const cal of rawCalendars) {
const name = cal["dav:name"];
const description = cal["dav:description"];
const id = cal["calendarserver:source"]
let delegated = false;
let source = cal["calendarserver:source"]
? cal["calendarserver:source"]._links.self.href
.replace("/calendars/", "")
.replace(".json", "")
: cal._links.self.href.replace("/calendars/", "").replace(".json", "");
: cal._links.self.href;
if (cal["calendarserver:delegatedsource"]) {
source = cal["calendarserver:delegatedsource"];
delegated = true;
}
console.log(source);
const id = source.replace("/calendars/", "").replace(".json", "");
const ownerData: any = await getUserDetails(id.split("/")[0]);
const color = cal["apple:color"];
importedCalendars[id] = { id, name, description, color, events: {} };
importedCalendars[id] = {
id,
name,
ownerEmails: ownerData.emails,
description,
delegated,
color,
events: {},
};
}
return importedCalendars;
@@ -41,8 +54,9 @@ export const getCalendarDetailAsync = createAsyncThunk<
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
(eventdata: any) => {
const vevents = eventdata.data[2] as any[][]; // array of ['vevent', RawEntry[], []]
const eventURL = eventdata._links.self.href;
return vevents.map((vevent: any[]) => {
return parseCalendarEvent(vevent[1], color, calId);
return parseCalendarEvent(vevent[1], color, calId, eventURL);
});
}
);
@@ -54,18 +68,19 @@ export const putEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] }, // Return type
{ cal: Calendars; newEvent: CalendarEvent } // Arg type
>("calendars/putEvent", async ({ cal, newEvent }) => {
const response = await putEvent(cal, newEvent);
const response = await putEvent(newEvent);
const calEvents = (await getCalendar(cal.id, {
start: formatDateToYYYYMMDDTHHMMSS(newEvent.start),
start: formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)),
end: formatDateToYYYYMMDDTHHMMSS(
new Date(newEvent.start.getTime() + 86400000)
new Date(new Date(newEvent.start).getTime() + 86400000)
),
})) as Record<string, any>;
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
(eventdata: any) => {
const vevents = eventdata.data[2] as any[][];
const eventURL = eventdata._links.self.href;
return vevents.map((vevent: any[]) => {
return parseCalendarEvent(vevent[1], cal.color ?? "", cal.id);
return parseCalendarEvent(vevent[1], cal.color ?? "", cal.id, eventURL);
});
}
);
@@ -78,9 +93,9 @@ export const putEventAsync = createAsyncThunk<
export const deleteEventAsync = createAsyncThunk<
{ calId: string; eventId: string }, // Return type
{ calId: string; eventId: string } // Arg type
>("calendars/delEvent", async ({ calId, eventId }) => {
const response = await deleteEvent(calId, eventId);
{ calId: string; eventId: string; eventURL: string } // Arg type
>("calendars/delEvent", async ({ calId, eventId, eventURL }) => {
const response = await deleteEvent(eventURL);
return { calId, eventId };
});
@@ -105,6 +120,11 @@ const CalendarSlice = createSlice({
}
state.list[action.payload.calendarUid].events[action.payload.event.uid] =
action.payload.event;
state.list[action.payload.calendarUid].events[
action.payload.event.uid
].URL = `dav/calendars/${action.payload.calendarUid}/${
action.payload.event.uid.split("/")[0]
}.isc`;
},
removeEvent: (
state,
+2
View File
@@ -3,8 +3,10 @@ import { CalendarEvent } from "../Events/EventsTypes";
export interface Calendars {
id: string;
name: string;
delegated?: boolean;
prodid?: string;
color?: string;
ownerEmails?: string[];
description?: string;
calscale?: string;
version?: string;
+16 -20
View File
@@ -1,28 +1,24 @@
import { api } from "../../utils/apiUtils";
import { Calendars } from "../Calendars/CalendarTypes";
import { CalendarEvent } from "./EventsTypes";
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(calendarEventToJCal(event)),
headers: {
"content-type": "text/calendar; charset=utf-8",
},
}
).json();
return response;
export async function putEvent(event: CalendarEvent) {
const response = await api(`dav${event.URL}`, {
method: "PUT",
body: JSON.stringify(calendarEventToJCal(event)),
headers: {
"content-type": "text/calendar; charset=utf-8",
},
});
if (response.status === 201) {
console.log("PUT (201) :", response.url);
}
return await response.json();
}
export async function deleteEvent(calId: string, eventId: string) {
const response = await api(
`dav/calendars/${calId}/${eventId.split("/")[0]}.isc`,
{
method: "DELETE",
}
).json();
export async function deleteEvent(eventURL: string) {
const response = await api(eventURL, {
method: "DELETE",
}).json();
return response;
}
+44 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { deleteEventAsync } from "../Calendars/CalendarSlice";
import { deleteEventAsync, putEventAsync } from "../Calendars/CalendarSlice";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
Popover,
@@ -25,6 +25,7 @@ import CircleIcon from "@mui/icons-material/Circle";
import CancelIcon from "@mui/icons-material/Cancel";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import { userAttendee } from "../User/userDataTypes";
import { putEvent } from "./EventApi";
function EventDisplayModal({
eventId,
@@ -74,6 +75,18 @@ function EventDisplayModal({
(a) => a.cal_address === event.organizer?.cal_address
);
function handleRSVP(rsvp: string) {
const newEvent = {
...event,
attendee: event.attendee?.map((a) =>
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
),
};
dispatch(putEventAsync({ cal: calendar, newEvent }));
onClose({}, "backdropClick");
}
return (
<Popover open={open} anchorEl={anchorEl} onClose={onClose}>
<Card sx={{ width: 300, p: 2, position: "relative" }}>
@@ -195,9 +208,36 @@ function EventDisplayModal({
Will you attend?
</Typography>
<ButtonGroup size="small" fullWidth>
<Button>Accept</Button>
<Button>Maybe</Button>
<Button>Decline</Button>
<Button
color={
currentUserAttendee.partstat === "ACCEPTED"
? "success"
: "primary"
}
onClick={() => handleRSVP("ACCEPTED")}
>
Accept
</Button>
<Button
color={
currentUserAttendee.partstat === "TENTATIVE"
? "warning"
: "primary"
}
onClick={() => handleRSVP("TENTATIVE")}
>
Maybe
</Button>
<Button
color={
currentUserAttendee.partstat === "DECLINED"
? "error"
: "primary"
}
onClick={() => handleRSVP("DECLINED")}
>
Decline
</Button>
</ButtonGroup>
</Box>
)}
+1
View File
@@ -1,6 +1,7 @@
import { userAttendee, userOrganiser } from "../User/userDataTypes";
export interface CalendarEvent {
URL: string;
calId: string;
uid: string;
transp?: string;
+15 -10
View File
@@ -8,7 +8,8 @@ type RawEntry = [string, Record<string, string>, string, any];
export function parseCalendarEvent(
data: RawEntry[],
color: string,
calendarid: string
calendarid: string,
eventURL: string
): CalendarEvent {
const event: Partial<CalendarEvent> = { color, attendee: [] };
let recurrenceId;
@@ -75,6 +76,7 @@ export function parseCalendarEvent(
event.uid = `${event.uid}/${recurrenceId}`;
}
event.URL = eventURL;
if (!event.uid || !event.start) {
console.error(
`missing crucial event param in calendar ${calendarid} `,
@@ -98,7 +100,7 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
"dtstart",
{ tzid },
event.allday ? "date" : "date-time",
formatDateToICal(event.start, event.allday ?? false),
formatDateToICal(new Date(event.start), event.allday ?? false),
],
["class", {}, "text", event.class ?? "PUBLIC"],
[
@@ -117,7 +119,7 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
"dtend",
{ tzid },
event.allday ? "date" : "date-time",
formatDateToICal(event.end, event.allday ?? false),
formatDateToICal(new Date(event.end), event.allday ?? false),
]);
}
if (event.organizer) {
@@ -139,15 +141,18 @@ export function calendarEventToJCal(event: CalendarEvent): any[] {
}
event.attendee.forEach((att) => {
const attendee: Record<string, string> = {
partstat: att.partstat,
rsvp: att.rsvp,
role: att.role,
cutype: att.cutype,
};
if (att.cn) {
attendee.cn = att.cn;
}
vevent[1].push([
"attendee",
{
cn: att.cn,
partstat: att.partstat,
rsvp: att.rsvp,
role: att.role,
cutype: att.cutype,
},
attendee,
"cal-address",
`mailto:${att.cal_address}`,
]);
+6 -1
View File
@@ -1,6 +1,6 @@
import { api } from "../../utils/apiUtils";
export default async function getOpenPaasUser() {
export async function getOpenPaasUser() {
const user = await api.get(`api/user`).json();
return user;
}
@@ -25,3 +25,8 @@ export async function searchUsers(query: string) {
avatarUrl: user.photos?.[0]?.url || "",
}));
}
export async function getUserDetails(id: string) {
const user = await api.get(`api/users/${id}`).json();
return user;
}
+1 -1
View File
@@ -14,7 +14,7 @@ export interface userOrganiser {
}
export interface userAttendee {
cn: string;
cn?: string;
cal_address: string;
partstat: string;
rsvp: string;
+1 -1
View File
@@ -1,6 +1,6 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { userData, userOrganiser } from "./userDataTypes";
import getOpenPaasUser from "./userAPI";
import { getOpenPaasUser } from "./userAPI";
export const getOpenPaasUserDataAsync = createAsyncThunk<any>(
"user/getOpenPaasUserData",