Commit 64d64a25 by Tấn Trần Thanh

Merge branch 'feature/WORKFLOW-export-times-entries-in-month' into 'master'

Feature/workflow export times entries in month

See merge request !16
parents 950db87a a3f94b3f
Pipeline #1624 canceled with stages
in 0 seconds
= workflow_report = workflow_report
This plugin is designed to produce monthly workflow reports with precise customization options. Users can select a specific month, year, and team of interest. Upon making these selections, the plugin generates a detailed report that is exclusively based on the chosen month and year. This plugin is designed to produce monthly workflow reports with precise customization options. Users can select a specific month, year, and team of interest. Upon making these selections, the plugin generates a detailed report that is exclusively based on the chosen month and year.
To install and configure the Workflow Report plugin for Redmine, follow these steps:
1. Navigate to the plugin's configuration directory:
cd plugins/workflow_report/config
2. Create the `application.yml` configuration file based on the example:
- Copy the contents from `application.yml.example` into a new file named `application.yml`.
- You can use the following command to do this quickly:
cp application.yml.example application.yml
3. Add your GitHub Token to the configuration file:
- Open the `application.yml` file in your text editor.
- Locate the line that reads `github_token:` and add your GitHub token after the colon. For example:
github_token: your_github_token_here
- Note: Replace `your_github_token_here` with your actual GitHub token.
class WorkflowReportController < ApplicationController class WorkflowReportController < ApplicationController
include WorkflowReport include WorkflowReport
before_action :authorize_global before_action :authorize_global
before_action :require_xhr_request, only: %i[export show_daily_report] before_action :require_xhr_request, only: %i[export show_daily_report export_time_entry]
def index def index
@team_options = $workflow_report_config['teams'].map { |team| team.keys() } @team_options = $app_report_config['teams'].map { |team| team.keys() }
end end
def index_daily_report def index_daily_report
...@@ -44,8 +44,9 @@ class WorkflowReportController < ApplicationController ...@@ -44,8 +44,9 @@ class WorkflowReportController < ApplicationController
def export def export
team = params[:team] team = params[:team]
project_ids = $workflow_report_config['teams'].select { |hash| hash.key?(team) }[0][team] project_ids = find_project_ids(team)
result = Rails.cache.fetch("#{team}_#{params[:year]}_#{params[:month]}", expires_in: 1.hours) do
result = Rails.cache.fetch("#{team}_#{params[:year]}_#{params[:month]}", expires_in: team == 'All-team' ? 1.hours : 1.days) do
WorkflowReport.build_report(params[:year].to_i, params[:month].to_i, project_ids) WorkflowReport.build_report(params[:year].to_i, params[:month].to_i, project_ids)
end end
...@@ -54,9 +55,31 @@ class WorkflowReportController < ApplicationController ...@@ -54,9 +55,31 @@ class WorkflowReportController < ApplicationController
end end
end end
def index_time_entry
@team_options = $app_report_config['teams'].map { |team| team.keys() }
end
def export_time_entry
team = params[:team]
project_ids = find_project_ids(team)
result = WorkflowReport.build_time_entry_report(project_ids, params[:date_from], params[:date_to])
respond_to do |format|
format.js { render 'build_table_time_entry', locals: { result: result, thead: TIME_ENTRY_HEADER } }
end
end
private private
def require_xhr_request def require_xhr_request
head :unprocessable_entity unless request.xhr? head :unprocessable_entity unless request.xhr?
end end
def find_project_ids(team)
if team == 'All-team'
$app_report_config['teams'].map(&:values).flatten.compact
else
$app_report_config['teams'].find { |hash| hash.key?(team) }[team]
end
end
end end
...@@ -5,11 +5,19 @@ class WorkflowReportIssue < Issue ...@@ -5,11 +5,19 @@ class WorkflowReportIssue < Issue
has_many :workflow_report_custom_values, class_name: 'WorkflowReportCustomValue', foreign_key: 'customized_id' has_many :workflow_report_custom_values, class_name: 'WorkflowReportCustomValue', foreign_key: 'customized_id'
scope :raw_tasks_records, -> (year, month, project_ids) { scope :raw_tasks_records, -> (year, month, project_ids) {
includes(:project, :workflow_report_version, :workflow_report_custom_values, :time_entries, :status, project: :enabled_modules) includes(:time_entries, project: :enabled_modules)
.where.not(projects: { status: CLOSED_STATUS_PROJECT }, issues: { tracker_id: BUG_TRACKER_ID }) .where.not(projects: { status: CLOSED_STATUS_PROJECT }, issues: { tracker_id: BUG_TRACKER_ID })
.where(projects: { id: project_ids }, enabled_modules: { name: 'time_tracking' }) .where(projects: { id: project_ids }, enabled_modules: { name: 'time_tracking' })
.where(['((time_entries.spent_on IS NOT NULL AND time_entries.tyear = ? AND time_entries.tmonth = ?) OR (issues.closed_on BETWEEN ? AND ?))', .where(['((time_entries.spent_on IS NOT NULL AND time_entries.tyear = ? AND time_entries.tmonth = ?) OR (issues.closed_on BETWEEN ? AND ?))',
year, month, DateTime.new(year, month).beginning_of_day, DateTime.new(year, month, -1).end_of_day]) year, month, DateTime.new(year, month).beginning_of_day, DateTime.new(year, month, -1).end_of_day])
.order(:root_id) .order(Arel.sql("FIELD(projects.id, #{project_ids.join(',')})"), :root_id)
}
scope :raw_task_in_date, -> (date_from, date_to, project_ids) {
includes(:project, :time_entries)
.where('time_entries.spent_on BETWEEN ? AND ?', date_from, date_to)
.where(projects: { id: project_ids })
.group('issues.root_id')
.order('projects.name', :root_id)
} }
end end
...@@ -2,3 +2,4 @@ h3 Report ...@@ -2,3 +2,4 @@ h3 Report
ul ul
li = link_to 'Workflow Report', workflow_report_path li = link_to 'Workflow Report', workflow_report_path
li = link_to 'Daily Report', workflow_report_daily_path li = link_to 'Daily Report', workflow_report_daily_path
li = link_to 'Time Entry Report', workflow_report_time_entry_path
- grand_total = result.last&.compact&.sum
h2 = "Total: #{result[0]&.length}"
- if result.present?
table.h-fit[border="1"]
tr.purple
th = "Grand Total (hours): #{grand_total.round(2)}"
th = "Grand Total (days): #{(grand_total / 8).round(2)}"
table[border="1"]
tr.header_table--visible
th.green[colspan="5"] Div Report
tr.header_table--visible.none_colspan
- thead.each_with_index do |head, index|
th.yellow = head
- result[0].each_with_index do |root_id, index|
tr
- (0..(thead.length - 1)).each do |i|
td = result[i][index]
- else
h1.text-center There is no data for the 'time entry report' table.
h2 = "Total: #{result[0]&.length}"
- if result.present? - if result.present?
table[border="1"] table[border="1"]
tr.header_table--visible tr.header_table--visible
...@@ -9,27 +10,35 @@ ...@@ -9,27 +10,35 @@
th.pink[colspan="8"] Github th.pink[colspan="8"] Github
tr.header_table--visible.none_colspan tr.header_table--visible.none_colspan
- thead.each_with_index do |head, index| - thead.each_with_index do |head, index|
- if (0..7).include?(index) - case index
- when 0..2
th[class="root_col_#{index} green"] = head
- when (3..7)
th.green = head th.green = head
- elsif (8..10).include?(index) - when (8..10)
th.red = head th.red = head
- elsif (11..13).include?(index) - when (11..13)
th.blue = head th.blue = head
- elsif (14..20).include?(index) - when (14..20)
th.purple = head th.purple = head
- elsif (21..27).include?(index) - when (21..27)
th.gray = head th.gray = head
- elsif (28..31).include?(index) - when (28..31)
th.yellow = head th.yellow = head
- else - else
th.pink = head th.pink = head
- result[0].each_with_index do |_root_id, index|
- result[0].each_with_index do |root_id, index|
tr tr
- (0..(thead.length - 1)).each do |i| - (0..(thead.length - 1)).each do |i|
- if i == 0 - case i
td.sidebar_visible = result[0][index] - when 0..2
- elsif i == 13 td[class="root_col_#{i}"]
td.break-line = result[i][index] p = result[i][index]
- when 13
td.notes
- notes = result[i][index]
p.break-line.note_est[class="note__est-#{root_id} #{notes.length.zero? ? 'none-over' : 'over_text'}"] = notes
- else - else
td = result[i][index] td = result[i][index]
- else - else
......
$("#data_workflow").html("<%= escape_javascript(render partial: 'table_time_entry', locals: { result: result,thead: thead } ) %>")
<% if result.present? %>
$('button.export-button').removeClass('d-none')
<% else %>
$('button.export-button').addClass('d-none')
<% end %>
= stylesheet_link_tag 'style', plugin: 'workflow_report'
= javascript_include_tag(:application, :plugin => 'workflow_report')
= content_for :sidebar do
= render 'side_content'
fieldset.box.tabular
legend
| EXPORT TIME ENTRY REPORT
= form_tag workflow_report_time_entry_export_path, method: :get, remote: true, id: 'export-form' do
p
= label :date_from, 'Date from'
= date_field_tag :date_from, Date.today - 7.days
p
= label :date_from, 'Date to'
= date_field_tag :date_to, Date.today
p
= label :team, 'Team'
= select_tag :team, options_for_select(@team_options), { prompt: "Select team" }
= submit_tag 'FIND', id: 'export'
button.export-button.d-none Download .CSV
#data_workflow
...@@ -32,7 +32,11 @@ window.addEventListener('load', function () { ...@@ -32,7 +32,11 @@ window.addEventListener('load', function () {
const aTag = document.createElement('a') const aTag = document.createElement('a')
const fileName = $('#year').val() + $('#month').val() + $('#team').val() const fileName = $('#year').val() + $('#month').val() + $('#team').val()
aTag.setAttribute('href', url) aTag.setAttribute('href', url)
if ($('#date_from').val()) {
aTag.setAttribute('download', `${$('#date_from').val()}-${$('#date_to').val()}-${$('#team').val()}.csv`)
} else {
aTag.setAttribute('download', `${$('#year').val()}-${$('#month').val()}-${$('#team').val()}.csv`) aTag.setAttribute('download', `${$('#year').val()}-${$('#month').val()}-${$('#team').val()}.csv`)
}
aTag.click() aTag.click()
} }
...@@ -77,3 +81,10 @@ window.addEventListener('load', function () { ...@@ -77,3 +81,10 @@ window.addEventListener('load', function () {
download(csvContent) download(csvContent)
} }
}) })
$(document).on('click', 'td.notes', function () {
if (!$(this).find('p.note_est').hasClass('none-over')) {
$(this).find('p.note_est').toggleClass('over_text')
$(this).find('p.note_est').toggleClass('w-100')
}
})
#data_workflow > table, #data_workflow > h1 { #data_workflow > table, #data_workflow > h1 {
width: 100%; max-width: 100%;
overflow: auto; overflow: auto;
height: 100vh; height: 100vh;
} }
...@@ -11,34 +11,24 @@ ...@@ -11,34 +11,24 @@
table { table {
display: inline-block; display: inline-block;
table-layout: fixed; table-layout: fixed;
border-collapse: collapse; border-collapse: separate;
} }
.header_table--visible { .header_table--visible {
position: sticky; position: sticky;
top: -1px; top: 0;
z-index: 2; z-index: 2;
height: 30px;
} }
.none_colspan { .none_colspan {
top: 29px; top: 32px;
}
.sidebar_visible {
position: sticky;
background: #dacbcb;
left: 0;
z-index: 1;
} }
.mt-10 { .mt-10 {
margin-top: 10px; margin-top: 10px;
} }
.green {
background-color: #92ce92;
}
.red { .red {
background-color: #e38585; background-color: #e38585;
} }
...@@ -88,3 +78,78 @@ p.error_link { ...@@ -88,3 +78,78 @@ p.error_link {
.break-line { .break-line {
white-space: break-spaces; white-space: break-spaces;
} }
.root_col_0 {
position: sticky;
left: 0;
z-index: 1;
padding: 5px 0;
background: #dacbcb;
}
.root_col_0 > p {
width: 78px;
text-align: center;
}
.root_col_1 {
position: sticky;
left: 80px;
z-index: 1;
padding: 5px 0;
background: #dacbcb;
}
.root_col_1 > p {
width: 120px;
text-align: center;
}
.root_col_2 {
position: sticky;
left: 202px;
z-index: 1;
padding: 5px 8px;
background: #dacbcb;
}
.green {
background: #92ce92;
}
td.notes {
position: relative;
}
.note_est {
width: 140px;
}
.over_text {
cursor: pointer;
overflow: hidden;
display: block;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.over_text::before {
cursor: pointer;
position: absolute;
right: 6px;
top: 8px;
content: '';
display: inline-block;
border: 5px solid transparent;
border-top-color: black;
vertical-align: middle;
}
.w-100 {
width: 100%;
}
.h-fit {
height: fit-content !important;
}
github_token: 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 github_token: your_github_token_here
teams: teams:
- Kyujin: - Kyujin:
- 9 - 9
...@@ -42,3 +42,4 @@ teams: ...@@ -42,3 +42,4 @@ teams:
- 146 - 146
- APW-SUB: - APW-SUB:
- 137 - 137
- All-team:
...@@ -5,3 +5,5 @@ get 'workflow_report', to: 'workflow_report#index' ...@@ -5,3 +5,5 @@ get 'workflow_report', to: 'workflow_report#index'
get 'workflow_report/export', to: 'workflow_report#export' get 'workflow_report/export', to: 'workflow_report#export'
get 'workflow_report/daily', to: 'workflow_report#index_daily_report' get 'workflow_report/daily', to: 'workflow_report#index_daily_report'
get 'workflow_report/daily/export', to: 'workflow_report#show_daily_report' get 'workflow_report/daily/export', to: 'workflow_report#show_daily_report'
get 'workflow_report/time_entry', to: 'workflow_report#index_time_entry'
get 'workflow_report/time_entry/export', to: 'workflow_report#export_time_entry'
...@@ -11,22 +11,16 @@ Redmine::Plugin.register :workflow_report do ...@@ -11,22 +11,16 @@ Redmine::Plugin.register :workflow_report do
version '0.0.1' version '0.0.1'
url 'http://example.com/path/to/plugin' url 'http://example.com/path/to/plugin'
author_url 'http://example.com/about' author_url 'http://example.com/about'
configfile = File.join(File.dirname(__FILE__), 'config', 'settings.yml') configfile = File.join(File.dirname(__FILE__), 'config', 'application.yml')
$workflow_report_config = YAML::load_file(configfile) $app_report_config = YAML::load_file(configfile)
project_module :workflow_report do
permission :workflow_report_view_and_export, { workflow_report: %i[index export index_daily_report show_daily_report
index_time_entry export_time_entry] }, require: :member
end
end end
Redmine::MenuManager.map :top_menu do |menu| Redmine::MenuManager.map :top_menu do |menu|
menu.push(:workflow_report, { controller: 'workflow_report', action: 'index' }, menu.push(:workflow_report, { controller: 'workflow_report', action: 'index' },
caption: 'Workflow Report', caption: 'Workflow Report',
if: proc { User.current.allowed_to_globally?(:view_workflow_report) }) if: proc { User.current.allowed_to_globally?(:workflow_report_view_and_export) })
end
ActionDispatch::Reloader.to_prepare do
Redmine::AccessControl.map do |map|
map.project_module :workflow_report do |pmap|
pmap.permission(:view_workflow_report, {
workflow_report: [:index, :export],
}, read: true)
end
end
end end
...@@ -2,6 +2,7 @@ module WorkflowReport ...@@ -2,6 +2,7 @@ module WorkflowReport
TABLE_HEADER = ['root_id', 'project', 'subject', 'target_version', 'created_on ', 'closed_on ', 'due_date ', 'status ', 'Estimated_hours (*final version)', 'Actual time', 'Diff', 'Estimated_hours (*Initial version)', TABLE_HEADER = ['root_id', 'project', 'subject', 'target_version', 'created_on ', 'closed_on ', 'due_date ', 'status ', 'Estimated_hours (*final version)', 'Actual time', 'Diff', 'Estimated_hours (*Initial version)',
'Number of estimation changes', 'Note of estimation changes', '1. Requirement', '2. Design', '3. Coding', '4. Testing', '5. Bug fixing', '6. Release', 'Others', '1. Requirement', '2. Design', '3. Coding', 'Number of estimation changes', 'Note of estimation changes', '1. Requirement', '2. Design', '3. Coding', '4. Testing', '5. Bug fixing', '6. Release', 'Others', '1. Requirement', '2. Design', '3. Coding',
'4. Testing', '5. Bug fixing', '6. Release', 'Others', 'testcases', 'vn STG bug', 'Jp STG bug', 'Production', 'Issue', 'Issue comment', 'PR comment', 'Review comment', 'Commits', 'File changed', 'Addtion', 'Deletetion'].freeze '4. Testing', '5. Bug fixing', '6. Release', 'Others', 'testcases', 'vn STG bug', 'Jp STG bug', 'Production', 'Issue', 'Issue comment', 'PR comment', 'Review comment', 'Commits', 'File changed', 'Addtion', 'Deletetion'].freeze
TIME_ENTRY_HEADER = ['Site', 'Title', 'JP Request', 'Grand Total']
EST_DETAIL_FIRST_COL = 14 EST_DETAIL_FIRST_COL = 14
ACTUAL_TIME_DETAIL_FIRST_COL = 21 ACTUAL_TIME_DETAIL_FIRST_COL = 21
PR_CMT_COL = 34 PR_CMT_COL = 34
...@@ -12,37 +13,39 @@ module WorkflowReport ...@@ -12,37 +13,39 @@ module WorkflowReport
COL_REQUIREMENT_TO_RELEASE = 6 COL_REQUIREMENT_TO_RELEASE = 6
PROCESS = ['1. Requirement', '2. Design', '3. Coding', '4. Testing', '5. Bug fixing', '6. Release', ''].freeze PROCESS = ['1. Requirement', '2. Design', '3. Coding', '4. Testing', '5. Bug fixing', '6. Release', ''].freeze
BUGS = { testcases: 22, bugs: 23, stg_bugs: 27, prod_bugs: 28 }.freeze BUGS = { testcases: 22, bugs: 23, stg_bugs: 27, prod_bugs: 28 }.freeze
QUANTITY_THREAD = 10
class << self class << self
def build_report(year, month, project_ids) def build_report(year, month, project_ids)
github = Github.new oauth_token: $workflow_report_config['github_token'] github = Github.new oauth_token: $app_report_config['github_token']
error_links = [] error_links = []
github_links = { prs: [], issues: [] } work_queue = Queue.new
result = TABLE_HEADER.length.times.map { [] } result = TABLE_HEADER.length.times.map { [] }
raw_tasks = WorkflowReportIssue.raw_tasks_records(year, month, project_ids) raw_tasks = WorkflowReportIssue.raw_tasks_records(year, month, project_ids)
return { workflow_report: [], error_links: [] } if raw_tasks.empty? return { workflow_report: [], error_links: [] } if raw_tasks.empty?
raw_tasks.group_by(&:root_id).each do |root_id, issues| raw_tasks.group_by(&:root_id).each do |root_id, issues|
issue_ids = issues.map(&:id) arr_issue = WorkflowReportIssue.where(root_id: root_id)
root_issue = WorkflowReportIssue.find(root_id) issue_ids = arr_issue.map(&:id)
root_issue = arr_issue.find(root_id)
journals = WorkflowReportJournal.find_journal_by_issue_ids(issue_ids).to_a journals = WorkflowReportJournal.find_journal_by_issue_ids(issue_ids).to_a
result[0] << root_id result[0] << root_id
result[1] << issues.first.project.name.gsub(/[^[:print:]]/, '') result[1] << issues.first.project.name.gsub(/[^[:print:]]/, '')
result[2] << root_issue&.subject.gsub(/[^[:print:]]/, '') result[2] << root_issue&.subject.gsub(/[^[:print:]]/, '')
result[3] << issues.first.workflow_report_version&.name result[3] << root_issue.workflow_report_version&.name
issue_created_on = issues.min_by { |i| i[:created_on] if i[:created_on].present? } issue_created_on = arr_issue.min_by { |i| i[:created_on] if i[:created_on].present? }
result[4].push(issue_created_on.present? ? issue_created_on.created_on&.strftime('%Y-%m-%d %H:%M:%S') : '') result[4].push(issue_created_on.present? ? issue_created_on.created_on&.strftime('%Y-%m-%d %H:%M:%S') : '')
closed_on_issues = issues.map(&:closed_on).compact closed_on_issues = arr_issue.map(&:closed_on).compact
issue_closed_on = closed_on_issues.max_by { |close_on| close_on } issue_closed_on = closed_on_issues.max_by { |close_on| close_on }
result[5].push(issue_closed_on.present? ? issue_closed_on.strftime('%Y-%m-%d %H:%M:%S') : '') result[5].push(issue_closed_on.present? ? issue_closed_on.strftime('%Y-%m-%d %H:%M:%S') : '')
issue_due_dates = issues.map(&:due_date).compact issue_due_dates = arr_issue.map(&:due_date).compact
issue_due_date = issue_due_dates.max_by { |due_date| due_date } issue_due_date = issue_due_dates.max_by { |due_date| due_date }
result[6].push(issue_due_date.present? ? issue_due_date&.strftime('%Y-%m-%d %H:%M:%S') : '') result[6].push(issue_due_date.present? ? issue_due_date&.strftime('%Y-%m-%d %H:%M:%S') : '')
result[7].push(root_issue.status&.name.present? ? root_issue.status&.name : '') result[7].push(root_issue.status&.name.present? ? root_issue.status&.name : '')
sum_estimated_hours = issues.map(&:estimated_hours).compact.sum sum_estimated_hours = arr_issue.map(&:estimated_hours).compact.sum
result[8] << sum_estimated_hours.round(2) result[8] << sum_estimated_hours.round(2)
actual_time = issues.map { |issue| issue.time_entries.sum(:hours) }.sum actual_time = TimeEntry.where(issue_id: issue_ids).sum(:hours)
if actual_time.present? if actual_time.present?
result[9] << actual_time.round(2) result[9] << actual_time.round(2)
result[10] << (actual_time - sum_estimated_hours).round(2) result[10] << (actual_time - sum_estimated_hours).round(2)
...@@ -61,12 +64,12 @@ module WorkflowReport ...@@ -61,12 +64,12 @@ module WorkflowReport
else else
(11..13).each { |i| result[i] << '' } (11..13).each { |i| result[i] << '' }
end end
issues.each do |issue| arr_issue.each do |issue|
if issue.tracker_id == USER_STORY_TRACKER_ID if issue.tracker_id == USER_STORY_TRACKER_ID
pr_links = issue.custom_values.find_by(custom_field_id: PR_FIELD_ID)&.value pr_links = issue.custom_values.find_by(custom_field_id: PR_FIELD_ID)&.value
jp_request = issue.custom_values.find_by(custom_field_id: JP_REQUEST_FIELD_ID)&.value jp_request = issue.custom_values.find_by(custom_field_id: JP_REQUEST_FIELD_ID)&.value
github_links[:prs].push({ row: result[0].length - 1, links: pr_links, root_id: root_id }) if pr_links.present? work_queue.push([:pr, { row: result[0].length - 1, links: pr_links, root_id: root_id }]) if pr_links.present?
github_links[:issues].push({ row: result[0].length - 1, links: jp_request, root_id: root_id }) if jp_request.present? work_queue.push([:issue, { row: result[0].length - 1, links: jp_request, root_id: root_id }]) if jp_request.present?
end end
process = get_process(issue.subject.gsub(/[^[:print:]]/, '')) process = get_process(issue.subject.gsub(/[^[:print:]]/, ''))
...@@ -99,22 +102,43 @@ module WorkflowReport ...@@ -99,22 +102,43 @@ module WorkflowReport
end end
end end
threads_pr = github_links[:prs].map do |prs| thread_pool = Array.new(QUANTITY_THREAD) do
Thread.new do Thread.new do
find_detail_pr(github, prs[:links], error_links, result, prs[:row], prs[:root_id]) until work_queue.empty?
type, item = work_queue.pop(true) rescue nil
if type == :pr
find_detail_pr(github, item[:links], error_links, result, item[:row], item[:root_id])
elsif type == :issue
find_detail_issue(github, result, error_links, item[:links], item[:row], item[:root_id])
end
end
end end
end end
thread_pool.each(&:join)
threads_issue = github_links[:issues].map do |issues| { workflow_report: result, error_links: error_links }
Thread.new do
find_detail_issue(github, result, error_links, issues[:links], issues[:row], issues[:root_id])
end end
def build_time_entry_report(project_ids, date_from, date_to)
result = TIME_ENTRY_HEADER.length.times.map { [] }
raw_tasks = WorkflowReportIssue.raw_task_in_date(date_from, date_to, project_ids)
return [] if raw_tasks.empty?
raw_tasks.group_by(&:root_id).each do |root_id, issues|
arr_issue = WorkflowReportIssue.where(root_id: root_id)
result[0] << issues.first.project.name
result[1] << "##{root_id}: #{Issue.find(root_id)&.subject}"
result[3] << arr_issue.map { |issue| issue.time_entries.where("time_entries.spent_on BETWEEN ? AND ?", date_from, date_to).sum(:hours) }.sum.round(2)
request = ''
arr_issue.each do |issue|
jp = issue.custom_values.find_by(custom_field_id: JP_REQUEST_FIELD_ID)&.value
request = jp if jp.present?
end end
threads_issue.each(&:join) result[2] << request
threads_pr.each(&:join) end
{ workflow_report: result, error_links: error_links } result
end end
private private
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment