Skip to content

[STU-101] Language CRUD for candidate profile#35

Merged
BAWES merged 9 commits into
mainfrom
feature/STU-101-language-crud
May 21, 2026
Merged

[STU-101] Language CRUD for candidate profile#35
BAWES merged 9 commits into
mainfrom
feature/STU-101-language-crud

Conversation

@BAWES
Copy link
Copy Markdown
Owner

@BAWES BAWES commented May 21, 2026

Summary

  • Added candidate_language Prisma model with soft-delete support
  • Added addCandidateLanguage and removeCandidateLanguage server actions with Zod validation
  • Added language query to candidate detail transaction in workspace data
  • Added language form section in CandidateEditForm with proficiency badges (basic/intermediate/advanced/native)
  • Added toast success/error notifications for language add/remove

Test plan

  • TypeScript passes (0 STU-101 errors)
  • Lint clean
  • Language CRUD functional at /candidate/edit:
    • Add language with proficiency level
    • Remove language (soft-delete)

Files changed

  • prisma/schema.prisma — new candidate_language model
  • src/modules/candidates/actions.ts — language server actions + Zod schema
  • src/modules/workspace/data.ts — language query in getCandidateDetail
  • src/modules/candidates/CandidateEditForm.tsx — language form section
  • src/app/candidate/edit/page.tsx — pass languages prop

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Candidates can manage languages and proficiency levels in their profile editor (add/remove).
    • Candidate certificates can include title, issuer, and URL fields.
  • UI / Styles

    • Civil ID panel added to candidate profiles with responsive photo layout and dark-theme support.
  • Security

    • Public route access tightened; some pages now require authentication.
  • Tests / Tooling

    • New end-to-end and unit tests for candidate language CRUD and validation.
    • Added test runner configuration and dev dependency for tests.

Review Change Stack

BAWES and others added 2 commits May 22, 2026 00:09
The game files were reverted in 56f25f4 but the middleware still
allowed unauthenticated access to the /games route.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add server actions (addCandidateLanguage, removeCandidateLanguage) with Zod
validation, candidate_language Prisma model, and language form section in
CandidateEditForm with proficiency badges and toast notifications.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
studenthub-next Error Error May 21, 2026 8:39pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds persistent candidate language management (DB model, server actions, data fetching, and edit-form UI), introduces a Civil ID display component plus styles, and restricts middleware public paths to only "/login" and "/".

Changes

Candidate Language Management

Layer / File(s) Summary
Database schema for candidate languages
prisma/schema.prisma
New candidate_language model stores language, proficiency, soft-delete flag, creation timestamp, and index; candidate gains candidate_language[] relation; candidate_certificate gains metadata fields.
Data fetching and server actions
src/modules/candidates/actions.ts, src/modules/workspace/data.ts
Adds Zod-validated addCandidateLanguage and removeCandidateLanguage server actions with capability checks and soft-delete; getCandidateDetail fetches non-deleted languages and maps them for the UI; inspector ID request now selects rejection_reason.
Form UI and page wiring
src/modules/candidates/CandidateEditForm.tsx, src/app/candidate/edit/page.tsx
CandidateEditForm accepts languages, wires add/remove action-state hooks with success toasts, and renders a Languages section (add form and per-language remove forms). Edit page passes mapped languages from data.

Civil ID UI

Layer / File(s) Summary
Civil ID component in profile
src/modules/candidates/CandidateProfile.tsx
Adds CivilIdPanel that conditionally renders ID number, expiry badge (needs verification/expired/expires soon), and front/back images based on candidate fields.
Civil ID CSS
src/app/styles.css
New styles for .civilIdPanel and children, dark-theme overrides, and responsive photo grid rule at max-width: 760px.

Public Route Access Control & Type tweaks

Layer / File(s) Summary
Public routes restriction
src/middleware.ts
publicPaths reduced to "/login" and "/", removing public access to "/games".
FormAction type alias and casts
src/modules/candidates/WorkLogStaffActions.tsx
Adds local FormAction type and updates reject/approve form action casts to use it.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • BAWES/studenthub-codex#30: Modifies src/middleware.ts publicPaths and is directly related to the routing change in this PR.
  • BAWES/studenthub-codex#26: Adds rejection_reason to candidate ID requests in the schema, which corresponds to this PR selecting that field in getInspectorIdRequestDetail.
  • BAWES/studenthub-codex#19: Extends candidate-edit flow with server actions and UI wiring (education vs languages), sharing implementation patterns and test coverage approaches.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding CRUD functionality for candidate languages in the profile.
