From eae976e6a1a9c80efdf2d8f86f050257cbde1cd9 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 9 Oct 2025 10:42:08 -0400 Subject: [PATCH 1/6] Enhance Payment admin interface: include user associations in scoped collection and sort user filter by email. Update index to allow sorting by user email. --- app/admin/payments.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/admin/payments.rb b/app/admin/payments.rb index 202e5ad..470d7bb 100644 --- a/app/admin/payments.rb +++ b/app/admin/payments.rb @@ -18,7 +18,13 @@ # end actions :index, :show, :new, :create, :update, :edit - filter :user_id, as: :select, collection: -> { User.all.map { |u| [u.email, u.id] } } + controller do + def scoped_collection + super.includes(:user) + end + end + + filter :user_id, as: :select, collection: -> { User.order(:email).map { |u| [u.email, u.id] } } filter :program_year, as: :select filter :account_type, as: :select filter :created_at @@ -46,7 +52,7 @@ end index do - column :user + column :user, sortable: 'users.email' column 'Type', &:transaction_type column 'Status', &:transaction_status column :transaction_id From 2fb09fb8a5cfa28692b27307e706182c3bbb79d3 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 9 Oct 2025 10:46:09 -0400 Subject: [PATCH 2/6] Add Rake task to generate and clean up sample payment data for testing --- lib/tasks/generate_sample_payments.rake | 140 ++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 lib/tasks/generate_sample_payments.rake diff --git a/lib/tasks/generate_sample_payments.rake b/lib/tasks/generate_sample_payments.rake new file mode 100644 index 0000000..442d626 --- /dev/null +++ b/lib/tasks/generate_sample_payments.rake @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +namespace :sample_data do + desc "Generate sample payment records for testing" + task generate_payments: :environment do + unless Rails.env.development? + puts "This task can only be run in development environment" + exit + end + + puts "Generating sample payment data..." + + # Get or create a program setting + program = ProgramSetting.find_or_create_by!(program_year: 2024) do |p| + p.active = true + p.program_open = Time.zone.now + p.program_close = 2.days.from_now + p.application_fee = 5000 # $50.00 + p.program_fee = 25000 # $250.00 + p.allow_payments = true + p.open_instructions = 'Open Instructions' + p.close_instructions = 'Close Instructions' + p.payment_instructions = 'Payment Instructions' + end + + # Sample last names for more realistic data + last_names = %w[ + Smith Johnson Williams Brown Jones Garcia Miller Davis Rodriguez Martinez + Hernandez Lopez Gonzalez Wilson Anderson Thomas Taylor Moore Jackson Martin + Lee Perez Thompson White Harris Sanchez Clark Ramirez Lewis Robinson Walker + Young Allen King Wright Scott Torres Nguyen Hill Flores Green Adams Nelson + Baker Hall Rivera Campbell Mitchell Carter Roberts Gomez Phillips Evans Turner + Diaz Parker Cruz Edwards Collins Reyes Stewart Morris Morales Murphy Cook + Rogers Gutierrez Ortiz Morgan Cooper Peterson Bailey Reed Kelly Howard Ramos + Kim Cox Ward Richardson Watson Brooks Chavez Wood James Bennett Gray Mendoza + Ruiz Hughes Price Alvarez Castillo Sanders Patel Myers Long Ross Foster Jimenez + ] + + first_names = %w[ + James Mary Michael Patricia Robert Jennifer John Linda William Elizabeth + David Barbara Richard Susan Joseph Jessica Thomas Sarah Charles Karen + Christopher Nancy Daniel Lisa Matthew Betty Donald Ashley Mark Kimberly + Paul Emily Donald Donna George Carol Kenneth Michelle Steven Laura + Edward Sandra Brian Dorothy Ronald Ashley Anthony Melissa Kevin Amanda + Jason Stephanie Jeff Rebecca Ryan Deborah Gary Sharon Nicholas Laura + Jacob Cynthia Tyler Amy Scott Angela Eric Kathleen Stephen Shirley + Jonathan Emma Brandon Donna William Ruth Frank Anna Raymond Diana + ] + + transaction_statuses = ['1', '2', '3'] # 1=success, 2=pending, 3=failed + account_types = %w[VISA MASTERCARD AMEX DISCOVER] + + # Keep track of created users to avoid duplicates + created_count = 0 + target_count = 100 + + puts "Creating users and payments..." + + target_count.times do |i| + # Generate unique email + first_name = first_names.sample + last_name = last_names.sample + email = "#{first_name.downcase}.#{last_name.downcase}.#{i}@example.com" + + # Create user + user = User.create!( + email: email, + password: 'password123', + password_confirmation: 'password123' + ) + + # Generate 1-3 payments per user to make it more realistic + payment_count = rand(1..3) + + payment_count.times do |payment_num| + status = transaction_statuses.sample + amount = case payment_num + when 0 + program.application_fee # First payment is application fee + when 1 + program.program_fee # Second payment is program fee + else + rand(5000..30000) # Additional payments vary + end + + Payment.create!( + user: user, + transaction_type: '1', + transaction_status: status, + transaction_id: "TXN#{Time.current.to_i}#{rand(1000..9999)}", + total_amount: amount.to_s, + transaction_date: rand(30.days.ago..Time.current).strftime('%Y%m%d%H%M'), + account_type: account_types.sample, + result_code: status == '1' ? '00' : rand(100..999).to_s, + result_message: status == '1' ? 'Success' : 'Processing', + user_account: "**** **** **** #{rand(1000..9999)}", + payer_identity: email, + timestamp: rand(30.days.ago..Time.current).to_i.to_s, + transaction_hash: Digest::SHA256.hexdigest("#{email}#{Time.current.to_i}#{rand}"), + program_year: 2024 + ) + + created_count += 1 + end + + print "\rCreated #{i + 1}/#{target_count} users with #{created_count} payments..." + end + + puts "\n" + puts "✓ Successfully created #{target_count} users" + puts "✓ Successfully created #{created_count} payments" + puts "✓ All records are associated with program year 2024" + puts "\nYou can now view them in ActiveAdmin at /admin/payments" + end + + desc "Clean up sample payment data" + task cleanup_payments: :environment do + unless Rails.env.development? + puts "This task can only be run in development environment" + exit + end + + print "Are you sure you want to delete all payments and users from example.com? (yes/no): " + confirmation = STDIN.gets.chomp + + if confirmation.downcase == 'yes' + users = User.where("email LIKE ?", "%@example.com") + payment_count = Payment.where(user: users).count + user_count = users.count + + Payment.where(user: users).destroy_all + users.destroy_all + + puts "✓ Deleted #{payment_count} payments" + puts "✓ Deleted #{user_count} users" + else + puts "Cleanup cancelled" + end + end +end From 657786c7248ffbaded3ffdc3e409eac93967ca01 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 9 Oct 2025 10:53:39 -0400 Subject: [PATCH 3/6] Implement user payment totals sorting and balance due calculation in dashboard. Enhance table display with sortable columns for user, total paid, and balance due, improving data presentation and usability. --- app/admin/dashboard.rb | 85 ++++++++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb index f772baf..e96e6f1 100644 --- a/app/admin/dashboard.rb +++ b/app/admin/dashboard.rb @@ -7,41 +7,92 @@ if ProgramSetting.active_program.exists? active_program = ProgramSetting.active_program.last + # Get sort parameters + sort_column = params[:sort_column] || 'total_paid' + sort_order = params[:sort_order] || 'desc' + # User Payment Totals Section user_totals = Payment.current_program_payments(active_program.program_year) .where(transaction_status: '1') # Only successful payments .joins(:user) .group('users.id', 'users.email') .sum('payments.total_amount::float / 100') - .sort_by { |_, amount| -amount } # Sort by amount descending + + # Calculate balance due for each user and prepare data for sorting + user_data_with_balance = user_totals.map do |user_data, total_paid| + user_id, user_email = user_data + user = User.find(user_id) + balance_due = Payment.current_balance_due_for_user(user, active_program.program_year) + { + user_id: user_id, + user_email: user_email, + user: user, + total_paid: total_paid, + balance_due: balance_due + } + end + + # Apply sorting + user_data_with_balance = case sort_column + when 'user' + user_data_with_balance.sort_by { |data| data[:user_email] } + when 'balance_due' + user_data_with_balance.sort_by { |data| data[:balance_due].to_f } + else # 'total_paid' or default + user_data_with_balance.sort_by { |data| data[:total_paid] } + end + + # Reverse if descending + user_data_with_balance.reverse! if sort_order == 'desc' div class: 'dashboard_section' do h2 "User Payment Totals - Program Year #{active_program.program_year}" - if user_totals.any? + if user_data_with_balance.any? table class: 'index_table' do thead do tr do - th 'User' + th do + next_order = (sort_column == 'user' && sort_order == 'asc') ? 'desc' : 'asc' + a href: admin_dashboard_path(sort_column: 'user', sort_order: next_order), title: 'Click to sort' do + text_node 'User ' + span class: 'sort_indicator' do + if sort_column == 'user' + text_node sort_order == 'asc' ? '▲' : '▼' + else + text_node '⇅' + end + end + end + end th 'Total Paid' th 'Program Cost' - th 'Balance Due' + th do + next_order = (sort_column == 'balance_due' && sort_order == 'asc') ? 'desc' : 'asc' + a href: admin_dashboard_path(sort_column: 'balance_due', sort_order: next_order), title: 'Click to sort' do + text_node 'Balance Due ' + span class: 'sort_indicator' do + if sort_column == 'balance_due' + text_node sort_order == 'asc' ? '▲' : '▼' + else + text_node '⇅' + end + end + end + end th 'Status' end end tbody do - user_totals.each do |user_data, total_paid| - user_id, user_email = user_data - user = User.find(user_id) - balance_due = Payment.current_balance_due_for_user(user, active_program.program_year) - status = balance_due.to_i.zero? ? 'Paid in Full' : 'Outstanding Balance' - status_class = balance_due.to_i.zero? ? 'paid_full' : 'outstanding' + user_data_with_balance.each do |data| + status = data[:balance_due].to_i.zero? ? 'Paid in Full' : 'Outstanding Balance' + status_class = data[:balance_due].to_i.zero? ? 'paid_full' : 'outstanding' tr class: status_class do - td user_email - td number_to_currency(total_paid) + td data[:user_email] + td number_to_currency(data[:total_paid]) td number_to_currency(active_program.total_cost) - td number_to_currency(balance_due) + td number_to_currency(data[:balance_due]) td status end end @@ -49,12 +100,8 @@ end div class: 'pagination_info' do - total_users = user_totals.count - paid_in_full = user_totals.count { |user_data, _| - user_id = user_data[0] - user = User.find(user_id) - Payment.current_balance_due_for_user(user, active_program.program_year).to_i.zero? - } + total_users = user_data_with_balance.count + paid_in_full = user_data_with_balance.count { |data| data[:balance_due].to_i.zero? } text_node "Total Users: #{total_users} | Paid in Full: #{paid_in_full} | Outstanding: #{total_users - paid_in_full}" end else From eb9b02a35b92581713d67dca11e96c545074554e Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 9 Oct 2025 11:17:46 -0400 Subject: [PATCH 4/6] Implement pagination for user payment totals in dashboard, enhancing data navigation and display. Update sorting links to maintain pagination state and improve user experience. --- app/admin/dashboard.rb | 87 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb index e96e6f1..afd520d 100644 --- a/app/admin/dashboard.rb +++ b/app/admin/dashboard.rb @@ -7,9 +7,11 @@ if ProgramSetting.active_program.exists? active_program = ProgramSetting.active_program.last - # Get sort parameters + # Get sort and pagination parameters sort_column = params[:sort_column] || 'total_paid' sort_order = params[:sort_order] || 'desc' + page = (params[:page] || 1).to_i + per_page = 20 # User Payment Totals Section user_totals = Payment.current_program_payments(active_program.program_year) @@ -45,6 +47,14 @@ # Reverse if descending user_data_with_balance.reverse! if sort_order == 'desc' + # Pagination + total_users = user_data_with_balance.count + total_pages = (total_users.to_f / per_page).ceil + page = [[page, 1].max, total_pages].min if total_pages > 0 # Ensure page is within valid range + start_index = (page - 1) * per_page + end_index = start_index + per_page - 1 + paginated_data = user_data_with_balance[start_index..end_index] || [] + div class: 'dashboard_section' do h2 "User Payment Totals - Program Year #{active_program.program_year}" @@ -54,7 +64,7 @@ tr do th do next_order = (sort_column == 'user' && sort_order == 'asc') ? 'desc' : 'asc' - a href: admin_dashboard_path(sort_column: 'user', sort_order: next_order), title: 'Click to sort' do + a href: admin_dashboard_path(sort_column: 'user', sort_order: next_order, page: page), title: 'Click to sort' do text_node 'User ' span class: 'sort_indicator' do if sort_column == 'user' @@ -69,7 +79,7 @@ th 'Program Cost' th do next_order = (sort_column == 'balance_due' && sort_order == 'asc') ? 'desc' : 'asc' - a href: admin_dashboard_path(sort_column: 'balance_due', sort_order: next_order), title: 'Click to sort' do + a href: admin_dashboard_path(sort_column: 'balance_due', sort_order: next_order, page: page), title: 'Click to sort' do text_node 'Balance Due ' span class: 'sort_indicator' do if sort_column == 'balance_due' @@ -84,7 +94,7 @@ end end tbody do - user_data_with_balance.each do |data| + paginated_data.each do |data| status = data[:balance_due].to_i.zero? ? 'Paid in Full' : 'Outstanding Balance' status_class = data[:balance_due].to_i.zero? ? 'paid_full' : 'outstanding' @@ -100,9 +110,63 @@ end div class: 'pagination_info' do - total_users = user_data_with_balance.count paid_in_full = user_data_with_balance.count { |data| data[:balance_due].to_i.zero? } - text_node "Total Users: #{total_users} | Paid in Full: #{paid_in_full} | Outstanding: #{total_users - paid_in_full}" + showing_start = total_users.zero? ? 0 : start_index + 1 + showing_end = [end_index + 1, total_users].min + text_node "Displaying users #{showing_start} - #{showing_end} of #{total_users} | " + text_node "Paid in Full: #{paid_in_full} | Outstanding: #{total_users - paid_in_full}" + end + + # Pagination controls + if total_pages > 1 + div class: 'pagination', style: 'text-align: center; margin: 20px 0;' do + # Previous button + if page > 1 + a href: admin_dashboard_path(sort_column: sort_column, sort_order: sort_order, page: page - 1), + class: 'pagination_link', + style: 'padding: 5px 10px; margin: 0 2px; text-decoration: none;' do + text_node '« Previous' + end + else + span style: 'padding: 5px 10px; margin: 0 2px; color: #ccc;' do + text_node '« Previous' + end + end + + # Page numbers + (1..total_pages).each do |p| + if p == page + span class: 'current', + style: 'padding: 5px 10px; margin: 0 2px; background-color: #5E6469; color: white; border-radius: 3px;' do + text_node p.to_s + end + else + a href: admin_dashboard_path(sort_column: sort_column, sort_order: sort_order, page: p), + class: 'pagination_link', + style: 'padding: 5px 10px; margin: 0 2px; text-decoration: none;' do + text_node p.to_s + end + end + end + + # Next button + if page < total_pages + a href: admin_dashboard_path(sort_column: sort_column, sort_order: sort_order, page: page + 1), + class: 'pagination_link', + style: 'padding: 5px 10px; margin: 0 2px; text-decoration: none;' do + text_node 'Next »' + end + else + span style: 'padding: 5px 10px; margin: 0 2px; color: #ccc;' do + text_node 'Next »' + end + end + end + end + + # Section separator with subtle styling + div style: 'width: 100%; height: 1px; background-color: #e2e8f0; margin: 3rem 0 2rem 0; border-radius: 1px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);' do + text_node '' end else div class: 'blank_slate' do @@ -111,11 +175,6 @@ end end - # Hard line separator - div style: 'width: 100%; height: 4px; background-color: #2d3748; margin: 2rem 0; border: none;' do - text_node '' - end - # Recent Payments Section recent_payments = Payment.current_program_payments(active_program.program_year) .includes(:user) @@ -156,7 +215,7 @@ end div class: 'pagination_info' do - text_node "Showing #{recent_payments.count} of #{Payment.current_program_payments(active_program.program_year).count} payments" + text_node "Showing last #{recent_payments.count} of #{Payment.current_program_payments(active_program.program_year).count} payments" end else div class: 'blank_slate' do @@ -172,10 +231,6 @@ end end - # Hard line separator - div style: 'width: 100%; height: 4px; background-color: #2d3748; margin: 2rem 0; border: none;' do - text_node '' - end # Static page message text_node StaticPage.find_by(location: 'dashboard').message if StaticPage.find_by(location: 'dashboard').present? end From ce5f6567c12e01b81024fd330b2466bfdb12c3ff Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 9 Oct 2025 11:37:14 -0400 Subject: [PATCH 5/6] Enhance dashboard user email display by linking to payment details, improving navigation for admin users. --- app/admin/dashboard.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb index afd520d..2e6a4ab 100644 --- a/app/admin/dashboard.rb +++ b/app/admin/dashboard.rb @@ -99,7 +99,10 @@ status_class = data[:balance_due].to_i.zero? ? 'paid_full' : 'outstanding' tr class: status_class do - td data[:user_email] + td do + link_to data[:user_email], admin_payments_path('q[user_id_eq]' => data[:user_id]), + title: "View all payments for #{data[:user_email]}" + end td number_to_currency(data[:total_paid]) td number_to_currency(active_program.total_cost) td number_to_currency(data[:balance_due]) From 927585dc1c0642008c990fb3399122f54d1ae2bf Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Thu, 9 Oct 2025 12:08:12 -0400 Subject: [PATCH 6/6] Refine dashboard display by linking user emails to payment details and updating pagination text for clarity, enhancing admin navigation and user experience. --- app/admin/dashboard.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb index 2e6a4ab..80cb993 100644 --- a/app/admin/dashboard.rb +++ b/app/admin/dashboard.rb @@ -203,7 +203,10 @@ tbody do recent_payments.each do |payment| tr do - td payment.user.email + td do + link_to payment.user.email, admin_payments_path('q[user_id_eq]' => payment.user_id), + title: "View all payments for #{payment.user.email}" + end td payment.transaction_id td number_to_currency(payment.total_amount.to_f / 100) td payment.transaction_status == '1' ? 'Success' : 'Failed' @@ -218,7 +221,7 @@ end div class: 'pagination_info' do - text_node "Showing last #{recent_payments.count} of #{Payment.current_program_payments(active_program.program_year).count} payments" + text_node "Showing most recent #{recent_payments.count} payments" end else div class: 'blank_slate' do