1
0
Fork 0

feat(evaluations): add profile evaluation actions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-08-27 18:36:04 -04:00
parent a21770e54e
commit 959f65b51e

View file

@ -0,0 +1,152 @@
"use server";
import config from "@payload-config";
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
import { getPayload } from "payload";
import type { User } from "@/payload-types";
import { emitGameEvent } from "@/utils/event-log/emit";
import { EventTypes } from "@/utils/event-log/eventTypes";
import { isPerformanceLevelId } from "@/lib/evaluations/levels";
import {
evaluateEligibility,
getLatestSubordinateLevel,
getLeaderAggregate,
upsertEvaluation,
} from "@/lib/evaluations";
interface ActionResult<T = undefined> {
success: boolean;
error?: string;
data?: T;
}
async function authenticate() {
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
const { headers } = await import("next/headers");
const hdrs = await headers();
const { user } = await payload.auth({
headers: hdrs,
canSetHeaders: false,
});
if (!isPayloadUser(user)) {
throw new Error("Unauthorized");
}
return { payload, user };
}
async function findUserByUsername(
payload: Awaited<ReturnType<typeof getPayload>>,
username: string,
) {
const res = await payload.find({
collection: "users",
where: { username: { equals: username } },
limit: 1,
depth: 0,
overrideAccess: true,
});
return (res.docs[0] as { id: number; username: string } | undefined) ?? null;
}
/**
* Submit (or re-submit) a leadership evaluation for the given profile. The kind
* is derived server-side from the rater/ratee/mission relationship — it is never
* accepted from the client. Eligibility is the permission gate here; any logged-in
* user with a qualifying relationship may evaluate.
*/
export async function submitEvaluation(input: {
username: string;
missionId: number;
level: string;
comment?: string;
}): Promise<ActionResult<{ created: boolean }>> {
try {
const { payload, user } = await authenticate();
if (!isPerformanceLevelId(input.level)) {
return { success: false, error: "Unknown performance level." };
}
const ratee = await findUserByUsername(payload, input.username);
if (!ratee) {
return { success: false, error: "User not found." };
}
const eligibility = await evaluateEligibility(payload, {
raterId: user.id as number,
rateeId: ratee.id,
missionId: input.missionId,
});
if (!eligibility.eligible || !eligibility.kind) {
return { success: false, error: eligibility.reason ?? "Not eligible to evaluate." };
}
const { evaluation, created } = await upsertEvaluation(payload, {
raterId: user.id as number,
rateeId: ratee.id,
missionId: input.missionId,
kind: eligibility.kind,
level: input.level,
comment: input.comment,
});
// No actor on the event: the event log is readable by any logged-in user and
// must not leak who rated whom.
await emitGameEvent(payload, {
type: EventTypes.EvaluationSubmit,
message: "A leadership evaluation was submitted.",
targetCollection: "evaluations",
targetId: evaluation.id,
data: { kind: eligibility.kind },
});
return { success: true, data: { created } };
} catch (e) {
if (e instanceof Error && e.message === "Unauthorized") throw e;
return {
success: false,
error: e instanceof Error ? e.message : "Unknown error",
};
}
}
/**
* Rater-free evaluation summary for a profile page. Any logged-in user may call
* this; the output contains no rater identity or comments — only the anonymous
* leader aggregate (with its visibility threshold) and the latest subordinate
* level.
*/
export async function getEvaluationSummary(
username: string,
): Promise<
| {
success: true;
data: {
leader: Awaited<ReturnType<typeof getLeaderAggregate>>;
subordinate: Awaited<ReturnType<typeof getLatestSubordinateLevel>>;
};
}
| { success: false; error: string }
> {
try {
const { payload } = await authenticate();
const ratee = await findUserByUsername(payload, username);
if (!ratee) {
return { success: false, error: "User not found." };
}
const [leader, subordinate] = await Promise.all([
getLeaderAggregate(payload, ratee.id),
getLatestSubordinateLevel(payload, ratee.id),
]);
return { success: true, data: { leader, subordinate } };
} catch (e) {
if (e instanceof Error && e.message === "Unauthorized") throw e;
return { success: false, error: e instanceof Error ? e.message : "Unknown error" };
}
}