Compare commits

...

13 commits

Author SHA1 Message Date
ae179703db
Move custom fields to global admin settings
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-12-03 14:33:31 +01:00
422cf37a1e Merge pull request 'Fix UI issues' (#242) from ui-fixes into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #242
Reviewed-by: simon <s.thiessen@local-it.org>
2025-12-03 14:30:13 +01:00
a10d42f1ed Merge pull request 'Add file_envs for secrets and allow passing database url via separate envs' (#246) from add-file-envs into main
Some checks reported errors
continuous-integration/drone/push Build was killed
Reviewed-on: #246
Reviewed-by: rafael <rafael@noreply.git.local-it.org>
2025-12-03 14:29:32 +01:00
d1bab1288c
Merge remote-tracking branch 'origin/main' into add-file-envs
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-12-03 14:29:04 +01:00
1623b63207
fix: resolve review comments
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-12-03 14:27:22 +01:00
e6c5a58c65
Show dates in european format
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-12-03 14:20:14 +01:00
ee414c9440
Hide OIDC ID and ID columns for users 2025-12-03 14:20:14 +01:00
366d4c104a
Prevent tables from growing the page horizontally 2025-12-03 14:20:14 +01:00
ce15b8f59b
fix: mailto formatting
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 12:54:49 +01:00
d8384098b4
chore: update prod-compose to use file-envs for secrets
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 12:38:24 +01:00
ee094eec2f
feat: add file env support for secrets 2025-12-03 12:36:13 +01:00
eedd24b93c
Truncate long entries in tables to prevent height changes 2025-12-02 16:33:15 +01:00
06ba50f05d
Fix translation "Bearbeite" -> "Bearbeiten" 2025-12-02 16:32:55 +01:00
32 changed files with 1023 additions and 721 deletions

3
.gitignore vendored
View file

@ -41,3 +41,6 @@ npm-debug.log
.env .env
.elixir_ls/ .elixir_ls/
# Docker secrets directory (generated by `just init-secrets`)
/secrets/

View file

@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- CopyToClipboard JavaScript hook with fallback for older browsers - CopyToClipboard JavaScript hook with fallback for older browsers
- Button shows count of visible selected members (respects search/filter) - Button shows count of visible selected members (respects search/filter)
- German/English translations - German/English translations
- Docker secrets support via `_FILE` environment variables for all sensitive configuration (SECRET_KEY_BASE, TOKEN_SIGNING_SECRET, OIDC_CLIENT_SECRET, DATABASE_URL, DATABASE_PASSWORD)
### Fixed ### Fixed
- Email validation false positive when linking user and member with identical emails (#168 Problem #4) - Email validation false positive when linking user and member with identical emails (#168 Problem #4)

View file

@ -91,3 +91,26 @@ remove-gettext-conflicts:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
find priv/gettext -type f -exec sed -i '/^<<<<<<< HEAD$/d; /^=======$/d; /^>>>>>>>/d' {} \; find priv/gettext -type f -exec sed -i '/^<<<<<<< HEAD$/d; /^=======$/d; /^>>>>>>>/d' {} \;
# Production environment commands
# ================================
# Initialize secrets directory with generated secrets (only if not exists)
init-prod-secrets:
#!/usr/bin/env bash
set -euo pipefail
if [ -d "secrets" ]; then
echo "Secrets directory already exists. Skipping generation."
exit 0
fi
echo "Creating secrets directory and generating secrets..."
mkdir -p secrets
mix phx.gen.secret > secrets/secret_key_base.txt
mix phx.gen.secret > secrets/token_signing_secret.txt
openssl rand -base64 32 | tr -d '\n' > secrets/db_password.txt
touch secrets/oidc_client_secret.txt
echo "Secrets generated in ./secrets/"
# Start production environment with Docker Compose
start-prod: init-prod-secrets
docker compose -f docker-compose.prod.yml up -d

View file

@ -217,6 +217,13 @@ For testing the production Docker build locally:
# OIDC_BASE_URL=http://localhost:8080/auth/v1 # OIDC_BASE_URL=http://localhost:8080/auth/v1
# OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback # OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback
# OIDC_CLIENT_SECRET=<from-rauthy-client> # OIDC_CLIENT_SECRET=<from-rauthy-client>
# Alternative: Use _FILE variables for Docker secrets (takes priority over regular vars):
# SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base
# TOKEN_SIGNING_SECRET_FILE=/run/secrets/token_signing_secret
# OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc_client_secret
# DATABASE_URL_FILE=/run/secrets/database_url
# DATABASE_PASSWORD_FILE=/run/secrets/database_password
``` ```
3. **Start development environment** (for Rauthy): 3. **Start development environment** (for Rauthy):
@ -250,7 +257,7 @@ For actual production deployment:
- Set `OIDC_BASE_URL` to your production OIDC provider - Set `OIDC_BASE_URL` to your production OIDC provider
- Configure proper Docker networks - Configure proper Docker networks
3. **Set up SSL/TLS** (e.g., via reverse proxy like Nginx/Traefik) 3. **Set up SSL/TLS** (e.g., via reverse proxy like Nginx/Traefik)
4. **Use secure secrets management** (environment variables, Docker secrets, vault) 4. **Use secure secrets management** — All sensitive environment variables support a `_FILE` suffix for Docker secrets (e.g., `SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base`). See `docker-compose.prod.yml` for an example setup with Docker secrets.
5. **Configure database backups** 5. **Configure database backups**

View file

@ -7,6 +7,75 @@ import Config
# any compile-time configuration in here, as it won't be applied. # any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration. # The block below contains prod specific runtime configuration.
# Helper function to read environment variables with Docker secrets support.
# Supports the _FILE suffix pattern: if VAR_FILE is set, reads the value from
# that file path. Otherwise falls back to VAR directly.
# VAR_FILE takes priority and must contain the full absolute path to the secret file.
get_env_or_file = fn var_name, default ->
file_var = "#{var_name}_FILE"
case System.get_env(file_var) do
nil ->
System.get_env(var_name, default)
file_path ->
case File.read(file_path) do
{:ok, content} ->
String.trim_trailing(content)
{:error, reason} ->
raise """
Failed to read secret from file specified in #{file_var}="#{file_path}".
Error: #{inspect(reason)}
"""
end
end
end
# Same as get_env_or_file but raises if the value is not set
get_env_or_file! = fn var_name, error_message ->
case get_env_or_file.(var_name, nil) do
nil -> raise error_message
value -> value
end
end
# Build database URL from individual components or use DATABASE_URL directly.
# Supports both approaches:
# 1. DATABASE_URL (or DATABASE_URL_FILE) - full connection URL
# 2. Separate vars: DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD (or _FILE), DATABASE_NAME, DATABASE_PORT
build_database_url = fn ->
case get_env_or_file.("DATABASE_URL", nil) do
nil ->
# Build URL from separate components
host =
System.get_env("DATABASE_HOST") ||
raise "DATABASE_HOST is required when DATABASE_URL is not set"
user =
System.get_env("DATABASE_USER") ||
raise "DATABASE_USER is required when DATABASE_URL is not set"
password =
get_env_or_file!.("DATABASE_PASSWORD", """
DATABASE_PASSWORD or DATABASE_PASSWORD_FILE is required when DATABASE_URL is not set.
""")
database =
System.get_env("DATABASE_NAME") ||
raise "DATABASE_NAME is required when DATABASE_URL is not set"
port = System.get_env("DATABASE_PORT", "5432")
# URL-encode the password to handle special characters
encoded_password = URI.encode_www_form(password)
"ecto://#{user}:#{encoded_password}@#{host}:#{port}/#{database}"
url ->
url
end
end
# ## Using releases # ## Using releases
# #
# If you use `mix release`, you need to explicitly enable the server # If you use `mix release`, you need to explicitly enable the server
@ -21,12 +90,7 @@ if System.get_env("PHX_SERVER") do
end end
if config_env() == :prod do if config_env() == :prod do
database_url = database_url = build_database_url.()
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
@ -41,12 +105,12 @@ if config_env() == :prod do
# want to use a different value for prod and you most likely don't want # want to use a different value for prod and you most likely don't want
# to check this value into version control, so we use an environment # to check this value into version control, so we use an environment
# variable instead. # variable instead.
# Supports SECRET_KEY_BASE or SECRET_KEY_BASE_FILE for Docker secrets.
secret_key_base = secret_key_base =
System.get_env("SECRET_KEY_BASE") || get_env_or_file!.("SECRET_KEY_BASE", """
raise """ environment variable SECRET_KEY_BASE (or SECRET_KEY_BASE_FILE) is missing.
environment variable SECRET_KEY_BASE is missing. You can generate one by calling: mix phx.gen.secret
You can generate one by calling: mix phx.gen.secret """)
"""
host = System.get_env("PHX_HOST") || raise "Please define the PHX_HOST environment variable." host = System.get_env("PHX_HOST") || raise "Please define the PHX_HOST environment variable."
port = String.to_integer(System.get_env("PORT") || "4000") port = String.to_integer(System.get_env("PORT") || "4000")
@ -54,32 +118,47 @@ if config_env() == :prod do
config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
# Rauthy OIDC configuration # Rauthy OIDC configuration
# Supports OIDC_CLIENT_SECRET or OIDC_CLIENT_SECRET_FILE for Docker secrets.
# OIDC_CLIENT_SECRET is required only if OIDC is being used (indicated by explicit OIDC env vars).
oidc_base_url = System.get_env("OIDC_BASE_URL")
oidc_client_id = System.get_env("OIDC_CLIENT_ID")
oidc_in_use = not is_nil(oidc_base_url) or not is_nil(oidc_client_id)
client_secret =
if oidc_in_use do
get_env_or_file!.("OIDC_CLIENT_SECRET", """
environment variable OIDC_CLIENT_SECRET (or OIDC_CLIENT_SECRET_FILE) is missing.
This is required when OIDC authentication is configured (OIDC_BASE_URL or OIDC_CLIENT_ID is set).
""")
else
get_env_or_file.("OIDC_CLIENT_SECRET", nil)
end
config :mv, :rauthy, config :mv, :rauthy,
client_id: System.get_env("OIDC_CLIENT_ID") || "mv", client_id: oidc_client_id || "mv",
base_url: System.get_env("OIDC_BASE_URL") || "http://localhost:8080/auth/v1", base_url: oidc_base_url || "http://localhost:8080/auth/v1",
client_secret: System.get_env("OIDC_CLIENT_SECRET"), client_secret: client_secret,
redirect_uri: redirect_uri:
System.get_env("OIDC_REDIRECT_URI") || "http://#{host}:#{port}/auth/user/rauthy/callback" System.get_env("OIDC_REDIRECT_URI") || "http://#{host}:#{port}/auth/user/rauthy/callback"
# Token signing secret from environment variable # Token signing secret from environment variable
# This overrides the placeholder value set in prod.exs # This overrides the placeholder value set in prod.exs
# Supports TOKEN_SIGNING_SECRET or TOKEN_SIGNING_SECRET_FILE for Docker secrets.
token_signing_secret = token_signing_secret =
System.get_env("TOKEN_SIGNING_SECRET") || get_env_or_file!.("TOKEN_SIGNING_SECRET", """
raise """ environment variable TOKEN_SIGNING_SECRET (or TOKEN_SIGNING_SECRET_FILE) is missing.
environment variable TOKEN_SIGNING_SECRET is missing. You can generate one by calling: mix phx.gen.secret
You can generate one by calling: mix phx.gen.secret """)
"""
config :mv, :token_signing_secret, token_signing_secret config :mv, :token_signing_secret, token_signing_secret
config :mv, MvWeb.Endpoint, config :mv, MvWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"], url: [host: host, port: 443, scheme: "https"],
http: [ http: [
# Enable IPv6 and bind on all interfaces. # Bind on all IPv4 interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. # Use {0, 0, 0, 0, 0, 0, 0, 0} for IPv6, or {127, 0, 0, 1} for localhost only.
# See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
# for details about using IPv6 vs IPv4 and loopback vs public addresses. ip: {0, 0, 0, 0},
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: port port: port
], ],
secret_key_base: secret_key_base, secret_key_base: secret_key_base,

View file

@ -2,21 +2,32 @@ services:
app: app:
image: git.local-it.org/local-it/mitgliederverwaltung:latest image: git.local-it.org/local-it/mitgliederverwaltung:latest
container_name: mv-prod-app container_name: mv-prod-app
# Use host network for local testing to access localhost:8080 (Rauthy) ports:
# In real production, remove this and use external OIDC provider - "4001:4001"
network_mode: host
environment: environment:
DATABASE_URL: "ecto://postgres:postgres@localhost:5001/mv_prod" # Database configuration using separate variables
SECRET_KEY_BASE: "${SECRET_KEY_BASE}" # Use Docker service name for internal networking
TOKEN_SIGNING_SECRET: "${TOKEN_SIGNING_SECRET}" DATABASE_HOST: "db-prod"
PHX_HOST: "${PHX_HOST}" DATABASE_PORT: "5432"
DATABASE_USER: "postgres"
DATABASE_NAME: "mv_prod"
DATABASE_PASSWORD_FILE: "/run/secrets/db_password"
# Phoenix secrets via Docker secrets
SECRET_KEY_BASE_FILE: "/run/secrets/secret_key_base"
TOKEN_SIGNING_SECRET_FILE: "/run/secrets/token_signing_secret"
PHX_HOST: "${PHX_HOST:-localhost}"
PORT: "4001" PORT: "4001"
PHX_SERVER: "true" PHX_SERVER: "true"
# Rauthy OIDC config - uses localhost because of host network mode # Rauthy OIDC config - use host.docker.internal to reach host services
OIDC_CLIENT_ID: "mv" OIDC_CLIENT_ID: "mv"
OIDC_BASE_URL: "http://localhost:8080/auth/v1" OIDC_BASE_URL: "http://host.docker.internal:8080/auth/v1"
OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET:-}" OIDC_CLIENT_SECRET_FILE: "/run/secrets/oidc_client_secret"
OIDC_REDIRECT_URI: "http://localhost:4001/auth/user/rauthy/callback" OIDC_REDIRECT_URI: "http://localhost:4001/auth/user/rauthy/callback"
secrets:
- db_password
- secret_key_base
- token_signing_secret
- oidc_client_secret
depends_on: depends_on:
- db-prod - db-prod
restart: unless-stopped restart: unless-stopped
@ -26,13 +37,25 @@ services:
container_name: mv-prod-db container_name: mv-prod-db
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: mv_prod POSTGRES_DB: mv_prod
secrets:
- db_password
volumes: volumes:
- postgres_data_prod:/var/lib/postgresql/data - postgres_data_prod:/var/lib/postgresql/data
ports: ports:
- "5001:5432" - "5001:5432"
restart: unless-stopped restart: unless-stopped
secrets:
db_password:
file: ./secrets/db_password.txt
secret_key_base:
file: ./secrets/secret_key_base.txt
token_signing_secret:
file: ./secrets/token_signing_secret.txt
oidc_client_secret:
file: ./secrets/oidc_client_secret.txt
volumes: volumes:
postgres_data_prod: postgres_data_prod:

View file

@ -79,7 +79,7 @@ defmodule MvWeb.CoreComponents do
<p>{msg}</p> <p>{msg}</p>
</div> </div>
<div class="flex-1" /> <div class="flex-1" />
<button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}> <button type="button" class="self-start cursor-pointer group" aria-label={gettext("close")}>
<.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" /> <.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
</button> </button>
</div> </div>
@ -368,61 +368,63 @@ defmodule MvWeb.CoreComponents do
end end
~H""" ~H"""
<table class="table table-zebra"> <div class="overflow-auto">
<thead> <table class="table table-zebra">
<tr> <thead>
<th :for={col <- @col}>{col[:label]}</th> <tr>
<th :for={dyn_col <- @dynamic_cols}> <th :for={col <- @col}>{col[:label]}</th>
<.live_component <th :for={dyn_col <- @dynamic_cols}>
module={MvWeb.Components.SortHeaderComponent} <.live_component
id={:"sort_custom_field_#{dyn_col[:custom_field].id}"} module={MvWeb.Components.SortHeaderComponent}
field={"custom_field_#{dyn_col[:custom_field].id}"} id={:"sort_custom_field_#{dyn_col[:custom_field].id}"}
label={dyn_col[:custom_field].name} field={"custom_field_#{dyn_col[:custom_field].id}"}
sort_field={@sort_field} label={dyn_col[:custom_field].name}
sort_order={@sort_order} sort_field={@sort_field}
/> sort_order={@sort_order}
</th> />
<th :if={@action != []}> </th>
<span class="sr-only">{gettext("Actions")}</span> <th :if={@action != []}>
</th> <span class="sr-only">{gettext("Actions")}</span>
</tr> </th>
</thead> </tr>
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}> </thead>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}> <tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
<td <tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
:for={col <- @col} <td
phx-click={@row_click && @row_click.(row)} :for={col <- @col}
class={@row_click && "hover:cursor-pointer"} phx-click={@row_click && @row_click.(row)}
> class={["max-w-xs truncate", @row_click && "hover:cursor-pointer"]}
{render_slot(col, @row_item.(row))} >
</td> {render_slot(col, @row_item.(row))}
<td </td>
:for={dyn_col <- @dynamic_cols} <td
phx-click={@row_click && @row_click.(row)} :for={dyn_col <- @dynamic_cols}
class={@row_click && "hover:cursor-pointer"} phx-click={@row_click && @row_click.(row)}
> class={["max-w-xs truncate", @row_click && "hover:cursor-pointer"]}
{if dyn_col[:render] do >
rendered = dyn_col[:render].(@row_item.(row)) {if dyn_col[:render] do
rendered = dyn_col[:render].(@row_item.(row))
if rendered == "" do if rendered == "" do
"" ""
else
rendered
end
else else
rendered ""
end end}
else </td>
"" <td :if={@action != []} class="w-0 font-semibold">
end} <div class="flex gap-4">
</td> <%= for action <- @action do %>
<td :if={@action != []} class="w-0 font-semibold"> {render_slot(action, @row_item.(row))}
<div class="flex gap-4"> <% end %>
<%= for action <- @action do %> </div>
{render_slot(action, @row_item.(row))} </td>
<% end %> </tr>
</div> </tbody>
</td> </table>
</tr> </div>
</tbody>
</table>
""" """
end end

View file

@ -23,7 +23,7 @@ defmodule MvWeb.Layouts.Navbar do
<a class="btn btn-ghost text-xl">{@club_name}</a> <a class="btn btn-ghost text-xl">{@club_name}</a>
<ul class="menu menu-horizontal bg-base-200"> <ul class="menu menu-horizontal bg-base-200">
<li><.link navigate="/members">{gettext("Members")}</.link></li> <li><.link navigate="/members">{gettext("Members")}</.link></li>
<li><.link navigate="/custom_fields">{gettext("Custom Fields")}</.link></li> <li><.link navigate="/settings">{gettext("Settings")}</.link></li>
<li><.link navigate="/users">{gettext("Users")}</.link></li> <li><.link navigate="/users">{gettext("Users")}</.link></li>
</ul> </ul>
</div> </div>

View file

@ -0,0 +1,27 @@
defmodule MvWeb.Helpers.DateFormatter do
@moduledoc """
Centralized date formatting helper for the application.
Formats dates in European format (dd.mm.yyyy).
"""
use Gettext, backend: MvWeb.Gettext
@doc """
Formats a Date struct to European format (dd.mm.yyyy).
## Examples
iex> MvWeb.Helpers.DateFormatter.format_date(~D[2024-03-15])
"15.03.2024"
iex> MvWeb.Helpers.DateFormatter.format_date(nil)
""
"""
def format_date(%Date{} = date) do
Calendar.strftime(date, "%d.%m.%Y")
end
def format_date(nil), do: ""
def format_date(_), do: "Invalid date"
end

View file

@ -19,7 +19,7 @@ defmodule MvWeb.Components.SortHeaderComponent do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div class="tooltip" data-tip={aria_sort(@field, @sort_field, @sort_order)}> <div class="tooltip tooltip-bottom" data-tip={aria_sort(@field, @sort_field, @sort_order)}>
<button <button
type="button" type="button"
aria-label={aria_sort(@field, @sort_field, @sort_order)} aria-label={aria_sort(@field, @sort_field, @sort_order)}

View file

@ -1,142 +0,0 @@
defmodule MvWeb.CustomFieldLive.Form do
@moduledoc """
LiveView form for creating and editing custom fields (admin).
## Features
- Create new custom field definitions
- Edit existing custom fields
- Select value type from supported types
- Set immutable and required flags
- Real-time validation
## Form Fields
**Required:**
- name - Unique identifier (e.g., "phone_mobile", "emergency_contact")
- value_type - Data type (:string, :integer, :boolean, :date, :email)
**Optional:**
- description - Human-readable explanation
- immutable - If true, values cannot be changed after creation (default: false)
- required - If true, all members must have this custom field (default: false)
- show_in_overview - If true, this custom field will be displayed in the member overview table (default: true)
## Value Type Selection
- `:string` - Text data (unlimited length)
- `:integer` - Numeric data
- `:boolean` - True/false flags
- `:date` - Date values
- `:email` - Validated email addresses
## Events
- `validate` - Real-time form validation
- `save` - Submit form (create or update custom field)
## Security
Custom field management is restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{@page_title}
<:subtitle>
{gettext("Use this form to manage custom_field records in your database.")}
</:subtitle>
</.header>
<.form for={@form} id="custom_field-form" phx-change="validate" phx-submit="save">
<.input field={@form[:name]} type="text" label={gettext("Name")} />
<.input
field={@form[:value_type]}
type="select"
label={gettext("Value type")}
options={
Ash.Resource.Info.attribute(Mv.Membership.CustomField, :value_type).constraints[:one_of]
}
/>
<.input field={@form[:description]} type="text" label={gettext("Description")} />
<.input field={@form[:immutable]} type="checkbox" label={gettext("Immutable")} />
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
<.input field={@form[:show_in_overview]} type="checkbox" label={gettext("Show in overview")} />
<.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save Custom field")}
</.button>
<.button navigate={return_path(@return_to, @custom_field)}>{gettext("Cancel")}</.button>
</.form>
</Layouts.app>
"""
end
@impl true
def mount(params, _session, socket) do
custom_field =
case params["id"] do
nil -> nil
id -> Ash.get!(Mv.Membership.CustomField, id)
end
action = if is_nil(custom_field), do: "New", else: "Edit"
page_title = action <> " " <> "Custom field"
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
|> assign(custom_field: custom_field)
|> assign(:page_title, page_title)
|> assign_form()}
end
defp return_to("show"), do: "show"
defp return_to(_), do: "index"
@impl true
def handle_event("validate", %{"custom_field" => custom_field_params}, socket) do
{:noreply,
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, custom_field_params))}
end
def handle_event("save", %{"custom_field" => custom_field_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.form, params: custom_field_params) do
{:ok, custom_field} ->
notify_parent({:saved, custom_field})
action =
case socket.assigns.form.source.type do
:create -> gettext("create")
:update -> gettext("update")
other -> to_string(other)
end
socket =
socket
|> put_flash(:info, gettext("Custom field %{action} successfully", action: action))
|> push_navigate(to: return_path(socket.assigns.return_to, custom_field))
{:noreply, socket}
{:error, form} ->
{:noreply, assign(socket, form: form)}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
defp assign_form(%{assigns: %{custom_field: custom_field}} = socket) do
form =
if custom_field do
AshPhoenix.Form.for_update(custom_field, :update, as: "custom_field")
else
AshPhoenix.Form.for_create(Mv.Membership.CustomField, :create, as: "custom_field")
end
assign(socket, form: to_form(form))
end
defp return_path("index", _custom_field), do: ~p"/custom_fields"
defp return_path("show", custom_field), do: ~p"/custom_fields/#{custom_field.id}"
end

View file

@ -0,0 +1,122 @@
defmodule MvWeb.CustomFieldLive.FormComponent do
@moduledoc """
LiveComponent form for creating and editing custom fields (embedded in settings).
## Features
- Create new custom field definitions
- Edit existing custom fields
- Select value type from supported types
- Set immutable and required flags
- Real-time validation
## Props
- `custom_field` - The custom field to edit (nil for new)
- `on_save` - Callback function to call when form is saved
- `on_cancel` - Callback function to call when form is cancelled
"""
use MvWeb, :live_component
@impl true
def render(assigns) do
~H"""
<div id={@id} class="card bg-base-200 shadow-xl mb-8">
<div class="card-body">
<div class="flex items-center gap-4 mb-4">
<.button type="button" phx-click="cancel" phx-target={@myself}>
<.icon name="hero-arrow-left" class="w-4 h-4" />
</.button>
<h3 class="card-title">
{if @custom_field, do: gettext("Edit Custom Field"), else: gettext("New Custom Field")}
</h3>
</div>
<.form
for={@form}
id={@id <> "-form"}
phx-change="validate"
phx-submit="save"
phx-target={@myself}
>
<.input field={@form[:name]} type="text" label={gettext("Name")} />
<.input
field={@form[:value_type]}
type="select"
label={gettext("Value type")}
options={
Ash.Resource.Info.attribute(Mv.Membership.CustomField, :value_type).constraints[:one_of]
}
/>
<.input field={@form[:description]} type="text" label={gettext("Description")} />
<.input field={@form[:immutable]} type="checkbox" label={gettext("Immutable")} />
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
<.input
field={@form[:show_in_overview]}
type="checkbox"
label={gettext("Show in overview")}
/>
<div class="card-actions justify-end mt-4">
<.button type="button" phx-click="cancel" phx-target={@myself}>
{gettext("Cancel")}
</.button>
<.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save Custom field")}
</.button>
</div>
</.form>
</div>
</div>
"""
end
@impl true
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_form()}
end
@impl true
def handle_event("validate", %{"custom_field" => custom_field_params}, socket) do
{:noreply,
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, custom_field_params))}
end
@impl true
def handle_event("save", %{"custom_field" => custom_field_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.form, params: custom_field_params) do
{:ok, custom_field} ->
action =
case socket.assigns.form.source.type do
:create -> gettext("create")
:update -> gettext("update")
other -> to_string(other)
end
socket.assigns.on_save.(custom_field, action)
{:noreply, socket}
{:error, form} ->
{:noreply, assign(socket, form: form)}
end
end
@impl true
def handle_event("cancel", _params, socket) do
socket.assigns.on_cancel.()
{:noreply, socket}
end
defp assign_form(%{assigns: %{custom_field: custom_field}} = socket) do
form =
if custom_field do
AshPhoenix.Form.for_update(custom_field, :update, as: "custom_field")
else
AshPhoenix.Form.for_create(Mv.Membership.CustomField, :create, as: "custom_field")
end
assign(socket, form: to_form(form))
end
end

View file

@ -1,199 +0,0 @@
defmodule MvWeb.CustomFieldLive.Index do
@moduledoc """
LiveView for managing custom field definitions (admin).
## Features
- List all custom fields
- Display type information (name, value type, description)
- Show immutable and required flags
- Create new custom fields
- Edit existing custom fields
- Delete custom fields with confirmation (cascades to all custom field values)
## Displayed Information
- Name: Unique identifier for the custom field
- Value type: Data type constraint (string, integer, boolean, date, email)
- Description: Human-readable explanation
- Immutable: Whether custom field values can be changed after creation
- Required: Whether all members must have this custom field (future feature)
## Events
- `prepare_delete` - Opens deletion confirmation modal with member count
- `confirm_delete` - Executes deletion after slug verification
- `cancel_delete` - Cancels deletion and closes modal
- `update_slug_confirmation` - Updates slug input state
## Security
Custom field management is restricted to admin users.
Deletion requires entering the custom field's slug to prevent accidental deletions.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Custom fields
<:actions>
<.button variant="primary" navigate={~p"/custom_fields/new"}>
<.icon name="hero-plus" /> New Custom field
</.button>
</:actions>
</.header>
<.table
id="custom_fields"
rows={@streams.custom_fields}
row_click={fn {_id, custom_field} -> JS.navigate(~p"/custom_fields/#{custom_field}") end}
>
<:col :let={{_id, custom_field}} label="Name">{custom_field.name}</:col>
<:col :let={{_id, custom_field}} label="Description">{custom_field.description}</:col>
<:action :let={{_id, custom_field}}>
<div class="sr-only">
<.link navigate={~p"/custom_fields/#{custom_field}"}>Show</.link>
</div>
<.link navigate={~p"/custom_fields/#{custom_field}/edit"}>Edit</.link>
</:action>
<:action :let={{_id, custom_field}}>
<.link phx-click={JS.push("prepare_delete", value: %{id: custom_field.id})}>
Delete
</.link>
</:action>
</.table>
<%!-- Delete Confirmation Modal --%>
<dialog :if={@show_delete_modal} id="delete-custom-field-modal" class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg">{gettext("Delete Custom Field")}</h3>
<div class="py-4 space-y-4">
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="h-5 w-5" />
<div>
<p class="font-semibold">
{ngettext(
"%{count} member has a value assigned for this custom field.",
"%{count} members have values assigned for this custom field.",
@custom_field_to_delete.assigned_members_count,
count: @custom_field_to_delete.assigned_members_count
)}
</p>
<p class="text-sm mt-2">
{gettext(
"All custom field values will be permanently deleted when you delete this custom field."
)}
</p>
</div>
</div>
<div>
<label for="slug-confirmation" class="label">
<span class="label-text">
{gettext("To confirm deletion, please enter this text:")}
</span>
</label>
<div class="font-mono font-bold text-lg mb-2 p-2 bg-base-200 rounded break-all">
{@custom_field_to_delete.slug}
</div>
<form phx-change="update_slug_confirmation">
<input
id="slug-confirmation"
name="slug"
type="text"
value={@slug_confirmation}
placeholder={gettext("Enter the text above to confirm")}
autocomplete="off"
phx-mounted={JS.focus()}
class="input input-bordered w-full"
/>
</form>
</div>
</div>
<div class="modal-action">
<button phx-click="cancel_delete" class="btn">
{gettext("Cancel")}
</button>
<button
phx-click="confirm_delete"
class="btn btn-error"
disabled={@slug_confirmation != @custom_field_to_delete.slug}
>
{gettext("Delete Custom Field and All Values")}
</button>
</div>
</div>
</dialog>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Custom fields")
|> assign(:show_delete_modal, false)
|> assign(:custom_field_to_delete, nil)
|> assign(:slug_confirmation, "")
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField))}
end
@impl true
def handle_event("prepare_delete", %{"id" => id}, socket) do
custom_field = Ash.get!(Mv.Membership.CustomField, id, load: [:assigned_members_count])
{:noreply,
socket
|> assign(:custom_field_to_delete, custom_field)
|> assign(:show_delete_modal, true)
|> assign(:slug_confirmation, "")}
end
@impl true
def handle_event("update_slug_confirmation", %{"slug" => slug}, socket) do
{:noreply, assign(socket, :slug_confirmation, slug)}
end
@impl true
def handle_event("confirm_delete", _params, socket) do
custom_field = socket.assigns.custom_field_to_delete
if socket.assigns.slug_confirmation == custom_field.slug do
# Delete the custom field (CASCADE will handle custom field values)
case Ash.destroy(custom_field) do
:ok ->
{:noreply,
socket
|> put_flash(:info, "Custom field deleted successfully")
|> assign(:show_delete_modal, false)
|> assign(:custom_field_to_delete, nil)
|> assign(:slug_confirmation, "")
|> stream_delete(:custom_fields, custom_field)}
{:error, error} ->
{:noreply,
socket
|> put_flash(:error, "Failed to delete custom field: #{inspect(error)}")}
end
else
{:noreply,
socket
|> put_flash(:error, "Slug does not match. Deletion cancelled.")}
end
end
@impl true
def handle_event("cancel_delete", _params, socket) do
{:noreply,
socket
|> assign(:show_delete_modal, false)
|> assign(:custom_field_to_delete, nil)
|> assign(:slug_confirmation, "")}
end
end

View file

@ -0,0 +1,259 @@
defmodule MvWeb.CustomFieldLive.IndexComponent do
@moduledoc """
LiveComponent for managing custom field definitions (embedded in settings).
## Features
- List all custom fields
- Display type information (name, value type, description)
- Show immutable and required flags
- Create new custom fields
- Edit existing custom fields
- Delete custom fields with confirmation (cascades to all custom field values)
"""
use MvWeb, :live_component
@impl true
def render(assigns) do
~H"""
<div id={@id}>
<.header>
{gettext("Custom Fields")}
<:subtitle>
{gettext("These will appear in addition to other data when adding new members.")}
</:subtitle>
<:actions>
<.button variant="primary" phx-click="new_custom_field" phx-target={@myself}>
<.icon name="hero-plus" /> {gettext("New Custom field")}
</.button>
</:actions>
</.header>
<%!-- Show form when creating or editing --%>
<div :if={@show_form} class="mb-8">
<.live_component
module={MvWeb.CustomFieldLive.FormComponent}
id={@form_id}
custom_field={@editing_custom_field}
on_save={fn custom_field, action -> send(self(), {:custom_field_saved, custom_field, action}) end}
on_cancel={fn -> send_update(__MODULE__, id: @id, show_form: false) end}
/>
</div>
<%!-- Hide table when form is visible --%>
<.table
:if={!@show_form}
id="custom_fields"
rows={@streams.custom_fields}
row_click={
fn {_id, custom_field} ->
JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself)
end
}
>
<:col :let={{_id, custom_field}} label={gettext("Name")}>{custom_field.name}</:col>
<:col :let={{_id, custom_field}} label={gettext("Value Type")}>
{custom_field.value_type}
</:col>
<:col :let={{_id, custom_field}} label={gettext("Description")}>
{custom_field.description}
</:col>
<:col :let={{_id, custom_field}} label={gettext("Show in Overview")}>
<span :if={custom_field.show_in_overview} class="badge badge-success">
{gettext("Yes")}
</span>
<span :if={!custom_field.show_in_overview} class="badge badge-ghost">
{gettext("No")}
</span>
</:col>
<:action :let={{_id, custom_field}}>
<.link phx-click={
JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself)
}>
{gettext("Edit")}
</.link>
</:action>
<:action :let={{_id, custom_field}}>
<.link phx-click={JS.push("prepare_delete", value: %{id: custom_field.id}, target: @myself)}>
{gettext("Delete")}
</.link>
</:action>
</.table>
<%!-- Delete Confirmation Modal --%>
<dialog :if={@show_delete_modal} id="delete-custom-field-modal" class="modal modal-open">
<div class="modal-box">
<h3 class="text-lg font-bold">{gettext("Delete Custom Field")}</h3>
<div class="py-4 space-y-4">
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<p class="font-semibold">
{ngettext(
"%{count} member has a value assigned for this custom field.",
"%{count} members have values assigned for this custom field.",
@custom_field_to_delete.assigned_members_count,
count: @custom_field_to_delete.assigned_members_count
)}
</p>
<p class="mt-2 text-sm">
{gettext(
"All custom field values will be permanently deleted when you delete this custom field."
)}
</p>
</div>
</div>
<div>
<label for="slug-confirmation" class="label">
<span class="label-text">
{gettext("To confirm deletion, please enter this text:")}
</span>
</label>
<div class="p-2 mb-2 font-mono text-lg font-bold break-all rounded bg-base-200">
{@custom_field_to_delete.slug}
</div>
<form phx-change="update_slug_confirmation" phx-target={@myself}>
<input
id="slug-confirmation"
name="slug"
type="text"
value={@slug_confirmation}
placeholder={gettext("Enter the text above to confirm")}
autocomplete="off"
phx-mounted={JS.focus()}
class="w-full input input-bordered"
/>
</form>
</div>
</div>
<div class="modal-action">
<button phx-click="cancel_delete" phx-target={@myself} class="btn">
{gettext("Cancel")}
</button>
<button
phx-click="confirm_delete"
phx-target={@myself}
class="btn btn-error"
disabled={@slug_confirmation != @custom_field_to_delete.slug}
>
{gettext("Delete Custom Field and All Values")}
</button>
</div>
</div>
</dialog>
</div>
"""
end
@impl true
def update(assigns, socket) do
# If show_form is explicitly provided in assigns, reset editing state
socket =
if Map.has_key?(assigns, :show_form) and assigns.show_form == false do
socket
|> assign(:editing_custom_field, nil)
|> assign(:form_id, "custom-field-form-new")
else
socket
end
{:ok,
socket
|> assign(assigns)
|> assign_new(:show_form, fn -> false end)
|> assign_new(:form_id, fn -> "custom-field-form-new" end)
|> assign_new(:editing_custom_field, fn -> nil end)
|> assign_new(:show_delete_modal, fn -> false end)
|> assign_new(:custom_field_to_delete, fn -> nil end)
|> assign_new(:slug_confirmation, fn -> "" end)
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField), reset: true)}
end
@impl true
def handle_event("new_custom_field", _params, socket) do
{:noreply,
socket
|> assign(:show_form, true)
|> assign(:editing_custom_field, nil)
|> assign(:form_id, "custom-field-form-new")}
end
@impl true
def handle_event("edit_custom_field", %{"id" => id}, socket) do
custom_field = Ash.get!(Mv.Membership.CustomField, id)
{:noreply,
socket
|> assign(:show_form, true)
|> assign(:editing_custom_field, custom_field)
|> assign(:form_id, "custom-field-form-#{id}")}
end
@impl true
def handle_event("prepare_delete", %{"id" => id}, socket) do
custom_field = Ash.get!(Mv.Membership.CustomField, id, load: [:assigned_members_count])
{:noreply,
socket
|> assign(:custom_field_to_delete, custom_field)
|> assign(:show_delete_modal, true)
|> assign(:slug_confirmation, "")}
end
@impl true
def handle_event("update_slug_confirmation", %{"slug" => slug}, socket) do
{:noreply, assign(socket, :slug_confirmation, slug)}
end
@impl true
def handle_event("confirm_delete", _params, socket) do
custom_field = socket.assigns.custom_field_to_delete
if socket.assigns.slug_confirmation == custom_field.slug do
case Ash.destroy(custom_field) do
:ok ->
send(self(), {:custom_field_deleted, custom_field})
{:noreply,
socket
|> assign(:show_delete_modal, false)
|> assign(:custom_field_to_delete, nil)
|> assign(:slug_confirmation, "")
|> stream_delete(:custom_fields, custom_field)}
{:error, error} ->
send(self(), {:custom_field_delete_error, error})
{:noreply,
socket
|> assign(:show_delete_modal, false)
|> assign(:custom_field_to_delete, nil)
|> assign(:slug_confirmation, "")}
end
else
send(self(), :custom_field_slug_mismatch)
{:noreply,
socket
|> assign(:show_delete_modal, false)
|> assign(:custom_field_to_delete, nil)
|> assign(:slug_confirmation, "")}
end
end
@impl true
def handle_event("cancel_delete", _params, socket) do
{:noreply,
socket
|> assign(:show_delete_modal, false)
|> assign(:custom_field_to_delete, nil)
|> assign(:slug_confirmation, "")}
end
end

View file

@ -1,75 +0,0 @@
defmodule MvWeb.CustomFieldLive.Show do
@moduledoc """
LiveView for displaying a single custom field's details (admin).
## Features
- Display custom field definition
- Show all attributes (name, value type, description, flags)
- Navigate to edit form
- Return to custom field list
## Displayed Information
- ID: Internal UUID identifier
- Slug: URL-friendly identifier (auto-generated, immutable)
- Name: Unique identifier
- Value type: Data type constraint
- Description: Optional explanation
- Immutable flag: Whether values can be changed
- Required flag: Whether all members need this custom field
## Navigation
- Back to custom field list
- Edit custom field
## Security
Custom field details are restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Custom field {@custom_field.slug}
<:subtitle>This is a custom_field record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/custom_fields"}>
<.icon name="hero-arrow-left" />
</.button>
<.button
variant="primary"
navigate={~p"/custom_fields/#{@custom_field}/edit?return_to=show"}
>
<.icon name="hero-pencil-square" /> Edit Custom field
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@custom_field.id}</:item>
<:item title="Slug">
{@custom_field.slug}
<p class="mt-2 text-sm leading-6 text-zinc-600">
{gettext("Auto-generated identifier (immutable)")}
</p>
</:item>
<:item title="Name">{@custom_field.name}</:item>
<:item title="Description">{@custom_field.description}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(%{"id" => id}, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Show Custom field")
|> assign(:custom_field, Ash.get!(Mv.Membership.CustomField, id))}
end
end

