Merge branch 'main' into feat/299_plz
This commit is contained in:
commit
bfc078d5aa
45 changed files with 2187 additions and 425 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
132
lib/mv/config.ex
132
lib/mv/config.ex
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 %>
|
||||||
|
|
|
||||||
101
lib/mv_web/live/auth/sign_in_live.ex
Normal file
101
lib/mv_web/live/auth/sign_in_live.ex
Normal 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
|
||||||
132
lib/mv_web/live/datafields_live.ex
Normal file
132
lib/mv_web/live/datafields_live.ex
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
29
priv/repo/migrations/20260224122831_add_oidc_to_settings.exs
Normal file
29
priv/repo/migrations/20260224122831_add_oidc_to_settings.exs
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -64,4 +64,4 @@
|
||||||
"repo": "Elixir.Mv.Repo",
|
"repo": "Elixir.Mv.Repo",
|
||||||
"schema": null,
|
"schema": null,
|
||||||
"table": "settings"
|
"table": "settings"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,4 +76,4 @@
|
||||||
"repo": "Elixir.Mv.Repo",
|
"repo": "Elixir.Mv.Repo",
|
||||||
"schema": null,
|
"schema": null,
|
||||||
"table": "settings"
|
"table": "settings"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,4 +100,4 @@
|
||||||
"repo": "Elixir.Mv.Repo",
|
"repo": "Elixir.Mv.Repo",
|
||||||
"schema": null,
|
"schema": null,
|
||||||
"table": "settings"
|
"table": "settings"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,4 +137,4 @@
|
||||||
"repo": "Elixir.Mv.Repo",
|
"repo": "Elixir.Mv.Repo",
|
||||||
"schema": null,
|
"schema": null,
|
||||||
"table": "settings"
|
"table": "settings"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
164
priv/resource_snapshots/repo/settings/20260224122831.json
Normal file
164
priv/resource_snapshots/repo/settings/20260224122831.json
Normal 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"
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue