Module: Models::EmployeeWorkStatus

Extended by:
ActiveSupport::Concern
Includes:
Memery, EmployeeActivityCapacity
Included in:
Employee
Defined in:
app/concerns/models/employee_work_status.rb

Overview

Day status, schedule preloading, and next-working-day logic for employees.

This concern is mixed into the Employee model and depends on
EmployeeActivityCapacity for capacity-related behaviour.

Instance Method Summary collapse

Methods included from EmployeeActivityCapacity

#activity_load_on_day, #can_take_activities_on_day?, #maximum_activities_per_day, #maximum_priority_tier_activities_per_day, #maximum_standard_tier_activities_per_day, #priority_tier_activities_ratio

Instance Method Details

#active_work_schedule(date) ⇒ ActiveRecord::Relation<WorkSchedule>

Effective work schedule for the employee on the given date.

Parameters:

  • date (Date, Time, String)

    date to evaluate

Returns:

  • (ActiveRecord::Relation<WorkSchedule>)

    schedules effective on the date



138
139
140
# File 'app/concerns/models/employee_work_status.rb', line 138

def active_work_schedule(date)
  work_schedules.effective_on(date)
end

#capacity_ratio(date) ⇒ Float

Ratio of capacity available on the given date, from 0.0 to 1.0.

Parameters:

  • date (Date, Time, String)

    date to evaluate

Returns:

  • (Float)

    capacity ratio where 1.0 is fully available and 0.0 is unavailable



123
124
125
126
127
128
129
130
131
# File 'app/concerns/models/employee_work_status.rb', line 123

def capacity_ratio(date)
  date = date.to_date unless date.is_a?(Date)
  day_status = day_status_employee_event(date)

  return 1.0 if day_status.nil? || day_status == :working
  return compute_partial_capacity(date) if day_status == :partial_timeoff

  0.0
end

#current_company_holidays_arrayArray<Date>

Company holidays on or after today for the employee's company.

Returns:

  • (Array<Date>)

    upcoming company holiday dates



21
22
23
# File 'app/concerns/models/employee_work_status.rb', line 21

def current_company_holidays_array
  @current_company_holidays_array ||= company.company_holidays.where(CompanyHoliday[:holiday_date].gteq(Date.current)).pluck(:holiday_date)
end

#day_employee_events(date) ⇒ Array<TimeOffRequestDate>?

Approved time-off events for the employee on the given date.

Parameters:

  • date (Date, Time, String)

    date to look up

Returns:

  • (Array<TimeOffRequestDate>, nil)

    approved events for the date, or nil when none are loaded



41
42
43
44
45
46
# File 'app/concerns/models/employee_work_status.rb', line 41

def day_employee_events(date)
  date = date.to_date unless date.is_a?(Date)
  return @preloaded_time_off_events[date] if @preloaded_time_off_events&.key?(date)

  employee_events_loaded(date)
end

#day_status_employee_event(date) ⇒ Symbol?

Determines the work status for the employee on a date based on approved time-off events.

Parameters:

  • date (Date, Time, String)

    date to evaluate

Returns:

  • (Symbol, nil)

    status symbol such as :working, :partial_timeoff, or a time-off type name



53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'app/concerns/models/employee_work_status.rb', line 53

def day_status_employee_event(date)
  evts = day_employee_events(date)
  return if evts.blank?

  wh = working_hours_on_date(date)

  if evts.any? { |ev| ev.time_off_request.time_off_type_id == TimeOffType::BANKED_TIME_ID }
    :working
  elsif partial_day_pto_present?(evts, wh)
    :partial_timeoff
  else
    evts.first.time_off_request.time_off_type.name.to_sym
  end
end

#day_status_holiday_check(date) ⇒ Symbol?

Checks whether the given date is a company holiday or weekend.

Parameters:

  • date (Date, Time, String)

    date to check

Returns:

  • (Symbol, nil)

    :not_working_closed if closed, otherwise nil



29
30
31
32
33
34
# File 'app/concerns/models/employee_work_status.rb', line 29

def day_status_holiday_check(date)
  date = date.to_date unless date.is_a?(Date)
  return :not_working_closed unless date.on_weekday?

  :not_working_closed if current_company_holidays_array.include?(date)
end

#day_status_work_schedule(date) ⇒ Symbol?

Checks whether the employee has no effective working schedule for the given date.

Parameters:

  • date (Date, Time, String)

    date to check

Returns:

  • (Symbol, nil)

    :not_working_closed when no hours are scheduled, otherwise nil



88
89
90
91
92
93
# File 'app/concerns/models/employee_work_status.rb', line 88

def day_status_work_schedule(date)
  has_effective = work_schedules.loaded? ? effective_schedule_in_memory?(date) : work_schedules.effective_on(date).exists?
  return unless has_effective && working_hours_on_date(date).zero?

  :not_working_closed
end

#employee_events_loaded(start_date = nil) ⇒ ActiveRecord::Relation<TimeOffRequestDate>

Approved time-off request dates for this employee, optionally scoped to a single date.

Parameters:

  • start_date (Date, Time, String, nil) (defaults to: nil)

    optional single date to scope the query

Returns:

  • (ActiveRecord::Relation<TimeOffRequestDate>)

    relation of approved time-off request dates



100
101
102
103
104
# File 'app/concerns/models/employee_work_status.rb', line 100

def employee_events_loaded(start_date = nil)
  evts = TimeOffRequestDate.joins(:time_off_request).where(time_off_requests: { state: 'approved', employee_id: id })
  evts = evts.where(date: start_date) if start_date
  evts.distinct
end

#next_business_day(start_date = nil, offset = 1) ⇒ Date?

Next date that is not a closed/non-working day for the employee.

