mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +00:00
Linting and Formatting
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { cx } from '@/utils/cx';
|
||||
import { daysAgoFromNow } from '@/lib/format';
|
||||
import type { Lead } from '@/types/entities';
|
||||
import { daysAgoFromNow } from "@/lib/format";
|
||||
import type { Lead } from "@/types/entities";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface AgingWidgetProps {
|
||||
leads: Lead[];
|
||||
@@ -26,21 +26,21 @@ export const AgingWidget = ({ leads }: AgingWidgetProps) => {
|
||||
|
||||
const brackets: AgingBracket[] = [
|
||||
{
|
||||
label: 'Fresh (<2 days)',
|
||||
color: 'text-success-primary',
|
||||
barColor: 'bg-success-solid',
|
||||
label: "Fresh (<2 days)",
|
||||
color: "text-success-primary",
|
||||
barColor: "bg-success-solid",
|
||||
count: freshCount,
|
||||
},
|
||||
{
|
||||
label: 'Warm (2-5 days)',
|
||||
color: 'text-warning-primary',
|
||||
barColor: 'bg-warning-solid',
|
||||
label: "Warm (2-5 days)",
|
||||
color: "text-warning-primary",
|
||||
barColor: "bg-warning-solid",
|
||||
count: warmCount,
|
||||
},
|
||||
{
|
||||
label: 'Cold (>5 days)',
|
||||
color: 'text-error-primary',
|
||||
barColor: 'bg-error-solid',
|
||||
label: "Cold (>5 days)",
|
||||
color: "text-error-primary",
|
||||
barColor: "bg-error-solid",
|
||||
count: coldCount,
|
||||
},
|
||||
];
|
||||
@@ -52,14 +52,12 @@ export const AgingWidget = ({ leads }: AgingWidgetProps) => {
|
||||
{brackets.map((bracket) => (
|
||||
<div key={bracket.label}>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className={cx('text-xs', bracket.color)}>{bracket.label}</span>
|
||||
<span className={cx('text-sm font-bold', bracket.color)}>
|
||||
{bracket.count}
|
||||
</span>
|
||||
<span className={cx("text-xs", bracket.color)}>{bracket.label}</span>
|
||||
<span className={cx("text-sm font-bold", bracket.color)}>{bracket.count}</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-tertiary">
|
||||
<div
|
||||
className={cx('h-full rounded-full transition-all', bracket.barColor)}
|
||||
className={cx("h-full rounded-full transition-all", bracket.barColor)}
|
||||
style={{ width: `${(bracket.count / total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
import { daysAgoFromNow } from '@/lib/format';
|
||||
import type { Lead } from '@/types/entities';
|
||||
import { daysAgoFromNow } from "@/lib/format";
|
||||
import type { Lead } from "@/types/entities";
|
||||
|
||||
interface AlertsWidgetProps {
|
||||
leads: Lead[];
|
||||
}
|
||||
|
||||
export const AlertsWidget = ({ leads }: AlertsWidgetProps) => {
|
||||
const agingCount = leads.filter(
|
||||
(l) =>
|
||||
l.leadStatus === 'NEW' &&
|
||||
l.createdAt !== null &&
|
||||
daysAgoFromNow(l.createdAt) > 5,
|
||||
).length;
|
||||
const agingCount = leads.filter((l) => l.leadStatus === "NEW" && l.createdAt !== null && daysAgoFromNow(l.createdAt) > 5).length;
|
||||
|
||||
if (agingCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-error-subtle bg-error-primary p-4">
|
||||
<p className="text-xs font-bold text-error-primary">
|
||||
{agingCount} leads aging > 5 days
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-error-primary opacity-80">
|
||||
These leads haven't been contacted and are at risk of going cold.
|
||||
</p>
|
||||
<div className="border-error-subtle rounded-xl border bg-error-primary p-4">
|
||||
<p className="text-xs font-bold text-error-primary">{agingCount} leads aging > 5 days</p>
|
||||
<p className="mt-1 text-xs text-error-primary opacity-80">These leads haven't been contacted and are at risk of going cold.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,39 +9,23 @@ interface BulkActionBarProps {
|
||||
export const BulkActionBar = ({ selectedCount, onAssign, onWhatsApp, onMarkSpam, onDeselect }: BulkActionBarProps) => {
|
||||
if (selectedCount === 0) return null;
|
||||
|
||||
const buttonBase = 'rounded-lg px-3 py-1.5 text-xs font-semibold border-none cursor-pointer transition duration-100 ease-linear';
|
||||
const buttonBase = "rounded-lg px-3 py-1.5 text-xs font-semibold border-none cursor-pointer transition duration-100 ease-linear";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-xl bg-brand-section p-3 text-white">
|
||||
<span className="text-sm font-semibold">{selectedCount} leads selected</span>
|
||||
|
||||
<div className="ml-auto flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAssign}
|
||||
className={`${buttonBase} bg-white/20 text-white hover:bg-white/30`}
|
||||
>
|
||||
<button type="button" onClick={onAssign} className={`${buttonBase} bg-white/20 text-white hover:bg-white/30`}>
|
||||
Assign to Call Center
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onWhatsApp}
|
||||
className={`${buttonBase} bg-success-solid text-white hover:bg-success-solid/90`}
|
||||
>
|
||||
<button type="button" onClick={onWhatsApp} className={`${buttonBase} bg-success-solid text-white hover:bg-success-solid/90`}>
|
||||
Send WhatsApp
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onMarkSpam}
|
||||
className={`${buttonBase} bg-white/15 text-error-primary hover:bg-white/25`}
|
||||
>
|
||||
<button type="button" onClick={onMarkSpam} className={`${buttonBase} bg-white/15 text-error-primary hover:bg-white/25`}>
|
||||
Mark Spam
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDeselect}
|
||||
className={`${buttonBase} bg-white/10 text-white/70 hover:bg-white/20 hover:text-white`}
|
||||
>
|
||||
<button type="button" onClick={onDeselect} className={`${buttonBase} bg-white/10 text-white/70 hover:bg-white/20 hover:text-white`}>
|
||||
Deselect
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faXmark } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { cx } from '@/utils/cx';
|
||||
import { faXmark } from "@fortawesome/pro-duotone-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
type FilterPill = {
|
||||
key: string;
|
||||
@@ -23,7 +23,7 @@ export const FilterPills = ({ filters, onRemove, onClearAll }: FilterPillsProps)
|
||||
<span
|
||||
key={filter.key}
|
||||
className={cx(
|
||||
'flex items-center gap-1 rounded-full border border-brand bg-brand-primary px-3 py-1 text-xs font-medium text-brand-secondary',
|
||||
"flex items-center gap-1 rounded-full border border-brand bg-brand-primary px-3 py-1 text-xs font-medium text-brand-secondary",
|
||||
)}
|
||||
>
|
||||
{filter.label}: {filter.value}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { cx } from '@/utils/cx';
|
||||
import { formatShortDate } from '@/lib/format';
|
||||
import type { FollowUp } from '@/types/entities';
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { formatShortDate } from "@/lib/format";
|
||||
import type { FollowUp } from "@/types/entities";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface FollowupWidgetProps {
|
||||
overdue: FollowUp[];
|
||||
@@ -26,26 +26,15 @@ export const FollowupWidget = ({ overdue, upcoming }: FollowupWidgetProps) => {
|
||||
<div
|
||||
key={item.id}
|
||||
className={cx(
|
||||
'rounded-lg border-l-2 p-3',
|
||||
isOverdue
|
||||
? 'border-l-error-solid bg-error-primary'
|
||||
: 'border-l-brand-solid bg-secondary',
|
||||
"rounded-lg border-l-2 p-3",
|
||||
isOverdue ? "border-l-error-solid bg-error-primary" : "border-l-brand-solid bg-secondary",
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cx(
|
||||
'text-xs font-semibold',
|
||||
isOverdue ? 'text-error-primary' : 'text-brand-secondary',
|
||||
)}
|
||||
>
|
||||
{item.scheduledAt
|
||||
? formatShortDate(item.scheduledAt)
|
||||
: 'No date'}
|
||||
{isOverdue && ' — Overdue'}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs font-medium text-primary">
|
||||
{item.description ?? item.followUpType ?? 'Follow-up'}
|
||||
<p className={cx("text-xs font-semibold", isOverdue ? "text-error-primary" : "text-brand-secondary")}>
|
||||
{item.scheduledAt ? formatShortDate(item.scheduledAt) : "No date"}
|
||||
{isOverdue && " — Overdue"}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs font-medium text-primary">{item.description ?? item.followUpType ?? "Follow-up"}</p>
|
||||
{(item.patientName || item.patientPhone) && (
|
||||
<p className="mt-0.5 text-xs text-tertiary">
|
||||
{item.patientName}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cx } from '@/utils/cx';
|
||||
import type { Lead } from '@/types/entities';
|
||||
import type { Lead } from "@/types/entities";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface KpiCardsProps {
|
||||
leads: Lead[];
|
||||
@@ -14,38 +14,38 @@ type KpiCard = {
|
||||
};
|
||||
|
||||
export const KpiCards = ({ leads }: KpiCardsProps) => {
|
||||
const newCount = leads.filter((l) => l.leadStatus === 'NEW').length;
|
||||
const newCount = leads.filter((l) => l.leadStatus === "NEW").length;
|
||||
const assignedCount = leads.filter((l) => l.assignedAgent !== null).length;
|
||||
const contactedCount = leads.filter((l) => l.leadStatus === 'CONTACTED').length;
|
||||
const convertedCount = leads.filter((l) => l.leadStatus === 'CONVERTED').length;
|
||||
const contactedCount = leads.filter((l) => l.leadStatus === "CONTACTED").length;
|
||||
const convertedCount = leads.filter((l) => l.leadStatus === "CONVERTED").length;
|
||||
|
||||
const cards: KpiCard[] = [
|
||||
{
|
||||
label: 'New Leads Today',
|
||||
label: "New Leads Today",
|
||||
value: newCount,
|
||||
delta: '+12% vs yesterday',
|
||||
deltaColor: 'text-success-primary',
|
||||
delta: "+12% vs yesterday",
|
||||
deltaColor: "text-success-primary",
|
||||
isHero: true,
|
||||
},
|
||||
{
|
||||
label: 'Assigned to CC',
|
||||
label: "Assigned to CC",
|
||||
value: assignedCount,
|
||||
delta: '85% assigned',
|
||||
deltaColor: 'text-brand-secondary',
|
||||
delta: "85% assigned",
|
||||
deltaColor: "text-brand-secondary",
|
||||
isHero: false,
|
||||
},
|
||||
{
|
||||
label: 'Contacted',
|
||||
label: "Contacted",
|
||||
value: contactedCount,
|
||||
delta: '+8% vs yesterday',
|
||||
deltaColor: 'text-success-primary',
|
||||
delta: "+8% vs yesterday",
|
||||
deltaColor: "text-success-primary",
|
||||
isHero: false,
|
||||
},
|
||||
{
|
||||
label: 'Converted',
|
||||
label: "Converted",
|
||||
value: convertedCount,
|
||||
delta: '+3 this week',
|
||||
deltaColor: 'text-warning-primary',
|
||||
delta: "+3 this week",
|
||||
deltaColor: "text-warning-primary",
|
||||
isHero: false,
|
||||
},
|
||||
];
|
||||
@@ -56,36 +56,13 @@ export const KpiCards = ({ leads }: KpiCardsProps) => {
|
||||
<div
|
||||
key={card.label}
|
||||
className={cx(
|
||||
'rounded-2xl p-5 transition hover:shadow-md',
|
||||
card.isHero
|
||||
? 'flex-[1.3] bg-brand-solid text-white'
|
||||
: 'flex-1 border border-secondary bg-primary',
|
||||
"rounded-2xl p-5 transition hover:shadow-md",
|
||||
card.isHero ? "flex-[1.3] bg-brand-solid text-white" : "flex-1 border border-secondary bg-primary",
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cx(
|
||||
'text-xs font-medium uppercase tracking-wider',
|
||||
card.isHero ? 'text-white/70' : 'text-quaternary',
|
||||
)}
|
||||
>
|
||||
{card.label}
|
||||
</p>
|
||||
<p
|
||||
className={cx(
|
||||
'mt-1 text-display-sm font-bold',
|
||||
card.isHero ? 'text-white' : 'text-primary',
|
||||
)}
|
||||
>
|
||||
{card.value}
|
||||
</p>
|
||||
<p
|
||||
className={cx(
|
||||
'mt-1 text-xs',
|
||||
card.isHero ? 'text-white/80' : card.deltaColor,
|
||||
)}
|
||||
>
|
||||
{card.delta}
|
||||
</p>
|
||||
<p className={cx("text-xs font-medium tracking-wider uppercase", card.isHero ? "text-white/70" : "text-quaternary")}>{card.label}</p>
|
||||
<p className={cx("mt-1 text-display-sm font-bold", card.isHero ? "text-white" : "text-primary")}>{card.value}</p>
|
||||
<p className={cx("mt-1 text-xs", card.isHero ? "text-white/80" : card.deltaColor)}>{card.delta}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
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;
|
||||
@@ -17,31 +17,29 @@ type ActivityConfig = {
|
||||
};
|
||||
|
||||
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' },
|
||||
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 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 && '→ '}
|
||||
{previousValue && <span className="mr-1 text-sm text-quaternary line-through">{previousValue}</span>}
|
||||
{previousValue && newValue && "→ "}
|
||||
{newValue && <span className="font-medium text-brand-secondary">{newValue}</span>}
|
||||
</span>
|
||||
);
|
||||
@@ -49,45 +47,33 @@ const StatusChangeContent = ({ previousValue, newValue }: { previousValue: strin
|
||||
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) : '';
|
||||
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" />
|
||||
)}
|
||||
{!isLast && <div className="absolute top-[36px] bottom-0 left-[15px] 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"
|
||||
>
|
||||
<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 min-w-0 flex-1 flex-col gap-0.5 pt-1">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="flex-1 text-sm font-semibold text-primary">
|
||||
{activity.summary ?? config.label}
|
||||
</span>
|
||||
<span className="flex-1 text-sm font-semibold text-primary">{activity.summary ?? config.label}</span>
|
||||
</div>
|
||||
|
||||
{type === 'STATUS_CHANGE' && (activity.previousValue || activity.newValue) && (
|
||||
{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>
|
||||
)}
|
||||
{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}` : ''}
|
||||
{activity.performedBy ? ` · by ${activity.performedBy}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,9 +81,9 @@ const ActivityItem = ({ activity, isLast }: { activity: LeadActivity; isLast: bo
|
||||
};
|
||||
|
||||
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 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;
|
||||
|
||||
@@ -116,11 +102,7 @@ export const LeadActivitySlideout = ({ isOpen, onOpenChange, lead, activities }:
|
||||
<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>
|
||||
<p className="text-sm text-tertiary">{[phone, email, lead.leadSource, lead.utmCampaign].filter(Boolean).join(" · ")}</p>
|
||||
</div>
|
||||
</SlideoutMenu.Header>
|
||||
|
||||
@@ -134,11 +116,7 @@ export const LeadActivitySlideout = ({ isOpen, onOpenChange, lead, activities }:
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
{filteredActivities.map((activity, idx) => (
|
||||
<ActivityItem
|
||||
key={activity.id}
|
||||
activity={activity}
|
||||
isLast={idx === filteredActivities.length - 1}
|
||||
/>
|
||||
<ActivityItem key={activity.id} activity={activity} isLast={idx === filteredActivities.length - 1} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Avatar } from '@/components/base/avatar/avatar';
|
||||
import { Badge } from '@/components/base/badges/badges';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { LeadStatusBadge } from '@/components/shared/status-badge';
|
||||
import { SourceTag } from '@/components/shared/source-tag';
|
||||
import { AgeIndicator } from '@/components/shared/age-indicator';
|
||||
import { formatPhone, getInitials } from '@/lib/format';
|
||||
import { cx } from '@/utils/cx';
|
||||
import type { Lead } from '@/types/entities';
|
||||
import { Avatar } from "@/components/base/avatar/avatar";
|
||||
import { Badge } from "@/components/base/badges/badges";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { AgeIndicator } from "@/components/shared/age-indicator";
|
||||
import { SourceTag } from "@/components/shared/source-tag";
|
||||
import { LeadStatusBadge } from "@/components/shared/status-badge";
|
||||
import { formatPhone, getInitials } from "@/lib/format";
|
||||
import type { Lead } from "@/types/entities";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface LeadCardProps {
|
||||
lead: Lead;
|
||||
@@ -19,34 +19,34 @@ interface LeadCardProps {
|
||||
}
|
||||
|
||||
const sourceLabelMap: Record<string, string> = {
|
||||
FACEBOOK_AD: 'Facebook',
|
||||
INSTAGRAM: 'Instagram',
|
||||
GOOGLE_AD: 'Google',
|
||||
GOOGLE_MY_BUSINESS: 'GMB',
|
||||
WHATSAPP: 'WhatsApp',
|
||||
WEBSITE: 'Website',
|
||||
REFERRAL: 'Referral',
|
||||
WALK_IN: 'Walk-in',
|
||||
PHONE: 'Phone',
|
||||
OTHER: 'Other',
|
||||
FACEBOOK_AD: "Facebook",
|
||||
INSTAGRAM: "Instagram",
|
||||
GOOGLE_AD: "Google",
|
||||
GOOGLE_MY_BUSINESS: "GMB",
|
||||
WHATSAPP: "WhatsApp",
|
||||
WEBSITE: "Website",
|
||||
REFERRAL: "Referral",
|
||||
WALK_IN: "Walk-in",
|
||||
PHONE: "Phone",
|
||||
OTHER: "Other",
|
||||
};
|
||||
|
||||
export const LeadCard = ({ lead, onAssign, onMessage, onMarkSpam, onMerge, onLogCall, onUpdateStatus }: LeadCardProps) => {
|
||||
const firstName = lead.contactName?.firstName ?? '';
|
||||
const lastName = lead.contactName?.lastName ?? '';
|
||||
const name = `${firstName} ${lastName}`.trim() || 'Unknown';
|
||||
const initials = firstName && lastName ? getInitials(firstName, lastName) : '??';
|
||||
const phone = lead.contactPhone?.[0] ? formatPhone(lead.contactPhone[0]) : '';
|
||||
const sourceLabel = lead.leadSource ? sourceLabelMap[lead.leadSource] ?? lead.leadSource : '';
|
||||
const firstName = lead.contactName?.firstName ?? "";
|
||||
const lastName = lead.contactName?.lastName ?? "";
|
||||
const name = `${firstName} ${lastName}`.trim() || "Unknown";
|
||||
const initials = firstName && lastName ? getInitials(firstName, lastName) : "??";
|
||||
const phone = lead.contactPhone?.[0] ? formatPhone(lead.contactPhone[0]) : "";
|
||||
const sourceLabel = lead.leadSource ? (sourceLabelMap[lead.leadSource] ?? lead.leadSource) : "";
|
||||
const isSpam = (lead.spamScore ?? 0) >= 60;
|
||||
const isDuplicate = lead.isDuplicate === true;
|
||||
const isAssigned = lead.assignedAgent !== null && lead.leadStatus !== 'NEW';
|
||||
const isAssigned = lead.assignedAgent !== null && lead.leadStatus !== "NEW";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'flex items-center gap-4 rounded-2xl border border-secondary p-5 transition hover:shadow-md',
|
||||
isSpam ? 'bg-warning-primary' : 'bg-primary',
|
||||
"flex items-center gap-4 rounded-2xl border border-secondary p-5 transition hover:shadow-md",
|
||||
isSpam ? "bg-warning-primary" : "bg-primary",
|
||||
)}
|
||||
>
|
||||
{/* Avatar */}
|
||||
|
||||
@@ -1,77 +1,64 @@
|
||||
import type { FC } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { TableBody as AriaTableBody } from 'react-aria-components';
|
||||
import type { SortDescriptor, Selection } from 'react-aria-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEllipsisVertical } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import type { FC } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { faEllipsisVertical } from "@fortawesome/pro-duotone-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { TableBody as AriaTableBody } from "react-aria-components";
|
||||
import type { Selection, SortDescriptor } from "react-aria-components";
|
||||
import { Table } from "@/components/application/table/table";
|
||||
import { Badge } from "@/components/base/badges/badges";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { AgeIndicator } from "@/components/shared/age-indicator";
|
||||
import { SourceTag } from "@/components/shared/source-tag";
|
||||
import { LeadStatusBadge } from "@/components/shared/status-badge";
|
||||
import { formatPhone, formatShortDate } from "@/lib/format";
|
||||
import type { Lead } from "@/types/entities";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const DotsVertical: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faEllipsisVertical} className={className} />;
|
||||
import { Badge } from '@/components/base/badges/badges';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { Table } from '@/components/application/table/table';
|
||||
import { LeadStatusBadge } from '@/components/shared/status-badge';
|
||||
import { SourceTag } from '@/components/shared/source-tag';
|
||||
import { AgeIndicator } from '@/components/shared/age-indicator';
|
||||
import { formatPhone, formatShortDate } from '@/lib/format';
|
||||
import { cx } from '@/utils/cx';
|
||||
import type { Lead } from '@/types/entities';
|
||||
|
||||
type LeadTableProps = {
|
||||
leads: Lead[];
|
||||
onSelectionChange: (selectedIds: string[]) => void;
|
||||
selectedIds: string[];
|
||||
sortField: string;
|
||||
sortDirection: 'asc' | 'desc';
|
||||
sortDirection: "asc" | "desc";
|
||||
onSort: (field: string) => void;
|
||||
onViewActivity?: (lead: Lead) => void;
|
||||
};
|
||||
|
||||
type TableRow = {
|
||||
id: string;
|
||||
type: 'lead' | 'dup-sub';
|
||||
type: "lead" | "dup-sub";
|
||||
lead: Lead;
|
||||
};
|
||||
|
||||
const SpamDisplay = ({ score }: { score: number }) => {
|
||||
const colorClass =
|
||||
score < 30
|
||||
? 'text-success-primary'
|
||||
: score < 60
|
||||
? 'text-warning-primary'
|
||||
: 'text-error-primary';
|
||||
const colorClass = score < 30 ? "text-success-primary" : score < 60 ? "text-warning-primary" : "text-error-primary";
|
||||
|
||||
const bgClass = score >= 60 ? 'rounded px-1.5 py-0.5 bg-error-primary' : '';
|
||||
const bgClass = score >= 60 ? "rounded px-1.5 py-0.5 bg-error-primary" : "";
|
||||
|
||||
return <span className={cx('text-xs font-semibold', colorClass, bgClass)}>{score}%</span>;
|
||||
return <span className={cx("text-xs font-semibold", colorClass, bgClass)}>{score}%</span>;
|
||||
};
|
||||
|
||||
export const LeadTable = ({
|
||||
leads,
|
||||
onSelectionChange,
|
||||
selectedIds,
|
||||
sortField,
|
||||
sortDirection,
|
||||
onSort,
|
||||
onViewActivity,
|
||||
}: LeadTableProps) => {
|
||||
export const LeadTable = ({ leads, onSelectionChange, selectedIds, sortField, sortDirection, onSort, onViewActivity }: LeadTableProps) => {
|
||||
const [expandedDupId, setExpandedDupId] = useState<string | null>(null);
|
||||
|
||||
const selectedKeys: Selection = new Set(selectedIds);
|
||||
|
||||
const handleSelectionChange = (keys: Selection) => {
|
||||
if (keys === 'all') {
|
||||
if (keys === "all") {
|
||||
// Only select actual lead rows, not dup sub-rows
|
||||
onSelectionChange(leads.map((l) => l.id));
|
||||
} else {
|
||||
// Filter out dup sub-row IDs
|
||||
const leadOnlyIds = [...keys].filter((k) => !String(k).endsWith('-dup')) as string[];
|
||||
const leadOnlyIds = [...keys].filter((k) => !String(k).endsWith("-dup")) as string[];
|
||||
onSelectionChange(leadOnlyIds);
|
||||
}
|
||||
};
|
||||
|
||||
const sortDescriptor: SortDescriptor = {
|
||||
column: sortField,
|
||||
direction: sortDirection === 'asc' ? 'ascending' : 'descending',
|
||||
direction: sortDirection === "asc" ? "ascending" : "descending",
|
||||
};
|
||||
|
||||
const handleSortChange = (descriptor: SortDescriptor) => {
|
||||
@@ -84,28 +71,28 @@ export const LeadTable = ({
|
||||
const tableRows = useMemo<TableRow[]>(() => {
|
||||
const rows: TableRow[] = [];
|
||||
for (const lead of leads) {
|
||||
rows.push({ id: lead.id, type: 'lead', lead });
|
||||
rows.push({ id: lead.id, type: "lead", lead });
|
||||
if (lead.isDuplicate === true && expandedDupId === lead.id) {
|
||||
rows.push({ id: `${lead.id}-dup`, type: 'dup-sub', lead });
|
||||
rows.push({ id: `${lead.id}-dup`, type: "dup-sub", lead });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}, [leads, expandedDupId]);
|
||||
|
||||
const columns = [
|
||||
{ id: 'phone', label: 'Phone', allowsSorting: true },
|
||||
{ id: 'name', label: 'Name', allowsSorting: true },
|
||||
{ id: 'email', label: 'Email', allowsSorting: false },
|
||||
{ id: 'campaign', label: 'Campaign', allowsSorting: false },
|
||||
{ id: 'ad', label: 'Ad', allowsSorting: false },
|
||||
{ id: 'source', label: 'Source', allowsSorting: true },
|
||||
{ id: 'firstContactedAt', label: 'First Contact', allowsSorting: true },
|
||||
{ id: 'lastContactedAt', label: 'Last Contact', allowsSorting: true },
|
||||
{ id: 'status', label: 'Status', allowsSorting: true },
|
||||
{ id: 'createdAt', label: 'Age', allowsSorting: true },
|
||||
{ id: 'spamScore', label: 'Spam', allowsSorting: true },
|
||||
{ id: 'dups', label: 'Dups', allowsSorting: false },
|
||||
{ id: 'actions', label: '', allowsSorting: false },
|
||||
{ id: "phone", label: "Phone", allowsSorting: true },
|
||||
{ id: "name", label: "Name", allowsSorting: true },
|
||||
{ id: "email", label: "Email", allowsSorting: false },
|
||||
{ id: "campaign", label: "Campaign", allowsSorting: false },
|
||||
{ id: "ad", label: "Ad", allowsSorting: false },
|
||||
{ id: "source", label: "Source", allowsSorting: true },
|
||||
{ id: "firstContactedAt", label: "First Contact", allowsSorting: true },
|
||||
{ id: "lastContactedAt", label: "Last Contact", allowsSorting: true },
|
||||
{ id: "status", label: "Status", allowsSorting: true },
|
||||
{ id: "createdAt", label: "Age", allowsSorting: true },
|
||||
{ id: "spamScore", label: "Spam", allowsSorting: true },
|
||||
{ id: "dups", label: "Dups", allowsSorting: false },
|
||||
{ id: "actions", label: "", allowsSorting: false },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -121,35 +108,22 @@ export const LeadTable = ({
|
||||
size="sm"
|
||||
>
|
||||
<Table.Header columns={columns}>
|
||||
{(column) => (
|
||||
<Table.Head
|
||||
key={column.id}
|
||||
id={column.id}
|
||||
label={column.label}
|
||||
allowsSorting={column.allowsSorting}
|
||||
/>
|
||||
)}
|
||||
{(column) => <Table.Head key={column.id} id={column.id} label={column.label} allowsSorting={column.allowsSorting} />}
|
||||
</Table.Header>
|
||||
|
||||
<AriaTableBody items={tableRows}>
|
||||
{(row) => {
|
||||
const { lead } = row;
|
||||
const firstName = lead.contactName?.firstName ?? '';
|
||||
const lastName = lead.contactName?.lastName ?? '';
|
||||
const name = `${firstName} ${lastName}`.trim() || '\u2014';
|
||||
const phone = lead.contactPhone?.[0]
|
||||
? formatPhone(lead.contactPhone[0])
|
||||
: '\u2014';
|
||||
const email = lead.contactEmail?.[0]?.address ?? '\u2014';
|
||||
const firstName = lead.contactName?.firstName ?? "";
|
||||
const lastName = lead.contactName?.lastName ?? "";
|
||||
const name = `${firstName} ${lastName}`.trim() || "\u2014";
|
||||
const phone = lead.contactPhone?.[0] ? formatPhone(lead.contactPhone[0]) : "\u2014";
|
||||
const email = lead.contactEmail?.[0]?.address ?? "\u2014";
|
||||
|
||||
// Render duplicate sub-row
|
||||
if (row.type === 'dup-sub') {
|
||||
if (row.type === "dup-sub") {
|
||||
return (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
id={row.id}
|
||||
className="bg-warning-primary"
|
||||
>
|
||||
<Table.Row key={row.id} id={row.id} className="bg-warning-primary">
|
||||
<Table.Cell className="pl-10">
|
||||
<span className="text-xs text-tertiary">{phone}</span>
|
||||
</Table.Cell>
|
||||
@@ -160,11 +134,7 @@ export const LeadTable = ({
|
||||
<span className="text-xs text-tertiary">{email}</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{lead.leadSource ? (
|
||||
<SourceTag source={lead.leadSource} size="sm" />
|
||||
) : (
|
||||
<span className="text-tertiary">{'\u2014'}</span>
|
||||
)}
|
||||
{lead.leadSource ? <SourceTag source={lead.leadSource} size="sm" /> : <span className="text-tertiary">{"\u2014"}</span>}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge size="sm" type="pill-color" color="warning">
|
||||
@@ -172,11 +142,7 @@ export const LeadTable = ({
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span className="text-xs text-tertiary">
|
||||
{lead.createdAt
|
||||
? formatShortDate(lead.createdAt)
|
||||
: '\u2014'}
|
||||
</span>
|
||||
<span className="text-xs text-tertiary">{lead.createdAt ? formatShortDate(lead.createdAt) : "\u2014"}</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell />
|
||||
<Table.Cell />
|
||||
@@ -208,10 +174,7 @@ export const LeadTable = ({
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
id={row.id}
|
||||
className={cx(
|
||||
isSpamRow && !isSelected && 'bg-warning-primary',
|
||||
isSelected && 'bg-brand-primary',
|
||||
)}
|
||||
className={cx(isSpamRow && !isSelected && "bg-warning-primary", isSelected && "bg-brand-primary")}
|
||||
>
|
||||
<Table.Cell>
|
||||
<span className="font-semibold text-primary">{phone}</span>
|
||||
@@ -228,7 +191,7 @@ export const LeadTable = ({
|
||||
{lead.utmCampaign}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-tertiary">{'\u2014'}</span>
|
||||
<span className="text-tertiary">{"\u2014"}</span>
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
@@ -237,50 +200,26 @@ export const LeadTable = ({
|
||||
Ad
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-tertiary">{'\u2014'}</span>
|
||||
<span className="text-tertiary">{"\u2014"}</span>
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{lead.leadSource ? (
|
||||
<SourceTag source={lead.leadSource} />
|
||||
) : (
|
||||
<span className="text-tertiary">{'\u2014'}</span>
|
||||
)}
|
||||
{lead.leadSource ? <SourceTag source={lead.leadSource} /> : <span className="text-tertiary">{"\u2014"}</span>}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span className="text-tertiary">
|
||||
{lead.firstContactedAt
|
||||
? formatShortDate(lead.firstContactedAt)
|
||||
: '\u2014'}
|
||||
</span>
|
||||
<span className="text-tertiary">{lead.firstContactedAt ? formatShortDate(lead.firstContactedAt) : "\u2014"}</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span className="text-tertiary">
|
||||
{lead.lastContactedAt
|
||||
? formatShortDate(lead.lastContactedAt)
|
||||
: '\u2014'}
|
||||
</span>
|
||||
<span className="text-tertiary">{lead.lastContactedAt ? formatShortDate(lead.lastContactedAt) : "\u2014"}</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{lead.leadStatus ? (
|
||||
<LeadStatusBadge status={lead.leadStatus} />
|
||||
) : (
|
||||
<span className="text-tertiary">{'\u2014'}</span>
|
||||
)}
|
||||
{lead.leadStatus ? <LeadStatusBadge status={lead.leadStatus} /> : <span className="text-tertiary">{"\u2014"}</span>}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{lead.createdAt ? (
|
||||
<AgeIndicator dateStr={lead.createdAt} />
|
||||
) : (
|
||||
<span className="text-tertiary">{'\u2014'}</span>
|
||||
)}
|
||||
{lead.createdAt ? <AgeIndicator dateStr={lead.createdAt} /> : <span className="text-tertiary">{"\u2014"}</span>}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{lead.spamScore != null ? (
|
||||
<SpamDisplay score={lead.spamScore} />
|
||||
) : (
|
||||
<span className="text-tertiary">0%</span>
|
||||
)}
|
||||
{lead.spamScore != null ? <SpamDisplay score={lead.spamScore} /> : <span className="text-tertiary">0%</span>}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{isDup ? (
|
||||
@@ -292,7 +231,7 @@ export const LeadTable = ({
|
||||
}}
|
||||
className="cursor-pointer border-none bg-transparent text-xs font-semibold text-warning-primary hover:text-warning-primary"
|
||||
>
|
||||
1 {isExpanded ? '\u25B4' : '\u25BE'}
|
||||
1 {isExpanded ? "\u25B4" : "\u25BE"}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-tertiary">0</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cx } from '@/utils/cx';
|
||||
import type { Lead, LeadSource } from '@/types/entities';
|
||||
import type { Lead, LeadSource } from "@/types/entities";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface SourceGridProps {
|
||||
leads: Lead[];
|
||||
@@ -19,64 +19,63 @@ type SourceConfig = {
|
||||
|
||||
const sourceConfigs: SourceConfig[] = [
|
||||
{
|
||||
source: 'FACEBOOK_AD',
|
||||
label: 'Facebook Ads',
|
||||
icon: 'f',
|
||||
iconBg: 'bg-utility-blue-50',
|
||||
iconText: 'text-utility-blue-700',
|
||||
countColor: 'text-utility-blue-700',
|
||||
delta: '+4',
|
||||
source: "FACEBOOK_AD",
|
||||
label: "Facebook Ads",
|
||||
icon: "f",
|
||||
iconBg: "bg-utility-blue-50",
|
||||
iconText: "text-utility-blue-700",
|
||||
countColor: "text-utility-blue-700",
|
||||
delta: "+4",
|
||||
},
|
||||
{
|
||||
source: 'GOOGLE_AD',
|
||||
label: 'Google Ads',
|
||||
icon: 'G',
|
||||
iconBg: 'bg-utility-success-50',
|
||||
iconText: 'text-utility-success-700',
|
||||
countColor: 'text-utility-success-700',
|
||||
delta: '+2',
|
||||
source: "GOOGLE_AD",
|
||||
label: "Google Ads",
|
||||
icon: "G",
|
||||
iconBg: "bg-utility-success-50",
|
||||
iconText: "text-utility-success-700",
|
||||
countColor: "text-utility-success-700",
|
||||
delta: "+2",
|
||||
},
|
||||
{
|
||||
source: 'INSTAGRAM',
|
||||
label: 'Instagram',
|
||||
icon: '@',
|
||||
iconBg: 'bg-utility-pink-50',
|
||||
iconText: 'text-utility-pink-700',
|
||||
countColor: 'text-utility-pink-700',
|
||||
delta: '+1',
|
||||
source: "INSTAGRAM",
|
||||
label: "Instagram",
|
||||
icon: "@",
|
||||
iconBg: "bg-utility-pink-50",
|
||||
iconText: "text-utility-pink-700",
|
||||
countColor: "text-utility-pink-700",
|
||||
delta: "+1",
|
||||
},
|
||||
{
|
||||
source: 'GOOGLE_MY_BUSINESS',
|
||||
label: 'Google My Business',
|
||||
icon: 'G',
|
||||
iconBg: 'bg-utility-blue-light-50',
|
||||
iconText: 'text-utility-blue-light-700',
|
||||
countColor: 'text-utility-blue-light-700',
|
||||
delta: '+3',
|
||||
source: "GOOGLE_MY_BUSINESS",
|
||||
label: "Google My Business",
|
||||
icon: "G",
|
||||
iconBg: "bg-utility-blue-light-50",
|
||||
iconText: "text-utility-blue-light-700",
|
||||
countColor: "text-utility-blue-light-700",
|
||||
delta: "+3",
|
||||
},
|
||||
{
|
||||
source: 'REFERRAL',
|
||||
label: 'Referrals',
|
||||
icon: 'R',
|
||||
iconBg: 'bg-utility-purple-50',
|
||||
iconText: 'text-utility-purple-700',
|
||||
countColor: 'text-utility-purple-700',
|
||||
delta: '+2',
|
||||
source: "REFERRAL",
|
||||
label: "Referrals",
|
||||
icon: "R",
|
||||
iconBg: "bg-utility-purple-50",
|
||||
iconText: "text-utility-purple-700",
|
||||
countColor: "text-utility-purple-700",
|
||||
delta: "+2",
|
||||
},
|
||||
{
|
||||
source: 'WALK_IN',
|
||||
label: 'Walk-ins',
|
||||
icon: 'W',
|
||||
iconBg: 'bg-utility-orange-50',
|
||||
iconText: 'text-utility-orange-700',
|
||||
countColor: 'text-utility-orange-700',
|
||||
delta: '0',
|
||||
source: "WALK_IN",
|
||||
label: "Walk-ins",
|
||||
icon: "W",
|
||||
iconBg: "bg-utility-orange-50",
|
||||
iconText: "text-utility-orange-700",
|
||||
countColor: "text-utility-orange-700",
|
||||
delta: "0",
|
||||
},
|
||||
];
|
||||
|
||||
export const SourceGrid = ({ leads, onSourceFilter, activeSource }: SourceGridProps) => {
|
||||
const countBySource = (source: LeadSource): number =>
|
||||
leads.filter((l) => l.leadSource === source).length;
|
||||
const countBySource = (source: LeadSource): number => leads.filter((l) => l.leadSource === source).length;
|
||||
|
||||
const handleClick = (source: LeadSource) => {
|
||||
if (activeSource === source) {
|
||||
@@ -98,37 +97,25 @@ export const SourceGrid = ({ leads, onSourceFilter, activeSource }: SourceGridPr
|
||||
type="button"
|
||||
onClick={() => handleClick(config.source)}
|
||||
className={cx(
|
||||
'cursor-pointer rounded-xl border border-secondary bg-primary p-4 text-left transition hover:shadow-md',
|
||||
isActive && 'ring-2 ring-brand',
|
||||
"cursor-pointer rounded-xl border border-secondary bg-primary p-4 text-left transition hover:shadow-md",
|
||||
isActive && "ring-2 ring-brand",
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span
|
||||
className={cx(
|
||||
'flex size-6 items-center justify-center rounded text-xs font-bold',
|
||||
config.iconBg,
|
||||
config.iconText,
|
||||
)}
|
||||
>
|
||||
<span className={cx("flex size-6 items-center justify-center rounded text-xs font-bold", config.iconBg, config.iconText)}>
|
||||
{config.icon}
|
||||
</span>
|
||||
<span className="text-xs text-quaternary">{config.label}</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className={cx('text-xl font-bold', config.countColor)}>
|
||||
{count}
|
||||
</span>
|
||||
<span className={cx("text-xl font-bold", config.countColor)}>{count}</span>
|
||||
<span
|
||||
className={cx(
|
||||
'text-xs',
|
||||
config.delta.startsWith('+')
|
||||
? 'text-success-primary'
|
||||
: config.delta === '0'
|
||||
? 'text-quaternary'
|
||||
: 'text-error-primary',
|
||||
"text-xs",
|
||||
config.delta.startsWith("+") ? "text-success-primary" : config.delta === "0" ? "text-quaternary" : "text-error-primary",
|
||||
)}
|
||||
>
|
||||
{config.delta === '0' ? 'same' : config.delta}
|
||||
{config.delta === "0" ? "same" : config.delta}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user