diff --git a/src/app/(frontend)/profile/[username]/actions.ts b/src/app/(frontend)/profile/[username]/actions.ts new file mode 100644 index 0000000..be1f522 --- /dev/null +++ b/src/app/(frontend)/profile/[username]/actions.ts @@ -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 { + 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>, + 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> { + 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>; + subordinate: Awaited>; + }; + } + | { 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" }; + } +}