- 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.
94 lines
3.1 KiB
TypeScript
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);
|
|
}
|
|
}
|