1
0
Fork 0
polaris-task-force/src/app/(frontend)/roster/actions.ts
Z8MB1E 62573f74e6 feat(promotions): add promotion nominations and leadership transfers
- Promotion nominations: members can nominate a peer for promotion from their
  profile, and a new /personnel/promotions page lets staff review and action
  the nominations (with an optional note on each).
- Leadership transfers: a member's leadership can be handed over to another
  member, surfaced on the transfers page and in the roster (which also gains a
  staff remove-member dialog).
- Adds the PromotionNominations and LeadershipTransfers collections (registered
  in the Payload config), their service libs, the migration, and integration
  coverage.
2026-10-03 03:29:24 -04:00

94 lines
3.1 KiB
TypeScript

"use server";
import config from "@payload-config";
import { getPayload } from "payload";
import type { LeadershipTransfer } from "@/payload-types";
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
import {
cancelLeadershipTransfer as cancelLeadershipTransferCore,
requestLeadershipTransfer as requestLeadershipTransferCore,
resolveLeadershipTransfer as resolveLeadershipTransferCore,
} from "@/lib/transfers/leadership";
export interface ActionResult<T = undefined> {
readonly success: boolean;
readonly error?: string;
readonly data?: T;
}
/**
* Web actions for leadership-initiated removals. The roster page's client
* components call requestDivisionRemoval/cancelDivisionRemoval; the transfers
* page's leadership section calls resolveLeadershipTransfer. The lib re-validates
* every guard (leader authority, pending-only transitions, superuser-only
* resolution), so these actions only authenticate and delegate.
*/
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 };
}
function actionFailure<T>(caughtError: unknown): ActionResult<T> {
const message = caughtError instanceof Error ? caughtError.message : "Unknown error";
return {
success: false,
error: message === "Unauthorized" ? "You must be logged in." : message,
};
}
export async function requestDivisionRemoval(input: {
readonly subjectId: number;
readonly fromAssignmentId: number;
readonly reason?: string;
}): Promise<ActionResult<LeadershipTransfer>> {
try {
const { payload, user } = await authenticate();
const transfer = await requestLeadershipTransferCore(payload, user, {
subjectId: input.subjectId,
fromAssignmentId: input.fromAssignmentId,
...(input.reason !== undefined ? { reason: input.reason } : {}),
});
return { success: true, data: transfer };
} catch (caughtError) {
return actionFailure<LeadershipTransfer>(caughtError);
}
}
export async function cancelDivisionRemoval(
transferId: number,
): Promise<ActionResult<LeadershipTransfer>> {
try {
const { payload, user } = await authenticate();
const transfer = await cancelLeadershipTransferCore(payload, user, transferId);
return { success: true, data: transfer };
} catch (caughtError) {
return actionFailure<LeadershipTransfer>(caughtError);
}
}
export async function resolveLeadershipTransfer(
transferId: number,
approve: boolean,
note?: string,
): Promise<ActionResult<LeadershipTransfer>> {
try {
const { payload, user } = await authenticate();
const transfer = await resolveLeadershipTransferCore(payload, user, transferId, {
approve,
...(note !== undefined ? { note } : {}),
});
return { success: true, data: transfer };
} catch (caughtError) {
return actionFailure<LeadershipTransfer>(caughtError);
}
}