A skill that can be assigned to staff people.
A flat, translatable taxonomy entry (no parent — unlike Team, which
belongs to a Department). Names are globally unique, case-insensitively
(a lower(name) expression index in the DB). See Department for the
translations shape and read-path semantics — Skill uses the same pattern.
People are assigned skills many-to-many via PhoenixKitStaff.Schemas.PersonSkill,
which carries the per-assignment selected level option ids.
Level selectors
A skill defines zero or more selectors (a.k.a. level groups) in its
levels JSONB column. Each selector has a translatable name, its own
allow_multiple toggle, and an ordered list of translatable options:
[
%{
"id" => "<id>", "name" => "Licence category", "translations" => %{"et" => "..."},
"allow_multiple" => true,
"options" => [
%{"id" => "<id>", "name" => "Category B", "translations" => %{"et" => "B-kategooria"}},
...
]
},
...
]PersonSkill.proficiency_levels holds a flat array of selected option
ids across all selectors (option ids are globally unique within a skill), so
multiple selectors need no join-table change. Per-selector cardinality
(≤1 unless allow_multiple) is enforced in PhoenixKitStaff.Skills.
Backward compatibility: older skills stored a flat option list directly in
levels (no selector wrapper) plus an allow_multiple_levels boolean column.
level_groups/1 wraps that legacy shape into a single default selector on
read (using the legacy boolean for its allow_multiple), so existing rows and
their assignments keep working; the next save persists the selector shape.
Summary
Types
A named selector (level group): a translatable name, an allow_multiple
flag, and an ordered list of options.
A single selectable level option. id is a stable short identifier that
PersonSkill.proficiency_levels references; translations maps a locale to a
translated name (flat lang => name, primary fallback to name).
Functions
Every option id across all selectors, in order.
Locates {group, option} for an option id, or nil.
Generates a stable short (8-hex) id for a selector or option.
A selector's options (always a list).
The skill's selectors (level groups), always a list.
Translated selector name in lang (primary name fallback). Returns the
fallback (possibly ""/nil) — never raises.
Translated option name for an option id in lang (primary name fallback).
Returns nil for an unknown id so callers can drop stray ids — never raises.
A selector's options as [{localized_name, id}] for chips/<select> in lang.
Groups a flat list of selected option ids by selector, in skill order, for
read-only display. Returns [{group, [selected_option, ...]}], dropping
selectors with no selected option and ids that match no option.
Toggles option id within its own selector, preserving selections in other
selectors. A multi-select selector toggles the option in/out; a single-select
selector replaces its selection (re-clicking the chosen option clears it).
Unknown ids are ignored. Order is normalised by Skills.validate_level_ids/2
on the way to the DB, so this need not preserve skill order.
DB-column field names that participate in the translations JSONB.
Types
A named selector (level group): a translatable name, an allow_multiple
flag, and an ordered list of options.
A single selectable level option. id is a stable short identifier that
PersonSkill.proficiency_levels references; translations maps a locale to a
translated name (flat lang => name, primary fallback to name).
@type t() :: %PhoenixKitStaff.Schemas.Skill{ __meta__: term(), allow_multiple_levels: boolean(), description: String.t() | nil, inserted_at: DateTime.t() | nil, levels: [level_group()], name: String.t() | nil, person_skills: [PhoenixKitStaff.Schemas.PersonSkill.t()] | Ecto.Association.NotLoaded.t(), translations: translations_map(), updated_at: DateTime.t() | nil, uuid: UUIDv7.t() | nil }
Functions
Every option id across all selectors, in order.
@spec changeset(t() | Ecto.Changeset.t(t()), map()) :: Ecto.Changeset.t(t())
@spec find_option(t(), String.t()) :: {level_group(), option()} | nil
Locates {group, option} for an option id, or nil.
@spec gen_level_id() :: String.t()
Generates a stable short (8-hex) id for a selector or option.
@spec group_options(level_group()) :: [option()]
A selector's options (always a list).
@spec level_groups(t()) :: [level_group()]
The skill's selectors (level groups), always a list.
Wraps a legacy flat option list (levels entries without an "options" key)
into a single default selector, using the legacy allow_multiple_levels
column for its allow_multiple, so old rows keep working until re-saved.
@spec localized_group_name(t(), level_group(), String.t() | nil) :: String.t() | nil
Translated selector name in lang (primary name fallback). Returns the
fallback (possibly ""/nil) — never raises.
Translated option name for an option id in lang (primary name fallback).
Returns nil for an unknown id so callers can drop stray ids — never raises.
@spec option_choices(t(), level_group(), String.t() | nil) :: [ {String.t(), String.t()} ]
A selector's options as [{localized_name, id}] for chips/<select> in lang.
@spec selected_by_group(t(), [String.t()]) :: [{level_group(), [option()]}]
Groups a flat list of selected option ids by selector, in skill order, for
read-only display. Returns [{group, [selected_option, ...]}], dropping
selectors with no selected option and ids that match no option.
Toggles option id within its own selector, preserving selections in other
selectors. A multi-select selector toggles the option in/out; a single-select
selector replaces its selection (re-clicking the chosen option clears it).
Unknown ids are ignored. Order is normalised by Skills.validate_level_ids/2
on the way to the DB, so this need not preserve skill order.
@spec translatable_fields() :: [String.t()]
DB-column field names that participate in the translations JSONB.