diff --git a/app/controllers/admin/reports/monthly_activities_controller.rb b/app/controllers/admin/reports/monthly_activities_controller.rb
index 995a96260..cc593cd60 100644
--- a/app/controllers/admin/reports/monthly_activities_controller.rb
+++ b/app/controllers/admin/reports/monthly_activities_controller.rb
@@ -2,30 +2,43 @@ module Admin
module Reports
class MonthlyActivitiesController < BaseController
def index
- fetch_monthly_activities
+ @monthly_activities = fetch_activities
end
- def fetch_monthly_activities
- @monthly_activities = {}
+ private
- # TODO: load_async in Rails 7
- appointments = MonthlyAppointment.all
- loans = MonthlyLoan.all
- members = MonthlyMember.all
+ def fetch_activities
+ records = [*MonthlyAppointment.all, *MonthlyLoan.all, *MonthlyMember.all, *MonthlyRenewal.all]
- assign_monthlies(appointments, %i[appointments_count completed_appointments_count])
- assign_monthlies(loans, %i[loans_count active_members_count])
- assign_monthlies(members, %i[new_members_count pending_members_count])
+ records.group_by(&:year).each_with_object([]) do |(year, records_for_year), grouped_year|
+ monthly_values = records_for_year.group_by(&:month).each_with_object([]) do |(month, records_for_month), grouped_month|
+ grouped_month << [month, records_to_amount_hash(records_for_month)]
+ end
- @monthly_activities = @monthly_activities.sort.to_h
+ grouped_year << [year, monthly_values.sort_by(&:first)]
+ end.sort_by(&:first)
end
- def assign_monthlies(records, columns)
- records.each do |record|
- key = "#{record.year}-#{record.month.to_s.rjust(2, "0")}"
- monthly = @monthly_activities[key] ||= Hash.new(0)
+ def columns_for_record(record)
+ case record
+ when MonthlyAppointment
+ %i[appointments_count completed_appointments_count]
+ when MonthlyLoan
+ %i[loans_count active_members_count]
+ when MonthlyMember
+ %i[new_members_count pending_members_count]
+ when MonthlyRenewal
+ %i[renewals_count]
+ else
+ raise "Unknow record type: #{record}"
+ end
+ end
- columns.each { |column| monthly[column] = record[column] }
+ def records_to_amount_hash(records)
+ records.each_with_object(Hash.new(0)) do |record, hash|
+ columns_for_record(record).each do |column|
+ hash[column] = record[column]
+ end
end
end
end
diff --git a/app/models/monthly_appointment.rb b/app/models/monthly_appointment.rb
index 3aeefaae5..c31f06003 100644
--- a/app/models/monthly_appointment.rb
+++ b/app/models/monthly_appointment.rb
@@ -1,2 +1,3 @@
+# View from the Scenic gem
class MonthlyAppointment < ApplicationRecord
end
diff --git a/app/models/monthly_loan.rb b/app/models/monthly_loan.rb
index 5ee9094c5..3a912b508 100644
--- a/app/models/monthly_loan.rb
+++ b/app/models/monthly_loan.rb
@@ -1,2 +1,3 @@
+# View from the Scenic gem
class MonthlyLoan < ApplicationRecord
end
diff --git a/app/models/monthly_member.rb b/app/models/monthly_member.rb
index d8fa479b4..6a45fff2f 100644
--- a/app/models/monthly_member.rb
+++ b/app/models/monthly_member.rb
@@ -1,2 +1,3 @@
+# View from the Scenic gem
class MonthlyMember < ApplicationRecord
end
diff --git a/app/models/monthly_renewal.rb b/app/models/monthly_renewal.rb
new file mode 100644
index 000000000..5c42e863a
--- /dev/null
+++ b/app/models/monthly_renewal.rb
@@ -0,0 +1,3 @@
+# View from the Scenic gem
+class MonthlyRenewal < ApplicationRecord
+end
diff --git a/app/views/admin/reports/monthly_activities/index.html.erb b/app/views/admin/reports/monthly_activities/index.html.erb
index bd51687d7..c26e56b7e 100644
--- a/app/views/admin/reports/monthly_activities/index.html.erb
+++ b/app/views/admin/reports/monthly_activities/index.html.erb
@@ -2,46 +2,83 @@
<%= index_header "Activity" %>
<% end %>
-
-
-
- |
- Activity |
- Members |
- Appointments |
-
-
- Month |
- Loans |
- Members |
- New |
- Pending |
- Scheduled |
- Completed |
-
-
-
- <% @monthly_activities.each do |date, activities| %>
-
- <% year, month = date.split("-") %>
- <%= Date::MONTHNAMES[month.to_i] %> <%= year %> |
- <%= activities[:loans_count] %> |
- <%= activities[:active_members_count] %> |
- <%= activities[:new_members_count] %> |
- <%= activities[:pending_members_count] %> |
- <%= activities[:appointments_count] %> |
- <%= activities[:completed_appointments_count] %> |
-
- <% end %>
-
-
- Total |
- <%= @monthly_activities.values.sum { |a| a[:loans_count] } %> |
- <%= @monthly_activities.values.sum { |a| a[:active_members_count] } %> |
- <%= @monthly_activities.values.sum { |a| a[:new_members_count] } %> |
- <%= @monthly_activities.values.sum { |a| a[:pending_members_count] } %> |
- <%= @monthly_activities.values.sum { |a| a[:appointments_count] } %> |
- <%= @monthly_activities.values.sum { |a| a[:completed_appointments_count] } %> |
-
-
-
+<% @monthly_activities.each do |(year, activities_by_month)| %>
+
+
<%= year -%>
+
+
+
+ |
+ Activity |
+ Members |
+ Appointments |
+
+
+ Month |
+ Loans |
+ Renewals |
+ Members |
+ New |
+ Pending |
+ Scheduled |
+ Completed |
+
+
+
+ <% activities_by_month.each do |month, activities| %>
+
+ <%= Date::MONTHNAMES[month.to_i] %> |
+ ">
+ <%= activities[:loans_count] %>
+ |
+ ">
+ <%= activities[:renewals_count] %>
+ |
+ ">
+ <%= activities[:active_members_count] %>
+ |
+ ">
+ <%= activities[:new_members_count] %>
+ |
+ ">
+ <%= activities[:pending_members_count] %>
+ |
+ ">
+ <%= activities[:appointments_count] %>
+ |
+ ">
+ <%= activities[:completed_appointments_count] %>
+ |
+
+ <% end %>
+
+
+ Total |
+ ">
+ <%= activities_by_month.map(&:second).sum { |a| a[:loans_count] } %>
+ |
+ ">
+ <%= activities_by_month.map(&:second).sum { |a| a[:renewals_count] } %>
+ |
+ ">
+ <%= activities_by_month.map(&:second).sum { |a| a[:active_members_count] } %>
+ |
+ ">
+ <%= activities_by_month.map(&:second).sum { |a| a[:new_members_count] } %>
+ |
+ ">
+ <%= activities_by_month.map(&:second).sum { |a| a[:pending_members_count] } %>
+ |
+ ">
+ <%= activities_by_month.map(&:second).sum { |a| a[:appointments_count] } %>
+ |
+ ">
+ <%= activities_by_month.map(&:second).sum { |a| a[:completed_appointments_count] } %>
+ |
+
+
+
+
+
+
+<% end %>
diff --git a/db/migrate/20250105181935_create_monthly_renewals.rb b/db/migrate/20250105181935_create_monthly_renewals.rb
new file mode 100644
index 000000000..a154ba838
--- /dev/null
+++ b/db/migrate/20250105181935_create_monthly_renewals.rb
@@ -0,0 +1,5 @@
+class CreateMonthlyRenewals < ActiveRecord::Migration[7.2]
+ def change
+ create_view :monthly_renewals
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 11c972c16..2de6e2d5c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.2].define(version: 2024_12_18_224853) do
+ActiveRecord::Schema[7.2].define(version: 2025_01_05_181935) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1145,4 +1145,22 @@
GROUP BY months.month
ORDER BY months.month;
SQL
+ create_view "monthly_renewals", sql_definition: <<-SQL
+ WITH dates AS (
+ SELECT min(date_trunc('month'::text, loans.created_at)) AS startm,
+ max(date_trunc('month'::text, loans.created_at)) AS endm
+ FROM loans
+ ), months AS (
+ SELECT generate_series(dates.startm, dates.endm, 'P1M'::interval) AS month
+ FROM dates
+ )
+ SELECT (EXTRACT(year FROM months.month))::integer AS year,
+ (EXTRACT(month FROM months.month))::integer AS month,
+ count(DISTINCT l.id) AS renewals_count
+ FROM (months
+ LEFT JOIN loans l ON ((date_trunc('month'::text, l.created_at) = months.month)))
+ WHERE (l.initial_loan_id IS NOT NULL)
+ GROUP BY months.month
+ ORDER BY months.month;
+ SQL
end
diff --git a/db/views/monthly_renewals_v01.sql b/db/views/monthly_renewals_v01.sql
new file mode 100644
index 000000000..530c12d5c
--- /dev/null
+++ b/db/views/monthly_renewals_v01.sql
@@ -0,0 +1,15 @@
+WITH dates AS
+ (SELECT min(date_trunc('month', created_at)) AS startm,
+ max(date_trunc('month', created_at)) AS endm
+ FROM loans),
+ months AS
+ (SELECT generate_series(startm, endm, '1 month') AS month
+ FROM dates)
+SELECT extract(YEAR FROM months.month) ::integer AS year,
+ extract(MONTH FROM months.month)::integer AS month,
+ count(DISTINCT l.id) AS renewals_count
+FROM months
+ LEFT JOIN loans l ON date_trunc('month', l.created_at) = months.month
+WHERE l.initial_loan_id IS NOT NULL
+GROUP BY months.month
+ORDER BY months.month;
diff --git a/test/system/admin/reports/monthly_activities_test.rb b/test/system/admin/reports/monthly_activities_test.rb
index b373894f2..159a93020 100644
--- a/test/system/admin/reports/monthly_activities_test.rb
+++ b/test/system/admin/reports/monthly_activities_test.rb
@@ -4,8 +4,8 @@ module Admin
class MonthlyActivitiesTest < ApplicationSystemTestCase
include AdminHelper
- def setup
- @date = Time.zone.parse("2022-1-1")
+ setup do
+ @date = Time.zone.parse("2022-1-15")
travel_to @date
# 2 new (verified) members this month and 1 pending
@@ -16,9 +16,10 @@ def setup
# 1 existing member from November last year
november_member_1 = create(:verified_member_with_membership, created_at: Time.zone.parse("2021-11-1"))
- # 2 loans for the same member this month, giving us 1 active member
+ # 3 loans (1 renewal) for the same member this month, giving us 1 active member
january_loan_1 = create(:loan, member: january_member_1)
- january_loan_2 = create(:loan, member: january_member_1)
+ january_loan_2 = create(:loan, member: january_member_1, due_at: 1.week.ago, ended_at: 1.week.ago, created_at: 8.days.ago)
+ january_loan_3 = create(:loan, initial_loan: january_loan_2, member: january_member_1, renewal_count: 1, item: january_loan_2.item)
# 1 loan for an old member last month, giving us 1 active member
december_loan_1 = create(:loan, member: november_member_1, created_at: Time.zone.parse("2021-12-10"))
@@ -27,7 +28,7 @@ def setup
create(:appointment, member: january_member_1, starts_at: Time.zone.parse("2022-1-5"),
ends_at: Time.zone.parse("2022-1-6"), loans: [january_loan_1])
create(:appointment, member: january_member_1, starts_at: Time.zone.parse("2022-1-27"),
- ends_at: Time.zone.parse("2022-1-28"), completed_at: Time.zone.parse("2022-1-28"), loans: [january_loan_2])
+ ends_at: Time.zone.parse("2022-1-28"), completed_at: Time.zone.parse("2022-1-28"), loans: [january_loan_3])
# 1 appointment which started and was completed last month but ended this month,
# giving us 1 for last month
@@ -37,100 +38,119 @@ def setup
sign_in_as_admin
end
- def teardown
- end
-
- # ╔═══════════════╦══════════════════════╦═════════════════════╦════════════════════════════╗
- # ║ ║ Activity ║ Members ║ Appointments ║
- # ├───────────────┼──────────────────────┼─────────────────────┼────────────────────────────┤
- # ║ Month ║ Loans ║ Members ║ New ║ Pending ║ Scheduled ║ Completed ║
- # ╠═══════════════╬═══════════╬══════════╬══════════╬══════════╬═══════════════╬════════════╣
- # ║ November 2021 ║ 0 ║ 0 ║ 1 ║ 0 ║ 0 ║ 0 ║
- # ║ December 2021 ║ 1 ║ 1 ║ 0 ║ 0 ║ 1 ║ 1 ║
- # ║ January 2022 ║ 2 ║ 1 ║ 2 ║ 1 ║ 2 ║ 1 ║
- # ╠═══════════════╬═══════════╬══════════╬══════════╬══════════╬═══════════════╬════════════╣
- # ║ Total ║ 3 ║ 2 ║ 3 ║ 1 ║ 3 ║ 2 ║
- # ╚═══════════════╩═══════════╩══════════╩══════════╩══════════╩═══════════════╩════════════╝
+ # Table for 2021
+ # ╔═══════════════╦══════════════════════════════════╦═════════════════════╦════════════════════════════╗
+ # ║ ║ Activity ║ Members ║ Appointments ║
+ # ├───────────────┼──────────────────────────────────┼─────────────────────┼────────────────────────────┤
+ # ║ Month ║ Loans ║ Renewals ║ Members ║ New ║ Pending ║ Scheduled ║ Completed ║
+ # ╠═══════════════╬═══════════╬═══════════╬══════════╬══════════╬══════════╬═══════════════╬════════════╣
+ # ║ November ║ 0 ║ 0 ║ 0 ║ 1 ║ 0 ║ 0 ║ 0 ║
+ # ║ December ║ 1 ║ 0 ║ 1 ║ 0 ║ 0 ║ 1 ║ 1 ║
+ # ╠═══════════════╬═══════════╬═══════════╬══════════╬══════════╬══════════╬═══════════════╬════════════╣
+ # ║ Total ║ 1 ║ 0 ║ 1 ║ 1 ║ 0 ║ 1 ║ 1 ║
+ # ╚═══════════════╩═══════════╩═══════════╩══════════╩══════════╩══════════╩═══════════════╩════════════╝
+ # Table for 2022
+ # ╔═══════════════╦══════════════════════════════════╦═════════════════════╦════════════════════════════╗
+ # ║ ║ Activity ║ Members ║ Appointments ║
+ # ├───────────────┼──────────────────────────────────┼─────────────────────┼────────────────────────────┤
+ # ║ Month ║ Loans ║ Renewals ║ Members ║ New ║ Pending ║ Scheduled ║ Completed ║
+ # ╠═══════════════╬═══════════╬═══════════╬══════════╬══════════╬══════════╬═══════════════╬════════════╣
+ # ║ January ║ 3 ║ 1 ║ 1 ║ 2 ║ 1 ║ 2 ║ 1 ║
+ # ╠═══════════════╬═══════════╬═══════════╬══════════╬══════════╬══════════╬═══════════════╬════════════╣
+ # ║ Total ║ 3 ║ 1 ║ 1 ║ 2 ║ 1 ║ 2 ║ 1 ║
+ # ╚═══════════════╩═══════════╩═══════════╩══════════╩══════════╩══════════╩═══════════════╩════════════╝
test "table is populated accordingly" do
visit admin_reports_monthly_activities_url
- assert_selector ".monthly-adjustments"
- within(".monthly-adjustments") do
- # ║ ║ Activity ║ Members ║ Appointments ║
- within("thead > tr:nth-child(1)") do
- within("th:nth-child(2)") { assert_text("Activity") }
- within("th:nth-child(3)") { assert_text("Members") }
- within("th:nth-child(4)") { assert_text("Appointments") }
- end
+ assert_selector "#year-2021"
+ assert_selector "#year-2022"
- # ║ Month ║ Loans ║ Members ║ New ║ Pending ║ Scheduled ║ Completed ║
- within("thead > tr:nth-child(2)") do
- within("th:nth-child(1)") { assert_text("Month") }
+ # table headings
+ ["#year-2021", "#year-2022"].each do |selector|
+ within("#year-2021") do
+ within("thead > tr:nth-child(1)") do
+ within("th:nth-child(2)") { assert_text("Activity") }
+ within("th:nth-child(3)") { assert_text("Members") }
+ within("th:nth-child(4)") { assert_text("Appointments") }
+ end
- within("th:nth-child(2)") { assert_text("Loans") }
- within("th:nth-child(3)") { assert_text("Members") }
+ within("thead > tr:nth-child(2)") do
+ within("th:nth-child(1)") { assert_text("Month") }
- within("th:nth-child(4)") { assert_text("New") }
- within("th:nth-child(5)") { assert_text("Pending") }
+ within("th:nth-child(2)") { assert_text("Loans") }
+ within("th:nth-child(3)") { assert_text("Renewals") }
+ within("th:nth-child(4)") { assert_text("Members") }
- within("th:nth-child(6)") { assert_text("Scheduled") }
- within("th:nth-child(7)") { assert_text("Completed") }
+ within("th:nth-child(5)") { assert_text("New") }
+ within("th:nth-child(6)") { assert_text("Pending") }
+
+ within("th:nth-child(7)") { assert_text("Scheduled") }
+ within("th:nth-child(8)") { assert_text("Completed") }
+ end
end
+ end
- # ║ November 2021 ║ 0 ║ 0 ║ 1 ║ 0 ║ 0 ║ 0 ║
+ within("#year-2021") do
within("tbody > tr:nth-child(1)") do
- within("td:nth-child(1)") { assert_text("November 2021") }
-
- within("td:nth-child(2)") { assert_text("0") }
- within("td:nth-child(3)") { assert_text("0") }
-
- within("td:nth-child(4)") { assert_text("1") }
- within("td:nth-child(5)") { assert_text("0") }
-
- within("td:nth-child(6)") { assert_text("0") }
- within("td:nth-child(7)") { assert_text("0") }
+ within("td.month") { assert_text("November") }
+
+ within("td.loans_count-2021-11") { assert_text("0") }
+ within("td.renewals_count-2021-11") { assert_text("0") }
+ within("td.active_members_count-2021-11") { assert_text("0") }
+ within("td.new_members_count-2021-11") { assert_text("1") }
+ within("td.pending_members_count-2021-11") { assert_text("0") }
+ within("td.appointments_count-2021-11") { assert_text("0") }
+ within("td.completed_appointments_count-2021-11") { assert_text("0") }
end
- # ║ December 2021 ║ 1 ║ 1 ║ 0 ║ 0 ║ 1 ║ 1 ║
within("tbody > tr:nth-child(2)") do
- within("td:nth-child(1)") { assert_text("December 2021") }
-
- within("td:nth-child(2)") { assert_text("1") }
- within("td:nth-child(3)") { assert_text("1") }
-
- within("td:nth-child(4)") { assert_text("0") }
- within("td:nth-child(5)") { assert_text("0") }
-
- within("td:nth-child(6)") { assert_text("1") }
- within("td:nth-child(7)") { assert_text("1") }
+ within("td.month") { assert_text("December") }
+
+ within("td.loans_count-2021-12") { assert_text("1") }
+ within("td.renewals_count-2021-12") { assert_text("0") }
+ within("td.active_members_count-2021-12") { assert_text("1") }
+ within("td.new_members_count-2021-12") { assert_text("0") }
+ within("td.pending_members_count-2021-12") { assert_text("0") }
+ within("td.appointments_count-2021-12") { assert_text("1") }
+ within("td.completed_appointments_count-2021-12") { assert_text("1") }
end
- # ║ January 2022 ║ 2 ║ 1 ║ 2 ║ 1 ║ 2 ║ 1 ║
- within("tbody > tr:nth-child(3)") do
- within("td:nth-child(1)") { assert_text("January 2022") }
-
- within("td:nth-child(2)") { assert_text("2") }
- within("td:nth-child(3)") { assert_text("1") }
-
- within("td:nth-child(4)") { assert_text("2") }
- within("td:nth-child(5)") { assert_text("1") }
+ within("tfoot > tr") do
+ within("td.total") { assert_text("Total") }
+
+ within("td.loans_count-2021") { assert_text("1") }
+ within("td.renewals_count-2021") { assert_text("0") }
+ within("td.active_members_count-2021") { assert_text("1") }
+ within("td.new_members_count-2021") { assert_text("1") }
+ within("td.pending_members_count-2021") { assert_text("0") }
+ within("td.appointments_count-2021") { assert_text("1") }
+ within("td.completed_appointments_count-2021") { assert_text("1") }
+ end
+ end
- within("td:nth-child(6)") { assert_text("2") }
- within("td:nth-child(7)") { assert_text("1") }
+ within("#year-2022") do
+ within("tbody > tr:nth-child(1)") do
+ within("td.month") { assert_text("January") }
+
+ within("td.loans_count-2022-1") { assert_text("3") }
+ within("td.renewals_count-2022-1") { assert_text("1") }
+ within("td.active_members_count-2022-1") { assert_text("1") }
+ within("td.new_members_count-2022-1") { assert_text("2") }
+ within("td.pending_members_count-2022-1") { assert_text("1") }
+ within("td.appointments_count-2022-1") { assert_text("2") }
+ within("td.completed_appointments_count-2022-1") { assert_text("1") }
end
- # ║ Total ║ 3 ║ 2 ║ 3 ║ 1 ║ 3 ║ 2 ║
within("tfoot > tr") do
- within("td:nth-child(1)") { assert_text("Total") }
-
- within("td:nth-child(2)") { assert_text("3") }
- within("td:nth-child(3)") { assert_text("2") }
-
- within("td:nth-child(4)") { assert_text("3") }
- within("td:nth-child(5)") { assert_text("1") }
-
- within("td:nth-child(6)") { assert_text("3") }
- within("td:nth-child(7)") { assert_text("2") }
+ within("td.total") { assert_text("Total") }
+
+ within("td.loans_count-2022") { assert_text("3") }
+ within("td.renewals_count-2022") { assert_text("1") }
+ within("td.active_members_count-2022") { assert_text("1") }
+ within("td.new_members_count-2022") { assert_text("2") }
+ within("td.pending_members_count-2022") { assert_text("1") }
+ within("td.appointments_count-2022") { assert_text("2") }
+ within("td.completed_appointments_count-2022") { assert_text("1") }
end
end
end