Refinex CSV import and PDf export closes #299 and #433 #446

Merged
carla merged 16 commits from feat/299_plz into main 2026-02-24 16:32:32 +01:00
45 changed files with 2187 additions and 425 deletions
Showing only changes of commit bfc078d5aa - Show all commits

View file

@ -31,6 +31,10 @@ ASSOCIATION_NAME="Sportsclub XYZ"
# OIDC_ADMIN_GROUP_NAME=admin # OIDC_ADMIN_GROUP_NAME=admin
# OIDC_GROUPS_CLAIM=groups # OIDC_GROUPS_CLAIM=groups
# Optional: Show only OIDC sign-in on login page (hide password form).
# When set to true and OIDC is configured, users see only the Single Sign-On button.
# OIDC_ONLY=true
# Optional: Vereinfacht accounting integration (finance-contacts sync) # Optional: Vereinfacht accounting integration (finance-contacts sync)
# If set, these override values from Settings UI; those fields become read-only. # If set, these override values from Settings UI; those fields become read-only.
# VEREINFACHT_API_URL=https://api.verein.visuel.dev/api/v1 # VEREINFACHT_API_URL=https://api.verein.visuel.dev/api/v1

View file

@ -369,4 +369,24 @@
left: 0 !important; left: 0 !important;
} }
/* Sign-in: hide SSO button and "or" divider when OIDC is not configured.
Scoped to #sign-in-page to avoid hiding unrelated elements. */
#sign-in-page[data-oidc-configured="false"] [id*="oidc"] {
display: none !important;
}
#sign-in-page[data-oidc-configured="false"] a[href*="oidc"] {
display: none !important;
}
#sign-in-page[data-oidc-configured="false"] .divider {
display: none !important;
}
/* Sign-in: when OIDC-only mode is on, hide password form and "or" divider (show only SSO). */
#sign-in-page[data-oidc-configured="true"][data-oidc-only="true"] [id*="password"] {
display: none !important;
}
#sign-in-page[data-oidc-configured="true"][data-oidc-only="true"] .divider {
display: none !important;
}
/* This file is for your main application CSS */ /* This file is for your main application CSS */

View file

@ -93,11 +93,13 @@ config :mv, :secret_key_base, "ryn7D6ssmIHQFWIks2sFiTGATgwwAR1+3bN8p7fy6qVtB8qnx
# Signing Secret for Authentication # Signing Secret for Authentication
config :mv, :token_signing_secret, "IwUwi65TrEeExwBXXFPGm2I7889NsL" config :mv, :token_signing_secret, "IwUwi65TrEeExwBXXFPGm2I7889NsL"
config :mv, :oidc, # OIDC: only use when ENV (or Settings) are set. When all OIDC ENVs are commented out,
client_id: "mv", # do not set defaults here so the SSO button stays hidden and no MissingSecret occurs.
base_url: "http://localhost:8080/auth/v1", # config :mv, :oidc,
client_secret: System.get_env("OIDC_CLIENT_SECRET"), # client_id: "mv",
redirect_uri: "http://localhost:4000/auth/user/oidc/callback" # base_url: "http://localhost:8080/auth/v1",
# client_secret: System.get_env("OIDC_CLIENT_SECRET"),
# redirect_uri: "http://localhost:4000/auth/user/oidc/callback"
# AshAuthentication development configuration # AshAuthentication development configuration
config :mv, :session_identifier, :jti config :mv, :session_identifier, :jti

View file

@ -49,6 +49,9 @@ config :mv, :session_identifier, :unsafe
config :mv, :require_token_presence_for_authentication, false config :mv, :require_token_presence_for_authentication, false
# Use English as default locale in tests so UI tests can assert on English strings.
config :mv, :default_locale, "en"
# Enable SQL Sandbox for async LiveView tests # Enable SQL Sandbox for async LiveView tests
# This flag controls sync vs async behavior in CycleGenerator after_action hooks # This flag controls sync vs async behavior in CycleGenerator after_action hooks
config :mv, :sql_sandbox, true config :mv, :sql_sandbox, true

View file

@ -33,6 +33,10 @@
- `OIDC_GROUPS_CLAIM` JWT claim name for group list (default "groups"). - `OIDC_GROUPS_CLAIM` JWT claim name for group list (default "groups").
- Module: Mv.OidcRoleSyncConfig (oidc_admin_group_name/0, oidc_groups_claim/0). - Module: Mv.OidcRoleSyncConfig (oidc_admin_group_name/0, oidc_groups_claim/0).
### Sign-in page (OIDC-only mode)
- `OIDC_ONLY` (or Settings → OIDC → "Only OIDC sign-in") When set to true/1/yes and OIDC is configured, the sign-in page shows only the Single Sign-On button (password login is hidden). ENV takes precedence over Settings.
### Sync Logic ### Sync Logic
- Mv.OidcRoleSync.apply_admin_role_from_user_info(user, user_info) If admin group configured, sets user role to Admin or Mitglied based on user_info groups. - Mv.OidcRoleSync.apply_admin_role_from_user_info(user, user_info) If admin group configured, sets user role to Admin or Mitglied based on user_info groups.

View file

@ -79,7 +79,14 @@ defmodule Mv.Membership.Setting do
:vereinfacht_api_url, :vereinfacht_api_url,
:vereinfacht_api_key, :vereinfacht_api_key,
:vereinfacht_club_id, :vereinfacht_club_id,
:vereinfacht_app_url :vereinfacht_app_url,
:oidc_client_id,
:oidc_base_url,
:oidc_redirect_uri,
:oidc_client_secret,
:oidc_admin_group_name,
:oidc_groups_claim,
:oidc_only
] ]
end end
@ -96,7 +103,14 @@ defmodule Mv.Membership.Setting do
:vereinfacht_api_url, :vereinfacht_api_url,
:vereinfacht_api_key, :vereinfacht_api_key,
:vereinfacht_club_id, :vereinfacht_club_id,
:vereinfacht_app_url :vereinfacht_app_url,
:oidc_client_id,
:oidc_base_url,
:oidc_redirect_uri,
:oidc_client_secret,
:oidc_admin_group_name,
:oidc_groups_claim,
:oidc_only
] ]
end end
@ -322,6 +336,52 @@ defmodule Mv.Membership.Setting do
description "Vereinfacht app base URL for contact view links (e.g. https://app.verein.visuel.dev)" description "Vereinfacht app base URL for contact view links (e.g. https://app.verein.visuel.dev)"
end end
# OIDC authentication (can be overridden by ENV)
attribute :oidc_client_id, :string do
allow_nil? true
public? true
description "OIDC client ID (e.g. from OIDC_CLIENT_ID)"
end
attribute :oidc_base_url, :string do
allow_nil? true
public? true
description "OIDC provider base URL (e.g. from OIDC_BASE_URL)"
end
attribute :oidc_redirect_uri, :string do
allow_nil? true
public? true
description "OIDC redirect URI for callback (e.g. from OIDC_REDIRECT_URI)"
end
attribute :oidc_client_secret, :string do
allow_nil? true
public? false
description "OIDC client secret (e.g. from OIDC_CLIENT_SECRET)"
sensitive? true
end
attribute :oidc_admin_group_name, :string do
allow_nil? true
public? true
description "OIDC group name that maps to Admin role (e.g. from OIDC_ADMIN_GROUP_NAME)"
end
attribute :oidc_groups_claim, :string do
allow_nil? true
public? true
description "JWT claim name for group list (e.g. from OIDC_GROUPS_CLAIM, default 'groups')"
end
attribute :oidc_only, :boolean do
allow_nil? false
default false
public? true
description "When true and OIDC is configured, sign-in shows only OIDC (password login hidden)"
end
timestamps() timestamps()
end end

View file

@ -262,13 +262,44 @@ defmodule Mv.Config do
end end
end end
defp env_or_setting_bool(env_key, setting_key) do
case System.get_env(env_key) do
nil ->
get_from_settings_bool(setting_key)
value when is_binary(value) ->
v = String.trim(value) |> String.downcase()
v in ["true", "1", "yes"]
_ ->
false
end
end
defp get_vereinfacht_from_settings(key) do defp get_vereinfacht_from_settings(key) do
get_from_settings(key)
end
defp get_from_settings(key) do
case Mv.Membership.get_settings() do case Mv.Membership.get_settings() do
{:ok, settings} -> settings |> Map.get(key) |> trim_nil() {:ok, settings} -> settings |> Map.get(key) |> trim_nil()
{:error, _} -> nil {:error, _} -> nil
end end
end end
defp get_from_settings_bool(key) do
case Mv.Membership.get_settings() do
{:ok, settings} ->
case Map.get(settings, key) do
true -> true
_ -> false
end
{:error, _} ->
false
end
end
defp trim_nil(nil), do: nil defp trim_nil(nil), do: nil
defp trim_nil(s) when is_binary(s) do defp trim_nil(s) when is_binary(s) do
@ -298,4 +329,105 @@ defmodule Mv.Config do
defp present?(nil), do: false defp present?(nil), do: false
defp present?(s) when is_binary(s), do: String.trim(s) != "" defp present?(s) when is_binary(s), do: String.trim(s) != ""
defp present?(_), do: false defp present?(_), do: false
# ---------------------------------------------------------------------------
# OIDC authentication
# ENV variables take priority; fallback to Settings from database.
# ---------------------------------------------------------------------------
@doc """
Returns the OIDC client ID. ENV first, then Settings.
"""
@spec oidc_client_id() :: String.t() | nil
def oidc_client_id do
env_or_setting("OIDC_CLIENT_ID", :oidc_client_id)
end
@doc """
Returns the OIDC provider base URL. ENV first, then Settings.
"""
@spec oidc_base_url() :: String.t() | nil
def oidc_base_url do
env_or_setting("OIDC_BASE_URL", :oidc_base_url)
end
@doc """
Returns the OIDC redirect URI. ENV first, then Settings.
"""
@spec oidc_redirect_uri() :: String.t() | nil
def oidc_redirect_uri do
env_or_setting("OIDC_REDIRECT_URI", :oidc_redirect_uri)
end
@doc """
Returns the OIDC client secret. ENV first, then Settings.
"""
@spec oidc_client_secret() :: String.t() | nil
def oidc_client_secret do
env_or_setting("OIDC_CLIENT_SECRET", :oidc_client_secret)
end
@doc """
Returns the OIDC admin group name (for role sync). ENV first, then Settings.
"""
@spec oidc_admin_group_name() :: String.t() | nil
def oidc_admin_group_name do
env_or_setting("OIDC_ADMIN_GROUP_NAME", :oidc_admin_group_name)
end
@doc """
Returns the OIDC groups claim name (default "groups"). ENV first, then Settings.
"""
@spec oidc_groups_claim() :: String.t() | nil
def oidc_groups_claim do
case env_or_setting("OIDC_GROUPS_CLAIM", :oidc_groups_claim) do
nil -> "groups"
v -> v
end
end
@doc """
Returns true if any OIDC ENV variable is set (used to show hint in Settings UI).
"""
@spec oidc_env_configured?() :: boolean()
def oidc_env_configured? do
oidc_client_id_env_set?() or oidc_base_url_env_set?() or
oidc_redirect_uri_env_set?() or oidc_client_secret_env_set?() or
oidc_admin_group_name_env_set?() or oidc_groups_claim_env_set?() or
oidc_only_env_set?()
end
@doc """
Returns true when OIDC is configured and can be used for sign-in (client ID, base URL,
redirect URI, and client secret must be set). Used to show or hide the Single Sign-On button on the
sign-in page. Without client secret, the OIDC flow fails with MissingSecret; without redirect_uri,
the OIDC Plug crashes with URI.new(nil).
"""
@spec oidc_configured?() :: boolean()
def oidc_configured? do
id = oidc_client_id()
base = oidc_base_url()
secret = oidc_client_secret()
redirect = oidc_redirect_uri()
present = &(is_binary(&1) and String.trim(&1) != "")
present.(id) and present.(base) and present.(secret) and present.(redirect)
end
@doc """
Returns true when only OIDC sign-in should be shown (password login hidden).
ENV OIDC_ONLY first (true/1/yes vs false/0/no), then Settings.oidc_only.
Only has effect when OIDC is configured; when false or OIDC not configured, both password and OIDC are shown as usual.
"""
@spec oidc_only?() :: boolean()
def oidc_only? do
env_or_setting_bool("OIDC_ONLY", :oidc_only)
end
def oidc_client_id_env_set?, do: env_set?("OIDC_CLIENT_ID")
def oidc_base_url_env_set?, do: env_set?("OIDC_BASE_URL")
def oidc_redirect_uri_env_set?, do: env_set?("OIDC_REDIRECT_URI")
def oidc_client_secret_env_set?, do: env_set?("OIDC_CLIENT_SECRET")
def oidc_admin_group_name_env_set?, do: env_set?("OIDC_ADMIN_GROUP_NAME")
def oidc_groups_claim_env_set?, do: env_set?("OIDC_GROUPS_CLAIM")
def oidc_only_env_set?, do: env_set?("OIDC_ONLY")
end end

View file

@ -2,23 +2,19 @@ defmodule Mv.OidcRoleSyncConfig do
@moduledoc """ @moduledoc """
Runtime configuration for OIDC group role sync (e.g. admin group Admin role). Runtime configuration for OIDC group role sync (e.g. admin group Admin role).
Reads from Application config `:mv, :oidc_role_sync`: Reads from Mv.Config (ENV first, then Settings):
- `:admin_group_name` OIDC group name that maps to Admin role (optional; when nil, no sync). - `oidc_admin_group_name/0` OIDC group name that maps to Admin role (optional; when nil, no sync).
- `:groups_claim` JWT/user_info claim name for groups (default: `"groups"`). - `oidc_groups_claim/0` JWT/user_info claim name for groups (default: `"groups"`).
Set via ENV in production: OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM (see config/runtime.exs). Set via ENV: OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM; or via Settings (Basic settings OIDC).
""" """
@doc "Returns the OIDC group name that maps to Admin role, or nil if not configured." @doc "Returns the OIDC group name that maps to Admin role, or nil if not configured."
def oidc_admin_group_name do def oidc_admin_group_name do
get(:admin_group_name) Mv.Config.oidc_admin_group_name()
end end
@doc "Returns the JWT/user_info claim name for groups; defaults to \"groups\"." @doc "Returns the JWT/user_info claim name for groups; defaults to \"groups\"."
def oidc_groups_claim do def oidc_groups_claim do
get(:groups_claim) || "groups" Mv.Config.oidc_groups_claim() || "groups"
end
defp get(key) do
Application.get_env(:mv, :oidc_role_sync, []) |> Keyword.get(key)
end end
end end

View file

