From dc1ce2fce3592b9a8a897dad45a57dfe60fb8dc3 Mon Sep 17 00:00:00 2001 From: Kaio Magalhaes Date: Tue, 7 Nov 2023 12:52:52 -0300 Subject: [PATCH] add analytics --- .../analytics/time_entries_controller.rb | 20 + app/models/time_off_type.rb | 6 +- app/services/team_maker_project_creator.rb | 3 + app/utils/analytics/time_entries_analytics.rb | 113 ++++ config/routes.rb | 5 +- db/schema.rb | 1 + .../analytics/time_entries_controller_spec.rb | 23 + spec/factories/time_entries.rb | 6 +- spec/factories/time_off_types.rb | 2 +- spec/factories/time_offs.rb | 10 +- .../analytics/time_entries_analytics_spec.rb | 483 ++++++++++++++++++ 11 files changed, 665 insertions(+), 7 deletions(-) create mode 100644 app/controllers/analytics/time_entries_controller.rb create mode 100644 app/utils/analytics/time_entries_analytics.rb create mode 100644 spec/controllers/analytics/time_entries_controller_spec.rb create mode 100644 spec/utils/analytics/time_entries_analytics_spec.rb diff --git a/app/controllers/analytics/time_entries_controller.rb b/app/controllers/analytics/time_entries_controller.rb new file mode 100644 index 0000000..65e1570 --- /dev/null +++ b/app/controllers/analytics/time_entries_controller.rb @@ -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 diff --git a/app/models/time_off_type.rb b/app/models/time_off_type.rb index 27dbeb6..b386d1f 100644 --- a/app/models/time_off_type.rb +++ b/app/models/time_off_type.rb @@ -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 diff --git a/app/services/team_maker_project_creator.rb b/app/services/team_maker_project_creator.rb index 0ba4421..91d9129 100644 --- a/app/services/team_maker_project_creator.rb +++ b/app/services/team_maker_project_creator.rb @@ -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) @@ -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 diff --git a/app/utils/analytics/time_entries_analytics.rb b/app/utils/analytics/time_entries_analytics.rb new file mode 100644 index 0000000..05ef00d --- /dev/null +++ b/app/utils/analytics/time_entries_analytics.rb @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 0d8393a..1668bec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' @@ -15,6 +14,10 @@ end end + namespace :analytics do + resources :time_entries, only: [:index] + end + resources :users resources :customers resources :professions, only: [:index] diff --git a/db/schema.rb b/db/schema.rb index 20d15f5..0f1f322 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,6 +12,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_06_173201) do # These are extensions that must be enabled in order to support this database + enable_extension "pg_stat_statements" enable_extension "plpgsql" create_table "assignments", force: :cascade do |t| diff --git a/spec/controllers/analytics/time_entries_controller_spec.rb b/spec/controllers/analytics/time_entries_controller_spec.rb new file mode 100644 index 0000000..8d84b6b --- /dev/null +++ b/spec/controllers/analytics/time_entries_controller_spec.rb @@ -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 diff --git a/spec/factories/time_entries.rb b/spec/factories/time_entries.rb index 76c7977..da8418f 100644 --- a/spec/factories/time_entries.rb +++ b/spec/factories/time_entries.rb @@ -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 diff --git a/spec/factories/time_off_types.rb b/spec/factories/time_off_types.rb index 9f3e275..2dbcf59 100644 --- a/spec/factories/time_off_types.rb +++ b/spec/factories/time_off_types.rb @@ -11,6 +11,6 @@ # FactoryBot.define do factory :time_off_type do - name { 'paid time off' } + name { TimeOffType::VACATION_TYPE } end end diff --git a/spec/factories/time_offs.rb b/spec/factories/time_offs.rb index 6a6dc6c..738a41f 100644 --- a/spec/factories/time_offs.rb +++ b/spec/factories/time_offs.rb @@ -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 diff --git a/spec/utils/analytics/time_entries_analytics_spec.rb b/spec/utils/analytics/time_entries_analytics_spec.rb new file mode 100644 index 0000000..72841b1 --- /dev/null +++ b/spec/utils/analytics/time_entries_analytics_spec.rb @@ -0,0 +1,483 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Analytics::TimeEntriesAnalytics, type: :service do + let(:sow) { FactoryBot.create(:statement_of_work, :with_fixed_bid) } + let(:start_date) { Date.parse('2023/11/29') } + let(:end_date) { start_date + 6.days } + + describe '#data' do + context 'when the user puts the information for the entire period' do + it 'returns the dataset for that SOW' do + user1 = FactoryBot.create(:user) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 1.day, user: user1, hours: 8) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 2.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 3.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 4.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + + requirement1 = FactoryBot.create(:requirement, statement_of_work: sow, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:assignment, requirement: requirement1, user: user1, coverage: 1, start_date:, + end_date:) + + data = Analytics::TimeEntriesAnalytics.new(sow, start_date, end_date).data + data_json = data.to_json + expected_response_json = { + labels: [user1.name], + datasets: [ + { + label: 'Worked', + data: [40.0] + }, + { + label: 'Missing', + data: [0.0] + }, + { + label: 'Paid time off', + data: [0] + }, + { + label: 'Sick leave', + data: [0] + }, + { + label: 'Over delivered', + data: [0.0] + } + ] + }.to_json + expect(data_json).to eql(expected_response_json) + end + end + + context 'when there are multiple users putting the information for the entire period' do + it 'returns the dataset for that SOW' do + user1 = FactoryBot.create(:user) + user2 = FactoryBot.create(:user) + user3 = FactoryBot.create(:user) + + # user 1 + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 1.day, user: user1, hours: 8) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 2.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 3.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 4.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + + # user 2 + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 1.day, user: user2, hours: 4) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 2.days, user: user2, hours: 4) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 3.days, user: user2, hours: 4) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 4.days, user: user2, hours: 4) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 5.days, user: user2, hours: 4) + # user 3 + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 1.day, user: user3, hours: 4) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 2.days, user: user3, hours: 4) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 3.days, user: user3, hours: 4) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 4.days, user: user3, hours: 4) + FactoryBot.create(:time_entry, + statement_of_work: sow, date: start_date + 5.days, user: user3, hours: 4) + + requirement1 = FactoryBot.create(:requirement, statement_of_work: sow, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:requirement, statement_of_work: sow, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:assignment, requirement: requirement1, user: user1, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:assignment, requirement: requirement1, user: user2, coverage: 0.5, start_date:, + end_date:) + FactoryBot.create(:assignment, requirement: requirement1, user: user3, coverage: 0.5, start_date:, + end_date:) + + data = Analytics::TimeEntriesAnalytics.new(sow, start_date, end_date).data + data_json = data.to_json + expected_response_json = { + labels: [user1.name, user2.name, user3.name], + datasets: [ + { + label: 'Worked', + data: [40.0, 20.0, 20.0] + }, + { + label: 'Missing', + data: [0.0, 0.0, 0.0] + }, + { + label: 'Paid time off', + data: [0, 0, 0] + }, + { + label: 'Sick leave', + data: [0, 0, 0] + }, + { + label: 'Over delivered', + data: [0.0, 0.0, 0.0] + } + ] + }.to_json + expect(data_json).to eql(expected_response_json) + end + end + + context 'when the user does not add 5 hours for that period' do + it 'returns 5 hours as missing' do + user1 = FactoryBot.create(:user) + + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 1.day, user: user1, hours: 3) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 2.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 3.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 4.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + + requirement1 = FactoryBot.create(:requirement, statement_of_work: sow, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:assignment, requirement: requirement1, user: user1, coverage: 1, start_date:, + end_date:) + + data = Analytics::TimeEntriesAnalytics.new(sow, start_date, end_date).data + data_json = data.to_json + expected_response_json = { + labels: [user1.name], + datasets: [ + { + label: 'Worked', + data: [35.0] + }, + { + label: 'Missing', + data: [5.0] + }, + { + label: 'Paid time off', + data: [0] + }, + { + label: 'Sick leave', + data: [0] + }, + { + label: 'Over delivered', + data: [0] + } + ] + }.to_json + expect(data_json).to eql(expected_response_json) + end + end + context 'when the user a full day of PTO during the period' do + it 'returns 8 hours as paid time off' do + user1 = FactoryBot.create(:user) + + FactoryBot.create(:time_off, :vacation, user: user1, starts_at: start_date, ends_at: start_date) + + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 2.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 3.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 4.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + + requirement1 = FactoryBot.create(:requirement, statement_of_work: sow, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:assignment, requirement: requirement1, user: user1, coverage: 1, start_date:, + end_date:) + + data = Analytics::TimeEntriesAnalytics.new(sow, start_date, end_date).data + data_json = data.to_json + expected_response_json = { + labels: [user1.name], + datasets: [ + { + label: 'Worked', + data: [32.0] + }, + { + label: 'Missing', + data: [0.0] + }, + { + label: 'Paid time off', + data: [8] + }, + { + label: 'Sick leave', + data: [0] + }, + { + label: 'Over delivered', + data: [0] + } + ] + }.to_json + expect(data_json).to eql(expected_response_json) + end + end + + context 'when the user has 2 hours of PTO during the period' do + it 'returns 8 hours as paid time off' do + user1 = FactoryBot.create(:user) + + FactoryBot.create(:time_off, user: user1, starts_at: start_date + 8.hours, ends_at: start_date + 10.hours) + + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 2.days, user: user1, hours: 6) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 3.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 4.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + + requirement1 = FactoryBot.create(:requirement, statement_of_work: sow, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:assignment, requirement: requirement1, user: user1, coverage: 1, start_date:, + end_date:) + + data = Analytics::TimeEntriesAnalytics.new(sow, start_date, end_date).data + data_json = data.to_json + expected_response_json = { + labels: [user1.name], + datasets: [ + { + label: 'Worked', + data: [38.0] + }, + { + label: 'Missing', + data: [0.0] + }, + { + label: 'Paid time off', + data: [2.0] + }, + { + label: 'Sick leave', + data: [0] + }, + { + label: 'Over delivered', + data: [0] + } + ] + }.to_json + expect(data_json).to eql(expected_response_json) + end + end + + context 'when the user has 5 hours of PTO during the period and is missing 5 hours' do + it 'returns 5 hours of pto and 5 hours of missing hours' do + user1 = FactoryBot.create(:user) + + FactoryBot.create(:time_off, user: user1, starts_at: start_date + 8.hours, ends_at: start_date + 13.hours) + + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 2.days, user: user1, hours: 3) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 3.days, user: user1, hours: 3) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 4.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + + requirement1 = FactoryBot.create(:requirement, statement_of_work: sow, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:assignment, requirement: requirement1, user: user1, coverage: 1, start_date:, + end_date:) + + data = Analytics::TimeEntriesAnalytics.new(sow, start_date, end_date).data + data_json = data.to_json + expected_response_json = { + labels: [user1.name], + datasets: [ + { + label: 'Worked', + data: [30.0] + }, + { + label: 'Missing', + data: [5.0] + }, + { + label: 'Paid time off', + data: [5.0] + }, + { + label: 'Sick leave', + data: [0] + }, + { + label: 'Over delivered', + data: [0] + } + ] + }.to_json + expect(data_json).to eql(expected_response_json) + end + end + + context 'when the user has 5 hours of PTO during the period and 8 hours of sick leave' do + it 'returns 5 hours of pto and 8 hours of sick leave' do + user1 = FactoryBot.create(:user) + + FactoryBot.create(:time_off, :vacation, user: user1, starts_at: start_date + 8.hours, + ends_at: start_date + 13.hours) + FactoryBot.create(:time_off, :sick_leave, user: user1, starts_at: start_date + 1.day, + ends_at: start_date + 1.day) + + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 2.days, user: user1, hours: 3) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 3.days, user: user1, hours: 3) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 4.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + + requirement1 = FactoryBot.create(:requirement, statement_of_work: sow, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:assignment, requirement: requirement1, user: user1, coverage: 1, start_date:, + end_date:) + + data = Analytics::TimeEntriesAnalytics.new(sow, start_date, end_date).data + data_json = data.to_json + expected_response_json = { + labels: [user1.name], + datasets: [ + { + label: 'Worked', + data: [30.0] + }, + { + label: 'Missing', + data: [0] + }, + { + label: 'Paid time off', + data: [5.0] + }, + { + label: 'Sick leave', + data: [8] + }, + { + label: 'Over delivered', + data: [0] + } + ] + }.to_json + expect(data_json).to eql(expected_response_json) + end + end + + context 'when the user overworked by 8 hours' do + it 'returns 8 hours in the overwork' do + user1 = FactoryBot.create(:user) + + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 2.days, user: user1, hours: 10) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 3.days, user: user1, hours: 9) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 4.days, user: user1, hours: 9) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 5.days, user: user1, hours: 8) + FactoryBot.create(:time_entry, statement_of_work: sow, date: start_date + 6.days, user: user1, hours: 12) + + requirement1 = FactoryBot.create(:requirement, statement_of_work: sow, coverage: 1, start_date:, + end_date:) + + FactoryBot.create(:assignment, requirement: requirement1, user: user1, coverage: 1, start_date:, + end_date:) + + data = Analytics::TimeEntriesAnalytics.new(sow, start_date, end_date).data + data_json = data.to_json + expected_response_json = { + labels: [user1.name], + datasets: [ + { + label: 'Worked', + data: [40.0] + }, + { + label: 'Missing', + data: [0] + }, + { + label: 'Paid time off', + data: [0] + }, + { + label: 'Sick leave', + data: [0] + }, + { + label: 'Over delivered', + data: [8.0] + } + ] + }.to_json + expect(data_json).to eql(expected_response_json) + end + end + end +end + +# const horizontalBarChartData = { +# labels: [ +# "Rheniery", +# "Carlos", +# "Albo", +# "Marla", +# "Carlos", +# "Gabriel", +# "Rheniery", +# "Carlos", +# "Albo", +# "Marla", +# "Carlos", +# "Gabriel", +# ], +# datasets: [ +# { +# label: "Worked", +# color: "success", +# data: [15, 20, 12, 60, 20, 15, 15, 20, 12, 60, 20, 15], +# }, +# { +# label: "Missing", +# color: "error", +# data: [15, 20, 12, 60, 20, 15, 15, 20, 12, 60, 20, 15], +# }, +# { +# label: "Paid time off", +# color: "info", +# data: [15, 20, 12, 60, 20, 15, 15, 20, 12, 60, 20, 15], +# }, +# { +# label: "Sick leave", +# color: "warning", +# data: [15, 20, 12, 60, 20, 15, 15, 20, 12, 60, 20, 15], +# }, +# { +# label: "Over delivered", +# color: "dark", +# data: [15, 20, 12, 60, 20, 15, 15, 20, 12, 60, 20, 15], +# }, +# ], +# };