mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: add shared StatusBadge, SourceTag, AgeIndicator components and format utilities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
24
src/components/shared/age-indicator.tsx
Normal file
24
src/components/shared/age-indicator.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { cx } from '@/utils/cx';
|
||||||
|
import { daysAgoFromNow, getAgeBracket } from '@/lib/format';
|
||||||
|
|
||||||
|
const ageBracketColorMap = {
|
||||||
|
fresh: 'text-success-primary',
|
||||||
|
warm: 'text-warning-primary',
|
||||||
|
cold: 'text-error-primary',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AgeIndicatorProps {
|
||||||
|
dateStr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgeIndicator = ({ dateStr }: AgeIndicatorProps) => {
|
||||||
|
const days = daysAgoFromNow(dateStr);
|
||||||
|
const bracket = getAgeBracket(days);
|
||||||
|
const colorClass = ageBracketColorMap[bracket];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cx('text-sm font-semibold', colorClass)}>
|
||||||
|
{days}d
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
45
src/components/shared/source-tag.tsx
Normal file
45
src/components/shared/source-tag.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
|
import type { LeadSource } from '@/types/entities';
|
||||||
|
|
||||||
|
type SourceColor = 'blue' | 'pink' | 'success' | 'blue-light' | 'gray' | 'purple' | 'orange' | 'warning';
|
||||||
|
|
||||||
|
const sourceColorMap: Record<LeadSource, SourceColor> = {
|
||||||
|
FACEBOOK_AD: 'blue',
|
||||||
|
INSTAGRAM: 'pink',
|
||||||
|
GOOGLE_AD: 'success',
|
||||||
|
GOOGLE_MY_BUSINESS: 'blue-light',
|
||||||
|
WHATSAPP: 'success',
|
||||||
|
WEBSITE: 'gray',
|
||||||
|
REFERRAL: 'purple',
|
||||||
|
WALK_IN: 'orange',
|
||||||
|
PHONE: 'warning',
|
||||||
|
OTHER: 'gray',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceLabelMap: Record<LeadSource, 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',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SourceTagProps {
|
||||||
|
source: LeadSource;
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SourceTag = ({ source, size = 'sm' }: SourceTagProps) => {
|
||||||
|
const color = sourceColorMap[source];
|
||||||
|
const label = sourceLabelMap[source];
|
||||||
|
return (
|
||||||
|
<Badge size={size} type="pill-color" color={color}>
|
||||||
|
{label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
54
src/components/shared/status-badge.tsx
Normal file
54
src/components/shared/status-badge.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { BadgeWithDot } from '@/components/base/badges/badges';
|
||||||
|
import type { CampaignStatus, LeadStatus } from '@/types/entities';
|
||||||
|
|
||||||
|
const toTitleCase = (str: string): string =>
|
||||||
|
str
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
|
||||||
|
type LeadStatusColor = 'blue' | 'brand' | 'success' | 'warning' | 'purple' | 'error' | 'gray';
|
||||||
|
type CampaignStatusColor = 'gray' | 'success' | 'warning' | 'blue';
|
||||||
|
|
||||||
|
const leadStatusColorMap: Record<LeadStatus, LeadStatusColor> = {
|
||||||
|
NEW: 'blue',
|
||||||
|
CONTACTED: 'brand',
|
||||||
|
QUALIFIED: 'success',
|
||||||
|
NURTURING: 'warning',
|
||||||
|
APPOINTMENT_SET: 'purple',
|
||||||
|
CONVERTED: 'success',
|
||||||
|
LOST: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
const campaignStatusColorMap: Record<CampaignStatus, CampaignStatusColor> = {
|
||||||
|
DRAFT: 'gray',
|
||||||
|
ACTIVE: 'success',
|
||||||
|
PAUSED: 'warning',
|
||||||
|
COMPLETED: 'blue',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LeadStatusBadgeProps {
|
||||||
|
status: LeadStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LeadStatusBadge = ({ status }: LeadStatusBadgeProps) => {
|
||||||
|
const color = leadStatusColorMap[status];
|
||||||
|
return (
|
||||||
|
<BadgeWithDot size="sm" type="pill-color" color={color}>
|
||||||
|
{toTitleCase(status)}
|
||||||
|
</BadgeWithDot>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CampaignStatusBadgeProps {
|
||||||
|
status: CampaignStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CampaignStatusBadge = ({ status }: CampaignStatusBadgeProps) => {
|
||||||
|
const color = campaignStatusColorMap[status];
|
||||||
|
return (
|
||||||
|
<BadgeWithDot size="sm" type="pill-color" color={color}>
|
||||||
|
{toTitleCase(status)}
|
||||||
|
</BadgeWithDot>
|
||||||
|
);
|
||||||
|
};
|
||||||
39
src/lib/format.ts
Normal file
39
src/lib/format.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Format currency from micros to display string (INR)
|
||||||
|
export const formatCurrency = (amountMicros: number, currency = 'INR'): string => {
|
||||||
|
const amount = amountMicros / 1_000_000;
|
||||||
|
return new Intl.NumberFormat('en-IN', { style: 'currency', currency, maximumFractionDigits: 0 }).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format phone number for display
|
||||||
|
export const formatPhone = (phone: { number: string; callingCode: string }): string =>
|
||||||
|
`${phone.callingCode} ${phone.number.replace(/(\d{5})(\d{5})/, '$1 $2')}`;
|
||||||
|
|
||||||
|
// Calculate days ago from ISO date string
|
||||||
|
export const daysAgoFromNow = (dateStr: string): number => {
|
||||||
|
const diff = Date.now() - new Date(dateStr).getTime();
|
||||||
|
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get aging bracket from days
|
||||||
|
export const getAgeBracket = (days: number): 'fresh' | 'warm' | 'cold' =>
|
||||||
|
days < 2 ? 'fresh' : days <= 5 ? 'warm' : 'cold';
|
||||||
|
|
||||||
|
// Format relative age string
|
||||||
|
export const formatRelativeAge = (dateStr: string): string => {
|
||||||
|
const days = daysAgoFromNow(dateStr);
|
||||||
|
if (days === 0) return 'Today';
|
||||||
|
if (days === 1) return '1 day ago';
|
||||||
|
return `${days} days ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format short date (Mar 15, 2:30 PM)
|
||||||
|
export const formatShortDate = (dateStr: string): string =>
|
||||||
|
new Intl.DateTimeFormat('en-IN', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }).format(new Date(dateStr));
|
||||||
|
|
||||||
|
// Get initials from a name
|
||||||
|
export const getInitials = (firstName: string, lastName: string): string =>
|
||||||
|
`${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase();
|
||||||
|
|
||||||
|
// Format large numbers (1234 -> "1.2K", 1234567 -> "1.2M")
|
||||||
|
export const formatCompact = (n: number): string =>
|
||||||
|
new Intl.NumberFormat('en-IN', { notation: 'compact', maximumFractionDigits: 1 }).format(n);
|
||||||
Reference in New Issue
Block a user