pelagia-portal/App/lib/crew-login.ts
Hardik df3b4bdc97
All checks were successful
PR checks / checks (pull_request) Successful in 42s
PR checks / integration (pull_request) Successful in 30s
feat(crewing): resolve self-contained deferred follow-ups (flagged)
Clears the self-contained deferrals tracked across phases. Stacks on 5b appraisal.
Behind NEXT_PUBLIC_CREWING_ENABLED.

- SITE_STAFF login on onboard/placement (Epic D follow-up): lib/crew-login.ts
  maybeCreateSiteStaffLogin creates a passwordless SITE_STAFF User (sharing the
  CRW- employee no., siteId = the assignment's site) when a grantsLogin rank is
  onboarded (onboardCandidate) or placed (placeCrew) and the crew member has an
  email. No-op otherwise.
- Own-site scoping (Epic E follow-up, §8.7): User.siteId added (migration
  crewing_followups); the Crew directory filters a SITE_STAFF user with a home site
  to crew whose active assignment is at that site (graceful when unset). The link is
  set at login creation.
- PPE / next-of-kin verify gates (Epic F/I follow-up): PpeIssue/NextOfKin gained
  verificationStatus + verifiedById; verifyPpe / verifyNextOfKin (verify_site_records,
  MPO) + queue sections in /crewing/verification.

Tests & docs
- Integration: crewing-followups.test.ts (6) — login created/skipped by rank+email
  (+ siteId set), PPE/NoK verify + reject-reason + already-decided guard + gating.
  type-check clean; full unit (245) + integration (211) green (RESEND_API_KEY unset).
- CLAUDE.md updated.

Part of Epic D (#78), Epic E (#79), Epic F (#80), Epic I (#83).
Still deferred (not self-contained): public careers API (A2); Pay-status pay rows (Phase 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:28:23 +05:30

37 lines
1.3 KiB
TypeScript

import type { Prisma } from "@prisma/client";
// Promote a crew member to a portal login when their rank grants one (PM /
// Assistant PM / Site In-charge — Rank.grantsLogin, spec §3/§4.1). Called from
// onboarding and direct placement, inside their transaction. Creates a SITE_STAFF
// User with no password (set later via the profile / SSO). No-op when the rank
// doesn't grant a login, the crew member has no email/employee no., or a matching
// user already exists. Returns true when a login was created.
export async function maybeCreateSiteStaffLogin(
tx: Prisma.TransactionClient,
crew: { name: string; email: string | null; employeeId: string | null },
rankId: string,
siteId?: string | null
): Promise<boolean> {
const rank = await tx.rank.findUnique({ where: { id: rankId }, select: { grantsLogin: true } });
if (!rank?.grantsLogin) return false;
if (!crew.email || !crew.employeeId) return false;
const existing = await tx.user.findFirst({
where: { OR: [{ email: crew.email }, { employeeId: crew.employeeId }] },
select: { id: true },
});
if (existing) return false;
await tx.user.create({
data: {
employeeId: crew.employeeId,
email: crew.email,
name: crew.name,
role: "SITE_STAFF",
passwordHash: null,
siteId: siteId ?? null,
},
});
return true;
}