berrypod/lib/simpleshop_theme_web/live/admin/dashboard.ex
jamey 0dac93ec0b add admin dashboard with setup checklist and stats
Dashboard at /admin shows setup progress (when not live), stat cards
(orders, revenue, products), and recent paid orders table. Replaces
the old AdminController redirect. Add Dashboard to sidebar nav as
first item, update admin bar and theme editor links to /admin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:17:38 +00:00

208 lines
6.9 KiB
Elixir

defmodule SimpleshopThemeWeb.Admin.Dashboard do
use SimpleshopThemeWeb, :live_view
alias SimpleshopTheme.{Cart, Orders, Setup}
@impl true
def mount(_params, _session, socket) do
status = Setup.setup_status()
status_counts = Orders.count_orders_by_status()
paid_count = Map.get(status_counts, "paid", 0)
recent_orders = Orders.list_orders(status: "paid") |> Enum.take(5)
{:ok,
socket
|> assign(:page_title, "Dashboard")
|> assign(:setup, status)
|> assign(:paid_count, paid_count)
|> assign(:revenue, Orders.total_revenue())
|> assign(:recent_orders, recent_orders)}
end
@impl true
def render(assigns) do
~H"""
<.header>
Dashboard
</.header>
<%!-- Setup checklist (when not fully set up) --%>
<.setup_checklist :if={!@setup.site_live} setup={@setup} />
<%!-- Stats --%>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-6">
<.stat_card
label="Orders"
value={@paid_count}
icon="hero-shopping-bag"
href={~p"/admin/orders"}
/>
<.stat_card
label="Revenue"
value={format_revenue(@revenue)}
icon="hero-banknotes"
href={~p"/admin/orders"}
/>
<.stat_card
label="Products"
value={@setup.product_count}
icon="hero-cube"
href={~p"/admin/settings"}
/>
</div>
<%!-- Recent orders --%>
<section class="mt-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold">Recent orders</h2>
<.link navigate={~p"/admin/orders"} class="text-sm text-zinc-500 hover:text-zinc-700">
View all &rarr;
</.link>
</div>
<%= if @recent_orders == [] do %>
<div class="rounded-lg border border-zinc-200 p-8 text-center text-zinc-500">
<.icon name="hero-inbox" class="size-10 mx-auto mb-3 text-zinc-300" />
<p class="font-medium">No orders yet</p>
<p class="text-sm mt-1">Orders will appear here once customers check out.</p>
</div>
<% else %>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-zinc-200 text-left text-zinc-500">
<th class="pb-2 font-medium">Order</th>
<th class="pb-2 font-medium">Date</th>
<th class="pb-2 font-medium">Customer</th>
<th class="pb-2 font-medium text-right">Total</th>
<th class="pb-2 font-medium">Fulfilment</th>
</tr>
</thead>
<tbody>
<tr
:for={order <- @recent_orders}
class="border-b border-zinc-100 hover:bg-zinc-50 cursor-pointer"
phx-click={JS.navigate(~p"/admin/orders/#{order}")}
>
<td class="py-2.5 font-medium">{order.order_number}</td>
<td class="py-2.5 text-zinc-600">{format_date(order.inserted_at)}</td>
<td class="py-2.5 text-zinc-600">{order.customer_email || "—"}</td>
<td class="py-2.5 text-right">{Cart.format_price(order.total)}</td>
<td class="py-2.5"><.fulfilment_pill status={order.fulfilment_status} /></td>
</tr>
</tbody>
</table>
</div>
<% end %>
</section>
"""
end
# -- Function components --
attr :setup, :map, required: true
defp setup_checklist(assigns) do
steps = [
%{done: assigns.setup.admin_created, label: "Create admin account", href: nil},
%{
done: assigns.setup.printify_connected,
label: "Connect to Printify",
href: ~p"/admin/providers/new"
},
%{done: assigns.setup.products_synced, label: "Sync products", href: nil},
%{done: assigns.setup.stripe_connected, label: "Connect Stripe", href: ~p"/admin/settings"},
%{done: assigns.setup.site_live, label: "Go live", href: ~p"/admin/settings"}
]
done_count = Enum.count(steps, & &1.done)
assigns = assign(assigns, steps: steps, done_count: done_count, total: length(steps))
~H"""
<div class="mt-6 rounded-lg border border-zinc-200 p-5">
<div class="flex items-center justify-between mb-3">
<h2 class="font-semibold">Setup progress</h2>
<span class="text-sm text-zinc-500">{@done_count}/{@total}</span>
</div>
<div class="w-full bg-zinc-100 rounded-full h-2 mb-4">
<div
class="bg-green-500 h-2 rounded-full transition-all"
style={"width: #{@done_count / @total * 100}%"}
>
</div>
</div>
<ul class="space-y-2">
<li :for={step <- @steps} class="flex items-center gap-2 text-sm">
<%= if step.done do %>
<.icon name="hero-check-circle" class="size-5 text-green-500 shrink-0" />
<span class="text-zinc-500 line-through">{step.label}</span>
<% else %>
<.icon name="hero-circle-stack" class="size-5 text-zinc-300 shrink-0" />
<%= if step.href do %>
<.link navigate={step.href} class="text-zinc-700 hover:underline">{step.label}</.link>
<% else %>
<span class="text-zinc-700">{step.label}</span>
<% end %>
<% end %>
</li>
</ul>
</div>
"""
end
attr :label, :string, required: true
attr :value, :any, required: true
attr :icon, :string, required: true
attr :href, :string, required: true
defp stat_card(assigns) do
~H"""
<.link
navigate={@href}
class="rounded-lg border border-zinc-200 p-4 hover:border-zinc-300 transition-colors"
>
<div class="flex items-center gap-3">
<div class="rounded-lg bg-zinc-100 p-2">
<.icon name={@icon} class="size-5 text-zinc-600" />
</div>
<div>
<p class="text-2xl font-bold">{@value}</p>
<p class="text-sm text-zinc-500">{@label}</p>
</div>
</div>
</.link>
"""
end
defp fulfilment_pill(assigns) do
{color, label} =
case assigns.status do
"unfulfilled" -> {"bg-zinc-100 text-zinc-600", "unfulfilled"}
"submitted" -> {"bg-blue-50 text-blue-700", "submitted"}
"processing" -> {"bg-amber-50 text-amber-700", "processing"}
"shipped" -> {"bg-purple-50 text-purple-700", "shipped"}
"delivered" -> {"bg-green-50 text-green-700", "delivered"}
"failed" -> {"bg-red-50 text-red-700", "failed"}
_ -> {"bg-zinc-100 text-zinc-600", assigns.status || ""}
end
assigns = assign(assigns, color: color, label: label)
~H"""
<span class={["inline-flex rounded-full px-2 py-0.5 text-xs font-medium", @color]}>
{@label}
</span>
"""
end
defp format_revenue(amount_pence) when is_integer(amount_pence) do
Cart.format_price(amount_pence)
end
defp format_revenue(_), do: "£0.00"
defp format_date(datetime) do
Calendar.strftime(datetime, "%d %b %Y")
end
end