Files
calendar/apps/web/components/booking/RoutingTraceSheet.tsx
T
Joe Au-YeungGitHubClaude Opus 4.5joe@cal.com <j.auyeung419@gmail.com>joe@cal.com <j.auyeung419@gmail.com>Devin 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

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

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Abstract functions

* Fix build error

* Add permission check

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

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

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

86 lines
3.3 KiB
TypeScript

"use client";
import dayjs from "@calcom/dayjs";
import { DomainIcon } from "@calcom/features/routing-trace/components/DomainIcon";
import { getDomainLabel } from "@calcom/features/routing-trace/presenters/getDomainLabel";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetBody } from "@calcom/ui/components/sheet";
interface RoutingTraceSheetProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
bookingUid: string;
}
export function RoutingTraceSheet({ isOpen, setIsOpen, bookingUid }: RoutingTraceSheetProps) {
const { t } = useLocale();
const { data, isLoading } = trpc.viewer.bookings.getRoutingTrace.useQuery(
{ bookingUid },
{
enabled: isOpen,
staleTime: 10 * 60 * 1000,
}
);
return (
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetContent>
<SheetHeader>
<SheetTitle>{t("routing_trace")}</SheetTitle>
</SheetHeader>
<SheetBody>
{isLoading && (
<div className="flex flex-col gap-4 py-2">
{[1, 2, 3].map((i) => (
<div key={i} className="flex gap-3">
<div className="bg-muted h-8 w-8 animate-pulse rounded-full" />
<div className="flex flex-1 flex-col gap-1.5">
<div className="bg-muted h-3 w-24 animate-pulse rounded" />
<div className="bg-muted h-4 w-full animate-pulse rounded" />
</div>
</div>
))}
</div>
)}
{!isLoading && !data?.steps?.length && (
<p className="text-subtle text-sm">{t("no_results_found")}</p>
)}
{!isLoading && data?.steps && data.steps.length > 0 && (
<div className="relative flex flex-col">
{data.steps.map((step, idx) => {
const isLast = idx === data.steps.length - 1;
return (
<div key={idx} className="relative flex gap-3 pb-6 last:pb-0">
{/* Timeline connector line */}
{!isLast && (
<div className="border-subtle absolute left-4 top-8 bottom-0 border-l" />
)}
{/* Icon circle */}
<div className="bg-default border-subtle relative z-10 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full border">
<DomainIcon domain={step.domain} />
</div>
{/* Content */}
<div className="flex min-w-0 flex-1 flex-col gap-0.5 pt-0.5">
<div className="flex items-center gap-2">
<span className="bg-subtle text-subtle rounded px-1.5 py-0.5 text-xs font-medium">
{getDomainLabel(step.domain)}
</span>
<span className="text-muted text-xs">
{dayjs(step.timestamp).format("h:mm:ss.SSS A")}
</span>
</div>
<p className="text-emphasis text-sm">{step.message}</p>
</div>
</div>
);
})}
</div>
)}
</SheetBody>
</SheetContent>
</Sheet>
);
}