diff --git a/README.md b/README.md index 48dfc4a..3bc1365 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,93 @@ Open [http://localhost:3000](http://localhost:3000). You'll see the CorvEd landi --- -### What the app can do right now (after E11) +### What the app can do right now (after E12) + +| Area | Status | +|---|---| +| Landing page at `/` | ✅ Full landing page with hero, how it works, subjects, packages, policies, intake form, FAQ, footer | +| Intake / lead capture form | ✅ React Hook Form + Zod — works without login; saves to Supabase `leads` table | +| WhatsApp CTA button | ✅ `wa.me` deep link with prefilled message (requires `NEXT_PUBLIC_WHATSAPP_NUMBER` env var) | +| `POST /api/leads` route | ✅ Server-side validation + Supabase insert via admin client | +| `leads` DB migration | ✅ `supabase/migrations/20260223000001_create_leads_table.sql` — RLS: anon insert allowed, auth read/update | +| Supabase clients wired up | ✅ `lib/supabase/client.ts`, `server.ts`, `admin.ts` | +| **Auth: sign up (email/password)** | ✅ `app/auth/sign-up/page.tsx` — display name, email, password, timezone; min 8-char password | +| **Auth: email verification** | ✅ `app/auth/verify/page.tsx` — instructions page; unverified users cannot reach dashboard | +| **Auth: sign in (email/password)** | ✅ `app/auth/sign-in/page.tsx` — generic error message (no email enumeration) | +| **Auth: Google OAuth** | ✅ Sign-in + sign-up pages both have "Sign in with Google" button | +| **Auth: callback handler** | ✅ `app/auth/callback/route.ts` — PKCE code exchange; redirects to profile-setup if profile incomplete | +| **Auth: profile setup** | ✅ `app/auth/profile-setup/page.tsx` — display name, WhatsApp number (auto-normalized), timezone (auto-detected) | +| **Auth: sign out** | ✅ `app/auth/sign-out/route.ts` — POST clears session, redirects to sign-in | +| **Route protection (middleware)** | ✅ `middleware.ts` — unauthenticated → sign-in for `/dashboard`, `/tutor`, `/admin`; authenticated → dashboard for auth pages | +| **Role-aware dashboard redirect** | ✅ `app/dashboard/page.tsx` — admin→`/admin`, tutor→`/tutor`, student/parent stays on dashboard | +| **Admin route protection** | ✅ `app/admin/layout.tsx` — verifies `admin` role server-side; non-admins → `/dashboard` | +| **Tutor route protection** | ✅ `app/tutor/layout.tsx` — verifies `tutor` or `admin` role; others → `/dashboard` | +| **Admin: user management screen** | ✅ `app/admin/users/page.tsx` — lists all users, shows roles, assign/remove roles, set primary role | +| **DB: enum types** | ✅ `supabase/migrations/20260223000002_create_enums.sql` — all 8 MVP enum types | +| **DB: subjects table** | ✅ `supabase/migrations/20260223000003_create_subjects.sql` — 9 MVP subjects seeded | +| **DB: user_profiles + user_roles** | ✅ `supabase/migrations/20260223000004_create_user_profiles.sql` — tables, helper functions, trigger, RLS | +| **DB: handle_new_user() trigger** | ✅ Auto-creates profile + `student` role on every signup | +| **DB: helper functions** | ✅ `has_role()`, `is_admin()`, `is_tutor()` — used in RLS policies | +| **DB: leads admin RLS** | ✅ `supabase/migrations/20260223000005_leads_admin_rls.sql` — admin-role users can read/update leads | +| **Student dashboard — next session card** | ✅ `app/dashboard/page.tsx` — next upcoming session with time (student's TZ), tutor name, Meet link, Reschedule button; empty state if no sessions yet | +| **Student dashboard — requests + packages** | ✅ `app/dashboard/page.tsx` — lists all requests with status badges; "New Request" CTA; package summary cards per request | +| **Tutoring request form** | ✅ `app/dashboard/requests/new/page.tsx` — React Hook Form + Zod; level, subject (from DB), exam board, availability, timezone (pre-filled), goals, preferred start date; duplicate request warning | +| **Request confirmation page** | ✅ `app/dashboard/requests/[id]/page.tsx` — read-only summary, status badge, status-aware "what's next" banner, "Select Package" CTA (links with requestId) | +| **DB: requests table + RLS** | ✅ `supabase/migrations/20260223000007_create_requests_table.sql` — full schema, indexes, updated_at trigger, 4 RLS policies (insert self, select creator/admin, update creator limited, admin update) | +| **Request status utilities** | ✅ `lib/utils/request.ts` — `STATUS_LABELS` + `STATUS_COLOURS` for all 7 request statuses | +| **Request Zod schema** | ✅ `lib/validators/request.ts` — validates all request fields | +| **Package selection page** | ✅ `app/dashboard/packages/new/page.tsx` — 3 package tier cards (8/12/20 sessions), PKR pricing, policy notes, creates package + payment rows, advances request to `payment_pending` | +| **Package payment page** | ✅ `app/dashboard/packages/[id]/page.tsx` — bank transfer instructions with personalised reference, optional proof upload (Supabase Storage), optional transaction reference, payment status display | +| **Package summary card** | ✅ `components/dashboards/PackageSummary.tsx` — shows package tier, month window, sessions remaining, progress bar; handles pending/active/expired states; renewal alert (≤3 sessions or ≤5 days to end) with WhatsApp "Chat to Renew" link | +| **Admin: payments list** | ✅ `app/admin/payments/page.tsx` — lists payments with filter (pending/paid/rejected/all), student name, subject, tier, amount, date, proof indicator | +| **Admin: mark payment paid** | ✅ Updates `payments.status → paid`, `packages.status → active`, `requests.status → ready_to_match`, writes audit log | +| **Admin: mark payment rejected** | ✅ Updates `payments.status → rejected` with optional rejection note, writes audit log | +| **DB: packages + payments tables** | ✅ `supabase/migrations/20260224000001_create_packages_payments.sql` — packages (tier_sessions 8/12/20, start/end date, sessions_total/used, status), payments (amount_pkr, method, reference, proof_path, rejection_note, verified_by/at), audit_logs; all with RLS | +| **Pricing config** | ✅ `lib/config/pricing.ts` — `PACKAGES` array (8/12/20 tiers, PKR prices, typicalFrequency) + `PAYMENT_INSTRUCTIONS` (bank details, reference format) | +| **Tutor application form** | ✅ `app/tutor/profile/page.tsx` — tutor can fill in bio, timezone, subjects × levels (O/A checkboxes), weekly availability grid; saves to `tutor_profiles`, `tutor_subjects`, `tutor_availability`; shows pending/approved status badge | +| **Admin: tutor directory** | ✅ `app/admin/tutors/page.tsx` — lists all tutors with status, subjects, levels, timezone; filter by status (pending/approved), subject, level; Approve and Revoke buttons | +| **Admin: tutor detail page** | ✅ `app/admin/tutors/[id]/page.tsx` — full tutor profile including bio, all subjects × levels, availability windows, WhatsApp number; approve/revoke controls | +| **Tutor approval workflow** | ✅ `app/admin/tutors/actions.ts` — `approveTutor` sets `approved = true`; `revokeTutorApproval` sets `approved = false`; both write audit log entries | +| **DB: tutor tables** | ✅ `supabase/migrations/20260224000002_create_tutor_tables.sql` — `tutor_profiles` (approved, bio, timezone), `tutor_subjects` (subject_id × level per tutor), `tutor_availability` (JSONB windows array, updated_at trigger); RLS policies for all three tables | +| **Tutor Zod schema** | ✅ `lib/validators/tutor.ts` — validates bio (min 50 chars), timezone, subjects array, availability windows | +| **Matching query helper** | ✅ `lib/services/matching.ts` — `fetchApprovedTutors()` correctly filters approved tutors by subject × level (same row); used in E7 matching screen | +| **Admin: requests inbox** | ✅ `app/admin/requests/page.tsx` — filterable list (status tabs, subject/level selects); priority sort (`ready_to_match` first); status badges; "Match →" CTA for actionable requests | +| **Admin: matching screen** | ✅ `app/admin/requests/[id]/page.tsx` — two-panel layout: request details (all fields) + eligible approved tutor cards filtered by subject × level; `AssignTutorForm` client component with Meet link + schedule fields | +| **Admin: assign tutor** | ✅ `assignTutor` server action — creates `matches` row (status=matched, optional meet_link + schedule_pattern), advances `requests.status → matched`, writes audit log | +| **Admin: matches list** | ✅ `app/admin/matches/page.tsx` — lists all matches with student, tutor, subject/level, status, meet link, assigned date; links to match detail | +| **Admin: match detail** | ✅ `app/admin/matches/[id]/page.tsx` — full match record; edit meet link + schedule pattern; reassign tutor with optional reason | +| **Admin: reassign tutor** | ✅ `reassignTutor` server action — updates `matches.tutor_user_id`, writes audit log with old/new tutor IDs + reason; RLS automatically updates session access | +| **Admin: update match details** | ✅ `updateMatchDetails` server action — updates meet_link and schedule_pattern on existing match; writes audit log | +| **DB: matches table + RLS** | ✅ `supabase/migrations/20260225000001_create_matches_table.sql` — matches table (request_id unique FK, tutor_user_id, status enum, meet_link, schedule_pattern JSONB, assigned_by/at); RLS: admin full access; tutor + request creator can select | +| **Admin: session generation** | ✅ `GenerateSessionsForm` on match detail page — creates N sessions from schedule_pattern + active package; advances match + request to `active`; writes audit log | +| **Admin: sessions overview** | ✅ `app/admin/sessions/page.tsx` — lists all sessions grouped by upcoming/past; shows student, tutor, subject, time (PKT), status badge, Meet link | +| **Admin: session status update** | ✅ `SessionStatusForm` — admin can mark sessions done/no-show-student/no-show-tutor; increments `packages.sessions_used` atomically via `increment_sessions_used` RPC | +| **Admin: reschedule session** | ✅ `RescheduleForm` — admin sets new date+time (in admin timezone, converted to UTC); sets status to rescheduled; writes audit log; shows ⚠ warning if within 24 hours | +| **Student: sessions list** | ✅ `app/dashboard/sessions/page.tsx` — next upcoming session card with Meet link + Reschedule button; full list of upcoming + past sessions in student's timezone; status badges; tutor notes; "Reschedule via WhatsApp" on each upcoming session | +| **Student: reschedule via WhatsApp** | ✅ `components/dashboards/RescheduleButton.tsx` — prefilled WhatsApp message with subject, level, current session time (student TZ); 24-hour late-reschedule warning | +| **Tutor: sessions list** | ✅ `app/tutor/sessions/page.tsx` — upcoming and past sessions in tutor's timezone; student name, subject, Meet link; inline session status update form | +| **Session generation algorithm** | ✅ `lib/services/scheduling.ts` — `generateSessions()` using luxon; iterates days, converts local time → UTC; stops at tier_sessions limit | +| **Session utilities** | ✅ `lib/utils/session.ts` — `SESSION_STATUS_LABELS`, `SESSION_STATUS_COLOURS`, `formatSessionTime()` (Intl.DateTimeFormat in viewer's timezone) | +| **Session server actions** | ✅ `lib/services/sessions.ts` — `generateSessionsForMatch`, `updateSessionStatus`, `rescheduleSession` | +| **DB: sessions table + RLS** | ✅ `supabase/migrations/20260225000002_create_sessions_table.sql` — sessions table (match_id FK, scheduled_start/end_utc, status enum, tutor_notes); 4 RLS policies (admin all, tutor select, student select, tutor update); `increment_sessions_used` RPC; `tutor_update_session` RPC | +| **Tutor: next session card on dashboard** | ✅ `app/tutor/page.tsx` — full tutor dashboard: next session card with student name, subject, level, date/time (tutor's TZ), Meet link; session counts (upcoming/completed); quick links to sessions + profile; empty state when no sessions yet | +| **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) | ✅ `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 | +| **Policies page (E12 T12.1)** | ✅ `app/policies/page.tsx` — public page at `/policies`; covers reschedule (24h cutoff, exceptions), no-show (student/tutor/late-join), refund/expiry (no carryover, admin discretion), package terms (per subject, 60 min, assigned tutor), privacy; linked from landing page footer | +| **Tutor code of conduct (E12 T12.2)** | ✅ `app/tutor/conduct/page.tsx` — public page at `/tutor/conduct`; covers punctuality, session quality, communication, privacy, quality expectations, incidents; acknowledgement checkbox added to tutor profile/application form (required before submit) | +| **Admin: audit log (E12 T12.3)** | ✅ `app/admin/audit/page.tsx` — admin-only; shows recent 200 audit events newest-first; human-readable action labels; actor name, entity type/ID (truncated), details; uses `audit_logs` table (created in E5 migration) | +| **Admin: analytics dashboard (E12 T12.4)** | ✅ `app/admin/analytics/page.tsx` — admin-only; 7 metric cards: active students, active tutors, upcoming sessions (next 7d), missed sessions (last 7d), unmarked sessions (needs follow-up), pending payments, pending tutor approvals; attention metrics highlighted in amber/orange; clickable cards link to relevant admin pages | | Area | Status | |---|---| diff --git a/app/admin/analytics/page.tsx b/app/admin/analytics/page.tsx new file mode 100644 index 0000000..4fc4f42 --- /dev/null +++ b/app/admin/analytics/page.tsx @@ -0,0 +1,235 @@ +// E12 T12.4: Admin analytics dashboard — active students, upcoming/missed sessions, pending items +// Closes #81 + +export const dynamic = 'force-dynamic' + +import Link from 'next/link' +import { createAdminClient } from '@/lib/supabase/admin' + +export default async function AdminAnalyticsPage() { + const admin = createAdminClient() + const now = new Date() + const plus7 = new Date(now.getTime() + 7 * 86400000).toISOString() + const minus7 = new Date(now.getTime() - 7 * 86400000).toISOString() + const nowIso = now.toISOString() + + const [ + activeStudents, + activeTutors, + upcomingSessions, + missedSessions, + unmarkedSessions, + pendingPayments, + pendingTutors, + ] = await Promise.all([ + // Active students: requests with status = 'active' + admin.from('requests').select('id', { count: 'exact', head: true }).eq('status', 'active'), + // Active tutors: tutor_profiles with approved = true + admin + .from('tutor_profiles') + .select('tutor_user_id', { count: 'exact', head: true }) + .eq('approved', true), + // Upcoming sessions: scheduled in the next 7 days + admin + .from('sessions') + .select('id', { count: 'exact', head: true }) + .eq('status', 'scheduled') + .gte('scheduled_start_utc', nowIso) + .lte('scheduled_start_utc', plus7), + // Missed sessions (last 7 days): no-show by student or tutor + admin + .from('sessions') + .select('id', { count: 'exact', head: true }) + .in('status', ['no_show_student', 'no_show_tutor']) + .gte('scheduled_start_utc', minus7) + .lte('scheduled_start_utc', nowIso), + // Sessions not marked yet: scheduled but start time has passed + admin + .from('sessions') + .select('id', { count: 'exact', head: true }) + .eq('status', 'scheduled') + .lt('scheduled_start_utc', nowIso), + // Payments pending verification + admin.from('payments').select('id', { count: 'exact', head: true }).eq('status', 'pending'), + // Tutor applications pending approval + admin + .from('tutor_profiles') + .select('tutor_user_id', { count: 'exact', head: true }) + .eq('approved', false), + ]) + + const firstError = + activeStudents.error || + activeTutors.error || + upcomingSessions.error || + missedSessions.error || + unmarkedSessions.error || + pendingPayments.error || + pendingTutors.error + + if (firstError) { + throw new Error(`Failed to load analytics metrics: ${firstError.message}`) + } + + const metrics = { + activeStudents: activeStudents.count ?? 0, + activeTutors: activeTutors.count ?? 0, + upcomingSessions: upcomingSessions.count ?? 0, + missedSessions: missedSessions.count ?? 0, + unmarkedSessions: unmarkedSessions.count ?? 0, + pendingPayments: pendingPayments.count ?? 0, + pendingTutors: pendingTutors.count ?? 0, + } + + return ( +
+
+

Analytics

+

+ Platform health snapshot — refreshed on every page load. +

+
+ + {/* ── Primary metrics ── */} +
+

+ Overview +

+
+ + + +
+
+ + {/* ── Sessions health ── */} +
+

+ Session Health +

+
+ + 0 ? 'warning' : 'normal'} + href="/admin/sessions" + linkLabel="Review sessions →" + /> +
+
+ + {/* ── Action items ── */} +
+