@ -7,59 +7,66 @@ defmodule Mv.Secrets do
particularly for OIDC (Rauthy) authentication. particularly for OIDC (Rauthy) authentication.
## Configuration Source ## Configuration Source
Secrets are read from the `:oidc` key in the application configuration, Secrets are read via `Mv.Config` which prefers environment variables and
which is typically set in `config/runtime.exs` from environment variables: falls back to Settings from the database:
- `OIDC_CLIENT_ID` - OIDC_CLIENT_ID / settings.oidc_client_id
- `OIDC_CLIENT_SECRET` - OIDC_CLIENT_SECRET / settings.oidc_client_secret
- `OIDC_BASE_URL` - OIDC_BASE_URL / settings.oidc_base_url
- `OIDC_REDIRECT_URI` - OIDC_REDIRECT_URI / settings.oidc_redirect_uri
## Usage When a value is nil, returns `{:error, MissingSecret}` so that AshAuthentication
This module is automatically called by AshAuthentication when resolving does not crash (e.g. URI.new(nil)) and can redirect to sign-in with an error.
secrets for the User resource's OIDC strategy.
""" """
use AshAuthentication.Secret use AshAuthentication.Secret
alias AshAuthentication.Errors.MissingSecret
def secret_for( def secret_for(
[:authentication, :strategies, :oidc, :client_id], [:authentication, :strategies, :oidc, :client_id],
Mv.Accounts.User, resource,
_opts, _opts,
_meth _meth
) do ) do
get_config(:client_id) secret_or_error(Mv.Config.oidc_client_id(), resource, :client_id)
end end
def secret_for( def secret_for(
[:authentication, :strategies, :oidc, :redirect_uri], [:authentication, :strategies, :oidc, :redirect_uri],
Mv.Accounts.User, resource,
_opts, _opts,
_meth _meth
) do ) do
get_config(:redirect_uri) secret_or_error(Mv.Config.oidc_redirect_uri(), resource, :redirect_uri)
end end
def secret_for( def secret_for(
[:authentication, :strategies, :oidc, :client_secret], [:authentication, :strategies, :oidc, :client_secret],
Mv.Accounts.User, resource,
_opts, _opts,
_meth _meth
) do ) do
get_config(:client_secret) secret_or_error(Mv.Config.oidc_client_secret(), resource, :client_secret)
end end
def secret_for( def secret_for(
[:authentication, :strategies, :oidc, :base_url], [:authentication, :strategies, :oidc, :base_url],
Mv.Accounts.User, resource,
_opts, _opts,
_meth _meth
) do ) do
get_config(:base_url) secret_or_error(Mv.Config.oidc_base_url(), resource, :base_url)
end end
defp get_config(key) do defp secret_or_error(nil, resource, key) do
:mv path = [:authentication, :strategies, :oidc, key]
|> Application.fetch_env!(:oidc) {:error, MissingSecret.exception(path: path, resource: resource)}
|> Keyword.fetch!(key) end
|> then(&{:ok, &1})
defp secret_or_error(value, resource, key) when is_binary(value) do
if String.trim(value) == "" do
secret_or_error(nil, resource, key)
else
{:ok, value}
end
end end
end end

View file

@ -10,6 +10,54 @@ defmodule Mv.Vereinfacht.Client do
@content_type "application/vnd.api+json" @content_type "application/vnd.api+json"
@doc """
Tests the connection to the Vereinfacht API with the given credentials.
Makes a lightweight `GET /finance-contacts?page[size]=1` request to verify
that the API URL, API key, and club ID are valid and reachable.
## Returns
- `{:ok, :connected}` credentials are valid (HTTP 200)
- `{:error, :not_configured}` any parameter is nil or blank
- `{:error, {:http, status, message}}` API returned an error (e.g. 401, 403)
- `{:error, {:request_failed, reason}}` network/transport error
## Examples
iex> test_connection("https://api.example.com/api/v1", "token", "2")
{:ok, :connected}
iex> test_connection(nil, "token", "2")
{:error, :not_configured}
"""
@spec test_connection(String.t() | nil, String.t() | nil, String.t() | nil) ::
{:ok, :connected} | {:error, term()}
def test_connection(api_url, api_key, club_id) do
if blank?(api_url) or blank?(api_key) or blank?(club_id) do
{:error, :not_configured}
else
url =
api_url
|> String.trim_trailing("/")
|> then(&"#{&1}/finance-contacts?page[size]=1")
case Req.get(url, [headers: headers(api_key)] ++ req_http_options()) do
{:ok, %{status: 200}} ->
{:ok, :connected}
{:ok, %{status: status, body: body}} ->
{:error, {:http, status, extract_error_message(body)}}
{:error, reason} ->
{:error, {:request_failed, reason}}
end
end
end
defp blank?(nil), do: true
defp blank?(s) when is_binary(s), do: String.trim(s) == ""
defp blank?(_), do: true
@doc """ @doc """
Creates a finance contact in Vereinfacht for the given member. Creates a finance contact in Vereinfacht for the given member.
@ -360,5 +408,16 @@ defmodule Mv.Vereinfacht.Client do
defp extract_error_message(%{"errors" => [%{"detail" => d} | _]}) when is_binary(d), do: d defp extract_error_message(%{"errors" => [%{"detail" => d} | _]}) when is_binary(d), do: d
defp extract_error_message(%{"errors" => [%{"title" => t} | _]}) when is_binary(t), do: t defp extract_error_message(%{"errors" => [%{"title" => t} | _]}) when is_binary(t), do: t
defp extract_error_message(body) when is_map(body), do: inspect(body) defp extract_error_message(body) when is_map(body), do: inspect(body)
defp extract_error_message(body) when is_binary(body) do
trimmed = String.trim(body)
if String.starts_with?(trimmed, "<") do
:html_response
else
trimmed
end
end
defp extract_error_message(other), do: inspect(other) defp extract_error_message(other), do: inspect(other)
end end

View file

@ -14,6 +14,27 @@ defmodule Mv.Vereinfacht do
alias Mv.Helpers.SystemActor alias Mv.Helpers.SystemActor
alias Mv.Helpers alias Mv.Helpers
@doc """
Tests the connection to the Vereinfacht API using the current configuration.
Delegates to `Mv.Vereinfacht.Client.test_connection/3` with the values from
`Mv.Config` (ENV variables take priority over database settings).
## Returns
- `{:ok, :connected}` credentials are valid and API is reachable
- `{:error, :not_configured}` URL, API key or club ID is missing
- `{:error, {:http, status, message}}` API returned an error (e.g. 401, 403)
- `{:error, {:request_failed, reason}}` network/transport error
"""
@spec test_connection() :: {:ok, :connected} | {:error, term()}
def test_connection do
Client.test_connection(
Mv.Config.vereinfacht_api_url(),
Mv.Config.vereinfacht_api_key(),
Mv.Config.vereinfacht_club_id()
)
end
@doc """ @doc """
Syncs a single member to Vereinfacht (create or update finance contact). Syncs a single member to Vereinfacht (create or update finance contact).

View file

@ -38,12 +38,10 @@ defmodule MvWeb.AuthOverrides do
set :image_url, nil set :image_url, nil
end end
# Translate the or in the horizontal rule to German # Translate the "or" in the horizontal rule (between password form and SSO).
# Uses auth domain so it respects the current locale (e.g. "oder" in German).
override AshAuthentication.Phoenix.Components.HorizontalRule do override AshAuthentication.Phoenix.Components.HorizontalRule do
set :text, set :text, dgettext("auth", "or")
Gettext.with_locale(MvWeb.Gettext, "de", fn ->
Gettext.gettext(MvWeb.Gettext, "or")
end)
end end
# Hide AshAuthentication's Flash component since we use flash_group in root layout # Hide AshAuthentication's Flash component since we use flash_group in root layout

View file

@ -80,11 +80,11 @@ defmodule MvWeb.Layouts.Sidebar do
/> />
<% end %> <% end %>
<%= if can_access_page?(@current_user, PagePaths.membership_fee_types()) do %> <%= if can_access_page?(@current_user, PagePaths.groups()) do %>
<.menu_item <.menu_item
href={~p"/membership_fee_types"} href={~p"/groups"}
icon="hero-currency-euro" icon="hero-user-group"
label={gettext("Fee Types")} label={gettext("Groups")}
/> />
<% end %> <% end %>
@ -102,24 +102,26 @@ defmodule MvWeb.Layouts.Sidebar do
label={gettext("Administration")} label={gettext("Administration")}
testid="sidebar-administration" testid="sidebar-administration"
> >
<%= if can_access_page?(@current_user, PagePaths.users()) do %> <%= if can_access_page?(@current_user, PagePaths.settings()) do %>
<.menu_subitem href={~p"/users"} label={gettext("Users")} /> <.menu_subitem href={~p"/settings"} label={gettext("Basic settings")} />
<% end %> <% end %>
<%= if can_access_page?(@current_user, PagePaths.groups()) do %> <%= if can_access_page?(@current_user, PagePaths.admin_datafields()) do %>
<.menu_subitem href={~p"/groups"} label={gettext("Groups")} /> <.menu_subitem href={~p"/admin/datafields"} label={gettext("Datafields")} />
<% end %>
<%= if can_access_page?(@current_user, PagePaths.admin_roles()) do %>
<.menu_subitem href={~p"/admin/roles"} label={gettext("Roles")} />
<% end %> <% end %>
<%= if can_access_page?(@current_user, PagePaths.membership_fee_settings()) do %> <%= if can_access_page?(@current_user, PagePaths.membership_fee_settings()) do %>
<.menu_subitem <.menu_subitem
href={~p"/membership_fee_settings"} href={~p"/membership_fee_settings"}
label={gettext("Fee Settings")} label={gettext("Membership fee settings")}
/> />
<% end %> <% end %>
<%= if can_access_page?(@current_user, PagePaths.settings()) do %> <%= if can_access_page?(@current_user, PagePaths.admin_import()) do %>
<.menu_subitem href={~p"/admin/import"} label={gettext("Import")} /> <.menu_subitem href={~p"/admin/import"} label={gettext("Import")} />
<.menu_subitem href={~p"/settings"} label={gettext("Settings")} /> <% end %>
<%= if can_access_page?(@current_user, PagePaths.users()) do %>
<.menu_subitem href={~p"/users"} label={gettext("Users")} />
<% end %>
<%= if can_access_page?(@current_user, PagePaths.admin_roles()) do %>
<.menu_subitem href={~p"/admin/roles"} label={gettext("Roles")} />
<% end %> <% end %>
</.menu_group> </.menu_group>
<% end %> <% end %>

View file

@ -0,0 +1,101 @@
defmodule MvWeb.SignInLive do
@moduledoc """
Custom sign-in page with language selector and conditional Single Sign-On button.
- Renders a language selector (same pattern as LinkOidcAccountLive).
- Wraps the default AshAuthentication SignIn component in a container with
`data-oidc-configured` so that CSS can hide the SSO button when OIDC is not configured.
"""
use Phoenix.LiveView
use Gettext, backend: MvWeb.Gettext
alias AshAuthentication.Phoenix.Components
alias Mv.Config
@impl true
def mount(_params, session, socket) do
overrides =
session
|> Map.get("overrides", [AshAuthentication.Phoenix.Overrides.Default])
# Locale: same fallback as LiveUserAuth so config :default_locale (e.g. "en" in test) is respected
locale =
session["locale"] || Application.get_env(:mv, :default_locale, "de")
Gettext.put_locale(MvWeb.Gettext, locale)
socket =
socket
|> assign(overrides: overrides)
|> assign_new(:otp_app, fn -> nil end)
|> assign(:path, session["path"] || "/")
|> assign(:reset_path, session["reset_path"])
|> assign(:register_path, session["register_path"])
|> assign(:current_tenant, session["tenant"])
|> assign(:resources, session["resources"])
|> assign(:context, session["context"] || %{})
|> assign(:auth_routes_prefix, session["auth_routes_prefix"])
|> assign(:gettext_fn, session["gettext_fn"])
|> assign(:live_action, :sign_in)
|> assign(:oidc_configured, Config.oidc_configured?())
|> assign(:oidc_only, Config.oidc_only?())
|> assign(:root_class, "grid h-screen place-items-center bg-base-100")
|> assign(:sign_in_id, "sign-in")
|> assign(:locale, locale)
{:ok, socket}
end
@impl true
def handle_params(_, _uri, socket) do
{:noreply, socket}
end
@impl true
def render(assigns) do
~H"""
<div
id="sign-in-page"
class={@root_class}
data-oidc-configured={to_string(@oidc_configured)}
data-oidc-only={to_string(@oidc_only)}
data-locale={@locale}
>
<%!-- Language selector --%>
<nav
aria-label={dgettext("auth", "Language selection")}
class="absolute top-4 right-4 flex justify-end z-10"
>
<form method="post" action="/set_locale" class="text-sm">
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
<select
name="locale"
onchange="this.form.submit()"
class="select select-sm select-bordered bg-base-100"
aria-label={dgettext("auth", "Select language")}
>
<option value="de" selected={@locale == "de"}>Deutsch</option>
<option value="en" selected={@locale == "en"}>English</option>
</select>
</form>
</nav>
<.live_component
module={Components.SignIn}
otp_app={@otp_app}
live_action={@live_action}
path={@path}
auth_routes_prefix={@auth_routes_prefix}
resources={@resources}
reset_path={@reset_path}
register_path={@register_path}
id={@sign_in_id}
overrides={@overrides}
current_tenant={@current_tenant}
context={@context}
gettext_fn={@gettext_fn}
/>
</div>
"""
end
end

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

View file

