diff --git a/src/components/outreach/message-list.tsx b/src/components/outreach/message-list.tsx new file mode 100644 index 0000000..e6551c5 --- /dev/null +++ b/src/components/outreach/message-list.tsx @@ -0,0 +1,83 @@ +import { Avatar } from '@/components/base/avatar/avatar'; +import { Badge } from '@/components/base/badges/badges'; +import type { PatientMessage, MessageStatus } from '@/types/entities'; + +interface MessageListProps { + messages: PatientMessage[]; +} + +const statusConfig: Record< + MessageStatus, + { label: string; color: 'blue' | 'success' | 'gray' | 'error' } +> = { + READ: { label: 'Read', color: 'blue' }, + DELIVERED: { label: 'Delivered', color: 'success' }, + SENT: { label: 'Sent', color: 'gray' }, + FAILED: { label: 'Failed', color: 'error' }, +}; + +const getInitials = (name: string | null | undefined): string => { + if (!name) return '?'; + const parts = name.trim().split(' '); + if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); +}; + +const formatTime = (sentAt: string | null): string => { + if (!sentAt) return ''; + const date = new Date(sentAt); + return date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true }); +}; + +export const MessageList = ({ messages }: MessageListProps) => { + return ( +
+ {/* Header */} +
+

Recent Messages

+ +
+ + {/* Message rows */} +
+ {messages.slice(0, 5).map((message) => { + const recipientName = message.recipientName ?? message.senderName ?? 'Unknown'; + const status = message.status ?? 'SENT'; + const statusInfo = statusConfig[status]; + + return ( +
+ {/* Avatar */} + + + {/* Name + preview */} +
+

{recipientName}

+

+ {message.subject ?? message.body ?? '—'} +

+
+ + {/* Time + status */} +
+ {formatTime(message.sentAt)} + + {statusInfo.label} + +
+
+ ); + })} + + {messages.length === 0 && ( +

No messages yet.

+ )} +
+
+ ); +}; diff --git a/src/components/outreach/template-list.tsx b/src/components/outreach/template-list.tsx new file mode 100644 index 0000000..5375fc9 --- /dev/null +++ b/src/components/outreach/template-list.tsx @@ -0,0 +1,135 @@ +import { useState } from 'react'; +import { SearchLg } from '@untitledui/icons'; +import { Input } from '@/components/base/input/input'; +import { Badge } from '@/components/base/badges/badges'; +import { Tabs, TabList, Tab, TabPanel } from '@/components/application/tabs/tabs'; +import { cx } from '@/utils/cx'; +import type { WhatsAppTemplate } from '@/types/entities'; + +interface TemplateListProps { + templates: WhatsAppTemplate[]; + selectedId: string | null; + onSelect: (id: string) => void; +} + +type TabId = 'all' | 'campaign' | 'custom'; + +export const TemplateList = ({ templates, selectedId, onSelect }: TemplateListProps) => { + const [search, setSearch] = useState(''); + const [activeTab, setActiveTab] = useState('all'); + + const filtered = templates.filter((t) => { + const matchesSearch = + !search || + t.name?.toLowerCase().includes(search.toLowerCase()) || + t.body?.toLowerCase().includes(search.toLowerCase()); + + const matchesTab = + activeTab === 'all' || + (activeTab === 'campaign' && t.linkedCampaignId !== null) || + (activeTab === 'custom' && t.linkedCampaignId === null); + + return matchesSearch && matchesTab; + }); + + const tabs = [ + { id: 'all' as TabId, label: `All (${templates.length})` }, + { id: 'campaign' as TabId, label: 'By Campaign' }, + { id: 'custom' as TabId, label: 'Custom' }, + ]; + + return ( +
+ {/* Header */} +
+

Message Templates

+ setSearch(val)} + aria-label="Search templates" + /> +
+ + {/* Tabs */} +
+ setActiveTab(key as TabId)}> + + {(item) => } + + {tabs.map((tab) => ( + + ))} + +
+ + {/* Scrollable list */} +
+ {filtered.length === 0 && ( +

No templates found.

+ )} + {filtered.map((template) => { + const isSelected = template.id === selectedId; + const isPending = template.approvalStatus === 'PENDING'; + const deliveredRate = + template.sendCount && template.sendCount > 0 + ? Math.round(((template.deliveredCount ?? 0) / template.sendCount) * 100) + : 0; + + return ( + + ); + })} +
+
+ ); +}; diff --git a/src/components/outreach/template-preview.tsx b/src/components/outreach/template-preview.tsx new file mode 100644 index 0000000..f1a4b2b --- /dev/null +++ b/src/components/outreach/template-preview.tsx @@ -0,0 +1,138 @@ +import { Button } from '@/components/base/buttons/button'; +import { Badge } from '@/components/base/badges/badges'; +import { WhatsAppMockup } from './whatsapp-mockup'; +import type { WhatsAppTemplate } from '@/types/entities'; + +interface TemplatePreviewProps { + template: WhatsAppTemplate; +} + +const variableDescriptions: Record = { + patient_name: "Lead's contact name", + hospital_name: 'Clinic or hospital name', + campaign_name: 'Linked campaign name', + booking_link: 'Appointment booking URL', +}; + +export const TemplatePreview = ({ template }: TemplatePreviewProps) => { + const totalSent = template.sendCount ?? 0; + const delivered = template.deliveredCount ?? 0; + const read = template.readCount ?? 0; + const clicked = template.clickedCount ?? 0; + const failed = template.failedCount ?? 0; + + const metrics = [ + { label: 'Sent', value: totalSent }, + { label: 'Delivered', value: delivered }, + { label: 'Read', value: read }, + { label: 'Clicked', value: clicked }, + { label: 'Failed', value: failed }, + ]; + + return ( +
+ {/* Header */} +
+

+ Template Preview — {template.name} +

+ +
+ + {/* Body */} +
+ {/* Left: phone mockup */} +
+ +
+ + {/* Right: details */} +
+ {/* Variables */} + {template.variables.length > 0 && ( +
+

Variables

+
+ {template.variables.map((variable) => ( +
+ + {`{{${variable}}}`} + + + + {variableDescriptions[variable] ?? variable} + +
+ ))} +
+
+ )} + + {/* Linked Campaign */} +
+

Linked Campaign

+ {template.linkedCampaignName ? ( + + {template.linkedCampaignName} + + ) : ( + + None (Custom) + + )} +
+ + {/* Approval Status */} +
+

Approval Status

+ {template.approvalStatus === 'APPROVED' ? ( + + ✓ Approved + + ) : template.approvalStatus === 'PENDING' ? ( + + ⏳ Pending Review + + ) : ( + + Rejected + + )} +
+ + {/* Performance */} +
+

Performance

+
+ {metrics.map((metric) => ( +
+

{metric.value}

+

{metric.label}

+
+ ))} +
+
+ + {/* Languages */} + {template.language.length > 0 && ( +
+

Language

+
+ {template.language.map((lang) => ( + + {lang.toUpperCase()} + + ))} +
+
+ )} +
+
+
+ ); +}; diff --git a/src/components/outreach/whatsapp-mockup.tsx b/src/components/outreach/whatsapp-mockup.tsx new file mode 100644 index 0000000..87d509c --- /dev/null +++ b/src/components/outreach/whatsapp-mockup.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +interface WhatsAppMockupProps { + body: string; + variables: string[]; +} + +const highlightVariables = (text: string): React.ReactNode => { + const parts = text.split(/({{[^}]+}})/g); + return parts.map((part, index) => { + if (/^{{[^}]+}}$/.test(part)) { + return ( + + {part} + + ); + } + return part; + }); +}; + +export const WhatsAppMockup = ({ body }: WhatsAppMockupProps) => { + const lines = body.split('\n'); + // Extract CTA line — last non-empty line that looks like a URL or "Book here" + const ctaLine = lines.find((l) => l.includes('{{booking_link}}') || l.toLowerCase().includes('book ')); + + return ( +
+ {/* WA Header */} +
+
+ R +
+ Ramaiah Memorial +
+ + {/* WA Body */} +
+
+

{highlightVariables(body)}

+ {ctaLine && ( + + Book Now → + + )} +

2:30 PM ✓✓

+
+
+
+ ); +}; diff --git a/src/pages/outreach.tsx b/src/pages/outreach.tsx index f52321f..ab48c10 100644 --- a/src/pages/outreach.tsx +++ b/src/pages/outreach.tsx @@ -1,11 +1,148 @@ -import { TopBar } from "@/components/layout/top-bar"; +import { useState } from 'react'; + +import { TopBar } from '@/components/layout/top-bar'; +import { TemplateList } from '@/components/outreach/template-list'; +import { TemplatePreview } from '@/components/outreach/template-preview'; +import { MessageList } from '@/components/outreach/message-list'; +import { useData } from '@/providers/data-provider'; +import { cx } from '@/utils/cx'; +import type { PatientMessage } from '@/types/entities'; + +type OutreachKpiCard = { + label: string; + value: number; + delta: string; + deltaColor: string; +}; + +const recentMessages: PatientMessage[] = [ + { + id: 'msg-1', + createdAt: '2026-03-16T08:30:00Z', + subject: "Women's Day Health Checkup", + body: 'Namaste Priya, Happy Women\'s Day from Ramaiah Memorial! Book your free checkup here.', + direction: 'CLINIC_TO_PATIENT', + channel: 'WHATSAPP', + priority: 'NORMAL', + sentAt: '2026-03-16T08:30:00Z', + readAt: '2026-03-16T09:15:00Z', + senderName: 'Care Team', + patientId: 'p-001', + status: 'READ', + recipientName: 'Priya Sharma', + recipientPhone: '+91 98765 43210', + }, + { + id: 'msg-2', + createdAt: '2026-03-16T09:10:00Z', + subject: 'IVF Free Consultation', + body: 'Namaste Anitha, We would like to invite you for a FREE first consultation with our IVF specialist.', + direction: 'CLINIC_TO_PATIENT', + channel: 'WHATSAPP', + priority: 'NORMAL', + sentAt: '2026-03-16T09:10:00Z', + readAt: null, + senderName: 'Care Team', + patientId: 'p-002', + status: 'DELIVERED', + recipientName: 'Anitha Reddy', + recipientPhone: '+91 87654 32109', + }, + { + id: 'msg-3', + createdAt: '2026-03-16T10:00:00Z', + subject: 'Cervical Screening Reminder', + body: 'Namaste Kavitha, This is a gentle reminder for your cervical cancer screening appointment.', + direction: 'CLINIC_TO_PATIENT', + channel: 'WHATSAPP', + priority: 'HIGH', + sentAt: '2026-03-16T10:00:00Z', + readAt: null, + senderName: 'Care Team', + patientId: 'p-003', + status: 'SENT', + recipientName: 'Kavitha Nair', + recipientPhone: '+91 76543 21098', + }, + { + id: 'msg-4', + createdAt: '2026-03-16T10:45:00Z', + subject: 'General Follow-up', + body: 'Namaste Meena, This is Ramaiah Memorial reaching out to follow up on your recent enquiry.', + direction: 'CLINIC_TO_PATIENT', + channel: 'WHATSAPP', + priority: 'NORMAL', + sentAt: '2026-03-16T10:45:00Z', + readAt: '2026-03-16T11:02:00Z', + senderName: 'Care Team', + patientId: 'p-004', + status: 'READ', + recipientName: 'Meena Iyer', + recipientPhone: '+91 65432 10987', + }, + { + id: 'msg-5', + createdAt: '2026-03-16T11:30:00Z', + subject: 'Senior Health Package', + body: 'Namaste Lakshmi, Our Senior Health Package is designed specifically for individuals above 60 years.', + direction: 'CLINIC_TO_PATIENT', + channel: 'WHATSAPP', + priority: 'NORMAL', + sentAt: '2026-03-16T11:30:00Z', + readAt: null, + senderName: 'Care Team', + patientId: 'p-005', + status: 'FAILED', + recipientName: 'Lakshmi Devi', + recipientPhone: '+91 54321 09876', + }, +]; + +const kpiCards: OutreachKpiCard[] = [ + { label: 'Messages Sent (24h)', value: 87, delta: '+14% vs yesterday', deltaColor: 'text-success-primary' }, + { label: 'Delivered', value: 78, delta: '90% delivery rate', deltaColor: 'text-brand-secondary' }, + { label: 'Read', value: 52, delta: '67% read rate', deltaColor: 'text-brand-secondary' }, + { label: 'CTA Clicked', value: 18, delta: '+5 vs yesterday', deltaColor: 'text-success-primary' }, +]; export const OutreachPage = () => { + const { templates } = useData(); + const [selectedId, setSelectedId] = useState(templates[0]?.id ?? null); + + const selectedTemplate = templates.find((t) => t.id === selectedId) ?? null; + return ( -
- -
-

Outreach — coming soon

+
+ + +
+ {/* Left: Template List */} + + + {/* Right: Preview + KPIs + Messages */} +
+ {/* Outreach KPIs */} +
+ {kpiCards.map((card) => ( +
+

+ {card.label} +

+

{card.value}

+

{card.delta}

+
+ ))} +
+ + {/* Template Preview */} + {selectedTemplate && } + + {/* Recent Messages */} + +
);