+ Action Required +

+
+ 0 ? 'attention' : 'normal'} + href="/admin/payments" + linkLabel="Review payments →" + /> + 0 ? 'attention' : 'normal'} + href="/admin/tutors" + linkLabel="Review tutors →" + /> +
+
+
+ ) +} + +function MetricCard({ + label, + value, + unit, + icon, + variant, + href, + linkLabel, +}: { + label: string + value: number + unit: string + icon: string + variant: 'normal' | 'warning' | 'attention' + href?: string + linkLabel?: string +}) { + const base = + 'rounded-xl border p-5 shadow-sm transition' + + const styles: Record = { + normal: + 'border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900', + warning: + 'border-amber-200 bg-amber-50 dark:border-amber-800/60 dark:bg-amber-950/30', + attention: + 'border-orange-200 bg-orange-50 dark:border-orange-800/60 dark:bg-orange-950/30', + } + + const valueStyles: Record = { + normal: 'text-zinc-900 dark:text-zinc-50', + warning: 'text-amber-700 dark:text-amber-400', + attention: 'text-orange-700 dark:text-orange-400', + } + + const content = ( +
+
+ {icon} +

+ {label} +

+
+

{value}

+

{unit}

+ {href && linkLabel && ( +

+ {linkLabel} +

+ )} +
+ ) + + if (href) { + return {content} + } + return content +} diff --git a/app/admin/audit/page.tsx b/app/admin/audit/page.tsx new file mode 100644 index 0000000..2a88a19 --- /dev/null +++ b/app/admin/audit/page.tsx @@ -0,0 +1,146 @@ +// E12 T12.3: Admin audit log page — shows recent 200 audit events ordered newest first +// Closes #80 + +export const dynamic = 'force-dynamic' + +import { createAdminClient } from '@/lib/supabase/admin' + +const ADMIN_TIMEZONE = 'Asia/Karachi' + +// Human-readable labels for known audit actions +const AUDIT_ACTION_LABELS: Record = { + payment_marked_paid: '💳 Payment marked paid', + payment_marked_rejected: '❌ Payment rejected', + tutor_approved: '✅ Tutor approved', + tutor_approval_revoked: '🚫 Tutor approval revoked', + tutor_assigned: '🎓 Tutor assigned to request', + tutor_reassigned: '🔄 Tutor reassigned', + sessions_generated: '📅 Sessions generated', + session_rescheduled: '📅 Session rescheduled', + session_status_updated: '📝 Session status updated', + match_details_updated: '🔗 Match details updated', +} + +function formatAuditTime(iso: string) { + return new Intl.DateTimeFormat('en-GB', { + timeZone: ADMIN_TIMEZONE, + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(iso)) +} + +type AuditLogRow = { + id: string + action: string + entity_type: string + entity_id: string | null + details: Record | null + created_at: string + user_profiles: { display_name: string } | null +} + +export default async function AdminAuditPage() { + const admin = createAdminClient() + + const { data: logsData } = await admin + .from('audit_logs') + .select( + 'id, action, entity_type, entity_id, details, created_at, user_profiles!actor_user_id(display_name)' + ) + .order('created_at', { ascending: false }) + .limit(200) + + const logs = (logsData ?? []) as unknown as AuditLogRow[] + + return ( +
+
+
+

