Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions apps/web/messages/en/positions.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
},
"detail": {
"edit": "Edit",
"ownerLine": "Owner: {name}",
"clientOwnerLine": "{client} · Owner: {name}",
"fields": {
"department": "Department",
"reportingLine": "Reports to",
Expand Down Expand Up @@ -93,7 +93,80 @@
"poolLink": "Find in my pool",
"benchmarkLink": "Market benchmark",
"boardLink": "Candidates in this position",
"historyLink": "What has this client liked before?"
"historyLink": "What has this client liked before?",
"experienceChip": "{value} experience",
"openingsChip": "{count, plural, one {# opening} other {# openings}}",
"process": {
"label": "Hiring process",
"stepTitle": "{n}. {name}",
"stepDetail": "{state} · {detail}",
"steps": {
"jd": "Job description",
"profile": "Success profile",
"sourcing": "Sourcing",
"matching": "Matching",
"outreach": "Outreach",
"submission": "Submission"
},
"states": {
"done": "Done",
"current": "Now",
"waiting": "Not started"
},
"counts": {
"sourcing": "{count} in pipeline",
"matching": "{count} analyzed",
"outreach": "{count} contacted",
"submission": "{count} submitted"
},
"jdStates": {
"none": "Not added",
"in_progress": "Waiting for approval",
"approved": "Approved"
},
"profileStates": {
"none": "Not written",
"draft": "Draft, not confirmed",
"confirmed": "Confirmed"
}
},
"next": {
"label": "Next:",
"addJd": "Add the client's job description below.",
"approveJd": "Structure the job description and get the hiring manager's approval.",
"confirmProfile": "Write and confirm the success profile. Matching compares candidates with it.",
"reviewDrafts": "{count, plural, one {# uploaded resume waits} other {# uploaded resumes wait}} for your review.",
"addCandidates": "Add candidates: upload resumes or LinkedIn PDFs, or find them in your pool.",
"analyze": "{count, plural, one {# candidate has} other {# candidates have}} no match analysis yet.",
"outreach": "Contact the candidates you want to approach.",
"board": "Keep the candidates moving on the candidate board.",
"open": "Open"
},
"numbers": {
"label": "Candidates",
"pipeline": "In pipeline",
"analyzed": "Analyzed",
"interested": "Interested",
"submitted": "Submitted"
},
"groups": {
"label": "Work on this position",
"candidates": "Candidates",
"candidatesHint": "Review board and resume uploads",
"find": "Find candidates",
"findHint": "Search criteria and your pool",
"prepare": "Prepare the search",
"prepareHint": "JD, intake questions and success profile",
"insights": "Insights",
"insightsHint": "Market benchmark and client history"
},
"sections": {
"jd": "Job description and intake",
"details": "Position details",
"weights": "Matching weights",
"status": "Change status",
"activity": "Activity"
}
},
"errors": {
"generic": "Could not save. Check the values and try again.",
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/app/api/permission-matrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ vi.mock('@/server/candidates/duplicates', () => ({
vi.mock('@/server/candidates/confirm', () => ({
confirmCandidate: async () => ({ candidateId: 'c1', resolution: 'create', mergedIntoId: null }),
}));
vi.mock('@/server/positions/progress', () => ({
getPositionProgress: async () => ({ jd: 'none', successProfile: 'none', pendingDrafts: 0 }),
}));
vi.mock('@/server/candidates/place', () => ({
listIntakeDrafts: async () => [],
placeConfirmedCandidate: async () => ({ entryId: 'e1', added: true, canAnalyze: true }),
Expand Down Expand Up @@ -616,6 +619,7 @@ import { GET as pipelineGet } from './v1/agencies/[agencyId]/positions/[position
import { POST as batchPost } from './v1/agencies/[agencyId]/positions/[positionId]/analyses/route';
import { GET as reviewGet } from './v1/agencies/[agencyId]/positions/[positionId]/review/route';
import { GET as intakeDraftsGet } from './v1/agencies/[agencyId]/positions/[positionId]/intake-drafts/route';
import { GET as progressGet } from './v1/agencies/[agencyId]/positions/[positionId]/progress/route';
import {
PUT as orderPut,
DELETE as orderDelete,
Expand Down Expand Up @@ -1434,6 +1438,13 @@ const endpoints: Endpoint[] = [
allowed: AGENCY_ALL,
call: () => intakeDraftsGet(new Request('http://x.test/'), agencyParams),
},
{
name: 'GET position progress',
file: 'v1/agencies/[agencyId]/positions/[positionId]/progress/route.ts',
audience: 'agency',
allowed: AGENCY_ALL,
call: () => progressGet(new Request('http://x.test/'), agencyParams),
},
{
name: 'POST batch analysis',
file: 'v1/agencies/[agencyId]/positions/[positionId]/analyses/route.ts',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) 2026 Team of Wakers
// SPDX-License-Identifier: MIT
// route-audience: agency
import { authorizeAgency } from '@/lib/auth/authorize';
import { withAgency } from '@/lib/db';
import { handleRoute, ok } from '@/lib/http/responses';
import { getPositionProgress } from '@/server/positions/progress';

export const dynamic = 'force-dynamic';

/** Where the position stands in the hiring process (G#10). Anyone who can see the position. */
export async function GET(
_req: Request,
ctx: { params: Promise<{ agencyId: string; positionId: string }> },
): Promise<Response> {
return handleRoute(async () => {
const { agencyId, positionId } = await ctx.params;
const authed = await authorizeAgency(agencyId);
if (authed instanceof Response) return authed;
return ok(
await withAgency(
authed.agency.id,
(tx) => getPositionProgress(tx, authed.actor, positionId),
{ userId: authed.userId },
),
);
});
}
Loading
Loading