diff --git a/README.md b/README.md
index 795bb8c..48dfc4a 100644
--- a/README.md
+++ b/README.md
@@ -121,7 +121,7 @@ Open [http://localhost:3000](http://localhost:3000). You'll see the CorvEd landi
---
-### What the app can do right now (after E10)
+### What the app can do right now (after E11)
| Area | Status |
|---|---|
@@ -194,7 +194,16 @@ Open [http://localhost:3000](http://localhost:3000). You'll see the CorvEd landi
| **Tutor: sessions list** | ✅ `app/tutor/sessions/page.tsx` — upcoming and past sessions in tutor's timezone; student name, subject, Meet link; `SessionCompleteForm` inline on each upcoming session card |
| **Tutor: session completion form** | ✅ `components/dashboards/SessionCompleteForm.tsx` — radio buttons (Done / Student No-show / My No-show), notes textarea, calls `tutor_update_session` RPC via server action; error state; success state |
| **DB: increment_sessions_used guard** | ✅ `supabase/migrations/20260225000003_increment_sessions_used_guard.sql` — adds `sessions_used < sessions_total` safety guard to prevent `sessions_used` from exceeding `sessions_total` (over-incrementing); restricts direct RPC access to `service_role` only |
-| WhatsApp templates (E11) | 🚧 Coming in E11 |
+| WhatsApp templates (E11) | ✅ `lib/whatsapp/templates.ts` — 14 typed template functions (greeting, intake, packages, paybank, paid, tutorAvailCheck, matched, rem1h, reschedAck, reschedConfirmed, lateJoin, studentNoShow, tutorNoShow, renewalReminder) |
+| WhatsApp link builder (E11) | ✅ `lib/whatsapp/buildLink.ts` — `buildWaLink(number, message?)` strips non-digits, returns `wa.me/` URL with optional `?text=` parameter |
+| WhatsApp `CopyMessageButton` component (E11) | ✅ `components/CopyMessageButton.tsx` — "📋 Copy message" button with ✅ Copied! toast + optional "💬 Open WhatsApp" link to `wa.me` with pre-filled text |
+| WhatsApp `WhatsAppLink` component (E11) | ✅ `components/WhatsAppLink.tsx` — standalone "💬 Open WhatsApp" link; graceful fallback when number is absent |
+| Admin: WhatsApp actions on match detail (E11) | ✅ `/admin/matches/[id]` — "Copy matched message", "Copy 1-hour reminder (student/tutor)", "Copy tutor availability check" buttons; "Open chat" links next to student/tutor numbers |
+| Admin: WhatsApp actions on payments (E11) | ✅ `/admin/payments` — "Copy payment confirmed" and "Copy payment instructions" buttons + "Open chat" link per payment row |
+| Admin: WhatsApp actions on sessions (E11) | ✅ `/admin/sessions` — per session: "Copy 1-hour reminder", "Copy late join follow-up", "Copy student no-show notice", "Copy tutor no-show apology", "Copy reschedule confirmed" buttons |
+| Admin: WhatsApp link on users page (E11) | ✅ `/admin/users` — "Open chat" link next to each user's WhatsApp number |
+| Admin: WhatsApp link on request detail (E11) | ✅ `/admin/requests/[id]` — "Open chat" link next to student's WhatsApp number |
+| Admin: WhatsApp link on tutor detail (E11) | ✅ `/admin/tutors/[id]` — "Open chat" link next to tutor's WhatsApp number |
---
diff --git a/app/admin/matches/[id]/page.tsx b/app/admin/matches/[id]/page.tsx
index cdd1917..e76df56 100644
--- a/app/admin/matches/[id]/page.tsx
+++ b/app/admin/matches/[id]/page.tsx
@@ -1,5 +1,5 @@
-// E7 T7.4 S7.2 E8 T8.1: Admin match detail page — view match, reassign tutor, edit schedule, generate sessions
-// Closes #50 #46 #54
+// E7 T7.4 S7.2 E8 T8.1 E11 T11.2: Admin match detail page — view match, reassign tutor, edit schedule, generate sessions, WhatsApp actions
+// Closes #50 #46 #54 #75
export const dynamic = 'force-dynamic'
@@ -9,6 +9,9 @@ import { createAdminClient } from '@/lib/supabase/admin'
import { fetchApprovedTutors } from '@/lib/services/matching'
import { LEVEL_LABELS } from '@/lib/utils/request'
import { ReassignTutorForm, EditMatchForm, GenerateSessionsForm } from './MatchActions'
+import { CopyMessageButton } from '@/components/CopyMessageButton'
+import { WhatsAppLink } from '@/components/WhatsAppLink'
+import { templates } from '@/lib/whatsapp/templates'
const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
@@ -97,6 +100,46 @@ export default async function AdminMatchDetailPage({
? request.for_student_name
: (studentProfile?.display_name ?? '—')
+ const tutorName = tutorUserProfile?.display_name ?? '—'
+ const schedDays = schedule?.days?.length
+ ? schedule.days.map((d) => DAY_NAMES[d]).join(', ')
+ : ''
+ const schedTime = schedule?.time ?? ''
+ const schedTz = schedule?.timezone ?? ''
+ const meetLink = match.meet_link ?? ''
+
+ // Pre-built template strings for WhatsApp buttons
+ const matchedMsg =
+ schedDays && schedTime && schedTz && meetLink
+ ? templates.matched({
+ tutorName,
+ days: schedDays,
+ time: schedTime,
+ tz: schedTz,
+ meetLink,
+ })
+ : null
+
+ const rem1hStudentMsg =
+ schedTime && schedTz && meetLink
+ ? templates.rem1h({
+ level: levelLabel,
+ subject: subjectName,
+ tutorName,
+ time: schedTime,
+ tz: schedTz,
+ meetLink,
+ })
+ : null
+
+ const tutorAvailCheckMsg = templates.tutorAvailCheck({
+ tutorName,
+ level: levelLabel,
+ subject: subjectName,
+ slot1: '[e.g. Mon 5:00 PM PKT]',
+ slot2: '[e.g. Wed 5:00 PM PKT]',
+ })
+
const assignedDate = new Date(match.assigned_at).toLocaleDateString('en-GB', {
day: 'numeric',
month: 'long',
@@ -150,7 +193,10 @@ export default async function AdminMatchDetailPage({
Student
{studentName}
{studentProfile?.whatsapp_number && (
- 📱 {studentProfile.whatsapp_number}
+
+ 📱 {studentProfile.whatsapp_number}
+
+
)}
@@ -161,7 +207,10 @@ export default async function AdminMatchDetailPage({
{tutorProfile?.timezone}
{tutorUserProfile?.whatsapp_number && (
- 📱 {tutorUserProfile.whatsapp_number}
+
+ 📱 {tutorUserProfile.whatsapp_number}
+
+
)}
@@ -233,6 +282,60 @@ export default async function AdminMatchDetailPage({
)}
+ {/* WhatsApp Actions */}
+
+
+ WhatsApp Messages
+
+
+ {matchedMsg ? (
+
+
Match confirmed (to student)
+
+
+ ) : (
+
+ Set schedule and Meet link to enable match confirmation template.
+
+ )}
+
+ {rem1hStudentMsg && (
+
+
1-hour reminder (to student)
+
+
+ )}
+
+ {rem1hStudentMsg && (
+
+
1-hour reminder (to tutor)
+
+
+ )}
+
+
+
Tutor availability check
+
+
+
+
+
{/* Audit info */}
Match ID: {match.id} · Last updated:{' '}
diff --git a/app/admin/payments/page.tsx b/app/admin/payments/page.tsx
index ed9800d..218b1d5 100644
--- a/app/admin/payments/page.tsx
+++ b/app/admin/payments/page.tsx
@@ -1,5 +1,5 @@
-// E5 T5.3 S5.2: Admin payments list — view, mark paid, mark rejected
-// Closes #35 #32
+// E5 T5.3 S5.2 E11 T11.2 T11.3: Admin payments list — view, mark paid, mark rejected, WhatsApp actions
+// Closes #35 #32 #75 #76
export const dynamic = 'force-dynamic'
@@ -7,6 +7,10 @@ import { createAdminClient } from '@/lib/supabase/admin'
import { LEVEL_LABELS } from '@/lib/utils/request'
import { MarkPaidForm, RejectForm } from './PaymentActions'
import Link from 'next/link'
+import { CopyMessageButton } from '@/components/CopyMessageButton'
+import { WhatsAppLink } from '@/components/WhatsAppLink'
+import { templates } from '@/lib/whatsapp/templates'
+import { PAYMENT_INSTRUCTIONS } from '@/lib/config/pricing'
const STATUS_COLOURS: Record = {
pending: 'bg-yellow-100 text-yellow-800',
@@ -170,6 +174,30 @@ export default async function AdminPaymentsPage({
)}
+
+ {/* WhatsApp message buttons — only when student and subject data are available */}
+ {profile?.display_name && subjectName !== '—' && (
+
+
+
+
+
+ )}
)
})}
diff --git a/app/admin/requests/[id]/page.tsx b/app/admin/requests/[id]/page.tsx
index f55bba9..3eca557 100644
--- a/app/admin/requests/[id]/page.tsx
+++ b/app/admin/requests/[id]/page.tsx
@@ -1,5 +1,5 @@
-// E7 T7.2: Admin request detail + matching screen
-// Closes #48
+// E7 T7.2 E11 T11.3: Admin request detail + matching screen + WhatsApp link
+// Closes #48 #76
export const dynamic = 'force-dynamic'
@@ -9,6 +9,7 @@ import { createAdminClient } from '@/lib/supabase/admin'
import { fetchApprovedTutors } from '@/lib/services/matching'
import { STATUS_COLOURS, STATUS_LABELS, LEVEL_LABELS } from '@/lib/utils/request'
import { AssignTutorForm } from './AssignTutorForm'
+import { WhatsAppLink } from '@/components/WhatsAppLink'
const EXAM_BOARD_LABELS: Record = {
cambridge: 'Cambridge',
@@ -160,7 +161,10 @@ export default async function AdminRequestDetailPage({
)}
{profile?.whatsapp_number && (
- 📱 {profile.whatsapp_number}
+
+ 📱 {profile.whatsapp_number}
+
+
)}
diff --git a/app/admin/sessions/page.tsx b/app/admin/sessions/page.tsx
index bbb19a3..2c2ecf2 100644
--- a/app/admin/sessions/page.tsx
+++ b/app/admin/sessions/page.tsx
@@ -1,5 +1,5 @@
-// E8 T8.4 S8.2: Admin sessions overview — list all sessions, update status, reschedule
-// Closes #57 #53
+// E8 T8.4 S8.2 E11 T11.2: Admin sessions overview — list all sessions, update status, reschedule, WhatsApp actions
+// Closes #57 #53 #75
export const dynamic = 'force-dynamic'
@@ -7,6 +7,8 @@ import { createAdminClient } from '@/lib/supabase/admin'
import { SESSION_STATUS_LABELS, SESSION_STATUS_COLOURS, formatSessionTime, type SessionStatus } from '@/lib/utils/session'
import { SessionStatusForm, RescheduleForm } from './SessionActions'
import Link from 'next/link'
+import { CopyMessageButton } from '@/components/CopyMessageButton'
+import { templates } from '@/lib/whatsapp/templates'
const ADMIN_TIMEZONE = 'Asia/Karachi'
@@ -21,15 +23,15 @@ type SessionRow = {
meet_link: string | null
request_id: string
tutor_user_id: string
- schedule_pattern: { duration_mins?: number } | null
+ schedule_pattern: { duration_mins?: number; timezone?: string } | null
tutor_profiles: {
- user_profiles: { display_name: string } | null
+ user_profiles: { display_name: string; whatsapp_number: string | null } | null
} | null
requests: {
id: string
level: string
subjects: { name: string } | null
- user_profiles: { display_name: string } | null
+ user_profiles: { display_name: string; whatsapp_number: string | null } | null
} | null
} | null
}
@@ -44,12 +46,12 @@ export default async function AdminSessionsPage() {
matches!sessions_match_id_fkey (
meet_link, request_id, tutor_user_id, schedule_pattern,
tutor_profiles!matches_tutor_user_id_fkey (
- user_profiles!tutor_user_id ( display_name )
+ user_profiles!tutor_user_id ( display_name, whatsapp_number )
),
requests!matches_request_id_fkey (
id, level,
subjects ( name ),
- user_profiles!requests_created_by_user_id_fkey ( display_name )
+ user_profiles!requests_created_by_user_id_fkey ( display_name, whatsapp_number )
)
)`
)
@@ -140,12 +142,42 @@ function SessionCard({
const tutorProfile = match?.tutor_profiles
const studentName =
(req?.user_profiles as { display_name: string } | null)?.display_name ?? '—'
+ const studentWhatsApp =
+ (req?.user_profiles as { display_name: string; whatsapp_number: string | null } | null)
+ ?.whatsapp_number ?? null
const tutorName =
(tutorProfile?.user_profiles as { display_name: string } | null)?.display_name ?? '—'
+ const tutorWhatsApp =
+ (tutorProfile?.user_profiles as { display_name: string; whatsapp_number: string | null } | null)
+ ?.whatsapp_number ?? null
const subjectName = (req?.subjects as { name: string } | null)?.name ?? '—'
+ const levelLabel = req?.level ?? ''
const timeDisplay = formatSessionTime(session.scheduled_start_utc, adminTimezone)
+ const scheduleTz = match?.schedule_pattern?.timezone ?? adminTimezone
+ // Format session time in the schedule/student timezone for WhatsApp messages
+ const waTimeDisplay = formatSessionTime(session.scheduled_start_utc, scheduleTz)
const requestId = match?.request_id ?? ''
const durationMins = match?.schedule_pattern?.duration_mins ?? 60
+ const meetLink = match?.meet_link ?? ''
+
+ // Template strings for WhatsApp buttons (use waTimeDisplay so tz label matches the time)
+ const rem1hMsg = meetLink
+ ? templates.rem1h({
+ level: levelLabel,
+ subject: subjectName,
+ tutorName,
+ time: waTimeDisplay,
+ tz: scheduleTz,
+ meetLink,
+ })
+ : null
+
+ const lateJoinMsg = meetLink
+ ? templates.lateJoin({ name: studentName, time: waTimeDisplay, meetLink })
+ : null
+
+ const studentNoShowMsg = templates.studentNoShow({ name: studentName, time: waTimeDisplay })
+ const tutorNoShowMsg = templates.tutorNoShow({ name: studentName })
return (
@@ -192,6 +224,41 @@ function SessionCard({
/>
)}
+
+ {/* WhatsApp message buttons */}
+
+ {rem1hMsg && (
+
+ )}
+ {lateJoinMsg && (
+
+ )}
+
+
+ {tutorWhatsApp && rem1hMsg && (
+
+ )}
+
)
}
diff --git a/app/admin/tutors/[id]/page.tsx b/app/admin/tutors/[id]/page.tsx
index 259a601..a031bf2 100644
--- a/app/admin/tutors/[id]/page.tsx
+++ b/app/admin/tutors/[id]/page.tsx
@@ -1,5 +1,5 @@
-// E6 T6.3 T6.4 S6.2: Admin tutor detail page — full profile view with approve/revoke
-// Closes #42 #43 #39
+// E6 T6.3 T6.4 S6.2 E11 T11.3: Admin tutor detail page — full profile view with approve/revoke + WhatsApp link
+// Closes #42 #43 #39 #76
export const dynamic = 'force-dynamic'
@@ -7,6 +7,7 @@ import { notFound } from 'next/navigation'
import Link from 'next/link'
import { createAdminClient } from '@/lib/supabase/admin'
import { ApproveButton, RevokeButton } from '../TutorActions'
+import { WhatsAppLink } from '@/components/WhatsAppLink'
const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
@@ -118,7 +119,12 @@ export default async function AdminTutorDetailPage({
WhatsApp:{' '}
- {profile?.whatsapp_number ?? (
+ {profile?.whatsapp_number ? (
+
+ {profile.whatsapp_number}
+
+
+ ) : (
not provided
)}
diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx
index e2c353a..76bcd11 100644
--- a/app/admin/users/page.tsx
+++ b/app/admin/users/page.tsx
@@ -1,8 +1,9 @@
-// E3 T3.4: admin user role management screen
-// Closes #19 #23
+// E3 T3.4 E11 T11.3: admin user role management screen + WhatsApp links
+// Closes #19 #23 #76
import { createAdminClient } from '@/lib/supabase/admin'
import { assignRole, removeRole, setPrimaryRole } from '../actions'
+import { WhatsAppLink } from '@/components/WhatsAppLink'
const ALL_ROLES = ['student', 'parent', 'tutor', 'admin'] as const
type Role = (typeof ALL_ROLES)[number]
@@ -128,7 +129,16 @@ export default async function AdminUsersPage() {
- {user.whatsapp_number ?? '—'}
+
+ {user.whatsapp_number ? (
+ <>
+ {user.whatsapp_number}
+
+ >
+ ) : (
+ '—'
+ )}
+
|
diff --git a/components/CopyMessageButton.tsx b/components/CopyMessageButton.tsx
new file mode 100644
index 0000000..d85344e
--- /dev/null
+++ b/components/CopyMessageButton.tsx
@@ -0,0 +1,71 @@
+'use client'
+
+// T11.2 / S11.2: "Copy message" and optional "Open WhatsApp" button component
+// Closes #75 #73
+
+import { useState } from 'react'
+import { buildWaLink } from '@/lib/whatsapp/buildLink'
+
+interface CopyMessageButtonProps {
+ message: string
+ whatsappNumber?: string
+ label?: string
+}
+
+export function CopyMessageButton({ message, whatsappNumber, label }: CopyMessageButtonProps) {
+ const [copied, setCopied] = useState(false)
+
+ async function handleCopy() {
+ 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)
+ }
+ }
+
+ const waHref = whatsappNumber ? buildWaLink(whatsappNumber, message) : null
+
+ return (
+
+
+
+ {waHref && (
+
+ 💬 Open WhatsApp
+
+ )}
+
+ )
+}
diff --git a/components/WhatsAppLink.tsx b/components/WhatsAppLink.tsx
new file mode 100644
index 0000000..f1c0e33
--- /dev/null
+++ b/components/WhatsAppLink.tsx
@@ -0,0 +1,30 @@
+// T11.3: Standalone "Open WhatsApp" link component
+// Closes #76
+
+import { buildWaLink } from '@/lib/whatsapp/buildLink'
+
+interface WhatsAppLinkProps {
+ number: string | null | undefined
+ message?: string
+ label?: string
+}
+
+export function WhatsAppLink({ number, message, label = 'Open WhatsApp' }: WhatsAppLinkProps) {
+ if (!number) {
+ return No WhatsApp number
+ }
+
+ const href = buildWaLink(number, message)
+
+ return (
+
+ 💬 {label}
+
+ )
+}
diff --git a/lib/whatsapp/buildLink.ts b/lib/whatsapp/buildLink.ts
new file mode 100644
index 0000000..19cc255
--- /dev/null
+++ b/lib/whatsapp/buildLink.ts
@@ -0,0 +1,16 @@
+// T11.3: wa.me deep link builder utility
+// Closes #76
+
+/**
+ * Builds a wa.me WhatsApp deep link for the given number and optional pre-filled message.
+ * Strips all non-digit characters from the number.
+ * Returns an empty string if no digits remain (caller should treat this as "no link").
+ * 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, '')
+ if (!digits) return ''
+ const base = `https://wa.me/${digits}`
+ if (!message) return base
+ return `${base}?text=${encodeURIComponent(message)}`
+}
diff --git a/lib/whatsapp/templates.ts b/lib/whatsapp/templates.ts
new file mode 100644
index 0000000..70344cf
--- /dev/null
+++ b/lib/whatsapp/templates.ts
@@ -0,0 +1,129 @@
+// T11.1: All 14 standard WhatsApp message templates from docs/OPS.md section 6
+// Closes #74
+
+export const templates = {
+ // 6.1 Greeting (auto-greeting in WhatsApp Business)
+ greeting: () =>
+ `Hello! Welcome to CorvEd 👋\n` +
+ `We provide 1:1 online tutoring for O Levels and A Levels with verified teachers.\n` +
+ `To get started, please share:\n` +
+ `1) Student or Parent?\n2) Level (O / A)\n3) Subject\n` +
+ `4) City + Timezone\n5) Availability (days + times)\n6) Your goal (exam date or weak areas)`,
+
+ // 6.2 Lead intake
+ intake: () =>
+ `To match you with the right teacher, please reply with:\n` +
+ `1) Student or Parent?\n` +
+ `2) Level: O Levels or A Levels\n` +
+ `3) Subject: Math/Physics/Chemistry/Biology/English/CS/Pak Studies/Islamiyat/Urdu\n` +
+ `4) Exam board (Cambridge/Edexcel/Other) (optional)\n` +
+ `5) Your availability (days + time windows) + your timezone\n` +
+ `6) Goal (target grade, weak topics, exam date)`,
+
+ // 6.3 Package options
+ packages: () =>
+ `We offer monthly packages per subject (60-minute sessions):\n` +
+ `- 8 sessions/month (~2x per week)\n` +
+ `- 12 sessions/month (~3x per week)\n` +
+ `- 20 sessions/month (~5x per week)\n\n` +
+ `Share your preferred package and we'll send payment details.`,
+
+ // 6.4 Payment instructions
+ paybank: (p: {
+ accountTitle: string
+ bank: string
+ accountNumber: string
+ studentName: string
+ level: string
+ subject: string
+ }) =>
+ `Bank transfer details:\n` +
+ `Account Title: ${p.accountTitle}\n` +
+ `Bank: ${p.bank}\n` +
+ `Account/IBAN: ${p.accountNumber}\n\n` +
+ `Reference: CorvEd | ${p.studentName} | ${p.level} ${p.subject}\n\n` +
+ `After payment, send a screenshot or transaction reference and we'll confirm.`,
+
+ // 6.5 Payment confirmed
+ paid: (p: { subject: string }) =>
+ `Payment received ✅ Thank you.\n` +
+ `Next step: we'll match you with a verified ${p.subject} teacher and confirm your schedule shortly.\n\n` +
+ `To finalize scheduling, please confirm:\n- preferred days/times (with timezone)\n- start date (if any preference)`,
+
+ // 6.6 Tutor availability check
+ tutorAvailCheck: (p: {
+ tutorName: string
+ level: string
+ subject: string
+ slot1: string
+ slot2: string
+ }) =>
+ `Hi ${p.tutorName}, I hope you're well.\n` +
+ `We have a new ${p.level} ${p.subject} student. Are you available for:\n` +
+ `Option 1: ${p.slot1}\nOption 2: ${p.slot2}\n\n` +
+ `If yes, please confirm which option works. If not, share 2–3 available slots.`,
+
+ // 6.7 Match confirmed to student
+ matched: (p: {
+ tutorName: string
+ days: string
+ time: string
+ tz: string
+ meetLink: string
+ }) =>
+ `You're matched ✅\n` +
+ `Teacher: ${p.tutorName}\n` +
+ `Schedule: ${p.days} at ${p.time} (${p.tz})\n` +
+ `Session duration: 60 minutes\n` +
+ `Google Meet link (recurring): ${p.meetLink}\n\n` +
+ `Reschedule policy: please request reschedules at least 24 hours before the class time.`,
+
+ // 6.8 1-hour reminder
+ rem1h: (p: {
+ level: string
+ subject: string
+ tutorName: string
+ time: string
+ tz: string
+ meetLink: string
+ }) =>
+ `Reminder ⏰ Your class starts in 1 hour:\n` +
+ `${p.level} ${p.subject} with ${p.tutorName}\n` +
+ `Time: ${p.time} (${p.tz})\n` +
+ `Meet link: ${p.meetLink}`,
+
+ // 6.9 Reschedule request acknowledgement
+ reschedAck: () =>
+ `Got it — I can help you reschedule.\n` +
+ `Please share 2–3 alternate time slots (days + times + your timezone).\n` +
+ `Note: reschedules are allowed if requested at least 24 hours before class.`,
+
+ // 6.10 Reschedule confirmed
+ reschedConfirmed: (p: { day: string; time: string; tz: string; meetLink: string }) =>
+ `Reschedule confirmed ✅\n` +
+ `New time: ${p.day} at ${p.time} (${p.tz})\n` +
+ `Meet link (same): ${p.meetLink}`,
+
+ // 6.11 Late join follow-up
+ lateJoin: (p: { name: string; time: string; meetLink: string }) =>
+ `Hi ${p.name}, your class started at ${p.time}. Are you joining?\nMeet link: ${p.meetLink}`,
+
+ // 6.12 Student no-show policy notice
+ studentNoShow: (p: { name: string; time: string }) =>
+ `Hi ${p.name}, we waited 10 minutes and couldn't connect today.\n` +
+ `As per our policy, a no-show counts as a used session.\n` +
+ `If you'd like, share your availability and we'll continue with the remaining sessions.`,
+
+ // 6.13 Tutor no-show apology
+ tutorNoShow: (p: { name: string }) =>
+ `Hi ${p.name}, we're sorry — the teacher could not join today.\n` +
+ `This session will not be deducted.\n` +
+ `Please share 2–3 alternate slots and we'll reschedule immediately.`,
+
+ // 6.14 Renewal reminder
+ renewalReminder: (p: { subject: string }) =>
+ `Your monthly package is ending soon.\n` +
+ `Would you like to renew for next month for ${p.subject}?\n\n` +
+ `Packages:\n- 8 sessions\n- 12 sessions\n- 20 sessions\n\n` +
+ `Reply with your package choice and we'll share payment details.`,
+}
diff --git a/package-lock.json b/package-lock.json
index 88727f3..8d26359 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -76,7 +76,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1355,7 +1354,6 @@
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.97.0.tgz",
"integrity": "sha512-kTD91rZNO4LvRUHv4x3/4hNmsEd2ofkYhuba2VMUPRVef1RCmnHtm7rIws38Fg0yQnOSZOplQzafn0GSiy6GVg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@supabase/auth-js": "2.97.0",
"@supabase/functions-js": "2.97.0",
@@ -1706,7 +1704,6 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1775,7 +1772,6 @@
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.56.0",
"@typescript-eslint/types": "8.56.0",
@@ -2301,7 +2297,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2672,7 +2667,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3283,7 +3277,6 @@
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3469,7 +3462,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -5819,7 +5811,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5829,7 +5820,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -5842,7 +5832,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -6611,7 +6600,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -6774,7 +6762,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -7093,7 +7080,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
|