-
-
Notifications
You must be signed in to change notification settings - Fork 572
Expand file tree
/
Copy pathdistributions_controller.rb
More file actions
345 lines (297 loc) · 14.1 KB
/
distributions_controller.rb
File metadata and controls
345 lines (297 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# Provides full CRUD+ for Distributions, which are the primary way for inventory to leave a Diaperbank. Most
# Distributions are given out through community partners (either via Partnerbase, or to Partners-on-record). It's
# technically possible to also do Direct Services by having a Partner called "Direct Services" and then issuing
# Distributions to them, though it would lack some of the additional featuers and failsafes that a Diaperbank
# might want if they were doing direct services.
class DistributionsController < ApplicationController
include DateRangeHelper
include DistributionHelper
include Validatable
include ActionController::Live
before_action :enable_turbo!, only: %i[new show]
skip_before_action :authenticate_user!, only: %i(calendar)
skip_before_action :authorize_user, only: %i(calendar)
skip_before_action :require_organization, only: %i(calendar)
def print
@distribution = Distribution.find(params[:id])
respond_to do |format|
format.any do
pdf = DistributionPdf.new(current_organization, @distribution)
send_data pdf.compute_and_render,
filename: format("%s %s.pdf", @distribution.partner.name, sortable_date(@distribution.created_at)),
type: "application/pdf",
disposition: "inline"
end
end
end
def destroy
service = DistributionDestroyService.new(params[:id])
result = service.call
if result.success?
flash[:notice] = "Distribution #{params[:id]} has been reclaimed!"
else
flash[:error] = result.error.message
end
redirect_to distributions_path
end
def index
setup_date_range_picker
@highlight_id = session.delete(:created_distribution_id)
@distributions = current_organization
.distributions
.order(issued_at: :desc)
.includes(:partner, :storage_location)
.class_filter(scope_filters)
@paginated_distributions = @distributions.page(params[:page])
@items = current_organization.items.alphabetized.select(:id, :name)
@item_categories = current_organization.item_categories.select(:id, :name)
@storage_locations = current_organization.storage_locations.active.alphabetized.select(:id, :name)
@partners = current_organization.partners.active.alphabetized.select(:id, :name)
@selected_item = filter_params[:by_item_id].presence
@distribution_totals = DistributionTotalsService.call(current_organization.distributions.class_filter(scope_filters))
@total_value_all_distributions = @distribution_totals.values.sum(&:value)
@total_items_all_distributions = @distribution_totals.values.sum(&:quantity)
paginated_ids = @paginated_distributions.ids
@total_value_paginated_distributions = @distribution_totals.slice(*paginated_ids).values.sum(&:value)
@total_items_paginated_distributions = @distribution_totals.slice(*paginated_ids).values.sum(&:quantity)
@selected_item_category = filter_params[:by_item_category_id].presence
@reporting_categories = Item.reporting_categories_for_select
@selected_reporting_category = filter_params[:by_reporting_category].presence
@selected_partner = filter_params[:by_partner]
@selected_status = filter_params[:by_state]
@selected_location = filter_params[:by_location]
# FIXME: one of these needs to be removed but it's unclear which at this point
@statuses = Distribution.states.transform_keys(&:humanize)
@distributions_with_inactive_items = @distributions.joins(:inactive_items).pluck(:id)
respond_to do |format|
format.html
format.csv do
send_stream filename: "Distributions-#{Time.zone.today}.csv" do |stream|
Exports::ExportDistributionsCSVService.new(distributions: @distributions.includes(line_items: :item), organization: current_organization, filters: scope_filters).generate_csv_stream do |row|
stream.write(row)
end
end
end
end
end
# This endpoint is in support of displaying a confirmation modal before a distribution is created.
# Since the modal should only be shown for a valid distribution, client side JS will invoke this
# endpoint, and if the distribution is valid, this endpoint also returns the HTML for the modal content.
# Important: The distribution model is intentionally NOT saved to the database at this point because
# the user has not yet confirmed that they want to create it.
def validate
@dist = Distribution.new(distribution_params.merge(organization: current_organization))
@dist.line_items.combine!
if @dist.valid?
body = render_to_string(template: 'distributions/validate', formats: [:html], layout: false)
render json: {valid: true, body: body}
else
render json: {valid: false}
end
end
def create
dist = Distribution.new(distribution_params.merge(organization: current_organization))
result = DistributionCreateService.new(dist, request_id).call
if result.success?
session[:created_distribution_id] = result.distribution.id
@distribution = result.distribution
perform_inventory_check
schedule_reminder_email(result.distribution) if @distribution.reminder_email_enabled
respond_to do |format|
format.turbo_stream do
redirect_to distribution_path(result.distribution), notice: "Distribution created!"
end
end
else
@distribution = result.distribution
if request_id
@request = Request.find(request_id)
@distribution.request = @request
@distribution.partner = @request.partner
end
if @distribution.line_items.size.zero?
@distribution.line_items.build
elsif request_id
@distribution.initialize_request_items
end
@items = current_organization.items.active.alphabetized
@partner_list = current_organization.partners.where.not(status: 'deactivated').alphabetized
inventory = View::Inventory.new(@distribution.organization_id)
@storage_locations = current_organization.storage_locations.active.alphabetized.select do |storage_loc|
inventory.quantity_for(storage_location: storage_loc.id).positive?
end
if @distribution.storage_location.present?
@item_labels_with_quantities = inventory
.items_for_location(@distribution.storage_location.id, include_omitted: true)
.map(&:to_dropdown_option)
end
flash_error = insufficient_error_message(result.error.message)
respond_to do |format|
format.turbo_stream do
flash.now[:error] = flash_error
render turbo_stream: [
turbo_stream.replace(@distribution, partial: "form", locals: {distribution: @distribution, date_place_holder: @distribution.issued_at}),
turbo_stream.replace("flash", partial: "shared/flash")
], status: :bad_request
end
end
end
end
def new
@distribution = Distribution.new
if params[:request_id]
@request = Request.find(request_id)
@distribution.copy_from_request(@request)
else
@distribution.line_items.build
@distribution.copy_from_donation(params[:donation_id], params[:storage_location_id])
end
@items = current_organization.items.active.alphabetized
@partner_list = current_organization.partners.where.not(status: 'deactivated').alphabetized
inventory = View::Inventory.new(current_organization.id)
@storage_locations = current_organization.storage_locations.active.alphabetized.select do |storage_loc|
inventory.quantity_for(storage_location: storage_loc.id).positive?
end
end
def show
@distribution = Distribution.includes(:storage_location, line_items: :item).find(params[:id])
@line_items = @distribution.line_items
@total_quantity = @distribution.total_quantity
@total_package_count = @line_items.sum { |item| item.has_packages || 0 }
if @total_package_count.zero?
@total_package_count = nil
end
end
def edit
@distribution = Distribution.includes(:line_items).includes(:storage_location).find(params[:id])
@distribution.initialize_request_items
if (!@distribution.complete? && @distribution.future?) ||
current_user.has_cached_role?(Role::ORG_ADMIN, current_organization)
@distribution.line_items.build if @distribution.line_items.size.zero?
@request = @distribution.request
@items = current_organization.items.active.alphabetized
@partner_list = current_organization.partners.alphabetized
@changes_disallowed = SnapshotEvent.intervening(@distribution).present?
@audit_warning = current_organization.audits
.where(storage_location_id: @distribution.storage_location_id)
.where("updated_at > ?", @distribution.created_at).any? && !@changes_disallowed
inventory = View::Inventory.new(@distribution.organization_id)
@storage_locations = current_organization.storage_locations.active.alphabetized.select do |storage_loc|
!inventory.quantity_for(storage_location: storage_loc.id).negative?
end
else
redirect_to distributions_path, error: 'To edit a distribution,
you must be an organization admin or the current date must be later than today.'
end
end
def update
@distribution = Distribution.includes(:line_items).includes(:storage_location).find(params[:id])
result = DistributionUpdateService.new(@distribution, distribution_params).call
if result.success?
if result.resend_notification? && @distribution.partner&.send_reminders
send_notification(current_organization.id, @distribution.id, subject: "Your Distribution Has Changed", distribution_changes: result.distribution_content.changes)
end
schedule_reminder_email(@distribution) if @distribution.reminder_email_enabled
perform_inventory_check
redirect_to @distribution, notice: "Distribution updated!"
else
flash.now[:error] = insufficient_error_message(result.error.message)
@distribution.line_items.build if @distribution.line_items.size.zero?
@distribution.initialize_request_items
@request = @distribution.request
@items = current_organization.items.active.alphabetized
@partner_list = current_organization.partners.alphabetized
@storage_locations = current_organization.storage_locations.active.alphabetized
render :edit
end
end
def itemized_breakdown
setup_date_range_picker
distributions = current_organization.distributions.during(helpers.selected_range)
itemized_distribution_data_csv = DistributionItemizedBreakdownService
.new(organization: current_organization, distribution_ids: distributions.pluck(:id))
.fetch_csv
respond_to do |format|
format.csv do
send_data itemized_distribution_data_csv, filename: "Itemized-Breakdown-Distributions-#{Time.zone.today}.csv"
end
end
end
# TODO: This needs a little more context. Is it JSON only? HTML?
def schedule
respond_to do |format|
format.html
format.json do
start_at = params[:start].to_datetime
end_at = params[:end].to_datetime
@pick_ups = current_organization.distributions.includes(:partner).where(issued_at: start_at..end_at)
end
end
end
def calendar
crypt = ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31])
organization_id = crypt.decrypt_and_verify(CGI.unescape(params[:hash]))
render body: CalendarService.calendar(organization_id), content_type: Mime::Type.lookup("text/calendar")
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
head :unauthorized
end
def picked_up
distribution = current_organization.distributions.find(params[:id])
if !distribution.complete? && distribution.complete!
flash[:notice] = 'This distribution has been marked as being completed!'
else
flash[:error] = 'Sorry, we encountered an error when trying to mark this distribution as being completed'
end
redirect_back(fallback_location: distribution_path)
end
def pickup_day
@pick_ups = current_organization.distributions.during(pickup_date).order(issued_at: :asc)
@daily_items = daily_items(@pick_ups)
@selected_date = pickup_day_params[:during]&.to_date || Time.zone.now.to_date
end
private
def insufficient_error_message(details)
"Sorry, we weren't able to save the distribution. \n #{details}"
end
def send_notification(org, dist, subject: 'Your Distribution', distribution_changes: {})
PartnerMailerJob.perform_now(org, dist, subject, distribution_changes)
end
def schedule_reminder_email(distribution)
return if distribution.past? || !distribution.partner.send_reminders
DistributionMailer.reminder_email(distribution.id).deliver_later(wait_until: distribution.issued_at - 1.day)
end
def distribution_params
params.require(:distribution).permit(:comment, :agency_rep, :issued_at, :partner_id, :storage_location_id, :reminder_email_enabled, :delivery_method, :shipping_cost, line_items_attributes: %i(item_id quantity _destroy))
end
def request_id
params[:request_id] || params.dig(:distribution, :request_attributes, :id)
end
def daily_items(pick_ups)
item_groups = LineItem.where(itemizable_type: "Distribution", itemizable_id: pick_ups.pluck(:id)).group_by(&:item_id)
item_groups.map do |_id, items|
{
name: items.first.item.name,
quantity: items.sum(&:quantity),
package_count: items.sum { |item| item.package_count.to_i }
}
end
end
def scope_filters
filter_params
.except(:date_range)
.merge(during: helpers.selected_range)
end
helper_method \
def filter_params
return {} unless params.key?(:filters)
params
.require(:filters)
.permit(:by_item_id, :by_item_category_id, :by_reporting_category, :by_partner, :by_state, :by_location, :date_range)
end
def perform_inventory_check
inventory_check_result = InventoryCheckService.new(@distribution).call
alerts = [inventory_check_result.minimum_alert, inventory_check_result.recommended_alert]
merged_alert = alerts.compact.join("\n")
flash[:alert] = merged_alert if merged_alert.present?
end
end