From 4875ea500ac0a3bf9caa0b12ec76d3ea5e6ec9b2 Mon Sep 17 00:00:00 2001 From: Jon Allured Date: Sat, 10 Feb 2024 16:19:51 -0600 Subject: [PATCH] Admin views financial report --- .../financial_reports_controller.rb | 4 + app/models/financial_report.rb | 62 +++++++++ app/models/financial_statement.rb | 4 + app/models/null_statement.rb | 12 ++ app/views/dashboard/show.html.haml | 1 + .../financial_reports/_amount_cents.html.haml | 5 + app/views/financial_reports/show.html.haml | 66 +++++++++ app/views/layouts/wide.html.haml | 7 + config/routes.rb | 1 + .../admin_views_financial_report_spec.rb | 128 ++++++++++++++++++ 10 files changed, 290 insertions(+) create mode 100644 app/controllers/financial_reports_controller.rb create mode 100644 app/models/financial_report.rb create mode 100644 app/models/null_statement.rb create mode 100644 app/views/financial_reports/_amount_cents.html.haml create mode 100644 app/views/financial_reports/show.html.haml create mode 100644 app/views/layouts/wide.html.haml create mode 100644 spec/system/financial_reports/admin_views_financial_report_spec.rb diff --git a/app/controllers/financial_reports_controller.rb b/app/controllers/financial_reports_controller.rb new file mode 100644 index 0000000..9b8785c --- /dev/null +++ b/app/controllers/financial_reports_controller.rb @@ -0,0 +1,4 @@ +class FinancialReportsController < ApplicationController + layout "wide" + expose(:financial_report) { FinancialReport.new(params[:year].to_i) } +end diff --git a/app/models/financial_report.rb b/app/models/financial_report.rb new file mode 100644 index 0000000..2802d08 --- /dev/null +++ b/app/models/financial_report.rb @@ -0,0 +1,62 @@ +class FinancialReport + def self.months_for_year(year) + start_on = Date.parse("#{year}-01-01") + end_on = Date.parse("#{year}-12-01") + (start_on..end_on).uniq(&:month) + end + + attr_reader :year + + def initialize(year = Time.now.year) + @year = year + end + + def prev_year + @year - 1 + end + + def next_year + @year + 1 + end + + def checking_data + AccountData.new(FinancialAccount.wf_checking, @year) + end + + def savings_data + AccountData.new(FinancialAccount.wf_savings, @year) + end + + class AccountData + def initialize(account, year) + @account = account + @year = year + end + + def starting_amounts + statements.map(&:starting_amount_cents) + end + + def ending_amounts + statements.map(&:ending_amount_cents) + end + + def net_amounts + statements.map(&:net_amount_cents) + end + + private + + def compute_statements + account_statements = @account.financial_statements.for_year(@year) + FinancialReport.months_for_year(@year).map do |date| + statement_for_period = account_statements.find { |statement| statement.period_start_on == date } + statement_for_period || NullStatement.instance + end + end + + def statements + @statements ||= compute_statements + end + end +end diff --git a/app/models/financial_statement.rb b/app/models/financial_statement.rb index b6f1a84..f175c70 100644 --- a/app/models/financial_statement.rb +++ b/app/models/financial_statement.rb @@ -5,4 +5,8 @@ class FinancialStatement < ApplicationRecord validates_uniqueness_of :period_start_on, scope: :financial_account scope :for_year, ->(year) { where("extract(year from period_start_on) = ?", year) } + + def net_amount_cents + ending_amount_cents - starting_amount_cents + end end diff --git a/app/models/null_statement.rb b/app/models/null_statement.rb new file mode 100644 index 0000000..ce96578 --- /dev/null +++ b/app/models/null_statement.rb @@ -0,0 +1,12 @@ +class NullStatement + include Singleton + + def starting_amount_cents + end + + def ending_amount_cents + end + + def net_amount_cents + end +end diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml index c3d1a56..b5f39b4 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/show.html.haml @@ -1,6 +1,7 @@ %h1 Dashboard %p= link_to "Artsy Viewer", artsy_viewer_path %p= link_to "Cybertail", cybertail_path +%p= link_to "Financial Report", financial_report_path(year: Time.now.year) %p= link_to "Fairing Direball", faring_direball_path %p= link_to "Reading List", reading_list_path(year: Time.now.year) %p= link_to "Root", root_path diff --git a/app/views/financial_reports/_amount_cents.html.haml b/app/views/financial_reports/_amount_cents.html.haml new file mode 100644 index 0000000..37b227a --- /dev/null +++ b/app/views/financial_reports/_amount_cents.html.haml @@ -0,0 +1,5 @@ +- if amount_cents + %td.text-right{class: "#{amount_cents < 0 && 'text-purple'}"} + = number_to_currency(amount_cents / 100.0, negative_format: "(%u%n)") +- else + %td.text-right - diff --git a/app/views/financial_reports/show.html.haml b/app/views/financial_reports/show.html.haml new file mode 100644 index 0000000..b524e63 --- /dev/null +++ b/app/views/financial_reports/show.html.haml @@ -0,0 +1,66 @@ +- content_for :header do + %nav.flex.justify-between.items-center.py-6.border-b-8.border-dark-gray + %p.m-0= link_to "prev", financial_report_path(financial_report.prev_year) + %h1.m-0= financial_report.year + %p.m-0= link_to "next", financial_report_path(financial_report.next_year) + +- checking_data = financial_report.checking_data +- savings_data = financial_report.savings_data + +%h2 Checking + +%table.text-sm.table-fixed + %thead + %tr + %th.text-left(class="w-[70px]") Month + %th.text-right(class="w-[120px]") Jan + %th.text-right(class="w-[120px]") Feb + %th.text-right(class="w-[120px]") Mar + %th.text-right(class="w-[120px]") Apr + %th.text-right(class="w-[120px]") May + %th.text-right(class="w-[120px]") Jun + %th.text-right(class="w-[120px]") Jul + %th.text-right(class="w-[120px]") Aug + %th.text-right(class="w-[120px]") Sep + %th.text-right(class="w-[120px]") Oct + %th.text-right(class="w-[120px]") Nov + %th.text-right(class="w-[120px]") Dec + %tbody + %tr + %td.text-left Starting + = render partial: "amount_cents", collection: checking_data.starting_amounts + %tr + %td.text-left Ending + = render partial: "amount_cents", collection: checking_data.ending_amounts + %tr + %td.text-left Net + = render partial: "amount_cents", collection: checking_data.net_amounts + +%h2 Savings + +%table.text-sm.table-fixed + %thead + %tr + %th.text-left(class="w-[70px]") Month + %th.text-right(class="w-[120px]") Jan + %th.text-right(class="w-[120px]") Feb + %th.text-right(class="w-[120px]") Mar + %th.text-right(class="w-[120px]") Apr + %th.text-right(class="w-[120px]") May + %th.text-right(class="w-[120px]") Jun + %th.text-right(class="w-[120px]") Jul + %th.text-right(class="w-[120px]") Aug + %th.text-right(class="w-[120px]") Sep + %th.text-right(class="w-[120px]") Oct + %th.text-right(class="w-[120px]") Nov + %th.text-right(class="w-[120px]") Dec + %tbody + %tr + %td.text-left Starting + = render partial: "amount_cents", collection: savings_data.starting_amounts + %tr + %td.text-left Ending + = render partial: "amount_cents", collection: savings_data.ending_amounts + %tr + %td.text-left Net + = render partial: "amount_cents", collection: savings_data.net_amounts diff --git a/app/views/layouts/wide.html.haml b/app/views/layouts/wide.html.haml new file mode 100644 index 0000000..83cb8fe --- /dev/null +++ b/app/views/layouts/wide.html.haml @@ -0,0 +1,7 @@ +!!! +%html + %head + = render partial: "layouts/shared_head" + = yield :head + %body.bg-off-black.text-off-white.selection:bg-pink.p-4.md:p-0.mx-8 + = render partial: "layouts/shared_main" diff --git a/config/routes.rb b/config/routes.rb index ca217f3..b04c004 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ get "cybertail", to: "cybertail#index" get "dashboard", to: "dashboard#show" get "faring_direball", to: "faring_direball#index" + get "financial_reports/:year", to: "financial_reports#show", as: :financial_report get "reading-list/:year", to: "reading_list#index", as: :reading_list get "today", to: "today#show" get "wishlist", to: "wishlist#index" diff --git a/spec/system/financial_reports/admin_views_financial_report_spec.rb b/spec/system/financial_reports/admin_views_financial_report_spec.rb new file mode 100644 index 0000000..b66781d --- /dev/null +++ b/spec/system/financial_reports/admin_views_financial_report_spec.rb @@ -0,0 +1,128 @@ +require "rails_helper" + +describe "Admin views financial reports" do + include_context "admin password matches" + + before do + FactoryBot.create(:usb_checking) + FactoryBot.create(:wf_checking) + FactoryBot.create(:wf_savings) + end + + let!(:last_year) { Date.today - 1.year } + + scenario "with no statements for that year" do + visit "/financial_reports/#{last_year}" + + header = page.find("header nav h1") + expect(header.text).to eq last_year.year.to_s + + prev_link, next_link = page.all("header nav a").to_a + + expect(prev_link.text).to eq "prev" + expect(prev_link["href"]).to end_with((last_year.year - 1).to_s) + + expect(next_link.text).to eq "next" + expect(next_link["href"]).to end_with((last_year.year + 1).to_s) + + page.all("table").to_a.each do |table| + month_row, starting_row, ending_row, net_row = table.all("tr").to_a + + expect(month_row.all("th").map(&:text)).to eq %w[ + Month Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec + ] + + expect(starting_row.all("td").map(&:text)).to eq %w[ + Starting - - - - - - - - - - - - + ] + + expect(ending_row.all("td").map(&:text)).to eq %w[ + Ending - - - - - - - - - - - - + ] + + expect(net_row.all("td").map(&:text)).to eq %w[ + Net - - - - - - - - - - - - + ] + end + end + + scenario "with one statement in the middle of that year" do + FactoryBot.create( + :financial_statement, + financial_account: FinancialAccount.wf_checking, + period_start_on: Date.parse("#{last_year.year}-05-01"), + starting_amount_cents: 1_034_89, + ending_amount_cents: 99_00 + ) + + visit "/financial_reports/#{last_year}" + + checking_table = page.all("table").to_a.first + _month_tr, starting_tr, ending_tr, net_tr = checking_table.all("tr").to_a + + may_starting_td = starting_tr.all("td").to_a[5] + expect(may_starting_td.text).to eq "$1,034.89" + + may_ending_td = ending_tr.all("td").to_a[5] + expect(may_ending_td.text).to eq "$99.00" + + may_net_td = net_tr.all("td").to_a[5] + expect(may_net_td.text).to eq "($935.89)" + end + + scenario "with statements for every month of that year" do + FinancialReport.months_for_year(last_year.year).each do |date| + FactoryBot.create( + :financial_statement, + financial_account: FinancialAccount.wf_checking, + period_start_on: date, + starting_amount_cents: 1_034_89, + ending_amount_cents: 99_00 + ) + + FactoryBot.create( + :financial_statement, + financial_account: FinancialAccount.wf_savings, + period_start_on: date, + starting_amount_cents: 99_00, + ending_amount_cents: 1_034_89 + ) + end + + visit "/financial_reports/#{last_year}" + + checking_table, savings_table = page.all("table").to_a + + within checking_table do + _month_tr, starting_tr, ending_tr, net_tr = checking_table.all("tr").to_a + + _starting_label, *starting_tds = starting_tr.all("td").to_a + expect(starting_tds.count).to eq 12 + expect(starting_tds.map(&:text).uniq).to eq ["$1,034.89"] + + _ending_label, *ending_tds = ending_tr.all("td").to_a + expect(ending_tds.count).to eq 12 + expect(ending_tds.map(&:text).uniq).to eq ["$99.00"] + + _net_label, *net_tds = net_tr.all("td").to_a + expect(net_tds.count).to eq 12 + expect(net_tds.map(&:text).uniq).to eq ["($935.89)"] + end + + within savings_table do + _month_tr, starting_tr, ending_tr, net_tr = savings_table.all("tr").to_a + + _starting_label, *starting_tds = starting_tr.all("td").to_a + expect(starting_tds.count).to eq 12 + expect(starting_tds.map(&:text).uniq).to eq ["$99.00"] + + _ending_label, *ending_tds = ending_tr.all("td").to_a + expect(ending_tds.count).to eq 12 + expect(ending_tds.map(&:text).uniq).to eq ["$1,034.89"] + + _net_label, *net_tds = net_tr.all("td").to_a + expect(net_tds.count).to eq 12 + expect(net_tds.map(&:text).uniq).to eq ["$935.89"] + end + end +end