Files
workavia-calendar-front/src/components/Calendar/utils/calendarUtils.ts
T
Camille Moussu 3cb445ea79 [#340] show declined event in settings (#398)
* [#340] show declined event in settings


Co-authored-by: Camille Moussu <cmoussu@linagora.com>
2025-12-09 11:11:20 +01:00

303 lines
7.9 KiB
TypeScript

import { CalendarEvent } from "../../../features/Events/EventsTypes";
import { Calendars } from "../../../features/Calendars/CalendarTypes";
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
import { getCalendarDetailAsync } from "../../../features/Calendars/CalendarSlice";
import { SlotLabelContentArg } from "@fullcalendar/core";
import moment from "moment-timezone";
import { refreshSingularCalendar } from "../../Event/utils/eventUtils";
import { ThunkDispatch } from "@reduxjs/toolkit";
import { useI18n } from "twake-i18n";
import { detectDateTimeFormat } from "../../Event/utils/dateTimeHelpers";
function convertEventDateTimeToISO(
datetime: string,
eventTimezone: string,
isAllDay: boolean
): string {
if (!datetime || isAllDay) return datetime;
if (datetime.includes("Z") || datetime.match(/[+-]\d{2}:\d{2}$/)) {
return datetime;
}
const dateOnlyRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
if (dateOnlyRegex.test(datetime)) {
return datetime;
}
const format = detectDateTimeFormat(datetime);
const momentDate = moment.tz(datetime, format, eventTimezone);
if (!momentDate.isValid()) {
console.warn(
`[convertEventDateTimeToISO] Invalid datetime: "${datetime}" with format "${format}" in timezone "${eventTimezone}"`
);
return datetime;
}
return momentDate.toISOString();
}
export const updateSlotLabelVisibility = (
currentTime: Date,
slotLabel: SlotLabelContentArg,
timezone: string
) => {
const isCurrentWeekOrDay = checkIfCurrentWeekOrDay();
if (!isCurrentWeekOrDay) {
return "fc-timegrid-slot-label";
}
const current = moment.tz(currentTime, timezone);
const currentMinutes = current.hours() * 60 + current.minutes();
const timeText = slotLabel?.text?.trim();
if (timeText && timeText.match(/^\d{1,2}:\d{2}$/)) {
const [hours, minutes] = timeText.split(":").map(Number);
const labelMinutes = hours * 60 + minutes;
let timeDiff = Math.abs(currentMinutes - labelMinutes);
if (timeDiff > 12 * 60) {
timeDiff = 24 * 60 - timeDiff;
}
if (timeDiff <= 15) {
return "timegrid-slot-label-hidden";
}
}
return "fc-timegrid-slot-label";
};
export const checkIfCurrentWeekOrDay = (): boolean => {
const todayColumn = document.querySelector(".fc-day-today");
if (!todayColumn) {
return false;
}
const nowIndicator = document.querySelector(
".fc-timegrid-now-indicator-arrow"
);
return !!nowIndicator;
};
export function formatEventChipTitle(e: CalendarEvent, t: Function) {
if (!e.title) {
return t("event.untitled");
}
return e.title === "Busy" && e.class === "PRIVATE"
? t("event.form.busy")
: e.title;
}
export const eventToFullCalendarFormat = (
filteredEvents: CalendarEvent[],
filteredTempEvents: CalendarEvent[],
userId: string | undefined
) => {
const { t } = useI18n();
return filteredEvents
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
.map((e) => {
const eventTimezone = e.timezone || "Etc/UTC";
const isAllDay = e.allday ?? false;
const convertedEvent: any = {
...e,
title: formatEventChipTitle(e, t),
colors: e.color,
editable: e.calId.split("/")[0] === userId,
};
if (!isAllDay && e.start && eventTimezone) {
const startISO = convertEventDateTimeToISO(
e.start,
eventTimezone,
isAllDay
);
if (startISO) {
convertedEvent.start = startISO;
}
}
if (!isAllDay && e.end && eventTimezone) {
const endISO = convertEventDateTimeToISO(
e.end,
eventTimezone,
isAllDay
);
if (endISO) {
convertedEvent.end = endISO;
}
}
return convertedEvent;
});
};
export const extractEvents = (
selectedCalendars: string[],
calendars: Record<string, Calendars>,
userAddress?: string,
hideDeclinedEvents?: boolean | null
) => {
const allEvents: CalendarEvent[] = [];
selectedCalendars.forEach((id) => {
const calendar = calendars[id];
if (calendar?.events) {
allEvents.push(...Object.values(calendar.events));
}
});
return allEvents
.filter((event) => event.status !== "CANCELLED")
.filter(
(event) =>
!(
hideDeclinedEvents &&
event.attendee?.some(
(a) => a.cal_address === userAddress && a.partstat === "DECLINED"
)
)
);
};
export const updateCalsDetails = (
selectedCalendars: string[],
previousSelectedCalendars: string[],
pending: boolean,
rangeKey: string,
previousRangeKey: string,
dispatch: Function,
calendarRange: { start: Date; end: Date },
calType?: "temp",
controllers?: Map<string, AbortController>
) => {
if (pending || !rangeKey) return;
const newCalendars = selectedCalendars.filter(
(id) => !previousSelectedCalendars.includes(id)
);
newCalendars.forEach((id) => {
if (controllers) {
const controller = new AbortController();
controllers.set(id, controller);
dispatch(
getCalendarDetailAsync({
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
calType,
signal: controller.signal,
})
);
} else {
dispatch(
getCalendarDetailAsync({
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
calType,
})
);
}
});
if (rangeKey !== previousRangeKey) {
selectedCalendars?.forEach((id) => {
if (id) {
if (controllers) {
const controller = new AbortController();
controllers.set(id, controller);
dispatch(
getCalendarDetailAsync({
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
calType,
signal: controller.signal,
})
);
} else {
dispatch(
getCalendarDetailAsync({
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
calType,
})
);
}
}
});
}
};
interface AclEntry {
privilege: string;
principal: string;
protected: boolean;
}
export function getCalendarVisibility(acl: AclEntry[]): "private" | "public" {
let hasRead = false;
let hasFreeBusy = false;
if (acl) {
for (const entry of acl) {
if (entry.principal !== "{DAV:}authenticated") continue;
if (entry.privilege === "{DAV:}read") {
hasRead = true;
break; // highest visibility, can stop
}
}
}
if (hasRead) return "public";
return "private";
}
export async function updateTempCalendar(
tempcalendars: Record<string, Calendars>,
event: CalendarEvent,
dispatch: ThunkDispatch<any, any, any>,
calendarRange: { start: Date; end: Date }
) {
if (tempcalendars && event?.attendee) {
const attendeesEmails = event.attendee
.map((a) => a.cal_address)
.filter(Boolean);
const tempCalendarValues = Object.values(tempcalendars);
for (const tempCalendar of tempCalendarValues) {
const ownerEmails = tempCalendar.ownerEmails || [];
// Check if any of the attendees are owners of this temp calendar
const isOwnerAttendee = ownerEmails.some((ownerEmail) =>
attendeesEmails.includes(ownerEmail)
);
if (isOwnerAttendee) {
await refreshSingularCalendar(
dispatch,
tempCalendar,
calendarRange,
"temp"
);
}
}
}
}