mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +00:00
151 lines
6.9 KiB
TypeScript
151 lines
6.9 KiB
TypeScript
import { SlideoutMenu } from '@/components/application/slideout-menus/slideout-menu';
|
|
import { formatPhone, formatShortDate } from '@/lib/format';
|
|
import type { Lead, LeadActivity, LeadActivityType } from '@/types/entities';
|
|
import { cx } from '@/utils/cx';
|
|
|
|
type LeadActivitySlideoutProps = {
|
|
isOpen: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
lead: Lead;
|
|
activities: LeadActivity[];
|
|
};
|
|
|
|
type ActivityConfig = {
|
|
icon: string;
|
|
dotClass: string;
|
|
label: string;
|
|
};
|
|
|
|
const ACTIVITY_CONFIG: Record<LeadActivityType, ActivityConfig> = {
|
|
STATUS_CHANGE: { icon: '🔄', dotClass: 'bg-brand-secondary', label: 'Status Changed' },
|
|
CALL_MADE: { icon: '📞', dotClass: 'bg-brand-secondary', label: 'Call Made' },
|
|
CALL_RECEIVED: { icon: '📲', dotClass: 'bg-brand-secondary', label: 'Call Received' },
|
|
WHATSAPP_SENT: { icon: '💬', dotClass: 'bg-success-solid', label: 'WhatsApp Sent' },
|
|
WHATSAPP_RECEIVED: { icon: '💬', dotClass: 'bg-success-solid', label: 'WhatsApp Received' },
|
|
SMS_SENT: { icon: '✉️', dotClass: 'bg-brand-secondary', label: 'SMS Sent' },
|
|
EMAIL_SENT: { icon: '📧', dotClass: 'bg-brand-secondary', label: 'Email Sent' },
|
|
EMAIL_RECEIVED: { icon: '📧', dotClass: 'bg-brand-secondary', label: 'Email Received' },
|
|
NOTE_ADDED: { icon: '📝', dotClass: 'bg-warning-solid', label: 'Note Added' },
|
|
ASSIGNED: { icon: '📤', dotClass: 'bg-brand-secondary', label: 'Assigned' },
|
|
APPOINTMENT_BOOKED: { icon: '📅', dotClass: 'bg-brand-secondary', label: 'Appointment Booked' },
|
|
FOLLOW_UP_CREATED: { icon: '🔁', dotClass: 'bg-brand-secondary', label: 'Follow-up Created' },
|
|
CONVERTED: { icon: '✅', dotClass: 'bg-success-solid', label: 'Converted' },
|
|
MARKED_SPAM: { icon: '🚫', dotClass: 'bg-error-solid', label: 'Marked as Spam' },
|
|
DUPLICATE_DETECTED: { icon: '🔍', dotClass: 'bg-warning-solid', label: 'Duplicate Detected' },
|
|
};
|
|
|
|
const DEFAULT_CONFIG: ActivityConfig = { icon: '📌', dotClass: 'bg-tertiary', label: 'Activity' };
|
|
|
|
const StatusChangeContent = ({ previousValue, newValue }: { previousValue: string | null; newValue: string | null }) => (
|
|
<span className="text-sm text-secondary">
|
|
{previousValue && (
|
|
<span className="mr-1 text-sm line-through text-quaternary">{previousValue}</span>
|
|
)}
|
|
{previousValue && newValue && '→ '}
|
|
{newValue && <span className="font-medium text-brand-secondary">{newValue}</span>}
|
|
</span>
|
|
);
|
|
|
|
const ActivityItem = ({ activity, isLast }: { activity: LeadActivity; isLast: boolean }) => {
|
|
const type = activity.activityType;
|
|
const config = type ? (ACTIVITY_CONFIG[type] ?? DEFAULT_CONFIG) : DEFAULT_CONFIG;
|
|
const occurredAt = activity.occurredAt ? formatShortDate(activity.occurredAt) : '';
|
|
|
|
return (
|
|
<div className="relative flex gap-3 pb-4">
|
|
{/* Vertical connecting line */}
|
|
{!isLast && (
|
|
<div className="absolute left-[15px] top-[36px] bottom-0 w-0.5 bg-tertiary" />
|
|
)}
|
|
|
|
{/* Dot */}
|
|
<div
|
|
className={cx(
|
|
'relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full text-sm',
|
|
config.dotClass,
|
|
)}
|
|
aria-hidden="true"
|
|
>
|
|
{config.icon}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex flex-1 flex-col gap-0.5 pt-1 min-w-0">
|
|
<div className="flex items-start gap-2">
|
|
<span className="flex-1 text-sm font-semibold text-primary">
|
|
{activity.summary ?? config.label}
|
|
</span>
|
|
</div>
|
|
|
|
{type === 'STATUS_CHANGE' && (activity.previousValue || activity.newValue) && (
|
|
<StatusChangeContent previousValue={activity.previousValue} newValue={activity.newValue} />
|
|
)}
|
|
|
|
{type !== 'STATUS_CHANGE' && activity.newValue && (
|
|
<p className="text-xs text-tertiary">{activity.newValue}</p>
|
|
)}
|
|
|
|
<p className="text-xs text-quaternary">
|
|
{occurredAt}
|
|
{activity.performedBy ? ` · by ${activity.performedBy}` : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const LeadActivitySlideout = ({ isOpen, onOpenChange, lead, activities }: LeadActivitySlideoutProps) => {
|
|
const firstName = lead.contactName?.firstName ?? '';
|
|
const lastName = lead.contactName?.lastName ?? '';
|
|
const fullName = `${firstName} ${lastName}`.trim() || 'Unknown Lead';
|
|
const phone = lead.contactPhone?.[0] ? formatPhone(lead.contactPhone[0]) : null;
|
|
const email = lead.contactEmail?.[0]?.address ?? null;
|
|
|
|
const filteredActivities = activities
|
|
.filter((a) => a.leadId === lead.id)
|
|
.sort((a, b) => {
|
|
if (!a.occurredAt) return 1;
|
|
if (!b.occurredAt) return -1;
|
|
return new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime();
|
|
});
|
|
|
|
return (
|
|
<SlideoutMenu isOpen={isOpen} onOpenChange={onOpenChange} isDismissable>
|
|
{({ close }) => (
|
|
<>
|
|
<SlideoutMenu.Header onClose={close}>
|
|
<div className="flex flex-col gap-0.5 pr-8">
|
|
<h2 className="text-lg font-semibold text-primary">Lead Activity — {fullName}</h2>
|
|
<p className="text-sm text-tertiary">
|
|
{[phone, email, lead.leadSource, lead.utmCampaign]
|
|
.filter(Boolean)
|
|
.join(' · ')}
|
|
</p>
|
|
</div>
|
|
</SlideoutMenu.Header>
|
|
|
|
<SlideoutMenu.Content>
|
|
{filteredActivities.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center gap-2 py-12 text-center">
|
|
<span className="text-3xl">📭</span>
|
|
<p className="text-sm font-medium text-secondary">No activity yet</p>
|
|
<p className="text-xs text-tertiary">Activity will appear here as interactions occur.</p>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col">
|
|
{filteredActivities.map((activity, idx) => (
|
|
<ActivityItem
|
|
key={activity.id}
|
|
activity={activity}
|
|
isLast={idx === filteredActivities.length - 1}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</SlideoutMenu.Content>
|
|
</>
|
|
)}
|
|
</SlideoutMenu>
|
|
);
|
|
};
|