feat(vereinfacht): Settings UI and bulk sync
- GlobalSettingsLive: Vereinfacht section, sync button, last sync result - Test: Vereinfacht Integration section visible
This commit is contained in:
parent
9808dba007
commit
81bcd2bc4d
2 changed files with 187 additions and 0 deletions
|
|
@ -23,6 +23,9 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
|
require Ash.Query
|
||||||
|
import Ash.Expr
|
||||||
|
|
||||||
alias Mv.Membership
|
alias Mv.Membership
|
||||||
|
|
||||||
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
|
@ -41,6 +44,8 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
|> assign(:settings, settings)
|
|> assign(:settings, settings)
|
||||||
|> assign(:active_editing_section, nil)
|
|> assign(:active_editing_section, nil)
|
||||||
|> assign(:locale, locale)
|
|> assign(:locale, locale)
|
||||||
|
|> assign(:vereinfacht_env_configured, Mv.Config.vereinfacht_env_configured?())
|
||||||
|
|> assign(:last_vereinfacht_sync_result, nil)
|
||||||
|> assign_form()
|
|> assign_form()
|
||||||
|
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
|
|
@ -74,6 +79,70 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
</.button>
|
</.button>
|
||||||
</.form>
|
</.form>
|
||||||
</.form_section>
|
</.form_section>
|
||||||
|
<%!-- Vereinfacht Integration Section --%>
|
||||||
|
<.form_section title={gettext("Vereinfacht Integration")}>
|
||||||
|
<%= if @vereinfacht_env_configured do %>
|
||||||
|
<p class="text-sm text-base-content/70 mb-4">
|
||||||
|
{gettext(
|
||||||
|
"Configured via environment variables (VEREINFACHT_API_URL, VEREINFACHT_API_KEY, VEREINFACHT_CLUB_ID). Fields below are read-only."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<.form for={@form} id="vereinfacht-form" phx-change="validate" phx-submit="save">
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<.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")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<.button
|
||||||
|
:if={not @vereinfacht_env_configured}
|
||||||
|
phx-disable-with={gettext("Saving...")}
|
||||||
|
variant="primary"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
{gettext("Save Vereinfacht Settings")}
|
||||||
|
</.button>
|
||||||
|
<.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")}
|
||||||
|
</.button>
|
||||||
|
<%= if @last_vereinfacht_sync_result do %>
|
||||||
|
<.vereinfacht_sync_result result={@last_vereinfacht_sync_result} />
|
||||||
|
<% end %>
|
||||||
|
</.form>
|
||||||
|
</.form_section>
|
||||||
<%!-- Memberdata Section --%>
|
<%!-- Memberdata Section --%>
|
||||||
<.form_section title={gettext("Memberdata")}>
|
<.form_section title={gettext("Memberdata")}>
|
||||||
<.live_component
|
<.live_component
|
||||||
|
|
@ -100,6 +169,40 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, setting_params))}
|
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, setting_params))}
|
||||||
end
|
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
|
@impl true
|
||||||
def handle_event("save", %{"setting" => setting_params}, socket) do
|
def handle_event("save", %{"setting" => setting_params}, socket) do
|
||||||
actor = MvWeb.LiveHelpers.current_actor(socket)
|
actor = MvWeb.LiveHelpers.current_actor(socket)
|
||||||
|
|
@ -213,4 +316,74 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
|
|
||||||
assign(socket, form: to_form(form))
|
assign(socket, form: to_form(form))
|
||||||
end
|
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"""
|
||||||
|
<div class="mt-4 p-4 rounded-lg border border-base-300 bg-base-200 space-y-2">
|
||||||
|
<p class="font-medium">
|
||||||
|
{gettext("Last sync result:")}
|
||||||
|
<span class="text-success ml-1">{gettext("%{count} synced", count: @result.synced)}</span>
|
||||||
|
<%= if @result.errors != [] do %>
|
||||||
|
<span class="text-error ml-1">
|
||||||
|
{gettext("%{count} failed", count: length(@result.errors))}
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<%= if @result.errors != [] do %>
|
||||||
|
<p class="text-sm text-base-content/70 mt-2">{gettext("Failed members:")}</p>
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1 max-h-48 overflow-y-auto">
|
||||||
|
<%= for err <- @result.errors do %>
|
||||||
|
<li>
|
||||||
|
<span class="font-medium">{err.member_name}</span>: {translate_vereinfacht_message(err)}
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,18 @@ defmodule MvWeb.GlobalSettingsLiveConfigTest do
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue