From 5343b78750bc7e37813c34857b1f83c623df0f97 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 18 Feb 2026 22:28:51 +0100 Subject: [PATCH] feat(vereinfacht): Settings UI and bulk sync - GlobalSettingsLive: Vereinfacht section, sync button, last sync result - Test: Vereinfacht Integration section visible --- lib/mv_web/live/global_settings_live.ex | 173 ++++++++++++++++++ .../live/global_settings_live_config_test.exs | 14 ++ 2 files changed, 187 insertions(+) diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index fafc955..fc91b03 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -23,6 +23,9 @@ defmodule MvWeb.GlobalSettingsLive do """ use MvWeb, :live_view + require Ash.Query + import Ash.Expr + alias Mv.Membership on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} @@ -41,6 +44,8 @@ defmodule MvWeb.GlobalSettingsLive do |> assign(:settings, settings) |> assign(:active_editing_section, nil) |> assign(:locale, locale) + |> assign(:vereinfacht_env_configured, Mv.Config.vereinfacht_env_configured?()) + |> assign(:last_vereinfacht_sync_result, nil) |> assign_form() {:ok, socket} @@ -74,6 +79,70 @@ defmodule MvWeb.GlobalSettingsLive do + <%!-- Vereinfacht Integration Section --%> + <.form_section title={gettext("Vereinfacht Integration")}> + <%= if @vereinfacht_env_configured do %> +

+ {gettext( + "Configured via environment variables (VEREINFACHT_API_URL, VEREINFACHT_API_KEY, VEREINFACHT_CLUB_ID). Fields below are read-only." + )} +

+ <% end %> + <.form for={@form} id="vereinfacht-form" phx-change="validate" phx-submit="save"> +
+ <.input + field={@form[:vereinfacht_api_url]} + type="text" + label={gettext("API URL")} + disabled={@vereinfacht_env_configured} + placeholder={ + if(@vereinfacht_env_configured, + do: gettext("From VEREINFACHT_API_URL"), + else: "https://api.verein.visuel.dev/api/v1" + ) + } + /> + <.input + field={@form[:vereinfacht_api_key]} + type="password" + label={gettext("API Key")} + disabled={@vereinfacht_env_configured} + placeholder={ + if(@vereinfacht_env_configured, do: gettext("From VEREINFACHT_API_KEY"), else: nil) + } + /> + <.input + field={@form[:vereinfacht_club_id]} + type="text" + label={gettext("Club ID")} + disabled={@vereinfacht_env_configured} + placeholder={ + if(@vereinfacht_env_configured, do: gettext("From VEREINFACHT_CLUB_ID"), else: "2") + } + /> +
+ <.button + :if={not @vereinfacht_env_configured} + phx-disable-with={gettext("Saving...")} + variant="primary" + class="mt-2" + > + {gettext("Save Vereinfacht Settings")} + + <.button + :if={Mv.Config.vereinfacht_configured?()} + type="button" + phx-click="sync_vereinfacht_contacts" + phx-disable-with={gettext("Syncing...")} + class="mt-4 btn-outline" + > + {gettext("Sync all members without Vereinfacht contact")} + + <%= if @last_vereinfacht_sync_result do %> + <.vereinfacht_sync_result result={@last_vereinfacht_sync_result} /> + <% end %> + + <%!-- Memberdata Section --%> <.form_section title={gettext("Memberdata")}> <.live_component @@ -100,6 +169,40 @@ defmodule MvWeb.GlobalSettingsLive do assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, setting_params))} end + @impl true + def handle_event("sync_vereinfacht_contacts", _params, socket) do + case Mv.Vereinfacht.sync_members_without_contact() do + {:ok, %{synced: synced, errors: errors}} -> + errors_with_names = enrich_sync_errors(errors) + result = %{synced: synced, errors: errors_with_names} + + socket = + socket + |> assign(:last_vereinfacht_sync_result, result) + |> put_flash( + :info, + if(errors_with_names == [], + do: gettext("Synced %{count} member(s) to Vereinfacht.", count: synced), + else: + gettext("Synced %{count} member(s). %{error_count} failed.", + count: synced, + error_count: length(errors_with_names) + ) + ) + ) + + {:noreply, socket} + + {:error, :not_configured} -> + {:noreply, + put_flash( + socket, + :error, + gettext("Vereinfacht is not configured. Set API URL, API Key, and Club ID.") + )} + end + end + @impl true def handle_event("save", %{"setting" => setting_params}, socket) do actor = MvWeb.LiveHelpers.current_actor(socket) @@ -213,4 +316,74 @@ defmodule MvWeb.GlobalSettingsLive do assign(socket, form: to_form(form)) end + + defp enrich_sync_errors([]), do: [] + + defp enrich_sync_errors(errors) when is_list(errors) do + name_by_id = fetch_member_names_by_ids(Enum.map(errors, fn {id, _} -> id end)) + + Enum.map(errors, fn {member_id, reason} -> + %{ + member_id: member_id, + member_name: Map.get(name_by_id, member_id) || to_string(member_id), + message: Mv.Vereinfacht.format_error(reason), + detail: extract_vereinfacht_detail(reason) + } + end) + end + + defp fetch_member_names_by_ids(ids) do + actor = Mv.Helpers.SystemActor.get_system_actor() + opts = Mv.Helpers.ash_actor_opts(actor) + query = Ash.Query.filter(Mv.Membership.Member, expr(id in ^ids)) + + case Ash.read(query, opts) do + {:ok, members} -> + Map.new(members, fn m -> {m.id, MvWeb.Helpers.MemberHelpers.display_name(m)} end) + + _ -> + %{} + end + end + + defp extract_vereinfacht_detail({:http, _status, detail}) when is_binary(detail), do: detail + defp extract_vereinfacht_detail(_), do: nil + + defp translate_vereinfacht_message(%{detail: detail}) when is_binary(detail) do + gettext("Vereinfacht: %{detail}", + detail: Gettext.dgettext(MvWeb.Gettext, "default", detail) + ) + end + + defp translate_vereinfacht_message(%{message: message}) do + Gettext.dgettext(MvWeb.Gettext, "default", message) + end + + attr :result, :map, required: true + + defp vereinfacht_sync_result(assigns) do + ~H""" +
+

+ {gettext("Last sync result:")} + {gettext("%{count} synced", count: @result.synced)} + <%= if @result.errors != [] do %> + + {gettext("%{count} failed", count: length(@result.errors))} + + <% end %> +

+ <%= if @result.errors != [] do %> +

{gettext("Failed members:")}

+ + <% end %> +
+ """ + end end diff --git a/test/mv_web/live/global_settings_live_config_test.exs b/test/mv_web/live/global_settings_live_config_test.exs index 9ac75fd..106a020 100644 --- a/test/mv_web/live/global_settings_live_config_test.exs +++ b/test/mv_web/live/global_settings_live_config_test.exs @@ -71,4 +71,18 @@ defmodule MvWeb.GlobalSettingsLiveConfigTest do end end end + + describe "Vereinfacht Integration section" do + setup %{conn: conn} do + admin_user = Mv.Fixtures.user_with_role_fixture("admin") + conn = MvWeb.ConnCase.conn_with_password_user(conn, admin_user) + {:ok, conn: conn} + end + + @tag :ui + test "settings page shows Vereinfacht Integration section", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/settings") + assert html =~ "Vereinfacht" + end + end end