feat: Add slotFormat query parameter to slots API V2 endpoint to return start and end time of a slot (#17873)
* feat: Add formatAsStartAndEndTime query parameter to slots API V2 endpoint to return start and end time of a slot * Update apps/api/v2/src/modules/slots/controllers/slots.controller.ts Co-authored-by: Praash <99237795+Praashh@users.noreply.github.com> * Fix duration was allowed to be any number * Refactor code * Show slotFormat property as optional in swagger doc --------- Co-authored-by: Praash <99237795+Praashh@users.noreply.github.com>
This commit is contained in:
@@ -78,4 +78,11 @@ export class EventTypesRepository_2024_04_15 {
|
||||
async deleteEventType(eventTypeId: number) {
|
||||
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async getEventTypeWithDuration(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
select: { length: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation }
|
||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries";
|
||||
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
|
||||
@@ -89,9 +90,31 @@ export class SlotsController {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time", example: "2024-09-25T08:00:00.000Z" },
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
properties: {
|
||||
time: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-09-25T08:00:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
startTime: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-09-25T08:00:00.000Z",
|
||||
},
|
||||
endTime: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-09-25T08:30:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -102,11 +125,18 @@ export class SlotsController {
|
||||
status: "success",
|
||||
data: {
|
||||
slots: {
|
||||
// Default format (when slotFormat is 'time' or not provided)
|
||||
"2024-09-25": [{ time: "2024-09-25T08:00:00.000Z" }, { time: "2024-09-25T08:15:00.000Z" }],
|
||||
// Alternative format (when slotFormat is 'range')
|
||||
"2024-09-26": [
|
||||
{ time: "2024-09-26T08:00:00.000Z" },
|
||||
{ time: "2024-09-26T08:15:00.000Z" },
|
||||
{ time: "2024-09-26T08:30:00.000Z" },
|
||||
{
|
||||
startTime: "2024-09-26T08:00:00.000Z",
|
||||
endTime: "2024-09-26T08:30:00.000Z",
|
||||
},
|
||||
{
|
||||
startTime: "2024-09-26T08:15:00.000Z",
|
||||
endTime: "2024-09-26T08:45:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -129,8 +159,20 @@ export class SlotsController {
|
||||
},
|
||||
});
|
||||
|
||||
const transformedSlots =
|
||||
query.slotFormat === SlotFormat.Range
|
||||
? await this.slotsService.formatSlots(
|
||||
availableSlots,
|
||||
query.duration,
|
||||
query.eventTypeId,
|
||||
query.slotFormat
|
||||
)
|
||||
: availableSlots.slots;
|
||||
|
||||
return {
|
||||
data: availableSlots,
|
||||
data: {
|
||||
slots: transformedSlots,
|
||||
},
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||
import { SlotsRepository } from "@/modules/slots/slots.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
import { ReserveSlotInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
@@ -54,4 +55,51 @@ export class SlotsService {
|
||||
const event = await this.eventTypeRepo.getEventTypeById(eventTypeId);
|
||||
return !!event?.teamId;
|
||||
}
|
||||
|
||||
async getEventTypeWithDuration(eventTypeId: number) {
|
||||
return await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
|
||||
}
|
||||
|
||||
async getDuration(duration?: number, eventTypeId?: number): Promise<number> {
|
||||
if (duration) {
|
||||
return duration;
|
||||
}
|
||||
|
||||
if (eventTypeId) {
|
||||
const eventType = await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
|
||||
if (!eventType) {
|
||||
throw new Error("Event type not found");
|
||||
}
|
||||
return eventType.length;
|
||||
}
|
||||
|
||||
throw new Error("duration or eventTypeId is required");
|
||||
}
|
||||
|
||||
async formatSlots(
|
||||
availableSlots: { slots: Record<string, { time: string }[]> },
|
||||
duration?: number,
|
||||
eventTypeId?: number,
|
||||
slotFormat?: SlotFormat
|
||||
): Promise<Record<string, { startTime: string; endTime: string }[]>> {
|
||||
if (slotFormat && !Object.values(SlotFormat).includes(slotFormat)) {
|
||||
throw new BadRequestException("Invalid slot format. Must be either 'range' or 'time'");
|
||||
}
|
||||
|
||||
const slotDuration = await this.getDuration(duration, eventTypeId);
|
||||
|
||||
return Object.entries(availableSlots.slots).reduce<
|
||||
Record<string, { startTime: string; endTime: string }[]>
|
||||
>((acc, [date, slots]) => {
|
||||
acc[date] = (slots as { time: string }[]).map((slot) => {
|
||||
const startTime = new Date(slot.time);
|
||||
const endTime = new Date(startTime.getTime() + slotDuration * 60000);
|
||||
return {
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
};
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5041,6 +5041,20 @@
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slotFormat",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Format of slot times in response. Use 'range' to get start and end times.",
|
||||
"example": "range",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"range",
|
||||
"time"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -5064,13 +5078,31 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"startTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
},
|
||||
"endTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:30:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5091,13 +5123,12 @@
|
||||
],
|
||||
"2024-09-26": [
|
||||
{
|
||||
"time": "2024-09-26T08:00:00.000Z"
|
||||
"startTime": "2024-09-26T08:00:00.000Z",
|
||||
"endTime": "2024-09-26T08:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:15:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:30:00.000Z"
|
||||
"startTime": "2024-09-26T08:15:00.000Z",
|
||||
"endTime": "2024-09-26T08:45:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4787,6 +4787,17 @@
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slotFormat",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Format of slot times in response. Use 'range' to get start and end times.",
|
||||
"example": "range",
|
||||
"schema": {
|
||||
"enum": ["range", "time"],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4810,13 +4821,31 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"startTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
},
|
||||
"endTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:30:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4837,13 +4866,12 @@
|
||||
],
|
||||
"2024-09-26": [
|
||||
{
|
||||
"time": "2024-09-26T08:00:00.000Z"
|
||||
"startTime": "2024-09-26T08:00:00.000Z",
|
||||
"endTime": "2024-09-26T08:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:15:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:30:00.000Z"
|
||||
"startTime": "2024-09-26T08:15:00.000Z",
|
||||
"endTime": "2024-09-26T08:45:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./event-types/scheduling-type";
|
||||
export * from "./event-types/frequency";
|
||||
export * from "./event-types/booker-layouts.enum";
|
||||
export * from "./event-types/confirmation-policy.enum";
|
||||
export * from "./slot-format.enum";
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum SlotFormat {
|
||||
Range = "range",
|
||||
Time = "time",
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsDateString, IsInt, IsNumber, IsOptional, IsString } from "class-validator";
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
IsEnum,
|
||||
} from "class-validator";
|
||||
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
|
||||
export class GetAvailableSlotsInput {
|
||||
@IsDateString()
|
||||
@@ -43,6 +55,7 @@ export class GetAvailableSlotsInput {
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Min(1, { message: "Duration must be a positive number" })
|
||||
@ApiProperty({ description: "Only for dynamic events - length of returned slots." })
|
||||
duration?: number;
|
||||
|
||||
@@ -57,6 +70,22 @@ export class GetAvailableSlotsInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
orgSlug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsEnum(SlotFormat, {
|
||||
message: "slotFormat must be either 'range' or 'time'",
|
||||
})
|
||||
@Transform(({ value }) => {
|
||||
if (!value) return undefined;
|
||||
return value.toLowerCase();
|
||||
})
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "Format of slot times in response. Use 'range' to get start and end times.",
|
||||
example: "range",
|
||||
enum: SlotFormat,
|
||||
})
|
||||
slotFormat?: SlotFormat;
|
||||
}
|
||||
|
||||
export class RemoveSelectedSlotInput {
|
||||
|
||||
Reference in New Issue
Block a user