Packages
phoenix_kit
1.7.74
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/shop/web/settings.ex
defmodule PhoenixKit.Modules.Shop.Web.Settings do
@moduledoc """
E-Commerce module settings LiveView.
Allows configuration of e-commerce settings including inventory tracking.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Modules.Billing
alias PhoenixKit.Modules.Shop
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
@impl true
def mount(_params, _session, socket) do
config = Shop.get_config()
# Load storefront filter configuration
storefront_filters = Shop.get_storefront_filters()
discovered_options = Shop.discover_filterable_options()
socket =
socket
|> assign(:page_title, "E-Commerce Settings")
|> assign(:enabled, config.enabled)
|> assign(:inventory_tracking, config.inventory_tracking)
|> assign(:billing_enabled, billing_enabled?())
|> assign(:category_name_display, get_category_name_display())
|> assign(:category_icon_mode, get_category_icon_mode())
|> assign(:sidebar_show_categories, get_sidebar_show_categories())
|> assign(:storefront_filters, storefront_filters)
|> assign(:discovered_options, discovered_options)
{:ok, socket}
end
defp get_category_name_display do
Settings.get_setting_cached("shop_category_name_display", "truncate")
end
defp get_category_icon_mode do
Settings.get_setting_cached("shop_category_icon_mode", "none")
end
defp get_sidebar_show_categories do
Settings.get_setting_cached("shop_sidebar_show_categories", "true") == "true"
end
@impl true
def handle_event("toggle_inventory_tracking", _params, socket) do
new_value = !socket.assigns.inventory_tracking
value_str = if(new_value, do: "true", else: "false")
case Settings.update_setting("shop_inventory_tracking", value_str) do
{:ok, _} ->
{:noreply,
socket
|> assign(:inventory_tracking, new_value)
|> put_flash(
:info,
if(new_value, do: "Inventory tracking enabled", else: "Inventory tracking disabled")
)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update inventory setting")}
end
end
@impl true
def handle_event("update_category_display", %{"display" => display}, socket) do
case Settings.update_setting("shop_category_name_display", display) do
{:ok, _} ->
{:noreply,
socket
|> assign(:category_name_display, display)
|> put_flash(:info, "Category display setting updated")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update category display setting")}
end
end
@impl true
def handle_event("toggle_sidebar_categories", _params, socket) do
new_value = !socket.assigns.sidebar_show_categories
value_str = if(new_value, do: "true", else: "false")
case Settings.update_setting("shop_sidebar_show_categories", value_str) do
{:ok, _} ->
{:noreply,
socket
|> assign(:sidebar_show_categories, new_value)
|> put_flash(
:info,
if(new_value,
do: "Categories in shop enabled",
else: "Categories in shop disabled"
)
)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update setting")}
end
end
@impl true
def handle_event("update_category_icon", %{"mode" => mode}, socket) do
case Settings.update_setting("shop_category_icon_mode", mode) do
{:ok, _} ->
{:noreply,
socket
|> assign(:category_icon_mode, mode)
|> put_flash(:info, "Category icon setting updated")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update category icon setting")}
end
end
@impl true
def handle_event("toggle_storefront_filter", %{"key" => key}, socket) do
filters =
Enum.map(socket.assigns.storefront_filters, fn f ->
if f["key"] == key, do: Map.put(f, "enabled", !f["enabled"]), else: f
end)
case Shop.update_storefront_filters(filters) do
{:ok, _} ->
{:noreply,
socket
|> assign(:storefront_filters, filters)
|> put_flash(:info, "Storefront filter updated")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update filter")}
end
end
@impl true
def handle_event("update_filter_label", %{"key" => key, "label" => label}, socket) do
filters =
Enum.map(socket.assigns.storefront_filters, fn f ->
if f["key"] == key, do: Map.put(f, "label", label), else: f
end)
case Shop.update_storefront_filters(filters) do
{:ok, _} ->
{:noreply, assign(socket, :storefront_filters, filters)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update filter label")}
end
end
@impl true
def handle_event("add_metadata_filter", %{"key" => option_key}, socket) do
existing_keys = Enum.map(socket.assigns.storefront_filters, & &1["key"])
if option_key in existing_keys do
{:noreply, put_flash(socket, :error, "Filter for '#{option_key}' already exists")}
else
max_pos =
socket.assigns.storefront_filters
|> Enum.map(& &1["position"])
|> Enum.max(fn -> 0 end)
new_filter = %{
"key" => option_key,
"type" => "metadata_option",
"option_key" => option_key,
"label" => String.capitalize(option_key),
"enabled" => true,
"position" => max_pos + 1
}
filters = socket.assigns.storefront_filters ++ [new_filter]
case Shop.update_storefront_filters(filters) do
{:ok, _} ->
{:noreply,
socket
|> assign(:storefront_filters, filters)
|> put_flash(:info, "Filter '#{option_key}' added")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to add filter")}
end
end
end
@impl true
def handle_event("remove_filter", %{"key" => key}, socket) do
filters = Enum.reject(socket.assigns.storefront_filters, &(&1["key"] == key))
case Shop.update_storefront_filters(filters) do
{:ok, _} ->
{:noreply,
socket
|> assign(:storefront_filters, filters)
|> put_flash(:info, "Filter removed")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to remove filter")}
end
end
@impl true
def handle_event("reset_default_filters", _params, socket) do
filters = Shop.default_storefront_filters()
case Shop.update_storefront_filters(filters) do
{:ok, _} ->
{:noreply,
socket
|> assign(:storefront_filters, filters)
|> put_flash(:info, "Filters reset to defaults")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to reset filters")}
end
end
@impl true
def render(assigns) do
~H"""
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={@phoenix_kit_current_scope}
current_path={@url_path}
current_locale={@current_locale}
page_title={@page_title}
>
<div class="container flex-col mx-auto px-4 py-6 max-w-5xl">
<.admin_page_header
back={Routes.path("/admin/shop")}
title="E-Commerce Settings"
subtitle="Configure your e-commerce store"
/>
<%!-- Inventory Settings (toggle pattern) --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-xl mb-6">
<.icon name="hero-archive-box" class="w-6 h-6" /> Inventory
</h2>
<div class="form-control">
<label class="label cursor-pointer justify-between">
<span class="label-text text-lg">
<span class="font-semibold">Track Inventory</span>
<div class="text-sm text-base-content/70 mt-1">
Enable stock tracking for products (Phase 2)
</div>
</span>
<input
type="checkbox"
class="toggle toggle-secondary"
checked={@inventory_tracking}
phx-click="toggle_inventory_tracking"
/>
</label>
</div>
</div>
</div>
<%!-- Info about Billing --%>
<div class="alert alert-info mb-6">
<.icon name="hero-information-circle" class="w-6 h-6" />
<div>
<h3 class="font-bold">Currency & Tax Settings</h3>
<p class="text-sm">
Currency and tax configuration is managed in the
<.link navigate={Routes.path("/admin/settings/billing")} class="link font-medium">
Billing module settings
</.link>
</p>
</div>
</div>
<%!-- Product Options --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-xl mb-6">
<.icon name="hero-tag" class="w-6 h-6" /> Product Options
</h2>
<div class="form-control">
<label class="label cursor-pointer justify-between">
<span class="label-text text-lg">
<span class="font-semibold">Global Product Options</span>
<div class="text-sm text-base-content/70 mt-1">
Define options that apply to all products (size, color, material, etc.)
</div>
<div class="text-xs text-base-content/50 mt-1">
Price override is configured per-option in the options settings.
</div>
</span>
<.link
navigate={Routes.path("/admin/shop/settings/options")}
class="btn btn-primary"
>
<.icon name="hero-cog-6-tooth" class="w-4 h-4 mr-2" /> Configure
</.link>
</label>
</div>
</div>
</div>
<%!-- Import Configurations --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-xl mb-6">
<.icon name="hero-funnel" class="w-6 h-6" /> Import Configurations
</h2>
<div class="form-control">
<label class="label cursor-pointer justify-between">
<span class="label-text text-lg">
<span class="font-semibold">CSV Import Filters</span>
<div class="text-sm text-base-content/70 mt-1">
Configure keyword filters and category rules for CSV product imports
</div>
</span>
<.link
navigate={Routes.path("/admin/shop/settings/import-configs")}
class="btn btn-primary"
>
<.icon name="hero-cog-6-tooth" class="w-4 h-4 mr-2" /> Configure
</.link>
</label>
</div>
</div>
</div>
<%!-- Storefront Filters --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex items-center justify-between mb-6">
<h2 class="card-title text-xl">
<.icon name="hero-funnel" class="w-6 h-6" /> Storefront Filters
</h2>
<button phx-click="reset_default_filters" class="btn btn-ghost btn-xs">
Reset to defaults
</button>
</div>
<p class="text-sm text-base-content/70 mb-4">
Configure product filters shown on the storefront sidebar.
Customers can filter by price, vendor, and product options.
</p>
<%!-- Current Filters Table --%>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Filter</th>
<th>Type</th>
<th>Label</th>
<th>Enabled</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for filter <- @storefront_filters do %>
<tr>
<td class="font-mono text-sm">{filter["key"]}</td>
<td>
<span class="badge badge-ghost badge-sm">{filter["type"]}</span>
</td>
<td>
<form phx-change="update_filter_label" phx-debounce="500">
<input type="hidden" name="key" value={filter["key"]} />
<input
type="text"
name="label"
value={filter["label"]}
class="input input-bordered input-xs w-32"
/>
</form>
</td>
<td>
<input
type="checkbox"
class="toggle toggle-primary toggle-sm"
checked={filter["enabled"]}
phx-click="toggle_storefront_filter"
phx-value-key={filter["key"]}
/>
</td>
<td>
<%= if filter["type"] == "metadata_option" do %>
<button
phx-click="remove_filter"
phx-value-key={filter["key"]}
class="btn btn-outline btn-error btn-xs tooltip tooltip-bottom"
data-tip={gettext("Remove")}
data-confirm="Remove this filter?"
>
<.icon name="hero-trash" class="w-4 h-4 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Remove")}</span>
</button>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%!-- Auto-discovered option keys --%>
<%= if @discovered_options != [] do %>
<div class="divider">Available Product Options</div>
<p class="text-sm text-base-content/70 mb-3">
These option keys were found in product metadata. Click to add as a filter.
</p>
<div class="flex flex-wrap gap-2">
<% existing_keys = Enum.map(@storefront_filters, & &1["key"]) %>
<%= for opt <- @discovered_options do %>
<%= if opt.key not in existing_keys do %>
<button
phx-click="add_metadata_filter"
phx-value-key={opt.key}
class="btn btn-outline btn-sm gap-1"
>
<.icon name="hero-plus" class="w-3 h-3" />
{opt.key}
<span class="badge badge-ghost badge-xs">{opt.count} products</span>
</button>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
<%!-- Sidebar Display Settings --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-xl mb-6">
<.icon name="hero-bars-3" class="w-6 h-6" /> Sidebar Display
</h2>
<%!-- Show Categories in Shop --%>
<div class="form-control mb-6">
<label class="label cursor-pointer justify-between">
<span class="label-text text-lg">
<span class="font-semibold">Show Categories in Shop</span>
<div class="text-sm text-base-content/70 mt-1">
Display category cards above products in the main shop page
</div>
</span>
<input
type="checkbox"
class="toggle toggle-primary"
checked={@sidebar_show_categories}
phx-click="toggle_sidebar_categories"
/>
</label>
</div>
<div class="divider"></div>
<%!-- Category Name Display --%>
<div class="form-control mb-6">
<label class="label">
<span class="label-text text-lg font-semibold">Category Name Display</span>
</label>
<p class="text-sm text-base-content/70 mb-3">
How category names should be displayed in the sidebar
</p>
<div class="flex gap-4">
<label class="label cursor-pointer gap-2">
<input
type="radio"
name="category_display"
class="radio radio-primary"
value="truncate"
checked={@category_name_display == "truncate"}
phx-click="update_category_display"
phx-value-display="truncate"
/>
<span class="label-text">Truncate (single line)</span>
</label>
<label class="label cursor-pointer gap-2">
<input
type="radio"
name="category_display"
class="radio radio-primary"
value="wrap"
checked={@category_name_display == "wrap"}
phx-click="update_category_display"
phx-value-display="wrap"
/>
<span class="label-text">Wrap (multi-line)</span>
</label>
</div>
</div>
<div class="divider"></div>
<%!-- Category Icon Mode --%>
<div class="form-control">
<label class="label">
<span class="label-text text-lg font-semibold">Category Icons</span>
</label>
<p class="text-sm text-base-content/70 mb-3">
Show icons next to category names in sidebar
</p>
<div class="flex gap-4">
<label class="label cursor-pointer gap-2">
<input
type="radio"
name="category_icon"
class="radio radio-primary"
value="none"
checked={@category_icon_mode == "none"}
phx-click="update_category_icon"
phx-value-mode="none"
/>
<span class="label-text">No icons</span>
</label>
<label class="label cursor-pointer gap-2">
<input
type="radio"
name="category_icon"
class="radio radio-primary"
value="folder"
checked={@category_icon_mode == "folder"}
phx-click="update_category_icon"
phx-value-mode="folder"
/>
<span class="label-text">Folder icon</span>
</label>
<label class="label cursor-pointer gap-2">
<input
type="radio"
name="category_icon"
class="radio radio-primary"
value="category"
checked={@category_icon_mode == "category"}
phx-click="update_category_icon"
phx-value-mode="category"
/>
<span class="label-text">Category image</span>
</label>
</div>
</div>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>
"""
end
defp billing_enabled? do
Code.ensure_loaded?(Billing) and
function_exported?(Billing, :enabled?, 0) and
Billing.enabled?()
rescue
_ -> false
end
end