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 */}
+
+
);