defmodule PhoenixKit.Modules.Sync.Web.History do @moduledoc """ LiveView for DB Sync transfer history. Displays all data transfers with filtering and approval workflow support. """ use PhoenixKitWeb, :live_view use Gettext, backend: PhoenixKitWeb.Gettext alias PhoenixKit.Modules.Sync alias PhoenixKit.Modules.Sync.Transfers alias PhoenixKit.Settings alias PhoenixKit.Utils.Routes @per_page 20 @impl true def mount(params, _session, socket) do locale = params["locale"] || "en" project_title = Settings.get_setting("project_title", "PhoenixKit") config = Sync.get_config() socket = socket |> assign(:page_title, "Transfer History") |> assign(:project_title, project_title) |> assign(:current_locale, locale) |> assign(:current_path, Routes.path("/admin/sync/history", locale: locale)) |> assign(:config, config) |> assign(:page, 1) |> assign(:direction_filter, nil) |> assign(:status_filter, nil) |> assign(:show_approval_modal, false) |> assign(:selected_transfer, nil) |> load_transfers() {:ok, socket} end @impl true def handle_params(params, _url, socket) do page = String.to_integer(params["page"] || "1") direction_filter = params["direction"] status_filter = params["status"] socket = socket |> assign(:page, page) |> assign(:direction_filter, direction_filter) |> assign(:status_filter, status_filter) |> load_transfers() {:noreply, socket} end defp load_transfers(socket) do page = socket.assigns.page direction = socket.assigns.direction_filter status = socket.assigns.status_filter opts = [ limit: @per_page, offset: (page - 1) * @per_page, preload: [:connection] ] opts = if direction, do: Keyword.put(opts, :direction, direction), else: opts opts = if status, do: Keyword.put(opts, :status, status), else: opts transfers = Transfers.list_transfers(opts) total_count = Transfers.count_transfers(Keyword.take(opts, [:direction, :status])) total_pages = max(1, ceil(total_count / @per_page)) pending_count = Transfers.count_transfers(status: "pending_approval") socket |> assign(:transfers, transfers) |> assign(:total_count, total_count) |> assign(:total_pages, total_pages) |> assign(:pending_approval_count, pending_count) end @impl true def handle_event("filter", %{"direction" => direction, "status" => status}, socket) do query_params = %{} query_params = if direction != "", do: Map.put(query_params, "direction", direction), else: query_params query_params = if status != "", do: Map.put(query_params, "status", status), else: query_params base_path = Routes.path("/admin/sync/history") path = if map_size(query_params) > 0 do base_path <> "?" <> URI.encode_query(query_params) else base_path end {:noreply, push_patch(socket, to: path)} end def handle_event("clear_filters", _params, socket) do path = Routes.path("/admin/sync/history") {:noreply, push_patch(socket, to: path)} end def handle_event("show_approval_modal", %{"id" => id}, socket) do transfer = Transfers.get_transfer_with_preloads(String.to_integer(id), preload: [:connection]) socket = socket |> assign(:show_approval_modal, true) |> assign(:selected_transfer, transfer) {:noreply, socket} end def handle_event("close_approval_modal", _params, socket) do socket = socket |> assign(:show_approval_modal, false) |> assign(:selected_transfer, nil) {:noreply, socket} end def handle_event("approve_transfer", %{"id" => id}, socket) do transfer = Transfers.get_transfer!(String.to_integer(id)) current_user = socket.assigns.phoenix_kit_current_scope.user case Transfers.approve_transfer(transfer, current_user.id) do {:ok, _transfer} -> socket = socket |> put_flash(:info, "Transfer approved successfully") |> assign(:show_approval_modal, false) |> assign(:selected_transfer, nil) |> load_transfers() {:noreply, socket} {:error, _changeset} -> {:noreply, put_flash(socket, :error, "Failed to approve transfer")} end end def handle_event("deny_transfer", %{"transfer_id" => id, "reason" => reason}, socket) do transfer = Transfers.get_transfer!(String.to_integer(id)) current_user = socket.assigns.phoenix_kit_current_scope.user reason = if reason == "", do: nil, else: reason case Transfers.deny_transfer(transfer, current_user.id, reason) do {:ok, _transfer} -> socket = socket |> put_flash(:info, "Transfer denied") |> assign(:show_approval_modal, false) |> assign(:selected_transfer, nil) |> load_transfers() {:noreply, socket} {:error, _changeset} -> {:noreply, put_flash(socket, :error, "Failed to deny transfer")} end end def handle_event("page", %{"page" => page}, socket) do query_params = %{"page" => page} query_params = if socket.assigns.direction_filter, do: Map.put(query_params, "direction", socket.assigns.direction_filter), else: query_params query_params = if socket.assigns.status_filter, do: Map.put(query_params, "status", socket.assigns.status_filter), else: query_params base_path = Routes.path("/admin/sync/history") path = base_path <> "?" <> URI.encode_query(query_params) {:noreply, push_patch(socket, to: path)} end @impl true def render(assigns) do ~H"""
<%!-- Header Section --%>
<.link navigate={Routes.path("/admin/sync", locale: @current_locale)} class="btn btn-outline btn-primary btn-sm absolute left-0 top-0 -mb-12" > <.icon name="hero-arrow-left" class="w-4 h-4 mr-2" /> Back to DB Sync

Transfer History

View and manage data transfers

<%= if not @config.enabled do %>
<.icon name="hero-exclamation-triangle" class="w-5 h-5" /> DB Sync module is disabled.
<% end %> <%!-- Pending Approvals Alert --%> <%= if @pending_approval_count > 0 do %>
<.icon name="hero-clock" class="w-5 h-5" /> {@pending_approval_count} transfer(s) pending approval
<% end %> <%!-- Filters --%>
<%= if @direction_filter || @status_filter do %> <% end %>
{@total_count} transfer(s) found
<%!-- Transfers Table --%>
<%= if Enum.empty?(@transfers) do %> <% else %> <%= for transfer <- @transfers do %> <% end %> <% end %>
Direction Table Records Status Connection Date Actions
No transfers found
<.direction_badge direction={transfer.direction} /> {transfer.table_name} <.record_counts transfer={transfer} /> <.status_badge status={transfer.status} /> <%= if transfer.connection do %> {transfer.connection.name} <% else %> Session <% end %> <.time_ago datetime={transfer.inserted_at} /> <%= if transfer.status == "pending_approval" do %> <% else %> <% end %>
<%!-- Pagination --%> <%= if @total_pages > 1 do %>
<% end %>
<%!-- Approval/Details Modal --%> <%= if @show_approval_modal && @selected_transfer do %> <.transfer_modal transfer={@selected_transfer} /> <% end %>
""" end # =========================================== # COMPONENTS # =========================================== defp direction_badge(assigns) do ~H""" <%= if @direction == "send" do %> <.icon name="hero-arrow-up-tray" class="w-3 h-3 mr-1" /> Sent <% else %> <.icon name="hero-arrow-down-tray" class="w-3 h-3 mr-1" /> Received <% end %> """ end defp status_badge(assigns) do color = case assigns.status do "pending" -> "badge-ghost" "pending_approval" -> "badge-warning" "approved" -> "badge-info" "denied" -> "badge-error" "in_progress" -> "badge-info" "completed" -> "badge-success" "failed" -> "badge-error" "cancelled" -> "badge-ghost" "expired" -> "badge-ghost" _ -> "badge-ghost" end assigns = assign(assigns, :color, color) ~H""" {String.replace(@status, "_", " ") |> String.capitalize()} """ end defp record_counts(assigns) do ~H"""
<%= if @transfer.records_transferred > 0 do %> {@transfer.records_transferred} transferred <%= if @transfer.records_created > 0 do %> +{@transfer.records_created} <% end %> <%= if @transfer.records_skipped > 0 do %> ~{@transfer.records_skipped} <% end %> <%= if @transfer.records_failed > 0 do %> ×{@transfer.records_failed} <% end %> <% else %> {@transfer.records_requested || 0} requested <% end %>
""" end defp transfer_modal(assigns) do ~H""" """ end defp format_bytes(bytes) when is_nil(bytes) or bytes == 0, do: "0 B" defp format_bytes(bytes) do cond do bytes >= 1_073_741_824 -> "#{Float.round(bytes / 1_073_741_824, 1)} GB" bytes >= 1_048_576 -> "#{Float.round(bytes / 1_048_576, 1)} MB" bytes >= 1024 -> "#{Float.round(bytes / 1024, 1)} KB" true -> "#{bytes} B" end end end