import { useActionQuery } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconAlertTriangle, IconArrowLeft } from "@tabler/icons-react"; import { useQueries, useQuery } from "@tanstack/react-query"; import { useMemo } from "react"; import { Link, useLocation, useParams } from "react-router"; import { aggregateSharedEmails, callAppAction, fetchAppEmailCatalog, type AppEmailCatalog, type LocalTransactionalEmailCatalog, } from "../../client/transactional-emails"; import { ActionQueryError } from "../../components/action-query-error"; import { DispatchShell } from "../../components/dispatch-shell"; import { ActivityTable, type EmailActivityEntry, } from "../../components/transactional-email-activity"; import { OpenRateCell, SendsCell, LastSentCell, type EmailEngagement, type ProviderMetricsResult, } from "../../components/transactional-email-metrics"; import { EmailPreviewPane } from "../../components/transactional-email-preview"; import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert"; import { Button } from "../../components/ui/button"; import { Skeleton } from "../../components/ui/skeleton"; export function meta() { return [{ title: "Transactional email detail — Dispatch" }]; } const WINDOW_DAYS = 30; const ACTIVITY_LIMIT = 50; interface WorkspaceAppRef { id: string; name: string; path: string; status?: "ready" | "pending"; } /** * Resolves the one email this page shows, plus the app name/path to display. * The "core" appId is Dispatch's own registry, not a cross-app fetch — see * the same split in transactional-email.tsx. */ function useEmailDetail(appId: string, id: string) { const isCore = appId === "core"; const t = useT(); const localQuery = useActionQuery( "list-transactional-emails", { windowDays: WINDOW_DAYS }, { enabled: isCore }, ); const appsQuery = useActionQuery("list-workspace-apps", { includeAgentCards: false, }); const readyApps = useMemo( () => (appsQuery.data ?? []).filter( (candidate) => candidate.status !== "pending", ), [appsQuery.data], ); const app = useMemo( () => readyApps.find((candidate) => candidate.id === appId), [readyApps, appId], ); const coreCatalogQueries = useQueries({ queries: readyApps.map((candidate) => ({ queryKey: ["transactional-email-catalog", candidate.id, WINDOW_DAYS], queryFn: () => fetchAppEmailCatalog(candidate, WINDOW_DAYS), enabled: isCore, staleTime: Infinity, })), }); const remoteQuery = useQuery({ queryKey: ["transactional-email-catalog", appId, WINDOW_DAYS], queryFn: () => fetchAppEmailCatalog(app!, WINDOW_DAYS), enabled: !isCore && !!app, staleTime: Infinity, }); if (isCore) { const appCatalogs = coreCatalogQueries .map((query) => query.data) .filter((catalog): catalog is AppEmailCatalog => Boolean(catalog)); const aggregate = localQuery.data ? aggregateSharedEmails(localQuery.data, appCatalogs) : undefined; const email = aggregate?.emails.find((candidate) => candidate.id === id); const catalogError = coreCatalogQueries.find( (query) => query.data?.error || query.data?.statsError, )?.data; const queryError = coreCatalogQueries.find((query) => query.isError)?.error; const isLoading = localQuery.isLoading || appsQuery.isLoading || coreCatalogQueries.some((query) => query.isLoading); const isError = localQuery.isError || appsQuery.isError || Boolean(queryError); return { isLoading, isError, error: localQuery.error ?? appsQuery.error ?? queryError, onRetry: () => { void localQuery.refetch(); void appsQuery.refetch(); for (const query of coreCatalogQueries) void query.refetch(); }, appName: t("dispatch.transactionalEmail.sharedTitle"), appPath: t("dispatch.transactionalEmail.sharedSubtitle"), email, catalogError: aggregate?.statsError ?? catalogError?.error ?? catalogError?.statsError ?? null, notFound: !isLoading && !isError && !email, }; } const notFound = !appsQuery.isLoading && !appsQuery.isError && (!app || (!remoteQuery.isLoading && !remoteQuery.isError && !remoteQuery.data?.error && !remoteQuery.data?.emails.some((candidate) => candidate.id === id))); return { isLoading: appsQuery.isLoading || remoteQuery.isLoading, isError: appsQuery.isError || remoteQuery.isError, error: appsQuery.error ?? remoteQuery.error, onRetry: () => { void appsQuery.refetch(); void remoteQuery.refetch(); }, appName: app?.name ?? appId, appPath: app?.path ?? "", email: remoteQuery.data?.emails.find((candidate) => candidate.id === id), catalogError: remoteQuery.data?.error ?? null, notFound, }; } export default function TransactionalEmailDetailRoute() { const t = useT(); const location = useLocation(); const params = useParams<{ appId: string; id: string }>(); const appId = params.appId ?? ""; const id = params.id ?? ""; const detail = useEmailDetail(appId, id); const sharedProviderReason = t( "dispatch.transactionalEmail.sharedProviderMetricsUnavailable", ); const engagementQuery = useQuery({ queryKey: ["list-email-engagement", detail.appPath, id, WINDOW_DAYS], queryFn: () => callAppAction>( detail.appPath, "list-email-engagement", { templateIds: [id], windowDays: WINDOW_DAYS }, "POST", ), enabled: appId !== "core" && id.length > 0 && detail.appPath.length > 0, }); const engagementResult = engagementQuery.data; const engagementUnavailable = appId === "core" ? sharedProviderReason : engagementResult && !engagementResult.available ? engagementResult.reason : engagementQuery.isError ? engagementQuery.error instanceof Error ? engagementQuery.error.message : String(engagementQuery.error) : null; const engagement = engagementResult?.available ? engagementResult.data.find((entry) => entry.templateId === id) : undefined; const activityQuery = useQuery({ queryKey: ["list-email-activity", detail.appPath, id, ACTIVITY_LIMIT], queryFn: () => callAppAction>( detail.appPath, "list-email-activity", { templateId: id, limit: ACTIVITY_LIMIT }, "GET", ), enabled: appId !== "core" && id.length > 0 && detail.appPath.length > 0 && !engagementUnavailable, }); return (
{detail.isError ? ( ) : detail.isLoading ? (
) : detail.notFound || !detail.email ? ( {t("dispatch.transactionalEmail.emailNotFound")} {detail.catalogError ?? t("dispatch.transactionalEmail.emailNotFoundDescription")} ) : ( <>
{detail.email.name}
{detail.email.id}
{detail.appName} {detail.appPath ? ` · ${detail.appPath}` : ""}
{t("dispatch.transactionalEmail.trigger")}
{detail.email.trigger}
{t("dispatch.transactionalEmail.recipient")}
{detail.email.recipient}
{t("dispatch.transactionalEmail.sender")}
{detail.email.sender}
{t("dispatch.transactionalEmail.sends")}
{t("dispatch.transactionalEmail.openRate")}
{t("dispatch.transactionalEmail.lastSent")}

{t("dispatch.transactionalEmail.preview")}

{t("dispatch.transactionalEmail.previewDescription")}

{t("dispatch.transactionalEmail.activityLink")}

{t("dispatch.transactionalEmail.retentionNote")}

void activityQuery.refetch()} />
)}
); }