mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	feat: Improve the rendering of CSV exports (#8914)
Improve the rendering of the datestamp
This commit is contained in:
		@@ -45,12 +45,8 @@ module Api::V2::Accounts::ReportsHelper
 | 
			
		||||
  def generate_readable_report_metrics(report_metric)
 | 
			
		||||
    [
 | 
			
		||||
      report_metric[:conversations_count],
 | 
			
		||||
      time_to_minutes(report_metric[:avg_first_response_time]),
 | 
			
		||||
      time_to_minutes(report_metric[:avg_resolution_time])
 | 
			
		||||
      Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format,
 | 
			
		||||
      Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format
 | 
			
		||||
    ]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def time_to_minutes(time_in_seconds)
 | 
			
		||||
    (time_in_seconds / 60).to_i
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								app/presenters/reports/time_format_presenter.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/presenters/reports/time_format_presenter.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
class Reports::TimeFormatPresenter
 | 
			
		||||
  include ActionView::Helpers::TextHelper
 | 
			
		||||
 | 
			
		||||
  attr_reader :seconds
 | 
			
		||||
 | 
			
		||||
  def initialize(seconds)
 | 
			
		||||
    @seconds = seconds.to_i
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def format
 | 
			
		||||
    return '--' if seconds.nil? || seconds.zero?
 | 
			
		||||
 | 
			
		||||
    days, remainder = seconds.divmod(86_400)
 | 
			
		||||
    hours, remainder = remainder.divmod(3600)
 | 
			
		||||
    minutes, seconds = remainder.divmod(60)
 | 
			
		||||
 | 
			
		||||
    format_components(days: days, hours: hours, minutes: minutes, seconds: seconds)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def format_components(components)
 | 
			
		||||
    formatted_components = components.filter_map do |unit, value|
 | 
			
		||||
      next if value.zero?
 | 
			
		||||
 | 
			
		||||
      I18n.t("time_units.#{unit}", count: value)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return I18n.t('time_units.seconds', count: 0) if formatted_components.empty?
 | 
			
		||||
 | 
			
		||||
    formatted_components.first(2).join(' ')
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
 | 
			
		||||
 | 
			
		||||
<% headers = [
 | 
			
		||||
    I18n.t('reports.agent_csv.agent_name'),
 | 
			
		||||
    I18n.t('reports.agent_csv.conversations_count'),
 | 
			
		||||
@@ -9,4 +11,3 @@
 | 
			
		||||
<% @report_data.each do |row| %>
 | 
			
		||||
<%= CSVSafe.generate_line row -%>
 | 
			
		||||
<% end %>
 | 
			
		||||
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
<%= CSV.generate_line [I18n.t('reports.conversation_traffic_csv.timezone'), @timezone] %>
 | 
			
		||||
 | 
			
		||||
<% @report_data.each do |row| %>
 | 
			
		||||
<%= CSVSafe.generate_line row -%>
 | 
			
		||||
<% end %>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
 | 
			
		||||
 | 
			
		||||
<% headers = [
 | 
			
		||||
    I18n.t('reports.inbox_csv.inbox_name'),
 | 
			
		||||
    I18n.t('reports.inbox_csv.inbox_type'),
 | 
			
		||||
@@ -10,4 +12,3 @@
 | 
			
		||||
<% @report_data.each do |row| %>
 | 
			
		||||
<%= CSVSafe.generate_line row -%>
 | 
			
		||||
<% end %>
 | 
			
		||||
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
 | 
			
		||||
 | 
			
		||||
<% headers = [
 | 
			
		||||
    I18n.t('reports.label_csv.label_title'),
 | 
			
		||||
    I18n.t('reports.label_csv.conversations_count'),
 | 
			
		||||
@@ -9,4 +11,3 @@
 | 
			
		||||
<% @report_data.each do |row| %>
 | 
			
		||||
<%= CSVSafe.generate_line row -%>
 | 
			
		||||
<% end %>
 | 
			
		||||
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
 | 
			
		||||
 | 
			
		||||
<% headers = [
 | 
			
		||||
    I18n.t('reports.team_csv.team_name'),
 | 
			
		||||
    I18n.t('reports.team_csv.conversations_count'),
 | 
			
		||||
@@ -9,4 +11,3 @@
 | 
			
		||||
<% @report_data.each do |row| %>
 | 
			
		||||
<%= CSVSafe.generate_line row -%>
 | 
			
		||||
<% end %>
 | 
			
		||||
<%= CSVSafe.generate_line [I18n.t('reports.period', since: Date.strptime(params[:since], '%s'), until: Date.strptime(params[:until], '%s'))] %>
 | 
			
		||||
 
 | 
			
		||||
@@ -83,25 +83,25 @@ en:
 | 
			
		||||
    utc_warning: The report generated is in UTC timezone
 | 
			
		||||
    agent_csv:
 | 
			
		||||
      agent_name: Agent name
 | 
			
		||||
      conversations_count: Conversations count
 | 
			
		||||
      avg_first_response_time: Avg first response time (Minutes)
 | 
			
		||||
      avg_resolution_time: Avg resolution time (Minutes)
 | 
			
		||||
      conversations_count: Assigned conversations
 | 
			
		||||
      avg_first_response_time: Avg first response time
 | 
			
		||||
      avg_resolution_time: Avg resolution time
 | 
			
		||||
    inbox_csv:
 | 
			
		||||
      inbox_name: Inbox name
 | 
			
		||||
      inbox_type: Inbox type
 | 
			
		||||
      conversations_count: No. of conversations
 | 
			
		||||
      avg_first_response_time: Avg first response time (Minutes)
 | 
			
		||||
      avg_resolution_time: Avg resolution time (Minutes)
 | 
			
		||||
      avg_first_response_time: Avg first response time
 | 
			
		||||
      avg_resolution_time: Avg resolution time
 | 
			
		||||
    label_csv:
 | 
			
		||||
      label_title: Label
 | 
			
		||||
      conversations_count: No. of conversations
 | 
			
		||||
      avg_first_response_time: Avg first response time (Minutes)
 | 
			
		||||
      avg_resolution_time: Avg resolution time (Minutes)
 | 
			
		||||
      avg_first_response_time: Avg first response time
 | 
			
		||||
      avg_resolution_time: Avg resolution time
 | 
			
		||||
    team_csv:
 | 
			
		||||
      team_name: Team name
 | 
			
		||||
      conversations_count: Conversations count
 | 
			
		||||
      avg_first_response_time: Avg first response time (Minutes)
 | 
			
		||||
      avg_resolution_time: Avg resolution time (Minutes)
 | 
			
		||||
      avg_first_response_time: Avg first response time
 | 
			
		||||
      avg_resolution_time: Avg resolution time
 | 
			
		||||
    conversation_traffic_csv:
 | 
			
		||||
      timezone: Timezone
 | 
			
		||||
    default_group_by: day
 | 
			
		||||
@@ -245,3 +245,16 @@ en:
 | 
			
		||||
     inbox_name: Inbox
 | 
			
		||||
     inbox_type: Inbox Type
 | 
			
		||||
    button: Open conversation
 | 
			
		||||
  time_units:
 | 
			
		||||
    days:
 | 
			
		||||
      one: "%{count} day"
 | 
			
		||||
      other: "%{count} days"
 | 
			
		||||
    hours:
 | 
			
		||||
      one: "%{count} hour"
 | 
			
		||||
      other: "%{count} hours"
 | 
			
		||||
    minutes:
 | 
			
		||||
      one: "%{count} minute"
 | 
			
		||||
      other: "%{count} minutes"
 | 
			
		||||
    seconds:
 | 
			
		||||
      one: "%{count} second"
 | 
			
		||||
      other: "%{count} seconds"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								spec/presenters/reports/time_format_presenter_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								spec/presenters/reports/time_format_presenter_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Reports::TimeFormatPresenter do
 | 
			
		||||
  describe '#format' do
 | 
			
		||||
    context 'when formatting days' do
 | 
			
		||||
      it 'formats single day correctly' do
 | 
			
		||||
        expect(described_class.new(86_400).format).to eq '1 day'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'formats multiple days correctly' do
 | 
			
		||||
        expect(described_class.new(172_800).format).to eq '2 days'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'includes seconds with days correctly' do
 | 
			
		||||
        expect(described_class.new(86_401).format).to eq '1 day 1 second'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'includes hours with days correctly' do
 | 
			
		||||
        expect(described_class.new(93_600).format).to eq '1 day 2 hours'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'includes minutes with days correctly' do
 | 
			
		||||
        expect(described_class.new(86_461).format).to eq '1 day 1 minute'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when formatting hours' do
 | 
			
		||||
      it 'formats single hour correctly' do
 | 
			
		||||
        expect(described_class.new(3600).format).to eq '1 hour'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'formats multiple hours correctly' do
 | 
			
		||||
        expect(described_class.new(7200).format).to eq '2 hours'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'includes seconds with hours correctly' do
 | 
			
		||||
        expect(described_class.new(3601).format).to eq '1 hour 1 second'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'includes minutes with hours correctly' do
 | 
			
		||||
        expect(described_class.new(3660).format).to eq '1 hour 1 minute'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when formatting minutes' do
 | 
			
		||||
      it 'formats single minute correctly' do
 | 
			
		||||
        expect(described_class.new(60).format).to eq '1 minute'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'formats multiple minutes correctly' do
 | 
			
		||||
        expect(described_class.new(120).format).to eq '2 minutes'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'includes seconds with minutes correctly' do
 | 
			
		||||
        expect(described_class.new(62).format).to eq '1 minute 2 seconds'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when formatting seconds' do
 | 
			
		||||
      it 'formats multiple seconds correctly' do
 | 
			
		||||
        expect(described_class.new(56).format).to eq '56 seconds'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'handles floating-point seconds by truncating to the nearest lower second' do
 | 
			
		||||
        expect(described_class.new(55.2).format).to eq '55 seconds'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'formats single second correctly' do
 | 
			
		||||
        expect(described_class.new(1).format).to eq '1 second'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
		Reference in New Issue
	
	Block a user