berrypod/lib/berrypod_web/live/admin/orders.ex
jamey ae6cf209aa complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
  primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
  semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
  order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
  chart dimensions) and one JS.toggle target

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00

181 lines
4.9 KiB
Elixir

defmodule BerrypodWeb.Admin.Orders do
use BerrypodWeb, :live_view
alias Berrypod.Orders
alias Berrypod.Cart
@impl true
def mount(_params, _session, socket) do
counts = Orders.count_orders_by_status()
socket =
socket
|> assign(:page_title, "Orders")
|> assign(:status_filter, "all")
|> assign(:status_counts, counts)
{:ok, socket}
end
@impl true
def handle_params(params, _uri, socket) do
page_num = Berrypod.Pagination.parse_page(params)
page = Orders.list_orders_paginated(status: socket.assigns.status_filter, page: page_num)
socket =
socket
|> assign(:pagination, page)
|> assign(:order_count, page.total_count)
|> stream(:orders, page.items, reset: true)
{:noreply, socket}
end
@impl true
def handle_event("filter", %{"status" => status}, socket) do
{:noreply,
socket
|> assign(:status_filter, status)
|> push_patch(to: ~p"/admin/orders")}
end
@impl true
def render(assigns) do
~H"""
<.header>
Orders
</.header>
<div class="admin-filter-row">
<.filter_tab
status="all"
label="All"
count={total_count(@status_counts)}
active={@status_filter}
/>
<.filter_tab
status="paid"
label="Paid"
count={@status_counts["paid"]}
active={@status_filter}
/>
<.filter_tab
status="pending"
label="Pending"
count={@status_counts["pending"]}
active={@status_filter}
/>
<.filter_tab
status="failed"
label="Failed"
count={@status_counts["failed"]}
active={@status_filter}
/>
<.filter_tab
status="refunded"
label="Refunded"
count={@status_counts["refunded"]}
active={@status_filter}
/>
</div>
<.table
:if={@order_count > 0}
id="orders"
rows={@streams.orders}
row_item={fn {_id, order} -> order end}
row_click={fn {_id, order} -> JS.navigate(~p"/admin/orders/#{order}") end}
>
<:col :let={order} label="Order">{order.order_number}</:col>
<:col :let={order} label="Date">{format_date(order.inserted_at)}</:col>
<:col :let={order} label="Customer">{order.customer_email || "—"}</:col>
<:col :let={order} label="Total">{Cart.format_price(order.total)}</:col>
<:col :let={order} label="Status"><.status_badge status={order.payment_status} /></:col>
<:col :let={order} label="Fulfilment">
<.fulfilment_badge status={order.fulfilment_status} />
</:col>
</.table>
<.admin_pagination :if={@order_count > 0} page={@pagination} patch={~p"/admin/orders"} />
<div :if={@order_count == 0} class="admin-empty-state">
<.icon name="hero-inbox" class="admin-empty-state-icon" />
<p class="admin-empty-state-title">No orders yet</p>
<p class="admin-empty-state-text">Orders will appear here once customers check out.</p>
</div>
"""
end
defp filter_tab(assigns) do
count = assigns[:count] || 0
active = assigns.active == assigns.status
assigns = assign(assigns, count: count, active: active)
~H"""
<button
phx-click="filter"
phx-value-status={@status}
class={[
"admin-btn admin-btn-sm",
@active && "admin-btn-primary",
!@active && "admin-btn-ghost"
]}
>
{@label}
<span :if={@count > 0} class="admin-badge admin-badge-sm admin-badge-count">
{@count}
</span>
</button>
"""
end
defp status_badge(assigns) do
{color, icon} =
case assigns.status do
"paid" -> {"green", "hero-check-circle-mini"}
"pending" -> {"amber", "hero-clock-mini"}
"failed" -> {"red", "hero-x-circle-mini"}
"refunded" -> {"zinc", "hero-arrow-uturn-left-mini"}
_ -> {"zinc", "hero-question-mark-circle-mini"}
end
assigns = assign(assigns, color: color, icon: icon)
~H"""
<span class={["admin-status-pill", "admin-status-pill-#{@color}"]}>
<.icon name={@icon} class="size-3" /> {@status}
</span>
"""
end
defp format_date(datetime) do
Calendar.strftime(datetime, "%d %b %Y %H:%M")
end
defp fulfilment_badge(assigns) do
{color, icon} =
case assigns.status do
"submitted" -> {"blue", "hero-paper-airplane-mini"}
"processing" -> {"amber", "hero-cog-6-tooth-mini"}
"shipped" -> {"purple", "hero-truck-mini"}
"delivered" -> {"green", "hero-check-circle-mini"}
"failed" -> {"red", "hero-x-circle-mini"}
"cancelled" -> {"zinc", "hero-no-symbol-mini"}
_ -> {"zinc", "hero-minus-circle-mini"}
end
assigns = assign(assigns, color: color, icon: icon)
~H"""
<span class={["admin-status-pill", "admin-status-pill-#{@color}"]}>
<.icon name={@icon} class="size-3" /> {@status}
</span>
"""
end
defp total_count(counts) do
counts |> Map.values() |> Enum.sum()
end
end