import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { Megaphone, Plus, AlertTriangle } from 'lucide-react'; import { useCampaignServiceContext, type Campaign } from '../../../services/CampaignService'; import { describeCampaign, isCancellable, NOT_CONFIGURED_COPY, campaignSetupDestination, CAMPAIGN_TONE_PILL as TONE_CLASS } from './campaignStatus'; import { useStaffPermissionServiceContext } from '../../../services'; import { useSaveScope } from '../../../utils/saveOperation'; import { onViewerChange, viewerEpoch } from '../../../utils/writeThroughHold'; import { holdSettingsWrite, settingsWriteHolder, unresolvedSettingsWrite, useUnresolvedSettingsWrite, newSettingsOperationId } from '../../../utils/unresolvedSettingsWrite'; import { readRefusal } from './campaignRefusal'; import CampaignConfirmDialog, { cancelConfirmation } from './CampaignConfirmDialog'; import { HttpError } from '../../../services/HttpService'; import { environment } from '../../../environments/environment'; import { Badge } from '../../../components/ui/badge'; import { Card } from '../../../components/ui/card'; import { Skeleton } from '../../../components/ui/skeleton'; // // S4d Phase 2 - the first screen that makes the campaign platform exist for a practice. // // The server half (entity, audience resolution, batched fan-out, quiet hours, per-send claims, the // scheduled sweep, per-org sending identity) has been deployed for days with NO way to reach it. // // THE ONE THING THIS SCREEN MUST NOT DO IS LOOK CALM WHEN A CAMPAIGN IS STUCK. Proven live on // staging: a campaign with no verified sending domain sits in `sending` and the sweep refuses it // every 15 minutes, logging where only an engineer looks. `describeCampaign` puts that in front of // the practice, and this page renders the FIX beside it. // export default function CampaignsPage() { const navigate = useNavigate(); const { listCampaigns, cancelCampaign, getCampaign } = useCampaignServiceContext(); const [campaigns, setCampaigns] = useState(null); const [error, setError] = useState(null); const [busyId, setBusyId] = useState(null); const [cancelling, setCancelling] = useState<{ campaign: Campaign; epoch: number } | null>(null); const { permissionsSettled, canManage } = useStaffPermissionServiceContext(); const mayManage = permissionsSettled && canManage('campaigns'); const authority = useRef(mayManage); authority.current = mayManage; const epoch = useSyncExternalStore(onViewerChange, viewerEpoch); const saves = useSaveScope(`${epoch}|campaign-list|${mayManage}`); const [loadedOwner, setLoadedOwner] = useState(epoch); const readGeneration = useRef(0); const flight = useRef(null); useUnresolvedSettingsWrite(settingsWriteHolder('campaign-cancellation')); const cancellationHolder = (id: string) => settingsWriteHolder(`campaign-cancellation:${id}`); const heldCancel = (campaignId: string) => { const held = unresolvedSettingsWrite(cancellationHolder(campaignId)); try { const value = JSON.parse(held?.detail ?? 'null') as { campaignId: string; recordVersion: string } | null; return value && value.campaignId === campaignId && typeof value.recordVersion === 'string' ? value : undefined; } catch { return undefined; } }; const load = useCallback(async () => { if (viewerEpoch() !== epoch) return; const read = saves.start(); const mine = ++readGeneration.current; setError(null); try { const result = await read.step(() => listCampaigns(read.transport)); read.admit(); if (mine !== readGeneration.current) return; setCampaigns(result.campaigns ?? []); setLoadedOwner(epoch); } catch { if (read.belongs() && mine === readGeneration.current) { setCampaigns(null); setLoadedOwner(epoch); setError('We could not load your campaigns. Please try again.'); } } finally { read.retire(); } }, [listCampaigns, saves, epoch]); useEffect(() => { setCampaigns(null); setError(null); setCancelling(null); setBusyId(null); flight.current = null; void load(); }, [load, mayManage]); const onCancel = async (campaign: Pick, retry = false) => { const holder = cancellationHolder(campaign.campaignId); const held = unresolvedSettingsWrite(holder); if (viewerEpoch() !== epoch || environment.inSandboxMode || !authority.current || flight.current || !campaign.recordVersion || (!retry && held)) return; const owner = saves.start(); const mine = Symbol(); flight.current = mine; const attemptId = retry ? held?.sent?.id : newSettingsOperationId(); if (!attemptId) { owner.retire(); flight.current = null; return; } setBusyId(campaign.campaignId); setCancelling(null); setError(null); let sent = false; const settle = () => { if (unresolvedSettingsWrite(holder)?.sent?.id === attemptId) holdSettingsWrite(holder, undefined); }; const admit = () => { owner.admit(); if (!authority.current) throw new Error('Campaign management is no longer permitted.'); }; const transport = { ...owner.transport, admit, onSend: () => { admit(); sent = true; holdSettingsWrite(holder, { kind: 'cancel', state: 'unconfirmed', sent: { id: attemptId, attempt: 0 }, detail: JSON.stringify({ campaignId: campaign.campaignId, recordVersion: campaign.recordVersion }) }); } }; try { const request = cancelCampaign(campaign.campaignId, transport, campaign.recordVersion).catch(error => { const status = error instanceof HttpError ? error.status : undefined; if (!retry && status && status >= 400 && status < 500) settle(); throw error; }); const updated = await owner.step(() => request, true); admit(); if (updated.campaignId !== campaign.campaignId || updated.status !== 'cancelled') throw new Error('Unconfirmed cancellation.'); settle(); setCampaigns(rows => rows?.map(row => row.campaignId === updated.campaignId ? updated : row) ?? null); } catch (error) { if (!owner.belongs()) return; if (error instanceof HttpError && [409, 412, 428].includes(error.status)) { await load(); if (!owner.belongs()) return; setError(retry ? 'The earlier cancellation remains unconfirmed. Check current campaign status.' : error.status === 409 ? readRefusal(error).message : 'This campaign changed or its edition was unavailable. Review the current campaign before cancelling again.'); } else setError(sent || retry ? 'Cancellation could not be confirmed. Check current status before another cancellation.' : 'Cancellation was not sent. Review the current campaign and try again.'); } finally { if (owner.belongs()) setBusyId(null); if (flight.current === mine) flight.current = null; owner.retire(); } }; const checkCancellation = async (campaignId: string) => { if (viewerEpoch() !== epoch) return; const holder = cancellationHolder(campaignId); const original = heldCancel(campaignId); if (!original || flight.current) return; const owner = saves.start(); const mine = Symbol(); flight.current = mine; setBusyId(original.campaignId); try { const current = await owner.step(() => getCampaign(original.campaignId, owner.transport)); owner.admit(); if (current.campaignId !== original.campaignId) throw new Error('Wrong campaign readback.'); setCampaigns(rows => rows?.map(row => row.campaignId === current.campaignId ? current : row) ?? null); const editionChanged = typeof current.recordVersion === 'string' && current.recordVersion.length > 0 && current.recordVersion !== original.recordVersion; if (['cancelled', 'sent', 'failed'].includes(current.status) || editionChanged) { holdSettingsWrite(holder, undefined); setError(editionChanged && isCancellable(current) ? 'The campaign has a different edition. Review its current state before cancelling again.' : null); } else setError('The campaign is still active. The original cancellation has not been confirmed.'); } catch { if (owner.belongs()) setError('Current campaign status could not be read. The cancellation remains held.'); } finally { if (owner.belongs()) setBusyId(null); if (flight.current === mine) flight.current = null; owner.retire(); } }; const visibleCampaigns = loadedOwner === epoch ? campaigns : null; return (

