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