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();
}

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.

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).

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 a pending evaluation. Hide the action when
      evaluation.progress.answered === 0.
    • 404 target team — show “That team was deleted or doesn’t exist.”
  • 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 }'