defmodule PhoenixKit.Modules.Shop.Web.ImportConfigs do @moduledoc """ Import configurations management LiveView. Allows administrators to manage CSV import filter configurations including keyword filters, category rules, and option mappings. """ use PhoenixKitWeb, :live_view alias PhoenixKit.Modules.Shop alias PhoenixKit.Utils.Routes @impl true def mount(_params, _session, socket) do # Auto-seed defaults on first visit Shop.ensure_default_import_config() Shop.ensure_prom_ua_import_config() configs = Shop.list_import_configs(active_only: false) socket = socket |> assign(:page_title, "Import Configurations") |> assign(:configs, configs) |> assign(:show_modal, false) |> assign(:editing_config, nil) |> assign(:form_data, initial_form_data()) |> assign(:delete_confirm_uuid, nil) {:ok, socket} end @impl true def handle_event("show_add_modal", _params, socket) do {:noreply, socket |> assign(:show_modal, true) |> assign(:editing_config, nil) |> assign(:form_data, initial_form_data())} end @impl true def handle_event("show_edit_modal", %{"uuid" => uuid}, socket) do config = Enum.find(socket.assigns.configs, &(to_string(&1.uuid) == uuid)) if config do form_data = %{ name: config.name || "", skip_filter: config.skip_filter || false, include_keywords_text: Enum.join(config.include_keywords || [], ", "), exclude_keywords_text: Enum.join(config.exclude_keywords || [], ", "), exclude_phrases_text: Enum.join(config.exclude_phrases || [], ", "), category_rules: config.category_rules || [], default_category_slug: config.default_category_slug || "", download_images: config.download_images || false, is_default: config.is_default || false, active: config.active } {:noreply, socket |> assign(:show_modal, true) |> assign(:editing_config, config) |> assign(:form_data, form_data)} else {:noreply, socket} end end @impl true def handle_event("close_modal", _params, socket) do {:noreply, socket |> assign(:show_modal, false) |> assign(:editing_config, nil) |> assign(:form_data, initial_form_data())} end @impl true def handle_event("validate_form", %{"config" => params}, socket) do form_data = %{ name: params["name"] || "", skip_filter: params["skip_filter"] == "true", include_keywords_text: params["include_keywords_text"] || "", exclude_keywords_text: params["exclude_keywords_text"] || "", exclude_phrases_text: params["exclude_phrases_text"] || "", category_rules: socket.assigns.form_data.category_rules, default_category_slug: params["default_category_slug"] || "", download_images: params["download_images"] == "true", is_default: params["is_default"] == "true", active: params["active"] == "true" } {:noreply, assign(socket, :form_data, form_data)} end @impl true def handle_event("save_config", %{"config" => params}, socket) do form_data = %{ name: params["name"] || "", skip_filter: params["skip_filter"] == "true", include_keywords_text: params["include_keywords_text"] || "", exclude_keywords_text: params["exclude_keywords_text"] || "", exclude_phrases_text: params["exclude_phrases_text"] || "", category_rules: socket.assigns.form_data.category_rules, default_category_slug: params["default_category_slug"] || "", download_images: params["download_images"] == "true", is_default: params["is_default"] == "true", active: params["active"] == "true" } attrs = build_attrs(form_data) editing = socket.assigns.editing_config result = if editing do Shop.update_import_config(editing, attrs) else Shop.create_import_config(attrs) end case result do {:ok, _} -> {:noreply, socket |> assign(:configs, Shop.list_import_configs(active_only: false)) |> assign(:show_modal, false) |> assign(:editing_config, nil) |> assign(:form_data, initial_form_data()) |> put_flash(:info, if(editing, do: "Config updated", else: "Config created"))} {:error, changeset} -> message = format_changeset_errors(changeset) {:noreply, put_flash(socket, :error, "Error: #{message}")} end end @impl true def handle_event("delete_config", %{"uuid" => uuid}, socket) do config = Enum.find(socket.assigns.configs, &(to_string(&1.uuid) == uuid)) if config do case Shop.delete_import_config(config) do {:ok, _} -> {:noreply, socket |> assign(:configs, Shop.list_import_configs(active_only: false)) |> put_flash(:info, "Config deleted")} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to delete config")} end else {:noreply, socket} end end @impl true def handle_event("toggle_active", %{"uuid" => uuid}, socket) do config = Enum.find(socket.assigns.configs, &(to_string(&1.uuid) == uuid)) if config do case Shop.update_import_config(config, %{active: !config.active}) do {:ok, _} -> {:noreply, socket |> assign(:configs, Shop.list_import_configs(active_only: false)) |> put_flash(:info, "Config #{if config.active, do: "deactivated", else: "activated"}")} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to update config")} end else {:noreply, socket} end end @impl true def handle_event("set_default", %{"uuid" => uuid}, socket) do config = Enum.find(socket.assigns.configs, &(to_string(&1.uuid) == uuid)) if config do case Shop.update_import_config(config, %{is_default: true}) do {:ok, _} -> {:noreply, socket |> assign(:configs, Shop.list_import_configs(active_only: false)) |> put_flash(:info, "\"#{config.name}\" set as default")} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to set default")} end else {:noreply, socket} end end # Category rule management @impl true def handle_event("add_category_rule", _params, socket) do form_data = socket.assigns.form_data new_rule = %{"keywords" => [], "slug" => "", "keywords_text" => ""} updated = %{form_data | category_rules: form_data.category_rules ++ [new_rule]} {:noreply, assign(socket, :form_data, updated)} end @impl true def handle_event("remove_category_rule", %{"index" => idx}, socket) do form_data = socket.assigns.form_data index = String.to_integer(idx) updated = %{form_data | category_rules: List.delete_at(form_data.category_rules, index)} {:noreply, assign(socket, :form_data, updated)} end @impl true def handle_event("update_category_rule", %{"index" => idx} = params, socket) do form_data = socket.assigns.form_data index = String.to_integer(idx) rule = Enum.at(form_data.category_rules, index) if rule do keywords_text = params["keywords"] || Map.get(rule, "keywords_text", "") slug = params["slug"] || Map.get(rule, "slug", "") keywords = parse_comma_list(keywords_text) updated_rule = %{ "keywords" => keywords, "slug" => slug, "keywords_text" => keywords_text } updated_rules = List.replace_at(form_data.category_rules, index, updated_rule) {:noreply, assign(socket, :form_data, %{form_data | category_rules: updated_rules})} else {:noreply, socket} end end @impl true def handle_event("toggle_skip_filter", _params, socket) do form_data = socket.assigns.form_data {:noreply, assign(socket, :form_data, %{form_data | skip_filter: !form_data.skip_filter})} end @impl true def render(assigns) do ~H"""
<.admin_page_header back={Routes.path("/admin/shop/settings")} title="Import Configurations" subtitle="Configure keyword filters and category rules for CSV product imports" /> <%!-- Controls Bar --%>
<%!-- Info Alert --%>
<.icon name="hero-information-circle" class="w-6 h-6" />

