PhoenixKitStaff.Skills (PhoenixKitStaff v0.6.0)

Copy Markdown View Source

Skills: CRUD for the Skill taxonomy plus person↔skill assignment (with proficiency level).

Skill CRUD mirrors PhoenixKitStaff.Teams. The assignment functions (assign_skill/unassign_skill/…) live here for cohesion — note this deviates from team membership, which lives in PhoenixKitStaff.Staff; thin delegators are exposed there (Staff.assign_skill/3, etc.) so both context boundaries reach it.

Summary

Functions

Assigns a skill to a person at zero or more of the skill's levels (a list of level ids); broadcasts :person_skill_added. Validates the ids against the skill (must exist; ≤1 unless the skill allows multiple).

Returns a changeset for the given skill.

Total number of skills.

Inserts a skill and broadcasts :skill_created.

Deletes a skill (cascades its assignments) and broadcasts :skill_deleted.

Fetches a skill by uuid, or nil if not found.

Fetches a skill by uuid. Raises if not found.

Lists skills ordered by name. Accepts :preload.

A person's skill assignments, skill preloaded, ordered by skill name.

People assigned a given skill, person + user preloaded. Trashed people are excluded (the assignment row survives a trash; restore brings them back).

Non-trashed people not yet assigned this skill (for the add picker).

Map of skill_uuid => assigned-people count (one query, for the list view). Trashed people are excluded so the count matches the skill-show roster.

Filters ids to options that still exist on the skill, ordered by the skill's selector→option order, pruning each single-select selector to its first selected option. Unlike validate_level_ids/2 this trims rather than erroring on over-count — used by reconciliation and by the form's concurrently-removed-level fallback.

Skills not yet assigned to a person, ordered by name (for the person-side add picker).

Removes an assignment (by struct or person/skill uuids); broadcasts :person_skill_removed.

Updates a skill and broadcasts :skill_updated.

Replaces an assignment's selected levels (a list of level ids); broadcasts :person_skill_updated. Validates against the parent skill, under the same FOR UPDATE skill-row lock as assign_skill/3.

Validates a list of selected option ids against a skill: every id must exist in some selector, and each single-select selector may hold at most one of its options. Returns the ids cleaned + reordered to the skill's selector→option order on success.

Functions

assign_skill(person_uuid, skill_uuid, level_ids \\ [])

@spec assign_skill(
  UUIDv7.t() | String.t(),
  UUIDv7.t() | String.t(),
  [String.t()] | String.t() | nil
) ::
  {:ok, PhoenixKitStaff.Schemas.PersonSkill.t()}
  | {:error,
     :skill_not_found
     | :person_trashed
     | :invalid_levels
     | :too_many_levels
     | Ecto.Changeset.t()}

Assigns a skill to a person at zero or more of the skill's levels (a list of level ids); broadcasts :person_skill_added. Validates the ids against the skill (must exist; ≤1 unless the skill allows multiple).

Refuses a trashed person with {:error, :person_trashed} — the UI pickers already exclude trashed people, so this guards the direct-API path (mirrors the rest of the soft-delete hardening).

Runs in a transaction that FOR UPDATE-locks the skill row before validating, so it can't race a concurrent update/2 and persist a just-removed level id.

change(skill, attrs \\ %{})

Returns a changeset for the given skill.

count()

@spec count() :: non_neg_integer()

Total number of skills.

create(attrs)

Inserts a skill and broadcasts :skill_created.

delete(skill)

Deletes a skill (cascades its assignments) and broadcasts :skill_deleted.

get(uuid, opts \\ [])

@spec get(
  UUIDv7.t() | String.t(),
  keyword()
) :: PhoenixKitStaff.Schemas.Skill.t() | nil

Fetches a skill by uuid, or nil if not found.

get!(uuid, opts \\ [])

Fetches a skill by uuid. Raises if not found.

list(opts \\ [])

Lists skills ordered by name. Accepts :preload.

list_for_person(person_uuid)

@spec list_for_person(UUIDv7.t() | String.t()) :: [
  PhoenixKitStaff.Schemas.PersonSkill.t()
]

A person's skill assignments, skill preloaded, ordered by skill name.

list_people_for_skill(skill_uuid)

@spec list_people_for_skill(UUIDv7.t() | String.t()) :: [
  PhoenixKitStaff.Schemas.PersonSkill.t()
]

People assigned a given skill, person + user preloaded. Trashed people are excluded (the assignment row survives a trash; restore brings them back).

people_without_skill(skill_uuid)

@spec people_without_skill(UUIDv7.t() | String.t()) :: [
  PhoenixKitStaff.Schemas.Person.t()
]

Non-trashed people not yet assigned this skill (for the add picker).

person_counts()

@spec person_counts() :: %{optional(UUIDv7.t()) => non_neg_integer()}

Map of skill_uuid => assigned-people count (one query, for the list view). Trashed people are excluded so the count matches the skill-show roster.

prune_level_ids(skill, ids)

@spec prune_level_ids(PhoenixKitStaff.Schemas.Skill.t(), [String.t()]) :: [String.t()]

Filters ids to options that still exist on the skill, ordered by the skill's selector→option order, pruning each single-select selector to its first selected option. Unlike validate_level_ids/2 this trims rather than erroring on over-count — used by reconciliation and by the form's concurrently-removed-level fallback.

skills_not_assigned_to(person_uuid)

@spec skills_not_assigned_to(UUIDv7.t() | String.t()) :: [
  PhoenixKitStaff.Schemas.Skill.t()
]

Skills not yet assigned to a person, ordered by name (for the person-side add picker).

unassign_skill(ps)

Removes an assignment (by struct or person/skill uuids); broadcasts :person_skill_removed.

unassign_skill(person_uuid, skill_uuid)

@spec unassign_skill(UUIDv7.t() | String.t(), UUIDv7.t() | String.t()) ::
  {:ok, PhoenixKitStaff.Schemas.PersonSkill.t()}
  | {:error, :not_found}
  | {:error, Ecto.Changeset.t(PhoenixKitStaff.Schemas.PersonSkill.t())}

update(skill, attrs)

Updates a skill and broadcasts :skill_updated.

Reconciles existing assignments to the new selectors in the same transaction: option ids removed from every selector are stripped from each person_skills.proficiency_levels, and a selector flipped multiple→single prunes its assignments to a single option (in skill order). This keeps assignments free of orphaned or over-count ids.

Takes a FOR UPDATE lock on the skill row first so a concurrent assignment write (which locks the same row) can't validate against the old selectors and then insert a now-removed option id — the two serialize on the lock.

update_assignment_levels(ps, level_ids)

@spec update_assignment_levels(
  PhoenixKitStaff.Schemas.PersonSkill.t(),
  [String.t()] | String.t() | nil
) ::
  {:ok, PhoenixKitStaff.Schemas.PersonSkill.t()}
  | {:error,
     :skill_not_found | :invalid_levels | :too_many_levels | Ecto.Changeset.t()}

Replaces an assignment's selected levels (a list of level ids); broadcasts :person_skill_updated. Validates against the parent skill, under the same FOR UPDATE skill-row lock as assign_skill/3.

validate_level_ids(skill, ids)

@spec validate_level_ids(PhoenixKitStaff.Schemas.Skill.t(), [String.t()]) ::
  {:ok, [String.t()]} | {:error, :invalid_levels | :too_many_levels}

Validates a list of selected option ids against a skill: every id must exist in some selector, and each single-select selector may hold at most one of its options. Returns the ids cleaned + reordered to the skill's selector→option order on success.