# Communication & Patient Outreach UI Patterns Dual-platform: Web (Bootstrap 5/Tabler + PHP) and Android (Jetpack Compose + Material 3). --- ## 1. Secure Messaging System **Layout:** Desktop 3-panel (conversations | chat | patient context), tablet 2-panel (conversations 40% | chat 60%), mobile full-screen list or chat with back nav. **Left:** Conversation list with Open/Recent/All tabs, unread badges, search. **Center:** Message bubbles + delivery status + composer (attach, template, send). **Right (desktop):** Patient name, MRN, allergies, vitals, shared docs. **Message types:** Text (2000 chars), Image (JPEG/PNG <512KB), Document (PDF <10MB, virus-scanned), Audio (MP3/AAC, 2 min max). **Delivery:** Sent > Delivered > Read (blue double-check + timestamp). **Rules:** Patient threads use plain language with template suggestions. Provider threads allow clinical shorthand and DICOM links. E2E encryption lock icon in header. PHI warning on external shares. Templates support `{patient_name}`, `{provider_name}`, `{appointment_date}` tokens. ### Web: Three-Panel Chat ```html
Jane Doe
Online
Jane Doe, F, 42 | MRN: 7823
Allergy: Sulfa
``` ### Android: Adaptive Messaging ```kotlin @Composable fun SecureMessagingScreen(viewModel: MessagingViewModel = hiltViewModel()) { val conversations by viewModel.conversations.collectAsStateWithLifecycle() val activeChat by viewModel.activeChat.collectAsStateWithLifecycle() val windowSize = currentWindowAdaptiveInfo().windowSizeClass when (windowSize.widthSizeClass) { WindowWidthSizeClass.Expanded -> TwoPanelMessaging(conversations, activeChat, viewModel) else -> if (activeChat != null) ChatDetailScreen(activeChat!!, viewModel) else ConversationListScreen(conversations, viewModel) } } @Composable fun ChatDetailScreen(chat: ChatDetail, viewModel: MessagingViewModel) { val messages by viewModel.messages.collectAsStateWithLifecycle() Scaffold( topBar = { ChatTopBar(chat.participant, isEncrypted = true) }, bottomBar = { MessageComposer(onSend = { viewModel.send(it) }, onAttach = { viewModel.showAttachSheet() }) } ) { padding -> LazyColumn(Modifier.padding(padding).fillMaxSize(), reverseLayout = true) { items(messages, key = { it.id }) { msg -> MessageBubble(msg, isOwn = msg.senderId == viewModel.currentUserId) } } } // ModalBottomSheet for attachment options (image, document, audio) } ``` --- ## 2. Multi-Provider Communication Group threads linked to patient records. @mention autocomplete for team members. Priority flags: Normal, Important (amber), Urgent (red + sound). Case discussion threads branch from main chat. **Handoff notes:** Patient ID (auto-filled), Condition Summary, Active Issues (bullets), Pending Items (checklist), Notes (free text). ### Web: Thread-Based Team Chat ```html
DSMJ +3

Care Team: Jane Doe (MRN 7823)

Urgent
Dr. Smith 10:15

@nurse.johnson Please check BP every 2h.

``` ### Android: Group Chat with Mention Chips ```kotlin @Composable fun CareTeamChat(viewModel: CareTeamViewModel = hiltViewModel()) { val members by viewModel.members.collectAsStateWithLifecycle() val messages by viewModel.messages.collectAsStateWithLifecycle() Scaffold( topBar = { TopAppBar(title = { Text("Care Team: ${viewModel.patientName}") }, actions = { PriorityFlagButton(viewModel.priority, onChange = { viewModel.setPriority(it) }) }) }, bottomBar = { MentionComposer(members, onSend = { viewModel.send(it) }) } ) { padding -> LazyColumn(Modifier.padding(padding), reverseLayout = true) { items(messages, key = { it.id }) { CareTeamMessageBubble(it) } } } } // MentionComposer: OutlinedTextField that triggers LazyRow of SuggestionChips // when user types "@". Chip tap inserts "@name " into text field. ``` --- ## 3. Patient Outreach Campaigns **Dashboard:** Active/inactive campaign list with toggle, performance metrics (sent, delivered, opened, responded), stat cards at top. **Builder steps:** 1) Audience filters (age, condition, last visit, risk level, insurance) 2) Template editor with `{patient_name}`, `{provider_name}`, `{appointment_date}` tokens 3) Channel selection (email, SMS, push, portal) 4) Schedule (now/delayed/recurring) 5) Review + confirm. **Anti-spam:** Show patient's active subscriptions, frequency cap (default 3/week), one-tap unsubscribe, outbound audit log. ### Web: Campaign Dashboard ```html
Active Campaigns
4
Total Sent (Month)
3,240

Campaigns

NameStatusSentDelivered OpenedRespondedActions
``` ### Android: Campaign Cards ```kotlin @Composable fun CampaignListScreen(viewModel: CampaignViewModel = hiltViewModel()) { val campaigns by viewModel.campaigns.collectAsStateWithLifecycle() Scaffold(floatingActionButton = { ExtendedFloatingActionButton(onClick = { viewModel.createNew() }, icon = { Icon(Icons.Default.Add, null) }, text = { Text("New Campaign") }) }) { padding -> LazyColumn(Modifier.padding(padding), contentPadding = PaddingValues(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { items(campaigns, key = { it.id }) { c -> ElevatedCard(Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp)) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(c.name, style = MaterialTheme.typography.titleMedium) Switch(checked = c.isActive, onCheckedChange = { viewModel.toggleStatus(c) }) } Row(Modifier.fillMaxWidth().padding(top = 8.dp), horizontalArrangement = Arrangement.SpaceEvenly) { MetricColumn("Sent", "${c.sent}"); MetricColumn("Delivered", "${c.delivered}") MetricColumn("Opened", "${c.opened}"); MetricColumn("Responded", "${c.responded}") } } } } } } } ``` --- ## 4. Pre-Appointment Screening Digital questionnaire sent via SMS/email link. Provider sees pre-populated summary during appointment. **Components:** Pill-button symptom selection (multi-select), pain scale slider (0-10), Yes/No segmented buttons, free text (500 chars), symptom search with ICD autocomplete. ### Web: Mobile-Friendly Survey ```html

