import { ReactElement } from 'react'; import { PaginationState } from '@tanstack/react-table'; import { EmailEngagementPoint } from './email-engagement-chart.mjs'; import { EmailStatus } from './email-marketing-primitives.mjs'; import { EmailDeliverySegment } from './email-stat-widgets.mjs'; import 'react/jsx-runtime'; /** * Campaign detail — a right-side Sheet that adapts to campaign status * (BUILD-2285). Modelled on GoHighLevel's campaign Summary page. * * - sent / sending / paused → **Statistics**: meta + KPI tiles + engagement * chart + delivery health, and a Recipients tab (recipient-level table). * Header actions vary by status (Pause / Resume / Duplicate, Export CSV). * - scheduled → **Schedule info**: nothing has sent yet, so no stats — shows * the schedule, an email preview, and Edit / Cancel / Send-now actions. * * Rates are numeric percentages; pass `formatRate` to change the default * one-decimal "42.1%" rendering. */ type CampaignRecipientStatus = "queued" | "delivered" | "opened" | "clicked" | "bounced" | "complained" | "unsubscribed" | "failed"; interface CampaignRecipientRow { id: string; name: string; email: string; status: CampaignRecipientStatus; timestamp: string; } interface CampaignDetail { id: string; name: string; status: EmailStatus; templateName?: string; scheduleSummary?: string; /** Undefined while the count is still being fetched. */ recipientCount?: number; /** Engagement + delivery rates as percentages (e.g. 42.1). */ openRate?: number; clickRate?: number; /** SES-tracked delivery / bounce rates. */ deliveredRate?: number; bounceRate?: number; complaintRate?: number; unsubscribeRate?: number; /** The template's body as it stands now; rendered sandboxed in the preview. */ htmlContent?: string; /** Why the campaign errored; present only for `status: "error"`. */ errorReason?: string; /** Recurring series — gates whether a "sending" campaign can be paused. */ isRecurring?: boolean; /** * Whether the campaign has ever completed a run. A recurring campaign sits at `scheduled` * between runs, so status alone cannot tell "waiting for its first send" from "waiting for * its next one" — and the second has real statistics to show. */ hasSent?: boolean; } /** * Server-side recipient totals. Supplied by a backend that reduces the roster itself, so the * chips and the "all" total describe the whole campaign rather than whatever page is loaded. */ interface CampaignRecipientCounts { /** Rows matching EVERY active filter, the status chip included — this is what paging counts. */ total: number; /** Per-status totals for the whole roster, narrowed by search but never by the chip. */ byStatus: Partial>; } /** * Server-driven recipient table. Supply it and the roster is filtered, searched and paged by the * backend: the component renders exactly the rows it is given and reports intent through the * callbacks. Omit it and the table filters and pages the rows it already holds, which is only * correct when those rows are the entire roster. */ interface CampaignRecipientQuery { status: CampaignRecipientStatus | "all"; onStatusChange: (status: CampaignRecipientStatus | "all") => void; search: string; onSearchChange: (search: string) => void; pagination: PaginationState; onPaginationChange: (pagination: PaginationState) => void; /** Rows matching the active filters, across every page. */ rowCount: number; isLoading?: boolean; /** * The campaign has not sent yet, so these rows are who it WOULD reach, resolved live rather * than read from a roster. Status filtering is meaningless (everyone is queued) so the chips * are hidden; search still applies. */ isAudiencePreview?: boolean; } interface CampaignDetailSheetProps { /** The campaign to show; `null` closes the sheet. */ campaign: CampaignDetail | null; onOpenChange: (open: boolean) => void; recipients: CampaignRecipientRow[]; /** Omit for a single-run campaign; the label then shows one number. */ /** Omit to count the loaded rows instead — correct only when they are the whole roster. */ recipientCounts?: CampaignRecipientCounts; /** Omit for the client-side table; supply for server-driven filtering/search/paging. */ recipientQuery?: CampaignRecipientQuery; engagement: EmailEngagementPoint[]; deliveryBreakdown: EmailDeliverySegment[]; /** Edit a scheduled campaign (opens the composer). */ onEdit?: (id: string) => void; /** Pause a sending recurring campaign / resume a paused one. */ onTogglePause?: (id: string) => void; onExport?: (id: string) => void; /** Cancel a scheduled campaign before it sends. */ onCancelSchedule?: (id: string) => void; /** Dispatch a scheduled campaign immediately. */ onSendNow?: (id: string) => void; /** Renders every rate value; defaults to one-decimal percent. */ formatRate?: (value: number) => string; } declare function CampaignDetailSheet({ campaign, onOpenChange, recipients, recipientCounts, recipientQuery, engagement, deliveryBreakdown, onEdit, onTogglePause, onExport, onCancelSchedule, onSendNow, formatRate, }: CampaignDetailSheetProps): ReactElement; export { type CampaignDetail, CampaignDetailSheet, type CampaignDetailSheetProps, type CampaignRecipientCounts, type CampaignRecipientQuery, type CampaignRecipientRow, type CampaignRecipientStatus, CampaignDetailSheet as default };