mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +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