1
0
Fork 0
polaris-task-force/src/app/(frontend)/awards/actions.ts
Z8MB1E 2e1fbf8006 feat(awards): support bulk recipient selection in award grant flow
Refactors grantAward to accept an array of recipientUserIds, adds
duplicate-award and eligibility checks per recipient, and returns a
per-recipient result summary. GrantAwardDialog gains a third step
showing grant results when some recipients fail. RecipientPicker is
rewritten to support multi-select with chip-based selected state and
a maxSelections cap.
2026-09-02 16:14:07 -04:00

241 lines
6.9 KiB
TypeScript

"use server";
import config from "@payload-config";
import { getPayload } from "payload";
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
import { isSuperuser } from "@/utils/access-control/hasPermission";
import { emitGameEvent } from "@/utils/event-log/emit";
import { EventTypes } from "@/utils/event-log/eventTypes";
import {
countGrantsByGrantor,
findUserProfile,
getGrantorAssignments,
isRecipientEligible,
} from "@/lib/awards/grant";
import { cleanupDuplicateAwards, type CleanupResult } from "@/lib/awards/index";
import type { Assignment, Profile } from "@/payload-types";
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 };
}
export interface GrantResult {
recipientUserId: number;
success: boolean;
error?: string;
}
export async function grantAward(input: {
recipientUserIds: number[];
awardId: number;
reason?: string;
}): Promise<ActionResult<{ results: GrantResult[]; granted: number; failed: number }>> {
try {
const { payload, user } = await authenticate();
if (input.recipientUserIds.length === 0) {
return { success: false, error: "No recipients selected." };
}
let award;
try {
award = await payload.findByID({
collection: "awards",
id: input.awardId,
depth: 0,
overrideAccess: true,
});
} catch {
return { success: false, error: "Award not found." };
}
let grantorProfile: Profile | null = null;
const superuser = await isSuperuser(payload, user);
const isMedal = award.type === "medal";
if (isMedal && !superuser) {
return { success: false, error: "Only superusers can grant medals." };
}
let grantorAssignmentId: number | undefined;
let scope: "same_command" | "whitelist" | "anyone" | null | undefined;
let whitelist: (number | Assignment)[] | null | undefined;
let remaining: number | null = null;
if (!superuser && !isMedal) {
const allowedTypes = award.grantConfig?.allowedGrantorAssignmentTypes ?? [];
if (allowedTypes.length === 0) {
return { success: false, error: "This award cannot be granted by leaders." };
}
const grantorAssignments = await getGrantorAssignments(
payload,
user.id as number,
allowedTypes,
);
if (grantorAssignments.length === 0) {
return { success: false, error: "You are not authorized to grant this award." };
}
grantorAssignmentId = grantorAssignments[0].id;
scope = award.grantConfig?.recipientScope ?? "same_command";
whitelist = award.grantConfig?.eligibleRecipientAssignments;
const maxGrants = award.grantConfig?.maxGrantsPerGrantor ?? null;
if (maxGrants) {
grantorProfile = await findUserProfile(payload, user.id as number);
if (!grantorProfile) {
return { success: false, error: "You do not have a profile." };
}
const issued = await countGrantsByGrantor(payload, award.id, grantorProfile.id);
remaining = maxGrants - issued;
if (remaining <= 0) {
return {
success: false,
error: "You have reached the maximum number of grants for this award.",
};
}
}
}
if (!grantorProfile) {
grantorProfile = await findUserProfile(payload, user.id as number);
}
if (!grantorProfile) {
return { success: false, error: "You do not have a profile." };
}
const results: GrantResult[] = [];
let granted = 0;
let processed = 0;
for (const recipientUserId of input.recipientUserIds) {
if (remaining !== null && processed >= remaining) break;
if (grantorAssignmentId !== undefined) {
const eligible = await isRecipientEligible(
payload,
recipientUserId,
scope,
grantorAssignmentId,
whitelist,
);
if (!eligible) {
results.push({
recipientUserId,
success: false,
error: "Recipient is not eligible for this award.",
});
processed += 1;
continue;
}
}
const recipientProfile = await findUserProfile(payload, recipientUserId);
if (!recipientProfile) {
results.push({
recipientUserId,
success: false,
error: "Recipient does not have a profile.",
});
processed += 1;
continue;
}
const progression = recipientProfile.progression;
const currentAwards = progression?.awards ?? [];
const alreadyHasAward = currentAwards.some((e) => {
const id = typeof e.award === "number" ? e.award : e.award?.id;
return id === award.id;
});
if (alreadyHasAward) {
results.push({
recipientUserId,
success: false,
error: "Recipient already has this award.",
});
processed += 1;
continue;
}
await payload.update({
collection: "profiles",
id: recipientProfile.id,
data: {
progression: {
...progression,
awards: [
...currentAwards,
{
award: award.id,
awardedBy: grantorProfile.id,
awardedAt: new Date().toISOString(),
awardReason: input.reason ?? null,
},
],
},
},
overrideAccess: true,
});
await emitGameEvent(payload, {
type: EventTypes.AwardGrant,
message: `Granted "${award.name}" to recipient profile #${recipientProfile.id}`,
actor: user.id,
data: { awardId: award.id, grantorProfileId: grantorProfile.id },
});
results.push({ recipientUserId, success: true });
granted += 1;
processed += 1;
}
return {
success: true,
data: { results, granted, failed: results.length - granted },
};
} catch (e) {
return {
success: false,
error: e instanceof Error ? e.message : "Unknown error",
};
}
}
export async function cleanupDuplicateAwardsAction(): Promise<
ActionResult<CleanupResult>
> {
try {
const { payload, user } = await authenticate();
const superuser = await isSuperuser(payload, user);
if (!superuser) {
return { success: false, error: "Only superusers can run award cleanup." };
}
const result = await cleanupDuplicateAwards(payload);
return { success: true, data: result };
} catch (e) {
return {
success: false,
error: e instanceof Error ? e.message : "Unknown error",
};
}
}