* feat: make source required on EventBusyDetails for Troubleshooter display - Make source a required property on EventBusyDetails type - Update LimitManager to accept and store source when adding busy times - Add user-friendly source names for all busy time types: - 'Booking Limit' for booking limit busy times - 'Duration Limit' for duration limit busy times - 'Team Booking Limit' for team booking limit busy times - 'Buffer Time' for seated event buffer times - 'Calendar' for external calendar busy times - Ensure all entries in detailedBusyTimes have source set - Cover includeManagedEventsInLimits and teamBookingLimits branches Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: add limit value and unit metadata to busy time sources - Include limit value and unit in source strings for Troubleshooter display - Booking Limit: shows as 'Booking Limit: 5 per day' - Duration Limit: shows as 'Duration Limit: 120 min per week' - Team Booking Limit: shows as 'Team Booking Limit: 10 per month' - Preserves existing calendar sources (e.g., 'google-calendar') Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: enhance busy time management with new limit sources - Introduced new limit sources for busy times, including event booking and duration limits, with user-friendly titles for display. - Updated LimitManager to accept and store detailed busy time information, including title and source. - Refactored busy time addition logic across various services to utilize the new structure, improving clarity and maintainability. * fixes * feat: conditionally include source and translate busy time titles - Add withSource parameter to conditionally include/exclude source from response - Translate busy time titles on frontend using useLocale hook - Source is only included when withSource=true (for Troubleshooter display) Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: enhance user availability service and busy time handling - Updated LargeCalendar component to include event ID check for enabling busy times. - Added translation for "busy" in common.json for better user experience. - Refactored getUserAvailability service to include new method for fetching user availability with busy times from limits. - Introduced parseLimits function to streamline booking and duration limit parsing. - Improved error handling in user handler for better user feedback. * refactor: remove unnecessary timeZone: undefined from addBusyTime calls Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: add buffer_time and calendar translation keys for busy times Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: add event source to calendar components and improve busy time handling - Updated EventList component to include event source in data attributes for better tracking. - Enhanced LargeCalendar component to pass event * fix: add missing source property to Date Override calendar event Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use 'date-override' as source for Date Override calendar events Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Avoid type assertion * fix: pass both bookingLimits and durationLimits to getStartEndDateforLimitCheck (#27898) * test: add unit tests for getUserAvailabilityIncludingBusyTimesFromLimits Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: pass both bookingLimits and durationLimits to getStartEndDateforLimitCheck - Fix bug where bookingLimits || durationLimits was passed as single param - Skip getBusyTimesForLimitChecks when eventType has no limits - Remove as never casts, use proper typing for mock dependencies - Replace expect.any(String) with exact ISO date assertions Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: replace loose assertions with exact values in tests Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: address review feedback on busy time sources - Fix t("busy") fallback to t("busy_time.busy") for correct translation lookup - Add missing title property to buffer time entries for Troubleshooter display - Use descriptive debug strings for buffer time source field - Fix import ordering in LargeCalendar.tsx Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-Authored-By: unknown <> * fix: preserve pre-existing busyTimesFromLimitsBookings from initialData Address review feedback from @hariombalhara (comment #30, #31): - Initialize busyTimesFromLimitsBookings from initialData instead of [] to avoid silently overwriting pre-existing data with an empty array - Use conditional spread to only include busyTimesFromLimitsBookings when it has a value - Add test verifying pre-existing busyTimesFromLimitsBookings is preserved and passed through to _getUserAvailability - Add test verifying busyTimesFromLimitsBookings is not passed when there are no limits and no initialData bookings Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-Authored-By: bot_apk <apk@cognition.ai> * fix: pass fetched eventType to _getUserAvailability to avoid duplicate DB query Addresses Devin Review comment r2863593564: the wrapper method fetches eventType but wasn't passing it through, causing _getUserAvailability to re-fetch the same eventType from the database. Also adds a test verifying eventType is forwarded correctly. Co-Authored-By: bot_apk <apk@cognition.ai> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: bot_apk <apk@cognition.ai>
334 lines
9.1 KiB
TypeScript
334 lines
9.1 KiB
TypeScript
import type { calendar_v3 } from "@googleapis/calendar";
|
|
import type { Dayjs } from "dayjs";
|
|
import type { TFunction } from "i18next";
|
|
import type { Time } from "ical.js";
|
|
import type { Frequency } from "rrule";
|
|
import type z from "zod";
|
|
|
|
import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
|
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
|
import type {
|
|
BookingSeat,
|
|
DestinationCalendar,
|
|
Prisma,
|
|
SelectedCalendar as _SelectedCalendar,
|
|
} from "@calcom/prisma/client";
|
|
import type { SchedulingType } from "@calcom/prisma/enums";
|
|
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
|
|
|
import type { Ensure } from "./utils";
|
|
|
|
export type { VideoCallData } from "./VideoApiAdapter";
|
|
|
|
type PaymentInfo = {
|
|
link?: string | null;
|
|
reason?: string | null;
|
|
id?: string | null;
|
|
paymentOption?: string | null;
|
|
amount?: number;
|
|
currency?: string;
|
|
};
|
|
|
|
export type Person = {
|
|
name: string;
|
|
email: string;
|
|
timeZone: string;
|
|
language: { translate: TFunction; locale: string };
|
|
username?: string;
|
|
usernameInOrg?: string;
|
|
id?: number;
|
|
bookingId?: number | null;
|
|
locale?: string | null;
|
|
timeFormat?: TimeFormat;
|
|
bookingSeat?: BookingSeat | null;
|
|
phoneNumber?: string | null;
|
|
};
|
|
|
|
export type TeamMember = {
|
|
id?: number;
|
|
name: string;
|
|
email: string;
|
|
phoneNumber?: string | null;
|
|
timeZone: string;
|
|
language: { translate: TFunction; locale: string };
|
|
};
|
|
|
|
export type EventBusyDate = {
|
|
start: Date | string;
|
|
end: Date | string;
|
|
source?: string | null;
|
|
timeZone?: string;
|
|
};
|
|
|
|
export type EventBusyDetails = EventBusyDate & {
|
|
title?: string;
|
|
source: string;
|
|
userId?: number | null;
|
|
};
|
|
|
|
export type AdditionalInfo = Record<string, unknown> & { calWarnings?: string[] };
|
|
|
|
export type NewCalendarEventType = {
|
|
uid: string;
|
|
id: string;
|
|
thirdPartyRecurringEventId?: string | null;
|
|
type: string;
|
|
password: string;
|
|
url: string;
|
|
additionalInfo: AdditionalInfo;
|
|
iCalUID?: string | null;
|
|
location?: string | null;
|
|
hangoutLink?: string | null;
|
|
conferenceData?: ConferenceData;
|
|
delegatedToId?: string | null;
|
|
};
|
|
|
|
export type CalendarEventType = {
|
|
uid: string;
|
|
etag: string;
|
|
/** This is the actual caldav event url, not the location url. */
|
|
url: string;
|
|
summary: string;
|
|
description: string;
|
|
location: string;
|
|
sequence: number;
|
|
startDate: Date | Dayjs;
|
|
endDate: Date | Dayjs;
|
|
duration: {
|
|
weeks: number;
|
|
days: number;
|
|
hours: number;
|
|
minutes: number;
|
|
seconds: number;
|
|
isNegative: boolean;
|
|
};
|
|
organizer: string;
|
|
attendees: unknown[][];
|
|
recurrenceId: Time;
|
|
timezone: string | object;
|
|
};
|
|
|
|
export type BatchResponse = {
|
|
responses: SubResponse[];
|
|
};
|
|
|
|
export type SubResponse = {
|
|
body: {
|
|
value: {
|
|
showAs: "free" | "tentative" | "away" | "busy" | "workingElsewhere";
|
|
start: { dateTime: string };
|
|
end: { dateTime: string };
|
|
}[];
|
|
};
|
|
};
|
|
|
|
export interface ConferenceData {
|
|
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
|
|
}
|
|
|
|
export interface RecurringEvent {
|
|
dtstart?: Date | undefined;
|
|
interval: number;
|
|
count: number;
|
|
freq: Frequency;
|
|
until?: Date | undefined;
|
|
tzid?: string | undefined;
|
|
}
|
|
|
|
export type { IntervalLimit, IntervalLimitUnit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
|
|
|
export type AppsStatus = {
|
|
appName: string;
|
|
type: (typeof App)["type"];
|
|
success: number;
|
|
failures: number;
|
|
errors: string[];
|
|
warnings?: string[];
|
|
};
|
|
|
|
export type CalEventResponses = Record<
|
|
string,
|
|
{
|
|
label: string;
|
|
value: z.infer<typeof bookingResponse>;
|
|
isHidden?: boolean;
|
|
}
|
|
>;
|
|
|
|
export interface ExistingRecurringEvent {
|
|
recurringEventId: string;
|
|
}
|
|
|
|
// If modifying this interface, probably should update builders/calendarEvent files
|
|
export interface CalendarEvent {
|
|
// Instead of sending this per event.
|
|
// TODO: Links sent in email should be validated and automatically redirected to org domain or regular app. It would be a much cleaner way. Maybe use existing /api/link endpoint
|
|
bookerUrl?: string;
|
|
hashedLink?: string | null;
|
|
type: string;
|
|
title: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
organizer: Person;
|
|
attendees: Person[];
|
|
length?: number | null;
|
|
additionalNotes?: string | null;
|
|
customInputs?: Prisma.JsonObject | null;
|
|
description?: string | null;
|
|
team?: {
|
|
name: string;
|
|
members: TeamMember[];
|
|
id: number;
|
|
};
|
|
location?: string | null;
|
|
conferenceCredentialId?: number;
|
|
conferenceData?: ConferenceData;
|
|
additionalInformation?: AdditionalInformation;
|
|
uid?: string | null;
|
|
existingRecurringEvent?: ExistingRecurringEvent | null;
|
|
bookingId?: number;
|
|
videoCallData?: VideoCallData;
|
|
paymentInfo?: PaymentInfo | null;
|
|
requiresConfirmation?: boolean | null;
|
|
destinationCalendar?: DestinationCalendar[] | null;
|
|
cancellationReason?: string | null;
|
|
rejectionReason?: string | null;
|
|
hideCalendarNotes?: boolean;
|
|
hideCalendarEventDetails?: boolean;
|
|
recurrence?: string;
|
|
recurringEvent?: RecurringEvent | null;
|
|
eventTypeId?: number | null;
|
|
appsStatus?: AppsStatus[];
|
|
seatsShowAttendees?: boolean | null;
|
|
seatsShowAvailabilityCount?: boolean | null;
|
|
attendeeSeatId?: string;
|
|
seatsPerTimeSlot?: number | null;
|
|
schedulingType?: SchedulingType | null;
|
|
iCalUID?: string | null;
|
|
iCalSequence?: number | null;
|
|
hideOrganizerEmail?: boolean;
|
|
disableCancelling?: boolean;
|
|
disableRescheduling?: boolean;
|
|
|
|
// It has responses to all the fields(system + user)
|
|
responses?: CalEventResponses | null;
|
|
|
|
// It just has responses to only the user fields. It allows to easily iterate over to show only user fields
|
|
userFieldsResponses?: CalEventResponses | null;
|
|
platformClientId?: string | null;
|
|
platformRescheduleUrl?: string | null;
|
|
platformCancelUrl?: string | null;
|
|
platformBookingUrl?: string | null;
|
|
hideBranding?: boolean;
|
|
oneTimePassword?: string | null;
|
|
delegationCredentialId?: string | null;
|
|
domainWideDelegationCredentialId?: string | null;
|
|
customReplyToEmail?: string | null;
|
|
rescheduledBy?: string;
|
|
organizationId?: number | null;
|
|
hasOrganizerChanged?: boolean;
|
|
assignmentReason?: {
|
|
category: string; // Translated label like "Routed", "Reassigned", etc.
|
|
details?: string | null; // The detailed reason string
|
|
} | null;
|
|
}
|
|
|
|
export interface EntryPoint {
|
|
entryPointType?: string;
|
|
uri?: string;
|
|
label?: string;
|
|
pin?: string;
|
|
accessCode?: string;
|
|
meetingCode?: string;
|
|
passcode?: string;
|
|
password?: string;
|
|
}
|
|
|
|
export interface AdditionalInformation {
|
|
conferenceData?: ConferenceData;
|
|
entryPoints?: EntryPoint[];
|
|
hangoutLink?: string;
|
|
}
|
|
|
|
export interface IntegrationCalendar extends Ensure<Partial<_SelectedCalendar>, "externalId" | "integration"> {
|
|
primary?: boolean;
|
|
name?: string;
|
|
readOnly?: boolean;
|
|
// For displaying the connected email address
|
|
email?: string;
|
|
primaryEmail?: string | null;
|
|
credentialId?: number | null;
|
|
integrationTitle?: string;
|
|
integration: string;
|
|
customCalendarReminder?: DestinationCalendar["customCalendarReminder"];
|
|
}
|
|
|
|
/**
|
|
* Mode for calendar fetch operations to control caching behavior:
|
|
* - "slots": For getting actual calendar availability (uses cache when available)
|
|
* - "overlay": For getting overlay calendar availability (does not use cache)
|
|
* - "booking": For booking confirmation (does not use cache)
|
|
* - "none": For operations that don't use getAvailability (e.g., deleteEvent, listCalendars)
|
|
*/
|
|
export type CalendarFetchMode = "slots" | "overlay" | "booking" | "none";
|
|
|
|
/**
|
|
* Parameters for getAvailability and getAvailabilityWithTimeZones methods
|
|
*/
|
|
export interface GetAvailabilityParams {
|
|
dateFrom: string;
|
|
dateTo: string;
|
|
selectedCalendars: IntegrationCalendar[];
|
|
mode: CalendarFetchMode;
|
|
fallbackToPrimary?: boolean;
|
|
}
|
|
|
|
/**
|
|
* null is to refer to user-level SelectedCalendar
|
|
*/
|
|
export type SelectedCalendarEventTypeIds = (number | null)[];
|
|
|
|
export interface CalendarServiceEvent extends CalendarEvent {
|
|
calendarDescription: string;
|
|
}
|
|
|
|
export interface Calendar {
|
|
getCredentialId?(): number;
|
|
createEvent(
|
|
event: CalendarServiceEvent,
|
|
credentialId: number,
|
|
externalCalendarId?: string
|
|
): Promise<NewCalendarEventType>;
|
|
|
|
updateEvent(
|
|
uid: string,
|
|
event: CalendarServiceEvent,
|
|
externalCalendarId?: string | null
|
|
): Promise<NewCalendarEventType | NewCalendarEventType[]>;
|
|
|
|
deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string | null): Promise<unknown>;
|
|
|
|
getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]>;
|
|
|
|
// for OOO calibration (only google calendar for now)
|
|
getAvailabilityWithTimeZones?(params: GetAvailabilityParams): Promise<EventBusyDate[]>;
|
|
|
|
fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise<unknown>;
|
|
|
|
listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]>;
|
|
|
|
testDelegationCredentialSetup?(): Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* @see [How to inference class type that implements an interface](https://stackoverflow.com/a/64765554/6297100)
|
|
*/
|
|
type Class<I, Args extends unknown[] = unknown[]> = new (...args: Args) => I;
|
|
|
|
export type CalendarClass = Class<Calendar, [CredentialForCalendarService]>;
|
|
|
|
export type SelectedCalendar = Pick<
|
|
_SelectedCalendar,
|
|
"userId" | "integration" | "externalId" | "credentialId"
|
|
>;
|