diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 5f7df47..0c90f4d 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -509,10 +509,6 @@ defmodule Mv.Membership.Member do constraints min_length: 5, max_length: 254 end - attribute :paid, :boolean do - allow_nil? true - end - attribute :phone_number, :string do allow_nil? true end diff --git a/lib/mv/constants.ex b/lib/mv/constants.ex index 843ad2b..c81dbd6 100644 --- a/lib/mv/constants.ex +++ b/lib/mv/constants.ex @@ -7,7 +7,6 @@ defmodule Mv.Constants do :first_name, :last_name, :email, - :paid, :phone_number, :join_date, :exit_date, diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 822bce6..7ed4007 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -819,7 +819,7 @@ defmodule MvWeb.MemberLive.Index do defp valid_sort_field?(field) when is_atom(field) do # All member fields are sortable, but we exclude some that don't make sense # :id is not in member_fields, but we don't want to sort by it anyway - non_sortable_fields = [:notes, :paid] + non_sortable_fields = [:notes] valid_fields = Mv.Constants.member_fields() -- non_sortable_fields field in valid_fields or custom_field_sort?(field) diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 47162d5..5b27e6f 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -307,14 +307,6 @@ > {MvWeb.MemberLive.Index.format_date(member.join_date)} - <:col :let={member} :if={:paid in @member_fields_visible} label={gettext("Paid")}> - - {if member.paid == true, do: gettext("Yes"), else: gettext("No")} - - <:col :let={member} label={ diff --git a/lib/mv_web/translations/member_fields.ex b/lib/mv_web/translations/member_fields.ex index 3750bcb..f10e0d2 100644 --- a/lib/mv_web/translations/member_fields.ex +++ b/lib/mv_web/translations/member_fields.ex @@ -20,7 +20,6 @@ defmodule MvWeb.Translations.MemberFields do def label(:first_name), do: gettext("First Name") def label(:last_name), do: gettext("Last Name") def label(:email), do: gettext("Email") - def label(:paid), do: gettext("Paid") def label(:phone_number), do: gettext("Phone") def label(:join_date), do: gettext("Join Date") def label(:exit_date), do: gettext("Exit Date") diff --git a/priv/repo/migrations/20251218113900_remove_paid_from_members.exs b/priv/repo/migrations/20251218113900_remove_paid_from_members.exs new file mode 100644 index 0000000..3722137 --- /dev/null +++ b/priv/repo/migrations/20251218113900_remove_paid_from_members.exs @@ -0,0 +1,21 @@ +defmodule Mv.Repo.Migrations.RemovePaidFromMembers do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:members) do + remove :paid + end + end + + def down do + alter table(:members) do + add :paid, :boolean + end + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 2e8694d..feb7170 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -134,7 +134,6 @@ for member_attrs <- [ last_name: "Müller", email: "hans.mueller@example.de", join_date: ~D[2023-01-15], - paid: true, phone_number: "+49301234567", city: "München", street: "Hauptstraße", @@ -146,7 +145,6 @@ for member_attrs <- [ last_name: "Schmidt", email: "greta.schmidt@example.de", join_date: ~D[2023-02-01], - paid: false, phone_number: "+49309876543", city: "Hamburg", street: "Lindenstraße", @@ -159,7 +157,6 @@ for member_attrs <- [ last_name: "Wagner", email: "friedrich.wagner@example.de", join_date: ~D[2022-11-10], - paid: true, phone_number: "+49301122334", city: "Berlin", street: "Kastanienallee", @@ -170,7 +167,6 @@ for member_attrs <- [ last_name: "Wagner", email: "marianne.wagner@example.de", join_date: ~D[2022-11-10], - paid: true, phone_number: "+49301122334", city: "Berlin", street: "Kastanienallee", @@ -204,7 +200,6 @@ linked_members = [ last_name: "Weber", email: "maria.weber@example.de", join_date: ~D[2023-03-15], - paid: true, phone_number: "+49301357924", city: "Frankfurt", street: "Goetheplatz", @@ -219,7 +214,6 @@ linked_members = [ last_name: "Klein", email: "thomas.klein@example.de", join_date: ~D[2023-04-01], - paid: false, phone_number: "+49302468135", city: "Köln", street: "Rheinstraße", diff --git a/priv/resource_snapshots/repo/custom_fields/20251218113900.json b/priv/resource_snapshots/repo/custom_fields/20251218113900.json new file mode 100644 index 0000000..b8fee81 --- /dev/null +++ b/priv/resource_snapshots/repo/custom_fields/20251218113900.json @@ -0,0 +1,132 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "slug", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "value_type", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "description", + "type": "text" + }, + { + "allow_nil?": false, + "default": "false", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "required", + "type": "boolean" + }, + { + "allow_nil?": false, + "default": "true", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "show_in_overview", + "type": "boolean" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "6FEA699A67D34CFBA261DA8316AB711F6853C4F953D42C5D7940B22D17699B2E", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "custom_fields_unique_name_index", + "keys": [ + { + "type": "atom", + "value": "name" + } + ], + "name": "unique_name", + "nils_distinct?": true, + "where": null + }, + { + "all_tenants?": false, + "base_filter": null, + "index_name": "custom_fields_unique_slug_index", + "keys": [ + { + "type": "atom", + "value": "slug" + } + ], + "name": "unique_slug", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "custom_fields" +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/members/20251218113900.json b/priv/resource_snapshots/repo/members/20251218113900.json new file mode 100644 index 0000000..96ad143 --- /dev/null +++ b/priv/resource_snapshots/repo/members/20251218113900.json @@ -0,0 +1,233 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v7()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "first_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "last_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "email", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "phone_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "join_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "exit_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "notes", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "city", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "street", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "house_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "postal_code", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "search_vector", + "type": "tsvector" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "membership_fee_start_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "members_membership_fee_type_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "membership_fee_types" + }, + "scale": null, + "size": null, + "source": "membership_fee_type_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "E18E4B404581EFF050F85E895FAE986B79DB62C9E1611164C92B46B954C371C1", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "members_unique_email_index", + "keys": [ + { + "type": "atom", + "value": "email" + } + ], + "name": "unique_email", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "members" +} \ No newline at end of file diff --git a/test/membership/member_test.exs b/test/membership/member_test.exs index 653a2d4..40a68ac 100644 --- a/test/membership/member_test.exs +++ b/test/membership/member_test.exs @@ -6,7 +6,6 @@ defmodule Mv.Membership.MemberTest do @valid_attrs %{ first_name: "John", last_name: "Doe", - paid: true, email: "john@example.com", phone_number: "+49123456789", join_date: ~D[2020-01-01], @@ -42,14 +41,6 @@ defmodule Mv.Membership.MemberTest do assert error_message(errors, :email) =~ "is not a valid email" end - test "Paid is optional but must be boolean if specified" do - attrs = Map.put(@valid_attrs, :paid, nil) - attrs2 = Map.put(@valid_attrs, :paid, "yes") - assert {:ok, _member} = Membership.create_member(Map.delete(attrs, :paid)) - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs2) - assert error_message(errors, :paid) =~ "is invalid" - end - test "Phone number is optional but must have a valid format if specified" do attrs = Map.put(@valid_attrs, :phone_number, "abc") assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index 3232cc0..60bf2aa 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -456,221 +456,4 @@ defmodule MvWeb.MemberLive.IndexTest do assert has_element?(view, "#flash-group") end end - - describe "payment filter integration" do - setup do - # Create members with different payment status - # Use unique names that won't appear elsewhere in the HTML - {:ok, paid_member} = - Mv.Membership.create_member(%{ - first_name: "Zahler", - last_name: "Mitglied", - email: "zahler@example.com", - paid: true - }) - - {:ok, unpaid_member} = - Mv.Membership.create_member(%{ - first_name: "Nichtzahler", - last_name: "Mitglied", - email: "nichtzahler@example.com", - paid: false - }) - - {:ok, nil_paid_member} = - Mv.Membership.create_member(%{ - first_name: "Unbestimmt", - last_name: "Mitglied", - email: "unbestimmt@example.com" - # paid is nil by default - }) - - %{paid_member: paid_member, unpaid_member: unpaid_member, nil_paid_member: nil_paid_member} - end - - test "filter shows all members when no filter is active", %{ - conn: conn, - paid_member: paid_member, - unpaid_member: unpaid_member, - nil_paid_member: nil_paid_member - } do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - assert html =~ paid_member.first_name - assert html =~ unpaid_member.first_name - assert html =~ nil_paid_member.first_name - end - - test "filter shows only paid members when paid filter is active", %{ - conn: conn, - paid_member: paid_member, - unpaid_member: unpaid_member, - nil_paid_member: nil_paid_member - } do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members?paid_filter=paid") - - assert html =~ paid_member.first_name - refute html =~ unpaid_member.first_name - refute html =~ nil_paid_member.first_name - end - - test "filter shows only unpaid members (including nil) when not_paid filter is active", %{ - conn: conn, - paid_member: paid_member, - unpaid_member: unpaid_member, - nil_paid_member: nil_paid_member - } do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members?paid_filter=not_paid") - - refute html =~ paid_member.first_name - assert html =~ unpaid_member.first_name - assert html =~ nil_paid_member.first_name - end - - test "filter combines with search query (AND)", %{ - conn: conn, - paid_member: paid_member - } do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members?query=Zahler&paid_filter=paid") - - assert html =~ paid_member.first_name - end - - test "filter combines with sorting", %{conn: conn} do - conn = conn_with_oidc_user(conn) - - {:ok, view, _html} = - live(conn, "/members?paid_filter=paid&sort_field=first_name&sort_order=asc") - - # Click on email sort header - view - |> element("[data-testid='email']") - |> render_click() - - # Filter should be preserved in URL - path = assert_patch(view) - assert path =~ "paid_filter=paid" - assert path =~ "sort_field=email" - end - - test "URL parameter paid_filter is set when selecting filter", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Open filter dropdown - view - |> element("#payment-filter button[aria-haspopup='true']") - |> render_click() - - # Select "Paid" option - view - |> element("#payment-filter button[phx-value-filter='paid']") - |> render_click() - - path = assert_patch(view) - assert path =~ "paid_filter=paid" - end - - test "URL parameter is correctly read on page load", %{ - conn: conn, - paid_member: paid_member - } do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members?paid_filter=paid") - - # Only paid member should be visible - assert html =~ paid_member.first_name - # Filter badge should be visible - assert html =~ "badge" - end - - test "invalid URL parameter is ignored", %{ - conn: conn, - paid_member: paid_member, - unpaid_member: unpaid_member - } do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members?paid_filter=invalid_value") - - # All members should be visible (filter not applied) - assert html =~ paid_member.first_name - assert html =~ unpaid_member.first_name - end - - test "search maintains filter state", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members?paid_filter=paid") - - # Perform search - view - |> element("[data-testid='search-input']") - |> render_change(%{"query" => "test"}) - - # Filter state should be maintained in URL - path = assert_patch(view) - assert path =~ "paid_filter=paid" - end - end - - describe "paid column in table" do - setup do - {:ok, paid_member} = - Mv.Membership.create_member(%{ - first_name: "Paid", - last_name: "Member", - email: "paid.column@example.com", - paid: true - }) - - {:ok, unpaid_member} = - Mv.Membership.create_member(%{ - first_name: "Unpaid", - last_name: "Member", - email: "unpaid.column@example.com", - paid: false - }) - - %{paid_member: paid_member, unpaid_member: unpaid_member} - end - - test "paid column shows green badge for paid members", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - # Check for success badge (green) - assert html =~ "badge-success" - end - - test "paid column shows red badge for unpaid members", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - # Check for error badge (red) - assert html =~ "badge-error" - end - - test "paid column shows 'Yes' for paid members", %{conn: conn} do - conn = conn_with_oidc_user(conn) - Gettext.put_locale(MvWeb.Gettext, "en") - {:ok, _view, html} = live(conn, "/members") - - # The table should contain "Yes" text inside badge - assert html =~ "badge-success" - assert html =~ "Yes" - end - - test "paid column shows 'No' for unpaid members", %{conn: conn} do - conn = conn_with_oidc_user(conn) - Gettext.put_locale(MvWeb.Gettext, "en") - {:ok, _view, html} = live(conn, "/members") - - # The table should contain "No" text inside badge - assert html =~ "badge-error" - assert html =~ "No" - end - end end