lib/luminous/panel/table.ex

defmodule Luminous.Panel.Table do
  alias Luminous.Attributes

  use Luminous.Panel

  @impl true
  def data_attributes(),
    do: [
      title: [type: :string, default: ""],
      halign: [type: {:in, [:left, :right, :center]}, default: :left],
      table_totals: [type: {:in, [:avg, :sum, nil]}, default: nil],
      number_formatting: [
        type: :keyword_list,
        keys: [
          thousand_separator: [type: {:or, [:string, :boolean]}, default: false],
          decimal_separator: [type: {:or, [:string, :boolean]}, default: false],
          precision: [type: {:or, [:non_neg_integer, :boolean]}, default: false]
        ]
      ]
    ]

  @impl true
  def panel_attributes(),
    do: [
      hook: [type: :string, default: "TableHook"]
    ]

  @impl true
  def transform(rows, panel) do
    col_defs =
      rows
      |> extract_labels()
      |> Enum.map(fn label ->
        attrs =
          Map.get(panel.data_attributes, label) ||
            Map.get(panel.data_attributes, to_string(label)) ||
            Attributes.parse!([], data_attributes() ++ Attributes.Schema.data())

        {label, attrs}
      end)
      |> Enum.sort_by(fn {_, attrs} -> attrs.order end)
      |> Enum.map(fn {label, attrs} ->
        %{
          field: label,
          title: attrs.title,
          hozAlign: attrs.halign,
          headerHozAlign: attrs.halign
        }
        |> add_table_totals_option(attrs)
        |> add_number_formatting_option(attrs)
      end)

    %{rows: rows, columns: col_defs}
  end

  @impl true
  def reduce(datasets, _panel, _dashboard) do
    columns = Enum.flat_map(datasets, &Map.get(&1, :columns))

    datasets =
      datasets
      |> Enum.map(&Map.get(&1, :rows))
      |> Enum.zip()
      |> Enum.map(&Tuple.to_list/1)
      |> Enum.map(fn maps ->
        Enum.reduce(maps, %{}, &Map.merge(&2, &1))
      end)

    %{rows: datasets, columns: columns}
  end

  @impl true
  def actions(), do: [%{label: "Download CSV", event: "download:csv"}]

  @impl true
  def render(assigns) do
    ~H"""
    <div class="w-full z-0">
      <div id={"#{Luminous.Utils.dom_id(@panel)}"} phx-hook={@panel.hook} phx-update="ignore" />
    </div>
    """
  end

  defp extract_labels(rows) when is_list(rows) do
    rows
    |> Enum.flat_map(&Map.keys/1)
    |> Enum.uniq()
  end

  defp add_table_totals_option(col_params, attr) do
    if is_nil(attr.table_totals),
      do: col_params,
      else: Map.put(col_params, :bottomCalc, attr.table_totals)
  end

  defp add_number_formatting_option(col_params, %{number_formatting: nf}) do
    formatterParams = %{
      thousand: Keyword.get(nf, :thousand_separator),
      decimal: Keyword.get(nf, :decimal_separator),
      precision: Keyword.get(nf, :precision)
    }

    col_params
    |> Map.put(:formatter, "money")
    |> Map.put(:formatterParams, formatterParams)
    |> Map.put(:bottomCalcFormatter, "money")
    |> Map.put(:bottomCalcFormatterParams, formatterParams)
  end

  defp add_number_formatting_option(col_params, _), do: col_params
end