@ -34,15 +34,14 @@ defmodule MvWeb.GlobalSettingsLive do
def mount(_params, session, socket) do def mount(_params, session, socket) do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
# Get locale from session for translations # Get locale from session; same fallback as router/LiveUserAuth (respects config :default_locale in test)
locale = session["locale"] || "de" locale = session["locale"] || Application.get_env(:mv, :default_locale, "de")
Gettext.put_locale(MvWeb.Gettext, locale) Gettext.put_locale(MvWeb.Gettext, locale)
socket = socket =
socket socket
|> assign(:page_title, gettext("Settings")) |> assign(:page_title, gettext("Settings"))
|> assign(:settings, settings) |> assign(:settings, settings)
|> assign(:active_editing_section, nil)
|> assign(:locale, locale) |> assign(:locale, locale)
|> assign(:vereinfacht_env_configured, Mv.Config.vereinfacht_env_configured?()) |> assign(:vereinfacht_env_configured, Mv.Config.vereinfacht_env_configured?())
|> assign(:vereinfacht_api_url_env_set, Mv.Config.vereinfacht_api_url_env_set?()) |> assign(:vereinfacht_api_url_env_set, Mv.Config.vereinfacht_api_url_env_set?())
@ -51,6 +50,17 @@ defmodule MvWeb.GlobalSettingsLive do
|> assign(:vereinfacht_app_url_env_set, Mv.Config.vereinfacht_app_url_env_set?()) |> assign(:vereinfacht_app_url_env_set, Mv.Config.vereinfacht_app_url_env_set?())
|> assign(:vereinfacht_api_key_set, present?(settings.vereinfacht_api_key)) |> assign(:vereinfacht_api_key_set, present?(settings.vereinfacht_api_key))
|> assign(:last_vereinfacht_sync_result, nil) |> assign(:last_vereinfacht_sync_result, nil)
|> assign(:vereinfacht_test_result, nil)
|> assign(:oidc_env_configured, Mv.Config.oidc_env_configured?())
|> assign(:oidc_client_id_env_set, Mv.Config.oidc_client_id_env_set?())
|> assign(:oidc_base_url_env_set, Mv.Config.oidc_base_url_env_set?())
|> assign(:oidc_redirect_uri_env_set, Mv.Config.oidc_redirect_uri_env_set?())
|> assign(:oidc_client_secret_env_set, Mv.Config.oidc_client_secret_env_set?())
|> assign(:oidc_admin_group_name_env_set, Mv.Config.oidc_admin_group_name_env_set?())
|> assign(:oidc_groups_claim_env_set, Mv.Config.oidc_groups_claim_env_set?())
|> assign(:oidc_only_env_set, Mv.Config.oidc_only_env_set?())
|> assign(:oidc_configured, Mv.Config.oidc_configured?())
|> assign(:oidc_client_secret_set, present?(settings.oidc_client_secret))
|> assign_form() |> assign_form()
{:ok, socket} {:ok, socket}
@ -167,35 +177,162 @@ defmodule MvWeb.GlobalSettingsLive do
> >
{gettext("Save Vereinfacht Settings")} {gettext("Save Vereinfacht Settings")}
</.button> </.button>
<.button <div class="mt-2 flex flex-wrap gap-2">
:if={Mv.Config.vereinfacht_configured?()} <.button
type="button" :if={Mv.Config.vereinfacht_configured?()}
phx-click="sync_vereinfacht_contacts" type="button"
phx-disable-with={gettext("Syncing...")} phx-click="test_vereinfacht_connection"
class="mt-4 btn-outline" phx-disable-with={gettext("Testing...")}
> class="btn-outline"
{gettext("Sync all members without Vereinfacht contact")} >
</.button> {gettext("Test Integration")}
</.button>
<.button
:if={Mv.Config.vereinfacht_configured?()}
type="button"
phx-click="sync_vereinfacht_contacts"
phx-disable-with={gettext("Syncing...")}
class="btn-outline"
>
{gettext("Sync all members without Vereinfacht contact")}
</.button>
</div>
<%= if @vereinfacht_test_result do %>
<.vereinfacht_test_result result={@vereinfacht_test_result} />
<% end %>
<%= if @last_vereinfacht_sync_result do %> <%= if @last_vereinfacht_sync_result do %>
<.vereinfacht_sync_result result={@last_vereinfacht_sync_result} /> <.vereinfacht_sync_result result={@last_vereinfacht_sync_result} />
<% end %> <% end %>
</.form> </.form>
</.form_section> </.form_section>
<%!-- Memberdata Section --%> <%!-- OIDC Section --%>
<.form_section title={gettext("Memberdata")}> <.form_section title={gettext("OIDC")}>
<.live_component <%= if @oidc_env_configured do %>
:if={@active_editing_section != :custom_fields} <p class="text-sm text-base-content/70 mb-4">
module={MvWeb.MemberFieldLive.IndexComponent} {gettext("Some values are set via environment variables. Those fields are read-only.")}
id="member-fields-component" </p>
settings={@settings} <% end %>
/> <.form for={@form} id="oidc-form" phx-change="validate" phx-submit="save">
<%!-- Custom Fields Section --%> <div class="grid gap-4">
<.live_component <.input
:if={@active_editing_section != :member_fields} field={@form[:oidc_client_id]}
module={MvWeb.CustomFieldLive.IndexComponent} type="text"
id="custom-fields-component" label={gettext("Client ID")}
actor={@current_user} disabled={@oidc_client_id_env_set}
/> placeholder={
if(@oidc_client_id_env_set, do: gettext("From OIDC_CLIENT_ID"), else: "mv")
}
/>
<.input
field={@form[:oidc_base_url]}
type="text"
label={gettext("Base URL")}
disabled={@oidc_base_url_env_set}
placeholder={
if(@oidc_base_url_env_set,
do: gettext("From OIDC_BASE_URL"),
else: "http://localhost:8080/auth/v1"
)
}
/>
<.input
field={@form[:oidc_redirect_uri]}
type="text"
label={gettext("Redirect URI")}
disabled={@oidc_redirect_uri_env_set}
placeholder={
if(@oidc_redirect_uri_env_set,
do: gettext("From OIDC_REDIRECT_URI"),
else: "http://localhost:4000/auth/user/oidc/callback"
)
}
/>
<div class="form-control">
<label class="label" for={@form[:oidc_client_secret].id}>
<span class="label-text">{gettext("Client Secret")}</span>
<%= if @oidc_client_secret_set do %>
<span class="label-text-alt badge badge-ghost">{gettext("(set)")}</span>
<% end %>
</label>
<.input
field={@form[:oidc_client_secret]}
type="password"
label=""
disabled={@oidc_client_secret_env_set}
placeholder={
if(@oidc_client_secret_env_set,
do: gettext("From OIDC_CLIENT_SECRET"),
else:
if(@oidc_client_secret_set,
do: gettext("Leave blank to keep current"),
else: nil
)
)
}
/>
</div>
<.input
field={@form[:oidc_admin_group_name]}
type="text"
label={gettext("Admin group name")}
disabled={@oidc_admin_group_name_env_set}
placeholder={
if(@oidc_admin_group_name_env_set,
do: gettext("From OIDC_ADMIN_GROUP_NAME"),
else: gettext("e.g. admin")
)
}
/>
<.input
field={@form[:oidc_groups_claim]}
type="text"
label={gettext("Groups claim")}
disabled={@oidc_groups_claim_env_set}
placeholder={
if(@oidc_groups_claim_env_set,
do: gettext("From OIDC_GROUPS_CLAIM"),
else: "groups"
)
}
/>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-2">
<.input
field={@form[:oidc_only]}
type="checkbox"
class="checkbox checkbox-sm"
disabled={@oidc_only_env_set or not @oidc_configured}
/>
<span class="label-text">
{gettext("Only OIDC sign-in (hide password login)")}
<%= if @oidc_only_env_set do %>
<span class="label-text-alt text-base-content/70 ml-1">
({gettext("From OIDC_ONLY")})
</span>
<% end %>
</span>
</label>
<p class="label-text-alt text-base-content/70 mt-1">
{gettext(
"When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button."
)}
</p>
</div>
</div>
<.button
:if={
not (@oidc_client_id_env_set and @oidc_base_url_env_set and
@oidc_redirect_uri_env_set and @oidc_client_secret_env_set and
@oidc_admin_group_name_env_set and @oidc_groups_claim_env_set and
@oidc_only_env_set)
}
phx-disable-with={gettext("Saving...")}
variant="primary"
class="mt-2"
>
{gettext("Save OIDC Settings")}
</.button>
</.form>
</.form_section> </.form_section>
</Layouts.app> </Layouts.app>
""" """
@ -207,6 +344,12 @@ 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("test_vereinfacht_connection", _params, socket) do
result = Mv.Vereinfacht.test_connection()
{:noreply, assign(socket, :vereinfacht_test_result, result)}
end
@impl true @impl true
def handle_event("sync_vereinfacht_contacts", _params, socket) do def handle_event("sync_vereinfacht_contacts", _params, socket) do
case Mv.Vereinfacht.sync_members_without_contact() do case Mv.Vereinfacht.sync_members_without_contact() do
@ -244,17 +387,28 @@ defmodule MvWeb.GlobalSettingsLive do
@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)
# Never send blank API key so we do not overwrite the stored secret (security) # Never send blank API key / client secret so we do not overwrite stored secrets
setting_params_clean = drop_blank_vereinfacht_api_key(setting_params) setting_params_clean =
setting_params
|> drop_blank_vereinfacht_api_key()
|> drop_blank_oidc_client_secret()
saves_vereinfacht = vereinfacht_params?(setting_params_clean)
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, setting_params_clean, actor) do case MvWeb.LiveHelpers.submit_form(socket.assigns.form, setting_params_clean, actor) do
{:ok, _updated_settings} -> {:ok, _updated_settings} ->
{:ok, fresh_settings} = Membership.get_settings() {:ok, fresh_settings} = Membership.get_settings()
test_result =
if saves_vereinfacht, do: Mv.Vereinfacht.test_connection(), else: nil
socket = socket =
socket socket
|> assign(:settings, fresh_settings) |> assign(:settings, fresh_settings)
|> assign(:vereinfacht_api_key_set, present?(fresh_settings.vereinfacht_api_key)) |> assign(:vereinfacht_api_key_set, present?(fresh_settings.vereinfacht_api_key))
|> assign(:oidc_client_secret_set, present?(fresh_settings.oidc_client_secret))
|> assign(:oidc_configured, Mv.Config.oidc_configured?())
|> assign(:vereinfacht_test_result, test_result)
|> put_flash(:info, gettext("Settings updated successfully")) |> put_flash(:info, gettext("Settings updated successfully"))
|> assign_form() |> assign_form()
@ -265,6 +419,12 @@ defmodule MvWeb.GlobalSettingsLive do
end end
end end
@vereinfacht_param_keys ~w[vereinfacht_api_url vereinfacht_api_key vereinfacht_club_id vereinfacht_app_url]
defp vereinfacht_params?(params) when is_map(params) do
Enum.any?(@vereinfacht_param_keys, &Map.has_key?(params, &1))
end
defp drop_blank_vereinfacht_api_key(params) when is_map(params) do defp drop_blank_vereinfacht_api_key(params) when is_map(params) do
case params do case params do
%{"vereinfacht_api_key" => v} when v in [nil, ""] -> %{"vereinfacht_api_key" => v} when v in [nil, ""] ->
@ -275,88 +435,28 @@ defmodule MvWeb.GlobalSettingsLive do
end end
end end
@impl true defp drop_blank_oidc_client_secret(params) when is_map(params) do
def handle_info({:custom_field_saved, _custom_field, action}, socket) do case params do
send_update(MvWeb.CustomFieldLive.IndexComponent, %{"oidc_client_secret" => v} when v in [nil, ""] ->
id: "custom-fields-component", Map.delete(params, "oidc_client_secret")
show_form: false
)
{:noreply, _ ->
socket params
|> assign(:active_editing_section, nil) end
|> 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
# Reload settings to get updated member_field_visibility
{:ok, updated_settings} = Membership.get_settings()
# Send update to member fields component to close form
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
# Legacy event - reload settings and update component
{: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
defp assign_form(%{assigns: %{settings: settings}} = socket) do defp assign_form(%{assigns: %{settings: settings}} = socket) do
# Never put API key into form/DOM to avoid secret leak in source or DevTools # Show ENV values in disabled fields (Vereinfacht and OIDC); never expose API key / client secret
settings_for_form = %{settings | vereinfacht_api_key: nil} settings_display =
settings
|> merge_vereinfacht_env_values()
|> merge_oidc_env_values()
settings_for_form = %{
settings_display
| vereinfacht_api_key: nil,
oidc_client_secret: nil
}
form = form =
AshPhoenix.Form.for_update( AshPhoenix.Form.for_update(
@ -370,6 +470,66 @@ defmodule MvWeb.GlobalSettingsLive do
assign(socket, form: to_form(form)) assign(socket, form: to_form(form))
end end
defp put_if_env_set(map, _key, false, _value), do: map
defp put_if_env_set(map, key, true, value), do: Map.put(map, key, value)
defp merge_vereinfacht_env_values(s) do
s
|> put_if_env_set(
:vereinfacht_api_url,
Mv.Config.vereinfacht_api_url_env_set?(),
Mv.Config.vereinfacht_api_url()
)
|> put_if_env_set(
:vereinfacht_club_id,
Mv.Config.vereinfacht_club_id_env_set?(),
Mv.Config.vereinfacht_club_id()
)
|> put_if_env_set(
:vereinfacht_app_url,
Mv.Config.vereinfacht_app_url_env_set?(),
Mv.Config.vereinfacht_app_url()
)
end
defp merge_oidc_env_values(s) do
s
|> put_if_env_set(
:oidc_client_id,
Mv.Config.oidc_client_id_env_set?(),
Mv.Config.oidc_client_id()
)
|> put_if_env_set(
:oidc_base_url,
Mv.Config.oidc_base_url_env_set?(),
Mv.Config.oidc_base_url()
)
|> put_if_env_set(
:oidc_redirect_uri,
Mv.Config.oidc_redirect_uri_env_set?(),
Mv.Config.oidc_redirect_uri()
)
|> put_if_env_set(
:oidc_admin_group_name,
Mv.Config.oidc_admin_group_name_env_set?(),
Mv.Config.oidc_admin_group_name()
)
|> put_if_env_set(
:oidc_groups_claim,
Mv.Config.oidc_groups_claim_env_set?(),
Mv.Config.oidc_groups_claim()
)
|> put_if_oidc_only_env_set()
end
defp put_if_oidc_only_env_set(s) do
if Mv.Config.oidc_only_env_set?() do
Map.put(s, :oidc_only, Mv.Config.oidc_only?())
else
s
end
end
defp enrich_sync_errors([]), do: [] defp enrich_sync_errors([]), do: []
defp enrich_sync_errors(errors) when is_list(errors) do defp enrich_sync_errors(errors) when is_list(errors) do
@ -412,6 +572,109 @@ defmodule MvWeb.GlobalSettingsLive do
Gettext.dgettext(MvWeb.Gettext, "default", message) Gettext.dgettext(MvWeb.Gettext, "default", message)
end end
attr :result, :any, required: true
defp vereinfacht_test_result(%{result: {:ok, :connected}} = assigns) do
~H"""
<div class="mt-3 flex items-center gap-2 p-3 rounded-lg border border-success bg-success/10 text-success-aa text-sm">
<.icon name="hero-check-circle" class="size-5 shrink-0" />
<span>{gettext("Connection successful. API URL, API Key and Club ID are valid.")}</span>
</div>
"""
end
defp vereinfacht_test_result(%{result: {:error, :not_configured}} = assigns) do
~H"""
<div class="mt-3 flex items-center gap-2 p-3 rounded-lg border border-warning bg-warning/10 text-warning-aa text-sm">
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
<span>{gettext("Not configured. Please set API URL, API Key and Club ID.")}</span>
</div>
"""
end
defp vereinfacht_test_result(%{result: {:error, {:http, _status, :html_response}}} = assigns) do
~H"""
<div class="mt-3 flex items-start gap-2 p-3 rounded-lg border border-error bg-error/10 text-error-aa text-sm">
<.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
<span>
{gettext(
"Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
)}
</span>
</div>
"""
end
defp vereinfacht_test_result(%{result: {:error, {:http, 401, _}}} = assigns) do
~H"""
<div class="mt-3 flex items-start gap-2 p-3 rounded-lg border border-error bg-error/10 text-error-aa text-sm">
<.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
<span>{gettext("Connection failed (HTTP 401): API key is invalid or missing.")}</span>
</div>
"""
end
defp vereinfacht_test_result(%{result: {:error, {:http, 403, _}}} = assigns) do
~H"""
<div class="mt-3 flex items-start gap-2 p-3 rounded-lg border border-error bg-error/10 text-error-aa text-sm">
<.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
<span>
{gettext(
"Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
)}
</span>
</div>
"""
end
defp vereinfacht_test_result(%{result: {:error, {:http, 404, _}}} = assigns) do
~H"""
<div class="mt-3 flex items-start gap-2 p-3 rounded-lg border border-error bg-error/10 text-error-aa text-sm">
<.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
<span>
{gettext(
"Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
)}
</span>
</div>
"""
end
defp vereinfacht_test_result(%{result: {:error, {:http, status, message}}} = assigns) do
assigns = assign(assigns, :status, status)
assigns = assign(assigns, :message, message)
~H"""
<div class="mt-3 flex items-start gap-2 p-3 rounded-lg border border-error bg-error/10 text-error-aa text-sm">
<.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
<span>
{gettext("Connection failed (HTTP %{status}):", status: @status)}
<span class="ml-1">{@message}</span>
</span>
</div>
"""
end
defp vereinfacht_test_result(%{result: {:error, {:request_failed, _reason}}} = assigns) do
~H"""
<div class="mt-3 flex items-center gap-2 p-3 rounded-lg border border-error bg-error/10 text-error-aa text-sm">
<.icon name="hero-x-circle" class="size-5 shrink-0" />
<span>
{gettext("Connection failed. Could not reach the API (network error or wrong URL).")}
</span>
</div>
"""
end
defp vereinfacht_test_result(%{result: {:error, _}} = assigns) do
~H"""
<div class="mt-3 flex items-center gap-2 p-3 rounded-lg border border-error bg-error/10 text-error-aa text-sm">
<.icon name="hero-x-circle" class="size-5 shrink-0" />
<span>{gettext("Connection failed. Unknown error.")}</span>
</div>
"""
end
attr :result, :map, required: true attr :result, :map, required: true
defp vereinfacht_sync_result(assigns) do defp vereinfacht_sync_result(assigns) do

View file

@ -1,17 +1,23 @@
defmodule MvWeb.MembershipFeeSettingsLive do defmodule MvWeb.MembershipFeeSettingsLive do
@moduledoc """ @moduledoc """
LiveView for managing membership fee settings (Admin). LiveView for membership fee settings and fee types (Admin).
Allows administrators to configure: Combines:
- Default membership fee type for new members - Global settings (default fee type, include joining cycle)
- Whether to include the joining cycle in membership fee generation - Membership fee types table (CRUD links to new/edit routes; delete inline)
Examples and info are collapsible to save space.
""" """
use MvWeb, :live_view use MvWeb, :live_view
require Ash.Query
import MvWeb.LiveHelpers, only: [current_actor: 1] import MvWeb.LiveHelpers, only: [current_actor: 1]
alias Mv.Membership alias Mv.Membership
alias Mv.Membership.Member
alias Mv.MembershipFees
alias Mv.MembershipFees.MembershipFeeType alias Mv.MembershipFees.MembershipFeeType
alias MvWeb.Helpers.MembershipFeeHelpers
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -23,11 +29,14 @@ defmodule MvWeb.MembershipFeeSettingsLive do
|> Ash.Query.sort(name: :asc) |> Ash.Query.sort(name: :asc)
|> Ash.read!(domain: Mv.MembershipFees, actor: actor) |> Ash.read!(domain: Mv.MembershipFees, actor: actor)
member_counts = load_member_counts(membership_fee_types, actor)
{:ok, {:ok,
socket socket
|> assign(:page_title, gettext("Membership Fee Settings")) |> assign(:page_title, gettext("Membership Fee Settings"))
|> assign(:settings, settings) |> assign(:settings, settings)
|> assign(:membership_fee_types, membership_fee_types) |> assign(:membership_fee_types, membership_fee_types)
|> assign(:member_counts, member_counts)
|> assign_form()} |> assign_form()}
end end
@ -81,6 +90,51 @@ defmodule MvWeb.MembershipFeeSettingsLive do
end end
end end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
actor = current_actor(socket)
case Ash.get(MembershipFeeType, id, domain: MembershipFees, actor: actor) do
{:ok, fee_type} ->
case Ash.destroy(fee_type, domain: MembershipFees, actor: actor) do
:ok ->
updated_types = Enum.reject(socket.assigns.membership_fee_types, &(&1.id == id))
updated_counts = Map.delete(socket.assigns.member_counts, id)
{:noreply,
socket
|> assign(:membership_fee_types, updated_types)
|> assign(:member_counts, updated_counts)
|> put_flash(:info, gettext("Membership fee type deleted"))}
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
put_flash(
socket,
:error,
gettext("You do not have permission to delete this membership fee type")
)}
{:error, error} ->
{:noreply, put_flash(socket, :error, format_error(error))}
end
{:error, %Ash.Error.Query.NotFound{}} ->
{:noreply, put_flash(socket, :error, gettext("Membership fee type not found"))}
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
put_flash(
socket,
:error,
gettext("You do not have permission to access this membership fee type")
)}
{:error, error} ->
{:noreply, put_flash(socket, :error, format_error(error))}
end
end
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
@ -88,8 +142,13 @@ defmodule MvWeb.MembershipFeeSettingsLive do
<.header> <.header>
{gettext("Membership Fee Settings")} {gettext("Membership Fee Settings")}
<:subtitle> <:subtitle>
{gettext("Configure global settings for membership fees.")} {gettext("Configure global settings and fee types for membership fees.")}
</:subtitle> </:subtitle>
<:actions>
<.button variant="primary" navigate={~p"/membership_fee_settings/new_fee_type"}>
<.icon name="hero-plus" /> {gettext("New Membership Fee Type")}
</.button>
</:actions>
</.header> </.header>
<div class="grid gap-6 lg:grid-cols-2"> <div class="grid gap-6 lg:grid-cols-2">
@ -188,58 +247,169 @@ defmodule MvWeb.MembershipFeeSettingsLive do
</div> </div>
</div> </div>
<%!-- Examples Card --%> <%!-- Examples Card (collapsible) --%>
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body"> <div class="card-body">
<h2 class="card-title"> <details class="group">
<.icon name="hero-light-bulb" class="size-5" /> <summary class="card-title cursor-pointer list-none flex items-center gap-2">
{gettext("Examples")} <.icon name="hero-chevron-right" class="size-5 transition group-open:rotate-90" />
</h2> <.icon name="hero-light-bulb" class="size-5" />
{gettext("Examples")}
</summary>
<.example_section <div class="pt-4 space-y-4">
title={gettext("Yearly Interval - Joining Cycle Included")} <.example_section
joining_date="15.03.2023" title={gettext("Yearly Interval - Joining Cycle Included")}
include_joining={true} joining_date="15.03.2023"
start_date="01.01.2023" include_joining={true}
periods={["2023", "2024", "2025"]} start_date="01.01.2023"
note={gettext("Member pays for the year they joined")} periods={["2023", "2024", "2025"]}
/> note={gettext("Member pays for the year they joined")}
/>
<div class="divider"></div> <div class="divider"></div>
<.example_section <.example_section
title={gettext("Yearly Interval - Joining Cycle Excluded")} title={gettext("Yearly Interval - Joining Cycle Excluded")}
joining_date="15.03.2023" joining_date="15.03.2023"
include_joining={false} include_joining={false}
start_date="01.01.2024" start_date="01.01.2024"
periods={["2024", "2025"]} periods={["2024", "2025"]}
note={gettext("Member pays from the next full year")} note={gettext("Member pays from the next full year")}
/> />
<div class="divider"></div> <div class="divider"></div>
<.example_section <.example_section
title={gettext("Quarterly Interval - Joining Cycle Excluded")} title={gettext("Quarterly Interval - Joining Cycle Excluded")}
joining_date="15.05.2024" joining_date="15.05.2024"
include_joining={false} include_joining={false}
start_date="01.07.2024" start_date="01.07.2024"
periods={["Q3/2024", "Q4/2024", "Q1/2025"]} periods={["Q3/2024", "Q4/2024", "Q1/2025"]}
note={gettext("Member pays from the next full quarter")} note={gettext("Member pays from the next full quarter")}
/> />
<div class="divider"></div> <div class="divider"></div>
<.example_section <.example_section
title={gettext("Monthly Interval - Joining Cycle Included")} title={gettext("Monthly Interval - Joining Cycle Included")}
joining_date="15.03.2024" joining_date="15.03.2024"
include_joining={true} include_joining={true}
start_date="01.03.2024" start_date="01.03.2024"
periods={["03/2024", "04/2024", "05/2024", "..."]} periods={["03/2024", "04/2024", "05/2024", "..."]}
note={gettext("Member pays from the joining month")} note={gettext("Member pays from the joining month")}
/> />
</div>
</details>
</div> </div>
</div> </div>
</div> </div>
<%!-- Fee Types Table --%>
<div class="mt-8">
<h2 class="text-lg font-semibold mb-4">{gettext("Membership Fee Types")}</h2>
<.table
id="membership_fee_types"
rows={@membership_fee_types}
row_id={fn mft -> "mft-#{mft.id}" end}
>
<:col :let={mft} label={gettext("Name")}>
<span class="font-medium">{mft.name}</span>
<p :if={mft.description} class="text-sm text-base-content/70">{mft.description}</p>
</:col>
<:col :let={mft} label={gettext("Amount")}>
<span class="font-mono">{MembershipFeeHelpers.format_currency(mft.amount)}</span>
</:col>
<:col :let={mft} label={gettext("Interval")}>
<span class="badge badge-outline">
{MembershipFeeHelpers.format_interval(mft.interval)}
</span>
</:col>
<:col :let={mft} label={gettext("Members")}>
<span class="badge badge-ghost">{get_member_count(mft, @member_counts)}</span>
</:col>
<:action :let={mft}>
<.link
navigate={~p"/membership_fee_settings/#{mft.id}/edit_fee_type"}
class="btn btn-ghost btn-xs"
aria-label={gettext("Edit membership fee type")}
>
<.icon name="hero-pencil" class="size-4" />
</.link>
</:action>
<:action :let={mft}>
<div
:if={get_member_count(mft, @member_counts) > 0}
class="tooltip tooltip-left"
data-tip={
gettext("Cannot delete - %{count} member(s) assigned",
count: get_member_count(mft, @member_counts)
)
}
>
<button
phx-click="delete"
phx-value-id={mft.id}
data-confirm={gettext("Are you sure?")}
class="btn btn-ghost btn-xs text-error opacity-50 cursor-not-allowed"
aria-label={
gettext("Cannot delete - %{count} member(s) assigned",
count: get_member_count(mft, @member_counts)
)
}
disabled={true}
>
<.icon name="hero-trash" class="size-4" />
</button>
</div>
<button
:if={get_member_count(mft, @member_counts) == 0}
phx-click="delete"
phx-value-id={mft.id}
data-confirm={gettext("Are you sure?")}
class="btn btn-ghost btn-xs text-error"
aria-label={gettext("Delete Membership Fee Type")}
>
<.icon name="hero-trash" class="size-4" />
</button>
</:action>
</.table>
<details class="mt-6 card bg-base-200">
<summary class="card-body cursor-pointer list-none card-title">
<.icon name="hero-information-circle" class="size-5" />
{gettext("About Membership Fee Types")}
</summary>
<div class="card-body pt-0 prose prose-sm max-w-none">
<p>
{gettext(
"Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
)}
</p>
<ul>
<li>
<strong>{gettext("Name & Amount")}</strong>
- {gettext("Can be changed at any time. Amount changes affect future periods only.")}
</li>
<li>
<strong>{gettext("Interval")}</strong>
- {gettext(
"Fixed after creation. Members can only switch between types with the same interval."
)}
</li>
<li>
<strong>{gettext("Deletion")}</strong>
- {gettext("Only possible if no members are assigned to this type.")}
</li>
</ul>
</div>
</details>
</div>
</Layouts.app> </Layouts.app>
""" """
end end
@ -286,6 +456,32 @@ defmodule MvWeb.MembershipFeeSettingsLive do
defp format_interval(:half_yearly), do: gettext("Half-yearly") defp format_interval(:half_yearly), do: gettext("Half-yearly")
defp format_interval(:yearly), do: gettext("Yearly") defp format_interval(:yearly), do: gettext("Yearly")
defp load_member_counts(fee_types, actor) do
fee_type_ids = Enum.map(fee_types, & &1.id)
members =
Member
|> Ash.Query.filter(membership_fee_type_id in ^fee_type_ids)
|> Ash.Query.select([:membership_fee_type_id])
|> Ash.read!(domain: Membership, actor: actor)
members
|> Enum.group_by(& &1.membership_fee_type_id)
|> Enum.map(fn {fee_type_id, members_list} -> {fee_type_id, length(members_list)} end)
|> Map.new()
end
defp get_member_count(fee_type, member_counts) do
Map.get(member_counts, fee_type.id, 0)
end
defp format_error(%Ash.Error.Invalid{} = error) do
Enum.map_join(error.errors, ", ", fn e -> e.message end)
end
defp format_error(error) when is_binary(error), do: error
defp format_error(_error), do: gettext("An error occurred")
defp assign_form(%{assigns: %{settings: settings}} = socket) do defp assign_form(%{assigns: %{settings: settings}} = socket) do
form = form =
AshPhoenix.Form.for_update( AshPhoenix.Form.for_update(

View file

@ -384,7 +384,8 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do
defp format_interval_value(value), do: to_string(value) defp format_interval_value(value), do: to_string(value)
@spec return_path(String.t(), MembershipFeeType.t() | nil) :: String.t() @spec return_path(String.t(), MembershipFeeType.t() | nil) :: String.t()
defp return_path("index", _membership_fee_type), do: ~p"/membership_fee_types" defp return_path("index", _membership_fee_type), do: ~p"/membership_fee_settings"
defp return_path(_, _), do: ~p"/membership_fee_settings"
@spec get_affected_member_count(String.t(), Mv.Accounts.User.t() | nil) :: non_neg_integer() @spec get_affected_member_count(String.t(), Mv.Accounts.User.t() | nil) :: non_neg_integer()
# Checks if amount changed and updates socket assigns accordingly # Checks if amount changed and updates socket assigns accordingly

View file

@ -47,7 +47,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
{gettext("Manage membership fee types for membership fees.")} {gettext("Manage membership fee types for membership fees.")}
</:subtitle> </:subtitle>
<:actions> <:actions>
<.button variant="primary" navigate={~p"/membership_fee_types/new"}> <.button variant="primary" navigate={~p"/membership_fee_settings/new_fee_type"}>
<.icon name="hero-plus" /> {gettext("New Membership Fee Type")} <.icon name="hero-plus" /> {gettext("New Membership Fee Type")}
</.button> </.button>
</:actions> </:actions>
@ -79,7 +79,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
<:action :let={mft}> <:action :let={mft}>
<.link <.link
navigate={~p"/membership_fee_types/#{mft.id}/edit"} navigate={~p"/membership_fee_settings/#{mft.id}/edit_fee_type"}
class="btn btn-ghost btn-xs" class="btn btn-ghost btn-xs"
aria-label={gettext("Edit membership fee type")} aria-label={gettext("Edit membership fee type")}
> >

View file

@ -42,9 +42,8 @@ defmodule MvWeb.LiveUserAuth do
end end
def on_mount(:live_no_user, _params, session, socket) do def on_mount(:live_no_user, _params, session, socket) do
# Set the locale for not logged in user to set the language in the Log-In Screen # Set the locale for not logged in user (default from config, "de" in dev/prod).
# otherwise the locale is not taken for the Log-In Screen locale = session["locale"] || Application.get_env(:mv, :default_locale, "de")
locale = session["locale"] || "en"
Gettext.put_locale(MvWeb.Gettext, locale) Gettext.put_locale(MvWeb.Gettext, locale)
{:cont, assign(socket, :locale, locale)} {:cont, assign(socket, :locale, locale)}

View file

@ -1,10 +1,11 @@
defmodule MvWeb.LocaleController do defmodule MvWeb.LocaleController do
use MvWeb, :controller use MvWeb, :controller
def set_locale(conn, %{"locale" => locale}) do @supported_locales ["de", "en"]
def set_locale(conn, %{"locale" => locale}) when locale in @supported_locales do
conn conn
|> put_session(:locale, locale) |> put_session(:locale, locale)
# Store locale in a cookie that persists beyond the session
|> put_resp_cookie("locale", locale, |> put_resp_cookie("locale", locale,
max_age: 365 * 24 * 60 * 60, max_age: 365 * 24 * 60 * 60,
same_site: "Lax", same_site: "Lax",
@ -14,6 +15,8 @@ defmodule MvWeb.LocaleController do
|> redirect(to: get_referer(conn) || "/") |> redirect(to: get_referer(conn) || "/")
end end
def set_locale(conn, _params), do: redirect(conn, to: get_referer(conn) || "/")
defp get_referer(conn) do defp get_referer(conn) do
conn.req_headers conn.req_headers
|> Enum.find(fn {k, _v} -> k == "referer" end) |> Enum.find(fn {k, _v} -> k == "referer" end)

View file

@ -8,30 +8,30 @@ defmodule MvWeb.PagePaths do
# Sidebar top-level menu paths # Sidebar top-level menu paths
@members "/members" @members "/members"
@membership_fee_types "/membership_fee_types"
@statistics "/statistics" @statistics "/statistics"
# Administration submenu paths (all must match router) # Administration submenu paths (all must match router)
@users "/users" @users "/users"
@groups "/groups" @groups "/groups"
@admin_roles "/admin/roles" @admin_roles "/admin/roles"
@admin_datafields "/admin/datafields"
@membership_fee_settings "/membership_fee_settings" @membership_fee_settings "/membership_fee_settings"
@admin_import "/admin/import"
@settings "/settings" @settings "/settings"
@admin_page_paths [ @admin_page_paths [
@users, @users,
@groups, @groups,
@admin_roles, @admin_roles,
@admin_datafields,
@membership_fee_settings, @membership_fee_settings,
@admin_import,
@settings @settings
] ]
@doc "Path for Members index (sidebar and page permission check)." @doc "Path for Members index (sidebar and page permission check)."
def members, do: @members def members, do: @members
@doc "Path for Membership Fee Types index (sidebar and page permission check)."
def membership_fee_types, do: @membership_fee_types
@doc "Path for Statistics page (sidebar and page permission check)." @doc "Path for Statistics page (sidebar and page permission check)."
def statistics, do: @statistics def statistics, do: @statistics
@ -41,6 +41,8 @@ defmodule MvWeb.PagePaths do
def users, do: @users def users, do: @users
def groups, do: @groups def groups, do: @groups
def admin_roles, do: @admin_roles def admin_roles, do: @admin_roles
def admin_datafields, do: @admin_datafields
def membership_fee_settings, do: @membership_fee_settings def membership_fee_settings, do: @membership_fee_settings
def admin_import, do: @admin_import
def settings, do: @settings def settings, do: @settings
end end

View file

@ -68,16 +68,13 @@ defmodule MvWeb.Router do
live "/settings", GlobalSettingsLive live "/settings", GlobalSettingsLive
# Membership Fee Settings # Membership Fee Settings (includes fee types list; new/edit under sub-routes)
live "/membership_fee_settings", MembershipFeeSettingsLive live "/membership_fee_settings", MembershipFeeSettingsLive
live "/membership_fee_settings/new_fee_type", MembershipFeeTypeLive.Form, :new
# Membership Fee Types Management live "/membership_fee_settings/:id/edit_fee_type", MembershipFeeTypeLive.Form, :edit
live "/membership_fee_types", MembershipFeeTypeLive.Index, :index
# Statistics # Statistics
live "/statistics", StatisticsLive, :index live "/statistics", StatisticsLive, :index
live "/membership_fee_types/new", MembershipFeeTypeLive.Form, :new
live "/membership_fee_types/:id/edit", MembershipFeeTypeLive.Form, :edit
# Groups Management # Groups Management
live "/groups", GroupLive.Index, :index live "/groups", GroupLive.Index, :index
@ -91,6 +88,9 @@ defmodule MvWeb.Router do
live "/admin/roles/:id", RoleLive.Show, :show live "/admin/roles/:id", RoleLive.Show, :show
live "/admin/roles/:id/edit", RoleLive.Form, :edit live "/admin/roles/:id/edit", RoleLive.Form, :edit
# Datafields (member fields + custom fields)
live "/admin/datafields", DatafieldsLive
# Import (Admin only) # Import (Admin only)
live "/admin/import", ImportLive live "/admin/import", ImportLive
@ -112,7 +112,8 @@ defmodule MvWeb.Router do
auth_routes_prefix: "/auth", auth_routes_prefix: "/auth",
on_mount: [{MvWeb.LiveUserAuth, :live_no_user}], on_mount: [{MvWeb.LiveUserAuth, :live_no_user}],
overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.DaisyUI], overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.DaisyUI],
gettext_backend: {MvWeb.Gettext, "auth"} gettext_backend: {MvWeb.Gettext, "auth"},
live_view: MvWeb.SignInLive
# Remove this if you do not want to use the reset password feature # Remove this if you do not want to use the reset password feature
reset_route auth_routes_prefix: "/auth", reset_route auth_routes_prefix: "/auth",
@ -212,8 +213,8 @@ defmodule MvWeb.Router do
end) end)
end end
# Our supported languages for now are german and english, english as fallback language # Our supported languages: German and English; default German.
defp supported_locale?(locale), do: locale in ["en", "de"] defp supported_locale?(locale), do: locale in ["en", "de"]
defp fallback_locale(nil), do: "en" defp fallback_locale(nil), do: Application.get_env(:mv, :default_locale, "de")
defp fallback_locale(locale), do: locale defp fallback_locale(locale), do: locale
end end

View file

@ -137,11 +137,18 @@ msgid "This OIDC account is already linked to another user. Please contact suppo
msgstr "" msgstr ""
#: lib/mv_web/live/auth/link_oidc_account_live.ex #: lib/mv_web/live/auth/link_oidc_account_live.ex
#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language selection" msgid "Language selection"
msgstr "" msgstr ""
#: lib/mv_web/live/auth/link_oidc_account_live.ex #: lib/mv_web/live/auth/link_oidc_account_live.ex
#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select language" msgid "Select language"
msgstr "" msgstr ""
#: lib/mv_web/auth_overrides.ex
#, elixir-autogen, elixir-format
msgid "or"
msgstr ""

View file

@ -133,11 +133,18 @@ msgid "This OIDC account is already linked to another user. Please contact suppo
msgstr "Dieses OIDC-Konto ist bereits mit einem anderen Benutzer verknüpft. Bitte kontaktiere den Support." msgstr "Dieses OIDC-Konto ist bereits mit einem anderen Benutzer verknüpft. Bitte kontaktiere den Support."
#: lib/mv_web/live/auth/link_oidc_account_live.ex #: lib/mv_web/live/auth/link_oidc_account_live.ex
#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language selection" msgid "Language selection"
msgstr "Sprachauswahl" msgstr "Sprachauswahl"
#: lib/mv_web/live/auth/link_oidc_account_live.ex #: lib/mv_web/live/auth/link_oidc_account_live.ex
#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select language" msgid "Select language"
msgstr "Sprache auswählen" msgstr "Sprache auswählen"
#: lib/mv_web/auth_overrides.ex
#, elixir-autogen, elixir-format
msgid "or"
msgstr "oder"

View file

@ -18,6 +18,7 @@ msgid "Actions"
msgstr "Aktionen" msgstr "Aktionen"
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
@ -322,6 +323,7 @@ msgstr "Benutzer*innen auflisten"
#: lib/mv_web/live/group_live/show.ex #: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_live/index.ex #: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/statistics_live.ex #: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -335,6 +337,7 @@ msgstr "Mitglieder"
#: lib/mv_web/live/group_live/show.ex #: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
@ -382,7 +385,6 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member" msgid "Select member"
msgstr "Mitglied auswählen" msgstr "Mitglied auswählen"
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
@ -842,6 +844,7 @@ msgid "Create Member"
msgstr "Mitglied erstellen" msgstr "Mitglied erstellen"
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -853,11 +856,13 @@ msgstr "Betrag"
msgid "Back to Settings" msgid "Back to Settings"
msgstr "Zurück zu den Einstellungen" msgstr "Zurück zu den Einstellungen"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Can be changed at any time. Amount changes affect future periods only." msgid "Can be changed at any time. Amount changes affect future periods only."
msgstr "Kann jederzeit geändert werden. Änderungen des Betrags betreffen nur zukünftige Zyklen." msgstr "Kann jederzeit geändert werden. Änderungen des Betrags betreffen nur zukünftige Zyklen."
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Deletion" msgid "Deletion"
@ -868,6 +873,7 @@ msgstr "Löschen"
msgid "Examples" msgid "Examples"
msgstr "Beispiele" msgstr "Beispiele"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Fixed after creation. Members can only switch between types with the same interval." msgid "Fixed after creation. Members can only switch between types with the same interval."
@ -886,6 +892,7 @@ msgid "Half-yearly"
msgstr "Halbjährlich" msgstr "Halbjährlich"
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -924,11 +931,13 @@ msgstr "Mitglied zahlt ab dem nächsten vollständigen Jahr"
msgid "Monthly" msgid "Monthly"
msgstr "Monatlich" msgstr "Monatlich"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name & Amount" msgid "Name & Amount"
msgstr "Name & Betrag" msgstr "Name & Betrag"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Only possible if no members are assigned to this type." msgid "Only possible if no members are assigned to this type."
@ -1002,7 +1011,7 @@ msgstr "Alle auswählen"
msgid "Select none" msgid "Select none"
msgstr "Keine auswählen" msgstr "Keine auswählen"
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Slug does not match. Deletion cancelled." msgid "Slug does not match. Deletion cancelled."
msgstr "Eingegebener Text war nicht korrekt. Vorgang abgebrochen." msgstr "Eingegebener Text war nicht korrekt. Vorgang abgebrochen."
@ -1044,11 +1053,6 @@ msgstr "Textfeld"
msgid "Yes/No-Selection" msgid "Yes/No-Selection"
msgstr "Ja/Nein-Auswahl" msgstr "Ja/Nein-Auswahl"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Memberdata"
msgstr "Mitgliederdaten"
#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1060,7 +1064,7 @@ msgstr "Optional"
msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
msgstr "Diese Datenfelder sind für MILA notwendig um Mitglieder zu identifizieren und zukünftig Beitragszahlungen zu berechnen. Aus diesem Grund können sie nicht gelöscht, aber in der Übersicht ausgeblendet werden." msgstr "Diese Datenfelder sind für MILA notwendig um Mitglieder zu identifizieren und zukünftig Beitragszahlungen zu berechnen. Aus diesem Grund können sie nicht gelöscht, aber in der Übersicht ausgeblendet werden."
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member field %{action} successfully" msgid "Member field %{action} successfully"
msgstr "Mitgliedsfeld wurde erfolgreich %{action}" msgstr "Mitgliedsfeld wurde erfolgreich %{action}"
@ -1070,6 +1074,7 @@ msgstr "Mitgliedsfeld wurde erfolgreich %{action}"
msgid "A cycle for this period already exists" msgid "A cycle for this period already exists"
msgstr "Ein Zyklus für diesen Zeitraum existiert bereits" msgstr "Ein Zyklus für diesen Zeitraum existiert bereits"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "About Membership Fee Types" msgid "About Membership Fee Types"
@ -1086,6 +1091,7 @@ msgid "Already paid cycles will remain with the old amount."
msgstr "Bereits bezahlte Zyklen bleiben mit dem alten Betrag." msgstr "Bereits bezahlte Zyklen bleiben mit dem alten Betrag."
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/helpers.ex #: lib/mv_web/live/role_live/helpers.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1097,6 +1103,7 @@ msgstr "Ein Fehler ist aufgetreten"
msgid "Are you sure you want to delete this cycle?" msgid "Are you sure you want to delete this cycle?"
msgstr "Möchtest du diesen Zyklus wirklich löschen?" msgstr "Möchtest du diesen Zyklus wirklich löschen?"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Cannot delete - %{count} member(s) assigned" msgid "Cannot delete - %{count} member(s) assigned"
@ -1117,11 +1124,6 @@ msgstr "Die Änderung des Betrags betrifft %{count} Mitglied(er)."
msgid "Click to edit amount" msgid "Click to edit amount"
msgstr "Klicke, um den Betrag zu bearbeiten" msgstr "Klicke, um den Betrag zu bearbeiten"
#: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Configure global settings for membership fees."
msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren."
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Confirm Change" msgid "Confirm Change"
@ -1232,6 +1234,7 @@ msgstr "Feld bearbeiten: %{field}"
msgid "Edit Membership Fee Type" msgid "Edit Membership Fee Type"
msgstr "Mitgliedsbeitragsart bearbeiten" msgstr "Mitgliedsbeitragsart bearbeiten"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Edit membership fee type" msgid "Edit membership fee type"
@ -1330,6 +1333,7 @@ msgstr "Mitgliedsbeitragsstatus"
msgid "Membership Fee Type" msgid "Membership Fee Type"
msgstr "Mitgliedsbeitragsart" msgstr "Mitgliedsbeitragsart"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Membership Fee Types" msgid "Membership Fee Types"
@ -1346,6 +1350,7 @@ msgstr "Mitgliedsbeiträge"
msgid "Membership fee start" msgid "Membership fee start"
msgstr "Beitragsbeginn" msgstr "Beitragsbeginn"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Membership fee type deleted" msgid "Membership fee type deleted"
@ -1366,6 +1371,7 @@ msgstr "Mitgliedsbeitragsart erfolgreich gespeichert"
msgid "Membership fee type updated. Cycles regenerated." msgid "Membership fee type updated. Cycles regenerated."
msgstr "Mitgliedsbeitragsart aktualisiert. Zyklen regeneriert." msgstr "Mitgliedsbeitragsart aktualisiert. Zyklen regeneriert."
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
@ -1376,6 +1382,7 @@ msgstr "Mitgliedsbeitragsarten definieren verschiedene Mitgliedsbeitragsstruktur
msgid "Monthly Interval - Joining Cycle Included" msgid "Monthly Interval - Joining Cycle Included"
msgstr "Monatliches Intervall Beitrittszeitraum einbezogen" msgstr "Monatliches Intervall Beitrittszeitraum einbezogen"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1550,6 +1557,7 @@ msgstr "Jährliches Intervall Beitrittszeitraum einbezogen"
msgid "You are about to delete all %{count} cycles for this member." msgid "You are about to delete all %{count} cycles for this member."
msgstr "Du bist dabei alle %{count} Zyklen für dieses Mitglied zu löschen." msgstr "Du bist dabei alle %{count} Zyklen für dieses Mitglied zu löschen."
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Delete Membership Fee Type" msgid "Delete Membership Fee Type"
@ -1571,12 +1579,12 @@ msgstr "Spalten ein-/ausblenden"
msgid "Back to settings" msgid "Back to settings"
msgstr "Zurück zu den Einstellungen" msgstr "Zurück zu den Einstellungen"
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Data field %{action} successfully" msgid "Data field %{action} successfully"
msgstr "Datenfeld erfolgreich %{action}" msgstr "Datenfeld erfolgreich %{action}"
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Data field deleted successfully" msgid "Data field deleted successfully"
msgstr "Datenfeld erfolgreich gelöscht" msgstr "Datenfeld erfolgreich gelöscht"
@ -1591,7 +1599,7 @@ msgstr "Datenfeld löschen"
msgid "Edit Data Field" msgid "Edit Data Field"
msgstr "Datenfeld bearbeiten" msgstr "Datenfeld bearbeiten"
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Failed to delete data field: %{error}" msgid "Failed to delete data field: %{error}"
msgstr "Konnte Datenfeld nicht löschen: %{error}" msgstr "Konnte Datenfeld nicht löschen: %{error}"
@ -1823,6 +1831,7 @@ msgstr "Zyklus löschen"
msgid "The cycle period will be calculated based on this date and the interval." msgid "The cycle period will be calculated based on this date and the interval."
msgstr "Der Zyklus wird basierend auf diesem Datum und dem Intervall berechnet." msgstr "Der Zyklus wird basierend auf diesem Datum und dem Intervall berechnet."
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Membership fee type not found" msgid "Membership fee type not found"
@ -1843,6 +1852,7 @@ msgstr "Benutzer*in erfolgreich gelöscht"
msgid "User not found" msgid "User not found"
msgstr "Benutzer*in nicht gefunden" msgstr "Benutzer*in nicht gefunden"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "You do not have permission to access this membership fee type" msgid "You do not have permission to access this membership fee type"
@ -1853,6 +1863,7 @@ msgstr "Du hast keine Berechtigung, auf diese Mitgliedsbeitragsart zuzugreifen"
msgid "You do not have permission to access this user" msgid "You do not have permission to access this user"
msgstr "Du hast keine Berechtigung, auf diese*n Benutzer*in zuzugreifen" msgstr "Du hast keine Berechtigung, auf diese*n Benutzer*in zuzugreifen"
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "You do not have permission to delete this membership fee type" msgid "You do not have permission to delete this membership fee type"
@ -1924,16 +1935,6 @@ msgstr "E-Mail ist erforderlich."
msgid "Roles" msgid "Roles"
msgstr "Rollen" msgstr "Rollen"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Fee Settings"
msgstr "Beitragseinstellungen"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Fee Types"
msgstr "Beitragstypen"
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Administration" msgid "Administration"
@ -2267,7 +2268,7 @@ msgstr "Dieser Benutzer kann nicht angezeigt werden."
msgid "Not authorized." msgid "Not authorized."
msgstr "Nicht berechtigt." msgstr "Nicht berechtigt."
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Could not load data fields. Please check your permissions." msgid "Could not load data fields. Please check your permissions."
msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfe deine Berechtigungen." msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfe deine Berechtigungen."
@ -2413,6 +2414,7 @@ msgstr "Beitragsart auswählen"
msgid "Linked" msgid "Linked"
msgstr "Verknüpft" msgstr "Verknüpft"
#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/user_live/index.html.heex #: lib/mv_web/live/user_live/index.html.heex
#: lib/mv_web/live/user_live/show.ex #: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -2679,6 +2681,61 @@ msgstr "Vereinfacht-Integration"
msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID." msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID."
msgstr "Vereinfacht ist nicht konfiguriert. Bitte setze API-URL, API-Schlüssel und Vereins-ID." msgstr "Vereinfacht ist nicht konfiguriert. Bitte setze API-URL, API-Schlüssel und Vereins-ID."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Test Integration"
msgstr "Integration testen"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Testing..."
msgstr "Wird getestet..."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection successful. API URL, API Key and Club ID are valid."
msgstr "Verbindung erfolgreich. API-URL, API-Schlüssel und Vereins-ID sind korrekt."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Not configured. Please set API URL, API Key and Club ID."
msgstr "Nicht konfiguriert. Bitte API-URL, API-Schlüssel und Vereins-ID setzen."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP %{status}):"
msgstr "Verbindung fehlgeschlagen (HTTP %{status}):"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP 401): API key is invalid or missing."
msgstr "Verbindung fehlgeschlagen (HTTP 401): API-Schlüssel ist ungültig oder fehlt."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
msgstr "Verbindung fehlgeschlagen (HTTP 403): Zugriff verweigert. Bitte Vereins-ID und Berechtigungen des API-Schlüssels prüfen."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
msgstr "Verbindung fehlgeschlagen (HTTP 404): API-Endpunkt nicht gefunden. Bitte die API-URL prüfen (z. B. korrekter Versionspfad)."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
msgstr "Verbindung fehlgeschlagen. Die URL zeigt nicht auf eine Vereinfacht-API (HTML statt JSON erhalten)."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed. Could not reach the API (network error or wrong URL)."
msgstr "Verbindung fehlgeschlagen. API nicht erreichbar (Netzwerkfehler oder falsche URL)."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed. Unknown error."
msgstr "Verbindung fehlgeschlagen. Unbekannter Fehler."
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "View contact in Vereinfacht" msgid "View contact in Vereinfacht"
@ -2942,3 +2999,124 @@ msgstr "Mitglieder importieren (CSV)"
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Datei auswählen" #~ msgid "Datei auswählen"
#~ msgstr "" #~ msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Admin group name"
msgstr "Admin-Gruppenname"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Base URL"
msgstr "Basis-URL"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Basic settings"
msgstr "Grundeinstellungen"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Client ID"
msgstr "Client-ID"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Client Secret"
msgstr "Client-Geheimnis"
#: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Configure global settings and fee types for membership fees."
msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren."
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Configure member fields and custom data fields."
msgstr "Mitgliedsfelder und benutzerdefinierte Datenfelder konfigurieren."
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Custom fields"
msgstr "Benutzerdefinierte Felder"
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Datafields"
msgstr "Datenfelder"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_ADMIN_GROUP_NAME"
msgstr "Aus OIDC_ADMIN_GROUP_NAME"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_BASE_URL"
msgstr "Aus OIDC_BASE_URL"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_CLIENT_ID"
msgstr "Aus OIDC_CLIENT_ID"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_CLIENT_SECRET"
msgstr "Aus OIDC_CLIENT_SECRET"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_GROUPS_CLAIM"
msgstr "Aus OIDC_GROUPS_CLAIM"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_REDIRECT_URI"
msgstr "Aus OIDC_REDIRECT_URI"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Groups claim"
msgstr "Gruppenclaim"
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Member fields"
msgstr "Mitgliedsfelder"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee settings"
msgstr "Beitragseinstellungen"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Redirect URI"
msgstr "Weiterleitungs-URI"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Save OIDC Settings"
msgstr "OIDC-Einstellungen speichern"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "e.g. admin"
msgstr "z. B. admin"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_ONLY"
msgstr "Aus OIDC_ONLY"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Only OIDC sign-in (hide password login)"
msgstr "Nur OIDC-Anmeldung (Passwort-Login ausblenden)"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button."
msgstr "Wenn aktiviert und OIDC konfiguriert ist, zeigt die Anmeldeseite nur den Single-Sign-On-Button."

