import { ElementType, ReactNode } from "react"; /** One assignable role offered in the Add User and Change Role selects. */ export type RoleOption = { /** Identifier passed back as `roleId` on change; compared by `label`, not id, to pre-select the current role. */ id: number; /** Role name shown in the select and matched case-insensitively against `SelectedUserDetail.role`. */ label: string; }; /** One row in the left-panel user list — see SelectedUserDetail for the richer shape used once selected. */ export type UserListItem = { /** Row key; compared with `selectedUserId` to highlight the selected row. */ id: number; /** Joined with `lastName` as the row title and avatar initials. */ firstName: string; /** Joined after `firstName` as the row title. */ lastName: string; /** Secondary line under the name in the row. */ email: string; /** Display-only — UserDetails never reads this in the list row itself, only in SelectedUserDetail once selected. */ role: string; /** Avatar image URL; initials show when omitted. */ profile?: string; /** Selects the green Active or red Inactive badge on the row. */ isActive: boolean; /** isPending/isCurrent/isSuperAdmin here are decorative — only SelectedUserDetail's copies (after selection) drive any badge or action-bar behavior. */ isPending?: boolean; isCurrent?: boolean; isSuperAdmin?: boolean; }; /** One label/value row in the "Personal Information" panel; icon falls back to a generic user icon when omitted. */ export type UserDetailRow = { /** Caption on the left of the row. */ label: string; /** Text shown for this field. */ value: string; /** Lucide-style icon component for the row tile; a generic user icon when omitted. */ icon?: ElementType; }; /** Right-panel shape for the selected user — distinct field names from UserListItem (name, userId); not derived from it. */ export type SelectedUserDetail = { /** When null, Change Role and Resend Invitation silently no-op on click (no disabled state) — unlike Deactivate, which greys out. */ userId?: number; /** Heading and avatar name in the detail header; also named in the deactivate confirmation. */ name: string; /** Feeds ChangeRoleModal's currentRole — must case-insensitively match a roleOptions[].label or the modal opens with nothing pre-selected. */ role: string; /** Shown only inside ChangeRoleModal's header — the Personal Information panel does not render this field, only `details`. */ email: string; /** Avatar image URL; initials show when omitted. */ profilePhoto?: string; /** Selects the green Active or red Inactive badge; Resend Invitation shows only while inactive. */ isActive: boolean; /** Renders the "Pending Invitation" badge only — does not affect which action buttons show. */ isPending?: boolean; /** Hides Resend Invitation and Deactivate (both gated on !isCurrent) and shows a "You" badge on the avatar. */ isCurrent?: boolean; /** True hides the entire action bar (reinvite/change-role/deactivate), not just the deactivate button. */ isSuperAdmin?: boolean; /** Rows of the Personal Information panel, in order, separated by dividers. */ details: UserDetailRow[]; }; /** Form values submitted by the Add User modal; role stays null until the user picks one. */ export type AddUserFormValues = { /** Entered first name; required, trimmed before the empty check. */ firstName: string; /** Entered last name; required, trimmed before the empty check. */ lastName: string; /** Entered email; required and validated as an address. */ email: string; /** The picked role option, or null while none is chosen. */ role: RoleOption | null; }; /** Payload from the Change Role modal — just the ids, unlike AddUserFormValues which carries the full RoleOption. */ export type ChangeRoleSubmitPayload = { /** Id of the user whose role changes (`SelectedUserDetail.userId`). */ userId: number; /** `id` of the newly picked RoleOption. */ roleId: number; }; /** * All user-facing strings. Provide translated values from the host app; every * key has a sensible English default so the component works standalone. */ export type UserDetailsLabels = { /** List panel heading and its accessible region name. */ teamMembers?: string; /** Accessible name of the refresh button; the button exists only when `onRefresh` is passed. */ refreshList?: string; /** Detail panel heading and region name while a user is selected. */ userDetails?: string; /** Detail panel heading and region name, and the empty-state title, while no user is selected. */ selectTeamMember?: string; /** Add User button text in the detail header; the button needs `canManageUsers`. */ addUser?: string; /** Resend Invitation button text. */ resendInvitation?: string; /** Deactivate button text, also the confirm button in the deactivation dialog. */ deactivateUser?: string; /** Change Role button text. */ changeRole?: string; /** Badge on the avatar of the current user. */ you?: string; /** Badge text for an active user, in the list and the detail header. */ active?: string; /** Badge text for an inactive user, in the list and the detail header. */ inactive?: string; /** Badge text for a user whose invitation is pending. */ pendingInvitation?: string; /** Uppercase caption above the details rows. */ personalInformation?: string; /** Empty-state title of the list when `users` is empty. */ noUsers?: string; /** Empty-state description in the detail panel while no user is selected. */ noUserSelected?: string; /** Title of the Add User modal. */ addNewUser?: string; /** Label of the first-name field in the Add User modal. */ firstName?: string; /** Label of the last-name field in the Add User modal. */ lastName?: string; /** Label of the email field in the Add User modal and of the read-only email in the Change Role modal. */ email?: string; /** Label of the role select in the Add User and Change Role modals. */ role?: string; /** Placeholder of the first-name field. */ firstNamePlaceholder?: string; /** Placeholder of the last-name field. */ lastNamePlaceholder?: string; /** Placeholder of the email field. */ emailPlaceholder?: string; /** Text of the add-role action inside the role select; shown when `onAddRoleClick` is passed. */ addNewRole?: string; /** Cancel button text in all three modals. */ cancel?: string; /** Save button text in the Add User and Change Role modals. */ save?: string; /** Error shown when the first name is empty on save. */ firstNameRequired?: string; /** Error shown when the last name is empty on save. */ lastNameRequired?: string; /** Error shown when the email is empty on save. */ emailRequired?: string; /** Error shown when the email is not a valid address. */ emailInvalid?: string; /** Error shown when no role is picked on save. */ roleRequired?: string; /** Title of the Change Role modal. */ changeRoleTitle?: string; /** Title of the deactivation dialog. */ confirmDeactivation?: string; /** Question line of the deactivation dialog. */ deactivateConfirmQuestion?: string; /** Text following the user name in the deactivation dialog, e.g. "Ann will no longer have access". */ deactivateConfirmDescription?: string; }; /** Two-pane user directory + detail panel with bundled Add/Change-Role/Deactivate modals; not a role editor — pair with RolesPermission for managing roles themselves. */ export interface UserDetailsProps { /** Full list of users shown in the left panel. */ users: UserListItem[]; /** The currently selected user rendered in the right panel. */ selectedUser: SelectedUserDetail | null; /** Id of the selected user, used to highlight the active row. */ selectedUserId?: number; /** Role options for the Add User and Change Role modals. */ roleOptions?: RoleOption[]; /** Whether the current viewer can manage users (super-admin gate). */ canManageUsers?: boolean; /** Loading flags driven by the host app's async calls. */ isUsersLoading?: boolean; /** Spins only the Resend Invitation button, not the list or either modal. */ isInviteLoading?: boolean; /** Unlike isAddUserLoading/isChangeRoleLoading, the confirm modal closes immediately on click — this flag can't keep it open to show a spinner. */ isDeactivateLoading?: boolean; /** Spinner on the Add User modal's Save button. */ isAddUserLoading?: boolean; /** Spinner on the Change Role modal's Save button. */ isChangeRoleLoading?: boolean; /** Selection + refresh. */ onSelectUser?: (user: UserListItem) => void; /** Omitting this hides the refresh button entirely, not just disables it. */ onRefresh?: () => void; /** Back navigation handler for the list panel header. */ onBack?: () => void; /** Modal submit / action callbacks. */ onAddUserSubmit?: (values: AddUserFormValues) => void | Promise; /** Awaited before the modal closes — unlike onDeactivate, which fires and closes immediately. */ onChangeRoleSubmit?: (payload: ChangeRoleSubmitPayload) => void | Promise; /** Never called when selectedUser.userId is null — the button stays enabled but silently no-ops. */ onReinvite?: (userId: number) => void; /** Not awaited — the confirm modal closes right after this fires, so a slow or failing deactivation gets no in-modal feedback. */ onDeactivate?: (userId: number) => void; /** "Add New Role" actions inside the SearchSelect of each modal. */ onAddRoleClick?: () => void; /** App-specific node rendered inside the Add User modal header (e.g. an upgrade notice). */ addUserUpgradeNotice?: ReactNode; /** Translated/override strings. */ labels?: UserDetailsLabels; /** Also feeds a calc() for the sidebar's max-height — a non-CSS-length value (e.g. "auto") silently breaks that calc. Defaults to "90vh". */ minHeight?: string; } /** Self-contained invite form (first/last/email/role) with its own validation — not a generic modal; it always renders these exact fields. */ export interface AddUserModalProps { isOpen: boolean; onClose: () => void; roleOptions: RoleOption[]; isLoading?: boolean; onSubmit?: (values: AddUserFormValues) => void | Promise; onAddRoleClick?: () => void; upgradeNotice?: ReactNode; /** A strict subset of UserDetailsLabels — the merged defaults from UserDetails always satisfy it. */ labels: Required>; } /** Reassigns one user's role only — Save stays disabled until a genuinely different role is picked (see currentRole below). */ export interface ChangeRoleModalProps { isOpen: boolean; onClose: () => void; /** Submit silently no-ops when null — Save disables only for isUnchanged/no-selection, not for this. */ userId: number | null; email?: string; /** Matched to roleOptions by case-insensitive label text; no match silently pre-selects nothing and leaves Save disabled. */ currentRole?: string; roleOptions: RoleOption[]; isLoading?: boolean; onSubmit?: (payload: ChangeRoleSubmitPayload) => void | Promise; onAddRoleClick?: () => void; labels: Required>; } /** Confirm dialog for deactivating one user — closes immediately on confirm (see isDeactivateLoading); not a delete, the user can be reactivated later. */ export interface DeactivateConfirmModalProps { isOpen: boolean; onClose: () => void; userName?: string; /** Effectively unusable — the modal always closes before an async confirm could flip this. */ isLoading?: boolean; /** Synchronous by type — the modal closes in the same tick this fires, so it can't be awaited. */ onConfirm: () => void; labels: Required>; }