defmodule PhoenixKitEcommerce.Web.Carts do
@moduledoc """
Carts admin list LiveView for E-Commerce module.
"""
use PhoenixKitEcommerce.Web, :live_view
import PhoenixKitWeb.Components.Core.TableDefault
alias PhoenixKit.Utils.Routes
alias PhoenixKitBilling.Currency
alias PhoenixKitEcommerce, as: Shop
@per_page 25
# Last-resort currency symbol used only when no default currency is
# configured in Billing (i.e. `@currency` resolved to nil at mount).
# When a currency struct is present we always defer to its own symbol.
@default_currency_symbol "$"
@impl true
def mount(_params, _session, socket) do
{carts, total} = Shop.list_carts_with_count(per_page: @per_page)
currency = Shop.get_default_currency()
socket =
socket
|> assign(:page_title, "Shopping Carts")
|> assign(:carts, carts)
|> assign(:total, total)
|> assign(:page, 1)
|> assign(:per_page, @per_page)
|> assign(:status_filter, nil)
|> assign(:search, "")
|> assign(:currency, currency)
{:ok, socket}
end
@impl true
def handle_params(params, _uri, socket) do
page = String.to_integer(params["page"] || "1")
status = params["status"]
search = params["search"] || ""
{carts, total} =
Shop.list_carts_with_count(
page: page,
per_page: @per_page,
status: status,
search: search
)
socket =
socket
|> assign(:carts, carts)
|> assign(:total, total)
|> assign(:page, page)
|> assign(:status_filter, status)
|> assign(:search, search)
{:noreply, socket}
end
@impl true
def handle_event("filter_status", %{"status" => status}, socket) do
status = if status == "", do: nil, else: status
{:noreply, push_patch(socket, to: build_url(socket.assigns, status: status, page: 1))}
end
@impl true
def handle_event("search", %{"search" => search}, socket) do
{:noreply, push_patch(socket, to: build_url(socket.assigns, search: search, page: 1))}
end
defp build_url(assigns, overrides) do
params =
%{
status: Keyword.get(overrides, :status, assigns.status_filter),
search: Keyword.get(overrides, :search, assigns.search),
page: Keyword.get(overrides, :page, assigns.page)
}
|> Enum.filter(fn {_k, v} -> v && v != "" end)
|> URI.encode_query()
if params == "" do
Routes.path("/admin/shop/carts")
else
Routes.path("/admin/shop/carts?#{params}")
end
end
@impl true
def render(assigns) do
~H"""
<.admin_page_header back={Routes.path("/admin/shop")}>
Shopping Carts
{@total} carts total
<%!-- Controls Bar --%>
<%!-- Search --%>
<%!-- Status Filter --%>
<%!-- Carts Table --%>
<.table_default id="carts-table" variant="zebra" class="w-full" toggleable={true} items={@carts}
card_fields={fn cart -> [
%{label: "Items", value: "#{cart.items_count || 0} items"},
%{label: "Total", value: if(cart.total, do: Decimal.to_string(Decimal.round(cart.total, 2)), else: "-")},
%{label: "Status", value: cart.status || "-"},
%{label: "Updated", value: Calendar.strftime(cart.updated_at, "%Y-%m-%d %H:%M")}
] end}>
<:card_header :let={cart}>
<%= if cart.user do %>
{cart.user.email}
<% else %>
Guest — {String.slice(cart.session_id || "", 0, 16)}...
<% end %>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>Customer
<.table_default_header_cell>Items
<.table_default_header_cell class="text-right">Total
<.table_default_header_cell>Status
<.table_default_header_cell>Updated
<.table_default_body>
<%= if @carts == [] do %>
<.table_default_row>
<.table_default_cell class="text-center py-12 text-base-content/50">
<.icon name="hero-shopping-cart" class="w-12 h-12 mx-auto mb-3 opacity-50" />
No carts found
Carts will appear here when customers add items
<% else %>
<%= for cart <- @carts do %>
<.table_default_row class="hover">
<.table_default_cell>
<%= if cart.user do %>
{cart.user.email}
User UUID: {cart.user.uuid}
<% else %>
Guest
{String.slice(cart.session_id || "", 0, 16)}...
<% end %>
<.table_default_cell>
{cart.items_count || 0} items
<%= if cart.total_weight_grams && cart.total_weight_grams > 0 do %>
{format_weight(cart.total_weight_grams)}
<% end %>
<.table_default_cell class="text-right">
{format_price(cart.total, @currency)}
<%= if Decimal.compare(cart.subtotal || Decimal.new("0"), cart.total || Decimal.new("0")) != :eq do %>
Subtotal: {format_price(cart.subtotal, @currency)}
<% end %>
<.table_default_cell>
{cart.status}
<.table_default_cell>
{format_datetime(cart.updated_at)}
<%= if cart.expires_at do %>
Expires: {format_datetime(cart.expires_at)}
<% end %>
<% end %>
<% end %>
<%!-- Pagination --%>
<%= if @total > @per_page do %>
<%= for page_num <- 1..ceil(@total / @per_page) do %>
<.link
patch={build_url(assigns, page: page_num)}
class={["join-item btn btn-sm", if(@page == page_num, do: "btn-active")]}
>
{page_num}
<% end %>
<% end %>
"""
end
defp status_badge_class("active"), do: "badge badge-success"
defp status_badge_class("converted"), do: "badge badge-info"
defp status_badge_class("abandoned"), do: "badge badge-warning"
defp status_badge_class("expired"), do: "badge badge-neutral"
defp status_badge_class("merged"), do: "badge badge-secondary"
defp status_badge_class(_), do: "badge"
defp format_price(nil, _currency), do: "-"
defp format_price(amount, nil) do
"#{@default_currency_symbol}#{Decimal.round(amount, 2)}"
end
defp format_price(amount, currency) do
Currency.format_amount(amount, currency)
end
defp format_weight(grams) when grams >= 1000, do: "#{Float.round(grams / 1000, 1)} kg"
defp format_weight(grams), do: "#{grams} g"
defp format_datetime(nil), do: "-"
defp format_datetime(datetime) do
Calendar.strftime(datetime, "%Y-%m-%d %H:%M")
end
end