defmodule DaProductAppWeb.TerminalGroupLive.Index do use DaProductAppWeb, :live_view require Logger alias DaProductApp.TerminalManagement @impl true def mount(_params, _session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(DaProductApp.PubSub, "tms:groups") Phoenix.PubSub.subscribe(DaProductApp.PubSub, "terminal:events") end groups = TerminalManagement.list_terminal_groups() statistics = TerminalManagement.get_group_statistics() {:ok, assign(socket, groups: groups, statistics: statistics, current_page: "terminal_groups", selected_group: nil, show_new_group_slider: false, show_edit_group_slider: false, show_rules_slider: false, show_assign_terminals_slider: false, new_group_form: %{}, edit_group_form: %{}, new_rule_form: %{}, group_rules: [], group_terminals: [], available_terminals: [], filters: %{"group_type" => "all", "active_only" => true}, errors: %{}, applying_rules: false, last_event: nil, auto_events_enabled: true )} end @impl true def handle_params(params, _url, socket) do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end defp apply_action(socket, :index, _params) do socket |> assign(:page_title, "Terminal Groups") end defp apply_action(socket, :show, %{"id" => id}) do case TerminalManagement.get_terminal_group(id) do {:ok, group} -> rules = TerminalManagement.list_group_rules(group.id) terminals = TerminalManagement.get_group_terminals(group.id) socket |> assign(:page_title, "Group: #{group.name}") |> assign(:selected_group, group) |> assign(:group_rules, rules) |> assign(:group_terminals, terminals) {:error, :not_found} -> socket |> put_flash(:error, "Group not found") |> redirect(to: ~p"/terminals/groups") end end @impl true def handle_event("filter", %{"filters" => filters}, socket) do groups = TerminalManagement.list_terminal_groups(filters) {:noreply, assign(socket, groups: groups, filters: filters )} end def handle_event("show_new_group", _params, socket) do {:noreply, assign(socket, show_new_group_slider: true, new_group_form: %{ "name" => "", "description" => "", "group_type" => "custom", "color" => "#3B82F6", "icon" => "hero-squares-2x2" }, errors: %{} )} end def handle_event("hide_new_group", _params, socket) do {:noreply, assign(socket, show_new_group_slider: false, new_group_form: %{}, errors: %{} )} end def handle_event("update_new_group_form", %{"group" => group_params}, socket) do {:noreply, assign(socket, new_group_form: group_params)} end def handle_event("create_group", %{"group" => group_params}, socket) do attrs = Map.put(group_params, "created_by", "current_user") # Replace with actual user case TerminalManagement.create_terminal_group(attrs) do {:ok, _group} -> groups = TerminalManagement.list_terminal_groups(socket.assigns.filters) statistics = TerminalManagement.get_group_statistics() {:noreply, socket |> assign(groups: groups, statistics: statistics) |> assign(show_new_group_slider: false, new_group_form: %{}, errors: %{}) |> put_flash(:info, "Group created successfully") } {:error, changeset} -> errors = changeset.errors |> Enum.into(%{}, fn {field, {message, _}} -> {Atom.to_string(field), message} end) {:noreply, assign(socket, errors: errors)} end end def handle_event("edit_group", %{"id" => id}, socket) do case TerminalManagement.get_terminal_group(id) do {:ok, group} -> form = %{ "name" => group.name, "description" => group.description || "", "color" => group.color, "icon" => group.icon } {:noreply, assign(socket, selected_group: group, show_edit_group_slider: true, edit_group_form: form, errors: %{} )} {:error, :not_found} -> {:noreply, put_flash(socket, :error, "Group not found")} end end def handle_event("update_edit_group_form", %{"group" => group_params}, socket) do {:noreply, assign(socket, edit_group_form: group_params)} end def handle_event("update_group", %{"group" => group_params}, socket) do group = socket.assigns.selected_group case TerminalManagement.update_terminal_group(group, group_params) do {:ok, _updated_group} -> groups = TerminalManagement.list_terminal_groups(socket.assigns.filters) {:noreply, socket |> assign(groups: groups) |> assign(show_edit_group_slider: false, edit_group_form: %{}, errors: %{}) |> put_flash(:info, "Group updated successfully") } {:error, changeset} -> errors = changeset.errors |> Enum.into(%{}, fn {field, {message, _}} -> {Atom.to_string(field), message} end) {:noreply, assign(socket, errors: errors)} end end def handle_event("delete_group", %{"id" => id}, socket) do case TerminalManagement.get_terminal_group(id) do {:ok, group} -> case TerminalManagement.delete_terminal_group(group) do {:ok, _} -> groups = TerminalManagement.list_terminal_groups(socket.assigns.filters) statistics = TerminalManagement.get_group_statistics() {:noreply, socket |> assign(groups: groups, statistics: statistics) |> put_flash(:info, "Group deleted successfully") } {:error, :cannot_delete_system_group} -> {:noreply, put_flash(socket, :error, "Cannot delete system groups")} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to delete group")} end {:error, :not_found} -> {:noreply, put_flash(socket, :error, "Group not found")} end end def handle_event("show_rules", %{"id" => id}, socket) do case TerminalManagement.get_terminal_group(id) do {:ok, group} -> rules = TerminalManagement.list_group_rules(group.id) {:noreply, assign(socket, selected_group: group, group_rules: rules, show_rules_slider: true, new_rule_form: %{ "rule_name" => "", "rule_type" => "field_match", "field_name" => "vendor", "operator" => "equals", "value" => "", "priority" => "100" } )} {:error, :not_found} -> {:noreply, put_flash(socket, :error, "Group not found")} end end def handle_event("hide_rules", _params, socket) do {:noreply, assign(socket, show_rules_slider: false, selected_group: nil, group_rules: [], new_rule_form: %{} )} end def handle_event("update_rule_form", %{"rule" => rule_params}, socket) do {:noreply, assign(socket, new_rule_form: rule_params)} end def handle_event("apply_rule_template", %{"template" => template}, socket) do form = case template do "vendor_ingenico" -> %{ "rule_name" => "Ingenico Terminals", "rule_type" => "field_match", "field_name" => "vendor", "operator" => "equals", "value" => "Ingenico", "priority" => "100" } "status_active" -> %{ "rule_name" => "Active Terminals", "rule_type" => "field_match", "field_name" => "status", "operator" => "equals", "value" => "active", "priority" => "50" } "high_tier" -> %{ "rule_name" => "High Tier Terminals", "rule_type" => "field_match", "field_name" => "tier", "operator" => "in", "value" => "premium,enterprise,high", "priority" => "75" } "production_deployment" -> %{ "rule_name" => "Production Deployment", "rule_type" => "field_match", "field_name" => "deployment_type", "operator" => "equals", "value" => "production", "priority" => "25" } _ -> socket.assigns.new_rule_form end {:noreply, assign(socket, new_rule_form: form)} end def handle_event("create_rule", %{"rule" => rule_params}, socket) do group = socket.assigns.selected_group attrs = Map.put(rule_params, "group_id", group.id) # Validate rule before creating case validate_rule_params(rule_params) do {:ok, _} -> case TerminalManagement.create_group_rule(attrs) do {:ok, _rule} -> rules = TerminalManagement.list_group_rules(group.id) statistics = TerminalManagement.get_group_statistics() groups = TerminalManagement.list_terminal_groups(socket.assigns.filters) {:noreply, socket |> assign(group_rules: rules, statistics: statistics, groups: groups) |> assign(new_rule_form: %{ "rule_name" => "", "rule_type" => "field_match", "field_name" => "vendor", "operator" => "equals", "value" => "", "priority" => "100" }) |> put_flash(:info, "Rule created and applied successfully") } {:error, changeset} -> errors = changeset.errors |> Enum.into(%{}, fn {field, {message, _}} -> {Atom.to_string(field), message} end) {:noreply, assign(socket, errors: errors)} end {:error, message} -> {:noreply, put_flash(socket, :error, message)} end end def handle_event("delete_rule", %{"id" => _rule_id}, socket) do # Implementation for deleting rules {:noreply, put_flash(socket, :info, "Rule deletion not implemented yet")} end def handle_event("apply_all_rules", _params, socket) do # Set loading state and then process in the background send(self(), :process_apply_rules) {:noreply, assign(socket, applying_rules: true)} end def handle_info(:process_apply_rules, socket) do start_time = System.monotonic_time(:millisecond) Logger.info("Starting rule application process for all terminals") case TerminalManagement.apply_all_rules() do {:ok, count} -> end_time = System.monotonic_time(:millisecond) duration = end_time - start_time Logger.info("Successfully applied #{count} rules to all terminals in #{duration}ms") # Refresh both statistics and groups list to update terminal counts statistics = TerminalManagement.get_group_statistics() groups = TerminalManagement.list_terminal_groups(socket.assigns.filters) {:noreply, socket |> assign(statistics: statistics, groups: groups, applying_rules: false) |> put_flash(:info, "Applied #{count} rules to all terminals (#{duration}ms)") } {:error, reason} -> Logger.error("Failed to apply rules: #{inspect(reason)}") {:noreply, socket |> assign(applying_rules: false) |> put_flash(:error, "Failed to apply rules - check logs for details") } end end def handle_info({:event_processed, event}, socket) do # Real-time update when rules are automatically applied groups = TerminalManagement.list_terminal_groups(socket.assigns.filters) statistics = TerminalManagement.get_group_statistics() {:noreply, socket |> assign(groups: groups, statistics: statistics, last_event: event) |> put_flash(:info, "✨ Auto-applied rules for #{event.type} event") } end def handle_info({:group_change, _event}, socket) do # Refresh data when groups change groups = TerminalManagement.list_terminal_groups(socket.assigns.filters) statistics = TerminalManagement.get_group_statistics() {:noreply, assign(socket, groups: groups, statistics: statistics)} end def handle_event("refresh_counts", _params, socket) do # Preserve filters when refreshing filters = socket.assigns.filters groups = TerminalManagement.list_terminal_groups(filters) statistics = TerminalManagement.get_group_statistics() {:noreply, socket |> assign(groups: groups, statistics: statistics, filters: filters) |> put_flash(:info, "Terminal counts refreshed") } end def handle_event("close_slide_over", _params, socket) do {:noreply, assign(socket, show_new_group_slider: false, show_edit_group_slider: false, show_rules_slider: false, show_assign_terminals_slider: false, new_group_form: %{}, edit_group_form: %{}, new_rule_form: %{}, errors: %{}, applying_rules: false )} end @impl true def render(assigns) do ~H"""

Terminal Groups

Organize terminals into logical groups with automated rules

Total Groups
<%= @statistics.groups.total %>
Active Rules
<%= @statistics.rules.active %>
Active Assignments
<%= @statistics.assignments.active %>
Rule-based
<%= Map.get(@statistics.assignments.by_type, "rule_based", 0) %>
<%= for group <- @groups do %> <% end %>
Group Type Terminals Rules Status Actions
<.icon name={group.icon} class="w-4 h-4" style={"color: #{group.color}"} />
<%= group.name %>
<%= group.description %>
<%= String.capitalize(group.group_type) %> <%= group.terminal_count || 0 %> <%= length(group.group_rules || []) %> <%= if group.is_active, do: "Active", else: "Inactive" %>
<%= unless group.group_type == "system" do %> <% end %>
<%= if @show_new_group_slider do %> <.slide_over origin="right" title="Create New Group">
<%= if @errors["name"] do %>

<%= @errors["name"] %>

<% end %>
<% end %> <%= if @show_edit_group_slider && @selected_group do %> <.slide_over origin="right" title="Edit Group">
<%= if @errors["name"] do %>

<%= @errors["name"] %>

<% end %>
<% end %> <%= if @show_rules_slider && @selected_group do %> <.slide_over origin="right" title={"Rules for #{@selected_group.name}"}>

Existing Rules

<%= if length(@group_rules) > 0 do %>
<%= for rule <- @group_rules do %>
<%= rule.rule_name %>

<%= String.capitalize(rule.rule_type) %> - <%= rule.field_name %> <%= rule.operator %> "<%= rule.value %>"

Priority: <%= rule.priority %>

<% end %>
<% else %>

No rules defined yet

<% end %>
<%= if @selected_group.group_type in ["auto", "custom"] do %>

Add New Rule

<%= get_value_help_text(@new_rule_form["operator"] || "equals") %>

<% end %>
<% end %>
""" end defp group_type_class("system"), do: "bg-gray-100 text-gray-800" defp group_type_class("auto"), do: "bg-blue-100 text-blue-800" defp group_type_class("custom"), do: "bg-green-100 text-green-800" defp group_type_class(_), do: "bg-gray-100 text-gray-800" defp get_value_placeholder(operator) do case operator do "equals" -> "e.g., Ingenico" "not_equals" -> "e.g., PAX" "contains" -> "e.g., POS" "starts_with" -> "e.g., NYC" "ends_with" -> "e.g., -DEV" "in" -> "e.g., Ingenico,PAX,Verifone" "not_in" -> "e.g., test,demo,staging" "regex" -> "e.g., ^[A-Z]{3}[0-9]{3}$" "range" -> "e.g., 1,100" _ -> "Enter value to match" end end defp get_value_help_text(operator) do case operator do "equals" -> "Exact match (case-sensitive)" "not_equals" -> "Must not equal this value" "contains" -> "Field must contain this text" "starts_with" -> "Field must start with this text" "ends_with" -> "Field must end with this text" "in" -> "Comma-separated list of values" "not_in" -> "Must not be in this comma-separated list" "regex" -> "Regular expression pattern" "range" -> "Numeric range: minimum,maximum" _ -> "" end end defp validate_rule_params(rule_params) do cond do is_nil(rule_params["rule_name"]) or String.trim(rule_params["rule_name"]) == "" -> {:error, "Rule name is required"} is_nil(rule_params["value"]) or String.trim(rule_params["value"]) == "" -> {:error, "Rule value is required"} rule_params["operator"] == "range" and not valid_range?(rule_params["value"]) -> {:error, "Range must be in format: min,max (e.g., 1,100)"} rule_params["operator"] == "regex" and not valid_regex?(rule_params["value"]) -> {:error, "Invalid regular expression pattern"} rule_params["operator"] in ["in", "not_in"] and not valid_list?(rule_params["value"]) -> {:error, "List values must be comma-separated (e.g., value1,value2,value3)"} true -> {:ok, rule_params} end end defp valid_range?(value) do case String.split(value, ",") do [min_str, max_str] -> case {Integer.parse(String.trim(min_str)), Integer.parse(String.trim(max_str))} do {{min_val, ""}, {max_val, ""}} when min_val <= max_val -> true _ -> false end _ -> false end end defp valid_regex?(value) do case Regex.compile(value) do {:ok, _} -> true {:error, _} -> false end end defp valid_list?(value) do values = String.split(value, ",") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) length(values) > 0 end end