View file

@ -19,6 +19,7 @@ msgid "Actions"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
@ -323,6 +324,7 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex #: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_live/index.ex #: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/statistics_live.ex #: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -336,6 +338,7 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex #: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
@ -383,7 +386,6 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
@ -843,6 +845,7 @@ msgid "Create Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -854,11 +857,13 @@ msgstr ""
msgid "Back to Settings" msgid "Back to Settings"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Can be changed at any time. Amount changes affect future periods only." msgid "Can be changed at any time. Amount changes affect future periods only."
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Deletion" msgid "Deletion"
@ -869,6 +874,7 @@ msgstr ""
msgid "Examples" msgid "Examples"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Fixed after creation. Members can only switch between types with the same interval." msgid "Fixed after creation. Members can only switch between types with the same interval."
@ -887,6 +893,7 @@ msgid "Half-yearly"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -925,11 +932,13 @@ msgstr ""
msgid "Monthly" msgid "Monthly"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name & Amount" msgid "Name & Amount"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Only possible if no members are assigned to this type." msgid "Only possible if no members are assigned to this type."
@ -1003,7 +1012,7 @@ msgstr ""
msgid "Select none" msgid "Select none"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Slug does not match. Deletion cancelled." msgid "Slug does not match. Deletion cancelled."
msgstr "" msgstr ""
@ -1045,11 +1054,6 @@ msgstr ""
msgid "Yes/No-Selection" msgid "Yes/No-Selection"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Memberdata"
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1061,7 +1065,7 @@ msgstr ""
msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member field %{action} successfully" msgid "Member field %{action} successfully"
msgstr "" msgstr ""
@ -1071,6 +1075,7 @@ msgstr ""
msgid "A cycle for this period already exists" msgid "A cycle for this period already exists"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "About Membership Fee Types" msgid "About Membership Fee Types"
@ -1087,6 +1092,7 @@ msgid "Already paid cycles will remain with the old amount."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/helpers.ex #: lib/mv_web/live/role_live/helpers.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1098,6 +1104,7 @@ msgstr ""
msgid "Are you sure you want to delete this cycle?" msgid "Are you sure you want to delete this cycle?"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Cannot delete - %{count} member(s) assigned" msgid "Cannot delete - %{count} member(s) assigned"
@ -1118,11 +1125,6 @@ msgstr ""
msgid "Click to edit amount" msgid "Click to edit amount"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Configure global settings for membership fees."
msgstr ""
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Confirm Change" msgid "Confirm Change"
@ -1233,6 +1235,7 @@ msgstr ""
msgid "Edit Membership Fee Type" msgid "Edit Membership Fee Type"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit membership fee type" msgid "Edit membership fee type"
@ -1331,6 +1334,7 @@ msgstr ""
msgid "Membership Fee Type" msgid "Membership Fee Type"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Membership Fee Types" msgid "Membership Fee Types"
@ -1347,6 +1351,7 @@ msgstr ""
msgid "Membership fee start" msgid "Membership fee start"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Membership fee type deleted" msgid "Membership fee type deleted"
@ -1367,6 +1372,7 @@ msgstr ""
msgid "Membership fee type updated. Cycles regenerated." msgid "Membership fee type updated. Cycles regenerated."
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
@ -1377,6 +1383,7 @@ msgstr ""
msgid "Monthly Interval - Joining Cycle Included" msgid "Monthly Interval - Joining Cycle Included"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1551,6 +1558,7 @@ msgstr ""
msgid "You are about to delete all %{count} cycles for this member." msgid "You are about to delete all %{count} cycles for this member."
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete Membership Fee Type" msgid "Delete Membership Fee Type"
@ -1572,12 +1580,12 @@ msgstr ""
msgid "Back to settings" msgid "Back to settings"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Data field %{action} successfully" msgid "Data field %{action} successfully"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Data field deleted successfully" msgid "Data field deleted successfully"
msgstr "" msgstr ""
@ -1592,7 +1600,7 @@ msgstr ""
msgid "Edit Data Field" msgid "Edit Data Field"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to delete data field: %{error}" msgid "Failed to delete data field: %{error}"
msgstr "" msgstr ""
@ -1824,6 +1832,7 @@ msgstr ""
msgid "The cycle period will be calculated based on this date and the interval." msgid "The cycle period will be calculated based on this date and the interval."
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Membership fee type not found" msgid "Membership fee type not found"
@ -1844,6 +1853,7 @@ msgstr ""
msgid "User not found" msgid "User not found"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "You do not have permission to access this membership fee type" msgid "You do not have permission to access this membership fee type"
@ -1854,6 +1864,7 @@ msgstr ""
msgid "You do not have permission to access this user" msgid "You do not have permission to access this user"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "You do not have permission to delete this membership fee type" msgid "You do not have permission to delete this membership fee type"
@ -1925,16 +1936,6 @@ msgstr ""
msgid "Roles" msgid "Roles"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Fee Settings"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Fee Types"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Administration" msgid "Administration"
@ -2268,7 +2269,7 @@ msgstr ""
msgid "Not authorized." msgid "Not authorized."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Could not load data fields. Please check your permissions." msgid "Could not load data fields. Please check your permissions."
msgstr "" msgstr ""
@ -2414,6 +2415,7 @@ msgstr ""
msgid "Linked" msgid "Linked"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/user_live/index.html.heex #: lib/mv_web/live/user_live/index.html.heex
#: lib/mv_web/live/user_live/show.ex #: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -2680,6 +2682,61 @@ msgstr ""
msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID." msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Test Integration"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Testing..."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection successful. API URL, API Key and Club ID are valid."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Not configured. Please set API URL, API Key and Club ID."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP %{status}):"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP 401): API key is invalid or missing."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed. Could not reach the API (network error or wrong URL)."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed. Unknown error."
msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "View contact in Vereinfacht" msgid "View contact in Vereinfacht"
@ -2937,3 +2994,124 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import Members" msgid "Import Members"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Admin group name"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Base URL"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Basic settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Client ID"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Client Secret"
msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Configure global settings and fee types for membership fees."
msgstr ""
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Configure member fields and custom data fields."
msgstr ""
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Custom fields"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Datafields"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_ADMIN_GROUP_NAME"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_BASE_URL"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_CLIENT_ID"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_CLIENT_SECRET"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_GROUPS_CLAIM"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_REDIRECT_URI"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Groups claim"
msgstr ""
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Member fields"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Membership fee settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Redirect URI"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Save OIDC Settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "e.g. admin"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_ONLY"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Only OIDC sign-in (hide password login)"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button."
msgstr ""

