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:
parent
a21770e54e
commit
959f65b51e
1 changed files with 152 additions and 0 deletions
152
src/app/(frontend)/profile/[username]/actions.ts
Normal file
152
src/app/(frontend)/profile/[username]/actions.ts
Normal 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" };
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue