@@ -18,6 +18,7 @@ import { Badge } from "@/components/ui/badge";
1818import { Button } from "@/components/ui/button" ;
1919import {
2020 getContactBySlug ,
21+ getContactMergeImpact ,
2122 listContactTimeline ,
2223 listPotentialDuplicateContacts ,
2324} from "@/lib/contacts" ;
@@ -37,6 +38,9 @@ type AvailableSequence = Awaited<
3738type DuplicateCandidate = Awaited <
3839 ReturnType < typeof listPotentialDuplicateContacts >
3940> [ number ] ;
41+ type MergeImpact = NonNullable <
42+ Awaited < ReturnType < typeof getContactMergeImpact > >
43+ > ;
4044type Enrollment = Awaited <
4145 ReturnType < typeof listContactSequenceEnrollments >
4246> [ number ] ;
@@ -55,6 +59,10 @@ function confidenceVariant(confidence: DuplicateCandidate["confidence"]) {
5559 return "outline" ;
5660}
5761
62+ function impactPillLabel ( label : string , value : number ) {
63+ return `${ value } ${ label } ` ;
64+ }
65+
5866function formatDueAt ( value : Date | null ) {
5967 if ( ! value ) {
6068 return "No due date" ;
@@ -511,7 +519,10 @@ function ContactEnrollmentsPanel(props: {
511519
512520function DuplicateCandidatesPanel ( props : {
513521 contact : ContactWorkspaceContact ;
514- duplicateCandidates : DuplicateCandidate [ ] ;
522+ duplicateCandidates : Array < {
523+ candidate : DuplicateCandidate ;
524+ mergeImpact : MergeImpact | null ;
525+ } > ;
515526} ) {
516527 return (
517528 < DashboardPanel >
@@ -526,7 +537,7 @@ function DuplicateCandidatesPanel(props: {
526537 No duplicate candidates detected for this contact right now.
527538 </ p >
528539 ) : (
529- props . duplicateCandidates . map ( ( candidate ) => (
540+ props . duplicateCandidates . map ( ( { candidate, mergeImpact } ) => (
530541 < div
531542 key = { candidate . id }
532543 className = "rounded-2xl border border-border/85 bg-background/75 px-4 py-4"
@@ -556,6 +567,25 @@ function DuplicateCandidatesPanel(props: {
556567 ) ) }
557568 </ div >
558569 </ div >
570+ { mergeImpact ? (
571+ < div className = "mt-3 flex flex-wrap gap-2" >
572+ < Badge variant = "secondary" >
573+ { impactPillLabel ( "identities" , mergeImpact . source . impact . identities ) }
574+ </ Badge >
575+ < Badge variant = "secondary" >
576+ { impactPillLabel ( "sources" , mergeImpact . source . impact . sources ) }
577+ </ Badge >
578+ < Badge variant = "secondary" >
579+ { impactPillLabel ( "tasks" , mergeImpact . source . impact . tasks ) }
580+ </ Badge >
581+ < Badge variant = "secondary" >
582+ { impactPillLabel (
583+ "enrollments" ,
584+ mergeImpact . source . impact . enrollments ,
585+ ) }
586+ </ Badge >
587+ </ div >
588+ ) : null }
559589 < div className = "mt-3 flex flex-wrap gap-3" >
560590 < form action = { mergeContactsAction } >
561591 < input
@@ -573,6 +603,10 @@ function DuplicateCandidatesPanel(props: {
573603 name = "targetContactId"
574604 value = { props . contact . id }
575605 />
606+ < label className = "mb-3 flex items-center gap-2 text-sm text-muted-foreground" >
607+ < input type = "checkbox" name = "confirmMerge" value = "yes" />
608+ Confirm merge and move linked history into this contact
609+ </ label >
576610 < Button type = "submit" variant = "outline" >
577611 Merge into this contact
578612 </ Button >
@@ -873,6 +907,15 @@ function ContactSplitPanel(props: { contact: ContactWorkspaceContact }) {
873907 </ div >
874908 </ div >
875909
910+ < p className = "text-sm text-muted-foreground" >
911+ Safety rule: the source contact must keep at least one identity or source
912+ anchor after the split.
913+ </ p >
914+
915+ < label className = "flex items-center gap-2 text-sm text-muted-foreground" >
916+ < input type = "checkbox" name = "confirmSplit" value = "yes" />
917+ Confirm split and move the selected identities and sources
918+ </ label >
876919 < Button type = "submit" variant = "outline" >
877920 Create split contact
878921 </ Button >
@@ -939,6 +982,15 @@ export async function ContactWorkspaceView(props: {
939982 listReplySignalsForContact ( contact . id ) ,
940983 listContactTimeline ( contact . id ) ,
941984 ] ) ;
985+ const duplicateCandidatesWithImpact = await Promise . all (
986+ duplicateCandidates . map ( async ( candidate ) => ( {
987+ candidate,
988+ mergeImpact : await getContactMergeImpact ( {
989+ sourceContactId : candidate . id ,
990+ targetContactId : contact . id ,
991+ } ) ,
992+ } ) ) ,
993+ ) ;
942994
943995 return (
944996 < div className = "space-y-6" >
@@ -961,10 +1013,10 @@ export async function ContactWorkspaceView(props: {
9611013 contactSlug = { contact . slug }
9621014 enrollments = { enrollments }
9631015 />
964- < DuplicateCandidatesPanel
965- contact = { contact }
966- duplicateCandidates = { duplicateCandidates }
967- />
1016+ < DuplicateCandidatesPanel
1017+ contact = { contact }
1018+ duplicateCandidates = { duplicateCandidatesWithImpact }
1019+ />
9681020 < ReplyHistoryPanel replyHistory = { replyHistory } />
9691021 < MergeReviewsPanel contact = { contact } />
9701022 < FollowUpTasksPanel contact = { contact } />
0 commit comments