Skip to content

feat(E7): Admin matching and assignment — requests inbox, matching screen, assign/reassign tutor - #100

Merged
Taleef7 merged 3 commits into
mainfrom
copilot/implement-admin-matching-assignment
Feb 25, 2026
Merged

feat(E7): Admin matching and assignment — requests inbox, matching screen, assign/reassign tutor#100
Taleef7 merged 3 commits into
mainfrom
copilot/implement-admin-matching-assignment

Conversation

Copilot AI commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Implements Epic E7 end-to-end: the admin manual matching workflow that connects a ready_to_match request to an approved tutor, creates a matches record, and supports reassignment with full audit history.

Summary

  • Database: matches table with RLS, assignTutor/reassignTutor/updateMatchDetails server actions, filterable requests inbox, two-panel matching screen, match list and match detail pages
  • Bug fix: composite subject+level filter in lib/services/matching.ts now correctly requires both conditions on the same tutor_subjects row
  • Review feedback: hardened server actions with input validation, fixed dynamic update payload, removed redundant DB fields, improved audit logging, added partial-schedule validation in forms

Changes

  • supabase/migrations/20260225000001_create_matches_table.sql: matches table with unique request_id FK, tutor_user_id, match_status_enum, meet_link, schedule_pattern (JSONB), assigned_by_user_id/assigned_at, updated_at trigger; RLS: admin full access, tutor + request creator can SELECT
  • app/admin/requests/actions.ts:
    • assignTutor: validates request exists and is ready_to_match before insert; friendly error for duplicate-match unique constraint; removes redundant assigned_at and updated_at (both managed by DB defaults/triggers)
    • reassignTutor: validates match exists and new tutor differs from current before updating; removes redundant updated_at
    • updateMatchDetails: builds update payload dynamically — only explicitly-provided fields are written, so updating only meet_link never touches an existing schedule_pattern; audit log now records all changed fields including schedule_pattern
  • app/admin/requests/[id]/AssignTutorForm.tsx: partial-schedule validation — if any of timezone/time/days is set but not all three, returns an error before submitting
  • app/admin/matches/[id]/MatchActions.tsx: editMatchAction applies same partial-schedule validation and passes meetLink/schedulePattern selectively so the server action preserves unchanged fields
  • app/admin/requests/page.tsx: removed redundant .order('created_at') DB call (JavaScript sort by status priority handles ordering)
  • /admin/requests (T7.1): Filterable table — status tabs + subject/level selects; priority sort puts ready_to_match first; "Match →" CTA for actionable rows
  • /admin/requests/[id] (T7.2): Two-panel layout — full request detail + eligible tutor cards filtered by approved = true AND subject × level on the same tutor_subjects row
  • /admin/matches (T7.4): Real match list replacing the old placeholder
  • /admin/matches/[id] (T7.4): Match detail with EditMatchForm and ReassignTutorForm

Testing

  • Verified locally
  • Checked key flows manually

Notes

  • schedule_pattern.days uses 0=Sunday … 6=Saturday, duration_mins is always 60 in MVP
  • Schedule fields (timezone, time, days) must all be provided together or all left empty — partial schedule input is rejected with a clear error message
  • "Generate Sessions" CTA shown on match detail once meet link + schedule are set; wired to E8
  • updated_at on matches and requests is always managed by DB triggers — server actions no longer set it explicitly

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

…reen, assign/reassign tutor

Closes #44 #45 #46 #47 #48 #49 #50

T7.1: Admin requests inbox at /admin/requests
- Full filterable table (status tabs + subject/level selects)
- Priority sort: ready_to_match first, then by created_at
- Status badges using STATUS_COLOURS, Match → CTA for actionable requests
- RequestFilters.tsx client component for subject/level dropdowns

T7.2: Matching screen at /admin/requests/[id]
- Two-panel layout: request details + eligible tutor cards
- Eligible tutors filtered by approved=true + subject × level (same row)
- AssignTutorForm.tsx with tutor selection, meet link, timezone, days, start time

T7.3: matches table migration + assignTutor server action
- supabase/migrations/20260225000001_create_matches_table.sql
  (request_id unique FK, tutor_user_id, status enum, meet_link, schedule_pattern JSONB,
   assigned_by/at; updated_at trigger; admin all + participants select RLS)
