mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +00:00
157 lines
6.0 KiB
TypeScript
157 lines
6.0 KiB
TypeScript
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 flex-1 flex-col gap-3 rounded-xl border-2 p-4", isPrimary ? "border-brand" : "border-secondary")}>
|
|
<p className={cx("text-xs font-bold tracking-wide uppercase", 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 overflow-hidden rounded-xl bg-primary shadow-xl ring-1 ring-secondary">
|
|
{/* Header */}
|
|
<div className="border-b border-secondary px-6 pt-6 pb-4">
|
|
<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 max-h-[70vh] flex-col gap-4 overflow-y-auto px-6 py-4">
|
|
{/* 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>
|
|
);
|
|
};
|