Files
calendar/packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts
T
+5
Hariom BalharaGitHubhariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Udit Takkar
2bc17313fd fix: deep link reschedule audit log to booking drawer history tab (#27709)
* fix: deep link reschedule audit log to booking drawer history tab

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: make booking drawer tab-agnostic for cross-tab deep links

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use status-agnostic /bookings URL for audit log deep links

- Update audit service URLs from /bookings/upcoming?uid=... to /bookings?uid=...
- Add /bookings/page.tsx redirect that routes to /bookings/upcoming preserving query params
- Update tests to expect new URL format

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fixes

* refactor: use client-side replaceState instead of server redirect for booking deep links

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use router.replace instead of replaceState to update tab and booking list

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* improvements

* refactor: extract deep link logic from BookingListContainer into usePreSelectedBooking hook

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use proper BookingOutput status type in test helper

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: decouple getTabForBooking from BookingOutput type for simpler testing

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: accept Date | string for endTime in BookingForTabResolution interface

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: eliminate initialBookingUid prop drilling and revert formatting-only changes

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use AuditDeepLink wrapper to preserve target=_blank through ServerTrans cloneElement

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Improve code organization

* refactor: rename usePreSelectedBooking to useSwitchToCorrectStatusTab

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: push preSelectedBooking into store so drawer opens on direct navigation

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fixes

* fixes

* fixes

* fix

---------

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>
2026-02-17 13:20:06 +05:30

181 lines
5.9 KiB
TypeScript

import { z } from "zod";
import { NumberChangeSchema, StringChangeSchema } from "../common/changeSchemas";
import type { DataRequirements } from "../service/EnrichmentDataStore";
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
import type {
BaseStoredAuditData,
GetDisplayJsonParams,
GetDisplayTitleParams,
IAuditActionService,
TranslationWithParams,
} from "./IAuditActionService";
/**
* Rescheduled Audit Action Service
* Handles RESCHEDULED action with per-action versioning
*/
// Module-level because it is passed to IAuditActionService type outside the class scope
const fieldsSchemaV1 = z.object({
startTime: NumberChangeSchema,
endTime: NumberChangeSchema,
rescheduledToUid: StringChangeSchema,
});
export class RescheduledAuditActionService implements IAuditActionService {
readonly VERSION = 1;
public static readonly TYPE = "RESCHEDULED" as const;
private static dataSchemaV1 = z.object({
version: z.literal(1),
fields: fieldsSchemaV1,
});
private static fieldsSchemaV1 = fieldsSchemaV1;
public static readonly latestFieldsSchema = fieldsSchemaV1;
// Union of all versions
public static readonly storedDataSchema = RescheduledAuditActionService.dataSchemaV1;
// Union of all versions
public static readonly storedFieldsSchema = RescheduledAuditActionService.fieldsSchemaV1;
private helper: AuditActionServiceHelper<
typeof RescheduledAuditActionService.latestFieldsSchema,
typeof RescheduledAuditActionService.storedDataSchema
>;
constructor() {
this.helper = new AuditActionServiceHelper({
latestVersion: this.VERSION,
latestFieldsSchema: RescheduledAuditActionService.latestFieldsSchema,
storedDataSchema: RescheduledAuditActionService.storedDataSchema,
});
}
getVersionedData(fields: unknown) {
return this.helper.getVersionedData(fields);
}
parseStored(data: unknown) {
return this.helper.parseStored(data);
}
getVersion(data: unknown): number {
return this.helper.getVersion(data);
}
migrateToLatest(data: unknown) {
// V1-only: validate and return as-is (no migration needed)
const validated = fieldsSchemaV1.parse(data);
return { isMigrated: false, latestData: validated };
}
getDataRequirements(): DataRequirements {
return { userUuids: [] };
}
async getDisplayTitle({ storedData, userTimeZone }: GetDisplayTitleParams): Promise<TranslationWithParams> {
const { fields } = this.parseStored(storedData);
const rescheduledToUid = fields.rescheduledToUid.new;
const timeZone = userTimeZone;
// Format dates in user timezone
const oldDate = fields.startTime.old
? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.old, timeZone)
: "";
const newDate = fields.startTime.new
? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.new, timeZone)
: "";
return {
key: "booking_audit_action.rescheduled",
params: {
oldDate,
newDate,
},
components: rescheduledToUid
? [{ type: "link", href: `/bookings?uid=${rescheduledToUid}&activeSegment=history` }]
: undefined,
};
}
getDisplayTitleForRescheduledFromLog({
fromRescheduleUid,
userTimeZone,
storedData,
}: {
fromRescheduleUid: string;
userTimeZone: string;
storedData: BaseStoredAuditData;
}): TranslationWithParams {
const timeZone = userTimeZone;
const { fields } = this.parseStored(storedData);
// Format dates in user timezone
const oldDate = fields.startTime.old
? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.old, timeZone)
: "";
const newDate = fields.startTime.new
? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.new, timeZone)
: "";
return {
key: "booking_audit_action.rescheduled_from",
params: {
oldDate,
newDate,
},
components: [{ type: "link", href: `/bookings?uid=${fromRescheduleUid}&activeSegment=history` }],
};
}
getDisplayJson({ storedData, userTimeZone }: GetDisplayJsonParams): RescheduledAuditDisplayData {
const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields });
const timeZone = userTimeZone;
return {
previousStartTime: fields.startTime.old
? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.startTime.old, timeZone)
: null,
newStartTime: fields.startTime.new
? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.startTime.new, timeZone)
: null,
previousEndTime: fields.endTime.old
? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.endTime.old, timeZone)
: null,
newEndTime: fields.endTime.new
? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.endTime.new, timeZone)
: null,
rescheduledToUid: fields.rescheduledToUid.new ?? null,
};
}
/**
* Finds the rescheduled log that created a specific booking
* by matching the rescheduledToUid field with the target booking UID
* @param rescheduledLogs - Array of rescheduled audit logs to search through
* @param rescheduledToBookingUid - The UID of the booking that was created from the reschedule
* @returns The matching log or null if not found
*/
getMatchingLog<T extends { data: unknown }>({
rescheduledLogs,
rescheduledToBookingUid,
}: {
rescheduledLogs: T[];
rescheduledToBookingUid: string;
}): T | null {
return (
rescheduledLogs.find((log) => {
const parsedData = this.parseStored(log.data);
return parsedData.fields.rescheduledToUid.new === rescheduledToBookingUid;
}) ?? null
);
}
}
export type RescheduledAuditData = z.infer<typeof fieldsSchemaV1>;
export type RescheduledAuditDisplayData = {
previousStartTime: string | null;
newStartTime: string | null;
previousEndTime: string | null;
newEndTime: string | null;
rescheduledToUid: string | null;
};