Import filter configurations

Each config defines keyword filters and category rules for CSV imports. The default config is used when no specific config is selected during import.

<%!-- Configs List --%>

<.icon name="hero-funnel" class="w-5 h-5" /> Configurations

<%= if @configs == [] do %>
<.icon name="hero-funnel" class="w-16 h-16 mx-auto mb-4 opacity-30" />

No configurations defined yet

Add your first import configuration to get started

<% else %>
<%= for config <- @configs do %>
{config.name} <%= if config.is_default do %> Default <% end %> <%= if config.active do %> Active <% else %> Inactive <% end %> <%= if config.skip_filter do %> Skip Filter <% end %> <%= if config.download_images do %> Download Images <% end %>
<.icon name="hero-plus-circle" class="w-3 h-3 inline" /> {length(config.include_keywords)} include <.icon name="hero-minus-circle" class="w-3 h-3 inline" /> {length(config.exclude_keywords)} exclude <.icon name="hero-tag" class="w-3 h-3 inline" /> {length(config.category_rules)} category rules <%= if config.default_category_slug && config.default_category_slug != "" do %> <.icon name="hero-folder" class="w-3 h-3 inline" /> default: {config.default_category_slug} <% end %>
<%= unless config.is_default do %> <% end %>
<% end %>
<% end %>
<%!-- Modal for Add/Edit Config --%> <%= if @show_modal do %> <% end %>
""" end # Private helpers defp initial_form_data do %{ name: "", skip_filter: false, include_keywords_text: "", exclude_keywords_text: "", exclude_phrases_text: "", category_rules: [], default_category_slug: "", download_images: false, is_default: false, active: true } end defp build_attrs(form_data) do category_rules = form_data.category_rules |> Enum.map(fn rule -> keywords_text = Map.get(rule, "keywords_text", Enum.join(Map.get(rule, "keywords", []), ", ")) %{ "keywords" => parse_comma_list(keywords_text), "slug" => Map.get(rule, "slug", "") } end) |> Enum.reject(fn rule -> rule["slug"] == "" and rule["keywords"] == [] end) %{ name: form_data.name, skip_filter: form_data.skip_filter, include_keywords: parse_comma_list(form_data.include_keywords_text), exclude_keywords: parse_comma_list(form_data.exclude_keywords_text), exclude_phrases: parse_comma_list(form_data.exclude_phrases_text), category_rules: category_rules, default_category_slug: form_data.default_category_slug, download_images: form_data.download_images, is_default: form_data.is_default, active: form_data.active } end defp parse_comma_list(text) when is_binary(text) do text |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) end defp parse_comma_list(_), do: [] defp format_changeset_errors(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> Regex.replace(~r"%{(\w+)}", msg, fn _, key -> opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() end) end) |> Enum.map_join(", ", fn {field, errors} -> "#{field}: #{Enum.join(errors, ", ")}" end) end end