Audit Log

+

+ Most recent {logs.length} platform events — times shown in PKT (Asia/Karachi) +

+
+
+ + {logs.length === 0 ? ( +
+

No audit events recorded yet.

+

+ Events are logged automatically when admin actions are performed (payments, tutor + approvals, session updates, etc.). +

+
+ ) : ( +
+ + + + + + + + + + + + {logs.map((log) => { + const actorName = + (log.user_profiles as { display_name: string } | null)?.display_name ?? 'System' + const actionLabel = AUDIT_ACTION_LABELS[log.action] ?? log.action + const detailsStr = log.details + ? Object.entries(log.details) + .map(([k, v]) => { + const str = + v !== null && typeof v === 'object' + ? JSON.stringify(v) + : String(v) + return `${k}: ${str.length > 80 ? str.slice(0, 77) + '…' : str}` + }) + .join(' · ') + : '—' + // Truncate entity_id to first 8 chars for readability (UUID) + const entityIdShort = log.entity_id ? log.entity_id.slice(0, 8) + '…' : '—' + + return ( + + + + + + + + ) + })} + +
+ Timestamp + + Actor + + Action + + Entity + + Details +
+ {formatAuditTime(log.created_at)} + {actorName}{actionLabel} + + {log.entity_type} + {' '} + {entityIdShort} + + {detailsStr} +
+
+ )} +
+ ) +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 066ec85..1dac6e4 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -79,6 +79,18 @@ export default async function AdminLayout({ > Sessions + + Audit Log + + + Analytics +
diff --git a/app/admin/page.tsx b/app/admin/page.tsx index b3e0302..ce37c1d 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -40,6 +40,16 @@ export default function AdminPage() { title="Sessions" description="Monitor session status and attendance." /> + + ) diff --git a/app/policies/page.tsx b/app/policies/page.tsx index e1125f5..128f3ab 100644 --- a/app/policies/page.tsx +++ b/app/policies/page.tsx @@ -1 +1,336 @@ -export default function Page() { return

TODO

} +// E12 T12.1: Public policies page — reschedule, no-show, refund/expiry, package terms, privacy +// Closes #78 + +import Link from 'next/link' + +export const metadata = { + title: 'Policies — CorvEd', + description: + 'CorvEd reschedule policy, no-show policy, refund and expiry policy, package terms, and privacy basics.', +} + +export default function PoliciesPage() { + return ( +
+ {/* ── NAVBAR ── */} +
+
+ +
+
+
+
+
+ + CorvEd + + + + ← Back to home + +
+
+ + {/* ── HERO ── */} +
+
+
+
+ + Platform Policies + +
+

+ CorvEd Policies +

+

+ These policies apply to all students, parents, and tutors on the CorvEd platform. + They are locked for the current MVP launch and are referenced in all confirmation + messages. +

+
+
+ +
+ {/* ── 1. Reschedule Policy ── */} +
+
+
+ 1 +
+

Reschedule Policy

+
+
+
    +
  • + + + Reschedule requests must be submitted via WhatsApp at least{' '} + 24 hours before the + scheduled session time. + +
  • +
  • + 📍 + + When requesting a reschedule, provide{' '} + 2–3 alternate time slots{' '} + along with your timezone. + +
  • +
  • + ⚠️ + + Late reschedule requests (less than 24 hours before the session) may be treated + as a no-show at admin + discretion. + +
  • +
  • + 🩺 + + Exceptions may be + granted for: medical emergency, verified power or internet outage, or a genuine + first-time mistake. These must be communicated to admin and may be logged. + +
  • +
+
+
+ + {/* ── 2. No-Show Policy ── */} +
+
+
+ 2 +
+

No-Show Policy

+
+
+
+ + + + + + + + + {[ + { + scenario: 'Student no-show', + effect: '1 session deducted from package', + highlight: true, + }, + { + scenario: 'Tutor no-show', + effect: '0 sessions deducted — reschedule arranged immediately', + highlight: false, + }, + { + scenario: 'Student joins late (> 10 min)', + effect: 'Treated as a student no-show', + highlight: true, + }, + { + scenario: 'Tutor joins late (> 10 min)', + effect: 'Tutor no-show procedure begins', + highlight: false, + }, + ].map(({ scenario, effect, highlight }) => ( + + + + + ))} + +
+ Scenario + + Effect on sessions +
{scenario}{effect}
+
+
+
+ + {/* ── 3. Refund and Expiry Policy ── */} +
+
+
+ 3 +
+

+ Refund & Expiry Policy +

+
+
+
    +
  • + 📅 + + Monthly packages expire at the{' '} + end date (30 days from + activation). + +
  • +
  • + 🚫 + + No session carryover{' '} + between months — unused sessions are forfeited at the package end date. + +
  • +
  • + 💬 + + Refund requests are considered at{' '} + admin discretion. Contact + us via WhatsApp to discuss your situation. + +
  • +
  • + + + If CorvEd cancels or is unable to deliver sessions, affected sessions will be{' '} + credited or refunded. + +
  • +
+
+
+ + {/* ── 4. Package Terms ── */} +
+
+
+ 4 +
+

Package Terms

+
+
+
    +
  • + 📦 + + Packages are{' '} + per subject — one package + covers one subject for one month. + +
  • +
  • + 🎥 + + All sessions are{' '} + 60 minutes via Google + Meet using a dedicated recurring link. + +
  • +
  • + 👤 + + Sessions are with your{' '} + assigned tutor — + substitutions are arranged by admin if needed. + +
  • +
  • + 🛡️ + + All communication between student and tutor is{' '} + mediated by admin via + WhatsApp. + +
  • +
+
+
+ + {/* ── 5. Privacy ── */} +
+
+
+ 5 +
+

Privacy

+
+
+
    +
  • + 🔒 + + Your contact details are used{' '} + + only for tutoring coordination + {' '} + — never shared with third parties. + +
  • +
  • + 🚫 + + Tutors do{' '} + not receive student + contact information — all communication is admin-mediated. + +
  • +
  • + 📝 + + Session notes are visible to the{' '} + student, tutor, and admin{' '} + only. + +
  • +
+
+
+ + {/* ── Contact ── */} +
+

Questions?

+

+ If you have questions about any of these policies or need to request an exception, + contact us via WhatsApp. We aim to respond within a few hours during business hours. +

+
+ + {/* ── Back ── */} +
+ + ← Back to CorvEd home + +
+
+ + {/* ── FOOTER ── */} +
+
+ CorvEd +

+ © {new Date().getFullYear()} CorvEd. All rights reserved. +

+
+
+
+ ) +} diff --git a/app/tutor/conduct/page.tsx b/app/tutor/conduct/page.tsx new file mode 100644 index 0000000..4ad3567 --- /dev/null +++ b/app/tutor/conduct/page.tsx @@ -0,0 +1,260 @@ +// E12 T12.2: Tutor code of conduct page — public, linked from tutor profile form +// Closes #79 + +import Link from 'next/link' + +export const metadata = { + title: 'Tutor Code of Conduct — CorvEd', + description: + 'CorvEd expectations for tutor punctuality, session quality, communication, privacy, and incident handling.', +} + +export default function TutorConductPage() { + return ( +
+ {/* Header */} +
+
+ + CorvEd + + + ← Back to Profile + +
+
+ +
+ {/* Title */} +
+ + Tutor Agreement + +

+ CorvEd Tutor Code of Conduct +

+

+ As a CorvEd tutor, you agree to uphold the following standards of professional + conduct. These expectations exist to protect students, maintain platform quality, and + provide a fair environment for all parties. +

+
+ + {/* Section 1 — Punctuality */} +
+
+ + 1 + +

Punctuality

+
+
    +
  • + + + Join your Google Meet session within{' '} + 5 minutes of the + scheduled start time. + +
  • +
  • + + + Notify admin at least{' '} + 24 hours in advance{' '} + if you cannot attend a session. + +
  • +
  • + + + Repeated late joins or unannounced absences are grounds for{' '} + + removal from the platform + + . + +
  • +
+
+ + {/* Section 2 — Session Quality */} +
+
+ + 2 + +

Session Quality

+
+
    +
  • + + + Conduct sessions professionally and with the{' '} + + student's learning goals + {' '} + in mind. + +
  • +
  • + + + Log attendance (done / no-show) and session notes within{' '} + 12 hours of each + session. + +
  • +
  • + + + Session notes should be meaningful — include topics covered and follow-up work + (not just “done”). + +
  • +
+
+ + {/* Section 3 — Communication */} +
+
+ + 3 + +

Communication

+
+
    +
  • + + + All student/parent communication is{' '} + + mediated through CorvEd admin + + . + +
  • +
  • + + + Do not share personal contact details with students or parents. + +
  • +
  • + + + If a student contacts you directly, redirect them to admin. + +
  • +
+
+ + {/* Section 4 — Privacy */} +
+
+ + 4 + +

Privacy

+
+
    +
  • + + + Do not share student information (name, contact, performance) with anyone outside + CorvEd. + +
  • +
  • + + + Respect student{' '} + confidentiality in + all communications. + +
  • +
+
+ + {/* Section 5 — Quality Expectations */} +
+
+ + 5 + +

Quality Expectations

+
+
    +
  • + + + Maintain a clear and effective teaching approach suited to the student's + level and goals. + +
  • +
  • + + + If you are struggling with a student, inform admin — do not ghost the student. + +
  • +
  • + + + Admin will review tutor quality based on session notes, student feedback, and + attendance records. + +
  • +
+
+ + {/* Section 6 — Incidents */} +
+
+ + 6 + +

Incidents & Enforcement

+
+
    +
  • + + + Three confirmed incidents (no-shows, quality complaints, late log submission) will + trigger a{' '} + formal review. + +
  • +
  • + + + Admin may pause your assignments during a review period. + +
  • +
  • + + + Serious misconduct (harassment, breach of privacy) results in{' '} + immediate removal{' '} + from the platform. + +
  • +
+
+ + {/* Back link */} +
+ + ← Return to tutor profile + +
+
+
+ ) +} diff --git a/app/tutor/profile/TutorProfileForm.tsx b/app/tutor/profile/TutorProfileForm.tsx index e147362..fcdbaa0 100644 --- a/app/tutor/profile/TutorProfileForm.tsx +++ b/app/tutor/profile/TutorProfileForm.tsx @@ -1,9 +1,11 @@ // E6 T6.1 T6.3: Tutor profile form — subjects, levels, availability, bio, timezone -// Closes #40 #42 +// E12 T12.2: Added code of conduct acknowledgement checkbox +// Closes #40 #42 #79 'use client' import { useState } from 'react' +import Link from 'next/link' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { tutorProfileSchema, TutorProfileFormData } from '@/lib/validators/tutor' @@ -320,6 +322,39 @@ export function TutorProfileForm({ subjects, defaultValues, approved }: TutorPro

)} + {/* E12 T12.2: Code of conduct acknowledgement (required) */} +
+
+ +
+ {' '} + + View Code of Conduct → + {' '} + +
+
+ {errors.conductAcknowledged && ( +

+ {errors.conductAcknowledged.message} +

+ )} +
+