"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" }; } }