View file

@ -130,11 +130,18 @@ msgid "This OIDC account is already linked to another user. Please contact suppo
msgstr "" msgstr ""
#: lib/mv_web/live/auth/link_oidc_account_live.ex #: lib/mv_web/live/auth/link_oidc_account_live.ex
#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language selection" msgid "Language selection"
msgstr "" msgstr ""
#: lib/mv_web/live/auth/link_oidc_account_live.ex #: lib/mv_web/live/auth/link_oidc_account_live.ex
#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select language" msgid "Select language"
msgstr "" msgstr ""
#: lib/mv_web/auth_overrides.ex
#, elixir-autogen, elixir-format
msgid "or"
msgstr "or"

View file

@ -19,6 +19,7 @@ msgid "Actions"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/index.html.heex #: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/role_live/show.ex
@ -323,6 +324,7 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex #: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_live/index.ex #: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/statistics_live.ex #: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -336,6 +338,7 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex #: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/form.ex
@ -383,7 +386,6 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
@ -843,6 +845,7 @@ msgid "Create Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -854,11 +857,13 @@ msgstr ""
msgid "Back to Settings" msgid "Back to Settings"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Can be changed at any time. Amount changes affect future periods only." msgid "Can be changed at any time. Amount changes affect future periods only."
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Deletion" msgid "Deletion"
@ -869,6 +874,7 @@ msgstr ""
msgid "Examples" msgid "Examples"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Fixed after creation. Members can only switch between types with the same interval." msgid "Fixed after creation. Members can only switch between types with the same interval."
@ -887,6 +893,7 @@ msgid "Half-yearly"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -925,11 +932,13 @@ msgstr ""
msgid "Monthly" msgid "Monthly"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name & Amount" msgid "Name & Amount"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Only possible if no members are assigned to this type." msgid "Only possible if no members are assigned to this type."
@ -1003,7 +1012,7 @@ msgstr ""
msgid "Select none" msgid "Select none"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Slug does not match. Deletion cancelled." msgid "Slug does not match. Deletion cancelled."
msgstr "" msgstr ""
@ -1045,11 +1054,6 @@ msgstr ""
msgid "Yes/No-Selection" msgid "Yes/No-Selection"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Memberdata"
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
@ -1061,7 +1065,7 @@ msgstr ""
msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Member field %{action} successfully" msgid "Member field %{action} successfully"
msgstr "" msgstr ""
@ -1071,6 +1075,7 @@ msgstr ""
msgid "A cycle for this period already exists" msgid "A cycle for this period already exists"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "About Membership Fee Types" msgid "About Membership Fee Types"
@ -1087,6 +1092,7 @@ msgid "Already paid cycles will remain with the old amount."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/helpers.ex #: lib/mv_web/live/role_live/helpers.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1098,6 +1104,7 @@ msgstr ""
msgid "Are you sure you want to delete this cycle?" msgid "Are you sure you want to delete this cycle?"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Cannot delete - %{count} member(s) assigned" msgid "Cannot delete - %{count} member(s) assigned"
@ -1118,11 +1125,6 @@ msgstr ""
msgid "Click to edit amount" msgid "Click to edit amount"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Configure global settings for membership fees."
msgstr ""
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Confirm Change" msgid "Confirm Change"
@ -1233,6 +1235,7 @@ msgstr ""
msgid "Edit Membership Fee Type" msgid "Edit Membership Fee Type"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Edit membership fee type" msgid "Edit membership fee type"
@ -1331,6 +1334,7 @@ msgstr ""
msgid "Membership Fee Type" msgid "Membership Fee Type"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Membership Fee Types" msgid "Membership Fee Types"
@ -1347,6 +1351,7 @@ msgstr ""
msgid "Membership fee start" msgid "Membership fee start"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee type deleted" msgid "Membership fee type deleted"
@ -1367,6 +1372,7 @@ msgstr ""
msgid "Membership fee type updated. Cycles regenerated." msgid "Membership fee type updated. Cycles regenerated."
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
@ -1377,6 +1383,7 @@ msgstr ""
msgid "Monthly Interval - Joining Cycle Included" msgid "Monthly Interval - Joining Cycle Included"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
@ -1551,6 +1558,7 @@ msgstr ""
msgid "You are about to delete all %{count} cycles for this member." msgid "You are about to delete all %{count} cycles for this member."
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Delete Membership Fee Type" msgid "Delete Membership Fee Type"
@ -1572,12 +1580,12 @@ msgstr ""
msgid "Back to settings" msgid "Back to settings"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Data field %{action} successfully" msgid "Data field %{action} successfully"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Data field deleted successfully" msgid "Data field deleted successfully"
msgstr "" msgstr ""
@ -1592,7 +1600,7 @@ msgstr ""
msgid "Edit Data Field" msgid "Edit Data Field"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Failed to delete data field: %{error}" msgid "Failed to delete data field: %{error}"
msgstr "" msgstr ""
@ -1824,6 +1832,7 @@ msgstr ""
msgid "The cycle period will be calculated based on this date and the interval." msgid "The cycle period will be calculated based on this date and the interval."
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee type not found" msgid "Membership fee type not found"
@ -1844,6 +1853,7 @@ msgstr ""
msgid "User not found" msgid "User not found"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "You do not have permission to access this membership fee type" msgid "You do not have permission to access this membership fee type"
@ -1854,6 +1864,7 @@ msgstr ""
msgid "You do not have permission to access this user" msgid "You do not have permission to access this user"
msgstr "" msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "You do not have permission to delete this membership fee type" msgid "You do not have permission to delete this membership fee type"
@ -1925,16 +1936,6 @@ msgstr ""
msgid "Roles" msgid "Roles"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Fee Settings"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Fee Types"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Administration" msgid "Administration"
@ -2268,7 +2269,7 @@ msgstr ""
msgid "Not authorized." msgid "Not authorized."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Could not load data fields. Please check your permissions." msgid "Could not load data fields. Please check your permissions."
msgstr "" msgstr ""
@ -2414,6 +2415,7 @@ msgstr ""
msgid "Linked" msgid "Linked"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/user_live/index.html.heex #: lib/mv_web/live/user_live/index.html.heex
#: lib/mv_web/live/user_live/show.ex #: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -2680,6 +2682,61 @@ msgstr ""
msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID." msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Test Integration"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Testing..."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection successful. API URL, API Key and Club ID are valid."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Not configured. Please set API URL, API Key and Club ID."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP %{status}):"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP 401): API key is invalid or missing."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed. Could not reach the API (network error or wrong URL)."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Connection failed. Unknown error."
msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "View contact in Vereinfacht" msgid "View contact in Vereinfacht"
@ -2937,3 +2994,124 @@ msgstr ""
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Import Members" msgid "Import Members"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Admin group name"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Base URL"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Basic settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Client ID"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Client Secret"
msgstr ""
#: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Configure global settings and fee types for membership fees."
msgstr ""
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Configure member fields and custom data fields."
msgstr ""
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Custom fields"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Datafields"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_ADMIN_GROUP_NAME"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_BASE_URL"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_CLIENT_ID"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_CLIENT_SECRET"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_GROUPS_CLAIM"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "From OIDC_REDIRECT_URI"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Groups claim"
msgstr ""
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Member fields"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Redirect URI"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Save OIDC Settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "e.g. admin"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "From OIDC_ONLY"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Only OIDC sign-in (hide password login)"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button."
msgstr ""

