Context for staff (people) and team memberships.
Staff are linked 1:1 to a PhoenixKit user (decision A for MVP).
A person can belong to multiple teams via TeamMembership.
Summary
Functions
Delegates to PhoenixKitStaff.Skills.assign_skill/3 (level_ids list).
Bulk permanent-deletes people. Broadcasts one bulk event. Returns
{:ok, deleted_count} or {:error, :referenced_by_external} if a
(hypothetical future) RESTRICT FK blocks the delete.
Bulk-restores trashed people to their stashed prior status (validated
to active/inactive, else active), clearing the stash key.
Broadcasts one bulk event. Returns {:ok, restored_count}.
Bulk-trashes the given people. Per-row stashes the prior status into
metadata["trashed_from_status"] (in a single UPDATE — the SET
expressions read the pre-update row, so status there is still the
old value). Skips rows already trashed. Broadcasts one bulk event.
Returns {:ok, trashed_count}.
Returns a changeset for the given person.
Number of non-trashed people (the active roster size).
Number of trashed (soft-deleted) people.
Inserts a person and broadcasts :person_created on success.
Find-or-create a user by email, then create a staff person linked to that user. If the person creation fails AND we just created a brand-new placeholder user, delete the placeholder so we don't leave orphans.
Permanently deletes a person (hard Repo.delete) and broadcasts
:person_deleted. Trash-only: refuses a non-trashed person with
{:error, :not_trashed} so permanent deletion is always a deliberate
two-step (trash, then delete) — a stray direct call can't nuke an
active person.
Users who don't yet have a staff profile. When exclude_person_uuid
is passed (edit mode), that person's linked user is kept in the list.
Returns the regex used to validate emails throughout the staff module.
Finds an existing user by email, or creates a placeholder user with no usable password. When the person later registers or logs in via OAuth with the same email, PhoenixKit's built-in lookup links them automatically.
Fetches a person by uuid, or nil if not found.
Fetches a person by uuid. Raises if not found.
Fetches a person by the linked user's uuid, or nil if no staff profile exists.
Lists people. Accepts :preload, :status filter, :search (matches
name or user email), and :include_trashed (default false).
The full department → team → people org tree. See PhoenixKitStaff.Staff.Org.
Renames the email of an unclaimed placeholder user directly in place.
Safe only for users we created via find_or_create_user_by_email/1
that nobody has signed up for yet. Refuses if another user already
exists with the new email.
Restores a trashed person to the status they had before trashing
(read from metadata["trashed_from_status"], validated against
Person.statuses/0, defaulting to "active"). Clears the stash key
but preserves any other metadata. Broadcasts :person_updated.
Returns {:error, :not_trashed} if the person isn't trashed.
Soft-deletes a person: sets status to "trashed" and stashes the
prior lifecycle status under metadata["trashed_from_status"] so
restore_person/1 can return them to active/inactive. Broadcasts
:person_updated. Returns {:error, :already_trashed} if it's
already trashed.
Upcoming birthdays within window_days (default 30). See PhoenixKitStaff.Staff.Org.
Updates a person and broadcasts :person_updated on success.
Whether the given string looks like a valid email.
Functions
Delegates to PhoenixKitStaff.Skills.assign_skill/3 (level_ids list).
@spec bulk_delete([UUIDv7.t() | String.t()]) :: {:ok, non_neg_integer()} | {:error, :referenced_by_external}
Bulk permanent-deletes people. Broadcasts one bulk event. Returns
{:ok, deleted_count} or {:error, :referenced_by_external} if a
(hypothetical future) RESTRICT FK blocks the delete.
@spec bulk_restore([UUIDv7.t() | String.t()]) :: {:ok, non_neg_integer()}
Bulk-restores trashed people to their stashed prior status (validated
to active/inactive, else active), clearing the stash key.
Broadcasts one bulk event. Returns {:ok, restored_count}.
@spec bulk_trash([UUIDv7.t() | String.t()]) :: {:ok, non_neg_integer()}
Bulk-trashes the given people. Per-row stashes the prior status into
metadata["trashed_from_status"] (in a single UPDATE — the SET
expressions read the pre-update row, so status there is still the
old value). Skips rows already trashed. Broadcasts one bulk event.
Returns {:ok, trashed_count}.
@spec change_person(PhoenixKitStaff.Schemas.Person.t(), map()) :: Ecto.Changeset.t(PhoenixKitStaff.Schemas.Person.t())
Returns a changeset for the given person.
@spec count_people() :: non_neg_integer()
Number of non-trashed people (the active roster size).
@spec count_trashed() :: non_neg_integer()
Number of trashed (soft-deleted) people.
@spec create_person(map()) :: {:ok, PhoenixKitStaff.Schemas.Person.t()} | {:error, Ecto.Changeset.t(PhoenixKitStaff.Schemas.Person.t())} | {:error, {:trashed_person_exists, PhoenixKitStaff.Schemas.Person.t()}}
Inserts a person and broadcasts :person_created on success.
Guards the strict 1:1 user_uuid unique constraint against a confusing
failure: if the target user already has a trashed staff profile,
re-adding them would trip the unique index. Instead this returns
{:error, {:trashed_person_exists, trashed_person}} so the caller can
offer to restore the existing record rather than creating a duplicate.
@spec create_person_with_user(String.t(), map()) :: {:ok, PhoenixKitStaff.Schemas.Person.t(), :created | :existing} | {:error, PhoenixKitStaff.Errors.error_atom() | Ecto.Changeset.t() | {:trashed_person_exists, PhoenixKitStaff.Schemas.Person.t()}}
Find-or-create a user by email, then create a staff person linked to that user. If the person creation fails AND we just created a brand-new placeholder user, delete the placeholder so we don't leave orphans.
Returns {:ok, person, user_status} or {:error, reason}.
@spec delete_person(PhoenixKitStaff.Schemas.Person.t()) :: {:ok, PhoenixKitStaff.Schemas.Person.t()} | {:error, :not_trashed | :referenced_by_external | Ecto.Changeset.t(PhoenixKitStaff.Schemas.Person.t())}
Permanently deletes a person (hard Repo.delete) and broadcasts
:person_deleted. Trash-only: refuses a non-trashed person with
{:error, :not_trashed} so permanent deletion is always a deliberate
two-step (trash, then delete) — a stray direct call can't nuke an
active person.
The rescue clauses guard a hypothetical future ON DELETE RESTRICT
FK into phoenix_kit_staff_people. Today none exist — the projects
assignee FK is ON DELETE SET NULL and team memberships are
ON DELETE CASCADE — so a delete won't raise; it succeeds and the
caller is expected to have warned that project-assignment links get
cleared. The rescue stays as cheap insurance against a future
restricting consumer.
@spec eligible_users(keyword()) :: [PhoenixKit.Users.Auth.User.t()]
Users who don't yet have a staff profile. When exclude_person_uuid
is passed (edit mode), that person's linked user is kept in the list.
@spec email_regex() :: Regex.t()
Returns the regex used to validate emails throughout the staff module.
@spec find_or_create_user_by_email(String.t()) :: {:ok, PhoenixKit.Users.Auth.User.t(), :created | :existing} | {:error, PhoenixKitStaff.Errors.error_atom() | Ecto.Changeset.t()}
Finds an existing user by email, or creates a placeholder user with no usable password. When the person later registers or logs in via OAuth with the same email, PhoenixKit's built-in lookup links them automatically.
@spec get_person( UUIDv7.t() | String.t() | any(), keyword() ) :: PhoenixKitStaff.Schemas.Person.t() | nil
Fetches a person by uuid, or nil if not found.
@spec get_person!( UUIDv7.t() | String.t(), keyword() ) :: PhoenixKitStaff.Schemas.Person.t()
Fetches a person by uuid. Raises if not found.
@spec get_person_by_user_uuid( UUIDv7.t() | String.t(), keyword() ) :: PhoenixKitStaff.Schemas.Person.t() | nil
Fetches a person by the linked user's uuid, or nil if no staff profile exists.
See PhoenixKitStaff.Staff.Memberships.list_memberships_for_person/1.
@spec list_people(keyword()) :: [PhoenixKitStaff.Schemas.Person.t()]
Lists people. Accepts :preload, :status filter, :search (matches
name or user email), and :include_trashed (default false).
Trashed (soft-deleted) people are excluded by default. Pass
status: "trashed" for the Trash view, or include_trashed: true to
list everything regardless of status.
Delegates to PhoenixKitStaff.Skills.list_for_person/1.
See PhoenixKitStaff.Staff.Memberships.list_team_memberships/1.
The full department → team → people org tree. See PhoenixKitStaff.Staff.Org.
@spec rename_placeholder_email(PhoenixKit.Users.Auth.User.t(), String.t()) :: :ok | {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, PhoenixKitStaff.Errors.error_atom() | Ecto.Changeset.t()}
Renames the email of an unclaimed placeholder user directly in place.
Safe only for users we created via find_or_create_user_by_email/1
that nobody has signed up for yet. Refuses if another user already
exists with the new email.
@spec restore_person(PhoenixKitStaff.Schemas.Person.t()) :: {:ok, PhoenixKitStaff.Schemas.Person.t()} | {:error, :not_trashed | Ecto.Changeset.t(PhoenixKitStaff.Schemas.Person.t())}
Restores a trashed person to the status they had before trashing
(read from metadata["trashed_from_status"], validated against
Person.statuses/0, defaulting to "active"). Clears the stash key
but preserves any other metadata. Broadcasts :person_updated.
Returns {:error, :not_trashed} if the person isn't trashed.
@spec trash_person(PhoenixKitStaff.Schemas.Person.t()) :: {:ok, PhoenixKitStaff.Schemas.Person.t()} | {:error, :already_trashed | Ecto.Changeset.t(PhoenixKitStaff.Schemas.Person.t())}
Soft-deletes a person: sets status to "trashed" and stashes the
prior lifecycle status under metadata["trashed_from_status"] so
restore_person/1 can return them to active/inactive. Broadcasts
:person_updated. Returns {:error, :already_trashed} if it's
already trashed.
Project assignments and team memberships are deliberately left intact (the FK rows survive), so the person — and their assignments — come back cleanly on restore. This is the whole point over hard delete, which would silently NULL out project assignments (FK is SET NULL).
Delegates to PhoenixKitStaff.Skills.unassign_skill/1.
Delegates to PhoenixKitStaff.Skills.unassign_skill/2.
@spec upcoming_birthdays(non_neg_integer()) :: [ %{ person: PhoenixKitStaff.Schemas.Person.t(), next_birthday: Date.t(), days_until: non_neg_integer() } ]
Upcoming birthdays within window_days (default 30). See PhoenixKitStaff.Staff.Org.
@spec update_person(PhoenixKitStaff.Schemas.Person.t(), map()) :: {:ok, PhoenixKitStaff.Schemas.Person.t()} | {:error, Ecto.Changeset.t(PhoenixKitStaff.Schemas.Person.t())}
Updates a person and broadcasts :person_updated on success.
Whether the given string looks like a valid email.