Skip to content

Commit

Permalink
add analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
kaiomagalhaes committed Nov 7, 2023
1 parent a70d22c commit dc1ce2f
Show file tree
Hide file tree
Showing 11 changed files with 665 additions and 7 deletions.
20 changes: 20 additions & 0 deletions app/controllers/analytics/time_entries_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Analytics
class TimeEntriesController < ApplicationController
before_action :set_statement_of_work, only: %i[index]

def index
@time_entries = Analytics::TimeEntriesAnalytics.new(@statement_of_work, params[:start_date],
params[:end_date]).data

render json: @time_entries
end

private

def set_statement_of_work
@statement_of_work = StatementOfWork.find(params[:statement_of_work_id])
end
end
end
6 changes: 5 additions & 1 deletion app/models/time_off_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
# updated_at :datetime not null
#
class TimeOffType < ApplicationRecord
validates :name, presence: true
VACATION_TYPE = 'vacation'
SICK_LEAVE_TYPE = 'sick leave'
ERRAND_TYPE = 'errand'

validates :name, presence: true, inclusion: { in: [VACATION_TYPE, SICK_LEAVE_TYPE, ERRAND_TYPE] }
end
3 changes: 3 additions & 0 deletions app/services/team_maker_project_creator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def call
private

def create_time_entires!(team_maker_project_time_entries)
statement_of_work.time_entries.destroy_all

team_maker_project_time_entries.each do |time_entry|
user = find_user(time_entry.resource)

Expand Down Expand Up @@ -81,6 +83,7 @@ def statement_of_work
model: 'maintenance', hour_delivery_schedule: 'contract_period',
total_revenue: 1
)

@statement_of_work.requirements.destroy_all
@statement_of_work
end
Expand Down
113 changes: 113 additions & 0 deletions app/utils/analytics/time_entries_analytics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# frozen_string_literal: true

module Analytics
class TimeEntriesAnalytics
def initialize(statement_of_work, start_date, end_date)
@statement_of_work = statement_of_work
@start_date = start_date.to_datetime
@end_date = end_date.to_datetime
end

# rubocop:disable Style/Metrics/AbcSize
def data
{
labels: users.map(&:name),
datasets: [
{ label: 'Worked', data: assignments.map(&method(:clean_worked_hours)) },
{ label: 'Missing', data: assignments.map(&method(:missing_hours)) },
{ label: 'Paid time off', data: assignments.map(&method(:vacation_hours)) },
{ label: 'Sick leave', data: assignments.map(&method(:sick_leave_hours)) },
{ label: 'Over delivered', data: assignments.map(&method(:over_delivered_hours)) }
]
}
end
# rubocop:enable Style/Metrics/AbcSize

def assignments
@assignments ||= Assignment.where(requirement: requirements)
end

def requirements
@requirements ||=
@statement_of_work.requirements.where('start_date <= ? AND end_date >= ?', @start_date, @end_date)
end

def over_delivered_hours(assignment)
worked = worked_hours(assignment)
expected = expected_hours(assignment)

expected > worked ? 0 : worked - expected
end

def clean_worked_hours(assignment)
[worked_hours(assignment), expected_hours(assignment)].min
end

def worked_hours(assignment)
time_entries = TimeEntry.where(statement_of_work: @statement_of_work, date: @start_date..@end_date,
user: assignment.user)

time_entries.sum(&:hours)
end

def expected_hours(assignment)
days = (@start_date..@end_date).count { |d| !d.sunday? && !d.saturday? }
days * assignment.coverage * 8
end

def missing_hours(assignment)
worked = worked_hours(assignment)

[[expected_hours(assignment) - worked, 0].max - vacation_hours(assignment) - sick_leave_hours(assignment), 0].max
end

def vacation_hours(assignment)
vacation_type = TimeOffType.find_by(name: TimeOffType::VACATION_TYPE)
paid_time_off_hours(assignment, vacation_type)
end

def sick_leave_hours(assignment)
sick_leave_type = TimeOffType.find_by(name: TimeOffType::SICK_LEAVE_TYPE)
paid_time_off_hours(assignment, sick_leave_type)
end

# rubocop:disable Style/Metrics/MethodLength
# rubocop:disable Style/Metrics/AbcSize
def paid_time_off_hours(assignment, time_off_type)
time_offs = time_offs_by_user_and_type(assignment.user, time_off_type)

time_offs.reduce(0) do |accumulator, time_off|
start_date = [time_off.starts_at, @start_date].max
end_date = [time_off.ends_at, @end_date].min
hours = 0

current_date = start_date
while current_date <= end_date
next(0) if current_date.saturday? || current_date.sunday?

if current_date == end_date
hours = 8
else
hours += [(end_date - current_date) / 3600, 8].min
end

current_date = current_date.next_day
end

accumulator + hours
end
end
# rubocop:enable Style/Metrics/AbcSize
# rubocop:enable Style/Metrics/MethodLength

def users
@users ||= assignments.map(&:user)
end

def time_offs_by_user_and_type(user, time_off_type)
TimeOff.where(user:, time_off_type:).where(
'starts_at <= ? AND ends_at >= ?', @end_date, @start_date
)
end
end
end
5 changes: 4 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
require 'sidekiq/cron/web'

Rails.application.routes.draw do
resources :time_entries
resources :assignments
mount RailsAdmin::Engine => '/admin', as: 'rails_admin'
mount Sidekiq::Web => '/sidekiq'
Expand All @@ -15,6 +14,10 @@
end
end

namespace :analytics do
resources :time_entries, only: [:index]
end

resources :users
resources :customers
resources :professions, only: [:index]
Expand Down
1 change: 1 addition & 0 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions spec/controllers/analytics/time_entries_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe AssignmentsController, type: :controller do
include_context 'authentication'
render_views

let(:valid_attributes) do
{
statement_of_work_id: FactoryBot.create(:statement_of_work, :with_maintenance).id,
start_date: Date.yesterday,
end_date: Date.tomorrow
}
end

describe 'GET #index' do
it 'returns a success response' do
get :index, params: valid_attributes
expect(response).to be_successful
end
end
end
6 changes: 3 additions & 3 deletions spec/factories/time_entries.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
FactoryBot.define do
factory :time_entry do
date { '2023-10-29' }
hours { 1.5 }
user { nil }
statement_of_work { nil }
hours { 8 }
user
statement_of_work
end
end
2 changes: 1 addition & 1 deletion spec/factories/time_off_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
#
FactoryBot.define do
factory :time_off_type do
name { 'paid time off' }
name { TimeOffType::VACATION_TYPE }
end
end
10 changes: 9 additions & 1 deletion spec/factories/time_offs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,16 @@
FactoryBot.define do
factory :time_off do
user
time_of_type
time_off_type
starts_at { '2023-11-06 17:32:01' }
ends_at { '2023-12-06 17:32:01' }

trait :vacation do
time_off_type { TimeOffType.create(name: TimeOffType::VACATION_TYPE) }
end

trait :sick_leave do
time_off_type { TimeOffType.create(name: TimeOffType::SICK_LEAVE_TYPE) }
end
end
end
Loading

0 comments on commit dc1ce2f

Please sign in to comment.