Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
152 lines
4.5 KiB
TypeScript
152 lines
4.5 KiB
TypeScript
"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" };
|
|
}
|
|
}
|