Class: Activity::Prioritizer
- Inherits:
-
BaseService
- Object
- BaseService
- Activity::Prioritizer
- Defined in:
- app/services/activity/prioritizer.rb
Overview
Service object: prioritizer.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- PRIORITIZATION_SELECT =
Prioritization select.
<<-EOS.freeze activities.*, COALESCE(cat.company_id, pc.company_id, 1) as company_id, activity_types.priority as activity_type_priority, pc.sales_priority_index, COALESCE(activities.original_target_datetime,activities.target_datetime,activities.created_at) as original_target_datetime, pc.full_name as customer_full_name, pc.id as customer_id, pc.primary_sales_rep_id, pc.secondary_sales_rep_id, pc.local_sales_rep_id, pc.service_rep_id, er.backup_rep_id EOS
Instance Attribute Summary collapse
-
#activity_deck ⇒ Object
readonly
Pass these options to the initializer :allocation_start, if not specified, will look at start_range and if not use today's date :start_range, how far back to look for activities to reschedule :end_range, how far ahead to look for activities to reschedule.
Attributes inherited from BaseService
Class Method Summary collapse
- .assignable_activity?(a) ⇒ Boolean
- .high_priority_activity_levels ⇒ Object
- .standard_priority_activity_levels ⇒ Object
- .workload_based_on_matrix(assignment_matrix, rep_id, target_date, priorities = []) ⇒ Object
Instance Method Summary collapse
- #activities_to_process? ⇒ Boolean
- #allocate_priority_activities(activities, allocation_start) ⇒ Object
- #allocate_standard_activities(activities, allocation_start) ⇒ Object
-
#append_to_decks(activities) ⇒ Object
Takes an list of activities, divide them in time sensitive and non time sensitive queues and load up our decks accordingly with a sort.
- #assign_activity(activities, assign_rep_id, target_date) ⇒ Object
-
#available_activities_on_day_for_rep(rep_id, target_date) ⇒ Object
Returns the number of activity slots available for a rep_id on a given day This is the maximum allocatable per day.
- #available_priority_activities_on_day_for_rep(rep_id, target_date) ⇒ Object
- #can_take_activities_on_day?(rep_id, target_date) ⇒ Boolean
- #can_take_priority_activities_on_day?(rep_id, target_date) ⇒ Boolean
- #commit_assignment_matrix ⇒ Object
- #employees_hash ⇒ Object
-
#extract_time_locked_activities ⇒ Object
this method extract all time locked activities in the future and pre-fills them in our assignment matrix.
-
#initialize_deck ⇒ Object
Initialize the deck of activities to sort.
-
#load_activities_for_processing(start_range = nil, end_range = nil) ⇒ Object
Loads up activities suitable for reorganization based on a date range.
- #log_error(msg) ⇒ Object
- #log_info(msg) ⇒ Object
- #log_warning(msg) ⇒ Object
-
#mark_processed(success, a) ⇒ Object
This marks an activity as having been processed by keeping track in an internal array.
- #prepare_activity(activity) ⇒ Object
-
#process(options = {}, &block) ⇒ Object
Start prioritization routine, will return a result object options are allocation_start: from what point you want to start allocating deck_limit: how many activities you want to process priorities: priorirty of activity to process rep_ids: focus only on those rep_ids (array) Yields back position, total to process, and info message to a block (optional).
-
#reps_available_for_activities_on_day(day) ⇒ Object
Returns an array of employee ids that have room for activities on a given day.
- #reset_time_locks ⇒ Object
-
#sort_deck ⇒ Object
Sort the decks.
- #to_s ⇒ Object
Methods inherited from BaseService
#initialize, #log_debug, #logger, #tagged_logger
Constructor Details
This class inherits a constructor from BaseService
Instance Attribute Details
#activity_deck ⇒ Object (readonly)
Pass these options to the initializer
:allocation_start, if not specified, will look at start_range and if not use today's date
:start_range, how far back to look for activities to reschedule
:end_range, how far ahead to look for activities to reschedule
9 10 11 |
# File 'app/services/activity/prioritizer.rb', line 9 def activity_deck @activity_deck end |
Class Method Details
.assignable_activity?(a) ⇒ Boolean
371 372 373 374 |
# File 'app/services/activity/prioritizer.rb', line 371 def self.assignable_activity?(a) (a.activity_type_priority <= ActivityType::PRIORITY_MAX and (a.assignment_resource_ids.present? or a.fallback_employee_id.present?)) or (a.activity_type_priority > ActivityType::PRIORITY_MAX and a.assignment_resource_ids.present?) end |
.high_priority_activity_levels ⇒ Object
376 377 378 |
# File 'app/services/activity/prioritizer.rb', line 376 def self.high_priority_activity_levels (1..ActivityType::PRIORITY_MAX).to_a end |
.standard_priority_activity_levels ⇒ Object
380 381 382 |
# File 'app/services/activity/prioritizer.rb', line 380 def self.standard_priority_activity_levels ((ActivityType::PRIORITY_MAX + 1)..ActivityType::TOTAL_TIERS).to_a end |
.workload_based_on_matrix(assignment_matrix, rep_id, target_date, priorities = []) ⇒ Object
206 207 208 209 210 211 212 213 214 |
# File 'app/services/activity/prioritizer.rb', line 206 def self.workload_based_on_matrix(assignment_matrix, rep_id, target_date, priorities = []) assignment_matrix[rep_id] ||= {} assignment_matrix[rep_id][target_date] ||= {} wl = 0 assignment_matrix[rep_id][target_date].each do |priority, activities| wl += activities.size if priorities.empty? || priorities.include?(priority) end wl end |
Instance Method Details
#activities_to_process? ⇒ Boolean
286 287 288 |
# File 'app/services/activity/prioritizer.rb', line 286 def activities_to_process? @activity_deck.present? end |
#allocate_priority_activities(activities, allocation_start) ⇒ Object
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 |
# File 'app/services/activity/prioritizer.rb', line 109 def allocate_priority_activities(activities, allocation_start) # allocation start can only start at the earliest with the original_target_datetime while activities.present? play_deck = activities.select { |a| a.original_target_datetime.to_date <= allocation_start } full_rep_list_ids = employees_hash.values.map(&:id) working_rep_ids = reps_available_for_activities_on_day(allocation_start) rep_ids_with_cap_reached = [] # Resets the rep with their cap reached, it's a new day! list_size = play_deck.size log_info "Allocating priority activities on #{allocation_start}, list is #{list_size}, working reps: #{working_rep_ids}, full list of reps: #{full_rep_list_ids}" rep_ids_in_activity_pool = play_deck.map(&:assignment_resource_ids).flatten.uniq play_deck.each_with_index do |a, i| log_info "[#{i}/#{list_size}] AID: #{a.id}, Running through priority activity, assignable to #{a.assignment_resource_ids}, working_rep_ids: #{working_rep_ids}, rep_ids_with_cap_reached: #{rep_ids_with_cap_reached} on #{allocation_start}" assignable_rep_ids = working_rep_ids - rep_ids_with_cap_reached rep_ids = (a.assignment_resource_ids & assignable_rep_ids).compact.uniq log_info "[#{i}/#{list_size}] AID: #{a.id}, final assignment possible: #{rep_ids}" rep_ids.each do |rep_id| if can_take_priority_activities_on_day?(rep_id, allocation_start) # Remove it from the global list and mark it for processing assign_activity(a, rep_id, allocation_start) # Remove it from our priority list activities.delete(a) break # no need to keep evaluating the other reps else log_warning "Cap reached for rep id #{rep_id}" rep_ids_with_cap_reached << rep_id rep_ids_in_activity_pool.delete(rep_id) end end if rep_ids_in_activity_pool.empty? log_warning 'No one left to allocate activities to, moving on to next day' break end end # Jump to the next day or the original target datetime of the earliest activity allocation_start = allocation_start.next_business_day rep_ids_with_cap_reached = [] end end |
#allocate_standard_activities(activities, allocation_start) ⇒ Object
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 |
# File 'app/services/activity/prioritizer.rb', line 148 def allocate_standard_activities(activities, allocation_start) # Standard run is next activities.size # Reject unassignable activities just in case raise 'Some activities are unassignable, abnormal at this state' if activities.find { |a| a.assignment_resource_ids.empty? } # Group by the first assignable rep id (the others do not count) grouped_activities = activities.group_by { |a| a.assignment_resource_ids.first } # Evaluate by rep grouped_activities.each do |rep_id, rep_activities| # Go through each day and fill in the rep's activities log_info "[Rep: #{rep_id}] looking for next working day from #{allocation_start}" rep_allocation_date = employees_hash[rep_id].next_working_day(allocation_start, 0) # Start on the first working day if rep_allocation_date.nil? log_info "[Rep: #{rep_id}] rep_allocation_date is not found, total activities for this rep: #{rep_activities.size}, skipping this rep" @unprocessed += rep_activities next end log_info "[Rep: #{rep_id}] [#{rep_allocation_date}] Total activities for this rep: #{rep_activities.size}" while rep_activities.present? allocatable_activities = rep_activities.select { |a| a.original_target_datetime <= rep_allocation_date.end_of_day } # how many can they take today? max_activities_today = available_activities_on_day_for_rep(rep_id, rep_allocation_date) log_info "[Rep: #{rep_id}] [#{rep_allocation_date}] Allocatable: #{allocatable_activities.size}, Available Slots: #{max_activities_today}" # pick the maximum possible for this day for this rep allocate_activities = allocatable_activities.shift(max_activities_today) # Allocate them assign_activity(allocate_activities, rep_id, rep_allocation_date) rep_activities -= allocate_activities log_info "[Rep: #{rep_id}] [#{rep_allocation_date}] Rep has #{rep_activities.size} remaining to allocate, moving on to next day" # Roll on to the next day with activities next_allocatable_activities_date = rep_activities.filter_map { |a| a.original_target_datetime.try(:to_date) }.min next_business_day = employees_hash[rep_id].next_working_day(rep_allocation_date) rep_allocation_date = [next_allocatable_activities_date, next_business_day].compact.max log_info "[Rep: #{rep_id}] About to process next working day: #{rep_allocation_date}" end end end |
#append_to_decks(activities) ⇒ Object
Takes an list of activities, divide them in time sensitive and non time sensitive queues and load up our decks accordingly with a sort
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 |
# File 'app/services/activity/prioritizer.rb', line 385 def append_to_decks(activities) log_info "Reviewing #{activities.size} activities for addition to deck" # Append activities to deck and also fix returned values from custom select to proper type cast. new_rep_ids = [] # Here we will keep track of possible rep, this way we can be smart and not load the whole employee db new_activities = activities.select { |a| a.activity_type_priority.to_i.positive? }.map do |a| a.activity_type_priority = a.activity_type_priority.to_i a.sales_priority_index = a.sales_priority_index.to_i a end good_counter = 0 bad_counter = 0 new_activities.each do |na| a = prepare_activity(na) if a && self.class.assignable_activity?(a) good_counter += 1 @activity_deck[a.id] = a new_rep_ids |= [a.fallback_employee_id, a.assignment_resource_ids].flatten.compact else bad_counter += 1 log_warning " * Activity #{na.id} marked as unprocessable, no one could be assigned to it, review queue and customer data." @unprocessed << a end end log_info " * Added #{good_counter} activities to activity deck, now we have #{@activity_deck.size} good activities." log_info " * Unprocessable: #{bad_counter}, total in unprocessable: #{@unprocessed.size}." log_info " * Rep Matrix: #{new_rep_ids.size}, we now have #{employees_hash.size} loaded" sort_deck # Perform magical sorting end |
#assign_activity(activities, assign_rep_id, target_date) ⇒ Object
194 195 196 197 198 199 200 201 202 203 204 |
# File 'app/services/activity/prioritizer.rb', line 194 def assign_activity(activities, assign_rep_id, target_date) activities = [activities].flatten log_info "[Rep: #{assign_rep_id}] [#{target_date}] Assigning activities #{activities.map(&:id)}" @assignment_matrix[assign_rep_id] ||= {} @assignment_matrix[assign_rep_id][target_date] ||= {} activities.group_by(&:activity_type_priority).each do |activity_type_priority, sub_activities| @assignment_matrix[assign_rep_id][target_date][activity_type_priority] ||= [] @assignment_matrix[assign_rep_id][target_date][activity_type_priority] += sub_activities.map(&:id) end activities.each { |a| mark_processed(true, a) } end |
#available_activities_on_day_for_rep(rep_id, target_date) ⇒ Object
Returns the number of activity slots available for a rep_id on a given day
This is the maximum allocatable per day
254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'app/services/activity/prioritizer.rb', line 254 def available_activities_on_day_for_rep(rep_id, target_date) target_date = target_date.to_date unless target_date.is_a?(Date) if (emp_obj = employees_hash[rep_id]) current = self.class.workload_based_on_matrix(@assignment_matrix, rep_id, target_date) max = employees_hash[rep_id].maximum_activities_per_day(target_date) available = [max - current, 0].max log_info "[Rep: #{rep_id}] [#{target_date}] Activity Load is #{current}/#{max}, #{available} available activity slots" available else log_error "[Rep: #{rep_id}] [#{target_date}] #{rep_id} is missing from employees_hash" unless emp_obj 0 end end |
#available_priority_activities_on_day_for_rep(rep_id, target_date) ⇒ Object
268 269 270 271 272 273 274 |
# File 'app/services/activity/prioritizer.rb', line 268 def available_priority_activities_on_day_for_rep(rep_id, target_date) current = self.class.workload_based_on_matrix(@assignment_matrix, rep_id, target_date, self.class.high_priority_activity_levels) max = employees_hash[rep_id].maximum_priority_tier_activities_per_day(target_date) available = [max - current, 0].max log_info "[Rep: #{rep_id}] [#{target_date}] Priority Activity Load is #{current}/#{max}, #{available} available priority activity slots" available end |
#can_take_activities_on_day?(rep_id, target_date) ⇒ Boolean
276 277 278 279 |
# File 'app/services/activity/prioritizer.rb', line 276 def can_take_activities_on_day?(rep_id, target_date) target_date = target_date.to_date unless target_date.is_a?(Date) available_activities_on_day_for_rep(rep_id, target_date) > 0 end |
#can_take_priority_activities_on_day?(rep_id, target_date) ⇒ Boolean
281 282 283 284 |
# File 'app/services/activity/prioritizer.rb', line 281 def can_take_priority_activities_on_day?(rep_id, target_date) target_date = target_date.to_date unless target_date.is_a?(Date) available_priority_activities_on_day_for_rep(rep_id, target_date) > 0 end |
#commit_assignment_matrix ⇒ Object
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 |
# File 'app/services/activity/prioritizer.rb', line 223 def commit_assignment_matrix Time.current.utc.to_fs(:db) @assignment_matrix.each do |rep_id, dates| Activity.transaction do # Grouping updates in one transaction speed things up dates.each do |date, priorities| target_datetime = WorkingHours.advance_to_closing_time(date) # target_datetime = target_datetime.utc.to_fs(:db) act_ids = [] priorities.each do |_priority, activities| act_ids += (activities || []).select { |i| i > 0 } # Negative ids are fake stubs at this point we ignore them end next if act_ids.blank? logger.info "Employee id: #{rep_id} -> Assigning #{act_ids.size} activities on #{target_datetime}, ids: #{act_ids.join(',')}" Activity.where(id: act_ids, activity_result_type_id: nil).find_each do |activity| activity.original_target_datetime ||= activity.target_datetime activity.original_assigned_resource_id ||= activity.assigned_resource_id activity.target_datetime = target_datetime activity.assigned_resource_id = rep_id activity.skip_callbacks = true # Don't run any validation or callbacks except for audit trail activity.skip_check_for_open_sales_activity = true activity.save # We want audit trails end end end end end |
#employees_hash ⇒ Object
349 350 351 352 353 354 355 356 |
# File 'app/services/activity/prioritizer.rb', line 349 def employees_hash @employees_hash ||= Employee .includes(:employee_events, :company, :employee_record) .active .select(:id, :full_name, :company_id) .to_a .index_by(&:id) end |
#extract_time_locked_activities ⇒ Object
this method extract all time locked activities in the future and pre-fills them in our assignment matrix
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 346 347 |
# File 'app/services/activity/prioritizer.rb', line 302 def extract_time_locked_activities log_info 'Extracting Time Locked Activities that are not overdue and where rep is working' time_locked_counter = 0 @activity_deck.select { |_aid, a| a.lock_target_datetime }.each do |aid, a| log_info "[AID:#{aid}] is time locked" if a.assigned_resource_id && (employee = employees_hash[a.assigned_resource_id]) if a.overdue? || (overloaded = !can_take_activities_on_day?(a.assigned_resource_id, a.target_datetime.to_date)) log_info "[AID:#{aid}] time locked and overdue on #{a.target_datetime.to_date}, looking for a new date" if a.overdue? log_info "[AID:#{aid}] time locked and rep #{employee.full_name} [#{employee.id}] is overloaded on #{a.target_datetime.to_date}, looking for a new date" if overloaded new_date = [Date.current, a.target_datetime.to_date].max until can_take_activities_on_day?(a.assigned_resource_id, new_date) new_date += 1.day new_date = employee.next_working_day(new_date, 0) end log_info "[AID:#{aid}] found new date for rep #{employee.full_name} on #{new_date}" new_target_datetime = WorkingHours.advance_to_closing_time(new_date) a.target_datetime = new_target_datetime log_info "[AID:#{aid}] is overdue and will be moved to next working day for employee from today #{new_target_datetime}" # a.new_note = "Time lock was overdue and moved to next working day for assigned employee" a.skip_callbacks = true a.save end # employees_hash[a.assigned_resource_id].working_on_day?(a.target_datetime) # The activity can be time locked time_locked_counter += 1 # Store in assignment matrix a stub to fool our counter @assignment_matrix[a.assigned_resource_id] ||= {} @assignment_matrix[a.assigned_resource_id][a.target_datetime.to_date] ||= {} @assignment_matrix[a.assigned_resource_id][a.target_datetime.to_date][a.activity_type_priority] ||= [] @assignment_matrix[a.assigned_resource_id][a.target_datetime.to_date][a.activity_type_priority] << -a.id # Negative activity id why not. mark_processed(true, a) else # The activity is marked time locked but is now invalid log_warning "[AID:#{aid}] was originally time locked but can no longer remain in that state" @reset_time_lock_aids << aid end end @assignment_matrix.each do |resource_id, target_datetime_hsh| target_datetime_hsh.each do |target_date, priority_hsh| priority_hsh.each do |priority, activity_ids| log_info "[Rep #{resource_id}] [#{target_date}] [Priority: #{priority}] #{activity_ids.size} locked activities: #{activity_ids.map(&:abs).join(', ')}" end end end log_info "Extracted #{time_locked_counter} Time Locked Activities that are assignable" end |
#initialize_deck ⇒ Object
Initialize the deck of activities to sort.
359 360 361 362 363 364 365 366 367 368 369 |
# File 'app/services/activity/prioritizer.rb', line 359 def initialize_deck ardeck = load_activities_for_processing # Append to decks and sort + clear date time as needed append_to_decks ardeck # Take all time locked activities and do something else extract_time_locked_activities # Non processable time locks get reset reset_time_locks # return true if values are present activities_to_process? end |
#load_activities_for_processing(start_range = nil, end_range = nil) ⇒ Object
Loads up activities suitable for reorganization based on a date range
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 |
# File 'app/services/activity/prioritizer.rb', line 467 def load_activities_for_processing(start_range = nil, end_range = nil) ardeck = Activity.non_notes.open_activities.joins(:activity_type) .where(target_datetime: ...1.year.from_now) # rule of thumb to help with strays .joins('INNER JOIN parties p ON p.id = activities.party_id') .joins('INNER JOIN parties pc ON pc.id = COALESCE(p.customer_id, p.id)') .joins('INNER JOIN catalogs cat ON cat.id = pc.catalog_id') .joins('LEFT JOIN parties e ON pc.primary_sales_rep_id = e.id') .joins('LEFT JOIN employee_records er ON e.id = er.party_id') .except(:select) .select(PRIORITIZATION_SELECT) ardeck = ardeck.where(activity_types: { priority: @priorities }) if @priorities.present? ardeck = ardeck.where(Activity[:target_datetime].gteq(start_range.beginning_of_day)) if start_range.present? ardeck = ardeck.where(Activity[:target_datetime].lteq(end_range.end_of_day)) if end_range.present? ardeck = ardeck.limit(@deck_limit) if @deck_limit.present? if @rep_ids.present? rep_ids = Array(@rep_ids) ardeck = ardeck.where.any_of({ assigned_resource_id: rep_ids }, { original_assigned_resource_id: rep_ids }) end @activity_deck_size = ardeck.size ardeck end |
#log_error(msg) ⇒ Object
494 495 496 497 |
# File 'app/services/activity/prioritizer.rb', line 494 def log_error(msg) super @block&.call(@total_processed, @activity_deck_size, msg) end |
#log_info(msg) ⇒ Object
489 490 491 492 |
# File 'app/services/activity/prioritizer.rb', line 489 def log_info(msg) super @block&.call(@total_processed, @activity_deck_size, msg) end |
#log_warning(msg) ⇒ Object
499 500 501 502 |
# File 'app/services/activity/prioritizer.rb', line 499 def log_warning(msg) super @block&.call(@total_processed, @activity_deck_size, msg) end |
#mark_processed(success, a) ⇒ Object
This marks an activity as having been processed by keeping track in an internal array
291 292 293 294 295 296 297 298 299 |
# File 'app/services/activity/prioritizer.rb', line 291 def mark_processed(success, a) if success @processed << a else @unprocessed << a end @total_processed += 1 @activity_deck.delete(a.id) end |
#prepare_activity(activity) ⇒ Object
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 |
# File 'app/services/activity/prioritizer.rb', line 425 def prepare_activity(activity) # Pre-fetch the assignment queue, saves us time. at = @activity_types[activity.activity_type_id] company_id = activity.company_id.to_i resource_ids = [] # find which assignement queue to use if (ataq = at.activity_type_assignment_queues.find { |ataq| ataq.company_id == company_id }) # Setup a fake open struct customer that quacks like a customer customer = OpenStruct.new customer.id = activity.customer_id.to_i customer.primary_sales_rep_id = activity.primary_sales_rep_id.to_i if activity.primary_sales_rep_id.present? customer.secondary_sales_rep_id = activity.secondary_sales_rep_id.to_i if activity.secondary_sales_rep_id.present? customer.local_sales_rep_id = activity.local_sales_rep_id.to_i if activity.local_sales_rep_id.present? customer.service_rep_id = activity.service_rep_id.to_i if activity.service_rep_id.present? customer.backup_rep_id = activity.backup_rep_id.to_i if activity.backup_rep_id.present? current_user_id = activity.assigned_resource_id || activity.creator_id # Current user id must excludes non reps, activity creator could be customers current_user_id = nil unless current_user_id.in?(employees_hash.keys) resource_ids += ataq.get_full_queue_resources(customer, current_user_id) activity.fallback_employee_id = ataq.fallback_employee_id # High priority activity have the fallback assignable resource_ids << ataq.fallback_employee_id if ataq.fallback_employee_id && activity.activity_type_priority <= ActivityType::PRIORITY_MAX end if resource_ids.empty? resource_ids << activity.assigned_resource_id resource_ids << activity.creator_id resource_ids << activity.primary_sales_rep_id end # Sometime a customer ends up in the resource ids, e.g creator_id is most commong # by doing this intersect we ensure we only take potential reps resource_ids = resource_ids.compact.uniq & employees_hash.keys activity.assignment_resource_ids = resource_ids return activity if activity.assignment_resource_ids.present? # log_info "Activity: #{activity.id}, Activity Type #{activity.activity_type_id} possible assignments are #{activity.assignment_resource_ids.join(',')}" log_warning "Activity: #{activity.id}, Activity Type #{activity.activity_type_id} does not have a valid assignment possible" nil end |
#process(options = {}, &block) ⇒ Object
Start prioritization routine, will return a result object
options are
allocation_start: from what point you want to start allocating
deck_limit: how many activities you want to process
priorities: priorirty of activity to process
rep_ids: focus only on those rep_ids (array)
Yields back position, total to process, and info message to a block (optional)
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 |
# File 'app/services/activity/prioritizer.rb', line 43 def process( = {}, &block) @block = block @allocation_start = [:allocation_start] || Date.current @allocation_end = @allocation_start raise 'Allocation cannot start in the past' if @allocation_start < Date.current @deck_limit = [:deck_limit] @priorities = [:priorities] @rep_ids = [:rep_ids] @current_user_id = [:current_user_id] # Initialize these arrays which will store our workload @activity_deck = {} @unprocessed = [] @processed = [] @loaded_days = [] @total_processed = 0 @reset_time_lock_aids = [] # Now pre-load the activity types and associated assignment queues in a nice fast hash @activity_types = ActivityType.eager_load(:assignment_queues).index_by(&:id) @assignment_matrix = {} whodunnit = ['Activity', 'Prioritizer', @current_user_id].compact.join('::') res = nil PaperTrail.request(whodunnit: whodunnit) do # Preload employees and their events initialize_deck log_info '* Activity Prioritizer ready to roll, call #prioritize' log_info " #{@activity_deck.size} activities loaded" log_info " #{@unprocessed.size} won't be processed" log_info " #{employees_hash.size} employees loaded" start_time = Time.current @allocation_end = @allocation_start.dup @total_activities = @activity_deck.size @total_processed = 0 log_info "Prioritization started at #{start_time.to_fs(:crm_default)} for #{@total_activities} activities" # Start on first allocation day @activity_deck.size # Splitting log_info 'Splitting into priority and standard list' priority_list, standard_list = @activity_deck.values.partition { |a| a.activity_type_priority <= ActivityType::PRIORITY_MAX } allocate_priority_activities priority_list, @allocation_end allocate_standard_activities standard_list, @allocation_end # Commit matrix Here commit_assignment_matrix # Wrap up end_time = Time.current duration = (end_time - start_time).round(2) throughput = (@total_activities / duration).round(2) = [] << "Prioritization ended at #{end_time.to_fs(:crm_default)}, duration: #{Heatwave::Duration.humanize(duration)}, #{throughput} activities / sec" << "!!! some activities remains and could find no allocation, total count #{@activity_deck.size}" if @activity_deck.present? res = Result.new(total_activities: @total_activities, duration: duration, throughput: throughput, unprocessable: @activity_deck.size, messages: ) res..each { |m| log_info m } end res end |
#reps_available_for_activities_on_day(day) ⇒ Object
Returns an array of employee ids that have room for activities on a given day
190 191 192 |
# File 'app/services/activity/prioritizer.rb', line 190 def reps_available_for_activities_on_day(day) employees_hash.values.map(&:id).select { |eid| can_take_activities_on_day?(eid, day) } end |
#reset_time_locks ⇒ Object
216 217 218 219 220 221 |
# File 'app/services/activity/prioritizer.rb', line 216 def reset_time_locks return if @reset_time_lock_aids.blank? log_info "Resetting time locks for #{@reset_time_lock_aids.size}" Activity.where(id: @reset_time_lock_aids).find_each(&:unlock) end |
#sort_deck ⇒ Object
Sort the decks
416 417 418 419 420 421 422 423 |
# File 'app/services/activity/prioritizer.rb', line 416 def sort_deck log_info 'Sorting Deck' # The time sensitive deck will now be sorted according to the activity priority, the original activity date and the sales priority index last @activity_deck = @activity_deck.sort_by do |_aid, a| [a.activity_type_priority, a.original_target_datetime, -a.sales_priority_index] end.to_h log_info 'Sorting Deck Complete' end |
#to_s ⇒ Object
32 33 34 |
# File 'app/services/activity/prioritizer.rb', line 32 def to_s "Activity:Prioritizer: #{@allocation_end} << #{@activity_deck.size}" end |