Description check ✅ Passed The PR description covers key changes, includes a test plan, lists affected files, and provides proper attribution for AI-assisted work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/STU-101-language-crud

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@prisma/schema.prisma`:
- Around line 425-434: The candidate_language_created_at field is nullable and
left unset by addCandidateLanguage, causing NULLs that break ordering in
getCandidateDetail; update the Prisma model (candidate_language ->
candidate_language_created_at) to be non-nullable and set a default timestamp
(e.g., `@default`(now()) with `@db.DateTime`(0)), then regenerate/prisma migrate to
apply the change, and ensure the addCandidateLanguage code path sets or relies
on the DB default for created_at so new rows always have a timestamp for
ordering/auditing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c718ccb0-a388-4d81-b567-eaeb65f3aed4

📥 Commits

Reviewing files that changed from the base of the PR and between 11e0d1e and 85a744e.

📒 Files selected for processing (6)
  • prisma/schema.prisma
  • src/app/candidate/edit/page.tsx
  • src/middleware.ts
  • src/modules/candidates/CandidateEditForm.tsx
  • src/modules/candidates/actions.ts
  • src/modules/workspace/data.ts

Comment thread prisma/schema.prisma
Show civil ID number, expiry date with warning/expired state, photo
front/back thumbnails, and needs-verification badge below the profile
facts grid. Adds a dedicated CivilIdPanel component with expiry date
threshold alerts (90 days) and responsive photo layout.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@BAWES
Copy link
Copy Markdown
Owner Author

BAWES commented May 21, 2026

DevRel Review

Status: Comment — one actionable finding to address

CodeRabbit finding: nullable created_at

CodeRabbit flagged that candidate_language_created_at is nullable and addCandidateLanguage does not set it, which would produce NULLs that break ordering in getCandidateDetail. The recommended fix is to make the field non-nullable with @default(now()). This is worth addressing before merge.

Vercel deploy failure

The Vercel preview deploy is failing — check the build logs to see if this is related to the changes or an infrastructure issue.

General notes

  • The feature itself (language CRUD with proficiency levels, toast notifications, soft-delete) looks well-structured
  • Good use of Zod validation on server actions
  • The UI with proficiency badges is a nice touch

Next step: Address the created_at column default, verify the Vercel build, and this should be good to merge.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/styles.css`:
- Around line 8700-8703: The CSS rule for .civilIdPanelFields span uses a
hardcoded fallback token var(--ink-muted, `#6b7280`); replace that with the theme
token var(--muted) to ensure consistent contrast and theming (including dark
mode). Update the declaration in the .civilIdPanelFields span rule and also the
identical rule elsewhere in the file (the second occurrence mentioned in the
review) so both use color: var(--muted) instead of var(--ink-muted, `#6b7280`).

In `@src/modules/candidates/CandidateProfile.tsx`:
- Around line 112-113: CandidateProfile is rendering sensitive Civil ID fields
and photos (e.g., the Fact at label "Civil ID" and the photo render block in the
component) without any access checks; use server-side RBAC/capability guards so
only authorized users see PII. Wrap the Civil ID and photo rendering behind
requireRole(...) or requireCapability(...) checks (depending on whether broad
role or fine-grained capability is desired) and only render the full value/photo
when the check passes; otherwise render a safe placeholder like "Hidden" or a
masked value. Update any places where viewerRole is currently unused to use the
proper requireRole/requireCapability calls, and ensure the checks are applied to
all occurrences referenced (the Fact at "Civil ID" and the photo rendering block
in CandidateProfile). Ensure you do this in the server component context so
authorization happens before sending data to the client.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 30b82cf7-7533-471c-bce1-fd6720c9695e

📥 Commits

Reviewing files that changed from the base of the PR and between 85a744e and f9eb5fb.

📒 Files selected for processing (2)
  • src/app/styles.css
  • src/modules/candidates/CandidateProfile.tsx