View file

@ -4,6 +4,7 @@ defmodule MvWeb.GlobalSettingsLive do
## Features ## Features
- Edit the association/club name - Edit the association/club name
- Manage custom fields
- Real-time form validation - Real-time form validation
- Success/error feedback - Success/error feedback
@ -28,7 +29,7 @@ defmodule MvWeb.GlobalSettingsLive do
{:ok, {:ok,
socket socket
|> assign(:page_title, gettext("Club Settings")) |> assign(:page_title, gettext("Settings"))
|> assign(:settings, settings) |> assign(:settings, settings)
|> assign_form()} |> assign_form()}
end end
@ -38,12 +39,16 @@ defmodule MvWeb.GlobalSettingsLive do
~H""" ~H"""
<Layouts.app flash={@flash} current_user={@current_user}> <Layouts.app flash={@flash} current_user={@current_user}>
<.header> <.header>
{gettext("Club Settings")} {gettext("Settings")}
<:subtitle> <:subtitle>
{gettext("Manage global settings for the association.")} {gettext("Manage global settings for the association.")}
</:subtitle> </:subtitle>
</.header> </.header>
<%!-- Club Settings Section --%>
<.header>
{gettext("Club Settings")}
</.header>
<.form for={@form} id="settings-form" phx-change="validate" phx-submit="save"> <.form for={@form} id="settings-form" phx-change="validate" phx-submit="save">
<.input <.input
field={@form[:club_name]} field={@form[:club_name]}
@ -56,6 +61,12 @@ defmodule MvWeb.GlobalSettingsLive do
{gettext("Save Settings")} {gettext("Save Settings")}
</.button> </.button>
</.form> </.form>
<%!-- Custom Fields Section --%>
<.live_component
module={MvWeb.CustomFieldLive.IndexComponent}
id="custom-fields-component"
/>
</Layouts.app> </Layouts.app>
""" """
end end
@ -66,6 +77,7 @@ 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("save", %{"setting" => setting_params}, socket) do def handle_event("save", %{"setting" => setting_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.form, params: setting_params) do case AshPhoenix.Form.submit(socket.assigns.form, params: setting_params) do
{:ok, updated_settings} -> {:ok, updated_settings} ->
@ -82,6 +94,37 @@ defmodule MvWeb.GlobalSettingsLive do
end end
end 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,
put_flash(socket, :info, gettext("Custom field %{action} successfully", action: action))}
end
@impl true
def handle_info({:custom_field_deleted, _custom_field}, socket) do
{:noreply, put_flash(socket, :info, gettext("Custom 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 custom 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
defp assign_form(%{assigns: %{settings: settings}} = socket) do defp assign_form(%{assigns: %{settings: settings}} = socket) do
form = form =
AshPhoenix.Form.for_update( AshPhoenix.Form.for_update(

View file

@ -32,6 +32,7 @@ defmodule MvWeb.MemberLive.Index do
alias Mv.Membership alias Mv.Membership
alias MvWeb.MemberLive.Index.Formatter alias MvWeb.MemberLive.Index.Formatter
alias MvWeb.Helpers.DateFormatter
# Prefix used in sort field names for custom fields (e.g., "custom_field_<id>") # Prefix used in sort field names for custom fields (e.g., "custom_field_<id>")
@custom_field_prefix "custom_field_" @custom_field_prefix "custom_field_"
@ -937,4 +938,7 @@ defmodule MvWeb.MemberLive.Index do
Map.get(visibility_config, Atom.to_string(field), true) Map.get(visibility_config, Atom.to_string(field), true)
end) end)
end end
# Public helper function to format dates for use in templates
def format_date(date), do: DateFormatter.format_date(date)
end end

View file

@ -224,7 +224,7 @@
""" """
} }
> >
{member.join_date} {MvWeb.MemberLive.Index.format_date(member.join_date)}
</:col> </:col>
<:col :let={member} label={gettext("Paid")}> <:col :let={member} label={gettext("Paid")}>
<span class={[ <span class={[

View file

@ -6,6 +6,7 @@ defmodule MvWeb.MemberLive.Index.Formatter do
formats them appropriately for display in the UI. formats them appropriately for display in the UI.
""" """
use Gettext, backend: MvWeb.Gettext use Gettext, backend: MvWeb.Gettext
alias MvWeb.Helpers.DateFormatter
@doc """ @doc """
Formats a custom field value for display. Formats a custom field value for display.
@ -61,11 +62,11 @@ defmodule MvWeb.MemberLive.Index.Formatter do
defp format_value_by_type(value, :boolean, _) when value == false, do: gettext("No") defp format_value_by_type(value, :boolean, _) when value == false, do: gettext("No")
defp format_value_by_type(value, :boolean, _), do: to_string(value) defp format_value_by_type(value, :boolean, _), do: to_string(value)
defp format_value_by_type(%Date{} = date, :date, _), do: Date.to_string(date) defp format_value_by_type(%Date{} = date, :date, _), do: DateFormatter.format_date(date)
defp format_value_by_type(value, :date, _) when is_binary(value) do defp format_value_by_type(value, :date, _) when is_binary(value) do
case Date.from_iso8601(value) do case Date.from_iso8601(value) do
{:ok, date} -> Date.to_string(date) {:ok, date} -> DateFormatter.format_date(date)
_ -> value _ -> value
end end
end end

View file

@ -23,6 +23,7 @@ defmodule MvWeb.MemberLive.Show do
""" """
use MvWeb, :live_view use MvWeb, :live_view
import Ash.Query import Ash.Query
alias MvWeb.Helpers.DateFormatter
@impl true @impl true
def render(assigns) do def render(assigns) do
@ -52,8 +53,8 @@ defmodule MvWeb.MemberLive.Show do
{if @member.paid, do: gettext("Yes"), else: gettext("No")} {if @member.paid, do: gettext("Yes"), else: gettext("No")}
</:item> </:item>
<:item title={gettext("Phone Number")}>{@member.phone_number}</:item> <:item title={gettext("Phone Number")}>{@member.phone_number}</:item>
<:item title={gettext("Join Date")}>{@member.join_date}</:item> <:item title={gettext("Join Date")}>{DateFormatter.format_date(@member.join_date)}</:item>
<:item title={gettext("Exit Date")}>{@member.exit_date}</:item> <:item title={gettext("Exit Date")}>{DateFormatter.format_date(@member.exit_date)}</:item>
<:item title={gettext("Notes")}>{@member.notes}</:item> <:item title={gettext("Notes")}>{@member.notes}</:item>
<:item title={gettext("City")}>{@member.city}</:item> <:item title={gettext("City")}>{@member.city}</:item>
<:item title={gettext("Street")}>{@member.street}</:item> <:item title={gettext("Street")}>{@member.street}</:item>
@ -81,10 +82,7 @@ defmodule MvWeb.MemberLive.Show do
# name # name
cfv.custom_field && cfv.custom_field.name, cfv.custom_field && cfv.custom_field.name,
# value # value
case cfv.value do format_custom_field_value(cfv)
%{value: v} -> v
v -> v
end
} }
end) end)
} /> } />
@ -114,4 +112,17 @@ defmodule MvWeb.MemberLive.Show do
defp page_title(:show), do: gettext("Show Member") defp page_title(:show), do: gettext("Show Member")
defp page_title(:edit), do: gettext("Edit Member") defp page_title(:edit), do: gettext("Edit Member")
defp format_custom_field_value(cfv) do
value =
case cfv.value do
%{value: v} -> v
v -> v
end
case value do
%Date{} = date -> DateFormatter.format_date(date)
other -> other
end
end
end end