View file

@ -0,0 +1,29 @@
defmodule Mv.Repo.Migrations.AddOidcToSettings do
@moduledoc """
Adds OIDC configuration columns to settings (ENV-overridable in UI).
"""
use Ecto.Migration
def up do
alter table(:settings) do
add :oidc_client_id, :string
add :oidc_base_url, :string
add :oidc_redirect_uri, :string
add :oidc_client_secret, :string
add :oidc_admin_group_name, :string
add :oidc_groups_claim, :string
end
end
def down do
alter table(:settings) do
remove :oidc_client_id
remove :oidc_base_url
remove :oidc_redirect_uri
remove :oidc_client_secret
remove :oidc_admin_group_name
remove :oidc_groups_claim
end
end
end

View file

@ -0,0 +1,20 @@
defmodule Mv.Repo.Migrations.AddOidcOnlyToSettings do
@moduledoc """
Adds oidc_only flag to settings. When true and OIDC is configured,
the sign-in page shows only OIDC (password login is hidden).
"""
use Ecto.Migration
def up do
alter table(:settings) do
add :oidc_only, :boolean, default: false, null: false
end
end
def down do
alter table(:settings) do
remove :oidc_only
end
end
end

View file

@ -64,4 +64,4 @@
"repo": "Elixir.Mv.Repo", "repo": "Elixir.Mv.Repo",
"schema": null, "schema": null,
"table": "settings" "table": "settings"
} }

