diff --git a/app/builders/agent_builder.rb b/app/builders/agent_builder.rb
index 07b0e7345..54f478920 100644
--- a/app/builders/agent_builder.rb
+++ b/app/builders/agent_builder.rb
@@ -16,7 +16,6 @@ class AgentBuilder
def perform
ActiveRecord::Base.transaction do
@user = find_or_create_user
- send_confirmation_if_required
create_account_user
end
@user
@@ -34,11 +33,6 @@ class AgentBuilder
User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password)
end
- # Sends confirmation instructions if the user is persisted and not confirmed.
- def send_confirmation_if_required
- @user.send_confirmation_instructions if user_needs_confirmation?
- end
-
# Checks if the user needs confirmation.
# @return [Boolean] true if the user is persisted and not confirmed, false otherwise.
def user_needs_confirmation?
diff --git a/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue
new file mode 100644
index 000000000..381ca53b1
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+ {{ slaStatusText }}
+
+
+
+ {{ slaStatus.threshold }}
+
+
+
+
+
diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json
index 227c802d6..2bdf2af7a 100644
--- a/app/javascript/dashboard/i18n/locale/en/conversation.json
+++ b/app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -64,7 +64,14 @@
"SNOOZED_UNTIL": "Snoozed until",
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
- "SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply"
+ "SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply",
+ "SLA_STATUS": {
+ "FRT": "FRT {status}",
+ "NRT": "NRT {status}",
+ "RT": "RT {status}",
+ "BREACH": "breach",
+ "DUE": "due"
+ }
},
"RESOLVE_DROPDOWN": {
"MARK_PENDING": "Mark as pending",
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index 0540696c5..7c6fb5d93 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -78,7 +78,6 @@ Vue.component('multiselect', Multiselect);
Vue.component('woot-switch', WootSwitch);
Vue.component('woot-wizard', WootWizard);
Vue.component('fluent-icon', FluentIcon);
-
Vue.directive('resize', resizeDirective);
const i18nConfig = new VueI18n({
diff --git a/app/models/conversation.rb b/app/models/conversation.rb
index 72518f250..513c993da 100644
--- a/app/models/conversation.rb
+++ b/app/models/conversation.rb
@@ -6,7 +6,7 @@
# additional_attributes :jsonb
# agent_last_seen_at :datetime
# assignee_last_seen_at :datetime
-# cached_label_list :string
+# cached_label_list :text
# contact_last_seen_at :datetime
# custom_attributes :jsonb
# first_reply_created_at :datetime
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4c4f0483c..92c56574d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -106,6 +106,15 @@ en:
avg_resolution_time: Avg resolution time
conversation_traffic_csv:
timezone: Timezone
+ sla_csv:
+ conversation_id: Conversation ID
+ sla_policy_breached: SLA Policy
+ assignee: Assignee
+ team: Team
+ inbox: Inbox
+ labels: Labels
+ conversation_link: Link to the Conversation
+ breached_events: Breached Events
default_group_by: day
csat:
headers:
diff --git a/config/routes.rb b/config/routes.rb
index 6be458e07..d002bd142 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -147,6 +147,7 @@ Rails.application.routes.draw do
resources :applied_slas, only: [:index] do
collection do
get :metrics
+ get :download
end
end
resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy]
diff --git a/db/migrate/20240322071629_convert_cached_label_list_to_text.rb b/db/migrate/20240322071629_convert_cached_label_list_to_text.rb
new file mode 100644
index 000000000..91dfb428a
--- /dev/null
+++ b/db/migrate/20240322071629_convert_cached_label_list_to_text.rb
@@ -0,0 +1,32 @@
+class ConvertCachedLabelListToText < ActiveRecord::Migration[7.0]
+ def up
+ change_column :conversations, :cached_label_list, :text
+ end
+
+ def down
+ # This might cause data loss if the text is longer than 255 characters
+ # lets start by truncating the data to 255 characters
+ Conversation.where('LENGTH(cached_label_list) > 255').find_in_batches do |conversation_batch|
+ Conversation.transaction do
+ conversation_batch.each do |conversation|
+ conversation.update!(cached_label_list: truncate_list(conversation.cached_label_list))
+ end
+ end
+ end
+
+ change_column :conversations, :cached_label_list, :string
+ end
+
+ private
+
+ # Truncate the list to 255 characters or less
+ # by removing the last element until the length is less than 255
+ def truncate_list(label_list)
+ labels = label_list.split(',')
+
+ # we add the `labels.length - 1` to account for the commas
+ labels.pop while (labels.join(',').length + labels.length - 1) > 255
+
+ labels.join(',')
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 498ad13d3..a877bf05d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2024_03_19_062553) do
+ActiveRecord::Schema[7.0].define(version: 2024_03_22_071629) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -472,7 +472,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_03_19_062553) do
t.integer "priority"
t.bigint "sla_policy_id"
t.datetime "waiting_since"
- t.string "cached_label_list"
+ t.text "cached_label_list"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id"
t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx"
diff --git a/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb b/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb
index 5eaaf2d68..4ec27bbbb 100644
--- a/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb
@@ -4,7 +4,7 @@ class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAc
RESULTS_PER_PAGE = 25
- before_action :set_applied_slas, only: [:index, :metrics]
+ before_action :set_applied_slas, only: [:index, :metrics, :download]
before_action :set_current_page, only: [:index]
before_action :paginate_slas, only: [:index]
before_action :check_admin_authorization?
@@ -19,8 +19,22 @@ class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAc
@hit_rate = hit_rate
end
+ def download
+ @breached_slas = breached_slas
+
+ response.headers['Content-Type'] = 'text/csv'
+ response.headers['Content-Disposition'] = 'attachment; filename=breached_conversation.csv'
+ render layout: false, formats: [:csv]
+ end
+
private
+ def breached_slas
+ @applied_slas.includes(:sla_policy).joins(:conversation)
+ .where.not(conversations: { status: :resolved })
+ .where(applied_slas: { sla_status: :missed })
+ end
+
def total_applied_slas
@total_applied_slas ||= @applied_slas.count
end
diff --git a/enterprise/app/views/api/v1/accounts/applied_slas/download.csv.erb b/enterprise/app/views/api/v1/accounts/applied_slas/download.csv.erb
new file mode 100644
index 000000000..676d6d680
--- /dev/null
+++ b/enterprise/app/views/api/v1/accounts/applied_slas/download.csv.erb
@@ -0,0 +1,26 @@
+<% headers = [
+ I18n.t('reports.sla_csv.conversation_id'),
+ I18n.t('reports.sla_csv.sla_policy_breached'),
+ I18n.t('reports.sla_csv.assignee'),
+ I18n.t('reports.sla_csv.team'),
+ I18n.t('reports.sla_csv.inbox'),
+ I18n.t('reports.sla_csv.labels'),
+ I18n.t('reports.sla_csv.conversation_link'),
+ I18n.t('reports.sla_csv.breached_events')
+] %>
+<%= CSV.generate_line headers %>
+
+<% @breached_slas.each do |sla| %>
+ <% breached_events = sla.sla_events.map(&:event_type).join(', ') %>
+ <% conversation = sla.conversation %>
+ <%= CSV.generate_line([
+ conversation.display_id,
+ sla.sla_policy.name,
+ conversation.assignee&.name,
+ conversation.team&.name,
+ conversation.inbox&.name,
+ conversation.cached_label_list,
+ app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id),
+ breached_events
+ ]) %>
+<% end %>
diff --git a/package.json b/package.json
index fcf7e23fa..313821424 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
"libphonenumber-js": "^1.10.24",
"logrocket": "^3.0.1",
"logrocket-vuex": "^0.0.3",
- "markdown-it": "^13.0.1",
+ "markdown-it": "^13.0.2",
"markdown-it-link-attributes": "^4.0.1",
"md5": "^2.3.0",
"ninja-keys": "^1.2.2",
diff --git a/spec/builders/agent_builder_spec.rb b/spec/builders/agent_builder_spec.rb
index 9d7667306..ac8a3229a 100644
--- a/spec/builders/agent_builder_spec.rb
+++ b/spec/builders/agent_builder_spec.rb
@@ -67,21 +67,5 @@ RSpec.describe AgentBuilder, type: :model do
expect(user.encrypted_password).not_to be_empty
end
end
-
- context 'with confirmation required' do
- let(:unconfirmed_user) { create(:user, email: email) }
-
- before do
- unconfirmed_user.confirmed_at = nil
- unconfirmed_user.save(validate: false)
- allow(unconfirmed_user).to receive(:confirmed?).and_return(false)
- end
-
- it 'sends confirmation instructions' do
- user = agent_builder.perform
- expect(user).to receive(:send_confirmation_instructions)
- agent_builder.send(:send_confirmation_if_required)
- end
- end
end
end
diff --git a/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
index e10d08f85..3945c8c93 100644
--- a/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
+++ b/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
@@ -104,6 +104,36 @@ RSpec.describe 'Applied SLAs API', type: :request do
end
end
+ describe 'GET /api/v1/accounts/{account.id}/applied_slas/download' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ get "/api/v1/accounts/#{account.id}/applied_slas/download"
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ it 'returns a CSV file with breached conversations' do
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, sla_status: 'missed')
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, sla_status: 'missed')
+ conversation1.update(status: 'open')
+ conversation2.update(status: 'resolved')
+
+ get "/api/v1/accounts/#{account.id}/applied_slas/download",
+ headers: administrator.create_new_auth_token
+
+ expect(response).to have_http_status(:success)
+ expect(response.headers['Content-Type']).to eq('text/csv')
+ expect(response.headers['Content-Disposition']).to include('attachment; filename=breached_conversation.csv')
+
+ csv_data = CSV.parse(response.body)
+ csv_data.reject! { |row| row.all?(&:nil?) }
+ expect(csv_data.size).to eq(2)
+ expect(csv_data[1][0].to_i).to eq(conversation1.display_id)
+ end
+ end
+ end
+
describe 'GET /api/v1/accounts/{account.id}/applied_slas' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
diff --git a/yarn.lock b/yarn.lock
index 9e919e2f4..71040ce17 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8140,13 +8140,13 @@ bn.js@^5.2.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
-body-parser@1.20.1:
- version "1.20.1"
- resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
- integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
+body-parser@1.20.2:
+ version "1.20.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
+ integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
dependencies:
bytes "3.1.2"
- content-type "~1.0.4"
+ content-type "~1.0.5"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
@@ -8154,7 +8154,7 @@ body-parser@1.20.1:
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.11.0"
- raw-body "2.5.1"
+ raw-body "2.5.2"
type-is "~1.6.18"
unpipe "1.0.0"
@@ -9129,6 +9129,11 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+content-type@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
+ integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
+
convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
@@ -9146,10 +9151,10 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
-cookie@0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
- integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
+cookie@0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
+ integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
copy-concurrently@^1.0.0:
version "1.0.5"
@@ -11054,16 +11059,16 @@ expect@^29.0.0, expect@^29.7.0:
jest-util "^29.7.0"
express@^4.17.1:
- version "4.18.2"
- resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
- integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
+ version "4.19.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
+ integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
- body-parser "1.20.1"
+ body-parser "1.20.2"
content-disposition "0.5.4"
content-type "~1.0.4"
- cookie "0.5.0"
+ cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
@@ -11407,9 +11412,9 @@ flush-write-stream@^1.0.0:
readable-stream "^2.3.6"
follow-redirects@^1.0.0, follow-redirects@^1.15.0:
- version "1.15.3"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
- integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
+ version "1.15.6"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
+ integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
for-each@^0.3.3:
version "0.3.3"
@@ -14551,10 +14556,10 @@ markdown-it@^10.0.0:
mdurl "^1.0.1"
uc.micro "^1.0.5"
-markdown-it@^13.0.1:
- version "13.0.1"
- resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430"
- integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==
+markdown-it@^13.0.2:
+ version "13.0.2"
+ resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.2.tgz#1bc22e23379a6952e5d56217fbed881e0c94d536"
+ integrity sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==
dependencies:
argparse "^2.0.1"
entities "~3.0.1"
@@ -17578,10 +17583,10 @@ range-parser@^1.2.1, range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-raw-body@2.5.1:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
- integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
+raw-body@2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
+ integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"