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:")}
+
+ <%= for err <- @result.errors do %>
+ -
+ {err.member_name}: {translate_vereinfacht_message(err)}
+
+ <% end %>
+
+ <% 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