Membership Fee 6 - UI Components & LiveViews closes #280 #304
11 changed files with 387 additions and 247 deletions
|
|
@ -509,10 +509,6 @@ defmodule Mv.Membership.Member do
|
||||||
constraints min_length: 5, max_length: 254
|
constraints min_length: 5, max_length: 254
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :paid, :boolean do
|
|
||||||
allow_nil? true
|
|
||||||
end
|
|
||||||
|
|
||||||
attribute :phone_number, :string do
|
attribute :phone_number, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ defmodule Mv.Constants do
|
||||||
:first_name,
|
:first_name,
|
||||||
:last_name,
|
:last_name,
|
||||||
:email,
|
:email,
|
||||||
:paid,
|
|
||||||
:phone_number,
|
:phone_number,
|
||||||
:join_date,
|
:join_date,
|
||||||
:exit_date,
|
:exit_date,
|
||||||
|
|
|
||||||
|
|
@ -819,7 +819,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
defp valid_sort_field?(field) when is_atom(field) 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
|
# 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
|
# :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
|
valid_fields = Mv.Constants.member_fields() -- non_sortable_fields
|
||||||
|
|
||||||
field in valid_fields or custom_field_sort?(field)
|
field in valid_fields or custom_field_sort?(field)
|
||||||
|
|
|
||||||
|
|
@ -307,14 +307,6 @@
|
||||||
>
|
>
|
||||||
{MvWeb.MemberLive.Index.format_date(member.join_date)}
|
{MvWeb.MemberLive.Index.format_date(member.join_date)}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={member} :if={:paid in @member_fields_visible} label={gettext("Paid")}>
|
|
||||||
<span class={[
|
|
||||||
"badge",
|
|
||||||
if(member.paid == true, do: "badge-success", else: "badge-error")
|
|
||||||
]}>
|
|
||||||
{if member.paid == true, do: gettext("Yes"), else: gettext("No")}
|
|
||||||
</span>
|
|
||||||
</:col>
|
|
||||||
<:col
|
<:col
|
||||||
:let={member}
|
:let={member}
|
||||||
label={
|
label={
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ defmodule MvWeb.Translations.MemberFields do
|
||||||
def label(:first_name), do: gettext("First Name")
|
def label(:first_name), do: gettext("First Name")
|
||||||
def label(:last_name), do: gettext("Last Name")
|
def label(:last_name), do: gettext("Last Name")
|
||||||
def label(:email), do: gettext("Email")
|
def label(:email), do: gettext("Email")
|
||||||
def label(:paid), do: gettext("Paid")
|
|
||||||
def label(:phone_number), do: gettext("Phone")
|
def label(:phone_number), do: gettext("Phone")
|
||||||
def label(:join_date), do: gettext("Join Date")
|
def label(:join_date), do: gettext("Join Date")
|
||||||
def label(:exit_date), do: gettext("Exit Date")
|
def label(:exit_date), do: gettext("Exit Date")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -134,7 +134,6 @@ for member_attrs <- [
|
||||||
last_name: "Müller",
|
last_name: "Müller",
|
||||||
email: "hans.mueller@example.de",
|
email: "hans.mueller@example.de",
|
||||||
join_date: ~D[2023-01-15],
|
join_date: ~D[2023-01-15],
|
||||||
paid: true,
|
|
||||||
phone_number: "+49301234567",
|
phone_number: "+49301234567",
|
||||||
city: "München",
|
city: "München",
|
||||||
street: "Hauptstraße",
|
street: "Hauptstraße",
|
||||||
|
|
@ -146,7 +145,6 @@ for member_attrs <- [
|
||||||
last_name: "Schmidt",
|
last_name: "Schmidt",
|
||||||
email: "greta.schmidt@example.de",
|
email: "greta.schmidt@example.de",
|
||||||
join_date: ~D[2023-02-01],
|
join_date: ~D[2023-02-01],
|
||||||
paid: false,
|
|
||||||
phone_number: "+49309876543",
|
phone_number: "+49309876543",
|
||||||
city: "Hamburg",
|
city: "Hamburg",
|
||||||
street: "Lindenstraße",
|
street: "Lindenstraße",
|
||||||
|
|
@ -159,7 +157,6 @@ for member_attrs <- [
|
||||||
last_name: "Wagner",
|
last_name: "Wagner",
|
||||||
email: "friedrich.wagner@example.de",
|
email: "friedrich.wagner@example.de",
|
||||||
join_date: ~D[2022-11-10],
|
join_date: ~D[2022-11-10],
|
||||||
paid: true,
|
|
||||||
phone_number: "+49301122334",
|
phone_number: "+49301122334",
|
||||||
city: "Berlin",
|
city: "Berlin",
|
||||||
street: "Kastanienallee",
|
street: "Kastanienallee",
|
||||||
|
|
@ -170,7 +167,6 @@ for member_attrs <- [
|
||||||
last_name: "Wagner",
|
last_name: "Wagner",
|
||||||
email: "marianne.wagner@example.de",
|
email: "marianne.wagner@example.de",
|
||||||
join_date: ~D[2022-11-10],
|
join_date: ~D[2022-11-10],
|
||||||
paid: true,
|
|
||||||
phone_number: "+49301122334",
|
phone_number: "+49301122334",
|
||||||
city: "Berlin",
|
city: "Berlin",
|
||||||
street: "Kastanienallee",
|
street: "Kastanienallee",
|
||||||
|
|
@ -204,7 +200,6 @@ linked_members = [
|
||||||
last_name: "Weber",
|
last_name: "Weber",
|
||||||
email: "maria.weber@example.de",
|
email: "maria.weber@example.de",
|
||||||
join_date: ~D[2023-03-15],
|
join_date: ~D[2023-03-15],
|
||||||
paid: true,
|
|
||||||
phone_number: "+49301357924",
|
phone_number: "+49301357924",
|
||||||
city: "Frankfurt",
|
city: "Frankfurt",
|
||||||
street: "Goetheplatz",
|
street: "Goetheplatz",
|
||||||
|
|
@ -219,7 +214,6 @@ linked_members = [
|
||||||
last_name: "Klein",
|
last_name: "Klein",
|
||||||
email: "thomas.klein@example.de",
|
email: "thomas.klein@example.de",
|
||||||
join_date: ~D[2023-04-01],
|
join_date: ~D[2023-04-01],
|
||||||
paid: false,
|
|
||||||
phone_number: "+49302468135",
|
phone_number: "+49302468135",
|
||||||
city: "Köln",
|
city: "Köln",
|
||||||
street: "Rheinstraße",
|
street: "Rheinstraße",
|
||||||
|
|
|
||||||
132
priv/resource_snapshots/repo/custom_fields/20251218113900.json
Normal file
132
priv/resource_snapshots/repo/custom_fields/20251218113900.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
233
priv/resource_snapshots/repo/members/20251218113900.json
Normal file
233
priv/resource_snapshots/repo/members/20251218113900.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,6 @@ defmodule Mv.Membership.MemberTest do
|
||||||
@valid_attrs %{
|
@valid_attrs %{
|
||||||
first_name: "John",
|
first_name: "John",
|
||||||
last_name: "Doe",
|
last_name: "Doe",
|
||||||
paid: true,
|
|
||||||
email: "john@example.com",
|
email: "john@example.com",
|
||||||
phone_number: "+49123456789",
|
phone_number: "+49123456789",
|
||||||
join_date: ~D[2020-01-01],
|
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"
|
assert error_message(errors, :email) =~ "is not a valid email"
|
||||||
end
|
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
|
test "Phone number is optional but must have a valid format if specified" do
|
||||||
attrs = Map.put(@valid_attrs, :phone_number, "abc")
|
attrs = Map.put(@valid_attrs, :phone_number, "abc")
|
||||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||||
|
|
|
||||||
|
|
@ -456,221 +456,4 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
assert has_element?(view, "#flash-group")
|
assert has_element?(view, "#flash-group")
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue