berrypod/lib/berrypod_web/live/admin/product_show.ex

334 lines
11 KiB
Elixir
Raw Permalink Normal View History

defmodule BerrypodWeb.Admin.ProductShow do
use BerrypodWeb, :live_view
alias Berrypod.Products
alias Berrypod.Products.{Product, ProductImage, ProductVariant}
alias Berrypod.Cart
@impl true
def mount(%{"id" => id}, _session, socket) do
case Products.get_product_with_preloads(id) do
nil ->
socket =
socket
|> put_flash(:error, "Product not found")
|> push_navigate(to: ~p"/admin/products")
{:ok, socket}
product ->
form =
product
|> Product.storefront_changeset(%{})
|> to_form(as: "product")
socket =
socket
|> assign(:page_title, product.title)
|> assign(:product, product)
|> assign(:form, form)
|> assign(:save_status, :idle)
{:ok, socket}
end
end
@impl true
def handle_event("validate_storefront", %{"product" => params}, socket) do
form =
socket.assigns.product
|> Product.storefront_changeset(params)
|> Map.put(:action, :validate)
|> to_form(as: "product")
{:noreply, assign(socket, form: form, save_status: :idle)}
end
@impl true
def handle_event("save_storefront", %{"product" => params}, socket) do
case Products.update_storefront(socket.assigns.product, params) do
{:ok, updated} ->
product = %{
updated
| provider_connection: socket.assigns.product.provider_connection,
images: socket.assigns.product.images,
variants: socket.assigns.product.variants
}
form =
product
|> Product.storefront_changeset(%{})
|> to_form(as: "product")
socket =
socket
|> assign(:product, product)
|> assign(:form, form)
|> assign(:save_status, :saved)
{:noreply, socket}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset, as: "product"), save_status: :error)}
end
end
@impl true
def handle_event("resync", _params, socket) do
product = socket.assigns.product
if product.provider_connection do
Products.enqueue_sync(product.provider_connection)
{:noreply, put_flash(socket, :info, "Sync queued for #{product.provider_connection.name}")}
else
{:noreply, put_flash(socket, :error, "No provider connection")}
end
end
@impl true
def render(assigns) do
~H"""
<.header>
<.link navigate={~p"/admin/products"} class="admin-back-link">
&larr; Products
</.link>
<div class="admin-product-header">
<span class="admin-product-title">{@product.title}</span>
<.visibility_badge visible={@product.visible} />
<.status_badge status={@product.status} />
</div>
<:actions>
<.external_link
:if={provider_edit_url(@product)}
href={provider_edit_url(@product)}
icon={false}
class="admin-btn admin-btn-ghost admin-btn-sm"
>
Edit on {provider_label(@product)}
<.icon name="hero-arrow-top-right-on-square-mini" class="size-4" />
</.external_link>
<.link
navigate={~p"/products/#{@product.slug}"}
class="admin-btn admin-btn-ghost admin-btn-sm"
>
View on shop <.icon name="hero-arrow-top-right-on-square-mini" class="size-4" />
</.link>
</:actions>
</.header>
<%!-- images + details --%>
<div class="admin-product-grid">
<div>
<div class="admin-product-image-grid">
<div
:for={image <- sorted_images(@product)}
class="admin-product-image-tile"
>
<img
src={ProductImage.url(image, 400)}
alt={image.alt || @product.title}
loading="lazy"
/>
</div>
</div>
<p
:if={@product.images == []}
class="admin-help-text"
>
No images
</p>
</div>
<div class="admin-card">
<div class="admin-card-body">
<h3 class="admin-card-title">Details</h3>
<.list>
<:item :if={@product.provider_connection} title="Provider">
{provider_label(@product)} via {@product.provider_connection.name}
</:item>
<:item title="Category">{@product.category || ""}</:item>
<:item title="Price">{Cart.format_price(@product.cheapest_price)}</:item>
<:item title="Variants">{length(@product.variants)}</:item>
<:item title="Images">{length(@product.images)}</:item>
<:item title="Created">{format_date(@product.inserted_at)}</:item>
<:item
:if={@product.provider_connection && @product.provider_connection.last_synced_at}
title="Last synced"
>
{format_date(@product.provider_connection.last_synced_at)}
</:item>
</.list>
</div>
</div>
</div>
<%!-- storefront controls --%>
<div class="admin-card admin-card-spaced">
<div class="admin-card-body">
<h3 class="admin-card-title">Storefront controls</h3>
<.form
for={@form}
phx-submit="save_storefront"
phx-change="validate_storefront"
class="admin-filter-row-end"
>
<label class="admin-filter-select">
<span class="admin-filter-label">
Visibility
</span>
<select
name="product[visible]"
class="admin-select admin-select-sm"
aria-label="Visibility"
>
<option
value="true"
selected={@form[:visible].value == true || @form[:visible].value == "true"}
>
Visible
</option>
<option
value="false"
selected={@form[:visible].value == false || @form[:visible].value == "false"}
>
Hidden
</option>
</select>
</label>
<label class="admin-filter-select-wide">
<span class="admin-filter-label">Category</span>
<input
type="text"
name="product[category]"
value={@form[:category].value}
class="admin-input admin-input-sm"
placeholder="e.g. Apparel"
/>
</label>
<.button type="submit" variant="primary" class="admin-btn-sm">Save</.button>
<.inline_feedback status={@save_status} />
</.form>
</div>
</div>
<%!-- variants --%>
<div class="admin-card admin-card-spaced">
<div class="admin-card-body">
<h3 class="admin-card-title">Variants ({length(@product.variants)})</h3>
<.table id="variants" rows={@product.variants}>
<:col :let={variant} label="Options">{ProductVariant.options_title(variant)}</:col>
<:col :let={variant} label="SKU">{variant.sku || ""}</:col>
<:col :let={variant} label="Price">{Cart.format_price(variant.price)}</:col>
<:col :let={variant} label="Cost">
{if variant.cost, do: Cart.format_price(variant.cost), else: ""}
</:col>
<:col :let={variant} label="Profit">
{if ProductVariant.profit(variant),
do: Cart.format_price(ProductVariant.profit(variant)),
else: ""}
</:col>
<:col :let={variant} label="Available">
<span
:if={variant.is_enabled && variant.is_available}
class="admin-icon-positive"
>
<.icon name="hero-check-circle-mini" class="size-5" />
</span>
<span
:if={!variant.is_enabled || !variant.is_available}
class="admin-icon-muted"
>
<.icon name="hero-x-circle-mini" class="size-5" />
</span>
</:col>
</.table>
</div>
</div>
<%!-- provider data --%>
<div :if={@product.provider_connection} class="admin-card admin-card-spaced">
<div class="admin-card-body">
<h3 class="admin-card-title">Provider data</h3>
<.list>
<:item title="Provider">
{provider_label(@product)} via {@product.provider_connection.name}
</:item>
<:item title="Provider product ID">{@product.provider_product_id}</:item>
<:item title="Status">{@product.status}</:item>
<:item title="Sync status">{@product.provider_connection.sync_status}</:item>
</.list>
<div class="admin-section-body">
<button phx-click="resync" class="admin-btn admin-btn-outline admin-btn-sm">
<.icon name="hero-arrow-path" class="size-4" /> Re-sync
</button>
</div>
</div>
</div>
"""
end
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
defp sorted_images(product) do
(product.images || []) |> Enum.sort_by(& &1.position)
end
defp visibility_badge(assigns) do
{color, label} =
if assigns.visible do
{"green", "visible"}
else
{"zinc", "hidden"}
end
assigns = assign(assigns, color: color, label: label)
~H"""
<span class={["admin-status-pill", "admin-status-pill-#{@color}"]}>{@label}</span>
"""
end
defp status_badge(assigns) do
color =
case assigns.status do
"active" -> "green"
"draft" -> "amber"
_ -> "zinc"
end
assigns = assign(assigns, :color, color)
~H"""
<span class={["admin-status-pill", "admin-status-pill-#{@color}"]}>{@status}</span>
"""
end
defp provider_label(%{provider_connection: %{provider_type: "printify"}}), do: "Printify"
defp provider_label(%{provider_connection: %{provider_type: "printful"}}), do: "Printful"
defp provider_label(%{provider_connection: %{provider_type: type}}), do: String.capitalize(type)
defp provider_label(_), do: nil
defp provider_edit_url(%{
provider_connection: %{provider_type: "printify", config: config},
provider_product_id: pid
}) do
shop_id = config["shop_id"]
if shop_id && pid, do: "https://printify.com/app/editor/#{shop_id}/#{pid}"
end
defp provider_edit_url(%{
provider_connection: %{provider_type: "printful"},
provider_product_id: pid
}) do
if pid, do: "https://www.printful.com/dashboard/sync/update?id=#{pid}"
end
defp provider_edit_url(_), do: nil
defp format_date(nil), do: ""
defp format_date(datetime), do: Calendar.strftime(datetime, "%d %b %Y %H:%M")
end