-
-
Notifications
You must be signed in to change notification settings - Fork 499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Issue 2899] Distribution by counties! #3328
Changes from 15 commits
40acd69
444ccfc
3a99547
94a1860
d195feb
c22c5d9
825acd5
ac03d8c
a6322c7
9b4d82e
676f088
d0f635b
0457a8f
d88f440
ebc3707
f83c4a5
cfeef34
925fc9c
8fca5cf
ae485a9
43e7e49
03df861
640b583
cdcad77
7cb003f
871cd56
6acbf89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,3 +63,11 @@ | |
.modal-body-warning-text { | ||
color: #ee5034 | ||
} | ||
|
||
.numeric { | ||
text-align: right; | ||
} | ||
|
||
.percent { | ||
text-align: right; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
class DistributionsByCountyController < ApplicationController | ||
include DateRangeHelper | ||
include DistributionHelper | ||
|
||
def report | ||
setup_date_range_picker | ||
distributions = current_organization.distributions.includes(:partner).during(helpers.selected_range) | ||
@breakdown = DistributionByCountyReportService.new.get_breakdown(distributions) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
module DistributionsByCountyHelper | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Controller } from "@hotwired/stimulus" | ||
export default class extends Controller { | ||
static targets= ["share", "total", "warning"] | ||
connect () { | ||
this.calculateClientShareTotal() | ||
} | ||
calculateClientShareTotal(){ | ||
let total = 0; | ||
let share_targets = this.shareTargets | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line isn't really necessary since the next line can just be |
||
share_targets.forEach( share_target =>{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The whitespace formatting/indent of this whole file seems a bit off - please switch to 2-spaces and on line-10 here add in a newline I think. [this is not a required change, but if you're in here anyway....] |
||
if(share_target.value){ | ||
total += parseInt(share_target.value); | ||
} | ||
} | ||
) | ||
|
||
this.totalTarget.innerHTML = total + " %" | ||
|
||
if(total == 0 || total == 100){ | ||
awwaiid marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.warningTarget.style.visibility= 'hidden'; | ||
}else { | ||
this.warningTarget.style.visibility= 'visible'; | ||
this.warningTarget.style.color= 'red'; | ||
} | ||
return total; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general there are quite a lot of spacing fixes I'd put here (we don't have a JS linter):
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still missing some spaces before |
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Controller } from "@hotwired/stimulus" | ||
export default class extends Controller { | ||
static targets= ["share"] | ||
|
||
connect(){} | ||
zeroShareValue(){ | ||
let share_target = this.shareTarget | ||
share_target.value = 0 | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# == Schema Information | ||
# | ||
# Table name: counties | ||
# | ||
# id :bigint not null, primary key | ||
# name :string | ||
# region :string | ||
# created_at :datetime not null | ||
# updated_at :datetime not null | ||
# | ||
class County < ApplicationRecord | ||
has_many :served_areas, class_name: "Partners::ServedArea", dependent: :destroy | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -87,10 +87,17 @@ class Profile < Base | |
|
||
has_one_attached :proof_of_partner_status | ||
has_one_attached :proof_of_form_990 | ||
|
||
has_many :served_areas, foreign_key: "partner_profile_id", class_name: "Partners::ServedArea", dependent: :destroy, inverse_of: :partner_profile | ||
|
||
accepts_nested_attributes_for :served_areas, allow_destroy: true | ||
|
||
has_many_attached :documents | ||
|
||
validates :no_social_media_presence, acceptance: {message: "must be checked if you have not provided any of Website, Twitter, Facebook, or Instagram."}, if: :has_no_social_media? | ||
|
||
validate :client_share_is_0_or_100 | ||
|
||
self.ignored_columns = %w[ | ||
evidence_based_description | ||
program_client_improvement | ||
|
@@ -108,5 +115,26 @@ class Profile < Base | |
def has_no_social_media? | ||
website.blank? && twitter.blank? && facebook.blank? && instagram.blank? | ||
end | ||
|
||
def client_share_total | ||
tot = 0 | ||
served_areas.each do |served_area| | ||
tot += served_area.client_share | ||
end | ||
tot | ||
cielf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
|
||
def client_share_is_0_or_100 | ||
# business logic: the client share has to be 0 or 100 -- although it is an estimate only, making it 0 (not | ||
# specified at all) or 100 means we won't have people overallocating (> 100) and that they think about what | ||
# their allocation actually is | ||
value = client_share_total | ||
check = (value == 0 || value == 100) | ||
if !check | ||
|
||
errors.add(:base, "Total client share must be 0 or 100") | ||
awwaiid marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
check | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be tidied up: def client_share_is_0_or_100
value = client_share_total
if value != 0 && value != 100
errors.add(:base, "Total client share must be 0 or 100")
end
end |
||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# == Schema Information | ||
# | ||
# Table name: partner_served_areas | ||
# | ||
# id :bigint not null, primary key | ||
# client_share :integer | ||
# created_at :datetime not null | ||
# updated_at :datetime not null | ||
# county_id :bigint not null | ||
# partner_profile_id :bigint not null | ||
# | ||
module Partners | ||
class ServedArea < ApplicationRecord | ||
awwaiid marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.table_name = "partner_served_areas" | ||
belongs_to :partner_profile, class_name: "Partners::Profile" | ||
belongs_to :county | ||
validates :client_share, numericality: {only_integer: true} | ||
validates :client_share, inclusion: {in: 1..100} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great -- so this validates on the server side, only the fast-feedback JS needs to have the new validation. |
||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# frozen_string_literal: true | ||
|
||
class DistributionByCountyReportService | ||
def get_breakdown(distributions) | ||
breakdown_struct = Struct.new(:name, :region, :num_items, :amount) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd have this as a constant above the method rather than defined in the method:
|
||
breakdowns = {} | ||
breakdowns["Unspecified"] = breakdown_struct.new("Unspecified", "ZZZ", 0, 0.00) | ||
distributions.each do |distribution| | ||
served_areas = distribution.partner.profile.served_areas | ||
num_items_for_distribution = distribution.line_items.total | ||
value_of_distribution = distribution.line_items.total_value | ||
if served_areas.size == 0 | ||
breakdowns["Unspecified"].num_items += num_items_for_distribution | ||
breakdowns["Unspecified"].amount += value_of_distribution | ||
else | ||
served_areas.each do |served_area| | ||
name = served_area.county.name | ||
percentage = served_area.client_share / 100.0 | ||
if !breakdowns[name] | ||
breakdowns[name] = breakdown_struct.new(name, served_area.county.region, | ||
(num_items_for_distribution * percentage).round(0), value_of_distribution * percentage) | ||
else | ||
breakdowns[name].num_items = breakdowns[name].num_items + (num_items_for_distribution * percentage).round(0) | ||
breakdowns[name].amount = breakdowns[name].amount + value_of_distribution * percentage | ||
end | ||
end | ||
end | ||
end | ||
|
||
breakdown_array = breakdowns.sort_by { |k, v| [v[:region], k] } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
@breakdown = breakdown_array.map { |a| a[1] } | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
class PartnerProfileUpdateService | ||
include ServiceObjectErrorsMixin | ||
attr_reader :error | ||
def initialize(old_partner, new_partner_params, new_profile_params) | ||
@partner = old_partner | ||
@profile = @partner.profile | ||
@partner_params = new_partner_params | ||
@profile_params = new_profile_params | ||
end | ||
|
||
def call | ||
@return_value = false | ||
perform_profile_service do | ||
@partner.update(@partner_params) | ||
@return_value = @partner.valid? | ||
|
||
if @return_value | ||
@profile.served_areas.each(&:destroy!) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can use |
||
@profile.reload | ||
@profile.update!(@profile_params) | ||
@profile.reload | ||
end | ||
end | ||
end | ||
|
||
def perform_profile_service(&block) | ||
begin | ||
@profile.transaction do | ||
yield block | ||
end | ||
rescue ActiveRecord::RecordNotFound => e | ||
Rails.logger.error "[!] #{self.class.name} failed to update profile #{@profile.id} because it does not exist" | ||
set_error(e) | ||
rescue => e | ||
Rails.logger.error "[!] #{self.class.name} failed to update profile for #{@profile.id}: #{@profile.errors.full_messages} [#{e.inspect}]" | ||
set_error(e) | ||
end | ||
self | ||
end | ||
|
||
def success? | ||
@error.nil? | ||
end | ||
|
||
def set_error(error) | ||
@error = error.to_s | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should delete this file; it was probably added automatically.