import type { ClinicianInvitation } from '../services/ClinicianInvitationService.tsx'; import { getPractitionerRoleWorkspaces, STAFF_WORKSPACES } from './staffRoleCatalog'; import { formatDayMonthYear } from './dateUtils'; /** * WHAT AN INVITATION IS, IN THE WORDS THE PRACTICE READS, and the one act that moves it on. * * Pending, accepted and eligible are distinct, and none of them holds a seat. An invitation that was * saved but never reached the inbox says "Invitation not sent" and offers "Retry invitation" (admin-v2 * implementation notes, save and resume contract). */ export type InvitationTone = 'plain' | 'attention' | 'done' | 'danger' | 'ended'; export type InvitationAction = 'send' | 'retry' | 'resend' | 'renew' | 'assign-seat' | 'set-up' | 'none'; export interface InvitationState { words: string; tone: InvitationTone; detail: string; action: InvitationAction; /** Whether it may still be revoked: only an open, unaccepted invitation can. */ revocable: boolean; } const dateOf = (iso: string | undefined) => formatDayMonthYear(iso); /** `seated` is what the invitee's role says now: true or false once read, undefined when it could not be. */ export const invitationStateOf = (invitation: ClinicianInvitation, { seated }: { seated?: boolean } = {}): InvitationState => { if (invitation.status === 'revoked') { return { words: 'Revoked', tone: 'ended', detail: 'The link no longer works.', action: 'none', revocable: false }; } // Accepted with no role id published: the admission held, or a role was made and its id not yet recorded. Which // one is not known here, and the invitee's own retry finishes either. The server neither revokes nor resends it. if (invitation.status === 'accepted' && !invitation.practitionerRoleId) { return { words: 'Accepted - joining not finished', tone: 'attention', detail: 'They accepted the invitation, but joining this practice is not finished. Their access here is not confirmed yet.', action: 'none', revocable: false, }; } if (invitation.status === 'accepted') { if (seated) { return { words: 'Accepted', tone: 'done', detail: 'They signed in and hold a clinician seat here. Services and availability are next.', action: 'set-up', revocable: false, }; } return { words: 'Accepted', tone: 'done', detail: seated === false ? 'They signed in and hold a practitioner role here. No clinician seat is assigned yet.' : 'They signed in and hold a practitioner role here.', action: 'assign-seat', revocable: false, }; } if (invitation.status === 'expired') { return { words: 'Invitation expired', tone: 'attention', detail: `The link stopped working on ${dateOf(invitation.expiresAt)}. A new link restarts the clock.`, action: 'renew', revocable: true, }; } if (invitation.attention === 'identity-conflict') { return { words: 'Identity needs attention', tone: 'attention', detail: 'Their NPI matches another practitioner record. Nothing was linked; this needs checking before they can join.', action: 'none', revocable: true, }; } if (invitation.delivery === 'draft') { return { words: 'Invitation draft saved', tone: 'plain', detail: 'No invitation has been sent and no clinician seat is assigned. Review the details, then send the invitation.', action: 'send', revocable: true, }; } if (invitation.delivery === 'unconfirmed') { return { words: 'Invitation email not confirmed', tone: 'attention', detail: 'The invitation is saved, but we could not confirm its email was sent. It may still arrive. Sending another link makes only the newest one work. No seat is assigned.', action: 'resend', revocable: true, }; } if (invitation.delivery === 'failed' || invitation.delivery === 'not-sent') { return { words: 'Invitation not sent', tone: 'danger', detail: invitation.delivery === 'not-sent' ? 'The practice has no usable staff sign-in address to link to. Set one, then retry.' : 'The invitation is saved, but its email could not be sent. No seat is assigned.', action: 'retry', revocable: true, }; } return { words: 'Awaiting acceptance', tone: 'plain', detail: `Sent to ${invitation.email}. The link works until ${dateOf(invitation.expiresAt)}. No seat is assigned.`, action: 'resend', revocable: true, }; }; /** Invitations worth showing on the step: everything except the ones the practice withdrew. */ export const openInvitations = (invitations: ClinicianInvitation[]): ClinicianInvitation[] => invitations.filter((invitation) => invitation.status !== 'revoked'); /** A clinician invitation, or a staff one that seats its person in the Practitioner workspace. */ export const invitesAClinician = (invitation: ClinicianInvitation): boolean => invitation.kind !== 'staff' || getPractitionerRoleWorkspaces(invitation.practitionerRole).includes(STAFF_WORKSPACES.PRACTITIONER); /** The first-clinician step's list: open invitations that bring a clinician, never a receptionist's. */ export const openClinicianInvitations = (invitations: ClinicianInvitation[]): ClinicianInvitation[] => openInvitations(invitations).filter(invitesAClinician);