Comment thread src/app/styles.css
Comment on lines +8700 to +8703
.civilIdPanelFields span {
font-size: 12px;
color: var(--ink-muted, #6b7280);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use existing theme token for secondary text color in Civil ID labels.

var(--ink-muted, #6b7280) bypasses your defined theme tokens and can produce inconsistent contrast (especially in dark mode). Use var(--muted) here.

Suggested fix
 .civilIdPanelFields span {
   font-size: 12px;
-  color: var(--ink-muted, `#6b7280`);
+  color: var(--muted);
 }
@@
 .civilIdPhotos span {
   font-size: 12px;
-  color: var(--ink-muted, `#6b7280`);
+  color: var(--muted);
 }

Also applies to: 8731-8734

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/styles.css` around lines 8700 - 8703, The CSS rule for
.civilIdPanelFields span uses a hardcoded fallback token var(--ink-muted,
`#6b7280`); replace that with the theme token var(--muted) to ensure consistent
contrast and theming (including dark mode). Update the declaration in the
.civilIdPanelFields span rule and also the identical rule elsewhere in the file
(the second occurrence mentioned in the review) so both use color: var(--muted)
instead of var(--ink-muted, `#6b7280`).

Comment on lines +112 to 113
<Fact label="Civil ID" value={candidate.candidate_civil_id ?? (candidate.candidate_civil_need_verification ? "Needs verification" : "Not set")} />
<Fact label="Updated" value={formatDate(candidate.candidate_updated_at)} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate Civil ID PII rendering by role/capability before display.

Line 112 and Lines 172-226 render Civil ID number/photos without an access check, and viewerRole is currently unused. This can expose sensitive data to unauthorized viewers.

Suggested fix
 export function CandidateProfile({
   detail,
   actions,
   backHref,
   compact = false,
   viewerRole
 }: {
@@
 }) {
@@
+  const canViewCivilId = viewerRole === "staff";
@@
-        <Fact label="Civil ID" value={candidate.candidate_civil_id ?? (candidate.candidate_civil_need_verification ? "Needs verification" : "Not set")} />
+        <Fact
+          label="Civil ID"
+          value={
+            canViewCivilId
+              ? candidate.candidate_civil_id ?? (candidate.candidate_civil_need_verification ? "Needs verification" : "Not set")
+              : "Restricted"
+          }
+        />
@@
-      <CivilIdPanel candidate={candidate} viewerRole={viewerRole} />
+      <CivilIdPanel candidate={candidate} viewerRole={viewerRole} />
@@
 function CivilIdPanel({ candidate, viewerRole }: { candidate: NonNullable<CandidateDetailData["candidate"]>; viewerRole?: string }) {
+  if (viewerRole !== "staff") {
+    return null;
+  }

As per coding guidelines, "Use requireRole() in server components for role-based access control" and "Use requireCapability() for granular capability-based access control".

Also applies to: 116-116, 172-226

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/candidates/CandidateProfile.tsx` around lines 112 - 113,
CandidateProfile is rendering sensitive Civil ID fields and photos (e.g., the
Fact at label "Civil ID" and the photo render block in the component) without
any access checks; use server-side RBAC/capability guards so only authorized
users see PII. Wrap the Civil ID and photo rendering behind requireRole(...) or
requireCapability(...) checks (depending on whether broad role or fine-grained
capability is desired) and only render the full value/photo when the check
passes; otherwise render a safe placeholder like "Hidden" or a masked value.
Update any places where viewerRole is currently unused to use the proper
requireRole/requireCapability calls, and ensure the checks are applied to all
occurrences referenced (the Fact at "Civil ID" and the photo rendering block in
CandidateProfile). Ensure you do this in the server component context so
authorization happens before sending data to the client.

Add missing certificate fields to candidate_certificate model,
rejection_reason to ID request detail query, and fix
WorkLogStaffActions form action type cast.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolves CodeRabbit finding: nullable created_at was left unset by
addCandidateLanguage, causing NULLs that break orderBy in
getCandidateDetail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BAWES and others added 2 commits May 22, 2026 02:52
- Add vitest unit tests for language schema validation (9 tests)
- Add Playwright e2e tests for full language CRUD flow on edit profile
  page (5 tests × 2 browser projects = 10 test cases)
- Tests cover: schema validation, add language, remove language,
  empty state, access control

Tests: added unit and integration tests for language CRUD

Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Updated validate.mjs model count from 128 to 129 (candidate_language added)
- Fixed corrupted portalContent.ts with stray merge artifacts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/modules/candidates/actions-language.test.ts (1)

12-19: ⚡ Quick win

Avoid duplicating the validation schema in tests.

This test can pass while production behavior changes if the action schema diverges. Prefer importing a shared languageSchema from the source module (or a dedicated shared schema file) and test that directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/candidates/actions-language.test.ts` around lines 12 - 19, The
test duplicates the validation schema (PROFICIENCY_LEVELS and languageSchema)
which risks divergence from production; instead, remove the local definitions
and import the shared languageSchema used by the action (or a dedicated shared
schema export) into the test, then use that imported languageSchema in
assertions; update any test references to PROFICIENCY_LEVELS to use the exported
enum/const from the source module (or derive values from the imported schema) so
the test always validates the same schema as the runtime code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/smoke/candidate-language-crud.spec.ts`:
- Line 2: Replace the relative import of getFixtures with the project path
alias: change the import statement that currently imports getFixtures from
"../fixtures/auth" to use "`@/fixtures/auth`" so the code uses the repository's `@/`
alias for internal imports (look for the getFixtures import at the top of the
spec file).

In `@vitest.config.ts`:
- Line 1: Add vitest as a devDependency so TypeScript can resolve imports from
"vitest/config" referenced in vitest.config.ts; update package.json's
devDependencies to include an appropriate vitest version (or run the package
manager command to add it) and then reinstall so module resolution errors
(Cannot find module 'vitest/config') are resolved by tsc when running
test:types; ensure package-lock or yarn.lock is updated accordingly.

---

Nitpick comments:
In `@src/modules/candidates/actions-language.test.ts`:
- Around line 12-19: The test duplicates the validation schema
(PROFICIENCY_LEVELS and languageSchema) which risks divergence from production;
instead, remove the local definitions and import the shared languageSchema used
by the action (or a dedicated shared schema export) into the test, then use that
imported languageSchema in assertions; update any test references to
PROFICIENCY_LEVELS to use the exported enum/const from the source module (or
derive values from the imported schema) so the test always validates the same
schema as the runtime code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7763cb90-8004-4982-905d-e282f19af3f3

📥 Commits

Reviewing files that changed from the base of the PR and between a600527 and db2f896.

📒 Files selected for processing (5)
  • e2e/smoke/candidate-language-crud.spec.ts
  • scripts/validate.mjs
  • src/modules/auth/portalContent.ts
  • src/modules/candidates/actions-language.test.ts
  • vitest.config.ts

@@ -0,0 +1,207 @@
import { test, expect } from "@playwright/test";
import { getFixtures } from "../fixtures/auth";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use the @/ alias for internal imports.

Please replace the relative import with the project alias to match repository standards.

Suggested change
-import { getFixtures } from "../fixtures/auth";
+import { getFixtures } from "`@/e2e/fixtures/auth`";

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Use @/ path alias for all internal imports".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { getFixtures } from "../fixtures/auth";
import { getFixtures } from "`@/e2e/fixtures/auth`";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/smoke/candidate-language-crud.spec.ts` at line 2, Replace the relative
import of getFixtures with the project path alias: change the import statement
that currently imports getFixtures from "../fixtures/auth" to use
"`@/fixtures/auth`" so the code uses the repository's `@/` alias for internal
imports (look for the getFixtures import at the top of the spec file).

Comment thread vitest.config.ts
PR #35 added vitest test files (actions-language.test.ts, vitest.config.ts)
but vitest was not installed, causing TypeScript CI failures:
"Cannot find module 'vitest/config'" and "Cannot find module 'vitest'"

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 67: package.json was updated to add the "vitest" dependency but the
lockfile wasn't updated, causing CI to fail with `npm ci`; regenerate and commit
the updated package-lock.json by running `npm install` (or `npm install
--package-lock-only`) locally so the lockfile reflects the new "vitest" entry,
then add and commit the updated package-lock.json alongside the package.json
change so CI `npm ci` succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 439ec31e-150f-4643-b2cf-0e84977be028

📥 Commits

Reviewing files that changed from the base of the PR and between db2f896 and d0cf220.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • package.json

Comment thread package.json
npm install on macOS omits optional WASM emnapi packages needed on Linux
CI runners (npm ci fails with "Missing: @emnapi/runtime@1.10.0 from lock
file"). Restored entries manually from main's lock file.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@BAWES BAWES merged commit e526c86 into main May 21, 2026
8 of 9 checks passed
@BAWES BAWES deleted the feature/STU-101-language-crud branch May 21, 2026 20:43
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.

1 participant