Class: EmployeeEvent

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/employee_event.rb

Overview

== Schema Information

Table name: employee_events
Database name: primary

id :integer not null, primary key
accumulated :integer
authorized :boolean default(FALSE)
avoid_lunch :boolean default(FALSE)
backup :string
broken_down :boolean default(FALSE)
broken_down_time :integer
date :date
day_block_departments :text
description :string(750)
event_time :integer
from_hour :time
from_hour_legacy :time
over_budget_alert :boolean default(FALSE)
status :enum default("working")
sub_status :integer
time_to_add :integer
to_hour :time
to_hour_legacy :time
created_at :datetime
updated_at :datetime
backup_employee_id :integer
employee_id :integer

Indexes

employee_events_backup_employee_id_idx (backup_employee_id)
index_employee_events_on_date (date)
index_employee_events_on_employee_id (employee_id)

Foreign Keys

fk_rails_... (backup_employee_id => parties.id) ON DELETE => nullify
fk_rails_... (employee_id => parties.id) ON DELETE => restrict

Constant Summary collapse

STATES_FOR_GOOGLE_CALENDAR =

States for google calendar.

%w[working_no_calls not_working_timeoff partial_timeoff not_working_unpaid_timeoff short_term_disability jury_duty bereavement birthday company_holiday community_service
working_on_service parental_leave].freeze
MAX_GOOGLE_REFRESH_ATTEMPTS =

One refresh per sync, mirroring TimeOffRequests::GoogleCalendar::MAX_RETRIES.
A token that refreshes cleanly and still 401s means the grant is bad, not
stale — retrying that spins against Google and hangs the Sidekiq job.

1
STATUS_COLOR =

Status color.

{ working_no_calls: "#f0ad4e", not_working_closed: "#d7d7d7", not_working_timeoff: "#d9534f",
partial_timeoff: "#94E994",
not_working_unpaid_timeoff: "#d9534f", short_term_disability: "#337ab7", jury_duty: "#337ab7",
bereavement: "#337ab7", birthday: "#5bc0de", community_service: "#5bc0de", parental_leave: "#5bc0de" }.freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#dateObject (readonly)

Pending: implement a validation that does not allow overlapping of time event of same status for a given time range/employee
validates_uniqueness_of :employee_id, :scope => :date

Validations:



55
# File 'app/models/employee_event.rb', line 55

validates :employee_id, :status, :date, presence: true

#employee_idObject (readonly)

Pending: implement a validation that does not allow overlapping of time event of same status for a given time range/employee
validates_uniqueness_of :employee_id, :scope => :date

Validations:



55
# File 'app/models/employee_event.rb', line 55

validates :employee_id, :status, :date, presence: true

#statusObject (readonly)

Pending: implement a validation that does not allow overlapping of time event of same status for a given time range/employee
validates_uniqueness_of :employee_id, :scope => :date

Validations:



55
# File 'app/models/employee_event.rb', line 55

validates :employee_id, :status, :date, presence: true

Class Method Details

.banked_time_select_optionsvoid

This method returns an undefined value.



142
143
144
145
# File 'app/models/employee_event.rb', line 142

def self.banked_time_select_options
  # (0..8).to_a.map{|i| [STATUS[i],i]}
  banked_times.map { |n, v| [n.to_s.humanize, v] } if CurrentScope.user&.is_manager?
end

.by_monthActiveRecord::Relation<EmployeeEvent>

A relation of EmployeeEvents that are by month. Active Record Scope

Returns:

See Also:



92
# File 'app/models/employee_event.rb', line 92

scope :by_month, ->(d) { where(date: d.all_month) }

.dates_blockedActiveRecord::Relation<EmployeeEvent>

A relation of EmployeeEvents that are dates blocked. Active Record Scope

Returns:

See Also:



104
# File 'app/models/employee_event.rb', line 104

scope :dates_blocked, -> { where(status: 'block_a_day') }

.events_needing_attentionActiveRecord::Relation<EmployeeEvent>

A relation of EmployeeEvents that are events needing attention. Active Record Scope

Returns:

See Also:



98
# File 'app/models/employee_event.rb', line 98

scope :events_needing_attention, ->(date = Date.current) { where('date = ? OR authorized = ?', date, false).where.not(status: %w[add_banked_time block_a_day]) }

.from_active_employeesActiveRecord::Relation<EmployeeEvent>

A relation of EmployeeEvents that are from active employees. Active Record Scope

Returns:

See Also:



101
# File 'app/models/employee_event.rb', line 101

scope :from_active_employees, -> { where(employee_id: Employee.active_employees.select(:id)) }

.full_day_requestsActiveRecord::Relation<EmployeeEvent>

A relation of EmployeeEvents that are full day requests. Active Record Scope

Returns:

See Also:



93
# File 'app/models/employee_event.rb', line 93

scope :full_day_requests, -> { where(from_hour: '00:00:00') }

.future_eventsActiveRecord::Relation<EmployeeEvent>

A relation of EmployeeEvents that are future events. Active Record Scope

Returns:

See Also:



102
# File 'app/models/employee_event.rb', line 102

scope :future_events, -> { where("date > (?)", Date.current) }

.partial_day_requestsActiveRecord::Relation<EmployeeEvent>

A relation of EmployeeEvents that are partial day requests. Active Record Scope

Returns:

See Also:



94
# File 'app/models/employee_event.rb', line 94

scope :partial_day_requests, -> { where.not(from_hour: '00:00:00') }

.status_report_optionsvoid

This method returns an undefined value.



136
137
138
139
# File 'app/models/employee_event.rb', line 136

