defmodule SimpleshopThemeWeb.Admin.Dashboard do use SimpleshopThemeWeb, :live_view alias SimpleshopTheme.{Cart, Orders, Products, Settings, Setup} alias SimpleshopTheme.Products.ProviderConnection alias SimpleshopTheme.Providers alias SimpleshopTheme.Stripe.Setup, as: StripeSetup @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) conn = Products.get_provider_connection_by_type("printify") if conn && connected?(socket) do Phoenix.PubSub.subscribe(SimpleshopTheme.PubSub, "sync:#{conn.id}") end active_step = determine_active_step(status) {:ok, socket |> assign(:page_title, "Dashboard") |> assign(:setup, status) |> assign(:active_step, active_step) # Printify state |> assign(:printify_conn, conn) |> assign(:printify_form, to_form(%{"api_key" => ""}, as: :printify)) |> assign(:printify_testing, false) |> assign(:printify_test_result, nil) |> assign(:printify_saving, false) |> assign(:pending_api_key, nil) |> assign(:sync_status, conn && conn.sync_status) # Stripe state |> assign(:stripe_form, to_form(%{"api_key" => ""}, as: :stripe)) |> assign(:stripe_connecting, false) |> assign(:stripe_api_key_hint, Settings.secret_hint("stripe_api_key")) # Celebration |> assign(:just_went_live, false) # Stats |> assign(:paid_count, paid_count) |> assign(:revenue, Orders.total_revenue()) |> assign(:recent_orders, recent_orders)} end # -- Step determination -- defp determine_active_step(status) do cond do !status.printify_connected -> :printify !status.products_synced -> :printify !status.stripe_connected -> :stripe !status.site_live -> :go_live true -> :complete end end # -- Events: Printify -- @impl true def handle_event("validate_printify", %{"printify" => params}, socket) do {:noreply, assign(socket, pending_api_key: params["api_key"])} end def handle_event("test_printify", _params, socket) do api_key = socket.assigns.pending_api_key if api_key in [nil, ""] do {:noreply, assign(socket, printify_test_result: {:error, :no_api_key})} else socket = assign(socket, printify_testing: true, printify_test_result: nil) temp_conn = %ProviderConnection{ provider_type: "printify", api_key_encrypted: encrypt_api_key(api_key) } result = Providers.test_connection(temp_conn) {:noreply, assign(socket, printify_testing: false, printify_test_result: result)} end end def handle_event("connect_printify", %{"printify" => %{"api_key" => api_key}}, socket) do if api_key == "" do {:noreply, put_flash(socket, :error, "Please enter your Printify API token")} else socket = assign(socket, printify_saving: true) params = %{"api_key" => api_key, "provider_type" => "printify"} |> maybe_add_shop_config(socket.assigns.printify_test_result) |> maybe_add_name(socket.assigns.printify_test_result) case Products.create_provider_connection(params) do {:ok, connection} -> Products.enqueue_sync(connection) if connected?(socket) do Phoenix.PubSub.subscribe(SimpleshopTheme.PubSub, "sync:#{connection.id}") end status = %{socket.assigns.setup | printify_connected: true} {:noreply, socket |> assign(:printify_saving, false) |> assign(:printify_conn, connection) |> assign(:sync_status, "syncing") |> assign(:setup, status) |> put_flash(:info, "Connected to Printify! Syncing products...")} {:error, _changeset} -> {:noreply, socket |> assign(:printify_saving, false) |> put_flash(:error, "Failed to save connection")} end end end def handle_event("retry_sync", _params, socket) do conn = socket.assigns.printify_conn if conn do Products.enqueue_sync(conn) {:noreply, assign(socket, sync_status: "syncing")} else {:noreply, socket} end end # -- Events: Stripe -- def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do if api_key == "" do {:noreply, put_flash(socket, :error, "Please enter your Stripe secret key")} else socket = assign(socket, stripe_connecting: true) case StripeSetup.connect(api_key) do {:ok, _result} -> status = %{socket.assigns.setup | stripe_connected: true, can_go_live: true} {:noreply, socket |> assign(:stripe_connecting, false) |> assign(:setup, status) |> assign(:stripe_api_key_hint, Settings.secret_hint("stripe_api_key")) |> assign(:active_step, :go_live) |> put_flash(:info, "Stripe connected")} {:error, message} -> {:noreply, socket |> assign(:stripe_connecting, false) |> put_flash(:error, "Stripe connection failed: #{message}")} end end end # -- Events: Go live -- def handle_event("go_live", _params, socket) do {:ok, _} = Settings.set_site_live(true) status = %{socket.assigns.setup | site_live: true} {:noreply, socket |> assign(:setup, status) |> assign(:just_went_live, true)} end # -- Events: Step navigation -- def handle_event("toggle_step", %{"step" => step}, socket) do step = String.to_existing_atom(step) new_active = if socket.assigns.active_step == step do determine_active_step(socket.assigns.setup) else step end {:noreply, assign(socket, active_step: new_active)} end # -- PubSub: Sync progress -- @impl true def handle_info({:sync_status, "completed", product_count}, socket) do status = %{ socket.assigns.setup | products_synced: true, product_count: product_count } active_step = if status.stripe_connected, do: :go_live, else: :stripe {:noreply, socket |> assign(:setup, status) |> assign(:sync_status, "completed") |> assign(:active_step, active_step) |> put_flash(:info, "#{product_count} products synced")} end def handle_info({:sync_status, "failed"}, socket) do {:noreply, socket |> assign(:sync_status, "failed") |> put_flash(:error, "Product sync failed — try again")} end def handle_info({:sync_status, status}, socket) do {:noreply, assign(socket, sync_status: status)} end # -- Render -- @impl true def render(assigns) do ~H""" <.header> Dashboard <%!-- Celebration state --%> <.celebration :if={@just_went_live} /> <%!-- Setup stepper (when not live and not celebrating) --%> <.setup_stepper :if={!@setup.site_live and !@just_went_live} setup={@setup} active_step={@active_step} printify_conn={@printify_conn} printify_form={@printify_form} printify_testing={@printify_testing} printify_test_result={@printify_test_result} printify_saving={@printify_saving} sync_status={@sync_status} stripe_form={@stripe_form} stripe_connecting={@stripe_connecting} stripe_api_key_hint={@stripe_api_key_hint} /> <%!-- Stats --%>
<.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"} />
<%!-- Recent orders --%>

Recent orders

<.link navigate={~p"/admin/orders"} class="text-sm text-base-content/60 hover:text-base-content" > View all →
<%= if @recent_orders == [] do %>
<.icon name="hero-inbox" class="size-10 mx-auto mb-3 text-base-content/30" />

No orders yet

Orders will appear here once customers check out.

<% else %>
Order Date Customer Total Fulfilment
{order.order_number} {format_date(order.inserted_at)} {order.customer_email || "—"} {Cart.format_price(order.total)} <.fulfilment_pill status={order.fulfilment_status} />
<% end %>
""" end # ========================================================================== # Setup stepper components # ========================================================================== attr :setup, :map, required: true attr :active_step, :atom, required: true attr :printify_conn, :any, required: true attr :printify_form, :any, required: true attr :printify_testing, :boolean, required: true attr :printify_test_result, :any, required: true attr :printify_saving, :boolean, required: true attr :sync_status, :string, required: true attr :stripe_form, :any, required: true attr :stripe_connecting, :boolean, required: true attr :stripe_api_key_hint, :string, required: true defp setup_stepper(assigns) do ~H"""
    <%!-- Step 1: Printify --%> <.setup_step step={:printify} number={1} title="Connect to Printify" active_step={@active_step} done={@setup.printify_connected and @setup.products_synced} last={false} next_done={@setup.stripe_connected} > <:summary :if={@setup.printify_connected and @setup.products_synced}> Connected · {@setup.product_count} products synced <:content> <.printify_step_content setup={@setup} printify_conn={@printify_conn} printify_form={@printify_form} printify_testing={@printify_testing} printify_test_result={@printify_test_result} printify_saving={@printify_saving} sync_status={@sync_status} /> <%!-- Step 2: Stripe --%> <.setup_step step={:stripe} number={2} title="Connect Stripe" active_step={@active_step} done={@setup.stripe_connected} last={false} next_done={@setup.site_live} > <:summary :if={@setup.stripe_connected}> Connected · {@stripe_api_key_hint} <:content> <.stripe_step_content stripe_form={@stripe_form} stripe_connecting={@stripe_connecting} /> <%!-- Step 3: Go live --%> <.setup_step step={:go_live} number={3} title="Go live" active_step={@active_step} done={@setup.site_live} last={true} next_done={false} > <:content> <.go_live_step_content setup={@setup} />
""" end attr :step, :atom, required: true attr :number, :integer, required: true attr :title, :string, required: true attr :active_step, :atom, required: true attr :done, :boolean, required: true attr :last, :boolean, required: true attr :next_done, :boolean, required: true slot :summary slot :content, required: true defp setup_step(assigns) do is_active = assigns.active_step == assigns.step is_clickable = assigns.done assigns = assigns |> assign(:is_active, is_active) |> assign(:is_clickable, is_clickable) ~H"""
  • <%!-- Connector line --%>
  • """ end # -- Printify step content -- attr :setup, :map, required: true attr :printify_conn, :any, required: true attr :printify_form, :any, required: true attr :printify_testing, :boolean, required: true attr :printify_test_result, :any, required: true attr :printify_saving, :boolean, required: true attr :sync_status, :string, required: true defp printify_step_content(assigns) do ~H""" <%!-- Not yet connected: show form --%>

    Connect your Printify account to import products. Get an API token from Printify → Account → Connections .

    <.form for={@printify_form} phx-change="validate_printify" phx-submit="connect_printify" > <.input field={@printify_form[:api_key]} type="password" label="Printify API token" placeholder="Paste your token here" autocomplete="off" />
    <.button type="submit" disabled={@printify_saving or @printify_testing}> {if @printify_saving, do: "Connecting...", else: "Connect to Printify"}
    <.printify_test_feedback :if={@printify_test_result} result={@printify_test_result} />
    <%!-- Connected, syncing --%>
    <.icon name="hero-arrow-path" class="size-4 animate-spin text-base-content/40" /> Syncing products from Printify...
    <%!-- Connected, sync failed --%>

    Product sync failed.

    <%!-- Connected, synced (shown when user expands a completed step) --%>

    {@setup.product_count} products synced from Printify.

    """ end attr :result, :any, required: true defp printify_test_feedback(assigns) do ~H"""
    <%= case @result do %> <% {:ok, info} -> %> <.icon name="hero-check-circle" class="size-4" /> Connected to {info.shop_name} <% {:error, reason} -> %> <.icon name="hero-x-circle" class="size-4" /> {format_printify_error(reason)} <% end %>
    """ end # -- Stripe step content -- attr :stripe_form, :any, required: true attr :stripe_connecting, :boolean, required: true defp stripe_step_content(assigns) do ~H"""

    Enter your Stripe secret key to accept payments. Find it in your Stripe dashboard under Developers → API keys.

    <.form for={@stripe_form} phx-submit="connect_stripe"> <.input field={@stripe_form[:api_key]} type="password" label="Secret key" autocomplete="off" placeholder="sk_test_... or sk_live_..." />

    Starts with sk_test_ or sk_live_. Encrypted at rest.

    <.button phx-disable-with="Connecting..."> {if @stripe_connecting, do: "Connecting...", else: "Connect Stripe"}
    """ end # -- Go live step content -- attr :setup, :map, required: true defp go_live_step_content(assigns) do ~H"""

    Your shop is ready. Visitors currently see a "coming soon" page — hit the button to make it live.

    """ end # -- Celebration -- defp celebration(assigns) do ~H"""
    <.icon name="hero-check-badge" class="size-12 mx-auto text-green-600 mb-3" />

    Your shop is live!

    Customers can now browse and buy from your shop.

    <.link navigate={~p"/"} class="inline-flex items-center justify-center gap-1.5 rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white hover:bg-green-500" > <.icon name="hero-arrow-top-right-on-square-mini" class="size-4" /> View your shop <.link navigate={~p"/admin/theme"} class="inline-flex items-center justify-center gap-1.5 rounded-md bg-base-100 px-3 py-2 text-sm font-medium text-base-content ring-1 ring-base-300 ring-inset hover:bg-base-200/50" > <.icon name="hero-paint-brush-mini" class="size-4" /> Customise theme
    """ end # ========================================================================== # Stats components # ========================================================================== 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-base-200 p-4 hover:border-base-300 transition-colors" >
    <.icon name={@icon} class="size-5 text-base-content/60" />

    {@value}

    {@label}

    """ end defp fulfilment_pill(assigns) do {color, label} = case assigns.status do "unfulfilled" -> {"bg-base-200 text-base-content/60", "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-base-200 text-base-content/60", assigns.status || "—"} end assigns = assign(assigns, color: color, label: label) ~H""" {@label} """ end # ========================================================================== # Helpers # ========================================================================== 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 defp encrypt_api_key(api_key) do case SimpleshopTheme.Vault.encrypt(api_key) do {:ok, encrypted} -> encrypted _ -> nil end end defp maybe_add_shop_config(params, {:ok, %{shop_id: shop_id}}) do config = Map.get(params, "config", %{}) |> Map.put("shop_id", to_string(shop_id)) Map.put(params, "config", config) end defp maybe_add_shop_config(params, _), do: params defp maybe_add_name(params, {:ok, %{shop_name: shop_name}}) when is_binary(shop_name) do Map.put_new(params, "name", shop_name) end defp maybe_add_name(params, _), do: Map.put_new(params, "name", "Printify") defp format_printify_error(:no_api_key), do: "Please enter your API token" defp format_printify_error(:unauthorized), do: "That token doesn't seem to be valid" defp format_printify_error(:timeout), do: "Couldn't reach Printify — try again" defp format_printify_error({:http_error, _code}), do: "Something went wrong — try again" defp format_printify_error(error) when is_binary(error), do: error defp format_printify_error(_), do: "Connection failed — check your token and try again" end