Files
Joe Au-YeungGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
7abbc8c22c feat: Routing trace presenter (#27372)
* feat: Add routing trace presenters

Add domain-specific presenters (SalesforceRoutingTracePresenter,
RoutingFormTracePresenter) that format trace steps into human-readable
strings, and a core RoutingTracePresenter that delegates to them based
on step domain. Includes unit tests for all presenters.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat: Add findByBookingUid to RoutingTraceRepository

Add method to look up a routing trace by booking UID, needed by the
routing trace presenter tRPC endpoint.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat: Add getRoutingTrace tRPC endpoint

Expose routing trace data for a booking via viewer.bookings.getRoutingTrace.
Tries the permanent RoutingTrace first (round robin bookings), then falls
back to PendingRoutingTrace via the booking's form response relation.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat: Add routing trace side sheet to booking list item

Add a route icon button on booking list items that came from routing
forms. Clicking it opens a side sheet displaying the full routing trace
as human-readable steps. Adds RoutingTraceSheet component, store state,
and translation key.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: Move routing trace action to dropdown menu

Move the routing trace button from a standalone icon on the booking list
item into the actions dropdown menu alongside "Report wrong assignment".
The RoutingTraceSheet is now rendered from BookingActionsDropdown.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: Improve routing trace UI and handle unnamed routes

- Redesign RoutingTraceSheet with vertical timeline layout, domain badges,
  skeleton loading state, and millisecond timestamps
- Use Salesforce app-store icon for Salesforce steps
- Fall back to "Unnamed route" when route name is missing or matches route ID
- Fix dropdown menu icon to git-merge (valid icon, unique in menu)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* test: Add tests for getRoutingTrace handler and findByBookingUid repository method

Co-Authored-By: [email protected] <[email protected]>

* Abstract functions

* Fix build error

* Add permission check

* fix: Update getRoutingTrace tests to include ctx and mock BookingAccessService

Co-Authored-By: [email protected] <[email protected]>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-29 15:26:44 -03:00

53 lines
1.9 KiB
TypeScript

import { BookingAccessService } from "@calcom/features/bookings/services/BookingAccessService";
import { RoutingTracePresenter } from "@calcom/features/routing-trace/presenters/RoutingTracePresenter";
import { PrismaRoutingTraceRepository } from "@calcom/features/routing-trace/repositories/PrismaRoutingTraceRepository";
import type { RoutingTrace } from "@calcom/features/routing-trace/repositories/RoutingTraceRepository.interface";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import type { TGetRoutingTraceInputSchema } from "./getRoutingTrace.schema";
type Options = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TGetRoutingTraceInputSchema;
};
export const getRoutingTraceHandler = async ({ ctx, input }: Options) => {
const bookingAccessService = new BookingAccessService(prisma);
const hasAccess = await bookingAccessService.doesUserIdHaveAccessToBooking({
userId: ctx.user.id,
bookingUid: input.bookingUid,
});
if (!hasAccess) {
throw new TRPCError({ code: "FORBIDDEN", message: "You don't have access to this booking" });
}
const repository = new PrismaRoutingTraceRepository(prisma);
// Try permanent RoutingTrace first (exists for round robin bookings)
const traceRecord = await repository.findByBookingUid(input.bookingUid);
if (traceRecord) {
return { steps: RoutingTracePresenter.present(traceRecord.trace) };
}
// Fall back to PendingRoutingTrace via the booking's form response
const formResponse = await prisma.app_RoutingForms_FormResponse.findFirst({
where: { routedToBookingUid: input.bookingUid },
include: { pendingRoutingTrace: true },
});
if (formResponse?.pendingRoutingTrace?.trace) {
const trace = formResponse.pendingRoutingTrace.trace as unknown as RoutingTrace;
return { steps: RoutingTracePresenter.present(trace) };
}
return { steps: [] };
};