- assignTutor: creates match, advances request to matched, writes audit log
- updateMatchDetails: edits meet_link/schedule_pattern, writes audit log

T7.4: Match detail page + reassignTutor server action
- /admin/matches/page.tsx: real match list with status, student, tutor, meet link
- /admin/matches/[id]/page.tsx: full match detail + admin actions
- ReassignTutorForm.tsx: select new tutor + optional reason
- EditMatchForm.tsx: edit meet link + schedule pattern
- reassignTutor: updates tutor_user_id, writes audit log (old/new IDs + reason)

Also fixed:
- lib/services/matching.ts: joint subject+level filter now uses same row (correct composite filter)
- README.md: updated to after E7, added all new capabilities and migration

Co-authored-by: Taleef7 <89072337+Taleef7@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement admin matching and assignment functionality feat(E7): Admin matching and assignment — requests inbox, matching screen, assign/reassign tutor Feb 25, 2026
Copilot AI requested a review from Taleef7 February 25, 2026 02:47
@Taleef7
Taleef7 marked this pull request as ready for review February 25, 2026 02:54
Copilot AI review requested due to automatic review settings February 25, 2026 02:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements Epic E7, delivering the complete admin manual matching workflow that connects ready-to-match student requests with approved tutors. The implementation includes a filterable requests inbox, a two-panel matching screen showing request details alongside eligible tutors, tutor assignment with meet link and schedule configuration, match listing, match detail management, and support for reassigning tutors with full audit history.

Changes:

  • Database: Created matches table with RLS policies for admin full access and participant read access
  • Server actions: Implemented assignTutor, reassignTutor, and updateMatchDetails with admin authorization and audit logging
  • Admin UI: Built requests inbox with status/subject/level filters, matching screen with eligible tutor cards, and match management pages
  • Bug fix: Fixed composite subject+level filter in matching service that could incorrectly match tutors

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
supabase/migrations/20260225000001_create_matches_table.sql Creates matches table with unique request_id FK, tutor assignment tracking, schedule pattern JSONB, and RLS policies
lib/services/matching.ts Fixes bug where subject AND level filters could match on separate tutor_subjects rows instead of requiring same row
app/admin/requests/actions.ts Implements assignTutor, reassignTutor, and updateMatchDetails server actions with admin auth and audit logging
app/admin/requests/page.tsx Requests inbox with status tabs, subject/level filters, priority sorting, and actionable CTAs
app/admin/requests/[id]/page.tsx Two-panel matching screen showing request details and eligible tutors filtered by subject × level
app/admin/requests/[id]/AssignTutorForm.tsx Client form for tutor selection with optional meet link and schedule pattern fields
app/admin/requests/RequestFilters.tsx Client component for subject and level dropdown filters
app/admin/matches/page.tsx Matches list showing student, tutor, subject/level, status, meet link, and assigned date
app/admin/matches/[id]/page.tsx Match detail page with full match information and admin action forms
app/admin/matches/[id]/MatchActions.tsx Client components for reassigning tutors and editing match details (meet link + schedule)
README.md Updated feature status table and migrations list to reflect E7 completion

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/admin/requests/actions.ts Outdated
Comment on lines +171 to +188
const { error } = await admin
.from('matches')
.update({
meet_link: meetLink ?? null,
schedule_pattern: schedulePattern ?? null,
updated_at: new Date().toISOString(),
})
.eq('id', matchId)

if (error) throw new Error(`Failed to update match: ${error.message}`)

