E11: WhatsApp-first operations — templates, copy buttons, and wa.me deep links - #105
Conversation
… S11.2) Co-authored-by: Taleef7 <89072337+Taleef7@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Implements Epic E11 by centralizing WhatsApp message templates and adding admin UI affordances to copy prefilled messages and open wa.me deep links across multiple admin pages.
Changes:
- Added a WhatsApp template library (
lib/whatsapp/templates.ts) and awa.melink builder (lib/whatsapp/buildLink.ts). - Introduced reusable UI components for copying messages and opening WhatsApp chats (
CopyMessageButton,WhatsAppLink). - Integrated WhatsApp actions/links into key admin surfaces (matches, payments, sessions, users, requests, tutors) and updated README status.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| package-lock.json | Lockfile churn (removes several "peer": true entries). |
| lib/whatsapp/templates.ts | Adds 14 lifecycle WhatsApp message templates. |
| lib/whatsapp/buildLink.ts | Adds buildWaLink() helper for generating wa.me URLs. |
| components/WhatsAppLink.tsx | New standalone “Open WhatsApp” link component with fallback UI. |
| components/CopyMessageButton.tsx | New client component to copy a message + optionally open a prefilled WhatsApp chat. |
| app/admin/users/page.tsx | Adds “Open chat” link next to users’ WhatsApp numbers. |
| app/admin/tutors/[id]/page.tsx | Adds “Open chat” link on tutor detail page. |
| app/admin/requests/[id]/page.tsx | Adds “Open chat” link on request detail page. |
| app/admin/payments/page.tsx | Adds WhatsApp copy/link actions for payment-confirmed + paybank templates. |
| app/admin/sessions/page.tsx | Adds WhatsApp copy actions per session (reminder, late-join, no-show, reschedule). |
| app/admin/matches/[id]/page.tsx | Adds WhatsApp copy/link actions for matched/reminder/availability-check messages. |
| README.md | Marks E11 WhatsApp features as completed and documents new modules/components. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * If message is provided, appends it as a URL-encoded ?text= parameter. | ||
| */ | ||
| export function buildWaLink(whatsappNumber: string, message?: string): string { | ||
| const digits = whatsappNumber.replace(/[^\d+]/g, '').replace(/^\+/, '') |
There was a problem hiding this comment.
buildWaLink is documented to strip all non-digits, but the current regex preserves '+' characters (except a leading one). This can generate an invalid wa.me/{number} path if the stored number contains '+' anywhere. Consider stripping everything except digits (e.g., keep only 0-9) and treating an empty result as “no number” (return null/empty or throw) so callers don’t render https://wa.me/ links.
| const digits = whatsappNumber.replace(/[^\d+]/g, '').replace(/^\+/, '') | |
| const digits = whatsappNumber.replace(/\D/g, '') | |
| if (!digits) return '' |
There was a problem hiding this comment.
Fixed in b138922. Changed to whatsappNumber.replace(/\D/g, '') to strip all non-digits (including any embedded +), and added an early return of '' when no digits remain so callers ({waHref && ...}, WhatsAppLink's !number guard) naturally suppress the link.
| await navigator.clipboard.writeText(message) | ||
| setCopied(true) | ||
| setTimeout(() => setCopied(false), 2000) |
There was a problem hiding this comment.
navigator.clipboard.writeText can throw/reject (non-HTTPS context, permissions denied, older browsers). Right now that rejection is unhandled, so the click may silently fail (or trigger an unhandled promise rejection). Wrap the call in try/catch and show a failure state or fallback copy mechanism when clipboard access isn’t available.
| await navigator.clipboard.writeText(message) | |
| setCopied(true) | |
| setTimeout(() => setCopied(false), 2000) | |
| try { | |
| if (navigator && 'clipboard' in navigator && navigator.clipboard?.writeText) { | |
| await navigator.clipboard.writeText(message) | |
| } else { | |
| // Fallback for older browsers or when Clipboard API is unavailable | |
| const textarea = document.createElement('textarea') | |
| textarea.value = message | |
| textarea.style.position = 'fixed' | |
| textarea.style.opacity = '0' | |
| document.body.appendChild(textarea) | |
| textarea.focus() | |
| textarea.select() | |
| try { | |
| document.execCommand('copy') | |
| } finally { | |
| document.body.removeChild(textarea) | |
| } | |
| } | |
| setCopied(true) | |
| setTimeout(() => setCopied(false), 2000) | |
| } catch (error) { | |
| console.error('Failed to copy message to clipboard:', error) | |
| setCopied(false) | |
| } |
There was a problem hiding this comment.
Fixed in b138922. handleCopy now wraps everything in try/catch. It checks for the Clipboard API first, falls back to a hidden textarea + execCommand('copy') for older browsers, and on any error logs to console and resets the copied state without showing a false success.
| const timeDisplay = formatSessionTime(session.scheduled_start_utc, adminTimezone) | ||
| const scheduleTz = match?.schedule_pattern?.timezone ?? adminTimezone | ||
| const requestId = match?.request_id ?? '' | ||
| const durationMins = match?.schedule_pattern?.duration_mins ?? 60 | ||
| const meetLink = match?.meet_link ?? '' |
There was a problem hiding this comment.
The WhatsApp templates include tz: scheduleTz, but timeDisplay is formatted using adminTimezone. This can produce messages like “Time: 7:00 PM (Asia/Karachi)” where the time is actually in a different timezone. Use scheduleTz when formatting the time for WhatsApp messages (and optionally keep a separate admin-time display for the card UI).
There was a problem hiding this comment.
Fixed in b138922. Added waTimeDisplay = formatSessionTime(session.scheduled_start_utc, scheduleTz) and used it in all WhatsApp templates (rem1h, lateJoin, studentNoShow) so the time in the message matches the timezone label shown to the student.
| const reschedConfirmedMsg = meetLink | ||
| ? templates.reschedConfirmed({ | ||
| day: '[Day]', | ||
| time: '[Time]', | ||
| tz: scheduleTz, | ||
| meetLink, | ||
| }) |
There was a problem hiding this comment.
reschedConfirmedMsg is built with placeholder values ([Day], [Time]), so the “Copy reschedule confirmed” button will copy an incomplete message. Either populate these fields from the actual reschedule data (if available) or remove/disable this button until the new day/time are known.
There was a problem hiding this comment.
Fixed in b138922. Removed the "Copy reschedule confirmed" button from the sessions page — the new day/time aren't available at session list render time, so sending a message with [Day]/[Time] placeholders would be harmful. The reschedule confirmed template remains available in lib/whatsapp/templates.ts for use in a dedicated reschedule confirmation flow later.
| const tutorAvailCheckMsg = templates.tutorAvailCheck({ | ||
| tutorName, | ||
| level: levelLabel, | ||
| subject: subjectName, | ||
| slot1: '[Day] [Time] [TZ]', | ||
| slot2: '[Day] [Time] [TZ]', | ||
| }) |
There was a problem hiding this comment.
The tutor availability check template is pre-filled with placeholder slots ([Day] [Time] [TZ]), which still requires manual editing before sending. If the goal is one-click “no manual typing,” consider passing real candidate slots (e.g., from request availability / schedule) or adding small UI inputs to set slot1/slot2 before copying.
There was a problem hiding this comment.
Updated in b138922. The slot placeholders are now [e.g. Mon 5:00 PM PKT] / [e.g. Wed 5:00 PM PKT] so it's immediately clear what format to use when editing before sending. Tutor name, level, and subject are still auto-filled from the DB.
| <CopyMessageButton | ||
| message={templates.paid({ subject: subjectName })} | ||
| whatsappNumber={profile?.whatsapp_number ?? undefined} | ||
| label="Copy payment confirmed" | ||
| /> | ||
| <CopyMessageButton | ||
| message={templates.paybank({ | ||
| accountTitle: PAYMENT_INSTRUCTIONS.accountTitle, | ||
| bank: PAYMENT_INSTRUCTIONS.bankName, | ||
| accountNumber: PAYMENT_INSTRUCTIONS.accountNumber, | ||
| studentName: profile?.display_name ?? '—', | ||
| level, | ||
| subject: subjectName, | ||
| })} |
There was a problem hiding this comment.
These WhatsApp templates are always generated even when subjectName fell back to '—' (e.g., missing subjects). That would send user-facing messages like “verified — teacher”. Consider guarding these buttons/messages when required data (subject/level/student name) is missing, or provide a clearer fallback message prompting admins to fix the underlying data first.
There was a problem hiding this comment.
Fixed in b138922. The WhatsApp buttons section is now guarded with profile?.display_name && subjectName !== '—', so buttons only appear when the student name and subject are both resolved from the DB. Also switched studentName: profile?.display_name ?? '—' to profile.display_name inside the guard (no nullish fallback needed).
|
@copilot apply changes based on the comments in this thread |
…k, timezone mismatch, remove placeholder reschedule button, guard payment templates Co-authored-by: Taleef7 <89072337+Taleef7@users.noreply.github.com>
Implements Epic E11 in full: all admin pages now have one-click WhatsApp message copy and direct chat open, eliminating manual template typing.
Summary
lib/whatsapp/templates.ts— 14 typed template functions covering the full OPS.md section 6 lifecycle (greeting → intake → packages → paybank → paid → tutorAvailCheck → matched → rem1h → reschedAck → reschedConfirmed → lateJoin → studentNoShow → tutorNoShow → renewalReminder). Each accepts only the variables it needs.lib/whatsapp/buildLink.ts—buildWaLink(number, message?)strips all non-digit characters (via/\D/g), returns''when no digits remain so callers suppress invalid links, buildshttps://wa.me/{digits}?text={encoded}.components/CopyMessageButton.tsx— client component; "📋 Copy message" with 2s ✅ Copied! toast + optional "💬 Open WhatsApp"wa.melink whenwhatsappNumberis supplied. Clipboard write is wrapped in try/catch with a hidden-textareaexecCommand('copy')fallback for older browsers; errors are logged without showing a false success state.components/WhatsAppLink.tsx— standalone "💬 Open WhatsApp" link; gracefulNo WhatsApp numberfallback.Changes
Admin page integrations
/admin/matches/[id][e.g. Mon 5:00 PM PKT]); Open chat links on student/tutor numbers/admin/payments/admin/sessionsscheduleTz(student/schedule timezone) so the time label matches the timezone shown. Reschedule confirmed button removed — new day/time are not available at list render time/admin/users/admin/requests/[id]/admin/tutors/[id]Testing
Notes
CopyMessageButtonreceives a fully rendered string as a prop.paybanktemplate pulls bank details fromPAYMENT_INSTRUCTIONSinlib/config/pricing.ts(fill in before launch).'—').tsc --noEmit); ESLint passes on all modified files.✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.