Parameters:

  • start_date (Date, Time, String, nil) (defaults to: nil)

    starting date; defaults to today

  • offset (Integer) (defaults to: 1)

    number of days forward from the start date to begin searching

Returns:

  • (Date, nil)

    the next business date, or nil if none is found within 30 days



178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'app/concerns/models/employee_work_status.rb', line 178

def next_business_day(start_date = nil, offset = 1)
  start_date ||= Date.current
  search_date = start_date.dup.to_date + offset.days
  counter = 1
  while work_status_on_day(search_date) == :not_working_closed
    search_date += 1.day
    if (counter += 1) > 30
      search_date = nil
      break
    end
  end
  search_date
end

#next_business_day_closing_time(start_date = nil, offset = 1) ⇒ DateTime?

Closing time on the employee's next business day.

Parameters:

  • start_date (Date, Time, String, nil) (defaults to: nil)

    starting date; defaults to today

  • offset (Integer) (defaults to: 1)

    number of days forward from the start date to begin searching

Returns:

  • (DateTime, nil)

    closing time on the next business day, or nil if none is found



197
198
199
200
# File 'app/concerns/models/employee_work_status.rb', line 197

def next_business_day_closing_time(start_date = nil, offset = 1)
  dt = next_business_day(start_date, offset)
  WorkingHours.advance_to_closing_time(dt)
end

#next_working_day(start_date = nil, offset = 1) ⇒ Date

Next date on which the employee is working.

Parameters:

  • start_date (Date, Time, String, nil) (defaults to: nil)

    starting date; defaults to today

  • offset (Integer) (defaults to: 1)

    number of days forward from the start date to begin searching

Returns:

  • (Date)

    the next working date

Raises:

  • (RuntimeError)

    if no working day is found within roughly two years



159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'app/concerns/models/employee_work_status.rb', line 159

def next_working_day(start_date = nil, offset = 1)
  start_date ||= Date.current
  search_date = start_date.dup.to_date + offset.days
  preload_time_off_events(search_date, search_date + 30.days)

  attempts = 0
  until working_on_day?(search_date)
    search_date += 1.day
    attempts += 1
    raise no_working_day_within_search_limit_error if attempts > 730
  end
  search_date
end

#preload_time_off_events(start_date, end_date) ⇒ Hash{Date => Array<TimeOffRequestDate>}

Preloads approved time-off events into an in-memory cache for a date range.

Parameters:

  • start_date (Date, Time, String)

    first date of the range

  • end_date (Date, Time, String)

    last date of the range

Returns:



207
208
209
210
211
212
213
214
215
216
217
# File 'app/concerns/models/employee_work_status.rb', line 207

def preload_time_off_events(start_date, end_date)
  @preloaded_time_off_events ||= {}
  events = TimeOffRequestDate.joins(:time_off_request)
                             .includes(time_off_request: :time_off_type)
                             .where(time_off_requests: { state: 'approved', employee_id: id })
                             .where(date: start_date..end_date)
                             .distinct

  events.group_by(&:date).each { |date, evts| @preloaded_time_off_events[date] = evts }
  (start_date..end_date).each { |date| @preloaded_time_off_events[date] ||= [] }
end

#preload_working_hours(start_date, end_date) ⇒ Hash{Date => Numeric}

Preloads scheduled working hours into an in-memory cache for a date range.

Parameters:

  • start_date (Date, Time, String)

    first date of the range

  • end_date (Date, Time, String)

    last date of the range

Returns:

  • (Hash{Date => Numeric})

    cached working hours per date



224
225
226
227
228
229
230
231
232
# File 'app/concerns/models/employee_work_status.rb', line 224

def preload_working_hours(start_date, end_date)
  @preloaded_working_hours ||= {}
  schedules = schedules_for_preload(start_date, end_date)
  hours_lookup = hours_by_schedule_and_wday(schedules)

  (start_date..end_date).each do |date|
    @preloaded_working_hours[date] = sum_working_hours_for_date(schedules, hours_lookup, date)
  end
end

#work_status_on_day(date) ⇒ Symbol

Overall work status for the employee on the given date.

Priority order is: holiday, employee event, work schedule, then :working.

Parameters:

  • date (Date, Time, String)

    date to evaluate

Returns:

  • (Symbol)

    work status for the date



113
114
115
116
# File 'app/concerns/models/employee_work_status.rb', line 113

def work_status_on_day(date)
  date = date.to_date unless date.is_a?(Date)
  day_status_holiday_check(date) || day_status_employee_event(date) || day_status_work_schedule(date) || :working
end

#working_hours_on_date(date) ⇒ Numeric

Total scheduled working hours for the employee on the given date.

Parameters:

  • date (Date, Time, String)

    date to evaluate

Returns:

  • (Numeric)

    sum of scheduled hours for the date



73
74
75
76
77
78
79
80
81
# File 'app/concerns/models/employee_work_status.rb', line 73

def working_hours_on_date(date)
  date = date.to_date unless date.is_a?(Date)
  return @preloaded_working_hours[date] if @preloaded_working_hours&.key?(date)

  work_schedules.effective_on(date)
                .joins(:work_schedule_days)
                .where(work_schedule_days: { day_of_week: date.wday })
                .sum('work_schedule_days.hours')
end

#working_on_day?(date) ⇒ Boolean

Whether the employee is working at all on the given date.

Parameters:

  • date (Date, Time, String)

    date to evaluate

Returns:

  • (Boolean)

    true when the employee has working hours on the date



147
148
149
150
151
# File 'app/concerns/models/employee_work_status.rb', line 147

def working_on_day?(date)
  date = date.to_date unless date.is_a?(Date)
  ws = work_status_on_day(date)
  ws == :working || (ws == :partial_timeoff && capacity_ratio(date).positive?)
end