1
0
Fork 0

feat(profile): integrate evaluations into profile pages

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 b8ba036965
commit 6bbafc7c5a

View file

@ -1,10 +1,16 @@
import config from "@payload-config";
import { getPayload } from "payload";
import { notFound } from "next/navigation";
import { headers as nextHeaders } from "next/headers";
import Link from "next/link";
import type { Assignment, Media, Qualification, Rank, User } from "@/payload-types";
import { RichText } from "@payloadcms/richtext-lexical/react";
import { resolveLevel } from "@/utils/xp/resolveLevel";
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
import { isSuperuser } from "@/utils/access-control/hasPermission";
import { isDirectLeaderOf, isMissionEvaluable } from "@/lib/evaluations";
import { PERFORMANCE_LEVELS } from "@/lib/evaluations/levels";
import { getEvaluationSummary } from "./actions";
import {
ArrowLeftIcon,
AwardIcon,
@ -34,14 +40,128 @@ import {
} from "@/components/ui/item";
import AwardCard from "@/components/frontend/account/AwardCard";
import { RibbonRack } from "@/components/frontend/account/RibbonRack";
import { LeadershipEvaluationSection } from "@/components/frontend/profile/LeadershipEvaluationSection";
import { getLeadershipDisplayLabel } from "@/lib/evaluations/levels";
import type {
EvaluationMissionOption,
EvaluationSummary,
LatestMissionRow,
} from "@/components/frontend/profile/evaluation-types";
interface ProfilePageProps {
params: Promise<{ username: string }>;
}
type Payload = Awaited<ReturnType<typeof getPayload>>;
async function findEligibleEvaluationMissions(
payload: Payload,
viewerId: number,
now: Date,
): Promise<readonly EvaluationMissionOption[]> {
const attendanceRes = await payload.find({
collection: "mission-attendances",
where: {
and: [{ user: { equals: viewerId } }, { response: { equals: "yes" } }],
},
limit: 100,
depth: 0,
select: { mission: true },
overrideAccess: true,
});
const missionIds = [
...new Set(
attendanceRes.docs.map((attendance) =>
typeof attendance.mission === "number" ? attendance.mission : attendance.mission.id,
),
),
];
if (missionIds.length === 0) return [];
const missionRes = await payload.find({
collection: "missions",
where: { id: { in: missionIds } },
limit: missionIds.length,
depth: 0,
select: {
name: true,
classification: { startDateTime: true },
ownershipAndStatus: { status: true },
},
overrideAccess: true,
});
return [...missionRes.docs]
.filter((mission) =>
isMissionEvaluable(
mission.ownershipAndStatus.status,
mission.classification.startDateTime,
now,
),
)
.sort(
(first, second) =>
new Date(second.classification.startDateTime).getTime() -
new Date(first.classification.startDateTime).getTime(),
)
.map((mission) => ({
id: mission.id,
name: mission.name,
startDateTime: mission.classification.startDateTime,
}));
}
async function findLatestLeaderMissionRows(
payload: Payload,
rateeId: number,
): Promise<readonly LatestMissionRow[]> {
const evaluationRes = await payload.find({
collection: "evaluations",
where: {
and: [{ ratee: { equals: rateeId } }, { kind: { in: ["leader-direct", "leader-indirect"] } }],
},
sort: "-updatedAt",
limit: 10,
depth: 1,
select: { mission: true, level: true, updatedAt: true },
overrideAccess: true,
});
const missionIds = new Set<number>();
const rows: LatestMissionRow[] = [];
for (const evaluation of evaluationRes.docs) {
if (typeof evaluation.mission === "number") continue;
const level = PERFORMANCE_LEVELS.find((candidate) => candidate.id === evaluation.level);
if (!level || missionIds.has(evaluation.mission.id)) continue;
missionIds.add(evaluation.mission.id);
rows.push({
missionId: evaluation.mission.id,
missionName: evaluation.mission.name,
startDateTime: evaluation.mission.classification.startDateTime,
level: {
id: level.id,
label: getLeadershipDisplayLabel(level),
score: level.score,
color: level.color,
},
});
if (rows.length === 5) break;
}
return rows;
}
export default async function ProfilePage({ params }: ProfilePageProps) {
const { username } = await params;
const payload = await getPayload({ config });
const { user: authUser } = await payload.auth({
headers: await nextHeaders(),
canSetHeaders: false,
});
const viewer = isPayloadUser(authUser) ? authUser : null;
const profileRes = await payload
.find({
@ -70,6 +190,69 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
const profile = profileRes.docs[0];
const user = profile.user as User;
const isProfileOwner = viewer?.id === user.id;
const isLegacyPrivileged =
viewer?.roles?.some((role) => role === "admin" || role === "developer") === true;
const evaluationSummaryPromise = viewer ? getEvaluationSummary(username) : Promise.resolve(null);
const eligibleMissionsPromise: Promise<readonly EvaluationMissionOption[]> = viewer
? findEligibleEvaluationMissions(payload, viewer.id, new Date())
: Promise.resolve([]);
const privilegedViewerPromise: Promise<boolean> = viewer
? isSuperuser(payload, viewer)
: Promise.resolve(false);
const directLeaderPromise: Promise<boolean> =
viewer && !isProfileOwner
? isDirectLeaderOf(payload, viewer.id, user.id)
: Promise.resolve(false);
const [evaluationSummaryResult, eligibleMissions, isSuperuserViewer, isDirectLeader] =
await Promise.all([
evaluationSummaryPromise,
eligibleMissionsPromise,
privilegedViewerPromise,
directLeaderPromise,
]);
const canViewSubordinate =
isProfileOwner || isLegacyPrivileged || isSuperuserViewer || isDirectLeader;
const latestMissionRows =
evaluationSummaryResult?.success && evaluationSummaryResult.data.leader.visible
? await findLatestLeaderMissionRows(payload, user.id)
: [];
const evaluationSummary: EvaluationSummary | null = evaluationSummaryResult?.success
? {
leader: {
totalRaters: evaluationSummaryResult.data.leader.totalRaters,
visible: evaluationSummaryResult.data.leader.visible,
averageScore: evaluationSummaryResult.data.leader.averageScore,
levels:
evaluationSummaryResult.data.leader.levels?.map((level) => ({
id: level.id,
label: level.label,
score: level.score,
color: level.color,
count: level.count,
percentage: level.percentage,
})) ?? null,
},
subordinate:
canViewSubordinate && evaluationSummaryResult.data.subordinate
? {
level: {
id: evaluationSummaryResult.data.subordinate.level.id,
label: evaluationSummaryResult.data.subordinate.level.label,
score: evaluationSummaryResult.data.subordinate.level.score,
color: evaluationSummaryResult.data.subordinate.level.color,
},
missionId: evaluationSummaryResult.data.subordinate.missionId,
at: evaluationSummaryResult.data.subordinate.at,
}
: null,
}
: null;
const evaluationSummaryError =
evaluationSummaryResult && !evaluationSummaryResult.success
? evaluationSummaryResult.error
: null;
const canSubmitEvaluation = viewer !== null && !isProfileOwner;
const rank = profile.rank as Rank;
const awards = (profile.progression?.awards ?? []).flatMap((entry) => {
if (typeof entry !== "object" || entry === null) return [];
@ -87,7 +270,7 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
collection: "experience",
sort: "requiredPoints",
limit: 100,
select: { name: true, requiredPoints: true },
select: { name: true, description: true, requiredPoints: true },
});
const xpLevel = resolveLevel(xp, experienceRes.docs);
@ -236,6 +419,16 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
)}
</section>
<LeadershipEvaluationSection
username={username}
summary={evaluationSummary}
summaryError={evaluationSummaryError}
eligibleMissions={eligibleMissions}
latestMissionRows={latestMissionRows}
canSubmitEvaluation={canSubmitEvaluation}
canViewSubordinate={canViewSubordinate}
/>
{/* ── Dossier ────────────────────────────────────────── */}
<section className="flex flex-col gap-3">
<div className="flex items-center gap-2">