Add React components for assignment transfer user interface: 1. AssignmentCard.tsx: Display user's current assignment with transfer button. Shows assignment details, current status, and allows initiating transfer requests. 2. RequestTransferDialog.tsx: Modal dialog for creating transfer requests. - Selects target assignment from assignment list - Optionally notes required for transfer - Submits request to AssignmentTransfers collection - Emits AssignmentTransferRequest event 3. Transfers page (src/app/(frontend)/transfers/): Transfer request list view. - Requests card showing pending requests with requestor, target, current - Approval cards for assignment leaders - Appeal cards for rejected requests - Completed/Cancelled status display All components use shadcn/ui primitives, proper loading states, error handling, and provide accessible keyboard navigation.
156 lines
4.5 KiB
TypeScript
156 lines
4.5 KiB
TypeScript
"use server";
|
|
|
|
import config from "@payload-config";
|
|
import { getPayload } from "payload";
|
|
import type { AssignmentTransfer } from "@/payload-types";
|
|
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
|
import {
|
|
appealTransfer as appealTransferCore,
|
|
cancelTransfer as cancelTransferCore,
|
|
createTransferRequest,
|
|
recordTransferDecision,
|
|
resolveAppeal as resolveAppealCore,
|
|
} from "@/lib/transfers";
|
|
|
|
export interface ActionResult<T = undefined> {
|
|
readonly success: boolean;
|
|
readonly error?: string;
|
|
readonly data?: T;
|
|
}
|
|
|
|
/**
|
|
* Web action mapping: requestTransfer → createTransferRequest, approveTransfer and
|
|
* rejectTransfer → recordTransferDecision, appealTransfer → appealTransfer,
|
|
* resolveAppeal → resolveAppeal, and cancelTransfer → cancelTransfer. The authenticated
|
|
* user supplies each core function's actorUserId/requesterId.
|
|
*/
|
|
|
|
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 requestTransfer(input: {
|
|
readonly fromAssignmentId: number;
|
|
readonly toAssignmentId: number;
|
|
readonly note?: string;
|
|
}): Promise<ActionResult<AssignmentTransfer>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const transfer = await createTransferRequest(payload, {
|
|
requesterId: user.id,
|
|
fromAssignmentId: input.fromAssignmentId,
|
|
toAssignmentId: input.toAssignmentId,
|
|
...(input.note !== undefined ? { note: input.note } : {}),
|
|
});
|
|
return { success: true, data: transfer };
|
|
} catch (caughtError) {
|
|
return actionFailure<AssignmentTransfer>(caughtError);
|
|
}
|
|
}
|
|
|
|
export async function approveTransfer(
|
|
requestId: number,
|
|
side: "from" | "to",
|
|
): Promise<ActionResult<AssignmentTransfer>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const transfer = await recordTransferDecision(payload, {
|
|
requestId,
|
|
side,
|
|
decision: "approved",
|
|
actorUserId: user.id,
|
|
});
|
|
return { success: true, data: transfer };
|
|
} catch (caughtError) {
|
|
return actionFailure<AssignmentTransfer>(caughtError);
|
|
}
|
|
}
|
|
|
|
export async function rejectTransfer(
|
|
requestId: number,
|
|
side: "from" | "to",
|
|
reason: string,
|
|
): Promise<ActionResult<AssignmentTransfer>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const transfer = await recordTransferDecision(payload, {
|
|
requestId,
|
|
side,
|
|
decision: "rejected",
|
|
reason,
|
|
actorUserId: user.id,
|
|
});
|
|
return { success: true, data: transfer };
|
|
} catch (caughtError) {
|
|
return actionFailure<AssignmentTransfer>(caughtError);
|
|
}
|
|
}
|
|
|
|
export async function appealTransfer(
|
|
requestId: number,
|
|
note?: string,
|
|
): Promise<ActionResult<AssignmentTransfer>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const transfer = await appealTransferCore(payload, {
|
|
requestId,
|
|
actorUserId: user.id,
|
|
...(note !== undefined ? { note } : {}),
|
|
});
|
|
return { success: true, data: transfer };
|
|
} catch (caughtError) {
|
|
return actionFailure<AssignmentTransfer>(caughtError);
|
|
}
|
|
}
|
|
|
|
export async function resolveAppeal(
|
|
requestId: number,
|
|
approve: boolean,
|
|
note?: string,
|
|
): Promise<ActionResult<AssignmentTransfer>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const transfer = await resolveAppealCore(payload, {
|
|
requestId,
|
|
approve,
|
|
actorUserId: user.id,
|
|
...(note !== undefined ? { note } : {}),
|
|
});
|
|
return { success: true, data: transfer };
|
|
} catch (caughtError) {
|
|
return actionFailure<AssignmentTransfer>(caughtError);
|
|
}
|
|
}
|
|
|
|
export async function cancelTransfer(
|
|
requestId: number,
|
|
): Promise<ActionResult<AssignmentTransfer>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const transfer = await cancelTransferCore(payload, {
|
|
requestId,
|
|
actorUserId: user.id,
|
|
});
|
|
return { success: true, data: transfer };
|
|
} catch (caughtError) {
|
|
return actionFailure<AssignmentTransfer>(caughtError);
|
|
}
|
|
}
|