feat: Datafields page, merge fee types into membership_fee_settings, sidebar

- Add /admin/datafields (DatafieldsLive) for member and custom field config
- Remove Memberdata block from GlobalSettingsLive
- Router: drop /membership_fee_types, add new_fee_type and edit_fee_type under membership_fee_settings
- MembershipFeeSettingsLive: fee types table, collapsible examples; Index links updated
- PagePaths: admin_datafields, admin_import; remove membership_fee_types
- Sidebar: order and labels (Basic settings, Datafields, Membership fee settings, Import, Users, Roles)
- Gettext: German translations for sidebar and OIDC
- Tests: datafields and fee routes, permission and form tests updated
This commit is contained in:
Moritz 2026-02-24 13:55:33 +01:00
parent 8edbbac95f
commit 62b37b9aa2
Signed by: moritz
GPG key ID: 1020A035E5DD0824
18 changed files with 886 additions and 251 deletions

View file

@ -0,0 +1,132 @@
defmodule MvWeb.DatafieldsLive do
@moduledoc """
LiveView for managing member field visibility/required and custom fields (datafields).
Renders MemberFieldLive.IndexComponent and CustomFieldLive.IndexComponent.
Moved from GlobalSettingsLive (Memberdata section) to a dedicated page.
"""
use MvWeb, :live_view
alias Mv.Membership
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
@impl true
def mount(_params, _session, socket) do
{:ok, settings} = Membership.get_settings()
{:ok,
socket
|> assign(:page_title, gettext("Datafields"))
|> assign(:settings, settings)
|> assign(:active_editing_section, nil)}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user} club_name={@settings.club_name}>
<.header>
{gettext("Datafields")}
<:subtitle>
{gettext("Configure member fields and custom data fields.")}
</:subtitle>
</.header>
<.form_section title={gettext("Member fields")}>
<.live_component
:if={@active_editing_section != :custom_fields}
module={MvWeb.MemberFieldLive.IndexComponent}
id="member-fields-component"
settings={@settings}
/>
</.form_section>
<.form_section title={gettext("Custom fields")}>
<.live_component
:if={@active_editing_section != :member_fields}
module={MvWeb.CustomFieldLive.IndexComponent}
id="custom-fields-component"
actor={@current_user}
/>
</.form_section>
</Layouts.app>
"""
end
@impl true
def handle_info({:custom_field_saved, _custom_field, action}, socket) do
send_update(MvWeb.CustomFieldLive.IndexComponent,
id: "custom-fields-component",
show_form: false
)
{:noreply,
socket
|> assign(:active_editing_section, nil)
|> put_flash(:info, gettext("Data field %{action} successfully", action: action))}
end
@impl true
def handle_info({:custom_field_deleted, _custom_field}, socket) do
{:noreply, put_flash(socket, :info, gettext("Data field deleted successfully"))}
end
@impl true
def handle_info({:custom_field_delete_error, error}, socket) do
{:noreply,
put_flash(
socket,
:error,
gettext("Failed to delete data field: %{error}", error: inspect(error))
)}
end
@impl true
def handle_info(:custom_field_slug_mismatch, socket) do
{:noreply, put_flash(socket, :error, gettext("Slug does not match. Deletion cancelled."))}
end
def handle_info({:custom_fields_load_error, _error}, socket) do
{:noreply,
put_flash(
socket,
:error,
gettext("Could not load data fields. Please check your permissions.")
)}
end
@impl true
def handle_info({:editing_section_changed, section}, socket) do
{:noreply, assign(socket, :active_editing_section, section)}
end
@impl true
def handle_info({:member_field_saved, _member_field, action}, socket) do
{:ok, updated_settings} = Membership.get_settings()
send_update(MvWeb.MemberFieldLive.IndexComponent,
id: "member-fields-component",
show_form: false,
settings: updated_settings
)
{:noreply,
socket
|> assign(:settings, updated_settings)
|> assign(:active_editing_section, nil)
|> put_flash(:info, gettext("Member field %{action} successfully", action: action))}
end
@impl true
def handle_info({:member_field_visibility_updated}, socket) do
{:ok, updated_settings} = Membership.get_settings()
send_update(MvWeb.MemberFieldLive.IndexComponent,
id: "member-fields-component",
settings: updated_settings
)
{:noreply, assign(socket, :settings, updated_settings)}
end
end