lib/pgflow_dashboard/components/progress_bar.ex

defmodule PgFlowDashboard.Components.ProgressBar do
  @moduledoc """
  Progress bar component for run progress visualization.
  """

  use Phoenix.Component

  @doc """
  Renders a progress bar.

  ## Attributes

    * `:progress` - Progress percentage (0-100)
    * `:completed` - Number of completed steps
    * `:total` - Total number of steps
    * `:failed` - Number of failed steps (optional)
    * `:size` - Bar size (:sm, :md, :lg). Default: :md

  """
  attr(:progress, :any, default: 0)
  attr(:completed, :integer, default: 0)
  attr(:total, :integer, default: 0)
  attr(:failed, :integer, default: 0)
  attr(:size, :atom, default: :md)

  def progress_bar(assigns) do
    progress = parse_progress(assigns.progress)
    assigns = assign(assigns, :progress_pct, progress)
    assigns = assign(assigns, :height_class, height_class(assigns.size))

    ~H"""
    <div class="w-full">
      <div class={["w-full bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden", @height_class]}>
        <div class="h-full flex">
          <div
            class="h-full bg-emerald-500 dark:bg-emerald-400 transition-all duration-300"
            style={"width: #{success_width(@completed, @failed, @total)}%"}
          />
          <div
            :if={@failed > 0}
            class="h-full bg-rose-500 dark:bg-rose-400"
            style={"width: #{failed_width(@failed, @total)}%"}
          />
        </div>
      </div>
      <div :if={@total > 0} class="mt-1 flex items-center justify-between text-xs text-slate-500 dark:text-slate-400">
        <span>{@completed}/{@total} steps</span>
        <span>{@progress_pct}%</span>
      </div>
    </div>
    """
  end

  defp parse_progress(progress) when is_number(progress) do
    progress
    |> Decimal.new()
    |> Decimal.round(1)
    |> Decimal.to_string()
  rescue
    _ -> "0"
  end

  defp parse_progress(%Decimal{} = progress) do
    progress
    |> Decimal.round(1)
    |> Decimal.to_string()
  end

  defp parse_progress(_), do: "0"

  defp success_width(completed, failed, total) when total > 0 do
    ((completed - failed) / total * 100)
    |> max(0.0)
    |> Float.round(1)
  end

  defp success_width(_, _, _), do: 0.0

  defp failed_width(failed, total) when total > 0 do
    (failed / total * 100)
    |> Float.round(1)
  end

  defp failed_width(_, _), do: 0.0

  defp height_class(:sm), do: "h-1"
  defp height_class(:md), do: "h-2"
  defp height_class(:lg), do: "h-3"
end