From 9d39590510d38dd143d21d4f48ecf26a3e115ae2 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Mon, 5 Jan 2026 16:41:05 +0530 Subject: [PATCH] feat: create BookingHistoryViewerService to combine audit logs with routing form submissions (#26045) * feat: create BookingHistoryViewerService to combine audit logs with routing form submissions Co-Authored-By: hariom@cal.com * refactor(booking): rename audit log query and enhance type safety - Updated the booking logs view to use the new getBookingHistory query instead of getAuditLogs. - Introduced DisplayBookingAuditLog type for improved clarity in BookingAuditViewerService. - Refactored BookingHistoryViewerService to utilize the new DisplayBookingAuditLog type and added sorting functionality for history logs. - Adjusted related types and methods to ensure consistency across services. * refactor(routing-forms): streamline imports and enhance type definitions - Consolidated type exports and imports from the features library for better organization. - Removed redundant type definitions and functions in zod.ts, findFieldValueByIdentifier.ts, getFieldIdentifier.ts, and parseRoutingFormResponse.ts. - Introduced new utility functions for handling field responses and parsing routing form responses. - Improved type safety and clarity across routing form response handling. * fix: remove double prefix from uniqueId in form submission entry Co-Authored-By: hariom@cal.com * 1c97f9cc5d50416788c01876fe539bcc9750e9b2 (#26453) --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../lib/findFieldValueByIdentifier.ts | 21 +--- .../routing-forms/lib/getFieldIdentifier.ts | 6 +- .../responseData/parseRoutingFormResponse.ts | 11 +- packages/app-store/routing-forms/zod.ts | 60 ++-------- .../client/components/BookingHistory.tsx | 2 +- .../BookingHistoryViewerService.container.ts | 14 +++ .../di/BookingHistoryViewerService.module.ts | 28 +++++ packages/features/booking-audit/di/tokens.ts | 2 + .../lib/service/BookingAuditViewerService.ts | 4 +- .../service/BookingHistoryViewerService.ts | 105 ++++++++++++++++++ packages/features/package.json | 1 + .../RoutingFormResponseRepository.module.ts | 21 ++++ packages/features/routing-forms/di/tokens.ts | 4 + .../lib/findFieldValueByIdentifier.ts | 21 ++++ .../routing-forms/lib/getFieldIdentifier.ts | 6 + .../lib/getFieldResponseByIdentifier.ts | 16 +++ .../lib/parseRoutingFormResponse.ts | 9 ++ packages/features/routing-forms/lib/types.ts | 22 ++++ packages/features/routing-forms/lib/zod.ts | 56 ++++++++++ .../routers/viewer/bookings/_router.tsx | 8 +- .../viewer/bookings/getAuditLogs.schema.ts | 10 -- ...andler.ts => getBookingHistory.handler.ts} | 15 ++- .../bookings/getBookingHistory.schema.ts | 9 ++ yarn.lock | 1 + 24 files changed, 341 insertions(+), 111 deletions(-) create mode 100644 packages/features/booking-audit/di/BookingHistoryViewerService.container.ts create mode 100644 packages/features/booking-audit/di/BookingHistoryViewerService.module.ts create mode 100644 packages/features/booking-audit/lib/service/BookingHistoryViewerService.ts create mode 100644 packages/features/routing-forms/di/RoutingFormResponseRepository.module.ts create mode 100644 packages/features/routing-forms/di/tokens.ts create mode 100644 packages/features/routing-forms/lib/findFieldValueByIdentifier.ts create mode 100644 packages/features/routing-forms/lib/getFieldIdentifier.ts create mode 100644 packages/features/routing-forms/lib/getFieldResponseByIdentifier.ts create mode 100644 packages/features/routing-forms/lib/parseRoutingFormResponse.ts create mode 100644 packages/features/routing-forms/lib/types.ts create mode 100644 packages/features/routing-forms/lib/zod.ts delete mode 100644 packages/trpc/server/routers/viewer/bookings/getAuditLogs.schema.ts rename packages/trpc/server/routers/viewer/bookings/{getAuditLogs.handler.ts => getBookingHistory.handler.ts} (78%) create mode 100644 packages/trpc/server/routers/viewer/bookings/getBookingHistory.schema.ts diff --git a/packages/app-store/routing-forms/lib/findFieldValueByIdentifier.ts b/packages/app-store/routing-forms/lib/findFieldValueByIdentifier.ts index dc57a341bf..3582a8586a 100644 --- a/packages/app-store/routing-forms/lib/findFieldValueByIdentifier.ts +++ b/packages/app-store/routing-forms/lib/findFieldValueByIdentifier.ts @@ -1,20 +1 @@ -import getFieldIdentifier from "./getFieldIdentifier"; -import type { RoutingFormResponseData } from "./responseData/types"; - -type FindFieldValueByIdentifierResult = - | { success: true; data: string | string[] | number | null } - | { success: false; error: string }; - -export function findFieldValueByIdentifier( - data: RoutingFormResponseData, - identifier: string -): FindFieldValueByIdentifierResult { - const field = data.fields.find((field) => getFieldIdentifier(field) === identifier); - if (!field) { - return { success: false, error: `Field with identifier ${identifier} not found` }; - } - - const fieldValue = data.response[field.id]?.value; - - return { success: true, data: fieldValue ?? null }; -} +export { findFieldValueByIdentifier } from "@calcom/features/routing-forms/lib/findFieldValueByIdentifier"; diff --git a/packages/app-store/routing-forms/lib/getFieldIdentifier.ts b/packages/app-store/routing-forms/lib/getFieldIdentifier.ts index 9d0eebdeaf..87e22a49bb 100644 --- a/packages/app-store/routing-forms/lib/getFieldIdentifier.ts +++ b/packages/app-store/routing-forms/lib/getFieldIdentifier.ts @@ -1,5 +1 @@ -import type { Field } from "../types/types"; - -const getFieldIdentifier = (field: Field) => field.identifier || field.label; - -export default getFieldIdentifier; +export { default } from "@calcom/features/routing-forms/lib/getFieldIdentifier"; diff --git a/packages/app-store/routing-forms/lib/responseData/parseRoutingFormResponse.ts b/packages/app-store/routing-forms/lib/responseData/parseRoutingFormResponse.ts index 438ffdb9d4..96f04ab970 100644 --- a/packages/app-store/routing-forms/lib/responseData/parseRoutingFormResponse.ts +++ b/packages/app-store/routing-forms/lib/responseData/parseRoutingFormResponse.ts @@ -1,10 +1 @@ -import { zodNonRouterField } from "@calcom/app-store/routing-forms/zod"; -import { routingFormResponseInDbSchema } from "@calcom/app-store/routing-forms/zod"; - -import type { RoutingFormResponseData } from "./types"; - -export function parseRoutingFormResponse(rawResponse: unknown, formFields: unknown): RoutingFormResponseData { - const response = routingFormResponseInDbSchema.parse(rawResponse); - const fields = zodNonRouterField.array().parse(formFields); - return { response, fields }; -} +export { parseRoutingFormResponse } from "@calcom/features/routing-forms/lib/parseRoutingFormResponse"; diff --git a/packages/app-store/routing-forms/zod.ts b/packages/app-store/routing-forms/zod.ts index 8054b8fd95..2045854e78 100644 --- a/packages/app-store/routing-forms/zod.ts +++ b/packages/app-store/routing-forms/zod.ts @@ -4,50 +4,15 @@ import { raqbQueryValueSchema } from "@calcom/lib/raqb/zod"; import { routingFormAppDataSchemas } from "./appDataSchemas"; -export type FieldOption = { - label: string; - id: string | null; -}; +export { + zodNonRouterField, + routingFormResponseInDbSchema, + type FieldOption, + type TNonRouterField, +} from "@calcom/features/routing-forms/lib/zod"; -export type TNonRouterField = { - id: string; - label: string; - identifier?: string; - placeholder?: string; - type: string; - /** @deprecated in favour of `options` */ - selectText?: string; - required?: boolean; - deleted?: boolean; - options?: FieldOption[]; -}; - -// Note: zodNonRouterField is NOT annotated with z.ZodType because it uses .extend() below -// which requires the full ZodObject type to be preserved -export const zodNonRouterField = z.object({ - id: z.string(), - label: z.string(), - identifier: z.string().optional(), - placeholder: z.string().optional(), - type: z.string(), - /** - * @deprecated in favour of `options` - */ - selectText: z.string().optional(), - required: z.boolean().optional(), - deleted: z.boolean().optional(), - options: z - .array( - z.object({ - label: z.string(), - // To keep backwards compatibility with the options generated from legacy selectText, we allow saving null as id - // It helps in differentiating whether the routing logic should consider the option.label as value or option.id as value. - // This is important for legacy routes which has option.label saved in conditions and it must keep matching with the value of the option - id: z.string().or(z.null()), - }) - ) - .optional(), -}); +import type { TNonRouterField } from "@calcom/features/routing-forms/lib/zod"; +import { zodNonRouterField } from "@calcom/features/routing-forms/lib/zod"; export type TRouterField = TNonRouterField & { routerId: string; @@ -158,12 +123,3 @@ export const zodRoutesView = z.union([z.array(zodRouteView), z.null()]).optional export const appDataSchema = z.any(); export const appKeysSchema = z.object({}); - -// This is different from FormResponse in types.d.ts in that it has label optional. We don't seem to be using label at this point, so we might want to use this only while saving the response when Routing Form is submitted -// Record key is formFieldId -export const routingFormResponseInDbSchema = z.record( - z.object({ - label: z.string().optional(), - value: z.union([z.string(), z.number(), z.array(z.string())]), - }) -); diff --git a/packages/features/booking-audit/client/components/BookingHistory.tsx b/packages/features/booking-audit/client/components/BookingHistory.tsx index d5f020a3e5..9c487a2c59 100644 --- a/packages/features/booking-audit/client/components/BookingHistory.tsx +++ b/packages/features/booking-audit/client/components/BookingHistory.tsx @@ -360,7 +360,7 @@ export function BookingHistory({ bookingUid }: BookingHistoryProps) { const [searchTerm, setSearchTerm] = useState(""); const [actorFilter, setActorFilter] = useState(null); const { t } = useLocale(); - const { data, isLoading, error } = trpc.viewer.bookings.getAuditLogs.useQuery({ + const { data, isLoading, error } = trpc.viewer.bookings.getBookingHistory.useQuery({ bookingUid, }); diff --git a/packages/features/booking-audit/di/BookingHistoryViewerService.container.ts b/packages/features/booking-audit/di/BookingHistoryViewerService.container.ts new file mode 100644 index 0000000000..c20cd3e1b6 --- /dev/null +++ b/packages/features/booking-audit/di/BookingHistoryViewerService.container.ts @@ -0,0 +1,14 @@ +import { createContainer } from "@calcom/features/di/di"; + +import { + type BookingHistoryViewerService, + moduleLoader as bookingHistoryViewerServiceModule, +} from "./BookingHistoryViewerService.module"; + +const container = createContainer(); + +export function getBookingHistoryViewerService() { + bookingHistoryViewerServiceModule.loadModule(container); + + return container.get(bookingHistoryViewerServiceModule.token); +} diff --git a/packages/features/booking-audit/di/BookingHistoryViewerService.module.ts b/packages/features/booking-audit/di/BookingHistoryViewerService.module.ts new file mode 100644 index 0000000000..e728da1c95 --- /dev/null +++ b/packages/features/booking-audit/di/BookingHistoryViewerService.module.ts @@ -0,0 +1,28 @@ +import { BookingHistoryViewerService } from "@calcom/features/booking-audit/lib/service/BookingHistoryViewerService"; +import { BOOKING_AUDIT_DI_TOKENS } from "@calcom/features/booking-audit/di/tokens"; +import { moduleLoader as bookingAuditViewerServiceModuleLoader } from "@calcom/features/booking-audit/di/BookingAuditViewerService.module"; +import { moduleLoader as routingFormResponseRepositoryModuleLoader } from "@calcom/features/routing-forms/di/RoutingFormResponseRepository.module"; + +import { createModule, bindModuleToClassOnToken } from "../../di/di"; + +export const bookingHistoryViewerServiceModule = createModule(); +const token = BOOKING_AUDIT_DI_TOKENS.BOOKING_HISTORY_VIEWER_SERVICE; +const moduleToken = BOOKING_AUDIT_DI_TOKENS.BOOKING_HISTORY_VIEWER_SERVICE_MODULE; + +export { BookingHistoryViewerService }; + +const loadModule = bindModuleToClassOnToken({ + module: bookingHistoryViewerServiceModule, + moduleToken, + token, + classs: BookingHistoryViewerService, + depsMap: { + bookingAuditViewerService: bookingAuditViewerServiceModuleLoader, + routingFormResponseRepository: routingFormResponseRepositoryModuleLoader, + }, +}); + +export const moduleLoader = { + token, + loadModule, +}; diff --git a/packages/features/booking-audit/di/tokens.ts b/packages/features/booking-audit/di/tokens.ts index 78472ceafb..50b03f750a 100644 --- a/packages/features/booking-audit/di/tokens.ts +++ b/packages/features/booking-audit/di/tokens.ts @@ -9,4 +9,6 @@ export const BOOKING_AUDIT_DI_TOKENS = { BOOKING_AUDIT_REPOSITORY_MODULE: Symbol("BookingAuditRepositoryModule"), AUDIT_ACTOR_REPOSITORY: Symbol("AuditActorRepository"), AUDIT_ACTOR_REPOSITORY_MODULE: Symbol("AuditActorRepositoryModule"), + BOOKING_HISTORY_VIEWER_SERVICE: Symbol("BookingHistoryViewerService"), + BOOKING_HISTORY_VIEWER_SERVICE_MODULE: Symbol("BookingHistoryViewerServiceModule"), }; diff --git a/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts b/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts index 165afacb21..e480b2345e 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts @@ -48,6 +48,8 @@ type EnrichedAuditLog = { }; }; +export type DisplayBookingAuditLog = EnrichedAuditLog; + /** * BookingAuditViewerService - Service for viewing and formatting booking audit logs */ @@ -93,7 +95,7 @@ export class BookingAuditViewerService { userEmail: string; userTimeZone: string; organizationId: number | null; - }): Promise<{ bookingUid: string; auditLogs: EnrichedAuditLog[] }> { + }): Promise<{ bookingUid: string; auditLogs: DisplayBookingAuditLog[] }> { const { bookingUid, userId, userTimeZone, organizationId } = params; await this.accessService.assertPermissions({ bookingUid, diff --git a/packages/features/booking-audit/lib/service/BookingHistoryViewerService.ts b/packages/features/booking-audit/lib/service/BookingHistoryViewerService.ts new file mode 100644 index 0000000000..e6d66452cc --- /dev/null +++ b/packages/features/booking-audit/lib/service/BookingHistoryViewerService.ts @@ -0,0 +1,105 @@ +import type { RoutingFormResponseRepositoryInterface } from "@calcom/lib/server/repository/RoutingFormResponseRepository.interface"; + +import type { BookingAuditViewerService, DisplayBookingAuditLog } from "./BookingAuditViewerService"; +import { getFieldResponseByIdentifier } from "@calcom/features/routing-forms/lib/getFieldResponseByIdentifier"; + +type GetHistoryForBookingParams = { + bookingUid: string; + userId: number; + userEmail: string; + userTimeZone: string; + organizationId: number | null; +}; + +type BookingHistoryLog = DisplayBookingAuditLog; + +interface BookingHistoryViewerServiceDeps { + bookingAuditViewerService: BookingAuditViewerService; + routingFormResponseRepository: RoutingFormResponseRepositoryInterface; +} + +export class BookingHistoryViewerService { + private readonly bookingAuditViewerService: BookingAuditViewerService; + private readonly routingFormResponseRepository: RoutingFormResponseRepositoryInterface; + + constructor(private readonly deps: BookingHistoryViewerServiceDeps) { + this.bookingAuditViewerService = deps.bookingAuditViewerService; + this.routingFormResponseRepository = deps.routingFormResponseRepository; + } + + private sortLogsReverseChronologically(historyLogs: BookingHistoryLog[]): BookingHistoryLog[] { + return historyLogs.sort((a, b) => { + const timestampA = new Date(a.timestamp).getTime(); + const timestampB = new Date(b.timestamp).getTime(); + return timestampB - timestampA; + }); + } + + private async getFormAuditLogsForBooking(bookingUid: string): Promise { + // TODO: Form doesn't have its Audit Logs yet, so we replicate them using the Form Response directly for now. + const formResponse = await this.routingFormResponseRepository.findByBookingUidIncludeForm(bookingUid); + if (!formResponse) { + return []; + } + return [this.createFormSubmissionEntry({ formResponse, bookingUid })]; + } + + async getHistoryForBooking( + params: GetHistoryForBookingParams + ): Promise<{ bookingUid: string; auditLogs: BookingHistoryLog[] }> { + const { bookingUid } = params; + + const { auditLogs: bookingAuditLogs } = await this.bookingAuditViewerService.getAuditLogsForBooking(params); + + const historyEntries: BookingHistoryLog[] = [...bookingAuditLogs, ...await this.getFormAuditLogsForBooking(bookingUid)]; + + const sortedLogs = this.sortLogsReverseChronologically(historyEntries); + + return { + bookingUid, + auditLogs: sortedLogs, + }; + } + + private createFormSubmissionEntry({ + formResponse, + bookingUid, + }: { + formResponse: NonNullable< + Awaited> + >; + bookingUid: string; + }): BookingHistoryLog { + const timestamp = formResponse.createdAt.toISOString(); + + const emailFieldResult = getFieldResponseByIdentifier({ responsePayload: formResponse.response, formFields: formResponse.form.fields, identifier: "email" }); + const emailFieldValueFromResponse = emailFieldResult.success ? emailFieldResult.data : null; + // A valid string can be the email otherwise we assume it is not an email + const submitterEmail = typeof emailFieldValueFromResponse === "string" ? emailFieldValueFromResponse : null; + const uniqueId = `form-submission-${formResponse.id}`; + return { + id: uniqueId, + bookingUid, + type: "RECORD_CREATED", + action: "CREATED", + timestamp, + createdAt: timestamp, + source: "WEBAPP", + operationId: uniqueId, + displayJson: null, + actionDisplayTitle: { key: "form_submitted" }, + displayFields: null, + actor: { + id: `form-submission-actor-${formResponse.id}`, + type: "GUEST", + userUuid: null, + attendeeId: null, + name: null, + createdAt: formResponse.createdAt, + displayName: submitterEmail ? `${submitterEmail}` : "Guest", + displayEmail: submitterEmail || null, + displayAvatar: null, + }, + }; + } +} diff --git a/packages/features/package.json b/packages/features/package.json index ba376ac260..2cd6240b47 100644 --- a/packages/features/package.json +++ b/packages/features/package.json @@ -27,6 +27,7 @@ "date-fns-tz": "3.2.0", "framer-motion": "10.12.8", "p-limit": "6.2.0", + "react-awesome-query-builder": "5.1.2", "react-select": "5.8.0", "react-sticky-box": "2.0.4", "recharts": "3.0.2", diff --git a/packages/features/routing-forms/di/RoutingFormResponseRepository.module.ts b/packages/features/routing-forms/di/RoutingFormResponseRepository.module.ts new file mode 100644 index 0000000000..fa1466a0b0 --- /dev/null +++ b/packages/features/routing-forms/di/RoutingFormResponseRepository.module.ts @@ -0,0 +1,21 @@ +import { PrismaRoutingFormResponseRepository } from "@calcom/lib/server/repository/PrismaRoutingFormResponseRepository"; +import { ROUTING_FORM_DI_TOKENS } from "@calcom/features/routing-forms/di/tokens"; +import { bindModuleToClassOnToken } from "@calcom/features/di/di"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; +import { createModule } from "../../di/di"; + +export const routingFormResponseRepositoryModule = createModule(); +const token = ROUTING_FORM_DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY; +const moduleToken = ROUTING_FORM_DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY_MODULE; +const loadModule = bindModuleToClassOnToken({ + module: routingFormResponseRepositoryModule, + moduleToken, + token, + classs: PrismaRoutingFormResponseRepository, + dep: prismaModuleLoader, +}); + +export const moduleLoader = { + token, + loadModule, +}; diff --git a/packages/features/routing-forms/di/tokens.ts b/packages/features/routing-forms/di/tokens.ts new file mode 100644 index 0000000000..fdd070bf4f --- /dev/null +++ b/packages/features/routing-forms/di/tokens.ts @@ -0,0 +1,4 @@ +export const ROUTING_FORM_DI_TOKENS = { + ROUTING_FORM_RESPONSE_REPOSITORY: Symbol("RoutingFormResponseRepository"), + ROUTING_FORM_RESPONSE_REPOSITORY_MODULE: Symbol("RoutingFormResponseRepositoryModule"), +}; diff --git a/packages/features/routing-forms/lib/findFieldValueByIdentifier.ts b/packages/features/routing-forms/lib/findFieldValueByIdentifier.ts new file mode 100644 index 0000000000..1485a17d7e --- /dev/null +++ b/packages/features/routing-forms/lib/findFieldValueByIdentifier.ts @@ -0,0 +1,21 @@ +import getFieldIdentifier from "./getFieldIdentifier"; +import type { RoutingFormResponseData } from "./types"; + +type FindFieldValueByIdentifierResult = + | { success: true; data: string | string[] | number | null } + | { success: false; error: string }; + +export function findFieldValueByIdentifier( + data: RoutingFormResponseData, + identifier: string +): FindFieldValueByIdentifierResult { + const field = data.fields.find((field) => getFieldIdentifier(field) === identifier); + if (!field) { + return { success: false, error: `Field with identifier ${identifier} not found` }; + } + + const fieldValue = data.response[field.id]?.value; + + return { success: true, data: fieldValue ?? null }; +} + diff --git a/packages/features/routing-forms/lib/getFieldIdentifier.ts b/packages/features/routing-forms/lib/getFieldIdentifier.ts new file mode 100644 index 0000000000..a7f7332572 --- /dev/null +++ b/packages/features/routing-forms/lib/getFieldIdentifier.ts @@ -0,0 +1,6 @@ +import type { Field } from "./types"; + +const getFieldIdentifier = (field: Field) => field.identifier || field.label; + +export default getFieldIdentifier; + diff --git a/packages/features/routing-forms/lib/getFieldResponseByIdentifier.ts b/packages/features/routing-forms/lib/getFieldResponseByIdentifier.ts new file mode 100644 index 0000000000..3e26e03b2d --- /dev/null +++ b/packages/features/routing-forms/lib/getFieldResponseByIdentifier.ts @@ -0,0 +1,16 @@ +import { parseRoutingFormResponse } from "./parseRoutingFormResponse"; +import { findFieldValueByIdentifier } from "./findFieldValueByIdentifier"; + +export const getFieldResponseByIdentifier = ({ + responsePayload, + formFields, + identifier, +}: { + responsePayload: unknown; + formFields: unknown; + identifier: string; +}) => { + const parsedResponse = parseRoutingFormResponse(responsePayload, formFields); + const emailFieldResult = findFieldValueByIdentifier(parsedResponse, identifier); + return emailFieldResult; +}; \ No newline at end of file diff --git a/packages/features/routing-forms/lib/parseRoutingFormResponse.ts b/packages/features/routing-forms/lib/parseRoutingFormResponse.ts new file mode 100644 index 0000000000..732231fa04 --- /dev/null +++ b/packages/features/routing-forms/lib/parseRoutingFormResponse.ts @@ -0,0 +1,9 @@ +import { zodNonRouterField, routingFormResponseInDbSchema } from "./zod"; +import type { RoutingFormResponseData } from "./types"; + +export function parseRoutingFormResponse(rawResponse: unknown, formFields: unknown): RoutingFormResponseData { + const response = routingFormResponseInDbSchema.parse(rawResponse); + const fields = zodNonRouterField.array().parse(formFields); + return { response, fields }; +} + diff --git a/packages/features/routing-forms/lib/types.ts b/packages/features/routing-forms/lib/types.ts new file mode 100644 index 0000000000..4664d98beb --- /dev/null +++ b/packages/features/routing-forms/lib/types.ts @@ -0,0 +1,22 @@ +import type z from "zod"; + +import type { zodNonRouterField, routingFormResponseInDbSchema } from "./zod"; + +export type FormResponse = Record< + // Field ID + string, + { + value: number | string | string[]; + label: string; + identifier?: string; + } +>; + +export type Field = z.infer; +export type Fields = Field[]; + +export type RoutingFormResponseData = { + fields: z.infer[]; + response: z.infer; +}; + diff --git a/packages/features/routing-forms/lib/zod.ts b/packages/features/routing-forms/lib/zod.ts new file mode 100644 index 0000000000..012fdddab4 --- /dev/null +++ b/packages/features/routing-forms/lib/zod.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +export type FieldOption = { + label: string; + id: string | null; +}; + +export type TNonRouterField = { + id: string; + label: string; + identifier?: string; + placeholder?: string; + type: string; + /** @deprecated in favour of `options` */ + selectText?: string; + required?: boolean; + deleted?: boolean; + options?: FieldOption[]; +}; + +// Note: zodNonRouterField is NOT annotated with z.ZodType because it uses .extend() below +// which requires the full ZodObject type to be preserved +export const zodNonRouterField = z.object({ + id: z.string(), + label: z.string(), + identifier: z.string().optional(), + placeholder: z.string().optional(), + type: z.string(), + /** + * @deprecated in favour of `options` + */ + selectText: z.string().optional(), + required: z.boolean().optional(), + deleted: z.boolean().optional(), + options: z + .array( + z.object({ + label: z.string(), + // To keep backwards compatibility with the options generated from legacy selectText, we allow saving null as id + // It helps in differentiating whether the routing logic should consider the option.label as value or option.id as value. + // This is important for legacy routes which has option.label saved in conditions and it must keep matching with the value of the option + id: z.string().or(z.null()), + }) + ) + .optional(), +}); + +// This is different from FormResponse in types.d.ts in that it has label optional. We don't seem to be using label at this point, so we might want to use this only while saving the response when Routing Form is submitted +// Record key is formFieldId +export const routingFormResponseInDbSchema = z.record( + z.object({ + label: z.string().optional(), + value: z.union([z.string(), z.number(), z.array(z.string())]), + }) +); + diff --git a/packages/trpc/server/routers/viewer/bookings/_router.tsx b/packages/trpc/server/routers/viewer/bookings/_router.tsx index 5d6ff61733..2b456aceb0 100644 --- a/packages/trpc/server/routers/viewer/bookings/_router.tsx +++ b/packages/trpc/server/routers/viewer/bookings/_router.tsx @@ -6,7 +6,7 @@ import { ZConfirmInputSchema } from "./confirm.schema"; import { ZEditLocationInputSchema } from "./editLocation.schema"; import { ZFindInputSchema } from "./find.schema"; import { ZGetInputSchema } from "./get.schema"; -import { ZGetAuditLogsInputSchema } from "./getAuditLogs.schema"; +import { ZGetBookingHistoryInputSchema } from "./getBookingHistory.schema"; import { ZGetBookingAttendeesInputSchema } from "./getBookingAttendees.schema"; import { ZGetBookingDetailsInputSchema } from "./getBookingDetails.schema"; import { ZInstantBookingInputSchema } from "./getInstantBookingLocation.schema"; @@ -108,10 +108,10 @@ export const bookingsRouter = router({ input, }); }), - getAuditLogs: authedProcedure.input(ZGetAuditLogsInputSchema).query(async ({ input, ctx }) => { - const { getAuditLogsHandler } = await import("./getAuditLogs.handler"); + getBookingHistory: authedProcedure.input(ZGetBookingHistoryInputSchema).query(async ({ input, ctx }) => { + const { getBookingHistoryHandler } = await import("./getBookingHistory.handler"); - return getAuditLogsHandler({ + return getBookingHistoryHandler({ ctx, input, }); diff --git a/packages/trpc/server/routers/viewer/bookings/getAuditLogs.schema.ts b/packages/trpc/server/routers/viewer/bookings/getAuditLogs.schema.ts deleted file mode 100644 index 470892626e..0000000000 --- a/packages/trpc/server/routers/viewer/bookings/getAuditLogs.schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from "zod"; - -export type TGetAuditLogsInputSchema = { - bookingUid: string; -}; - -export const ZGetAuditLogsInputSchema: z.ZodType = z.object({ - bookingUid: z.string(), -}); - diff --git a/packages/trpc/server/routers/viewer/bookings/getAuditLogs.handler.ts b/packages/trpc/server/routers/viewer/bookings/getBookingHistory.handler.ts similarity index 78% rename from packages/trpc/server/routers/viewer/bookings/getAuditLogs.handler.ts rename to packages/trpc/server/routers/viewer/bookings/getBookingHistory.handler.ts index b472b16048..5257c3b2dd 100644 --- a/packages/trpc/server/routers/viewer/bookings/getAuditLogs.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/getBookingHistory.handler.ts @@ -3,19 +3,19 @@ import type { TFunction } from "i18next"; import type { PrismaClient } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; -import { getBookingAuditViewerService } from "@calcom/features/booking-audit/di/BookingAuditViewerService.container"; +import { getBookingHistoryViewerService } from "@calcom/features/booking-audit/di/BookingHistoryViewerService.container"; import { BookingAuditErrorCode, BookingAuditPermissionError } from "@calcom/features/booking-audit/lib/service/BookingAuditAccessService"; import { getTranslation } from "@calcom/lib/server/i18n"; import type { TrpcSessionUser } from "../../../types"; -import type { TGetAuditLogsInputSchema } from "./getAuditLogs.schema"; +import type { TGetBookingHistoryInputSchema } from "./getBookingHistory.schema"; -type GetAuditLogsOptions = { +type GetBookingHistoryOptions = { ctx: { user: NonNullable; prisma: PrismaClient; }; - input: TGetAuditLogsInputSchema; + input: TGetBookingHistoryInputSchema; }; const getErrorMessage = (code: BookingAuditErrorCode, t: TFunction): string => { @@ -35,15 +35,15 @@ const getErrorMessage = (code: BookingAuditErrorCode, t: TFunction): string => { } }; -export const getAuditLogsHandler = async ({ ctx, input }: GetAuditLogsOptions) => { +export const getBookingHistoryHandler = async ({ ctx, input }: GetBookingHistoryOptions) => { const { user } = ctx; const { bookingUid } = input; const t = await getTranslation(user.locale ?? "en", "common"); - const bookingAuditViewerService = getBookingAuditViewerService(); + const bookingHistoryViewerService = getBookingHistoryViewerService(); try { - const result = await bookingAuditViewerService.getAuditLogsForBooking({ + const result = await bookingHistoryViewerService.getHistoryForBooking({ bookingUid, userId: user.id, userEmail: user.email, @@ -62,4 +62,3 @@ export const getAuditLogsHandler = async ({ ctx, input }: GetAuditLogsOptions) = throw error; } }; - diff --git a/packages/trpc/server/routers/viewer/bookings/getBookingHistory.schema.ts b/packages/trpc/server/routers/viewer/bookings/getBookingHistory.schema.ts new file mode 100644 index 0000000000..e44c481efe --- /dev/null +++ b/packages/trpc/server/routers/viewer/bookings/getBookingHistory.schema.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export type TGetBookingHistoryInputSchema = { + bookingUid: string; +}; + +export const ZGetBookingHistoryInputSchema: z.ZodType = z.object({ + bookingUid: z.string(), +}); diff --git a/yarn.lock b/yarn.lock index 695f49742d..704ba70ffc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2598,6 +2598,7 @@ __metadata: date-fns-tz: "npm:3.2.0" framer-motion: "npm:10.12.8" p-limit: "npm:6.2.0" + react-awesome-query-builder: "npm:5.1.2" react-select: "npm:5.8.0" react-sticky-box: "npm:2.0.4" recharts: "npm:3.0.2"