Pre-Visit Screening

0510
``` ### Android: Screening Composable ```kotlin @Composable fun PreScreeningForm(viewModel: ScreeningViewModel = hiltViewModel()) { val symptoms = listOf("Headache", "Fever", "Cough", "Fatigue", "Nausea", "Chest Pain") val selected by viewModel.selectedSymptoms.collectAsStateWithLifecycle() val painLevel by viewModel.painLevel.collectAsStateWithLifecycle() LazyColumn(Modifier.fillMaxSize().padding(16.dp)) { item { // Symptom chips (FlowRow with FilterChip per symptom) Text("Select current symptoms:", style = MaterialTheme.typography.titleMedium) FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { symptoms.forEach { s -> FilterChip(selected = s in selected, onClick = { viewModel.toggleSymptom(s) }, label = { Text(s) }) } } } item { // Pain slider 0-10 Text("Pain level: ${painLevel.toInt()}/10", style = MaterialTheme.typography.titleMedium) Slider(value = painLevel, onValueChange = { viewModel.setPain(it) }, valueRange = 0f..10f, steps = 9) } item { // Yes/No segmented button + free text + submit Text("Do you have a fever?", style = MaterialTheme.typography.titleMedium) SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) { SegmentedButton(selected = viewModel.hasFever == true, onClick = { viewModel.setFever(true) }, shape = SegmentedButtonDefaults.itemShape(0, 2), label = { Text("Yes") }) SegmentedButton(selected = viewModel.hasFever == false, onClick = { viewModel.setFever(false) }, shape = SegmentedButtonDefaults.itemShape(1, 2), label = { Text("No") }) } OutlinedTextField(value = viewModel.additionalNotes, onValueChange = { viewModel.setNotes(it) }, label = { Text("Additional details") }, modifier = Modifier.fillMaxWidth(), maxLines = 5) Button(onClick = { viewModel.submit() }, modifier = Modifier.fillMaxWidth()) { Text("Submit Screening") } } } } ``` --- ## 5. Health Bot / Post-Discharge Monitoring Automated check-ins at Day 1 (pain/wound/meds), Day 3 (symptom changes), Day 7 (recovery), Day 14 (follow-up). Escalation triggers: pain > 7, new symptoms, no improvement, fever. **UI:** Chat bubbles with bot avatar, quick-reply buttons, escalation to human provider on threshold, summary report for care team. Log unanswered/failed attempts. ### Web: Floating Chat Widget ```html
Bot Recovery Check-In
How is your pain today?
``` ### Android: Bot Chat Composable ```kotlin @Composable fun HealthBotScreen(viewModel: HealthBotViewModel = hiltViewModel()) { val messages by viewModel.botMessages.collectAsStateWithLifecycle() val quickReplies by viewModel.quickReplies.collectAsStateWithLifecycle() Scaffold(topBar = { TopAppBar(title = { Text("Recovery Check-In") }, navigationIcon = { BotAvatar() }) }) { padding -> Column(Modifier.padding(padding).fillMaxSize()) { LazyColumn(Modifier.weight(1f).padding(horizontal = 12.dp), reverseLayout = true) { items(messages, key = { it.id }) { BotMessageBubble(it) } } if (quickReplies.isNotEmpty()) { FlowRow(Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { quickReplies.forEach { r -> OutlinedButton(onClick = { viewModel.selectReply(r) }) { Text(r.label) } } } } } } } ``` --- ## 6. Notification System Architecture **Types:** Clinical (lab results, vital alerts, med reminders -- High/Critical -- in-app, push, SMS), Administrative (appt reminders, billing -- Normal -- in-app, push, email), Communication (messages, care team -- Normal/High -- in-app, push). **Priority:** Critical bypasses DND + requires ack. High respects DND. Normal respects quiet hours (10PM-7AM). Preferences stored per channel per type in `notification_preferences`. ### Web: Notification Dropdown ```html ``` ### Android: Notification Channels + Deep Links ```kotlin object HealthNotificationChannels { fun createAll(context: Context) { val mgr = context.getSystemService(NotificationManager::class.java) mgr.createNotificationChannels(listOf( NotificationChannel("clinical_critical", "Critical Alerts", NotificationManager.IMPORTANCE_HIGH).apply { setBypassDnd(true); enableVibration(true) }, NotificationChannel("clinical_results", "Lab Results", NotificationManager.IMPORTANCE_DEFAULT), NotificationChannel("admin_reminders", "Reminders", NotificationManager.IMPORTANCE_DEFAULT), NotificationChannel("messages", "Messages", NotificationManager.IMPORTANCE_DEFAULT) )) } } fun buildLabNotification(ctx: Context, patient: String, test: String): Notification { val intent = NavDeepLinkBuilder(ctx).setGraph(R.navigation.nav_graph) .setDestination(R.id.labResultFragment).createPendingIntent() return NotificationCompat.Builder(ctx, "clinical_results") .setSmallIcon(R.drawable.ic_flask).setContentTitle("Lab Results Ready") .setContentText("$patient - $test available").setContentIntent(intent) .setAutoCancel(true).build() } ``` --- ## 7. Feedback Collection Post-visit survey: star rating (1-5), NPS (0-10), provider-specific feedback with anonymous toggle, free-text suggestions (max 500 chars). Triggered after visit completion. ### Web: Survey Modal ```html ``` ### Android: Feedback BottomSheet ```kotlin @Composable fun FeedbackBottomSheet(onDismiss: () -> Unit, onSubmit: (Feedback) -> Unit) { var rating by remember { mutableIntStateOf(0) } var nps by remember { mutableFloatStateOf(5f) } var anonymous by remember { mutableStateOf(false) } var comments by remember { mutableStateOf("") } ModalBottomSheet(onDismissRequest = onDismiss) { Column(Modifier.padding(24.dp).fillMaxWidth()) { Text("How was your visit?", style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) Spacer(Modifier.height(16.dp)) Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { (1..5).forEach { s -> IconButton(onClick = { rating = s }) { Icon(if (s <= rating) Icons.Filled.Star else Icons.Outlined.Star, tint = if (s <= rating) Color(0xFFF59F00) else Color.Gray, contentDescription = "$s stars", modifier = Modifier.size(36.dp)) } } } Text("Recommend us? ${nps.toInt()}/10") Slider(value = nps, onValueChange = { nps = it }, valueRange = 0f..10f, steps = 9) Row(verticalAlignment = Alignment.CenterVertically) { Checkbox(checked = anonymous, onCheckedChange = { anonymous = it }) Text("Submit anonymously") } OutlinedTextField(value = comments, onValueChange = { comments = it }, label = { Text("Suggestions") }, modifier = Modifier.fillMaxWidth(), maxLines = 4) Button(onClick = { onSubmit(Feedback(rating, nps.toInt(), anonymous, comments)) }, modifier = Modifier.fillMaxWidth().padding(top = 16.dp)) { Text("Submit Feedback") } } } } ``` --- ## Compliance Reminders - **PHI encryption:** All message content is PHI. Encrypt at rest and in transit. Never log message body in plain-text server logs. - **Audit trail:** Log every send/receive, campaign dispatch, notification delivery, and feedback with user ID + timestamp. - **Session timeout:** Messaging auto-locks after 15 min inactivity. Re-authenticate to resume. - **External sharing:** PHI warning banner when sharing outside the system. - **Consent tracking:** Record patient opt-in/opt-out per channel with timestamp and method.