diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index d460b1ac5..f5a3ae953 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -13,7 +13,7 @@ module Admin scope = scope.where(role: params[:role]) if params[:role].present? scope = apply_trial_filter(scope) if params[:trial_status].present? - @users = scope.order( + users = scope.order( Arel.sql( "CASE " \ "WHEN subscriptions.status = 'trialing' THEN 0 " \ @@ -23,14 +23,18 @@ module Admin ) ) - family_ids = @users.map(&:family_id).uniq + family_ids = users.map(&:family_id).uniq @accounts_count_by_family = Account.where(family_id: family_ids).group(:family_id).count @entries_count_by_family = Entry.joins(:account).where(accounts: { family_id: family_ids }).group("accounts.family_id").count - user_ids = @users.map(&:id).uniq + user_ids = users.map(&:id).uniq @last_login_by_user = Session.where(user_id: user_ids).group(:user_id).maximum(:created_at) @sessions_count_by_user = Session.where(user_id: user_ids).group(:user_id).count + @families_with_users = users.group_by(&:family).sort_by do |family, _users| + -(@entries_count_by_family[family.id] || 0) + end + @trials_expiring_in_7_days = Subscription .where(status: :trialing) .where(trial_ends_at: Time.current..7.days.from_now) diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb index abc09a652..0c21d2060 100644 --- a/app/views/admin/users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -43,80 +43,110 @@ - +

<%= t(".section_title") %>

-
- <% if @users.any? %> - - - - - - - - - - - - <% @users.each do |user| %> - - - - - - +
<%= t(".table.user") %><%= t(".table.trial_ends_at") %><%= t(".table.family_accounts") %><%= t(".table.family_transactions") %><%= t(".table.role") %>
-
-
- <%= user.initials %> -
-
-

<%= user.display_name %>

-

<%= user.email %>

-

- <%= t(".table.last_login") %>: <%= @last_login_by_user[user.id]&.to_fs(:long) || t(".table.never") %> - <%= t(".table.session_count") %>: <%= number_with_delimiter(@sessions_count_by_user[user.id] || 0) %> -

-
-
-
- <%= user.family.subscription&.trial_ends_at&.to_fs(:long) || t(".not_available") %> - - <%= number_with_delimiter(@accounts_count_by_family[user.family_id] || 0) %> - - <%= number_with_delimiter(@entries_count_by_family[user.family_id] || 0) %> - - <% if user.id == Current.user.id %> - <%= t(".you") %> - <% else %> - <%= form_with model: [:admin, user], method: :patch, class: "flex items-center justify-end gap-2" do |form| %> - <%= form.select :role, - options_for_select([ - [t(".roles.guest"), "guest"], - [t(".roles.member", default: "Member"), "member"], - [t(".roles.admin"), "admin"], - [t(".roles.super_admin"), "super_admin"] - ], user.role), - {}, - class: "text-sm rounded-lg border border-primary bg-container text-primary px-2 py-1", - onchange: "this.form.requestSubmit()" %> - <% end %> + + <% if @families_with_users.any? %> +
+ <% @families_with_users.each do |family, users| %> +
+ +
+ <%= icon "users", class: "w-5 h-5 text-secondary shrink-0" %> +
+

<%= family.name.presence || t(".unnamed_family") %>

+

+ <%= t(".family_summary", + members: users.size, + accounts: number_with_delimiter(@accounts_count_by_family[family.id] || 0), + transactions: number_with_delimiter(@entries_count_by_family[family.id] || 0)) %> +

+
+
+
+ <% sub = family.subscription %> + <% if sub&.trialing? %> + + <%= t(".table.trial_ends_at") %>: <%= sub.trial_ends_at&.to_fs(:long) || t(".not_available") %> + + <% elsif sub %> + + <%= sub.status.humanize %> + + <% else %> + <%= t(".no_subscription") %> + <% end %> + <%= icon "chevron-down", class: "w-4 h-4 text-secondary transition-transform group-open:rotate-180" %> +
+
+ +
+ + + + + + + + + + + <% users.each do |user| %> + + + + + + <% end %> - - - <% end %> - -
<%= t(".table.user") %><%= t(".table.last_login") %><%= t(".table.session_count") %><%= t(".table.role") %>
+
+
+ <%= user.initials %> +
+
+

<%= user.display_name %>

+

<%= user.email %>

+
+
+
+ <%= @last_login_by_user[user.id]&.to_fs(:long) || t(".table.never") %> + + <%= number_with_delimiter(@sessions_count_by_user[user.id] || 0) %> + + <% if user.id == Current.user.id %> + <%= t(".you") %> + <% else %> + <%= form_with model: [:admin, user], method: :patch, class: "flex items-center justify-end gap-2", data: { controller: "auto-submit-form" } do |form| %> + <%= form.select :role, + options_for_select([ + [t(".roles.guest"), "guest"], + [t(".roles.member", default: "Member"), "member"], + [t(".roles.admin"), "admin"], + [t(".roles.super_admin"), "super_admin"] + ], user.role), + {}, + class: "text-sm rounded-lg border border-primary bg-container text-primary px-2 py-1", + data: { auto_submit_form_target: "auto" } %> + <% end %> + <% end %> +
- <% else %> -
- <%= icon "users", class: "w-12 h-12 mx-auto text-secondary mb-3" %> -

<%= t(".no_users") %>

-
- <% end %> -
+
+
+ + <% end %> +
+ <% else %> +
+ <%= icon "users", class: "w-12 h-12 mx-auto text-secondary mb-3" %> +

<%= t(".no_users") %>

+
+ <% end %> - <%= settings_section title: t(".role_descriptions_title"), collapsible: true, open: false do %> + <%= settings_section title: t(".role_descriptions_title"), collapsible: true, open: true do %>
diff --git a/config/locales/views/admin/users/en.yml b/config/locales/views/admin/users/en.yml index 7eb7102a7..b14feb785 100644 --- a/config/locales/views/admin/users/en.yml +++ b/config/locales/views/admin/users/en.yml @@ -5,11 +5,14 @@ en: index: title: "User Management" description: "Manage user roles for your instance. Super admins can access SSO provider settings and user management." - section_title: "Users" + section_title: "Families / Groups" you: "(You)" trial_ends_at: "Trial ends" not_available: "n/a" no_users: "No users found." + unnamed_family: "Unnamed Family/Group" + no_subscription: "No subscription" + family_summary: "%{members} members · %{accounts} accounts · %{transactions} transactions" filters: role: "Role" role_all: "All roles" diff --git a/test/controllers/admin/users_controller_test.rb b/test/controllers/admin/users_controller_test.rb index e23180e2c..1273c4b18 100644 --- a/test/controllers/admin/users_controller_test.rb +++ b/test/controllers/admin/users_controller_test.rb @@ -5,43 +5,45 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest sign_in users(:sure_support_staff) end - test "index sorts users by subscription trial end date with nils last" do - user_with_trial = User.find_by!(email: "user1@example.com") - user_without_trial = User.find_by!(email: "bob@bobdylan.com") + test "index groups users by family sorted by transaction count" do + family_with_more = users(:family_admin).family + family_with_fewer = users(:empty).family - user_with_trial.family.subscription&.destroy - Subscription.create!( - family_id: user_with_trial.family_id, - status: :trialing, - trial_ends_at: 2.days.from_now - ) - - user_without_trial.family.subscription&.destroy - Subscription.create!( - family_id: user_without_trial.family_id, - status: :active, - trial_ends_at: nil, - stripe_id: "cus_test_#{user_without_trial.family_id}" - ) + account = Account.create!(family: family_with_more, name: "Test", balance: 0, currency: "USD", accountable: Depository.new) + 3.times { |i| account.entries.create!(name: "Txn #{i}", date: Date.current, amount: 10, currency: "USD", entryable: Transaction.new) } get admin_users_url - assert_response :success body = response.body - trial_user_index = body.index("user1@example.com") - no_trial_user_index = body.index("bob@bobdylan.com") + more_idx = body.index(family_with_more.name) + fewer_idx = body.index(family_with_fewer.name) - assert_not_nil trial_user_index - assert_not_nil no_trial_user_index - assert_operator trial_user_index, :<, no_trial_user_index, - "User with trialing subscription (user1@example.com) should appear before user with non-trial subscription (bob@bobdylan.com)" + assert_not_nil more_idx + assert_not_nil fewer_idx + assert_operator more_idx, :<, fewer_idx, + "Family with more transactions should appear before family with fewer" end - test "index shows n/a when trial end date is unavailable" do - get admin_users_url + test "index shows subscription status for families" do + family = users(:family_admin).family + family.subscription&.destroy + Subscription.create!( + family_id: family.id, + status: :active, + stripe_id: "cus_test_#{family.id}" + ) + get admin_users_url assert_response :success - assert_match(/n\/a/, response.body, "Page should show n/a for users without trial end date") + assert_match(/Active/, response.body, "Page should show subscription status for families with active subscriptions") + end + + test "index shows no subscription label for families without subscription" do + users(:family_admin).family.subscription&.destroy + + get admin_users_url + assert_response :success + assert_match(/No subscription/, response.body, "Page should show 'No subscription' for families without one") end end