feat(vereinfacht): member form flash and show page

- Form: show Vereinfacht sync warning after save via SyncFlash
- Show: load API debug response; MembershipFees: contact ID, link, no-contact warning
This commit is contained in:
Moritz 2026-02-18 22:28:55 +01:00
parent 81bcd2bc4d
commit d0fa3991f7
Signed by: moritz
GPG key ID: 1020A035E5DD0824
3 changed files with 117 additions and 2 deletions

View file

@ -319,11 +319,40 @@ defmodule MvWeb.MemberLive.Form do
socket =
socket
|> put_flash(:info, flash_message)
|> maybe_put_vereinfacht_sync_flash(member.id)
|> push_navigate(to: return_path(socket.assigns.return_to, member))
{:noreply, socket}
end
defp maybe_put_vereinfacht_sync_flash(socket, member_id) do
case Mv.Vereinfacht.SyncFlash.take(to_string(member_id)) do
{:warning, message} ->
put_flash(socket, :warning, translate_vereinfacht_flash(message))
{:ok, _message} ->
# Optionally show sync success; for now we keep only the main success message
socket
nil ->
socket
end
end
defp translate_vereinfacht_flash(message) when is_binary(message) do
prefix = "Vereinfacht: "
if String.starts_with?(message, prefix) do
detail = message |> String.trim_leading(prefix) |> String.trim()
Gettext.dgettext(MvWeb.Gettext, "default", "Vereinfacht: %{detail}",
detail: Gettext.dgettext(MvWeb.Gettext, "default", detail)
)
else
Gettext.dgettext(MvWeb.Gettext, "default", message)
end
end
defp handle_save_error(socket, form) do
# Always show a flash message when save fails
# Field-level validation errors are displayed in form fields, but flash provides additional feedback

View file

@ -256,6 +256,7 @@ defmodule MvWeb.MemberLive.Show do
id={"membership-fees-#{@member.id}"}
member={@member}
current_user={@current_user}
vereinfacht_debug_response={@vereinfacht_debug_response}
/>
<% end %>
</Layouts.app>
@ -264,7 +265,10 @@ defmodule MvWeb.MemberLive.Show do
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :active_tab, :contact)}
{:ok,
socket
|> assign(:active_tab, :contact)
|> assign(:vereinfacht_debug_response, nil)}
end
@impl true
@ -316,6 +320,16 @@ defmodule MvWeb.MemberLive.Show do
{:noreply, assign(socket, :active_tab, :membership_fees)}
end
def handle_event("load_vereinfacht_debug", %{"contact_id" => contact_id}, socket) do
response =
case Mv.Vereinfacht.Client.get_contact(contact_id) do
{:ok, body} -> {:ok, body}
{:error, reason} -> {:error, reason}
end
{:noreply, assign(socket, :vereinfacht_debug_response, response)}
end
# Flash set in LiveComponent is not shown in parent layout; child sends this to display flash
@impl true
def handle_info({:put_flash, type, message}, socket) do

View file

@ -50,6 +50,60 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
<% end %>
</div>
<%!-- Vereinfacht: contact info when synced, or warning when API is configured but no contact --%>
<%= if Mv.Config.vereinfacht_configured?() do %>
<%= if @member.vereinfacht_contact_id do %>
<div class="mb-4">
<label class="label">
<span class="label-text font-semibold">{gettext("Vereinfacht")}</span>
</label>
<div class="flex flex-col gap-1">
<span class="font-mono text-sm">
{gettext("Contact ID: %{id}", id: @member.vereinfacht_contact_id)}
</span>
<.link
:if={Mv.Config.vereinfacht_contact_view_url(@member.vereinfacht_contact_id)}
href={Mv.Config.vereinfacht_contact_view_url(@member.vereinfacht_contact_id)}
target="_blank"
rel="noopener noreferrer"
class="link link-primary inline-flex items-center gap-1"
>
{gettext("View contact in Vereinfacht")}
<.icon name="hero-arrow-top-right-on-square" class="inline-block size-4" />
</.link>
<div class="mt-2">
<span class="text-base-content/70 text-sm">{gettext("Debug:")}</span>
<button
type="button"
phx-click="load_vereinfacht_debug"
phx-value-contact_id={@member.vereinfacht_contact_id}
class="btn btn-sm btn-ghost ml-1"
>
{gettext("Load API response")}
</button>
</div>
<%= if @vereinfacht_debug_response do %>
<div class="mt-2 rounded border border-base-300 bg-base-200 p-3 overflow-x-auto max-h-96 overflow-y-auto">
<pre class="text-xs whitespace-pre-wrap font-mono"><%= format_vereinfacht_debug_response(@vereinfacht_debug_response) %></pre>
</div>
<% end %>
</div>
</div>
<% else %>
<div class="mb-4 rounded-lg border border-warning/50 bg-warning/10 p-3">
<p class="text-warning font-medium flex items-center gap-2">
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
{gettext("No Vereinfacht contact exists for this member.")}
</p>
<p class="text-sm text-base-content/70 mt-1">
{gettext(
"Sync this member from Settings (Vereinfacht section) or save the member again to create the contact."
)}
</p>
</div>
<% end %>
<% end %>
<%!-- Action Buttons (only when user has permission) --%>
<div class="flex gap-2 mb-4">
<.button
@ -439,7 +493,8 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|> assign_new(:creating_cycle, fn -> false end)
|> assign_new(:create_cycle_date, fn -> nil end)
|> assign_new(:create_cycle_error, fn -> nil end)
|> assign_new(:regenerating, fn -> false end)}
|> assign_new(:regenerating, fn -> false end)
|> assign_new(:vereinfacht_debug_response, fn -> nil end)}
end
@impl true
@ -997,6 +1052,23 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
defp format_create_cycle_period(_date, _interval), do: ""
defp format_vereinfacht_debug_response({:ok, body}) when is_map(body) do
Jason.encode!(body, pretty: true)
end
defp format_vereinfacht_debug_response({:error, {:http, status, detail}})
when is_binary(detail) do
"Error: HTTP #{status} #{detail}"
end
defp format_vereinfacht_debug_response({:error, {:http, status, _}}) do
"Error: HTTP #{status}"
end
defp format_vereinfacht_debug_response({:error, reason}) do
"Error: " <> inspect(reason)
end
# Helper component for section box
attr :title, :string, required: true
slot :inner_block, required: true