import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconExternalLink, IconShieldCheck } from "@tabler/icons-react"; import { useState } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { LoadingRows, PageHeader, SetupEmptyState, } from "@/components/crm/Surface"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; interface ProposalField { name: string; before?: unknown; after?: unknown; beforeKnown?: boolean; } interface ProposalPreview { id: string; recordId?: string; recordName?: string; provider?: string; operation: string; status: string; risk?: string; createdAt?: string; appliedAt?: string; recordUrl?: string; recordUrlUnavailableReason?: string; fields?: ProposalField[]; } interface PreparedHandoff { proposalId: string; providerLabel: string; recordUrl: string | null; recordUrlUnavailableReason: string | null; fields: Array<{ name: string; beforeKnown: boolean; before: unknown; after: unknown; }>; } export default function ProposalsRoute() { const t = useT(); const query = useActionQuery( "list-crm-proposals" as never, { limit: 100 } as never, ); const prepare = useActionMutation( "apply-crm-proposals" as never, ); const proposals = normalizeProposals(query.data); const [preparingIds, setPreparingIds] = useState>(new Set()); async function prepareHandoff(proposal: ProposalPreview) { setPreparingIds((current) => new Set(current).add(proposal.id)); try { const handoff = await prepare.mutateAsync({ proposalId: proposal.id }); await query.refetch(); // Never phrased as a completed upstream write, and never as a failure. toast.info( t("proposals.preparedToast", { provider: handoff.providerLabel }), ); } catch (error) { toast.error( error instanceof Error ? error.message : t("proposals.prepareFailedToast"), ); } finally { setPreparingIds((current) => { const next = new Set(current); next.delete(proposal.id); return next; }); } } return ( <> {query.isLoading ? ( ) : proposals.length ? (
{proposals.map((proposal) => (

{proposal.recordName || t("proposals.untitledRecord")}

{t(`proposals.status.${statusKey(proposal)}`)} {proposal.risk ? ( {proposal.risk} ) : null}

{proposal.operation} {proposal.createdAt ? ` · ${formatDate(proposal.createdAt)}` : ""}

{proposal.recordId ? ( ) : null} {proposal.status === "pending" ? ( void prepareHandoff(proposal)} /> ) : proposal.recordUrl ? ( ) : null}
{proposal.status === "approved" && !proposal.appliedAt ? (

{t("proposals.handedOffNote", { provider: providerLabel(proposal.provider), })}

) : null} {proposal.recordUrl || proposal.status === "pending" || !proposal.recordUrlUnavailableReason ? null : (

{t("proposals.recordLinkUnavailable", { provider: providerLabel(proposal.provider), reason: proposal.recordUrlUnavailableReason, })}

)}
))}
) : ( )} ); } function PrepareButton({ proposal, pending, onPrepare, }: { proposal: ProposalPreview; pending: boolean; onPrepare: () => void; }) { const t = useT(); const provider = providerLabel(proposal.provider); return ( {t("proposals.prepareTitle", { provider })} {t("proposals.prepareDescription", { provider, record: proposal.recordName || t("proposals.untitledRecord"), })} {proposal.recordUrl || !proposal.recordUrlUnavailableReason ? null : (

{t("proposals.recordLinkUnavailable", { provider, reason: proposal.recordUrlUnavailableReason, })}

)} {t("proposals.cancel")} {/* The upstream tab has to open from the click itself — opening it after the prepare round-trip trips popup blockers. */} {proposal.recordUrl ? ( {t("proposals.prepareAndOpen", { provider })} ) : ( {t("proposals.prepareWithoutLink")} )}
); } function ProposalFields({ fields }: { fields?: ProposalField[] }) { const t = useT(); if (!fields?.length) { return (

{t("proposals.fieldPreviewUnavailable")}

); } return (
{fields.slice(0, 20).map((field) => (
{field.name}
{field.beforeKnown === false ? t("proposals.notMirrored") : displayValue(field.before, t)}
{displayValue(field.after, t)}
))}
); } /** * `approved` without `appliedAt` is a prepared handoff, not an upstream write. * Labelling it "Approved" would read as done. */ function statusKey(proposal: ProposalPreview) { if (proposal.status === "approved" && !proposal.appliedAt) return "prepared"; return proposal.status; } function providerLabel(provider?: string) { return provider === "salesforce" ? "Salesforce" : "HubSpot"; } function normalizeProposals(data: unknown): ProposalPreview[] { const rows = isObject(data) && Array.isArray(data.proposals) ? data.proposals : []; return rows.flatMap((row) => { if (!isObject(row) || typeof row.id !== "string") return []; const fields = Array.isArray(row.fields) ? row.fields.flatMap((field) => isObject(field) && typeof field.name === "string" ? [ { name: field.name, before: field.before, after: field.after, ...(typeof field.beforeKnown === "boolean" ? { beforeKnown: field.beforeKnown } : {}), }, ] : [], ) : undefined; return [ { id: row.id, recordId: typeof row.recordId === "string" ? row.recordId : undefined, recordName: typeof row.recordName === "string" ? row.recordName : undefined, provider: typeof row.provider === "string" ? row.provider : undefined, operation: typeof row.operation === "string" ? row.operation : "update", status: typeof row.status === "string" ? row.status : "pending", risk: typeof row.risk === "string" ? row.risk : undefined, createdAt: typeof row.createdAt === "string" ? row.createdAt : undefined, appliedAt: typeof row.appliedAt === "string" ? row.appliedAt : undefined, recordUrl: typeof row.recordUrl === "string" ? row.recordUrl : undefined, recordUrlUnavailableReason: typeof row.recordUrlUnavailableReason === "string" ? row.recordUrlUnavailableReason : undefined, fields, }, ]; }); } function isObject(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function displayValue( value: unknown, t: (key: string, options?: Record) => string, ) { if (value === null || value === undefined || value === "") return t("proposals.emptyValue"); if (typeof value === "string" || typeof value === "number") return String(value); if (typeof value === "boolean") return value ? t("proposals.booleanTrue") : t("proposals.booleanFalse"); return t("proposals.structuredValue"); } function formatDate(value: string) { const date = new Date(value); return Number.isNaN(date.valueOf()) ? value : date.toLocaleString(); }