Parent epic
Epic E7: admin matching and assignment (P0) — #44
Objective
Implement the assignTutor Server Action and the Supabase migration for the matches table — creating the Match record that connects a request to a tutor and stores the Meet link and schedule pattern.
Migration: <ts>_create_matches_table.sql
From docs/ARCHITECTURE.md section 5.6:
create table public.matches (
id uuid primary key default gen_random_uuid(),
request_id uuid not null unique references public.requests(id) on delete cascade,
tutor_user_id uuid not null references public.tutor_profiles(tutor_user_id) on delete restrict,
status public.match_status_enum not null default 'matched',
meet_link text,
schedule_pattern jsonb,
assigned_by_user_id uuid references public.user_profiles(user_id),
assigned_at timestamptz not null default now(),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table public.matches enable row level security;
-- Admin can do everything
create policy "matches_admin_write"
on public.matches for all to authenticated
using (public.is_admin(auth.uid()))
with check (public.is_admin(auth.uid()));
-- Participants can select
create policy "matches_select_participants"
on public.matches for select to authenticated
using (
public.is_admin(auth.uid())
or tutor_user_id = auth.uid()
or exists (
select 1 from public.requests r
where r.id = matches.request_id
and r.created_by_user_id = auth.uid()
)
);
Server Action: assignTutor
Location: app/admin/actions.ts
'use server'
import { createAdminClient } from '@/lib/supabase/admin'
import { revalidatePath } from 'next/cache'
export async function assignTutor({
requestId,
tutorUserId,
adminUserId,
meetLink,
schedulePattern,
}: {
requestId: string
tutorUserId: string
adminUserId: string
meetLink?: string
schedulePattern?: {
timezone: string
days: number[]
time: string
duration_mins: number
}
}) {
const admin = createAdminClient()
const { data: match, error } = await admin
.from('matches')
.insert([{
request_id: requestId,
tutor_user_id: tutorUserId,
status: 'matched',
meet_link: meetLink ?? null,
schedule_pattern: schedulePattern ?? null,
assigned_by_user_id: adminUserId,
assigned_at: new Date().toISOString(),
}])
.select()
.single()
if (error) throw error
// Advance request status
await admin
.from('requests')
.update({ status: 'matched', updated_at: new Date().toISOString() })
.eq('id', requestId)
// Audit log
await admin.from('audit_logs').insert([{
actor_user_id: adminUserId,
action: 'tutor_assigned',
entity_type: 'match',
entity_id: match.id,
details: { tutor_user_id: tutorUserId, request_id: requestId }
}])
revalidatePath(`/admin/requests/${requestId}`)
return match
}
Schedule pattern format
From docs/ARCHITECTURE.md section 9.3:
{
"timezone": "Asia/Karachi",
"days": [1, 3],
"time": "19:00",
"duration_mins": 60
}
days: 0 = Sunday, 1 = Monday, …, 6 = Saturday
time: local time in 24h format
duration_mins: always 60 in MVP
Acceptance criteria
Definition of done
References
docs/ARCHITECTURE.md — section 5.6 (matches table), section 6.4 (matches RLS), section 9.3 (schedule_pattern format), section 8.3 (matching workflow)
docs/OPS.md — section 4 Workflow C + D
Parent epic
Epic E7: admin matching and assignment (P0) — #44
Objective
Implement the
assignTutorServer Action and the Supabase migration for thematchestable — creating the Match record that connects a request to a tutor and stores the Meet link and schedule pattern.Migration:
<ts>_create_matches_table.sqlFrom
docs/ARCHITECTURE.mdsection 5.6:Server Action:
assignTutorLocation:
app/admin/actions.tsSchedule pattern format
From
docs/ARCHITECTURE.mdsection 9.3:{ "timezone": "Asia/Karachi", "days": [1, 3], "time": "19:00", "duration_mins": 60 }days: 0 = Sunday, 1 = Monday, …, 6 = Saturdaytime: local time in 24h formatduration_mins: always 60 in MVPAcceptance criteria
supabase/migrations/<ts>_create_matches_table.sqlexists with table + RLSassignTutorServer Action creates match and advances request tomatchedmeet_linkandschedule_pattern(can be null on initial assignment)meet_linkandschedule_patternafter initial assignment (via separate edit action)Definition of done
matchestable migration exists and applies cleanlyassignTutorServer Action works end-to-endmatchedReferences
docs/ARCHITECTURE.md— section 5.6 (matches table), section 6.4 (matches RLS), section 9.3 (schedule_pattern format), section 8.3 (matching workflow)docs/OPS.md— section 4 Workflow C + D