You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Last sprint shipped the list endpoint with filters/sort/pagination, plus seeding and indexes. What's missing is the detail view, the ability to change decisions and RSVP status inline, and CSV exports for admin data analysis.
Tasks
Detail endpoint
Implement getApplicant(id) in src/lib/applicants/service.ts. Returns the full applicant document from applicant_data with every field — email, all applicationResponses, applicationStatus, decisionStatus, rsvpStatus, postAcceptanceResponses, timestamps, and any linked upload records.
What this accomplishes: The applicant detail page needs everything about one applicant on one screen. Unlike the list which summarizes, this returns the full record for review.
For upload references in responses, resolve them to displayable info: filename and a link that calls /api/v1/uploads/[id] (which returns a signed download URL). Alternatively, return the upload record inline and let the frontend request the download URL when needed.
What this accomplishes: Admins reviewing an application can see what files the applicant uploaded and download them (resumes, etc.). Without this, the responses just show opaque upload IDs.
Wire GET /api/v1/applicants/[id]/route.ts to call getApplicant. Returns 404 for missing IDs.
Update endpoint
Implement updateApplicant(id, patch, updatedBy) in the service. Accepts a partial patch of { decisionStatus?, rsvpStatus? }. Validates each field is a valid enum value. Writes with $set to the applicant document, plus updatedBy and updatedAt audit fields.
What this accomplishes: The single function admins use to change decisions and RSVP statuses. Audit fields record who made the change and when.
Reject patches that try to change other fields (email, responses, etc.). Only decision and RSVP are admin-editable through this endpoint.
What this accomplishes: Prevents an admin from accidentally overwriting an applicant's responses. Application content is applicant-owned; only status fields are admin-owned.
Wire POST /api/v1/applicants/[id]/route.ts to call updateApplicant. Returns the updated applicant on success, 400 on invalid patch, 404 if missing.
CSV exports
Implement toCsv(rows, columns) streaming helper in src/lib/applicants/csv.ts. Uses ReadableStream to send row-by-row instead of buffering everything.
What this accomplishes: Exports with thousands of applicants don't blow up memory. Streaming lets the server hand off each row as it's serialized instead of holding the whole file at once.
Properly escape CSV fields: wrap in double quotes if the value contains commas, newlines, or double quotes; escape internal double quotes by doubling them.
What this accomplishes: Applicant answers contain commas, quotes, and newlines constantly. Naive CSV generation would produce malformed files that break Excel.
Implement GET /api/v1/export/applications/route.ts. Streams a CSV with one row per applicant containing email, name, status, decision, RSVP, and one column per application question response. Set Content-Type: text/csv and Content-Disposition: attachment; filename="applications-YYYY-MM-DD.csv".
What this accomplishes: Clicking "Export applications CSV" on the admin page downloads a file that opens directly in Excel. The date in the filename helps admins keep multiple exports organized.
Implement GET /api/v1/export/post-acceptance/route.ts with the same pattern but for postAcceptanceResponses.
What this accomplishes: Separate export for RSVP data — dietary restrictions, t-shirt sizes, etc. — that event ops uses.
Auth gating
Wire requireAdmin() into GET /api/v1/applicants, GET /api/v1/applicants/[id], POST /api/v1/applicants/[id], GET /api/v1/export/applications, and GET /api/v1/export/post-acceptance.
What this accomplishes: Applicant data never leaves the system to anyone but admins. Non-admins get 403 on all six endpoints.
End-to-end verification
Sign in as admin, open /admin/applicants, click a row, verify the detail page shows all responses and any uploaded files are downloadable.
Change a decision inline, refresh, verify it stuck.
Click both export buttons, open the resulting CSVs in Excel or a text editor, verify structure is sensible and special characters are escaped.
Definition of done
Admins can view a full applicant detail page including downloadable uploads.
Admins can change decision and RSVP status inline from the detail page and it persists.
Both CSV endpoints download well-formed files with all applicant data.
Context
Last sprint shipped the list endpoint with filters/sort/pagination, plus seeding and indexes. What's missing is the detail view, the ability to change decisions and RSVP status inline, and CSV exports for admin data analysis.
Tasks
Detail endpoint
getApplicant(id)insrc/lib/applicants/service.ts. Returns the full applicant document fromapplicant_datawith every field — email, allapplicationResponses,applicationStatus,decisionStatus,rsvpStatus,postAcceptanceResponses, timestamps, and any linked upload records./api/v1/uploads/[id](which returns a signed download URL). Alternatively, return the upload record inline and let the frontend request the download URL when needed.GET /api/v1/applicants/[id]/route.tsto callgetApplicant. Returns 404 for missing IDs.Update endpoint
updateApplicant(id, patch, updatedBy)in the service. Accepts a partial patch of{ decisionStatus?, rsvpStatus? }. Validates each field is a valid enum value. Writes with$setto the applicant document, plusupdatedByandupdatedAtaudit fields.POST /api/v1/applicants/[id]/route.tsto callupdateApplicant. Returns the updated applicant on success, 400 on invalid patch, 404 if missing.CSV exports
toCsv(rows, columns)streaming helper insrc/lib/applicants/csv.ts. UsesReadableStreamto send row-by-row instead of buffering everything.GET /api/v1/export/applications/route.ts. Streams a CSV with one row per applicant containing email, name, status, decision, RSVP, and one column per application question response. SetContent-Type: text/csvandContent-Disposition: attachment; filename="applications-YYYY-MM-DD.csv".GET /api/v1/export/post-acceptance/route.tswith the same pattern but forpostAcceptanceResponses.Auth gating
requireAdmin()intoGET /api/v1/applicants,GET /api/v1/applicants/[id],POST /api/v1/applicants/[id],GET /api/v1/export/applications, andGET /api/v1/export/post-acceptance.End-to-end verification
/admin/applicants, click a row, verify the detail page shows all responses and any uploaded files are downloadable.Definition of done
requireAdmin().