Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions app/controllers/internal_api/v1/reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,34 @@ class InternalApi::V1::ReportsController < InternalApi::V1::ApplicationControlle

def index
authorize :report
render :index, locals: { entries: filtered_reports, filter_options: }, status: :ok
render :index, locals: { reports:, filter_options: }, status: :ok
end

private

def filter_options
# Send filter options only when page loads
# and not for other requests where user is doing filtering on reports page
return {} if any_filter_added?

@_filter_options ||= { clients: current_company.clients, team_members: current_company.users }
end

def any_filter_added?
params[:date_range].present? ||
params[:status].present? ||
params[:team_member].present? ||
params[:team_member].present?
def reports
default_filter = current_company_filter.merge(this_month_filter)
where_clause = default_filter.merge(Report::Filters.process(params))
group_by_clause = Report::GroupBy.process(params["group_by"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how GroupBy works, for grouping you can use aggregations, as in the SearchKick documentation:

Product.search("wingtips", where: {color: "brandy"}, aggs: [:size])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using aggregation only which is defined in the service class


search_result = TimesheetEntry.search(
where: where_clause,
order: { work_date: :desc },
body_options: group_by_clause,
includes: [:user, { project: :client } ])

Report::Result.process(search_result, params["group_by"])
end

def current_company_filter
{ project_id: current_company.project_ids }
end

def filtered_reports
current_company_project_ids_filter = { project_id: current_company.project_ids }
filters_where_clause = Report::Filters.process(params)
where_clause = current_company_project_ids_filter.merge(filters_where_clause)
TimesheetEntry.search(where: where_clause, includes: [:user, :project])
def this_month_filter
{ work_date: DateTime.current.beginning_of_month..DateTime.current.end_of_month }
end
end
15 changes: 13 additions & 2 deletions app/models/timesheet_entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@
#

class TimesheetEntry < ApplicationRecord
searchkick

enum bill_status: [:non_billable, :unbilled, :billed]

belongs_to :user
Expand All @@ -44,6 +42,19 @@ class TimesheetEntry < ApplicationRecord

scope :in_workspace, -> (company) { where(project_id: company&.project_ids) }

searchkick
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For aggregations to work you have to have filterable is searchkick

  searchable = %i[name]
  filterable = %[created_at]

  searchkick searchable: searchable, filterable: filterable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. With aggregation query which I am passing, aggregations are working fine


def search_data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we individually list all the attributes we want to index?

For example

  def search_data
    {
      id: id.to_i,
      name: name,
      created_at: created_at.to_datetime
}
      

Reason for this is ES works on RAM, and unnecessary fields will clutter up the ES instance which becomes exponential over time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I was thinking of doing this but thought that fields are not that much so indexed everything. I will update code to index only required ones

{
id: id.to_i,
bill_status:,
project_id:,
client_id: self.project.client_id,
user_id:,
work_date:
}
end

def self.during(from, to)
where(work_date: from..to).order(work_date: :desc)
end
Expand Down
4 changes: 1 addition & 3 deletions app/services/report/filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ def status_filter
end

def client_filter
clients = Client.includes(:projects).where(id: filter_params[:client])
project_ids = clients.map { |client| client.project_ids }.flatten
{ project_id: project_ids }
{ client_id: filter_params[:client] }
end

def team_member_filter
Expand Down
52 changes: 52 additions & 0 deletions app/services/report/group_by.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Report
class GroupBy < ApplicationService
attr_reader :group_by_field

POSSIBLE_GROUP_BY_INPUTS = ["team_member", "client", "project", "week"].freeze
GROUP_BY_INPUT_TO_ES_FIELD = {
"team_member" => "user_id",
"client" => "client_id",
"project" => "project_id",
"week" => "week"
}

def initialize(group_by_field)
@group_by_field = group_by_field
end

def process
return {} if group_by_field.blank? || POSSIBLE_GROUP_BY_INPUTS.exclude?(group_by_field)

group_by_query(GROUP_BY_INPUT_TO_ES_FIELD[group_by_field])
end

def group_by_query(field)
{
aggs: {
grouped_reports: group_by_term(field).merge(
{
aggs: {
top_report_hits: {
top_hits: {
sort: [{ work_date: { order: "desc" } }],
_source: ["id"],
size: 100 # aggregation query can only return max top 100 hits
}
}
}
})
}
}
end

def group_by_term(field)
if field == "week"
{ date_histogram: { field: "work_date", calendar_interval: "week" } }
else
{ terms: { field: } }
end
end
end
end
62 changes: 62 additions & 0 deletions app/services/report/result.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module Report
class Result < ApplicationService
attr_reader :es_response, :group_by

def initialize(es_response, group_by)
@es_response = es_response
@group_by = group_by
end

def process
if group_by.blank?
[{ label: "", entries: es_response }]
else
process_aggregated_es_response
end
end

private

# When we query ES, we get all matching timesheet entries as response even when we pass aggregation query.
# Those timesheet entries contains all required association but not the ones in aggregated data.
# So, in order to avoid queries for associated records,
# creating map of id -> timesheet entries from the general ES response and
# then using it for building final data by merging that with ids found in aggregation query.
def process_aggregated_es_response
id_to_timesheet_entry = timsheet_id_to_timesheet_entry
buckets = es_response.aggs["grouped_reports"]["buckets"]
buckets.map do |bucket|
timesheet_entry_ids = bucket["top_report_hits"]["hits"]["hits"].pluck("_id")
timesheet_entries = id_to_timesheet_entry.fetch_values(*timesheet_entry_ids)
{
label: group_label(timesheet_entries.first, bucket["key_as_string"]),
entries: timesheet_entries
}
end
end

def timsheet_id_to_timesheet_entry
es_response.reduce({}) do |res, timesheet|
res.merge({ timesheet.id.to_s => timesheet })
end
end

def group_label(timesheet_entry, bucket_name)
case group_by
when "team_member"
timesheet_entry.user.full_name
when "client"
timesheet_entry.project.client.name
when "project"
timesheet_entry.project.name
when "week"
start_date = DateTime.parse(bucket_name)
end_date = start_date + 6.days
date_format = "%d %b %Y"
"#{start_date.strftime(date_format)} - #{end_date.strftime(date_format)}"
end
end
end
end
23 changes: 13 additions & 10 deletions app/views/internal_api/v1/reports/index.json.jbuilder
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

json.key_format! camelize: :lower
json.deep_format_keys!
json.entries entries do |report|
json.id report.id
json.note report.note
json.project report.project.name
json.project_id report.project_id
json.client report.project.client.name
json.duration report.duration
json.work_date report.work_date
json.bill_status report.bill_status
json.team_member report.user.full_name
json.reports reports do |grouped_report|
json.label grouped_report[:label]
json.entries grouped_report[:entries] do |report|
json.id report.id
json.note report.note
json.project report.project.name
json.project_id report.project_id
json.client report.project.client.name
json.duration report.duration
json.work_date report.work_date
json.bill_status report.bill_status
json.team_member report.user.full_name
end
end
json.filter_options do
json.clients filter_options[:clients] do |client|
Expand Down
2 changes: 1 addition & 1 deletion spec/factories/timesheet_entries.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
project
duration { 480.0 } # in minutes
note { "Did that" }
work_date { "2022-01-11" }
work_date { Date.current }
bill_status { "non_billable" }
end
end
10 changes: 6 additions & 4 deletions spec/requests/internal_api/v1/clients/index_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
let(:client_2) { create(:client, company:) }
let(:project_1) { create(:project, client: client_1) }
let(:project_2) { create(:project, client: client_2) }
let(:time_frame) { "last_week" }
let(:time_frame) { "week" }

context "when user is admin" do
before do
Expand Down Expand Up @@ -69,18 +69,20 @@
end

context "when user is employee" do
let(:time_frame) { "last_week" }

before do
create(:company_user, company:, user:)
user.add_role :admin, company
sign_in user
create_list(:timesheet_entry, 5, user:, project: project_1)
create_list(:timesheet_entry, 5, user:, project: project_2)
send_request :get, internal_api_v1_clients_path
send_request :get, internal_api_v1_clients_path, params: {
time_frame:
}
end

context "when time_frame is week" do
let(:time_frame) { "last_week" }

it "returns the total hours logged for a Company in the last_week" do
client_details = user.current_workspace.clients.kept.map do |client|
{
Expand Down
Loading