Compare Team — Admin Portal Integration Guide
This document is the Admin Portal-facing integration spec for the
POST /evaluation/:id/compare-team endpoint.
For the formal API reference, see
POST /evaluation/:id/compare-team
in the Evaluation Service docs.
Why this endpoint exists
A candidate completes an evaluation against a specific team (Team A). Admins
frequently want to know: “How would this same candidate score against Team
B / C / D?” — without re-collecting answers, without creating a new
evaluation, and without persisting hypothetical comparisons.
POST /evaluation/:id/compare-team reuses the candidate’s stored answers
and runs the algorithm against a different team’s members and answers.
Calling it for many target teams gives a “scan” view across the
organisation.
Endpoint summary
| Method & path | POST /evaluation/:id/compare-team |
| Service | Evaluation service (dev port 9103, prod via gateway at /evaluation) |
| Auth | Authorization: Bearer <accessToken> |
| Cache | Cache-Control: no-store (deterministic per inputs but not cacheable) |
| Persistence | None — pure read |
| Idempotent | Yes — same inputs → same output |
Path param
| Name | Type | Notes |
|---|---|---|
id |
string | The source evaluation UUID (created against Team A). |
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
algorithmTeamId |
integer (≥ 1) | ✓ | — | Target team to compare against. Must differ from the evaluation’s own team. |
roleId |
integer (≥ 1) | — | — | Optional role to also compute a role match (typically a role on the target team). |
algorithmUserId |
integer (≥ 0) | — | bridged from persona | Numeric algorithm user id used for role match UserExperience/UserGoal lookups. |
roleWeight |
number (0…100) | — | 50 |
Blend weight for combined Role + Workstyle score. Only used if a role match is computed. |
Response
{
"evaluationId": "abc-123",
"personaId": "3f4e2d1c-...",
"sourceTeam": { "algorithmTeamId": 1, "teamName": "Engineering Core" },
"targetTeam": { "algorithmTeamId": 2, "teamName": "Growth Squad" },
"workstyle": {
"overallScore": 67.8,
"overallCategory": "high",
"dimensions": [
{ "questionGroupId": 1, "name": "values", "scoreType": "TOP_TEN",
"scorePercentage": 72.4, "scoreCategory": "high", "weightPercentage": 100 }
// …16 dimensions
]
},
"roleMatch": null, // or { roleId, score, scoreCategory, experienceScore, goalScore, breakdown[] }
"combined": null, // or { score, scoreCategory, roleWeight }
"answersUsed": 158,
"computedAt": "2026-05-10T15:14:32.765Z"
}
Error responses
| HTTP | Body | When |
|---|---|---|
400 |
{ "message": "..." } |
Missing/invalid algorithmTeamId, no answers, or target = source |
401 |
{ "message": "Unauthorized" } |
Missing / invalid bearer token |
404 |
{ "message": "Evaluation not found" } |
Unknown evaluation id |
404 |
{ "message": "Target team {id} not found" } |
Unknown target team |
500 |
{ "message": "..." } |
Server error |
Admin Portal implementation
1. TypeScript types
// lib/api/evaluation-compare-team.ts
export interface EvaluationTeamRef {
algorithmTeamId: number;
teamName: string | null;
}
export interface EvaluationDimensionResult {
questionGroupId: number;
name: string;
scoreType: string;
scorePercentage: number;
scoreCategory: 'low' | 'medium' | 'high';
weightPercentage: number;
}
export interface EvaluationRoleMatch {
roleId: number;
score?: number;
scoreCategory?: 'low' | 'medium' | 'high';
experienceScore?: number;
goalScore?: number;
breakdown?: Array<Record<string, unknown>>;
error?: string;
}
export interface EvaluationCombinedScore {
score: number;
scoreCategory: 'low' | 'medium' | 'high';
roleWeight: number;
}
export interface CompareTeamRequest {
algorithmTeamId: number;
roleId?: number;
algorithmUserId?: number;
roleWeight?: number;
}
export interface CompareTeamResponse {
evaluationId: string;
personaId: string;
sourceTeam: EvaluationTeamRef;
targetTeam: EvaluationTeamRef;
workstyle: {
overallScore: number;
overallCategory: 'low' | 'medium' | 'high';
dimensions: EvaluationDimensionResult[];
};
roleMatch: EvaluationRoleMatch | null;
combined: EvaluationCombinedScore | null;
answersUsed: number;
computedAt: string;
}
2. Lib fetcher
import type { CompareTeamRequest, CompareTeamResponse } from './evaluation-compare-team';
import { EVAL_BASE } from './config';
export async function compareEvaluationToTeam(
evaluationId: string,
body: CompareTeamRequest,
token: string
): Promise<CompareTeamResponse> {
const res = await fetch(`${EVAL_BASE}/${evaluationId}/compare-team`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
cache: 'no-store',
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || `compareEvaluationToTeam ${res.status}`);
}
return res.json();
}
3. Recommended UX
Single-team compare (entry point)
On the Evaluation Detail page (where the original Team A results are
shown), add a “Compare to other team” action:
┌─ Evaluation a1b2c3d4 — Alice Rivera ─────────────────────────────┐
│ Team: Engineering Core Status: complete Overall: 72.5% HIGH │
│ │
│ [ Compare to other team ▾ ] ← opens team picker │
└──────────────────────────────────────────────────────────────────┘
Clicking opens a small picker (use the existing teams list endpoint to
populate). Filter out the source team from the dropdown — the API will
reject it with 400 if you don’t.
On submit, call compareEvaluationToTeam(id, { algorithmTeamId }) and
render the result as a secondary score panel beneath the original
results, clearly labelled with both team names.
Multi-team scan (power user)
For org-wide comparisons, run the call N times in parallel and render a
sortable table:
async function scanAllTeams(
evaluationId: string,
teamIds: number[],
token: string
) {
const results = await Promise.all(
teamIds.map((tid) =>
compareEvaluationToTeam(evaluationId, { algorithmTeamId: tid }, token)
.then((r) => ({ ok: true as const, r }))
.catch((e) => ({ ok: false as const, teamId: tid, error: e.message }))
)
);
return results;
}
Suggested table columns: Team, Workstyle %, Workstyle category,
Role match % (if computed), Combined %. Sort by Workstyle desc by
default. Highlight the source team’s own row (or omit it; API will reject).
With role match
Most admins will start with workstyle-only. Surface a secondary
“Include role match” toggle that, when enabled, lets the admin pick a
role from the target team. Only then send roleId (and let the server
auto-bridge algorithmUserId).
4. Score colour rules
Reuse the existing scoreCategory palette already used for the source
evaluation:
| Category | Suggested visual |
|---|---|
high |
success / green |
medium |
warning / amber |
low |
danger / red |
Apply the same rules to workstyle.overallCategory, each dimension’s
scoreCategory, roleMatch.scoreCategory, and combined.scoreCategory.
5. Loading & errors
- The endpoint is synchronous but does real work (algorithm + DB reads).
Show a spinner per call. Typical latency on local dev is < 500 ms. - Error states to handle in the UI:
- 400 same team — disable the source team in the picker; if it
still gets through, show “Pick a different team to compare against.” - 400 no answers — only relevant if admin opens the comparison panel
on apendingevaluation. Hide the action when
evaluation.progress.answered === 0. - 404 target team — show “That team was deleted or doesn’t exist.”
- 400 same team — disable the source team in the picker; if it
- The response is
Cache-Control: no-store. Don’t cache it across users.
Per-session in-memory caching keyed on(evaluationId, algorithmTeamId, roleId, algorithmUserId, roleWeight)is fine and recommended for the
multi-team scan view.
6. Layout & navigation
Recommended routes:
| Route | Purpose |
|---|---|
/admin/evaluations/[id] |
Existing evaluation detail page; add the Compare action here. |
/admin/evaluations/[id]/compare?team={n} |
Optional dedicated compare view for a single target team (deep-linkable). |
/admin/evaluations/[id]/scan |
Optional multi-team scan view (table). |
Smoke test (curl)
TOKEN="..."
EVAL_ID="..." # an evaluation with answers, source team = 1
# Compare against team 2 (workstyle only)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "algorithmTeamId": 2 }' \
http://localhost:9103/$EVAL_ID/compare-team \
| jq '{
source: .sourceTeam.teamName,
target: .targetTeam.teamName,
workstyle: .workstyle.overallScore,
category: .workstyle.overallCategory
}'
Expected:
{
"source": "Engineering Core",
"target": "Growth Squad",
"workstyle": 67.8,
"category": "high"
}
With role match:
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "algorithmTeamId": 2, "roleId": 2, "roleWeight": 50 }' \
http://localhost:9103/$EVAL_ID/compare-team \
| jq '{ workstyle: .workstyle.overallScore, role: .roleMatch.score, combined: .combined.score }'
Related endpoints
GET /evaluation/:id— original evaluation results (source team).POST /evaluation/:id/team-match— combined Role + Workstyle against the evaluation’s own team.POST /evaluation/compare— pairwise compatibility between two completed evaluations.