Final slice of Phase 5. The appraisal lifecycle raise → verify → approve across three role-gated surfaces, per Crewing-Implementation-Spec §5.4/§8.14. Stacks on 5a verification. Behind NEXT_PUBLIC_CREWING_ENABLED. Completes Phase 5. What's in - Schema: Appraisal (on CrewAssignment) + AppraisalStatus (DRAFT/SUBMITTED/MPO_VERIFIED/MANAGER_APPROVED/REJECTED); CrewActionType += APPRAISAL_SUBMITTED/VERIFIED/APPROVED/REJECTED. Migration crewing_appraisal. - State machine lib/appraisal-state-machine.ts: verify (SUBMITTED→MPO_VERIFIED, MPO/Manager), approve (MPO_VERIFIED→MANAGER_APPROVED, Manager); orthogonal reject. - Actions (crewing/appraisals/actions.ts): raiseAppraisal (raise_appraisal — PM/ site staff), verifyAppraisal (verify_appraisal — MPO), approveAppraisal (approve_appraisal — Manager); reject paths require remarks; notifications APPRAISAL_FOR_VERIFICATION / APPRAISAL_FOR_APPROVAL. - Three surfaces (§8.14): PM raises + tracks status on the crew-profile Appraisals tab; MPO verifies in the Verification queue (Appraisals section); Manager approves in the central /approvals queue (Appraisal kind). Tests & docs - Unit: appraisal-state-machine.test.ts (4). Integration: appraisal.test.ts (4) — raise→verify→approve happy path, MPO reject, permission gating (MPO can't raise, site staff can't verify, MPO can't approve). type-check clean; full unit (245) + integration (205) green (verified with RESEND_API_KEY unset). - CLAUDE.md updated — completes Phase 5 (I + H). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
40 lines
1.5 KiB
TypeScript
40 lines
1.5 KiB
TypeScript
import type { AppraisalStatus, Role } from "@prisma/client";
|
|
|
|
// Appraisal lifecycle (Crewing-Implementation-Spec §5.4) — mirrors the other
|
|
// crewing state machines. A PM raises the appraisal directly into SUBMITTED; this
|
|
// machine governs the two review advances. Rejection is orthogonal (handled in
|
|
// the actions: an MPO or Manager declines → REJECTED with remarks).
|
|
|
|
export type AppraisalAction = "verify" | "approve";
|
|
|
|
interface Transition {
|
|
to: AppraisalStatus;
|
|
allowedRoles: Role[];
|
|
}
|
|
|
|
const VERIFY_ROLES: Role[] = ["MANNING", "MANAGER", "SUPERUSER"]; // verify_appraisal
|
|
const APPROVE_ROLES: Role[] = ["MANAGER", "SUPERUSER"]; // approve_appraisal
|
|
|
|
const TRANSITIONS: Partial<Record<AppraisalStatus, Partial<Record<AppraisalAction, Transition>>>> = {
|
|
SUBMITTED: {
|
|
verify: { to: "MPO_VERIFIED", allowedRoles: VERIFY_ROLES },
|
|
},
|
|
MPO_VERIFIED: {
|
|
approve: { to: "MANAGER_APPROVED", allowedRoles: APPROVE_ROLES },
|
|
},
|
|
};
|
|
|
|
export function getTransition(from: AppraisalStatus, action: AppraisalAction): Transition | null {
|
|
return TRANSITIONS[from]?.[action] ?? null;
|
|
}
|
|
|
|
export function canPerformAction(from: AppraisalStatus, action: AppraisalAction, role: Role): boolean {
|
|
return getTransition(from, action)?.allowedRoles.includes(role) ?? false;
|
|
}
|
|
|
|
// A review may be declined while the appraisal is still in flight.
|
|
const REJECTABLE_FROM: AppraisalStatus[] = ["SUBMITTED", "MPO_VERIFIED"];
|
|
|
|
export function canReject(from: AppraisalStatus): boolean {
|
|
return REJECTABLE_FROM.includes(from);
|
|
}
|