View file

@ -76,4 +76,4 @@
"repo": "Elixir.Mv.Repo", "repo": "Elixir.Mv.Repo",
"schema": null, "schema": null,
"table": "settings" "table": "settings"
} }

View file

@ -100,4 +100,4 @@
"repo": "Elixir.Mv.Repo", "repo": "Elixir.Mv.Repo",
"schema": null, "schema": null,
"table": "settings" "table": "settings"
} }

View file

@ -137,4 +137,4 @@
"repo": "Elixir.Mv.Repo", "repo": "Elixir.Mv.Repo",
"schema": null, "schema": null,
"table": "settings" "table": "settings"
} }

View file

@ -0,0 +1,164 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "club_name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "member_field_visibility",
"type": "map"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "member_field_required",
"type": "map"
},
{
"allow_nil?": false,
"default": "true",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "include_joining_cycle",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "default_membership_fee_type_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "vereinfacht_api_url",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "vereinfacht_api_key",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "vereinfacht_club_id",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "vereinfacht_app_url",
"type": "text"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
}
],
"base_filter": null,
"check_constraints": [],
"create_table_options": null,
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "C84FC81A2A446451D6B5EA72F9BBB3593CD7F0D71C4B7C9CE04934414FDB52EB",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "settings"
}

View file

@ -1,21 +1,22 @@
defmodule Mv.OidcRoleSyncConfigTest do defmodule Mv.OidcRoleSyncConfigTest do
@moduledoc """ @moduledoc """
Tests for OIDC role sync configuration (OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM). Tests for OIDC role sync configuration (OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM).
Reads via Mv.Config (ENV first, then Settings).
""" """
use ExUnit.Case, async: false use Mv.DataCase, async: false
alias Mv.OidcRoleSyncConfig alias Mv.OidcRoleSyncConfig
describe "oidc_admin_group_name/0" do describe "oidc_admin_group_name/0" do
test "returns nil when OIDC_ADMIN_GROUP_NAME is not configured" do test "returns nil when OIDC_ADMIN_GROUP_NAME is not configured" do
restore = put_config(admin_group_name: nil) restore = clear_env("OIDC_ADMIN_GROUP_NAME")
on_exit(restore) on_exit(restore)
assert OidcRoleSyncConfig.oidc_admin_group_name() == nil assert OidcRoleSyncConfig.oidc_admin_group_name() == nil
end end
test "returns configured admin group name when set" do test "returns configured admin group name when set via ENV" do
restore = put_config(admin_group_name: "mila-admin") restore = set_env("OIDC_ADMIN_GROUP_NAME", "mila-admin")
on_exit(restore) on_exit(restore)
assert OidcRoleSyncConfig.oidc_admin_group_name() == "mila-admin" assert OidcRoleSyncConfig.oidc_admin_group_name() == "mila-admin"
@ -24,26 +25,35 @@ defmodule Mv.OidcRoleSyncConfigTest do
describe "oidc_groups_claim/0" do describe "oidc_groups_claim/0" do
test "returns default \"groups\" when OIDC_GROUPS_CLAIM is not configured" do test "returns default \"groups\" when OIDC_GROUPS_CLAIM is not configured" do
restore = put_config(groups_claim: nil) restore = clear_env("OIDC_GROUPS_CLAIM")
on_exit(restore) on_exit(restore)
assert OidcRoleSyncConfig.oidc_groups_claim() == "groups" assert OidcRoleSyncConfig.oidc_groups_claim() == "groups"
end end
test "returns configured claim name when OIDC_GROUPS_CLAIM is set" do test "returns configured claim name when OIDC_GROUPS_CLAIM is set via ENV" do
restore = put_config(groups_claim: "ak_groups") restore = set_env("OIDC_GROUPS_CLAIM", "ak_groups")
on_exit(restore) on_exit(restore)
assert OidcRoleSyncConfig.oidc_groups_claim() == "ak_groups" assert OidcRoleSyncConfig.oidc_groups_claim() == "ak_groups"
end end
end end
defp put_config(opts) do defp set_env(key, value) do
current = Application.get_env(:mv, :oidc_role_sync, []) previous = System.get_env(key)
Application.put_env(:mv, :oidc_role_sync, Keyword.merge(current, opts)) System.put_env(key, value)
fn -> fn ->
Application.put_env(:mv, :oidc_role_sync, current) if previous, do: System.put_env(key, previous), else: System.delete_env(key)
end
end
defp clear_env(key) do
previous = System.get_env(key)
System.delete_env(key)
fn ->
if previous, do: System.put_env(key, previous)
end end
end end
end end

View file

@ -12,14 +12,14 @@ defmodule Mv.OidcRoleSyncTest do
setup do setup do
ensure_roles_exist() ensure_roles_exist()
restore_config = put_oidc_config(admin_group_name: "mila-admin", groups_claim: "groups") restore_config = put_oidc_env(admin_group_name: "mila-admin", groups_claim: "groups")
on_exit(restore_config) on_exit(restore_config)
:ok :ok
end end
describe "apply_admin_role_from_user_info/2" do describe "apply_admin_role_from_user_info/2" do
test "when OIDC_ADMIN_GROUP_NAME not configured: does not change user (Mitglied stays)" do test "when OIDC_ADMIN_GROUP_NAME not configured: does not change user (Mitglied stays)" do
restore = put_oidc_config(admin_group_name: nil, groups_claim: "groups") restore = put_oidc_env(admin_group_name: nil, groups_claim: "groups")
on_exit(restore) on_exit(restore)
email = "sync-no-config-#{System.unique_integer([:positive])}@test.example.com" email = "sync-no-config-#{System.unique_integer([:positive])}@test.example.com"
@ -58,7 +58,7 @@ defmodule Mv.OidcRoleSyncTest do
end end
test "when OIDC_GROUPS_CLAIM is different: reads groups from that claim" do test "when OIDC_GROUPS_CLAIM is different: reads groups from that claim" do
restore = put_oidc_config(admin_group_name: "mila-admin", groups_claim: "ak_groups") restore = put_oidc_env(admin_group_name: "mila-admin", groups_claim: "ak_groups")
on_exit(restore) on_exit(restore)
email = "sync-claim-#{System.unique_integer([:positive])}@test.example.com" email = "sync-claim-#{System.unique_integer([:positive])}@test.example.com"
@ -131,13 +131,30 @@ defmodule Mv.OidcRoleSyncTest do
end end
end end
defp put_oidc_config(opts) do defp put_oidc_env(opts) do
current = Application.get_env(:mv, :oidc_role_sync, []) prev_admin = System.get_env("OIDC_ADMIN_GROUP_NAME")
merged = Keyword.merge(current, opts) prev_claim = System.get_env("OIDC_GROUPS_CLAIM")
Application.put_env(:mv, :oidc_role_sync, merged)
if opts[:admin_group_name] != nil do
System.put_env("OIDC_ADMIN_GROUP_NAME", to_string(opts[:admin_group_name]))
else
System.delete_env("OIDC_ADMIN_GROUP_NAME")
end
if opts[:groups_claim] != nil do
System.put_env("OIDC_GROUPS_CLAIM", to_string(opts[:groups_claim]))
else
System.delete_env("OIDC_GROUPS_CLAIM")
end
fn -> fn ->
Application.put_env(:mv, :oidc_role_sync, current) if prev_admin,
do: System.put_env("OIDC_ADMIN_GROUP_NAME", prev_admin),
else: System.delete_env("OIDC_ADMIN_GROUP_NAME")
if prev_claim,
do: System.put_env("OIDC_GROUPS_CLAIM", prev_claim),
else: System.delete_env("OIDC_GROUPS_CLAIM")
end end
end end

View file

@ -30,7 +30,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
html = render_sidebar(sidebar_assigns(user)) html = render_sidebar(sidebar_assigns(user))
assert html =~ ~s(href="/members") assert html =~ ~s(href="/members")
assert html =~ ~s(href="/membership_fee_types") assert html =~ ~s(href="/membership_fee_settings")
assert html =~ ~s(href="/statistics") assert html =~ ~s(href="/statistics")
assert html =~ ~s(data-testid="sidebar-administration") assert html =~ ~s(data-testid="sidebar-administration")
assert html =~ ~s(href="/users") assert html =~ ~s(href="/users")
@ -55,7 +55,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
user = Fixtures.user_with_role_fixture("read_only") user = Fixtures.user_with_role_fixture("read_only")
html = render_sidebar(sidebar_assigns(user)) html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/membership_fee_types") refute html =~ ~s(href="/membership_fee_settings")
refute html =~ ~s(href="/users") refute html =~ ~s(href="/users")
refute html =~ ~s(href="/admin/roles") refute html =~ ~s(href="/admin/roles")
refute html =~ ~s(href="/settings") refute html =~ ~s(href="/settings")
@ -76,7 +76,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
user = Fixtures.user_with_role_fixture("normal_user") user = Fixtures.user_with_role_fixture("normal_user")
html = render_sidebar(sidebar_assigns(user)) html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/membership_fee_types") refute html =~ ~s(href="/membership_fee_settings")
refute html =~ ~s(href="/users") refute html =~ ~s(href="/users")
refute html =~ ~s(href="/admin/roles") refute html =~ ~s(href="/admin/roles")
refute html =~ ~s(href="/settings") refute html =~ ~s(href="/settings")
@ -96,7 +96,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
html = render_sidebar(sidebar_assigns(user)) html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/statistics") refute html =~ ~s(href="/statistics")
refute html =~ ~s(href="/membership_fee_types") refute html =~ ~s(href="/membership_fee_settings")
refute html =~ ~s(href="/users") refute html =~ ~s(href="/users")
refute html =~ ~s(data-testid="sidebar-administration") refute html =~ ~s(data-testid="sidebar-administration")
end end
@ -117,7 +117,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
html = render_sidebar(sidebar_assigns(user)) html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/members") refute html =~ ~s(href="/members")
refute html =~ ~s(href="/membership_fee_types") refute html =~ ~s(href="/membership_fee_settings")
refute html =~ ~s(href="/users") refute html =~ ~s(href="/users")
end end
end end

View file

