diff --git a/app/expert-finder/lib/outreachChannels.ts b/app/expert-finder/lib/outreachChannels.ts index 2d7c5dd22..ca971e34e 100644 --- a/app/expert-finder/lib/outreachChannels.ts +++ b/app/expert-finder/lib/outreachChannels.ts @@ -8,7 +8,6 @@ export const OUTREACH_CHANNEL_LABELS: Record = { email: 'Email', linkedin: 'LinkedIn', x: 'X', - other: 'Other', }; export const OUTREACH_CHANNEL_OPTIONS: { value: OutreachChannel; label: string }[] = @@ -17,11 +16,8 @@ export const OUTREACH_CHANNEL_OPTIONS: { value: OutreachChannel; label: string } label: OUTREACH_CHANNEL_LABELS[value], })); -export function getOutreachChannelLabel( - channel: OutreachChannel | '' | null | undefined -): string | null { - if (!channel) return null; - return OUTREACH_CHANNEL_LABELS[channel] ?? null; +export function getOutreachChannelLabels(channels: OutreachChannel[] | null | undefined): string[] { + return (channels ?? []).map((channel) => OUTREACH_CHANNEL_LABELS[channel]); } /** Open Gmail compose (browser) with to/subject; body is copied separately for rich paste. */ diff --git a/app/expert-finder/library/[searchId]/outreach/[emailId]/OutreachDetailPageContent.tsx b/app/expert-finder/library/[searchId]/outreach/[emailId]/OutreachDetailPageContent.tsx index bc1c5c7bc..9b2532939 100644 --- a/app/expert-finder/library/[searchId]/outreach/[emailId]/OutreachDetailPageContent.tsx +++ b/app/expert-finder/library/[searchId]/outreach/[emailId]/OutreachDetailPageContent.tsx @@ -25,8 +25,7 @@ import { Input } from '@/components/ui/form/Input'; import { Badge } from '@/components/ui/Badge'; import { BaseMenu, BaseMenuItem } from '@/components/ui/form/BaseMenu'; import { ConfirmationModal } from '@/components/ui/form/ConfirmationModal'; -import { RadioGroup } from '@/components/ui/form/RadioGroup'; -import { Textarea } from '@/components/ui/form/Textarea'; +import { Checkbox } from '@/components/ui/form/Checkbox'; import { getGeneratedEmailStatusPresentation, isGeneratedEmailBounced, @@ -44,7 +43,8 @@ import { import { toast } from 'react-hot-toast'; import { TAB_OUTREACH } from '@/app/expert-finder/lib/searchDetailTabs'; import { - getOutreachChannelLabel, + getOutreachChannelLabels, + OUTREACH_CHANNEL_LABELS, OUTREACH_CHANNEL_OPTIONS, } from '@/app/expert-finder/lib/outreachChannels'; import type { OutreachChannel } from '@/types/expertFinder'; @@ -55,6 +55,14 @@ function buildOutreachDetailHref(librarySearchId: string, neighborEmailId: numbe return `/expert-finder/library/${librarySearchId}/outreach/${neighborEmailId}`; } +function withChannel( + existing: OutreachChannel[] | undefined, + channel: OutreachChannel +): OutreachChannel[] { + const channels = existing ?? []; + return channels.includes(channel) ? [...channels] : [...channels, channel]; +} + export interface OutreachDetailPageContentProps { emailId: string; librarySearchId: string; @@ -72,9 +80,9 @@ export function OutreachDetailPageContent({ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showCloseConfirm, setShowCloseConfirm] = useState(false); const [showMarkSentConfirm, setShowMarkSentConfirm] = useState(false); - const [closeNotes, setCloseNotes] = useState(''); - const [markSentNotes, setMarkSentNotes] = useState(''); - const [markSentChannel, setMarkSentChannel] = useState(''); + const [markSentChannels, setMarkSentChannels] = useState([]); + const [showChannelConfirm, setShowChannelConfirm] = useState(false); + const [confirmChannel, setConfirmChannel] = useState(null); const [actionError, setActionError] = useState(null); const [editSubject, setEditSubject] = useState(''); const [editBody, setEditBody] = useState(''); @@ -123,6 +131,29 @@ export function OutreachDetailPageContent({ isDraftLike && (editSubject !== (email.emailSubject ?? '') || editBody !== (email.emailBody ?? '')); + const openMarkSentConfirm = () => { + setMarkSentChannels([...(email?.channels ?? [])]); + setShowMarkSentConfirm(true); + }; + + const openChannelConfirm = (channel: OutreachChannel) => { + setConfirmChannel(channel); + setShowChannelConfirm(true); + }; + + const closeChannelConfirm = () => { + setShowChannelConfirm(false); + }; + + const toggleMarkSentChannel = (channel: OutreachChannel, checked: boolean) => { + setMarkSentChannels((prev) => { + if (checked) { + return prev.includes(channel) ? prev : [...prev, channel]; + } + return prev.filter((c) => c !== channel); + }); + }; + const handleSaveDraft = async () => { if (!emailId || !hasEdits) return; setActionError(null); @@ -138,34 +169,53 @@ export function OutreachDetailPageContent({ } }; - const handleMarkSentSubmit = async () => { + const saveChannels = async (channels: OutreachChannel[], successMessage: string) => { if (!emailId) return; - if (!markSentChannel) { - setActionError('Select how you sent this outreach.'); - return; - } setActionError(null); try { - const notes = markSentNotes.trim(); await updateEmail(emailId, { status: 'sent', - channel: markSentChannel, - ...(notes ? { notes } : {}), + channels, }); - setShowMarkSentConfirm(false); - setMarkSentNotes(''); - setMarkSentChannel(''); refetch(); - toast.success('Marked as sent.'); + toast.success(successMessage); } catch (e) { setActionError(e instanceof Error ? e.message : 'Failed to update'); + throw e; + } + }; + + const handleMarkSentSubmit = async () => { + if (markSentChannels.length === 0) { + setActionError('Select at least one channel.'); + return; + } + try { + await saveChannels( + markSentChannels, + email?.status === 'sent' ? 'Channels updated.' : 'Marked as sent.' + ); + setShowMarkSentConfirm(false); + setMarkSentChannels([]); + } catch { + // Error already surfaced via actionError + } + }; + + const handleChannelConfirmSubmit = async () => { + if (!confirmChannel) return; + const label = OUTREACH_CHANNEL_LABELS[confirmChannel]; + try { + await saveChannels(withChannel(email?.channels, confirmChannel), `Marked ${label} as sent.`); + closeChannelConfirm(); + } catch { + // Error already surfaced via actionError } }; const closeMarkSentConfirm = () => { setShowMarkSentConfirm(false); - setMarkSentChannel(''); - setMarkSentNotes(''); + setMarkSentChannels([]); }; const handleDelete = async () => { @@ -184,13 +234,10 @@ export function OutreachDetailPageContent({ if (!emailId) return; setActionError(null); try { - const notes = closeNotes.trim(); await updateEmail(emailId, { status: 'closed', - ...(notes ? { notes } : {}), }); setShowCloseConfirm(false); - setCloseNotes(''); refetch(); toast.success('Marked as closed.'); } catch (e) { @@ -222,8 +269,9 @@ export function OutreachDetailPageContent({ const isSent = email.status === 'sent'; const statusPresentation = getGeneratedEmailStatusPresentation(email.status, email.openCount); const pipelineBusy = isGeneratedEmailPipelineBusy(email.status); - const showOutreachMoreMenu = !isClosed && !isSent; - const channelLabel = getOutreachChannelLabel(email.channel); + const showOutreachMoreMenu = !isClosed; + const channelLabels = isSent ? getOutreachChannelLabels(email.channels) : []; + const showChannelActions = !isClosed && (isDraftLike || isSent); const displaySubject = isDraftLike ? editSubject : (email.emailSubject ?? ''); const displayTitle = displaySubject || `Outreach for ${email.expertName}`; @@ -323,7 +371,11 @@ export function OutreachDetailPageContent({
{neighborNavBar}
{statusPresentation.label} - {isSent && channelLabel ? via {channelLabel} : null} + {channelLabels.map((label) => ( + + via {label} + + ))} {isGeneratedEmailBounced(email.status) && email.bouncedAt && ( Bounced on {formatExactTime(email.bouncedAt)} @@ -352,28 +404,28 @@ export function OutreachDetailPageContent({ } > - {isDraftLike && ( + {(isDraftLike || isSent) && ( { - setMarkSentNotes(''); - setShowMarkSentConfirm(true); + openMarkSentConfirm(); }} > - Mark as sent + {isSent ? 'Update channels' : 'Mark as sent'} + + )} + {!isSent && ( + { + setShowCloseConfirm(true); + }} + > + + Mark as closed )} - { - setCloseNotes(''); - setShowCloseConfirm(true); - }} - > - - Mark as closed - {isDraftLike && ( setShowDeleteConfirm(true)}> @@ -436,13 +488,6 @@ export function OutreachDetailPageContent({ )}
- {(isClosed || email.status === 'sent') && email.notes?.trim() ? ( -
-

Notes

-

{email.notes}

-
- ) : null} -
@@ -490,13 +535,14 @@ export function OutreachDetailPageContent({
- {isDraftLike && ( + {showChannelActions && (
)} @@ -514,57 +560,69 @@ export function OutreachDetailPageContent({ onConfirm={handleDelete} /> + } + isConfirming={isUpdating} + onConfirm={() => void handleChannelConfirmSubmit()} + /> + } isConfirming={isUpdating} - confirmDisabled={!markSentChannel} + confirmDisabled={markSentChannels.length === 0} onConfirm={() => void handleMarkSentSubmit()} > - setMarkSentChannel(value as OutreachChannel)} - options={OUTREACH_CHANNEL_OPTIONS} - /> -