Campaigns

Marketing messages to patients who have opted in.

Marketing email setup
{visibleCampaigns?.filter(row => unresolvedSettingsWrite(cancellationHolder(row.campaignId))).map(row =>
An earlier cancellation is unconfirmed for {row.name}. Messages already in flight may still arrive. {mayManage && heldCancel(row.campaignId) && }
)} {error && (
{error} {visibleCampaigns === null && ( <> {' '} )}
)} {visibleCampaigns === null && !error && (
)} {visibleCampaigns !== null && visibleCampaigns.length === 0 && (

No campaigns yet

Campaigns go only to patients who have opted in to marketing messages.

)} {visibleCampaigns !== null && visibleCampaigns.length > 0 && (
{visibleCampaigns.map((campaign) => { const state = describeCampaign(campaign); const blocked = campaign.notConfigured ? NOT_CONFIGURED_COPY[campaign.notConfigured] : null; return (

{campaign.name}

{state.label}

{state.detail}

{campaign.subject}

{mayManage && isCancellable(campaign) && ( )}
{/* 🚨 THE FIX, NEXT TO THE FAULT. A practice told only "cannot send" can do nothing with that — and this is the state they hit FIRST, before any campaign has ever gone out. */} {blocked && (
{blocked.title} {blocked.fix} {campaignSetupDestination(campaign.notConfigured) && Open marketing email setup}
)}
); })}
)} { if (cancelling?.epoch === epoch) void onCancel(cancelling.campaign); }} onKeep={() => setCancelling(null)} />
); }