defmodule DaProductApp.TerminalManagement.TerminalGroup do use Ecto.Schema import Ecto.Changeset import Ecto.Query @derive {Jason.Encoder, only: [ :id, :name, :description, :group_type, :color, :icon, :parent_group_id, :is_active, :created_by, :metadata, :terminal_count, :inserted_at, :updated_at ]} schema "terminal_groups" do field :name, :string field :description, :string field :group_type, :string, default: "custom" # custom, auto, system field :color, :string, default: "#3B82F6" field :icon, :string, default: "hero-squares-2x2" field :is_active, :boolean, default: true field :created_by, :string field :metadata, :map, default: %{} # Virtual field for UI field :terminal_count, :integer, virtual: true # Self-referencing for hierarchical groups belongs_to :parent_group, __MODULE__, foreign_key: :parent_group_id has_many :child_groups, __MODULE__, foreign_key: :parent_group_id # Associations has_many :group_rules, DaProductApp.TerminalManagement.TerminalGroupRule, foreign_key: :group_id has_many :group_memberships, DaProductApp.TerminalManagement.TerminalGroupMembership, foreign_key: :group_id has_many :terminals, through: [:group_memberships, :terminal] timestamps() end @required_fields [:name, :group_type] @optional_fields [:description, :color, :icon, :parent_group_id, :is_active, :created_by, :metadata] @all_fields @required_fields ++ @optional_fields def changeset(group, attrs) do group |> cast(attrs, @all_fields) |> validate_required(@required_fields) |> validate_length(:name, min: 2, max: 100) |> validate_inclusion(:group_type, ["custom", "auto", "system"]) |> validate_format(:color, ~r/^#[0-9A-Fa-f]{6}$/, message: "must be a valid hex color") |> validate_length(:description, max: 500) |> unique_constraint(:name) |> validate_parent_group_hierarchy() end # Prevent circular references in parent-child relationships defp validate_parent_group_hierarchy(changeset) do case get_change(changeset, :parent_group_id) do nil -> changeset parent_id when is_integer(parent_id) -> group_id = get_field(changeset, :id) if group_id && parent_id == group_id do add_error(changeset, :parent_group_id, "cannot be self-referencing") else changeset end _ -> changeset end end # Get all child groups recursively def get_all_children(group_id, repo) do repo.all( from g in __MODULE__, where: g.parent_group_id == ^group_id, select: g.id ) |> Enum.flat_map(fn child_id -> [child_id | get_all_children(child_id, repo)] end) end # Group type helpers def system_group?(group), do: group.group_type == "system" def auto_group?(group), do: group.group_type == "auto" def custom_group?(group), do: group.group_type == "custom" # Scope for active groups def active_groups(query \\ __MODULE__) do from g in query, where: g.is_active == true end # Scope for groups by type def by_type(query \\ __MODULE__, type) do from g in query, where: g.group_type == ^type end end