def self.status_report_options
  [['Time Off', 'not_working_timeoff'], ['Unpaid Time Off', 'not_working_unpaid_timeoff'], ['Short Term Disability', 'short_term_disability'], ['Jury Duty', 'jury_duty'], ['Bereavement', 'bereavement'], ['Birthday', 'birthday'],
   ['Parental Leave', 'parental_leave']]
end

.status_select_options(options = {}) ⇒ Array

Returns employee event statuses grouped for a select list.

Parameters:

  • options (Hash) (defaults to: {})

    selection options

Options Hash (options):

  • current_user (Employee, nil)

    managers get the admin-only group

  • include_all_options (Boolean)

    always include the admin-only group

Returns:

  • (Array)

    grouped status options for a select list



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'app/models/employee_event.rb', line 118

def self.status_select_options(options = {})
  admin_options = statuses.reject { |n, _v| n.to_s != "add_banked_time" && n.to_s != "block_a_day" }.map { |n, v| [n.to_s.humanize, v] }
  employee_options = statuses.reject do |n, _v|
    %w[add_banked_time working not_working_closed partial_timeoff block_a_day company_holiday].include?(n.to_s)
  end.map { |n, v| [n.to_s.humanize, v] }
  if options[:current_user]&.is_manager? || options[:include_all_options]
    [
      ['Time Off types', employee_options],
      ['Admin only', admin_options]
    ]
  else
    [
      ['Time Off types', employee_options]
    ]
  end
end

.time_off_eventsActiveRecord::Relation<EmployeeEvent>

A relation of EmployeeEvents that are time off events. Active Record Scope

Returns:

See Also:



103
# File 'app/models/employee_event.rb', line 103

scope :time_off_events, -> { where.not(status: 'block_a_day') }

Instance Method Details

#backup_employeeEmployee?

Returns:



51
# File 'app/models/employee_event.rb', line 51

belongs_to :backup_employee, class_name: 'Employee', optional: true

#employeeEmployee?

Returns:



49
# File 'app/models/employee_event.rb', line 49

belongs_to :employee, optional: true

#full_day?Boolean

Returns:

  • (Boolean)


148
149
150
# File 'app/models/employee_event.rb', line 148

def full_day?
  from_hour == to_hour
end

#is_partial?Boolean

Returns:

  • (Boolean)


153
154
155
# File 'app/models/employee_event.rb', line 153

def is_partial?
  status == "not_working_timeoff" && from_hour != to_hour
end

#pto_takenvoid

This method returns an undefined value.



158
159
160
161
162
# File 'app/models/employee_event.rb', line 158

def pto_taken
  taken = 0
  taken = (event_time || 0) - (broken_down_time || 0) if status.in?(%w[not_working_timeoff partial_timeoff])
  taken
end

#send_to_google_calendar(action) ⇒ void

Parameters:

  • action (Object)
  • action (Object)

Returns:

  • (void)
  • (void)


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
# File 'app/models/employee_event.rb', line 168

def send_to_google_calendar(action)
  # Through the shared authorizer, which refreshes ahead of the stored expiry.
  # Hand-building the client here skipped google_auth_expires_at entirely, so
  # this path always spent a failing API call first and recovered on the 401 —
  # the very thing recording the expiry was meant to stop.
  client = TimeOffRequests::GoogleAuthService.authorize(employee)
  return if client.nil?

  service = Google::Apis::CalendarV3::CalendarService.new
  service.authorization = client
  # Now lets use it
  case action
  when 'create'
    if status.in?(STATES_FOR_GOOGLE_CALENDAR)
      define_event_times

      event = Google::Apis::CalendarV3::Event.new(
        id: "ee#{id}",
        summary: status.titleize,
        description: description,
        start: @start_time,
        end: @end_time
      )
      service.insert_event(employee.email, event)
    end
  when 'update'
    if status.in?(STATES_FOR_GOOGLE_CALENDAR)
      define_event_times

      event = service.get_event(employee.email, "ee#{id}")
      event.summary = status.titleize
      event.description = description
      event.start = @start_time
      event.end = @end_time

      service.update_event(employee.email, "ee#{id}", event)
    end
  when 'destroy'
    begin
      service.delete_event(employee.email, "ee#{id}")
    rescue StandardError
      nil
    end
  end
rescue Google::Apis::AuthorizationError
  begin
    # Counted in an ivar, not a local: `retry` re-runs the method body from
    # the top, so a counter reset there would never terminate the loop.
    if (@google_refresh_attempts = @google_refresh_attempts.to_i + 1) > MAX_GOOGLE_REFRESH_ATTEMPTS
      return TimeOffRequests::GoogleAuthService.report_refresh_exhausted(
        employee, custom_data: { model: 'EmployeeEvent', employee_event_id: id, action: action }
      )
    end

    # Through the shared refresher so google_auth_expires_at is recorded —
    # a local client.refresh! here would leave the stored expiry pointing at
    # the previous token and defeat proactive refresh everywhere else.
    raise 'Google token refresh failed' unless TimeOffRequests::GoogleAuthService.refresh_token(employee)

    retry
  rescue StandardError => e
    ErrorReporting.error e
  end
rescue Google::Apis::ClientError => e
  if e.status_code == 409
    # The event already exists, so we update it.
    service.update_event(employee.email, "ee#{id}", event)
  end
ensure
  # Scoped to one invocation. `retry` restarts the body without unwinding, so
  # the counter survives the retry loop but not the next create/update/destroy
  # on this same record — which would otherwise start at 1 and report
  # exhaustion without ever attempting a refresh.
  @google_refresh_attempts = nil
end