* 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>
86 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|