PhoenixKitStaff.Schemas.Skill (PhoenixKitStaff v0.6.0)

Copy Markdown View Source

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).

t()

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

level_group()

@type level_group() :: %{required(String.t()) => term()}

A named selector (level group): a translatable name, an allow_multiple flag, and an ordered list of options.

option()

@type option() :: %{
  required(String.t()) => String.t() | %{optional(String.t()) => String.t()}
}

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).

t()

@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
}

translations_map()

@type translations_map() :: %{
  optional(String.t()) => %{optional(String.t()) => String.t()}
}

Functions

all_option_ids(skill)

@spec all_option_ids(t()) :: [String.t()]

Every option id across all selectors, in order.

changeset(skill, attrs)

@spec changeset(t() | Ecto.Changeset.t(t()), map()) :: Ecto.Changeset.t(t())

find_option(skill, option_id)

@spec find_option(t(), String.t()) :: {level_group(), option()} | nil

Locates {group, option} for an option id, or nil.

gen_level_id()

@spec gen_level_id() :: String.t()

Generates a stable short (8-hex) id for a selector or option.

group_options(group)

@spec group_options(level_group()) :: [option()]

A selector's options (always a list).

level_groups(arg1)

@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.

localized_description(s, lang)

@spec localized_description(t(), String.t() | nil) :: String.t() | nil

localized_group_name(skill, group, lang)

@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.

localized_name(s, lang)

@spec localized_name(t(), String.t() | nil) :: String.t() | nil

localized_option_name(skill, id, lang)

@spec localized_option_name(t(), String.t(), String.t() | nil) :: String.t() | nil

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.

option_choices(skill, group, lang)

@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.

selected_by_group(skill, option_ids)

@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.

toggle_option(skill, ids, option_id)

@spec toggle_option(t(), [String.t()], String.t()) :: [String.t()]

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.

translatable_fields()

@spec translatable_fields() :: [String.t()]

DB-column field names that participate in the translations JSONB.