const { error: auditError } = await admin.from('audit_logs').insert([
{
actor_user_id: adminUserId,
action: 'match_details_updated',
entity_type: 'match',
entity_id: matchId,
details: { meet_link: meetLink ?? null },

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateMatchDetails function always updates both meet_link and schedule_pattern fields, setting them to null when undefined is passed. This means if an admin only wants to update the meet_link and leaves schedule fields empty, it will clear any existing schedule_pattern. Consider building the update object dynamically to only include fields that should be changed, or provide separate update functions for meet_link and schedule_pattern.

Suggested change
const { error } = await admin
.from('matches')
.update({
meet_link: meetLink ?? null,
schedule_pattern: schedulePattern ?? null,
updated_at: new Date().toISOString(),
})
.eq('id', matchId)
if (error) throw new Error(`Failed to update match: ${error.message}`)
const { error: auditError } = await admin.from('audit_logs').insert([
{
actor_user_id: adminUserId,
action: 'match_details_updated',
entity_type: 'match',
entity_id: matchId,
details: { meet_link: meetLink ?? null },
// Build update payload dynamically so that only explicitly provided fields are changed.
const updateData: { [key: string]: any } = {
updated_at: new Date().toISOString(),
}
if (typeof meetLink !== 'undefined') {
// Allow explicit null to clear the field, while undefined means "no change".
updateData.meet_link = meetLink
}
if (typeof schedulePattern !== 'undefined') {
// Allow explicit null to clear the field, while undefined means "no change".
updateData.schedule_pattern = schedulePattern
}
const { error } = await admin
.from('matches')
.update(updateData)
.eq('id', matchId)
if (error) throw new Error(`Failed to update match: ${error.message}`)
const auditDetails: { [key: string]: any } = {}
if (typeof meetLink !== 'undefined') {
auditDetails.meet_link = meetLink
}
if (typeof schedulePattern !== 'undefined') {
auditDetails.schedule_pattern = schedulePattern
}
const { error: auditError } = await admin.from('audit_logs').insert([
{
actor_user_id: adminUserId,
action: 'match_details_updated',
entity_type: 'match',
entity_id: matchId,
details: auditDetails,

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. updateMatchDetails now builds the update payload dynamically — meet_link and schedule_pattern are only included in the UPDATE statement when they are explicitly passed (not undefined), so updating only the meet link will never touch an existing schedule, and vice versa. The audit log now records all changed fields.

Comment thread app/admin/requests/actions.ts Outdated
.from('matches')
.update({
tutor_user_id: newTutorUserId,
updated_at: new Date().toISOString(),

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated_at field is automatically managed by the matches_updated_at trigger in the database. Explicitly setting it here is unnecessary and could potentially conflict with the trigger behavior. Consider removing this field from the update statement.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. Removed the explicit updated_at from reassignTutor — the matches_updated_at trigger handles it automatically.

Comment thread app/admin/requests/actions.ts Outdated
.update({
meet_link: meetLink ?? null,
schedule_pattern: schedulePattern ?? null,
updated_at: new Date().toISOString(),

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated_at field is automatically managed by the matches_updated_at trigger in the database. Explicitly setting it here is unnecessary and could potentially conflict with the trigger behavior. Consider removing this field from the update statement.

Suggested change
updated_at: new Date().toISOString(),

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. Removed the explicit updated_at from updateMatchDetails — the DB trigger manages it.

Comment thread app/admin/requests/actions.ts Outdated
action: 'match_details_updated',
entity_type: 'match',
entity_id: matchId,
details: { meet_link: meetLink ?? null },

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The audit log details only include meet_link, but schedule_pattern can also be updated by this function. Consider including schedule_pattern in the audit log details for complete change tracking.

Suggested change
details: { meet_link: meetLink ?? null },
details: { meet_link: meetLink ?? null, schedule_pattern: schedulePattern ?? null },

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. The audit log details now include both meet_link and schedule_pattern (only whichever fields were actually changed, using the same dynamic payload approach).

Comment on lines +116 to +122
const { error } = await admin
.from('matches')
.update({
tutor_user_id: newTutorUserId,
updated_at: new Date().toISOString(),
})
.eq('id', matchId)

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider validating that the match exists and the new tutor is different from the previous tutor before performing the reassignment. This would prevent redundant updates and provide clearer error messages.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. reassignTutor now fetches the existing match first and throws a clear error if it doesn't exist, and returns an early error ("The selected tutor is already assigned to this match.") if newTutorUserId === existingMatch.tutor_user_id.

Comment thread app/admin/requests/actions.ts Outdated
Comment on lines +51 to +67
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 new Error(`Failed to create match: ${error.message}`)

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unique constraint on request_id is good for preventing duplicate matches. However, consider what happens if an admin accidentally tries to assign a tutor to a request that's already matched. The database will return a unique constraint violation, but the error message in the UI might not be clear to the admin. Consider adding a validation check before the insert to provide a more user-friendly error message.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. assignTutor now checks the request status before inserting. Additionally, if the unique constraint is still somehow hit, the error.code === '23505' branch catches it and returns a clear message: "This request already has a match assigned. Use the match detail page to reassign the tutor."

Comment thread app/admin/requests/actions.ts Outdated
meet_link: meetLink || null,
schedule_pattern: schedulePattern ?? null,
assigned_by_user_id: adminUserId,
assigned_at: new Date().toISOString(),

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assigned_at field already has a default value of now() in the database schema, so setting it explicitly here is redundant. Consider removing this line and letting the database default handle it.

Suggested change
assigned_at: new Date().toISOString(),

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. Removed the explicit assigned_at from the insert — the column has DEFAULT now() in the migration so the DB sets it automatically.

Comment thread app/admin/requests/actions.ts Outdated
// Advance request status to 'matched'
const { error: reqError } = await admin
.from('requests')
.update({ status: 'matched', updated_at: new Date().toISOString() })

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated_at field is automatically managed by the matches_updated_at trigger in the database. Explicitly setting it here is unnecessary and could potentially conflict with the trigger behavior. Consider removing this field from the update statement.

Suggested change
.update({ status: 'matched', updated_at: new Date().toISOString() })
.update({ status: 'matched' })

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. The requests update in assignTutor now only sets { status: 'matched' }updated_at is handled by the requests_updated_at trigger.

Comment on lines +51 to +65
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()

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider validating that the request exists and is in an appropriate status (e.g., ready_to_match) before creating a match. Currently, if the request doesn't exist or is already matched, the database constraint will fail, but a more user-friendly error message would improve the admin experience.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. assignTutor now validates the request before inserting: fetches id, status via maybeSingle(), throws 'Request not found.' if absent, and throws a descriptive error if the status is anything other than ready_to_match.

Comment thread app/admin/matches/[id]/MatchActions.tsx Outdated
? { timezone, days: rawDays, time, duration_mins: 60 }
: undefined

return updateMatchDetails({ matchId, meetLink, schedulePattern })

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateMatchDetails function will set both meet_link and schedule_pattern to null if any of the schedule fields (timezone, time, days) is missing, even if the user only intended to update the meet_link. This could unintentionally clear existing schedule data. Consider preserving existing values when only updating one field, or provide separate actions for updating meet_link vs schedule_pattern.

Suggested change
return updateMatchDetails({ matchId, meetLink, schedulePattern })
const payload: {
matchId: string
meetLink?: string
schedulePattern?: {
timezone: string
days: number[]
time: string
duration_mins: number
}
} = { matchId }
if (meetLink !== undefined) {
payload.meetLink = meetLink
}
if (schedulePattern) {
payload.schedulePattern = schedulePattern
}
return updateMatchDetails(payload)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ac56731. editMatchAction now applies the same partial-schedule validation and builds the payload selectively — meetLink and schedulePattern are only included in the call to updateMatchDetails when they are explicitly set, so a meet-link-only edit will never touch an existing schedule pattern.

@Taleef7

Taleef7 commented Feb 25, 2026

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

- assignTutor: validate request exists and is ready_to_match before insert;
  friendly error for unique constraint (already matched); remove redundant
  assigned_at and updated_at (both have DB defaults/triggers)
- reassignTutor: validate match exists and new tutor differs from current;
  remove redundant updated_at (trigger handles it)
- updateMatchDetails: build update payload dynamically so only explicitly
  provided fields are written (prevents accidental null-out of schedule
  when only meet_link is being updated); include schedule_pattern in audit log
- AssignTutorForm: validate partial schedule — if any schedule field is set
  but not all three (timezone + time + days), return error before submitting
- MatchActions editMatchAction: same partial-schedule validation; pass
  meetLink/schedulePattern selectively so server action preserves unchanged fields
- requests/page.tsx: remove redundant DB .order() (JS sort overrides it)

Co-authored-by: Taleef7 <89072337+Taleef7@users.noreply.github.com>
Copilot AI requested a review from Taleef7 February 25, 2026 03:46
@Taleef7
Taleef7 merged commit 8bfd237 into main Feb 25, 2026
1 check passed
@Taleef7
Taleef7 deleted the copilot/implement-admin-matching-assignment branch February 25, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants