diff --git a/.gitignore b/.gitignore index 63ff39e..9517a21 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ npm-debug.log .env .elixir_ls/ + +# Docker secrets directory (generated by `just init-secrets`) +/secrets/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d9147..28b4a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - Button shows count of visible selected members (respects search/filter) - 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 - Email validation false positive when linking user and member with identical emails (#168 Problem #4) diff --git a/Justfile b/Justfile index a91c0e4..876591d 100644 --- a/Justfile +++ b/Justfile @@ -90,4 +90,27 @@ clean: remove-gettext-conflicts: #!/usr/bin/env bash set -euo pipefail - find priv/gettext -type f -exec sed -i '/^<<<<<<< HEAD$/d; /^=======$/d; /^>>>>>>>/d' {} \; \ No newline at end of file + 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 \ No newline at end of file diff --git a/README.md b/README.md index 6db7980..14435db 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,13 @@ For testing the production Docker build locally: # OIDC_BASE_URL=http://localhost:8080/auth/v1 # OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback # OIDC_CLIENT_SECRET= + + # 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): @@ -250,7 +257,7 @@ For actual production deployment: - Set `OIDC_BASE_URL` to your production OIDC provider - Configure proper Docker networks 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** diff --git a/config/runtime.exs b/config/runtime.exs index c50356c..71138ef 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -7,6 +7,75 @@ import Config # any compile-time configuration in here, as it won't be applied. # 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 # # If you use `mix release`, you need to explicitly enable the server @@ -21,12 +90,7 @@ if System.get_env("PHX_SERVER") do end if config_env() == :prod do - database_url = - System.get_env("DATABASE_URL") || - raise """ - environment variable DATABASE_URL is missing. - For example: ecto://USER:PASS@HOST/DATABASE - """ + database_url = build_database_url.() 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 # to check this value into version control, so we use an environment # variable instead. + # Supports SECRET_KEY_BASE or SECRET_KEY_BASE_FILE for Docker secrets. secret_key_base = - System.get_env("SECRET_KEY_BASE") || - raise """ - environment variable SECRET_KEY_BASE is missing. - You can generate one by calling: mix phx.gen.secret - """ + get_env_or_file!.("SECRET_KEY_BASE", """ + environment variable SECRET_KEY_BASE (or SECRET_KEY_BASE_FILE) is missing. + You can generate one by calling: mix phx.gen.secret + """) host = System.get_env("PHX_HOST") || raise "Please define the PHX_HOST environment variable." 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") # 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, - client_id: System.get_env("OIDC_CLIENT_ID") || "mv", - base_url: System.get_env("OIDC_BASE_URL") || "http://localhost:8080/auth/v1", - client_secret: System.get_env("OIDC_CLIENT_SECRET"), + client_id: oidc_client_id || "mv", + base_url: oidc_base_url || "http://localhost:8080/auth/v1", + client_secret: client_secret, redirect_uri: System.get_env("OIDC_REDIRECT_URI") || "http://#{host}:#{port}/auth/user/rauthy/callback" # Token signing secret from environment variable # This overrides the placeholder value set in prod.exs + # Supports TOKEN_SIGNING_SECRET or TOKEN_SIGNING_SECRET_FILE for Docker secrets. token_signing_secret = - System.get_env("TOKEN_SIGNING_SECRET") || - raise """ - environment variable TOKEN_SIGNING_SECRET is missing. - You can generate one by calling: mix phx.gen.secret - """ + get_env_or_file!.("TOKEN_SIGNING_SECRET", """ + environment variable TOKEN_SIGNING_SECRET (or TOKEN_SIGNING_SECRET_FILE) is missing. + You can generate one by calling: mix phx.gen.secret + """) config :mv, :token_signing_secret, token_signing_secret config :mv, MvWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ - # Enable IPv6 and bind on all interfaces. - # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. + # Bind on all IPv4 interfaces. + # 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 - # for details about using IPv6 vs IPv4 and loopback vs public addresses. - ip: {0, 0, 0, 0, 0, 0, 0, 0}, + ip: {0, 0, 0, 0}, port: port ], secret_key_base: secret_key_base, diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0bb2840..b4b7a1f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -2,21 +2,32 @@ services: app: image: git.local-it.org/local-it/mitgliederverwaltung:latest container_name: mv-prod-app - # Use host network for local testing to access localhost:8080 (Rauthy) - # In real production, remove this and use external OIDC provider - network_mode: host + ports: + - "4001:4001" environment: - DATABASE_URL: "ecto://postgres:postgres@localhost:5001/mv_prod" - SECRET_KEY_BASE: "${SECRET_KEY_BASE}" - TOKEN_SIGNING_SECRET: "${TOKEN_SIGNING_SECRET}" - PHX_HOST: "${PHX_HOST}" + # Database configuration using separate variables + # Use Docker service name for internal networking + DATABASE_HOST: "db-prod" + 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" 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_BASE_URL: "http://localhost:8080/auth/v1" - OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET:-}" + OIDC_BASE_URL: "http://host.docker.internal:8080/auth/v1" + OIDC_CLIENT_SECRET_FILE: "/run/secrets/oidc_client_secret" OIDC_REDIRECT_URI: "http://localhost:4001/auth/user/rauthy/callback" + secrets: + - db_password + - secret_key_base + - token_signing_secret + - oidc_client_secret depends_on: - db-prod restart: unless-stopped @@ -26,13 +37,25 @@ services: container_name: mv-prod-db environment: POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres + POSTGRES_PASSWORD_FILE: /run/secrets/db_password POSTGRES_DB: mv_prod + secrets: + - db_password volumes: - postgres_data_prod:/var/lib/postgresql/data ports: - "5001:5432" 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: postgres_data_prod: diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index 54a5a64..08133b5 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -79,7 +79,7 @@ defmodule MvWeb.CoreComponents do