View file

@ -42,7 +42,7 @@ defmodule MvWeb.UserLive.Form do
<:subtitle>{gettext("Use this form to manage user records in your database.")}</:subtitle> <:subtitle>{gettext("Use this form to manage user records in your database.")}</:subtitle>
</.header> </.header>
<.form for={@form} id="user-form" phx-change="validate" phx-submit="save"> <.form class="max-w-xl" for={@form} id="user-form" phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label={gettext("Email")} required type="email" /> <.input field={@form[:email]} label={gettext("Email")} required type="email" />
<!-- Password Section --> <!-- Password Section -->
@ -61,7 +61,7 @@ defmodule MvWeb.UserLive.Form do
</label> </label>
<%= if @show_password_fields do %> <%= if @show_password_fields do %>
<div class="mt-4 space-y-4 p-4 bg-gray-50 rounded-lg"> <div class="p-4 mt-4 space-y-4 rounded-lg bg-gray-50">
<.input <.input
field={@form[:password]} field={@form[:password]}
label={gettext("Password")} label={gettext("Password")}
@ -83,7 +83,7 @@ defmodule MvWeb.UserLive.Form do
<div class="text-sm text-gray-600"> <div class="text-sm text-gray-600">
<p><strong>{gettext("Password requirements")}:</strong></p> <p><strong>{gettext("Password requirements")}:</strong></p>
<ul class="list-disc list-inside text-xs mt-1 space-y-1"> <ul class="mt-1 space-y-1 text-xs list-disc list-inside">
<li>{gettext("At least 8 characters")}</li> <li>{gettext("At least 8 characters")}</li>
<li>{gettext("Include both letters and numbers")}</li> <li>{gettext("Include both letters and numbers")}</li>
<li>{gettext("Consider using special characters")}</li> <li>{gettext("Consider using special characters")}</li>
@ -91,7 +91,7 @@ defmodule MvWeb.UserLive.Form do
</div> </div>
<%= if @user do %> <%= if @user do %>
<div class="mt-3 p-3 bg-orange-50 border border-orange-200 rounded"> <div class="p-3 mt-3 border border-orange-200 rounded bg-orange-50">
<p class="text-sm text-orange-800"> <p class="text-sm text-orange-800">
<strong>{gettext("Admin Note")}:</strong> {gettext( <strong>{gettext("Admin Note")}:</strong> {gettext(
"As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system." "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
@ -102,7 +102,7 @@ defmodule MvWeb.UserLive.Form do
</div> </div>
<% else %> <% else %>
<%= if @user do %> <%= if @user do %>
<div class="mt-4 p-4 bg-blue-50 rounded-lg"> <div class="p-4 mt-4 rounded-lg bg-blue-50">
<p class="text-sm text-blue-800"> <p class="text-sm text-blue-800">
<strong>{gettext("Note")}:</strong> {gettext( <strong>{gettext("Note")}:</strong> {gettext(
"Check 'Change Password' above to set a new password for this user." "Check 'Change Password' above to set a new password for this user."
@ -110,7 +110,7 @@ defmodule MvWeb.UserLive.Form do
</p> </p>
</div> </div>
<% else %> <% else %>
<div class="mt-4 p-4 bg-yellow-50 rounded-lg"> <div class="p-4 mt-4 rounded-lg bg-yellow-50">
<p class="text-sm text-yellow-800"> <p class="text-sm text-yellow-800">
<strong>{gettext("Note")}:</strong> {gettext( <strong>{gettext("Note")}:</strong> {gettext(
"User will be created without a password. Check 'Set Password' to add one." "User will be created without a password. Check 'Set Password' to add one."
@ -123,11 +123,11 @@ defmodule MvWeb.UserLive.Form do
<!-- Member Linking Section --> <!-- Member Linking Section -->
<div class="mt-6"> <div class="mt-6">
<h2 class="text-base font-semibold mb-3">{gettext("Linked Member")}</h2> <h2 class="mb-3 text-base font-semibold">{gettext("Linked Member")}</h2>
<%= if @user && @user.member && !@unlink_member do %> <%= if @user && @user.member && !@unlink_member do %>
<!-- Show linked member with unlink button --> <!-- Show linked member with unlink button -->
<div class="p-4 bg-green-50 border border-green-200 rounded-lg"> <div class="p-4 border border-green-200 rounded-lg bg-green-50">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="font-medium text-green-900"> <p class="font-medium text-green-900">
@ -147,7 +147,7 @@ defmodule MvWeb.UserLive.Form do
<% else %> <% else %>
<%= if @unlink_member do %> <%= if @unlink_member do %>
<!-- Show unlink pending message --> <!-- Show unlink pending message -->
<div class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg"> <div class="p-4 border border-yellow-200 rounded-lg bg-yellow-50">
<p class="text-sm text-yellow-800"> <p class="text-sm text-yellow-800">
<strong>{gettext("Unlinking scheduled")}:</strong> {gettext( <strong>{gettext("Unlinking scheduled")}:</strong> {gettext(
"Member will be unlinked when you save. Cannot select new member until saved." "Member will be unlinked when you save. Cannot select new member until saved."
@ -219,7 +219,7 @@ defmodule MvWeb.UserLive.Form do
</div> </div>
<%= if @user && @user.email && @available_members != [] && Enum.all?(@available_members, &(&1.email == to_string(@user.email))) do %> <%= if @user && @user.email && @available_members != [] && Enum.all?(@available_members, &(&1.email == to_string(@user.email))) do %>
<div class="p-3 bg-yellow-50 border border-yellow-200 rounded"> <div class="p-3 border border-yellow-200 rounded bg-yellow-50">
<p class="text-sm text-yellow-800"> <p class="text-sm text-yellow-800">
<strong>{gettext("Note")}:</strong> {gettext( <strong>{gettext("Note")}:</strong> {gettext(
"A member with this email already exists. To link with a different member, please change one of the email addresses first." "A member with this email already exists. To link with a different member, please change one of the email addresses first."
@ -231,12 +231,12 @@ defmodule MvWeb.UserLive.Form do
<%= if @selected_member_id && @selected_member_name do %> <%= if @selected_member_id && @selected_member_name do %>
<div <div
id="member-selected" id="member-selected"
class="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg" class="p-3 mt-2 border border-blue-200 rounded-lg bg-blue-50"
> >
<p class="text-sm text-blue-800"> <p class="text-sm text-blue-800">
<strong>{gettext("Selected")}:</strong> {@selected_member_name} <strong>{gettext("Selected")}:</strong> {@selected_member_name}
</p> </p>
<p class="text-xs text-blue-600 mt-1"> <p class="mt-1 text-xs text-blue-600">
{gettext("Save to confirm linking.")} {gettext("Save to confirm linking.")}
</p> </p>
</div> </div>
@ -245,10 +245,12 @@ defmodule MvWeb.UserLive.Form do
<% end %> <% end %>
</div> </div>
<.button phx-disable-with={gettext("Saving...")} variant="primary"> <div class="mt-4">
{gettext("Save User")} <.button phx-disable-with={gettext("Saving...")} variant="primary">
</.button> {gettext("Save User")}
<.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")}</.button> </.button>
<.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")}</.button>
</div>
</.form> </.form>
</Layouts.app> </Layouts.app>
""" """

View file

@ -49,7 +49,6 @@
> >
{user.email} {user.email}
</:col> </:col>
<:col :let={user} label={gettext("OIDC ID")}>{user.oidc_id}</:col>
<:col :let={user} label={gettext("Linked Member")}> <:col :let={user} label={gettext("Linked Member")}>
<%= if user.member do %> <%= if user.member do %>
{user.member.first_name} {user.member.last_name} {user.member.first_name} {user.member.last_name}

View file

@ -46,9 +46,7 @@ defmodule MvWeb.UserLive.Show do
</.header> </.header>
<.list> <.list>
<:item title={gettext("ID")}>{@user.id}</:item>
<:item title={gettext("Email")}>{@user.email}</:item> <:item title={gettext("Email")}>{@user.email}</:item>
<:item title={gettext("OIDC ID")}>{@user.oidc_id || gettext("Not set")}</:item>
<:item title={gettext("Password Authentication")}> <:item title={gettext("Password Authentication")}>
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")} {if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
</:item> </:item>
@ -56,13 +54,13 @@ defmodule MvWeb.UserLive.Show do
<%= if @user.member do %> <%= if @user.member do %>
<.link <.link
navigate={~p"/members/#{@user.member}"} navigate={~p"/members/#{@user.member}"}
class="text-blue-600 hover:text-blue-800 underline" class="text-blue-600 underline hover:text-blue-800"
> >
<.icon name="hero-users" class="h-4 w-4 inline mr-1" /> <.icon name="hero-users" class="inline w-4 h-4 mr-1" />
{@user.member.first_name} {@user.member.last_name} {@user.member.first_name} {@user.member.last_name}
</.link> </.link>
<% else %> <% else %>
<span class="text-gray-500 italic">{gettext("No member linked")}</span> <span class="italic text-gray-500">{gettext("No member linked")}</span>
<% end %> <% end %>
</:item> </:item>
</.list> </.list>

View file

@ -55,12 +55,6 @@ defmodule MvWeb.Router do
live "/members/:id", MemberLive.Show, :show live "/members/:id", MemberLive.Show, :show
live "/members/:id/show/edit", MemberLive.Show, :edit live "/members/:id/show/edit", MemberLive.Show, :edit
live "/custom_fields", CustomFieldLive.Index, :index
live "/custom_fields/new", CustomFieldLive.Form, :new
live "/custom_fields/:id/edit", CustomFieldLive.Form, :edit
live "/custom_fields/:id", CustomFieldLive.Show, :show
live "/custom_fields/:id/show/edit", CustomFieldLive.Show, :edit
live "/custom_field_values", CustomFieldValueLive.Index, :index live "/custom_field_values", CustomFieldValueLive.Index, :index
live "/custom_field_values/new", CustomFieldValueLive.Form, :new live "/custom_field_values/new", CustomFieldValueLive.Form, :new
live "/custom_field_values/:id/edit", CustomFieldValueLive.Form, :edit live "/custom_field_values/:id/edit", CustomFieldValueLive.Form, :edit

View file

@ -34,18 +34,20 @@ msgstr "Verbindung wird wiederhergestellt"
msgid "City" msgid "City"
msgstr "Stadt" msgstr "Stadt"
#: lib/mv_web/live/custom_field_live/index_component.ex:82
#: lib/mv_web/live/member_live/index.html.heex:250 #: lib/mv_web/live/member_live/index.html.heex:250
#: lib/mv_web/live/user_live/index.html.heex:74 #: lib/mv_web/live/user_live/index.html.heex:74
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
msgstr "Löschen" msgstr "Löschen"
#: lib/mv_web/live/custom_field_live/index_component.ex:76
#: lib/mv_web/live/member_live/index.html.heex:242 #: lib/mv_web/live/member_live/index.html.heex:242
#: lib/mv_web/live/user_live/form.ex:265 #: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/index.html.heex:66 #: lib/mv_web/live/user_live/index.html.heex:66
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit" msgid "Edit"
msgstr "Bearbeite" msgstr "Bearbeiten"
#: lib/mv_web/live/member_live/show.ex:41 #: lib/mv_web/live/member_live/show.ex:41
#: lib/mv_web/live/member_live/show.ex:116 #: lib/mv_web/live/member_live/show.ex:116
@ -155,9 +157,9 @@ msgstr "Postleitzahl"
msgid "Save Member" msgid "Save Member"
msgstr "Mitglied speichern" msgstr "Mitglied speichern"
#: lib/mv_web/live/custom_field_live/form.ex:66 #: lib/mv_web/live/custom_field_live/form_component.ex:63
#: lib/mv_web/live/custom_field_value_live/form.ex:74 #: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/global_settings_live.ex:55 #: lib/mv_web/live/global_settings_live.ex:60
#: lib/mv_web/live/member_live/form.ex:78 #: lib/mv_web/live/member_live/form.ex:78
#: lib/mv_web/live/user_live/form.ex:248 #: lib/mv_web/live/user_live/form.ex:248
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -176,6 +178,7 @@ msgstr "Straße"
msgid "Id" msgid "Id"
msgstr "ID" msgstr "ID"
#: lib/mv_web/live/custom_field_live/index_component.ex:68
#: lib/mv_web/live/member_live/index.html.heex:234 #: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:61 #: lib/mv_web/live/member_live/index/formatter.ex:61
#: lib/mv_web/live/member_live/show.ex:52 #: lib/mv_web/live/member_live/show.ex:52
@ -193,6 +196,7 @@ msgstr "Mitglied anzeigen"
msgid "This is a member record from your database." msgid "This is a member record from your database."
msgstr "Dies ist ein Mitglied aus deiner Datenbank." msgstr "Dies ist ein Mitglied aus deiner Datenbank."
#: lib/mv_web/live/custom_field_live/index_component.ex:65
#: lib/mv_web/live/member_live/index.html.heex:234 #: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:60 #: lib/mv_web/live/member_live/index/formatter.ex:60
#: lib/mv_web/live/member_live/show.ex:52 #: lib/mv_web/live/member_live/show.ex:52
@ -200,14 +204,14 @@ msgstr "Dies ist ein Mitglied aus deiner Datenbank."
msgid "Yes" msgid "Yes"
msgstr "Ja" msgstr "Ja"
#: lib/mv_web/live/custom_field_live/form.ex:110 #: lib/mv_web/live/custom_field_live/form_component.ex:93
#: lib/mv_web/live/custom_field_value_live/form.ex:233 #: lib/mv_web/live/custom_field_value_live/form.ex:233
#: lib/mv_web/live/member_live/form.ex:137 #: lib/mv_web/live/member_live/form.ex:137
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "create" msgid "create"
msgstr "erstellt" msgstr "erstellt"
#: lib/mv_web/live/custom_field_live/form.ex:111 #: lib/mv_web/live/custom_field_live/form_component.ex:94
#: lib/mv_web/live/custom_field_value_live/form.ex:234 #: lib/mv_web/live/custom_field_value_live/form.ex:234
#: lib/mv_web/live/member_live/form.ex:138 #: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -249,8 +253,8 @@ msgstr "Ihre E-Mail-Adresse wurde bestätigt"
msgid "Your password has successfully been reset" msgid "Your password has successfully been reset"
msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt" msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
#: lib/mv_web/live/custom_field_live/form.ex:69 #: lib/mv_web/live/custom_field_live/form_component.ex:61
#: lib/mv_web/live/custom_field_live/index.ex:120 #: lib/mv_web/live/custom_field_live/index_component.ex:138
#: lib/mv_web/live/custom_field_value_live/form.ex:77 #: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/member_live/form.ex:81 #: lib/mv_web/live/member_live/form.ex:81
#: lib/mv_web/live/user_live/form.ex:251 #: lib/mv_web/live/user_live/form.ex:251
@ -263,7 +267,8 @@ msgstr "Abbrechen"
msgid "Choose a member" msgid "Choose a member"
msgstr "Mitglied auswählen" msgstr "Mitglied auswählen"
#: lib/mv_web/live/custom_field_live/form.ex:61 #: lib/mv_web/live/custom_field_live/form_component.ex:50
#: lib/mv_web/live/custom_field_live/index_component.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Description" msgid "Description"
msgstr "Beschreibung" msgstr "Beschreibung"
@ -283,7 +288,7 @@ msgstr "Aktiviert"
msgid "ID" msgid "ID"
msgstr "ID" msgstr "ID"
#: lib/mv_web/live/custom_field_live/form.ex:62 #: lib/mv_web/live/custom_field_live/form_component.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Immutable" msgid "Immutable"
msgstr "Unveränderlich" msgstr "Unveränderlich"
@ -311,7 +316,8 @@ msgstr "Mitglied"
msgid "Members" msgid "Members"
msgstr "Mitglieder" msgstr "Mitglieder"
#: lib/mv_web/live/custom_field_live/form.ex:51 #: lib/mv_web/live/custom_field_live/form_component.ex:40
#: lib/mv_web/live/custom_field_live/index_component.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
msgstr "Name" msgstr "Name"
@ -354,7 +360,7 @@ msgstr "Passwort-Authentifizierung"
msgid "Profil" msgid "Profil"
msgstr "Profil" msgstr "Profil"
#: lib/mv_web/live/custom_field_live/form.ex:63 #: lib/mv_web/live/custom_field_live/form_component.ex:52
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Required" msgid "Required"
msgstr "Erforderlich" msgstr "Erforderlich"
@ -369,7 +375,10 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member" msgid "Select member"
msgstr "Mitglied auswählen" msgstr "Mitglied auswählen"
#: lib/mv_web/components/layouts/navbar.ex:26
#: lib/mv_web/components/layouts/navbar.ex:99 #: lib/mv_web/components/layouts/navbar.ex:99
#: lib/mv_web/live/global_settings_live.ex:32
#: lib/mv_web/live/global_settings_live.ex:42
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
@ -410,7 +419,7 @@ msgstr "Benutzer*in"
msgid "Value" msgid "Value"
msgstr "Wert" msgstr "Wert"
#: lib/mv_web/live/custom_field_live/form.ex:56 #: lib/mv_web/live/custom_field_live/form_component.ex:45
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value type" msgid "Value type"
msgstr "Wertetyp" msgstr "Wertetyp"
@ -618,7 +627,7 @@ msgstr "Benutzerdefinierte Feldwerte"
msgid "Custom field" msgid "Custom field"
msgstr "Benutzerdefiniertes Feld" msgstr "Benutzerdefiniertes Feld"
#: lib/mv_web/live/custom_field_live/form.ex:117 #: lib/mv_web/live/global_settings_live.ex:105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Custom field %{action} successfully" msgid "Custom field %{action} successfully"
msgstr "Benutzerdefiniertes Feld erfolgreich %{action}" msgstr "Benutzerdefiniertes Feld erfolgreich %{action}"
@ -633,7 +642,7 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
msgid "Please select a custom field first" msgid "Please select a custom field first"
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld" msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
#: lib/mv_web/live/custom_field_live/form.ex:67 #: lib/mv_web/live/custom_field_live/form_component.ex:64
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save Custom field" msgid "Save Custom field"
msgstr "Benutzerdefiniertes Feld speichern" msgstr "Benutzerdefiniertes Feld speichern"
@ -643,12 +652,7 @@ msgstr "Benutzerdefiniertes Feld speichern"
msgid "Save Custom field value" msgid "Save Custom field value"
msgstr "Benutzerdefinierten Feldwert speichern" msgstr "Benutzerdefinierten Feldwert speichern"
#: lib/mv_web/live/custom_field_live/form.ex:46 #: lib/mv_web/live/custom_field_live/index_component.ex:20
#, elixir-autogen, elixir-format
msgid "Use this form to manage custom_field records in your database."
msgstr "Verwende dieses Formular, um Benutzerdefinierte Felder in deiner Datenbank zu verwalten."
#: lib/mv_web/components/layouts/navbar.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Custom Fields" msgid "Custom Fields"
msgstr "Benutzerdefinierte Felder" msgstr "Benutzerdefinierte Felder"
@ -658,70 +662,64 @@ msgstr "Benutzerdefinierte Felder"
msgid "Use this form to manage Custom Field Value records in your database." msgid "Use this form to manage Custom Field Value records in your database."
msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Datenbank zu verwalten." msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Datenbank zu verwalten."
#: lib/mv_web/live/custom_field_live/show.ex:56 #: lib/mv_web/live/custom_field_live/index_component.ex:97
#, elixir-autogen, elixir-format
msgid "Auto-generated identifier (immutable)"
msgstr "Automatisch generierter Bezeichner (unveränderlich)"
#: lib/mv_web/live/custom_field_live/index.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "%{count} member has a value assigned for this custom field." msgid "%{count} member has a value assigned for this custom field."
msgid_plural "%{count} members have values assigned for this custom field." msgid_plural "%{count} members have values assigned for this custom field."
msgstr[0] "%{count} Mitglied hat einen Wert für dieses benutzerdefinierte Feld zugewiesen." msgstr[0] "%{count} Mitglied hat einen Wert für dieses benutzerdefinierte Feld zugewiesen."
msgstr[1] "%{count} Mitglieder haben Werte für dieses benutzerdefinierte Feld zugewiesen." msgstr[1] "%{count} Mitglieder haben Werte für dieses benutzerdefinierte Feld zugewiesen."
#: lib/mv_web/live/custom_field_live/index.ex:87 #: lib/mv_web/live/custom_field_live/index_component.ex:105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "All custom field values will be permanently deleted when you delete this custom field." msgid "All custom field values will be permanently deleted when you delete this custom field."
msgstr "Alle benutzerdefinierten Feldwerte werden beim Löschen dieses benutzerdefinierten Feldes dauerhaft gelöscht." msgstr "Alle benutzerdefinierten Feldwerte werden beim Löschen dieses benutzerdefinierten Feldes dauerhaft gelöscht."
#: lib/mv_web/live/custom_field_live/index.ex:72 #: lib/mv_web/live/custom_field_live/index_component.ex:90
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete Custom Field" msgid "Delete Custom Field"
msgstr "Benutzerdefiniertes Feld löschen" msgstr "Benutzerdefiniertes Feld löschen"
#: lib/mv_web/live/custom_field_live/index.ex:127 #: lib/mv_web/live/custom_field_live/index_component.ex:146
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete Custom Field and All Values" msgid "Delete Custom Field and All Values"
msgstr "Benutzerdefiniertes Feld und alle Werte löschen" msgstr "Benutzerdefiniertes Feld und alle Werte löschen"
#: lib/mv_web/live/custom_field_live/index.ex:109 #: lib/mv_web/live/custom_field_live/index_component.ex:127
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Enter the text above to confirm" msgid "Enter the text above to confirm"
msgstr "Obigen Text zur Bestätigung eingeben" msgstr "Obigen Text zur Bestätigung eingeben"
#: lib/mv_web/live/custom_field_live/index.ex:97 #: lib/mv_web/live/custom_field_live/index_component.ex:115
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "To confirm deletion, please enter this text:" msgid "To confirm deletion, please enter this text:"
msgstr "Um die Löschung zu bestätigen, gib bitte folgenden Text ein:" msgstr "Um die Löschung zu bestätigen, gib bitte folgenden Text ein:"
#: lib/mv_web/live/custom_field_live/form.ex:64 #: lib/mv_web/live/custom_field_live/form_component.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show in overview" msgid "Show in overview"
msgstr "In der Mitglieder-Übersicht anzeigen" msgstr "In der Mitglieder-Übersicht anzeigen"
#: lib/mv_web/live/global_settings_live.ex:51 #: lib/mv_web/live/global_settings_live.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Association Name" msgid "Association Name"
msgstr "Vereinsname" msgstr "Vereinsname"
#: lib/mv_web/live/global_settings_live.ex:31 #: lib/mv_web/live/global_settings_live.ex:50
#: lib/mv_web/live/global_settings_live.ex:41
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Club Settings" msgid "Club Settings"
msgstr "Vereinsdaten" msgstr "Vereinsdaten"
#: lib/mv_web/live/global_settings_live.ex:43 #: lib/mv_web/live/global_settings_live.ex:44
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Manage global settings for the association." msgid "Manage global settings for the association."
msgstr "Passe übergreifende Einstellungen für den Verein an." msgstr "Passe übergreifende Einstellungen für den Verein an."
#: lib/mv_web/live/global_settings_live.ex:56 #: lib/mv_web/live/global_settings_live.ex:61
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Save Settings" msgid "Save Settings"
msgstr "Einstellungen speichern" msgstr "Einstellungen speichern"
#: lib/mv_web/live/global_settings_live.ex:75 #: lib/mv_web/live/global_settings_live.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings updated successfully" msgid "Settings updated successfully"
msgstr "Einstellungen erfolgreich gespeichert" msgstr "Einstellungen erfolgreich gespeichert"
@ -853,6 +851,51 @@ msgstr "Nicht bezahlt"
msgid "Payment filter" msgid "Payment filter"
msgstr "Zahlungsfilter" msgstr "Zahlungsfilter"
#: lib/mv_web/live/global_settings_live.ex:110
#, elixir-autogen, elixir-format, fuzzy
msgid "Custom field deleted successfully"
msgstr "Benutzerdefiniertes Feld erfolgreich %{action}"
#: lib/mv_web/live/custom_field_live/form_component.ex:29
#, elixir-autogen, elixir-format, fuzzy
msgid "Edit Custom Field"
msgstr "Benutzerdefiniertes Feld bearbeiten"
#: lib/mv_web/live/global_settings_live.ex:119
#, elixir-autogen, elixir-format
msgid "Failed to delete custom field: %{error}"
msgstr "Konnte benutzerdefiniertes Feld nicht löschen: %{error}"
#: lib/mv_web/live/custom_field_live/form_component.ex:29
#, elixir-autogen, elixir-format, fuzzy
msgid "New Custom Field"
msgstr "Neues Benutzerdefiniertes Feld"
#: lib/mv_web/live/custom_field_live/index_component.ex:26
#, elixir-autogen, elixir-format, fuzzy
msgid "New Custom field"
msgstr "Neues Benutzerdefiniertes Feld"
#: lib/mv_web/live/custom_field_live/index_component.ex:63
#, elixir-autogen, elixir-format, fuzzy
msgid "Show in Overview"
msgstr "In der Mitglieder-Übersicht anzeigen"
#: lib/mv_web/live/global_settings_live.ex:125
#, elixir-autogen, elixir-format
msgid "Slug does not match. Deletion cancelled."
msgstr "Eingegebener Text war nicht korrekt. Löschen wurde abgebrochen."
#: lib/mv_web/live/custom_field_live/index_component.ex:55
#, elixir-autogen, elixir-format, fuzzy
msgid "Value Type"
msgstr "Wertetyp"
#: lib/mv_web/live/custom_field_live/index_component.ex:22
#, elixir-autogen, elixir-format
msgid "These will appear in addition to other data when adding new members."
msgstr "Diese Felder können zusätzlich zu den normalen Daten ausgefüllt werden, wenn ein neues Mitglied angelegt wird."
#~ #: lib/mv_web/live/member_live/form.ex:48 #~ #: lib/mv_web/live/member_live/form.ex:48
#~ #: lib/mv_web/live/member_live/show.ex:51 #~ #: lib/mv_web/live/member_live/show.ex:51
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format

View file

@ -35,12 +35,14 @@ msgstr ""
msgid "City" msgid "City"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:82
#: lib/mv_web/live/member_live/index.html.heex:250 #: lib/mv_web/live/member_live/index.html.heex:250
#: lib/mv_web/live/user_live/index.html.heex:74 #: lib/mv_web/live/user_live/index.html.heex:74
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:76
#: lib/mv_web/live/member_live/index.html.heex:242 #: lib/mv_web/live/member_live/index.html.heex:242
#: lib/mv_web/live/user_live/form.ex:265 #: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/index.html.heex:66 #: lib/mv_web/live/user_live/index.html.heex:66
@ -156,9 +158,9 @@ msgstr ""
msgid "Save Member" msgid "Save Member"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:66 #: lib/mv_web/live/custom_field_live/form_component.ex:63
#: lib/mv_web/live/custom_field_value_live/form.ex:74 #: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/global_settings_live.ex:55 #: lib/mv_web/live/global_settings_live.ex:60
#: lib/mv_web/live/member_live/form.ex:78 #: lib/mv_web/live/member_live/form.ex:78
#: lib/mv_web/live/user_live/form.ex:248 #: lib/mv_web/live/user_live/form.ex:248
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -177,6 +179,7 @@ msgstr ""
msgid "Id" msgid "Id"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:68
#: lib/mv_web/live/member_live/index.html.heex:234 #: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:61 #: lib/mv_web/live/member_live/index/formatter.ex:61
#: lib/mv_web/live/member_live/show.ex:52 #: lib/mv_web/live/member_live/show.ex:52
@ -194,6 +197,7 @@ msgstr ""
msgid "This is a member record from your database." msgid "This is a member record from your database."
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:65
#: lib/mv_web/live/member_live/index.html.heex:234 #: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:60 #: lib/mv_web/live/member_live/index/formatter.ex:60
#: lib/mv_web/live/member_live/show.ex:52 #: lib/mv_web/live/member_live/show.ex:52
@ -201,14 +205,14 @@ msgstr ""
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:110 #: lib/mv_web/live/custom_field_live/form_component.ex:93
#: lib/mv_web/live/custom_field_value_live/form.ex:233 #: lib/mv_web/live/custom_field_value_live/form.ex:233
#: lib/mv_web/live/member_live/form.ex:137 #: lib/mv_web/live/member_live/form.ex:137
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "create" msgid "create"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:111 #: lib/mv_web/live/custom_field_live/form_component.ex:94
#: lib/mv_web/live/custom_field_value_live/form.ex:234 #: lib/mv_web/live/custom_field_value_live/form.ex:234
#: lib/mv_web/live/member_live/form.ex:138 #: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -250,8 +254,8 @@ msgstr ""
msgid "Your password has successfully been reset" msgid "Your password has successfully been reset"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:69 #: lib/mv_web/live/custom_field_live/form_component.ex:61
#: lib/mv_web/live/custom_field_live/index.ex:120 #: lib/mv_web/live/custom_field_live/index_component.ex:138
#: lib/mv_web/live/custom_field_value_live/form.ex:77 #: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/member_live/form.ex:81 #: lib/mv_web/live/member_live/form.ex:81
#: lib/mv_web/live/user_live/form.ex:251 #: lib/mv_web/live/user_live/form.ex:251
@ -264,7 +268,8 @@ msgstr ""
msgid "Choose a member" msgid "Choose a member"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:61 #: lib/mv_web/live/custom_field_live/form_component.ex:50
#: lib/mv_web/live/custom_field_live/index_component.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Description" msgid "Description"
msgstr "" msgstr ""
@ -284,7 +289,7 @@ msgstr ""
msgid "ID" msgid "ID"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:62 #: lib/mv_web/live/custom_field_live/form_component.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Immutable" msgid "Immutable"
msgstr "" msgstr ""
@ -312,7 +317,8 @@ msgstr ""
msgid "Members" msgid "Members"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:51 #: lib/mv_web/live/custom_field_live/form_component.ex:40
#: lib/mv_web/live/custom_field_live/index_component.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
msgstr "" msgstr ""
@ -355,7 +361,7 @@ msgstr ""
msgid "Profil" msgid "Profil"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:63 #: lib/mv_web/live/custom_field_live/form_component.ex:52
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Required" msgid "Required"
msgstr "" msgstr ""
@ -370,7 +376,10 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:26
#: lib/mv_web/components/layouts/navbar.ex:99 #: lib/mv_web/components/layouts/navbar.ex:99
#: lib/mv_web/live/global_settings_live.ex:32
#: lib/mv_web/live/global_settings_live.ex:42
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
@ -411,7 +420,7 @@ msgstr ""
msgid "Value" msgid "Value"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:56 #: lib/mv_web/live/custom_field_live/form_component.ex:45
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value type" msgid "Value type"
msgstr "" msgstr ""
@ -619,7 +628,7 @@ msgstr ""
msgid "Custom field" msgid "Custom field"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:117 #: lib/mv_web/live/global_settings_live.ex:105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Custom field %{action} successfully" msgid "Custom field %{action} successfully"
msgstr "" msgstr ""
@ -634,7 +643,7 @@ msgstr ""
msgid "Please select a custom field first" msgid "Please select a custom field first"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:67 #: lib/mv_web/live/custom_field_live/form_component.ex:64
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save Custom field" msgid "Save Custom field"
msgstr "" msgstr ""
@ -644,12 +653,7 @@ msgstr ""
msgid "Save Custom field value" msgid "Save Custom field value"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:46 #: lib/mv_web/live/custom_field_live/index_component.ex:20
#, elixir-autogen, elixir-format
msgid "Use this form to manage custom_field records in your database."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Custom Fields" msgid "Custom Fields"
msgstr "" msgstr ""
@ -659,70 +663,64 @@ msgstr ""
msgid "Use this form to manage Custom Field Value records in your database." msgid "Use this form to manage Custom Field Value records in your database."
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/show.ex:56 #: lib/mv_web/live/custom_field_live/index_component.ex:97
#, elixir-autogen, elixir-format
msgid "Auto-generated identifier (immutable)"
msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "%{count} member has a value assigned for this custom field." msgid "%{count} member has a value assigned for this custom field."
msgid_plural "%{count} members have values assigned for this custom field." msgid_plural "%{count} members have values assigned for this custom field."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: lib/mv_web/live/custom_field_live/index.ex:87 #: lib/mv_web/live/custom_field_live/index_component.ex:105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "All custom field values will be permanently deleted when you delete this custom field." msgid "All custom field values will be permanently deleted when you delete this custom field."
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:72 #: lib/mv_web/live/custom_field_live/index_component.ex:90
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete Custom Field" msgid "Delete Custom Field"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:127 #: lib/mv_web/live/custom_field_live/index_component.ex:146
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete Custom Field and All Values" msgid "Delete Custom Field and All Values"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:109 #: lib/mv_web/live/custom_field_live/index_component.ex:127
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Enter the text above to confirm" msgid "Enter the text above to confirm"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:97 #: lib/mv_web/live/custom_field_live/index_component.ex:115
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "To confirm deletion, please enter this text:" msgid "To confirm deletion, please enter this text:"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:64 #: lib/mv_web/live/custom_field_live/form_component.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show in overview" msgid "Show in overview"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:51 #: lib/mv_web/live/global_settings_live.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Association Name" msgid "Association Name"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:31 #: lib/mv_web/live/global_settings_live.ex:50
#: lib/mv_web/live/global_settings_live.ex:41
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Club Settings" msgid "Club Settings"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:43 #: lib/mv_web/live/global_settings_live.ex:44
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Manage global settings for the association." msgid "Manage global settings for the association."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:56 #: lib/mv_web/live/global_settings_live.ex:61
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save Settings" msgid "Save Settings"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:75 #: lib/mv_web/live/global_settings_live.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings updated successfully" msgid "Settings updated successfully"
msgstr "" msgstr ""
@ -853,3 +851,48 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Payment filter" msgid "Payment filter"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:110
#, elixir-autogen, elixir-format
msgid "Custom field deleted successfully"
msgstr ""
#: lib/mv_web/live/custom_field_live/form_component.ex:29
#, elixir-autogen, elixir-format
msgid "Edit Custom Field"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:119
#, elixir-autogen, elixir-format
msgid "Failed to delete custom field: %{error}"
msgstr ""
#: lib/mv_web/live/custom_field_live/form_component.ex:29
#, elixir-autogen, elixir-format
msgid "New Custom Field"
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:26
#, elixir-autogen, elixir-format
msgid "New Custom field"
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:63
#, elixir-autogen, elixir-format
msgid "Show in Overview"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:125
#, elixir-autogen, elixir-format
msgid "Slug does not match. Deletion cancelled."
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:55
#, elixir-autogen, elixir-format
msgid "Value Type"
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:22
#, elixir-autogen, elixir-format
msgid "These will appear in addition to other data when adding new members."
msgstr ""

View file

@ -35,12 +35,14 @@ msgstr ""
msgid "City" msgid "City"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:82
#: lib/mv_web/live/member_live/index.html.heex:250 #: lib/mv_web/live/member_live/index.html.heex:250
#: lib/mv_web/live/user_live/index.html.heex:74 #: lib/mv_web/live/user_live/index.html.heex:74
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:76
#: lib/mv_web/live/member_live/index.html.heex:242 #: lib/mv_web/live/member_live/index.html.heex:242
#: lib/mv_web/live/user_live/form.ex:265 #: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/index.html.heex:66 #: lib/mv_web/live/user_live/index.html.heex:66
@ -156,9 +158,9 @@ msgstr ""
msgid "Save Member" msgid "Save Member"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:66 #: lib/mv_web/live/custom_field_live/form_component.ex:63
#: lib/mv_web/live/custom_field_value_live/form.ex:74 #: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/global_settings_live.ex:55 #: lib/mv_web/live/global_settings_live.ex:60
#: lib/mv_web/live/member_live/form.ex:78 #: lib/mv_web/live/member_live/form.ex:78
#: lib/mv_web/live/user_live/form.ex:248 #: lib/mv_web/live/user_live/form.ex:248
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -177,6 +179,7 @@ msgstr ""
msgid "Id" msgid "Id"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:68
#: lib/mv_web/live/member_live/index.html.heex:234 #: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:61 #: lib/mv_web/live/member_live/index/formatter.ex:61
#: lib/mv_web/live/member_live/show.ex:52 #: lib/mv_web/live/member_live/show.ex:52
@ -194,6 +197,7 @@ msgstr ""
msgid "This is a member record from your database." msgid "This is a member record from your database."
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:65
#: lib/mv_web/live/member_live/index.html.heex:234 #: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:60 #: lib/mv_web/live/member_live/index/formatter.ex:60
#: lib/mv_web/live/member_live/show.ex:52 #: lib/mv_web/live/member_live/show.ex:52
@ -201,14 +205,14 @@ msgstr ""
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:110 #: lib/mv_web/live/custom_field_live/form_component.ex:93
#: lib/mv_web/live/custom_field_value_live/form.ex:233 #: lib/mv_web/live/custom_field_value_live/form.ex:233
#: lib/mv_web/live/member_live/form.ex:137 #: lib/mv_web/live/member_live/form.ex:137
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "create" msgid "create"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:111 #: lib/mv_web/live/custom_field_live/form_component.ex:94
#: lib/mv_web/live/custom_field_value_live/form.ex:234 #: lib/mv_web/live/custom_field_value_live/form.ex:234
#: lib/mv_web/live/member_live/form.ex:138 #: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -250,8 +254,8 @@ msgstr ""
msgid "Your password has successfully been reset" msgid "Your password has successfully been reset"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:69 #: lib/mv_web/live/custom_field_live/form_component.ex:61
#: lib/mv_web/live/custom_field_live/index.ex:120 #: lib/mv_web/live/custom_field_live/index_component.ex:138
#: lib/mv_web/live/custom_field_value_live/form.ex:77 #: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/member_live/form.ex:81 #: lib/mv_web/live/member_live/form.ex:81
#: lib/mv_web/live/user_live/form.ex:251 #: lib/mv_web/live/user_live/form.ex:251
@ -264,7 +268,8 @@ msgstr ""
msgid "Choose a member" msgid "Choose a member"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:61 #: lib/mv_web/live/custom_field_live/form_component.ex:50
#: lib/mv_web/live/custom_field_live/index_component.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Description" msgid "Description"
msgstr "" msgstr ""
@ -284,7 +289,7 @@ msgstr ""
msgid "ID" msgid "ID"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:62 #: lib/mv_web/live/custom_field_live/form_component.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Immutable" msgid "Immutable"
msgstr "" msgstr ""
@ -312,7 +317,8 @@ msgstr ""
msgid "Members" msgid "Members"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:51 #: lib/mv_web/live/custom_field_live/form_component.ex:40
#: lib/mv_web/live/custom_field_live/index_component.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
msgstr "" msgstr ""
@ -355,7 +361,7 @@ msgstr ""
msgid "Profil" msgid "Profil"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:63 #: lib/mv_web/live/custom_field_live/form_component.ex:52
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Required" msgid "Required"
msgstr "" msgstr ""
@ -370,7 +376,10 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:26
#: lib/mv_web/components/layouts/navbar.ex:99 #: lib/mv_web/components/layouts/navbar.ex:99
#: lib/mv_web/live/global_settings_live.ex:32
#: lib/mv_web/live/global_settings_live.ex:42
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
@ -411,7 +420,7 @@ msgstr ""
msgid "Value" msgid "Value"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:56 #: lib/mv_web/live/custom_field_live/form_component.ex:45
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value type" msgid "Value type"
msgstr "" msgstr ""
@ -619,7 +628,7 @@ msgstr ""
msgid "Custom field" msgid "Custom field"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:117 #: lib/mv_web/live/global_settings_live.ex:105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Custom field %{action} successfully" msgid "Custom field %{action} successfully"
msgstr "" msgstr ""
@ -634,7 +643,7 @@ msgstr ""
msgid "Please select a custom field first" msgid "Please select a custom field first"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:67 #: lib/mv_web/live/custom_field_live/form_component.ex:64
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save Custom field" msgid "Save Custom field"
msgstr "" msgstr ""
@ -644,12 +653,7 @@ msgstr ""
msgid "Save Custom field value" msgid "Save Custom field value"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:46 #: lib/mv_web/live/custom_field_live/index_component.ex:20
#, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage custom_field records in your database."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:26
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Custom Fields" msgid "Custom Fields"
msgstr "" msgstr ""
@ -659,70 +663,64 @@ msgstr ""
msgid "Use this form to manage Custom Field Value records in your database." msgid "Use this form to manage Custom Field Value records in your database."
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/show.ex:56 #: lib/mv_web/live/custom_field_live/index_component.ex:97
#, elixir-autogen, elixir-format
msgid "Auto-generated identifier (immutable)"
msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "%{count} member has a value assigned for this custom field." msgid "%{count} member has a value assigned for this custom field."
msgid_plural "%{count} members have values assigned for this custom field." msgid_plural "%{count} members have values assigned for this custom field."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: lib/mv_web/live/custom_field_live/index.ex:87 #: lib/mv_web/live/custom_field_live/index_component.ex:105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "All custom field values will be permanently deleted when you delete this custom field." msgid "All custom field values will be permanently deleted when you delete this custom field."
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:72 #: lib/mv_web/live/custom_field_live/index_component.ex:90
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete Custom Field" msgid "Delete Custom Field"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:127 #: lib/mv_web/live/custom_field_live/index_component.ex:146
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete Custom Field and All Values" msgid "Delete Custom Field and All Values"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:109 #: lib/mv_web/live/custom_field_live/index_component.ex:127
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Enter the text above to confirm" msgid "Enter the text above to confirm"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:97 #: lib/mv_web/live/custom_field_live/index_component.ex:115
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "To confirm deletion, please enter this text:" msgid "To confirm deletion, please enter this text:"
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:64 #: lib/mv_web/live/custom_field_live/form_component.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show in overview" msgid "Show in overview"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:51 #: lib/mv_web/live/global_settings_live.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Association Name" msgid "Association Name"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:31 #: lib/mv_web/live/global_settings_live.ex:50
#: lib/mv_web/live/global_settings_live.ex:41
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Club Settings" msgid "Club Settings"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:43 #: lib/mv_web/live/global_settings_live.ex:44
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Manage global settings for the association." msgid "Manage global settings for the association."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:56 #: lib/mv_web/live/global_settings_live.ex:61
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Save Settings" msgid "Save Settings"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:75 #: lib/mv_web/live/global_settings_live.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings updated successfully" msgid "Settings updated successfully"
msgstr "" msgstr ""
@ -854,8 +852,63 @@ msgstr ""
msgid "Payment filter" msgid "Payment filter"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex:110
#, elixir-autogen, elixir-format, fuzzy
msgid "Custom field deleted successfully"
msgstr ""
#: lib/mv_web/live/custom_field_live/form_component.ex:29
#, elixir-autogen, elixir-format, fuzzy
msgid "Edit Custom Field"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:119
#, elixir-autogen, elixir-format
msgid "Failed to delete custom field: %{error}"
msgstr ""
#: lib/mv_web/live/custom_field_live/form_component.ex:29
#, elixir-autogen, elixir-format, fuzzy
msgid "New Custom Field"
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:26
#, elixir-autogen, elixir-format, fuzzy
msgid "New Custom field"
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:63
#, elixir-autogen, elixir-format, fuzzy
msgid "Show in Overview"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:125
#, elixir-autogen, elixir-format
msgid "Slug does not match. Deletion cancelled."
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:55
#, elixir-autogen, elixir-format, fuzzy
msgid "Value Type"
msgstr ""
#: lib/mv_web/live/custom_field_live/index_component.ex:22
#, elixir-autogen, elixir-format
msgid "These will appear in addition to other data when adding new members."
msgstr ""
#~ #: lib/mv_web/live/custom_field_live/show.ex:56
#~ #, elixir-autogen, elixir-format
#~ msgid "Auto-generated identifier (immutable)"
#~ msgstr ""
#~ #: lib/mv_web/live/member_live/form.ex:48 #~ #: lib/mv_web/live/member_live/form.ex:48
#~ #: lib/mv_web/live/member_live/show.ex:51 #~ #: lib/mv_web/live/member_live/show.ex:51
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Birth Date" #~ msgid "Birth Date"
#~ msgstr "" #~ msgstr ""
#~ #: lib/mv_web/live/custom_field_live/form.ex:46
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Use this form to manage custom_field records in your database."
#~ msgstr ""

View file

@ -1,14 +0,0 @@
defmodule Mv.Membership.MemberFieldVisibilityTest do
@moduledoc """
Tests for member field visibility configuration.
Tests cover:
- Member fields are visible by default (show_in_overview: true)
- Member fields can be hidden (show_in_overview: false)
- Checking if a specific field is visible
- Configuration is stored in Settings resource
"""
use Mv.DataCase, async: true
alias Mv.Membership.Member
end

View file

@ -1,6 +1,7 @@
defmodule MvWeb.CustomFieldLive.DeletionTest do defmodule MvWeb.CustomFieldLive.DeletionTest do
@moduledoc """ @moduledoc """
Tests for CustomFieldLive.Index deletion modal and slug confirmation. Tests for CustomFieldLive.IndexComponent deletion modal and slug confirmation.
Tests the custom field management component embedded in the settings page.
Tests cover: Tests cover:
- Opening deletion confirmation modal - Opening deletion confirmation modal
@ -39,11 +40,11 @@ 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"/custom_fields") {:ok, view, _html} = live(conn, ~p"/settings")
# Click delete button # Click delete button - find the delete link within the component
view view
|> element("a", "Delete") |> element("#custom-fields-component a", "Delete")
|> render_click() |> render_click()
# Modal should be visible # Modal should be visible
@ -65,10 +66,10 @@ 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"/custom_fields") {:ok, view, _html} = live(conn, ~p"/settings")
view view
|> element("a", "Delete") |> element("#custom-fields-component a", "Delete")
|> render_click() |> render_click()
# Should show plural form # Should show plural form
@ -78,10 +79,10 @@ 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"/custom_fields") {:ok, view, _html} = live(conn, ~p"/settings")
view view
|> element("a", "Delete") |> element("#custom-fields-component a", "Delete")
|> render_click() |> render_click()
# Should show 0 members # Should show 0 members
@ -93,15 +94,16 @@ 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"/custom_fields") {:ok, view, _html} = live(conn, ~p"/settings")
view view
|> element("a", "Delete") |> element("#custom-fields-component a", "Delete")
|> render_click() |> render_click()
# Type in slug input # Type in slug input - use element to find the form with phx-target
view view
|> render_change("update_slug_confirmation", %{"slug" => custom_field.slug}) |> element("#delete-custom-field-modal form")
|> render_change(%{"slug" => custom_field.slug})
# Confirm button should be enabled now (no disabled attribute) # Confirm button should be enabled now (no disabled attribute)
html = render(view) html = render(view)
@ -111,15 +113,16 @@ 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"/custom_fields") {:ok, view, _html} = live(conn, ~p"/settings")
view view
|> element("a", "Delete") |> element("#custom-fields-component a", "Delete")
|> render_click() |> render_click()
# Type wrong slug # Type wrong slug - use element to find the form with phx-target
view view
|> render_change("update_slug_confirmation", %{"slug" => "wrong-slug"}) |> element("#delete-custom-field-modal form")
|> render_change(%{"slug" => "wrong-slug"})
# Button should be disabled # Button should be disabled
html = render(view) html = render(view)
@ -133,20 +136,21 @@ 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"/custom_fields") {:ok, view, _html} = live(conn, ~p"/settings")
# Open modal # Open modal
view view
|> element("a", "Delete") |> element("#custom-fields-component a", "Delete")
|> render_click() |> render_click()
# Enter correct slug # Enter correct slug - use element to find the form with phx-target
view view
|> render_change("update_slug_confirmation", %{"slug" => custom_field.slug}) |> element("#delete-custom-field-modal form")
|> render_change(%{"slug" => custom_field.slug})
# Click confirm # Click confirm
view view
|> element("button", "Delete Custom Field and All Values") |> element("#delete-custom-field-modal button", "Delete Custom Field and All Values")
|> render_click() |> render_click()
# Should show success message # Should show success message
@ -162,27 +166,28 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
assert {:ok, _} = Ash.get(Member, member.id) assert {:ok, _} = Ash.get(Member, member.id)
end end
test "shows error when slug doesn't match", %{conn: conn} do test "button remains disabled and custom field not deleted 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"/custom_fields") {:ok, view, _html} = live(conn, ~p"/settings")
view view
|> element("a", "Delete") |> element("#custom-fields-component a", "Delete")
|> render_click() |> render_click()
# Enter wrong slug # Enter wrong slug - use element to find the form with phx-target
view view
|> render_change("update_slug_confirmation", %{"slug" => "wrong-slug"}) |> element("#delete-custom-field-modal form")
|> render_change(%{"slug" => "wrong-slug"})
# Try to confirm (button should be disabled, but test the handler anyway) # Button should be disabled and we cannot click it
view # The test verifies that the button is properly disabled in the UI
|> render_click("confirm_delete", %{}) html = render(view)
assert html =~ ~r/disabled(?:=""|(?!\w))/
# Should show error message # Custom field should still exist since deletion couldn't proceed
assert render(view) =~ "Slug does not match"
# Custom field should still exist
assert {:ok, _} = Ash.get(CustomField, custom_field.id) assert {:ok, _} = Ash.get(CustomField, custom_field.id)
end end
end end
@ -191,10 +196,10 @@ 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"/custom_fields") {:ok, view, _html} = live(conn, ~p"/settings")
view view
|> element("a", "Delete") |> element("#custom-fields-component a", "Delete")
|> render_click() |> render_click()
# Modal should be visible # Modal should be visible
@ -202,7 +207,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
# Click cancel # Click cancel
view view
|> element("button", "Cancel") |> element("#delete-custom-field-modal button", "Cancel")
|> render_click() |> render_click()
# Modal should be gone # Modal should be gone

View file

@ -90,8 +90,6 @@ defmodule MvWeb.ProfileNavigationTest do
# Verify we're on the correct profile page with OIDC specific information # Verify we're on the correct profile page with OIDC specific information
{:ok, _profile_view, html} = live(conn, "/users/#{user.id}") {:ok, _profile_view, html} = live(conn, "/users/#{user.id}")
assert html =~ to_string(user.email) assert html =~ to_string(user.email)
# OIDC ID should be visible
assert html =~ "oidc_123"
# Password auth should be disabled for OIDC users # Password auth should be disabled for OIDC users
assert html =~ "Not enabled" assert html =~ "Not enabled"
end end
@ -150,8 +148,6 @@ defmodule MvWeb.ProfileNavigationTest do
"/members/new", "/members/new",
"/custom_field_values", "/custom_field_values",
"/custom_field_values/new", "/custom_field_values/new",
"/custom_fields",
"/custom_fields/new",
"/users", "/users",
"/users/new" "/users/new"
] ]

View file

@ -231,8 +231,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members") {:ok, _view, html} = live(conn, "/members")
# Date should be displayed in readable format # Date should be displayed in European format (dd.mm.yyyy)
assert html =~ "1990" or html =~ "1990-05-15" or html =~ "15.05.1990" assert html =~ "15.05.1990"
end end
test "formats email custom field values correctly", %{conn: conn, member1: _member1} do test "formats email custom field values correctly", %{conn: conn, member1: _member1} do

View file

@ -33,8 +33,6 @@ defmodule MvWeb.UserLive.IndexTest do
assert html =~ "alice@example.com" assert html =~ "alice@example.com"
assert html =~ "bob@example.com" assert html =~ "bob@example.com"
assert html =~ "alice123"
assert html =~ "bob456"
end end
test "shows correct action links", %{conn: conn} do test "shows correct action links", %{conn: conn} do
@ -386,10 +384,6 @@ defmodule MvWeb.UserLive.IndexTest do
# Should still show the table structure # Should still show the table structure
assert html =~ "Email" assert html =~ "Email"
assert html =~ "OIDC ID"
# Should show the authenticated user at minimum
# Matches the generated email pattern oidc.user{unique_id}@example.com
assert html =~ "oidc.user"
end end
test "handles users with missing OIDC ID", %{conn: conn} do test "handles users with missing OIDC ID", %{conn: conn} do