@ -54,7 +54,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
# Create custom field value # Create custom field value
create_custom_field_value(member, custom_field, "test") create_custom_field_value(member, custom_field, "test")
{:ok, view, _html} = live(conn, ~p"/settings") {:ok, view, _html} = live(conn, ~p"/admin/datafields")
# Click delete button - find the delete link within the component # Click delete button - find the delete link within the component
view view
@ -80,7 +80,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
create_custom_field_value(member1, custom_field, "test1") create_custom_field_value(member1, custom_field, "test1")
create_custom_field_value(member2, custom_field, "test2") create_custom_field_value(member2, custom_field, "test2")
{:ok, view, _html} = live(conn, ~p"/settings") {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view view
|> element("#custom-fields-component a", "Delete") |> element("#custom-fields-component a", "Delete")
@ -93,7 +93,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
test "shows 0 members for custom field without values", %{conn: conn} do test "shows 0 members for custom field without values", %{conn: conn} do
{:ok, _custom_field} = create_custom_field("test_field", :string) {:ok, _custom_field} = create_custom_field("test_field", :string)
{:ok, view, _html} = live(conn, ~p"/settings") {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view view
|> element("#custom-fields-component a", "Delete") |> element("#custom-fields-component a", "Delete")
@ -108,7 +108,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
test "updates confirmation state when typing", %{conn: conn} do test "updates confirmation state when typing", %{conn: conn} do
{:ok, custom_field} = create_custom_field("test_field", :string) {:ok, custom_field} = create_custom_field("test_field", :string)
{:ok, view, _html} = live(conn, ~p"/settings") {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view view
|> element("#custom-fields-component a", "Delete") |> element("#custom-fields-component a", "Delete")
@ -126,7 +126,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
test "delete button is disabled when slug doesn't match", %{conn: conn} do test "delete button is disabled when slug doesn't match", %{conn: conn} do
{:ok, _custom_field} = create_custom_field("test_field", :string) {:ok, _custom_field} = create_custom_field("test_field", :string)
{:ok, view, _html} = live(conn, ~p"/settings") {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view view
|> element("#custom-fields-component a", "Delete") |> element("#custom-fields-component a", "Delete")
@ -148,7 +148,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
{:ok, custom_field} = create_custom_field("test_field", :string) {:ok, custom_field} = create_custom_field("test_field", :string)
{:ok, custom_field_value} = create_custom_field_value(member, custom_field, "test") {:ok, custom_field_value} = create_custom_field_value(member, custom_field, "test")
{:ok, view, _html} = live(conn, ~p"/settings") {:ok, view, _html} = live(conn, ~p"/admin/datafields")
# Open modal # Open modal
view view
@ -185,7 +185,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
} do } do
{:ok, custom_field} = create_custom_field("test_field", :string) {:ok, custom_field} = create_custom_field("test_field", :string)
{:ok, view, _html} = live(conn, ~p"/settings") {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view view
|> element("#custom-fields-component a", "Delete") |> element("#custom-fields-component a", "Delete")
@ -209,7 +209,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
test "closes modal without deleting", %{conn: conn} do test "closes modal without deleting", %{conn: conn} do
{:ok, custom_field} = create_custom_field("test_field", :string) {:ok, custom_field} = create_custom_field("test_field", :string)
{:ok, view, _html} = live(conn, ~p"/settings") {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view view
|> element("#custom-fields-component a", "Delete") |> element("#custom-fields-component a", "Delete")
@ -234,7 +234,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
describe "create custom field" do describe "create custom field" do
test "submitting new data field form creates custom field and shows success", %{conn: conn} do test "submitting new data field form creates custom field and shows success", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings") {:ok, view, _html} = live(conn, ~p"/admin/datafields")
# Open "New Data Field" form # Open "New Data Field" form
view view

View file

@ -64,21 +64,5 @@ defmodule MvWeb.GlobalSettingsLiveTest do
assert html =~ "must be present" assert html =~ "must be present"
end end
test "displays Memberdata section", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ "Memberdata" or html =~ "Member Data"
end
test "displays flash message after member field visibility update", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# Simulate member field visibility update
send(view.pid, {:member_field_visibility_updated})
# Check for flash message
assert render(view) =~ "updated" or render(view) =~ "success"
end
end end
end end

View file

@ -23,7 +23,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
describe "rendering" do describe "rendering" do
test "renders all member fields from Constants", %{conn: conn} do test "renders all member fields from Constants", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings") {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Check that all member fields are displayed # Check that all member fields are displayed
member_fields = Mv.Constants.member_fields() member_fields = Mv.Constants.member_fields()
@ -36,7 +36,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end end
test "displays show_in_overview status as badge", %{conn: conn} do test "displays show_in_overview status as badge", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings") {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Should have "Show in overview" column header # Should have "Show in overview" column header
assert html =~ "Show in overview" or html =~ "Show in Overview" assert html =~ "Show in overview" or html =~ "Show in Overview"
@ -46,7 +46,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end end
test "displays required status column", %{conn: conn} do test "displays required status column", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings") {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Should have "Required" column; email is always required # Should have "Required" column; email is always required
assert html =~ "Required" or html =~ "required" assert html =~ "Required" or html =~ "required"
@ -59,7 +59,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
{:ok, _updated} = {:ok, _updated} =
Membership.update_settings(settings, %{member_field_visibility: %{}}) Membership.update_settings(settings, %{member_field_visibility: %{}})
{:ok, _view, html} = live(conn, ~p"/settings") {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# All fields should show as visible (Yes) by default # All fields should show as visible (Yes) by default
# Check for "Yes" badge or similar indicator # Check for "Yes" badge or similar indicator
@ -74,7 +74,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
{:ok, _updated} = {:ok, _updated} =
Membership.update_member_field_visibility(settings, visibility_config) Membership.update_member_field_visibility(settings, visibility_config)
{:ok, _view, html} = live(conn, ~p"/settings") {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Street and house_number should show as hidden (No) # Street and house_number should show as hidden (No)
# Other fields should show as visible (Yes) # Other fields should show as visible (Yes)
@ -102,7 +102,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end end
test "marks email as required (always from settings)", %{conn: conn} do test "marks email as required (always from settings)", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings") {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Email is always required # Email is always required
assert html =~ "email" or html =~ "Email" assert html =~ "email" or html =~ "Email"
@ -119,7 +119,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
required: true required: true
) )
{:ok, _view, html} = live(conn, ~p"/settings") {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# First name row should show Required (and Optional for others) # First name row should show Required (and Optional for others)
assert html =~ "First name" or html =~ "first_name" assert html =~ "First name" or html =~ "first_name"
@ -127,7 +127,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end end
test "optional fields show Optional when not required in settings", %{conn: conn} do test "optional fields show Optional when not required in settings", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings") {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Email is required; other fields default to optional # Email is required; other fields default to optional
assert html =~ "Optional" assert html =~ "Optional"

View file

@ -11,7 +11,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
require Ash.Query require Ash.Query
setup %{conn: conn} do setup %{conn: conn} do
# User must have admin role (or normal_user) to access /membership_fee_types pages # User must have admin role (or normal_user) to access /membership_fee_settings pages
user = Mv.Fixtures.user_with_role_fixture("admin") user = Mv.Fixtures.user_with_role_fixture("admin")
authenticated_conn = conn_with_password_user(conn, user) authenticated_conn = conn_with_password_user(conn, user)
%{conn: authenticated_conn, user: user} %{conn: authenticated_conn, user: user}
@ -51,7 +51,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
describe "create form" do describe "create form" do
test "creates new membership fee type", %{conn: conn, user: user} do test "creates new membership fee type", %{conn: conn, user: user} do
{:ok, view, _html} = live(conn, "/membership_fee_types/new") {:ok, view, _html} = live(conn, "/membership_fee_settings/new_fee_type")
form_data = %{ form_data = %{
"membership_fee_type[name]" => "New Type", "membership_fee_type[name]" => "New Type",
@ -65,7 +65,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|> form("#membership-fee-type-form", form_data) |> form("#membership-fee-type-form", form_data)
|> render_submit() |> render_submit()
assert to == "/membership_fee_types" assert to == "/membership_fee_settings"
# Verify type was created (use actor so read is authorized) # Verify type was created (use actor so read is authorized)
type = type =
@ -79,7 +79,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
end end
test "interval field is editable on create", %{conn: conn} do test "interval field is editable on create", %{conn: conn} do
{:ok, _view, html} = live(conn, "/membership_fee_types/new") {:ok, _view, html} = live(conn, "/membership_fee_settings/new_fee_type")
# Interval field should be editable (not disabled) # Interval field should be editable (not disabled)
refute html =~ "disabled" || html =~ "readonly" refute html =~ "disabled" || html =~ "readonly"
@ -90,7 +90,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
test "loads existing type data", %{conn: conn} do test "loads existing type data", %{conn: conn} do
fee_type = create_fee_type(%{name: "Existing Type", amount: Decimal.new("60.00")}) fee_type = create_fee_type(%{name: "Existing Type", amount: Decimal.new("60.00")})
{:ok, _view, html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit") {:ok, _view, html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
assert html =~ "Existing Type" assert html =~ "Existing Type"
assert html =~ "60" || html =~ "60,00" assert html =~ "60" || html =~ "60,00"
@ -99,7 +99,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
test "interval field is grayed out on edit", %{conn: conn} do test "interval field is grayed out on edit", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly}) fee_type = create_fee_type(%{interval: :yearly})
{:ok, _view, html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit") {:ok, _view, html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Interval field should be disabled # Interval field should be disabled
assert html =~ "disabled" || html =~ "readonly" assert html =~ "disabled" || html =~ "readonly"
@ -109,7 +109,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
fee_type = create_fee_type(%{amount: Decimal.new("50.00")}) fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
create_member(%{membership_fee_type_id: fee_type.id}) create_member(%{membership_fee_type_id: fee_type.id})
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit") {:ok, view, _html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Change amount # Change amount
view view
@ -129,7 +129,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
create_member(%{membership_fee_type_id: fee_type.id}) create_member(%{membership_fee_type_id: fee_type.id})
end) end)
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit") {:ok, view, _html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Change amount # Change amount
html = html =
@ -144,7 +144,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
test "amount change can be confirmed", %{conn: conn, user: user} do test "amount change can be confirmed", %{conn: conn, user: user} do
fee_type = create_fee_type(%{amount: Decimal.new("50.00")}) fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit") {:ok, view, _html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Change amount and confirm # Change amount and confirm
view view
@ -173,7 +173,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
test "amount change can be cancelled", %{conn: conn, user: user} do test "amount change can be cancelled", %{conn: conn, user: user} do
fee_type = create_fee_type(%{amount: Decimal.new("50.00")}) fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
{:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit") {:ok, view, _html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Change amount and cancel # Change amount and cancel
view view
@ -195,7 +195,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
end end
test "validation errors display correctly", %{conn: conn} do test "validation errors display correctly", %{conn: conn} do
{:ok, view, _html} = live(conn, "/membership_fee_types/new") {:ok, view, _html} = live(conn, "/membership_fee_settings/new_fee_type")
# Submit with invalid data # Submit with invalid data
html = html =
@ -214,7 +214,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
describe "permissions" do describe "permissions" do
test "only admin can access", %{conn: conn} do test "only admin can access", %{conn: conn} do
# This test assumes non-admin users cannot access # This test assumes non-admin users cannot access
{:ok, _view, html} = live(conn, "/membership_fee_types/new") {:ok, _view, html} = live(conn, "/membership_fee_settings/new_fee_type")
# Should show the form (admin user in setup) # Should show the form (admin user in setup)
assert html =~ "Membership Fee Type" || html =~ "Beitragsart" assert html =~ "Membership Fee Type" || html =~ "Beitragsart"

View file

@ -60,7 +60,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
admin_user admin_user
) )
{:ok, _view, html} = live(conn, "/membership_fee_types") {:ok, _view, html} = live(conn, "/membership_fee_settings")
assert html =~ "Regular" assert html =~ "Regular"
assert html =~ "Reduced" assert html =~ "Reduced"
@ -77,33 +77,33 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
create_member(%{membership_fee_type_id: fee_type.id}, admin_user) create_member(%{membership_fee_type_id: fee_type.id}, admin_user)
end) end)
{:ok, _view, html} = live(conn, "/membership_fee_types") {:ok, _view, html} = live(conn, "/membership_fee_settings")
assert html =~ "3" || html =~ "Members" || html =~ "Mitglieder" assert html =~ "3" || html =~ "Members" || html =~ "Mitglieder"
end end
test "create button navigates to form", %{conn: conn} do test "create button navigates to form", %{conn: conn} do
{:ok, view, _html} = live(conn, "/membership_fee_types") {:ok, view, _html} = live(conn, "/membership_fee_settings")
{:error, {:live_redirect, %{to: to}}} = {:error, {:live_redirect, %{to: to}}} =
view view
|> element("a[href='/membership_fee_types/new']") |> element("a[href='/membership_fee_settings/new_fee_type']")
|> render_click() |> render_click()
assert to == "/membership_fee_types/new" assert to == "/membership_fee_settings/new_fee_type"
end end
test "edit button per row navigates to edit form", %{conn: conn, current_user: admin_user} do test "edit button per row navigates to edit form", %{conn: conn, current_user: admin_user} do
fee_type = create_fee_type(%{interval: :yearly}, admin_user) fee_type = create_fee_type(%{interval: :yearly}, admin_user)
{:ok, view, _html} = live(conn, "/membership_fee_types") {:ok, view, _html} = live(conn, "/membership_fee_settings")
{:error, {:live_redirect, %{to: to}}} = {:error, {:live_redirect, %{to: to}}} =
view view
|> element("a[href='/membership_fee_types/#{fee_type.id}/edit']") |> element("a[href='/membership_fee_settings/#{fee_type.id}/edit_fee_type']")
|> render_click() |> render_click()
assert to == "/membership_fee_types/#{fee_type.id}/edit" assert to == "/membership_fee_settings/#{fee_type.id}/edit_fee_type"
end end
end end
@ -112,7 +112,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
fee_type = create_fee_type(%{interval: :yearly}, admin_user) fee_type = create_fee_type(%{interval: :yearly}, admin_user)
create_member(%{membership_fee_type_id: fee_type.id}, admin_user) create_member(%{membership_fee_type_id: fee_type.id}, admin_user)
{:ok, _view, html} = live(conn, "/membership_fee_types") {:ok, _view, html} = live(conn, "/membership_fee_settings")
# Delete button should be disabled # Delete button should be disabled
assert html =~ "disabled" || html =~ "cursor-not-allowed" assert html =~ "disabled" || html =~ "cursor-not-allowed"
@ -122,7 +122,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
fee_type = create_fee_type(%{interval: :yearly}, admin_user) fee_type = create_fee_type(%{interval: :yearly}, admin_user)
# No members assigned # No members assigned
{:ok, view, _html} = live(conn, "/membership_fee_types") {:ok, view, _html} = live(conn, "/membership_fee_settings")
# Delete button should be enabled # Delete button should be enabled
view view
@ -142,10 +142,11 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
test "only admin can access", %{conn: conn} do test "only admin can access", %{conn: conn} do
# This test assumes non-admin users cannot access # This test assumes non-admin users cannot access
# Adjust based on actual permission implementation # Adjust based on actual permission implementation
{:ok, _view, html} = live(conn, "/membership_fee_types") {:ok, _view, html} = live(conn, "/membership_fee_settings")
# Should show the page (admin user in setup) # Should show the page (admin user in setup)
assert html =~ "Membership Fee Types" || html =~ "Beitragsarten" assert html =~ "Membership Fee Settings" || html =~ "Beitragseinstellungen" ||
html =~ "Membership Fee Types"
end end
end end
end end

View file

@ -279,17 +279,11 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
end end
@tag role: :member @tag role: :member
test "GET /membership_fee_types redirects to user profile", %{conn: conn, current_user: user} do test "GET /membership_fee_settings/new_fee_type redirects to user profile", %{
conn = get(conn, "/membership_fee_types")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :member
test "GET /membership_fee_types/new redirects to user profile", %{
conn: conn, conn: conn,
current_user: user current_user: user
} do } do
conn = get(conn, "/membership_fee_types/new") conn = get(conn, "/membership_fee_settings/new_fee_type")
assert redirected_to(conn) == "/users/#{user.id}" assert redirected_to(conn) == "/users/#{user.id}"
end end
@ -385,7 +379,7 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
end end
@tag role: :member @tag role: :member
test "GET /membership_fee_types/:id/edit redirects to user profile", %{ test "GET /membership_fee_settings/:id/edit_fee_type redirects to user profile", %{
conn: conn, conn: conn,
current_user: user current_user: user
} do } do
@ -396,7 +390,7 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
|> List.first() |> List.first()
if type do if type do
conn = get(conn, "/membership_fee_types/#{type.id}/edit") conn = get(conn, "/membership_fee_settings/#{type.id}/edit_fee_type")
assert redirected_to(conn) == "/users/#{user.id}" assert redirected_to(conn) == "/users/#{user.id}"
end end
end end
@ -680,15 +674,6 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
assert redirected_to(conn) == "/users/#{user.id}" assert redirected_to(conn) == "/users/#{user.id}"
end end
@tag role: :read_only
test "GET /membership_fee_types redirects to user profile", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/membership_fee_types")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :read_only @tag role: :read_only
test "GET /groups/new redirects to user profile", %{conn: conn, current_user: user} do test "GET /groups/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/groups/new") conn = get(conn, "/groups/new")
@ -864,15 +849,6 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
assert redirected_to(conn) == "/users/#{user.id}" assert redirected_to(conn) == "/users/#{user.id}"
end end
@tag role: :normal_user
test "GET /membership_fee_types redirects to user profile", %{
conn: conn,
current_user: user
} do
conn = get(conn, "/membership_fee_types")
assert redirected_to(conn) == "/users/#{user.id}"
end
@tag role: :normal_user @tag role: :normal_user
test "GET /admin/roles redirects to user profile", %{conn: conn, current_user: user} do test "GET /admin/roles redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/admin/roles") conn = get(conn, "/admin/roles")