mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +00:00
feat: add Assign, WhatsApp Send, Mark Spam, Merge modals and Lead Activity slideout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
191
src/components/modals/merge-modal.tsx
Normal file
191
src/components/modals/merge-modal.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { formatPhone, formatShortDate } from '@/lib/format';
|
||||
import type { Lead } from '@/types/entities';
|
||||
import { cx } from '@/utils/cx';
|
||||
|
||||
type MergeModalProps = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
primaryLead: Lead;
|
||||
duplicateLead: Lead;
|
||||
onMerge: () => void;
|
||||
onKeepSeparate: () => void;
|
||||
};
|
||||
|
||||
type FieldRow = {
|
||||
label: string;
|
||||
primary: string;
|
||||
duplicate: string;
|
||||
};
|
||||
|
||||
const getLeadName = (lead: Lead) => {
|
||||
if (lead.contactName) {
|
||||
return `${lead.contactName.firstName} ${lead.contactName.lastName}`.trim();
|
||||
}
|
||||
return '—';
|
||||
};
|
||||
|
||||
const getLeadPhone = (lead: Lead) => {
|
||||
if (lead.contactPhone && lead.contactPhone.length > 0) {
|
||||
return formatPhone(lead.contactPhone[0]);
|
||||
}
|
||||
return '—';
|
||||
};
|
||||
|
||||
const getLeadEmail = (lead: Lead) => lead.contactEmail?.[0]?.address ?? '—';
|
||||
|
||||
const buildFieldRows = (primary: Lead, duplicate: Lead): FieldRow[] => [
|
||||
{
|
||||
label: 'Name',
|
||||
primary: getLeadName(primary),
|
||||
duplicate: getLeadName(duplicate),
|
||||
},
|
||||
{
|
||||
label: 'Phone',
|
||||
primary: getLeadPhone(primary),
|
||||
duplicate: getLeadPhone(duplicate),
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
primary: getLeadEmail(primary),
|
||||
duplicate: getLeadEmail(duplicate),
|
||||
},
|
||||
{
|
||||
label: 'Source',
|
||||
primary: primary.leadSource ?? '—',
|
||||
duplicate: duplicate.leadSource ?? '—',
|
||||
},
|
||||
{
|
||||
label: 'Campaign',
|
||||
primary: primary.campaignId ?? '—',
|
||||
duplicate: duplicate.campaignId ?? '—',
|
||||
},
|
||||
{
|
||||
label: 'Created',
|
||||
primary: primary.createdAt ? formatShortDate(primary.createdAt) : '—',
|
||||
duplicate: duplicate.createdAt ? formatShortDate(duplicate.createdAt) : '—',
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
primary: primary.leadStatus ?? '—',
|
||||
duplicate: duplicate.leadStatus ?? '—',
|
||||
},
|
||||
];
|
||||
|
||||
const LeadCard = ({
|
||||
label,
|
||||
isPrimary,
|
||||
fieldRows,
|
||||
}: {
|
||||
label: string;
|
||||
isPrimary: boolean;
|
||||
fieldRows: FieldRow[];
|
||||
}) => (
|
||||
<div
|
||||
className={cx(
|
||||
'flex-1 rounded-xl border-2 p-4 flex flex-col gap-3',
|
||||
isPrimary ? 'border-brand' : 'border-secondary',
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cx(
|
||||
'text-xs font-bold uppercase tracking-wide',
|
||||
isPrimary ? 'text-brand-secondary' : 'text-quaternary',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{fieldRows.map((row) => {
|
||||
const value = isPrimary ? row.primary : row.duplicate;
|
||||
const conflicts = row.primary !== row.duplicate;
|
||||
return (
|
||||
<div key={row.label} className="flex flex-col gap-0.5">
|
||||
<span className="text-xs text-quaternary">{row.label}</span>
|
||||
<span
|
||||
className={cx(
|
||||
'text-xs break-words',
|
||||
!isPrimary && conflicts ? 'font-semibold text-warning-primary' : 'text-primary',
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const MergeModal = ({ isOpen, onOpenChange, primaryLead, duplicateLead, onMerge, onKeepSeparate }: MergeModalProps) => {
|
||||
const fieldRows = buildFieldRows(primaryLead, duplicateLead);
|
||||
const primaryName = getLeadName(primaryLead);
|
||||
|
||||
const handleMerge = () => {
|
||||
onMerge();
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleKeepSeparate = () => {
|
||||
onKeepSeparate();
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalOverlay isOpen={isOpen} onOpenChange={onOpenChange} isDismissable>
|
||||
<Modal className="sm:max-w-2xl">
|
||||
<Dialog>
|
||||
{() => (
|
||||
<div className="flex w-full flex-col rounded-xl bg-primary shadow-xl ring-1 ring-secondary overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-6 pb-4 border-b border-secondary">
|
||||
<h2 className="text-lg font-semibold text-primary">Merge Duplicate Leads</h2>
|
||||
<p className="mt-1 text-sm text-tertiary">
|
||||
Compare and merge two leads with the same phone number.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex flex-col gap-4 px-6 py-4 overflow-y-auto max-h-[70vh]">
|
||||
{/* Side-by-side comparison */}
|
||||
<div className="flex items-start gap-2">
|
||||
<LeadCard
|
||||
label="Keep (Primary)"
|
||||
isPrimary={true}
|
||||
fieldRows={fieldRows}
|
||||
/>
|
||||
|
||||
<div className="flex shrink-0 items-center self-center px-1 pt-6">
|
||||
<span className="text-xl text-quaternary">→</span>
|
||||
</div>
|
||||
|
||||
<LeadCard
|
||||
label="Merge Into Primary"
|
||||
isPrimary={false}
|
||||
fieldRows={fieldRows}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Note */}
|
||||
<p className="text-xs text-quaternary">
|
||||
Fields shown in amber differ between records. The primary lead's values will be preserved after merging.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 border-t border-secondary px-6 py-4">
|
||||
<Button size="md" color="secondary" onClick={handleKeepSeparate}>
|
||||
Keep Separate
|
||||
</Button>
|
||||
<Button size="md" color="primary" onClick={handleMerge}>
|
||||
Merge → Keep {primaryName}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user