mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +00:00 
			
		
		
		
	feat: better download for conversation traffic heatmap (#6755)
* feat: genearte report in a grid * refactor: update API usage * refactor: separate generate method * refactor: abstract transform_data * feat: annotate with comments * feat: add explicit timezone * feat: download data only in user timezone * fix: dates included in heatmap
This commit is contained in:
		@@ -1,5 +1,7 @@
 | 
				
			|||||||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
 | 
					class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
 | 
				
			||||||
  include Api::V2::Accounts::ReportsHelper
 | 
					  include Api::V2::Accounts::ReportsHelper
 | 
				
			||||||
 | 
					  include Api::V2::Accounts::HeatmapHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before_action :check_authorization
 | 
					  before_action :check_authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def index
 | 
					  def index
 | 
				
			||||||
@@ -34,6 +36,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def conversation_traffic
 | 
					  def conversation_traffic
 | 
				
			||||||
    @report_data = generate_conversations_heatmap_report
 | 
					    @report_data = generate_conversations_heatmap_report
 | 
				
			||||||
 | 
					    timezone_offset = (params[:timezone_offset] || 0).to_f
 | 
				
			||||||
 | 
					    @timezone = ActiveSupport::TimeZone[timezone_offset]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    generate_csv('conversation_traffic_reports', 'api/v2/accounts/reports/conversation_traffic')
 | 
					    generate_csv('conversation_traffic_reports', 'api/v2/accounts/reports/conversation_traffic')
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										103
									
								
								app/helpers/api/v2/accounts/heatmap_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								app/helpers/api/v2/accounts/heatmap_helper.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
				
			|||||||
 | 
					module Api::V2::Accounts::HeatmapHelper
 | 
				
			||||||
 | 
					  def generate_conversations_heatmap_report
 | 
				
			||||||
 | 
					    timezone_data = generate_heatmap_data_for_timezone(params[:timezone_offset])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    group_traffic_data(timezone_data)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def group_traffic_data(data)
 | 
				
			||||||
 | 
					    # start with an empty array
 | 
				
			||||||
 | 
					    result_arr = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # pick all the unique dates from the data in ascending order
 | 
				
			||||||
 | 
					    dates = data.pluck(:date).uniq.sort
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # add the dates as the first row, leave an empty cell for the hour column
 | 
				
			||||||
 | 
					    # e.g. [nil, '2023-01-01', '2023-1-02', '2023-01-03']
 | 
				
			||||||
 | 
					    result_arr << ([nil] + dates)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # group the data by hour, we do not need to sort it, because the data is already sorted
 | 
				
			||||||
 | 
					    # given it starts from the beginning of the day
 | 
				
			||||||
 | 
					    # here each hour is a key, and the value is an array of all the items for that hour at each date
 | 
				
			||||||
 | 
					    # e.g. hour = 1
 | 
				
			||||||
 | 
					    # value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
 | 
				
			||||||
 | 
					    data.group_by { |d| d[:hour] }.each do |hour, items|
 | 
				
			||||||
 | 
					      # create a new row for each hour
 | 
				
			||||||
 | 
					      row = [hour]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # group the items by date, so we can easily access the value for each date
 | 
				
			||||||
 | 
					      # grouped values will be a hasg with the date as the key, and the value as the value
 | 
				
			||||||
 | 
					      # e.g. { '2023-01-01' => [{date: 2023-01-01, value: 1}], '2023-01-02' => [{date: 2023-01-02, value: 1}], ... }
 | 
				
			||||||
 | 
					      grouped_values = items.group_by { |d| d[:date] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # now for each unique date we have, we can access the value for that date and append it to the array
 | 
				
			||||||
 | 
					      dates.each do |date|
 | 
				
			||||||
 | 
					        row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # row will look like [22, 0, 0, 1, 4, 6, 7, 4]
 | 
				
			||||||
 | 
					      # add the row to the result array
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      result_arr << row
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # return the resultant array
 | 
				
			||||||
 | 
					    # the result looks like this
 | 
				
			||||||
 | 
					    # [
 | 
				
			||||||
 | 
					    #   [nil, '2023-01-01', '2023-1-02', '2023-01-03'],
 | 
				
			||||||
 | 
					    #   [0, 0, 0, 0],
 | 
				
			||||||
 | 
					    #   [1, 0, 0, 0],
 | 
				
			||||||
 | 
					    #   [2, 0, 0, 0],
 | 
				
			||||||
 | 
					    #   [3, 0, 0, 0],
 | 
				
			||||||
 | 
					    #   [4, 0, 0, 0],
 | 
				
			||||||
 | 
					    # ]
 | 
				
			||||||
 | 
					    result_arr
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def generate_heatmap_data_for_timezone(offset)
 | 
				
			||||||
 | 
					    timezone = ActiveSupport::TimeZone[offset]&.name
 | 
				
			||||||
 | 
					    timezone_today = DateTime.now.in_time_zone(timezone).beginning_of_day
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    timezone_data_raw = generate_heatmap_data(timezone_today, offset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    transform_data(timezone_data_raw, false)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def generate_heatmap_data(date, offset)
 | 
				
			||||||
 | 
					    report_params = {
 | 
				
			||||||
 | 
					      type: :account,
 | 
				
			||||||
 | 
					      group_by: 'hour',
 | 
				
			||||||
 | 
					      metric: 'conversations_count',
 | 
				
			||||||
 | 
					      business_hours: false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    V2::ReportBuilder.new(Current.account, report_params.merge({
 | 
				
			||||||
 | 
					                                                                 since: since_timestamp(date),
 | 
				
			||||||
 | 
					                                                                 until: until_timestamp(date),
 | 
				
			||||||
 | 
					                                                                 timezone_offset: offset
 | 
				
			||||||
 | 
					                                                               })).build
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def transform_data(data, zone_transform)
 | 
				
			||||||
 | 
					    # rubocop:disable Rails/TimeZone
 | 
				
			||||||
 | 
					    data.map do |d|
 | 
				
			||||||
 | 
					      date = zone_transform ? Time.zone.at(d[:timestamp]) : Time.at(d[:timestamp])
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        date: date.to_date.to_s,
 | 
				
			||||||
 | 
					        hour: date.hour,
 | 
				
			||||||
 | 
					        value: d[:value]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    # rubocop:enable Rails/TimeZone
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def since_timestamp(date)
 | 
				
			||||||
 | 
					    (date - 6.days).to_i.to_s
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def until_timestamp(date)
 | 
				
			||||||
 | 
					    date.to_i.to_s
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -27,27 +27,6 @@ module Api::V2::Accounts::ReportsHelper
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def generate_conversations_heatmap_report
 | 
					 | 
				
			||||||
    report_params = {
 | 
					 | 
				
			||||||
      type: :account,
 | 
					 | 
				
			||||||
      group_by: 'hour',
 | 
					 | 
				
			||||||
      since: params[:since],
 | 
					 | 
				
			||||||
      until: params[:until],
 | 
					 | 
				
			||||||
      metric: 'conversations_count',
 | 
					 | 
				
			||||||
      business_hours: false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    data = V2::ReportBuilder.new(Current.account, report_params).build
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # data format is { timestamp: 1231242342, value: 3}
 | 
					 | 
				
			||||||
    # we need to convert it to { date: "2020-01-01", hour: 12, value: 3}
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # the generated report is **always** in UTC timezone
 | 
					 | 
				
			||||||
    data.map do |d|
 | 
					 | 
				
			||||||
      date = Time.zone.at(d[:timestamp]).to_s
 | 
					 | 
				
			||||||
      [date, d[:value]]
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def generate_report(report_params)
 | 
					  def generate_report(report_params)
 | 
				
			||||||
    V2::ReportBuilder.new(
 | 
					    V2::ReportBuilder.new(
 | 
				
			||||||
      Current.account,
 | 
					      Current.account,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,9 +59,9 @@ class ReportsAPI extends ApiClient {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getConversationTrafficCSV({ from: since, to: until }) {
 | 
					  getConversationTrafficCSV() {
 | 
				
			||||||
    return axios.get(`${this.url}/conversation_traffic`, {
 | 
					    return axios.get(`${this.url}/conversation_traffic`, {
 | 
				
			||||||
      params: { since, until },
 | 
					      params: { timezone_offset: getTimeOffset() },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -142,10 +142,8 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    downloadHeatmapData() {
 | 
					    downloadHeatmapData() {
 | 
				
			||||||
      let to = endOfDay(new Date());
 | 
					      let to = endOfDay(new Date());
 | 
				
			||||||
      let from = startOfDay(subDays(to, 6));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.$store.dispatch('downloadAccountConversationHeatmap', {
 | 
					      this.$store.dispatch('downloadAccountConversationHeatmap', {
 | 
				
			||||||
        from: getUnixTime(from),
 | 
					 | 
				
			||||||
        to: getUnixTime(to),
 | 
					        to: getUnixTime(to),
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -180,7 +180,7 @@ export const actions = {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  downloadAccountConversationHeatmap(_, reportObj) {
 | 
					  downloadAccountConversationHeatmap(_, reportObj) {
 | 
				
			||||||
    Report.getConversationTrafficCSV(reportObj)
 | 
					    Report.getConversationTrafficCSV()
 | 
				
			||||||
      .then(response => {
 | 
					      .then(response => {
 | 
				
			||||||
        downloadCsvFile(
 | 
					        downloadCsvFile(
 | 
				
			||||||
          generateFileName({
 | 
					          generateFileName({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,4 @@
 | 
				
			|||||||
<% headers = [
 | 
					<%= CSVSafe.generate_line [I18n.t('reports.conversation_traffic_csv.timezone'), @timezone] %>
 | 
				
			||||||
    I18n.t('reports.conversation_traffic_csv.date'),
 | 
					 | 
				
			||||||
    I18n.t('reports.conversation_traffic_csv.conversations_count'),
 | 
					 | 
				
			||||||
  ]
 | 
					 | 
				
			||||||
%>
 | 
					 | 
				
			||||||
<%= CSVSafe.generate_line headers -%>
 | 
					 | 
				
			||||||
<% @report_data.each do |row| %>
 | 
					<% @report_data.each do |row| %>
 | 
				
			||||||
<%= CSVSafe.generate_line row -%>
 | 
					<%= CSVSafe.generate_line row -%>
 | 
				
			||||||
<% end %>
 | 
					<% end %>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
 | 
					 | 
				
			||||||
<%= CSVSafe.generate_line [I18n.t('reports.utc_warning')] %>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,8 +96,7 @@ en:
 | 
				
			|||||||
      avg_first_response_time: Avg first response time (Minutes)
 | 
					      avg_first_response_time: Avg first response time (Minutes)
 | 
				
			||||||
      avg_resolution_time: Avg resolution time (Minutes)
 | 
					      avg_resolution_time: Avg resolution time (Minutes)
 | 
				
			||||||
    conversation_traffic_csv:
 | 
					    conversation_traffic_csv:
 | 
				
			||||||
      date: Date and time
 | 
					      timezone: Timezone
 | 
				
			||||||
      conversations_count: No. of conversations
 | 
					 | 
				
			||||||
    default_group_by: day
 | 
					    default_group_by: day
 | 
				
			||||||
    csat:
 | 
					    csat:
 | 
				
			||||||
      headers:
 | 
					      headers:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user