berrypod/lib/simpleshop_theme_web/live/admin/dashboard.ex

208 lines
6.9 KiB
Elixir
Raw Normal View History

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