From 47a24231eccce0146950014c4900141c2590b401 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Fri, 31 Jul 2026 23:22:49 +0300 Subject: [PATCH 1/2] [Expert Finder] Multi-channel outreach completion --- app/expert-finder/lib/outreachChannels.ts | 8 +- .../[emailId]/OutreachDetailPageContent.tsx | 152 +++++++++--------- .../components/OutreachChannelActions.tsx | 14 +- .../components/OutreachMobileCard.tsx | 10 +- .../outreach/components/OutreachTable.tsx | 10 +- services/expertFinder.service.ts | 2 +- types/expertFinder.ts | 15 +- 7 files changed, 110 insertions(+), 101 deletions(-) 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..37cf45aab 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,7 @@ import { import { toast } from 'react-hot-toast'; import { TAB_OUTREACH } from '@/app/expert-finder/lib/searchDetailTabs'; import { - getOutreachChannelLabel, + getOutreachChannelLabels, OUTREACH_CHANNEL_OPTIONS, } from '@/app/expert-finder/lib/outreachChannels'; import type { OutreachChannel } from '@/types/expertFinder'; @@ -72,9 +71,7 @@ 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 [actionError, setActionError] = useState(null); const [editSubject, setEditSubject] = useState(''); const [editBody, setEditBody] = useState(''); @@ -123,6 +120,23 @@ export function OutreachDetailPageContent({ isDraftLike && (editSubject !== (email.emailSubject ?? '') || editBody !== (email.emailBody ?? '')); + const openMarkSentConfirm = (preselected?: OutreachChannel) => { + const existing = email?.channels ?? []; + setMarkSentChannels( + preselected && !existing.includes(preselected) ? [...existing, preselected] : [...existing] + ); + setShowMarkSentConfirm(true); + }; + + 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); @@ -140,23 +154,20 @@ export function OutreachDetailPageContent({ const handleMarkSentSubmit = async () => { if (!emailId) return; - if (!markSentChannel) { - setActionError('Select how you sent this outreach.'); + if (markSentChannels.length === 0) { + setActionError('Select at least one channel.'); return; } setActionError(null); try { - const notes = markSentNotes.trim(); await updateEmail(emailId, { status: 'sent', - channel: markSentChannel, - ...(notes ? { notes } : {}), + channels: markSentChannels, }); setShowMarkSentConfirm(false); - setMarkSentNotes(''); - setMarkSentChannel(''); + setMarkSentChannels([]); refetch(); - toast.success('Marked as sent.'); + toast.success(email?.status === 'sent' ? 'Channels updated.' : 'Marked as sent.'); } catch (e) { setActionError(e instanceof Error ? e.message : 'Failed to update'); } @@ -164,8 +175,7 @@ export function OutreachDetailPageContent({ const closeMarkSentConfirm = () => { setShowMarkSentConfirm(false); - setMarkSentChannel(''); - setMarkSentNotes(''); + setMarkSentChannels([]); }; const handleDelete = async () => { @@ -184,13 +194,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 +229,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 +331,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 +364,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 +448,6 @@ export function OutreachDetailPageContent({ )}
- {(isClosed || email.status === 'sent') && email.notes?.trim() ? ( -
-

Notes

-

{email.notes}

-
- ) : null} -
@@ -490,13 +495,14 @@ export function OutreachDetailPageContent({
- {isDraftLike && ( + {showChannelActions && (
)} @@ -517,54 +523,46 @@ export function OutreachDetailPageContent({ } isConfirming={isUpdating} - confirmDisabled={!markSentChannel} + confirmDisabled={markSentChannels.length === 0} onConfirm={() => void handleMarkSentSubmit()} > - setMarkSentChannel(value as OutreachChannel)} - options={OUTREACH_CHANNEL_OPTIONS} - /> -