{msg}

-
@@ -368,61 +368,63 @@ defmodule MvWeb.CoreComponents do end ~H""" - - - - - - - - - - - - + + + +
{col[:label]} - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:"sort_custom_field_#{dyn_col[:custom_field].id}"} - field={"custom_field_#{dyn_col[:custom_field].id}"} - label={dyn_col[:custom_field].name} - sort_field={@sort_field} - sort_order={@sort_order} - /> - - {gettext("Actions")} -
- {render_slot(col, @row_item.(row))} - - {if dyn_col[:render] do - rendered = dyn_col[:render].(@row_item.(row)) +
+ + + + + + + + + + + + - - - -
{col[:label]} + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:"sort_custom_field_#{dyn_col[:custom_field].id}"} + field={"custom_field_#{dyn_col[:custom_field].id}"} + label={dyn_col[:custom_field].name} + sort_field={@sort_field} + sort_order={@sort_order} + /> + + {gettext("Actions")} +
+ {render_slot(col, @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 - rendered - end - else - "" - end} - -
- <%= for action <- @action do %> - {render_slot(action, @row_item.(row))} - <% end %> -
-
+ "" + end} +
+
+ <%= for action <- @action do %> + {render_slot(action, @row_item.(row))} + <% end %> +
+
+ """ end diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex index 7ff7f25..acbed90 100644 --- a/lib/mv_web/components/layouts/navbar.ex +++ b/lib/mv_web/components/layouts/navbar.ex @@ -23,7 +23,7 @@ defmodule MvWeb.Layouts.Navbar do {@club_name} diff --git a/lib/mv_web/helpers/date_formatter.ex b/lib/mv_web/helpers/date_formatter.ex new file mode 100644 index 0000000..eaa9271 --- /dev/null +++ b/lib/mv_web/helpers/date_formatter.ex @@ -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 diff --git a/lib/mv_web/live/components/sort_header_component.ex b/lib/mv_web/live/components/sort_header_component.ex index b847308..3817d90 100644 --- a/lib/mv_web/live/components/sort_header_component.ex +++ b/lib/mv_web/live/components/sort_header_component.ex @@ -19,7 +19,7 @@ defmodule MvWeb.Components.SortHeaderComponent do @impl true def render(assigns) do ~H""" -
+
- -
-
- - - """ - 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 diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex new file mode 100644 index 0000000..9b8ff0d --- /dev/null +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -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""" +
+ <.header> + {gettext("Custom Fields")} + <:subtitle> + {gettext("These will appear in addition to other data when adding new members.")} + + <:actions> + <.button variant="primary" phx-click="new_custom_field" phx-target={@myself}> + <.icon name="hero-plus" /> {gettext("New Custom field")} + + + + + <%!-- Show form when creating or editing --%> +
+ <.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} + /> +
+ + <%!-- 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 :let={{_id, custom_field}} label={gettext("Value Type")}> + {custom_field.value_type} + + + <:col :let={{_id, custom_field}} label={gettext("Description")}> + {custom_field.description} + + + <:col :let={{_id, custom_field}} label={gettext("Show in Overview")}> + + {gettext("Yes")} + + + {gettext("No")} + + + + <:action :let={{_id, custom_field}}> + <.link phx-click={ + JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself) + }> + {gettext("Edit")} + + + + <:action :let={{_id, custom_field}}> + <.link phx-click={JS.push("prepare_delete", value: %{id: custom_field.id}, target: @myself)}> + {gettext("Delete")} + + + + + <%!-- Delete Confirmation Modal --%> + + + +
+ """ + 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 diff --git a/lib/mv_web/live/custom_field_live/show.ex b/lib/mv_web/live/custom_field_live/show.ex deleted file mode 100644 index 239b844..0000000 --- a/lib/mv_web/live/custom_field_live/show.ex +++ /dev/null @@ -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""" - - <.header> - Custom field {@custom_field.slug} - <:subtitle>This is a custom_field record from your database. - - <:actions> - <.button navigate={~p"/custom_fields"}> - <.icon name="hero-arrow-left" /> - - <.button - variant="primary" - navigate={~p"/custom_fields/#{@custom_field}/edit?return_to=show"} - > - <.icon name="hero-pencil-square" /> Edit Custom field - - - - - <.list> - <:item title="Id">{@custom_field.id} - - <:item title="Slug"> - {@custom_field.slug} -

- {gettext("Auto-generated identifier (immutable)")} -

- - - <:item title="Name">{@custom_field.name} - - <:item title="Description">{@custom_field.description} - -
- """ - 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 diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index 0be4559..bb919cb 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -4,6 +4,7 @@ defmodule MvWeb.GlobalSettingsLive do ## Features - Edit the association/club name + - Manage custom fields - Real-time form validation - Success/error feedback @@ -28,7 +29,7 @@ defmodule MvWeb.GlobalSettingsLive do {:ok, socket - |> assign(:page_title, gettext("Club Settings")) + |> assign(:page_title, gettext("Settings")) |> assign(:settings, settings) |> assign_form()} end @@ -38,12 +39,16 @@ defmodule MvWeb.GlobalSettingsLive do ~H""" <.header> - {gettext("Club Settings")} + {gettext("Settings")} <:subtitle> {gettext("Manage global settings for the association.")} + <%!-- Club Settings Section --%> + <.header> + {gettext("Club Settings")} + <.form for={@form} id="settings-form" phx-change="validate" phx-submit="save"> <.input field={@form[:club_name]} @@ -56,6 +61,12 @@ defmodule MvWeb.GlobalSettingsLive do {gettext("Save Settings")} + + <%!-- Custom Fields Section --%> + <.live_component + module={MvWeb.CustomFieldLive.IndexComponent} + id="custom-fields-component" + /> """ end @@ -66,6 +77,7 @@ defmodule MvWeb.GlobalSettingsLive do assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, setting_params))} end + @impl true def handle_event("save", %{"setting" => setting_params}, socket) do case AshPhoenix.Form.submit(socket.assigns.form, params: setting_params) do {:ok, updated_settings} -> @@ -82,6 +94,37 @@ defmodule MvWeb.GlobalSettingsLive do 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 form = AshPhoenix.Form.for_update( diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 67ce522..792466c 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -32,6 +32,7 @@ defmodule MvWeb.MemberLive.Index do alias Mv.Membership alias MvWeb.MemberLive.Index.Formatter + alias MvWeb.Helpers.DateFormatter # Prefix used in sort field names for custom fields (e.g., "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) end) end + + # Public helper function to format dates for use in templates + def format_date(date), do: DateFormatter.format_date(date) end diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 9f8851b..959a3bc 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -224,7 +224,7 @@ """ } > - {member.join_date} + {MvWeb.MemberLive.Index.format_date(member.join_date)} <:col :let={member} label={gettext("Paid")}> Date.to_string(date) + {:ok, date} -> DateFormatter.format_date(date) _ -> value end end diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index de46a3a..7601f46 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -23,6 +23,7 @@ defmodule MvWeb.MemberLive.Show do """ use MvWeb, :live_view import Ash.Query + alias MvWeb.Helpers.DateFormatter @impl true def render(assigns) do @@ -52,8 +53,8 @@ defmodule MvWeb.MemberLive.Show do {if @member.paid, do: gettext("Yes"), else: gettext("No")} <:item title={gettext("Phone Number")}>{@member.phone_number} - <:item title={gettext("Join Date")}>{@member.join_date} - <:item title={gettext("Exit Date")}>{@member.exit_date} + <:item title={gettext("Join Date")}>{DateFormatter.format_date(@member.join_date)} + <:item title={gettext("Exit Date")}>{DateFormatter.format_date(@member.exit_date)} <:item title={gettext("Notes")}>{@member.notes} <:item title={gettext("City")}>{@member.city} <:item title={gettext("Street")}>{@member.street} @@ -81,10 +82,7 @@ defmodule MvWeb.MemberLive.Show do # name cfv.custom_field && cfv.custom_field.name, # value - case cfv.value do - %{value: v} -> v - v -> v - end + format_custom_field_value(cfv) } end) } /> @@ -114,4 +112,17 @@ defmodule MvWeb.MemberLive.Show do defp page_title(:show), do: gettext("Show 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 diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index 9619a15..0639e75 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -42,7 +42,7 @@ defmodule MvWeb.UserLive.Form do <:subtitle>{gettext("Use this form to manage user records in your database.")} - <.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" /> @@ -61,7 +61,7 @@ defmodule MvWeb.UserLive.Form do <%= if @show_password_fields do %> -
+
<.input field={@form[:password]} label={gettext("Password")} @@ -83,7 +83,7 @@ defmodule MvWeb.UserLive.Form do

{gettext("Password requirements")}:

-
    +
    • {gettext("At least 8 characters")}
    • {gettext("Include both letters and numbers")}
    • {gettext("Consider using special characters")}
    • @@ -91,7 +91,7 @@ defmodule MvWeb.UserLive.Form do
<%= if @user do %> -
+

{gettext("Admin Note")}: {gettext( "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

<% else %> <%= if @user do %> -
+

{gettext("Note")}: {gettext( "Check 'Change Password' above to set a new password for this user." @@ -110,7 +110,7 @@ defmodule MvWeb.UserLive.Form do

<% else %> -
+

{gettext("Note")}: {gettext( "User will be created without a password. Check 'Set Password' to add one." @@ -123,11 +123,11 @@ defmodule MvWeb.UserLive.Form do

-

{gettext("Linked Member")}

+

{gettext("Linked Member")}

<%= if @user && @user.member && !@unlink_member do %> -
+

@@ -147,7 +147,7 @@ defmodule MvWeb.UserLive.Form do <% else %> <%= if @unlink_member do %> -

+

{gettext("Unlinking scheduled")}: {gettext( "Member will be unlinked when you save. Cannot select new member until saved." @@ -219,7 +219,7 @@ defmodule MvWeb.UserLive.Form do

<%= if @user && @user.email && @available_members != [] && Enum.all?(@available_members, &(&1.email == to_string(@user.email))) do %> -
+

{gettext("Note")}: {gettext( "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 %>

{gettext("Selected")}: {@selected_member_name}

-

+

{gettext("Save to confirm linking.")}

@@ -245,10 +245,12 @@ defmodule MvWeb.UserLive.Form do <% end %>
- <.button phx-disable-with={gettext("Saving...")} variant="primary"> - {gettext("Save User")} - - <.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")} +
+ <.button phx-disable-with={gettext("Saving...")} variant="primary"> + {gettext("Save User")} + + <.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")} +
""" diff --git a/lib/mv_web/live/user_live/index.html.heex b/lib/mv_web/live/user_live/index.html.heex index 3582046..9a98159 100644 --- a/lib/mv_web/live/user_live/index.html.heex +++ b/lib/mv_web/live/user_live/index.html.heex @@ -49,7 +49,6 @@ > {user.email} - <:col :let={user} label={gettext("OIDC ID")}>{user.oidc_id} <:col :let={user} label={gettext("Linked Member")}> <%= if user.member do %> {user.member.first_name} {user.member.last_name} diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex index 664f99f..777def1 100644 --- a/lib/mv_web/live/user_live/show.ex +++ b/lib/mv_web/live/user_live/show.ex @@ -46,9 +46,7 @@ defmodule MvWeb.UserLive.Show do <.list> - <:item title={gettext("ID")}>{@user.id} <:item title={gettext("Email")}>{@user.email} - <:item title={gettext("OIDC ID")}>{@user.oidc_id || gettext("Not set")} <:item title={gettext("Password Authentication")}> {if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")} @@ -56,13 +54,13 @@ defmodule MvWeb.UserLive.Show do <%= if @user.member do %> <.link 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} <% else %> - {gettext("No member linked")} + {gettext("No member linked")} <% end %> diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex index 09a2792..8b1b0e6 100644 --- a/lib/mv_web/router.ex +++ b/lib/mv_web/router.ex @@ -55,12 +55,6 @@ defmodule MvWeb.Router do live "/members/:id", MemberLive.Show, :show 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/new", CustomFieldValueLive.Form, :new live "/custom_field_values/:id/edit", CustomFieldValueLive.Form, :edit diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 57df5ab..776fa1e 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -34,18 +34,20 @@ msgstr "Verbindung wird wiederhergestellt" msgid "City" 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/user_live/index.html.heex:74 #, elixir-autogen, elixir-format msgid "Delete" 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/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 #, elixir-autogen, elixir-format msgid "Edit" -msgstr "Bearbeite" +msgstr "Bearbeiten" #: lib/mv_web/live/member_live/show.ex:41 #: lib/mv_web/live/member_live/show.ex:116 @@ -155,9 +157,9 @@ msgstr "Postleitzahl" msgid "Save Member" 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/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/user_live/form.ex:248 #, elixir-autogen, elixir-format @@ -176,6 +178,7 @@ msgstr "Straße" msgid "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/formatter.ex:61 #: 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." 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/formatter.ex:60 #: lib/mv_web/live/member_live/show.ex:52 @@ -200,14 +204,14 @@ msgstr "Dies ist ein Mitglied aus deiner Datenbank." msgid "Yes" 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/member_live/form.ex:137 #, elixir-autogen, elixir-format msgid "create" 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/member_live/form.ex:138 #, elixir-autogen, elixir-format @@ -249,8 +253,8 @@ msgstr "Ihre E-Mail-Adresse wurde bestätigt" msgid "Your password has successfully been reset" msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt" -#: lib/mv_web/live/custom_field_live/form.ex:69 -#: lib/mv_web/live/custom_field_live/index.ex:120 +#: lib/mv_web/live/custom_field_live/form_component.ex:61 +#: 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/member_live/form.ex:81 #: lib/mv_web/live/user_live/form.ex:251 @@ -263,7 +267,8 @@ msgstr "Abbrechen" msgid "Choose a member" 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 msgid "Description" msgstr "Beschreibung" @@ -283,7 +288,7 @@ msgstr "Aktiviert" msgid "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 msgid "Immutable" msgstr "Unveränderlich" @@ -311,7 +316,8 @@ msgstr "Mitglied" msgid "Members" 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 msgid "Name" msgstr "Name" @@ -354,7 +360,7 @@ msgstr "Passwort-Authentifizierung" msgid "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 msgid "Required" msgstr "Erforderlich" @@ -369,7 +375,10 @@ msgstr "Alle Mitglieder auswählen" msgid "Select member" msgstr "Mitglied auswählen" +#: lib/mv_web/components/layouts/navbar.ex:26 #: 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 msgid "Settings" msgstr "Einstellungen" @@ -410,7 +419,7 @@ msgstr "Benutzer*in" msgid "Value" 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 msgid "Value type" msgstr "Wertetyp" @@ -618,7 +627,7 @@ msgstr "Benutzerdefinierte Feldwerte" msgid "Custom field" 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 msgid "Custom field %{action} successfully" msgstr "Benutzerdefiniertes Feld erfolgreich %{action}" @@ -633,7 +642,7 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}" msgid "Please select a custom field first" 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 msgid "Save Custom field" msgstr "Benutzerdefiniertes Feld speichern" @@ -643,12 +652,7 @@ msgstr "Benutzerdefiniertes Feld speichern" msgid "Save Custom field value" msgstr "Benutzerdefinierten Feldwert speichern" -#: lib/mv_web/live/custom_field_live/form.ex:46 -#, 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 +#: lib/mv_web/live/custom_field_live/index_component.ex:20 #, elixir-autogen, elixir-format msgid "Custom Fields" msgstr "Benutzerdefinierte Felder" @@ -658,70 +662,64 @@ msgstr "Benutzerdefinierte Felder" 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." -#: lib/mv_web/live/custom_field_live/show.ex:56 -#, 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 +#: lib/mv_web/live/custom_field_live/index_component.ex:97 #, elixir-autogen, elixir-format msgid "%{count} member has a value 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[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 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." -#: 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 msgid "Delete Custom Field" 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 msgid "Delete Custom Field and All Values" 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 msgid "Enter the text above to confirm" 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 msgid "To confirm deletion, please enter this text:" 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 msgid "Show in overview" 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 msgid "Association Name" msgstr "Vereinsname" -#: lib/mv_web/live/global_settings_live.ex:31 -#: lib/mv_web/live/global_settings_live.ex:41 +#: lib/mv_web/live/global_settings_live.ex:50 #, elixir-autogen, elixir-format, fuzzy msgid "Club Settings" msgstr "Vereinsdaten" -#: lib/mv_web/live/global_settings_live.ex:43 +#: lib/mv_web/live/global_settings_live.ex:44 #, elixir-autogen, elixir-format msgid "Manage global settings for the association." 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 msgid "Save Settings" 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 msgid "Settings updated successfully" msgstr "Einstellungen erfolgreich gespeichert" @@ -853,6 +851,51 @@ msgstr "Nicht bezahlt" msgid "Payment filter" 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/show.ex:51 #~ #, elixir-autogen, elixir-format diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 1e0e954..daeb4d9 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -35,12 +35,14 @@ msgstr "" msgid "City" 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/user_live/index.html.heex:74 #, elixir-autogen, elixir-format msgid "Delete" 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/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 @@ -156,9 +158,9 @@ msgstr "" msgid "Save Member" 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/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/user_live/form.ex:248 #, elixir-autogen, elixir-format @@ -177,6 +179,7 @@ msgstr "" msgid "Id" 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/formatter.ex:61 #: lib/mv_web/live/member_live/show.ex:52 @@ -194,6 +197,7 @@ msgstr "" msgid "This is a member record from your database." 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/formatter.ex:60 #: lib/mv_web/live/member_live/show.ex:52 @@ -201,14 +205,14 @@ msgstr "" msgid "Yes" 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/member_live/form.ex:137 #, elixir-autogen, elixir-format msgid "create" 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/member_live/form.ex:138 #, elixir-autogen, elixir-format @@ -250,8 +254,8 @@ msgstr "" msgid "Your password has successfully been reset" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:69 -#: lib/mv_web/live/custom_field_live/index.ex:120 +#: lib/mv_web/live/custom_field_live/form_component.ex:61 +#: 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/member_live/form.ex:81 #: lib/mv_web/live/user_live/form.ex:251 @@ -264,7 +268,8 @@ msgstr "" msgid "Choose a member" 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 msgid "Description" msgstr "" @@ -284,7 +289,7 @@ msgstr "" msgid "ID" 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 msgid "Immutable" msgstr "" @@ -312,7 +317,8 @@ msgstr "" msgid "Members" 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 msgid "Name" msgstr "" @@ -355,7 +361,7 @@ msgstr "" msgid "Profil" 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 msgid "Required" msgstr "" @@ -370,7 +376,10 @@ msgstr "" msgid "Select member" msgstr "" +#: lib/mv_web/components/layouts/navbar.ex:26 #: 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 msgid "Settings" msgstr "" @@ -411,7 +420,7 @@ msgstr "" msgid "Value" 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 msgid "Value type" msgstr "" @@ -619,7 +628,7 @@ msgstr "" msgid "Custom field" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:117 +#: lib/mv_web/live/global_settings_live.ex:105 #, elixir-autogen, elixir-format msgid "Custom field %{action} successfully" msgstr "" @@ -634,7 +643,7 @@ msgstr "" msgid "Please select a custom field first" 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 msgid "Save Custom field" msgstr "" @@ -644,12 +653,7 @@ msgstr "" msgid "Save Custom field value" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:46 -#, 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 +#: lib/mv_web/live/custom_field_live/index_component.ex:20 #, elixir-autogen, elixir-format msgid "Custom Fields" msgstr "" @@ -659,70 +663,64 @@ msgstr "" msgid "Use this form to manage Custom Field Value records in your database." 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/custom_field_live/index.ex:79 +#: lib/mv_web/live/custom_field_live/index_component.ex:97 #, elixir-autogen, elixir-format msgid "%{count} member has a value assigned for this custom field." msgid_plural "%{count} members have values assigned for this custom field." msgstr[0] "" 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 msgid "All custom field values will be permanently deleted when you delete this custom field." 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 msgid "Delete Custom Field" 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 msgid "Delete Custom Field and All Values" 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 msgid "Enter the text above to confirm" 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 msgid "To confirm deletion, please enter this text:" 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 msgid "Show in overview" msgstr "" -#: lib/mv_web/live/global_settings_live.ex:51 +#: lib/mv_web/live/global_settings_live.ex:56 #, elixir-autogen, elixir-format msgid "Association Name" msgstr "" -#: lib/mv_web/live/global_settings_live.ex:31 -#: lib/mv_web/live/global_settings_live.ex:41 +#: lib/mv_web/live/global_settings_live.ex:50 #, elixir-autogen, elixir-format msgid "Club Settings" msgstr "" -#: lib/mv_web/live/global_settings_live.ex:43 +#: lib/mv_web/live/global_settings_live.ex:44 #, elixir-autogen, elixir-format msgid "Manage global settings for the association." msgstr "" -#: lib/mv_web/live/global_settings_live.ex:56 +#: lib/mv_web/live/global_settings_live.ex:61 #, elixir-autogen, elixir-format msgid "Save Settings" msgstr "" -#: lib/mv_web/live/global_settings_live.ex:75 +#: lib/mv_web/live/global_settings_live.ex:87 #, elixir-autogen, elixir-format msgid "Settings updated successfully" msgstr "" @@ -853,3 +851,48 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Payment filter" 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 "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 319bcc3..e10c455 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -35,12 +35,14 @@ msgstr "" msgid "City" 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/user_live/index.html.heex:74 #, elixir-autogen, elixir-format msgid "Delete" 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/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 @@ -156,9 +158,9 @@ msgstr "" msgid "Save Member" 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/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/user_live/form.ex:248 #, elixir-autogen, elixir-format @@ -177,6 +179,7 @@ msgstr "" msgid "Id" 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/formatter.ex:61 #: lib/mv_web/live/member_live/show.ex:52 @@ -194,6 +197,7 @@ msgstr "" msgid "This is a member record from your database." 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/formatter.ex:60 #: lib/mv_web/live/member_live/show.ex:52 @@ -201,14 +205,14 @@ msgstr "" msgid "Yes" 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/member_live/form.ex:137 #, elixir-autogen, elixir-format msgid "create" 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/member_live/form.ex:138 #, elixir-autogen, elixir-format @@ -250,8 +254,8 @@ msgstr "" msgid "Your password has successfully been reset" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:69 -#: lib/mv_web/live/custom_field_live/index.ex:120 +#: lib/mv_web/live/custom_field_live/form_component.ex:61 +#: 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/member_live/form.ex:81 #: lib/mv_web/live/user_live/form.ex:251 @@ -264,7 +268,8 @@ msgstr "" msgid "Choose a member" 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 msgid "Description" msgstr "" @@ -284,7 +289,7 @@ msgstr "" msgid "ID" 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 msgid "Immutable" msgstr "" @@ -312,7 +317,8 @@ msgstr "" msgid "Members" 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 msgid "Name" msgstr "" @@ -355,7 +361,7 @@ msgstr "" msgid "Profil" 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 msgid "Required" msgstr "" @@ -370,7 +376,10 @@ msgstr "" msgid "Select member" msgstr "" +#: lib/mv_web/components/layouts/navbar.ex:26 #: 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 msgid "Settings" msgstr "" @@ -411,7 +420,7 @@ msgstr "" msgid "Value" 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 msgid "Value type" msgstr "" @@ -619,7 +628,7 @@ msgstr "" msgid "Custom field" msgstr "" -#: lib/mv_web/live/custom_field_live/form.ex:117 +#: lib/mv_web/live/global_settings_live.ex:105 #, elixir-autogen, elixir-format msgid "Custom field %{action} successfully" msgstr "" @@ -634,7 +643,7 @@ msgstr "" msgid "Please select a custom field first" 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 msgid "Save Custom field" msgstr "" @@ -644,12 +653,7 @@ msgstr "" msgid "Save Custom field value" 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 "" - -#: lib/mv_web/components/layouts/navbar.ex:26 +#: lib/mv_web/live/custom_field_live/index_component.ex:20 #, elixir-autogen, elixir-format, fuzzy msgid "Custom Fields" msgstr "" @@ -659,70 +663,64 @@ msgstr "" msgid "Use this form to manage Custom Field Value records in your database." 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/custom_field_live/index.ex:79 +#: lib/mv_web/live/custom_field_live/index_component.ex:97 #, elixir-autogen, elixir-format msgid "%{count} member has a value assigned for this custom field." msgid_plural "%{count} members have values assigned for this custom field." msgstr[0] "" 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 msgid "All custom field values will be permanently deleted when you delete this custom field." 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 msgid "Delete Custom Field" 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 msgid "Delete Custom Field and All Values" 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 msgid "Enter the text above to confirm" 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 msgid "To confirm deletion, please enter this text:" 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 msgid "Show in overview" msgstr "" -#: lib/mv_web/live/global_settings_live.ex:51 +#: lib/mv_web/live/global_settings_live.ex:56 #, elixir-autogen, elixir-format msgid "Association Name" msgstr "" -#: lib/mv_web/live/global_settings_live.ex:31 -#: lib/mv_web/live/global_settings_live.ex:41 +#: lib/mv_web/live/global_settings_live.ex:50 #, elixir-autogen, elixir-format, fuzzy msgid "Club Settings" msgstr "" -#: lib/mv_web/live/global_settings_live.ex:43 +#: lib/mv_web/live/global_settings_live.ex:44 #, elixir-autogen, elixir-format msgid "Manage global settings for the association." msgstr "" -#: lib/mv_web/live/global_settings_live.ex:56 +#: lib/mv_web/live/global_settings_live.ex:61 #, elixir-autogen, elixir-format, fuzzy msgid "Save Settings" msgstr "" -#: lib/mv_web/live/global_settings_live.ex:75 +#: lib/mv_web/live/global_settings_live.ex:87 #, elixir-autogen, elixir-format msgid "Settings updated successfully" msgstr "" @@ -854,8 +852,63 @@ msgstr "" msgid "Payment filter" 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/show.ex:51 #~ #, elixir-autogen, elixir-format #~ msgid "Birth Date" #~ 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 "" diff --git a/test/membership/member_field_visibility_test.exs b/test/membership/member_field_visibility_test.exs deleted file mode 100644 index 9963169..0000000 --- a/test/membership/member_field_visibility_test.exs +++ /dev/null @@ -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 diff --git a/test/mv_web/live/custom_field_live/deletion_test.exs b/test/mv_web/live/custom_field_live/deletion_test.exs index f0317e0..322cf38 100644 --- a/test/mv_web/live/custom_field_live/deletion_test.exs +++ b/test/mv_web/live/custom_field_live/deletion_test.exs @@ -1,6 +1,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do @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: - Opening deletion confirmation modal @@ -39,11 +40,11 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do # Create 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") - # Click delete button + # Click delete button - find the delete link within the component view - |> element("a", "Delete") + |> element("#custom-fields-component a", "Delete") |> render_click() # 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(member2, custom_field, "test2") - {:ok, view, _html} = live(conn, ~p"/custom_fields") + {:ok, view, _html} = live(conn, ~p"/settings") view - |> element("a", "Delete") + |> element("#custom-fields-component a", "Delete") |> render_click() # 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 {: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 - |> element("a", "Delete") + |> element("#custom-fields-component a", "Delete") |> render_click() # Should show 0 members @@ -93,15 +94,16 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do test "updates confirmation state when typing", %{conn: conn} do {: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 - |> element("a", "Delete") + |> element("#custom-fields-component a", "Delete") |> render_click() - # Type in slug input + # Type in slug input - use element to find the form with phx-target 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) 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 {: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 - |> element("a", "Delete") + |> element("#custom-fields-component a", "Delete") |> render_click() - # Type wrong slug + # Type wrong slug - use element to find the form with phx-target view - |> render_change("update_slug_confirmation", %{"slug" => "wrong-slug"}) + |> element("#delete-custom-field-modal form") + |> render_change(%{"slug" => "wrong-slug"}) # Button should be disabled html = render(view) @@ -133,20 +136,21 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do {:ok, custom_field} = create_custom_field("test_field", :string) {: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 view - |> element("a", "Delete") + |> element("#custom-fields-component a", "Delete") |> render_click() - # Enter correct slug + # Enter correct slug - use element to find the form with phx-target view - |> render_change("update_slug_confirmation", %{"slug" => custom_field.slug}) + |> element("#delete-custom-field-modal form") + |> render_change(%{"slug" => custom_field.slug}) # Click confirm view - |> element("button", "Delete Custom Field and All Values") + |> element("#delete-custom-field-modal button", "Delete Custom Field and All Values") |> render_click() # Should show success message @@ -162,27 +166,28 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do assert {:ok, _} = Ash.get(Member, member.id) 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, view, _html} = live(conn, ~p"/custom_fields") + {:ok, view, _html} = live(conn, ~p"/settings") view - |> element("a", "Delete") + |> element("#custom-fields-component a", "Delete") |> render_click() - # Enter wrong slug + # Enter wrong slug - use element to find the form with phx-target 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) - view - |> render_click("confirm_delete", %{}) + # Button should be disabled and we cannot click it + # The test verifies that the button is properly disabled in the UI + html = render(view) + assert html =~ ~r/disabled(?:=""|(?!\w))/ - # Should show error message - assert render(view) =~ "Slug does not match" - - # Custom field should still exist + # Custom field should still exist since deletion couldn't proceed assert {:ok, _} = Ash.get(CustomField, custom_field.id) end end @@ -191,10 +196,10 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do test "closes modal without deleting", %{conn: conn} do {: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 - |> element("a", "Delete") + |> element("#custom-fields-component a", "Delete") |> render_click() # Modal should be visible @@ -202,7 +207,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do # Click cancel view - |> element("button", "Cancel") + |> element("#delete-custom-field-modal button", "Cancel") |> render_click() # Modal should be gone diff --git a/test/mv_web/live/profile_navigation_test.exs b/test/mv_web/live/profile_navigation_test.exs index 3222825..4b383c6 100644 --- a/test/mv_web/live/profile_navigation_test.exs +++ b/test/mv_web/live/profile_navigation_test.exs @@ -90,8 +90,6 @@ defmodule MvWeb.ProfileNavigationTest do # Verify we're on the correct profile page with OIDC specific information {:ok, _profile_view, html} = live(conn, "/users/#{user.id}") assert html =~ to_string(user.email) - # OIDC ID should be visible - assert html =~ "oidc_123" # Password auth should be disabled for OIDC users assert html =~ "Not enabled" end @@ -150,8 +148,6 @@ defmodule MvWeb.ProfileNavigationTest do "/members/new", "/custom_field_values", "/custom_field_values/new", - "/custom_fields", - "/custom_fields/new", "/users", "/users/new" ] diff --git a/test/mv_web/member_live/index_custom_fields_display_test.exs b/test/mv_web/member_live/index_custom_fields_display_test.exs index 0485f5e..802cc8f 100644 --- a/test/mv_web/member_live/index_custom_fields_display_test.exs +++ b/test/mv_web/member_live/index_custom_fields_display_test.exs @@ -231,8 +231,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, "/members") - # Date should be displayed in readable format - assert html =~ "1990" or html =~ "1990-05-15" or html =~ "15.05.1990" + # Date should be displayed in European format (dd.mm.yyyy) + assert html =~ "15.05.1990" end test "formats email custom field values correctly", %{conn: conn, member1: _member1} do diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs index c0b0275..360ef72 100644 --- a/test/mv_web/user_live/index_test.exs +++ b/test/mv_web/user_live/index_test.exs @@ -33,8 +33,6 @@ defmodule MvWeb.UserLive.IndexTest do assert html =~ "alice@example.com" assert html =~ "bob@example.com" - assert html =~ "alice123" - assert html =~ "bob456" end test "shows correct action links", %{conn: conn} do @@ -386,10 +384,6 @@ defmodule MvWeb.UserLive.IndexTest do # Should still show the table structure 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 test "handles users with missing OIDC ID", %{conn: conn} do