-
- {{ $t('SLA.HEADER_BTN_TXT') }}
-
-
-
-
- {{ $t('SLA.LIST.404') }}
-
-
+
+
+
+
+
+
+
+
+ {{ $t('SLA.LIST.404') }}
+
+
+
-
-
- |
- {{ thHeader }}
- |
-
-
-
- |
-
- {{ sla.name }}
-
- |
- {{ sla.description }} |
-
-
- {{ displayTime(sla.first_response_time_threshold) }}
-
- |
-
-
- {{ displayTime(sla.next_response_time_threshold) }}
-
- |
-
-
- {{ displayTime(sla.resolution_time_threshold) }}
-
- |
-
-
- {{ sla.only_during_business_hours }}
-
- |
-
-
- |
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+ {{
+ hasBusinessHours
+ ? $t('SLA.LIST.BUSINESS_HOURS_ON')
+ : $t('SLA.LIST.BUSINESS_HOURS_OFF')
+ }}
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/components/SLAListItem.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/components/SLAListItem.vue
new file mode 100644
index 000000000..08e66dbfe
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/sla/components/SLAListItem.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/components/SLAListItemLoading.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/components/SLAListItemLoading.vue
new file mode 100644
index 000000000..7521ef9f5
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/sla/components/SLAListItemLoading.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/components/SLAResponseTime.vue b/app/javascript/dashboard/routes/dashboard/settings/sla/components/SLAResponseTime.vue
new file mode 100644
index 000000000..a2caaa4e5
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/sla/components/SLAResponseTime.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+ {{ $t(`SLA.LIST.RESPONSE_TYPES.SHORT_HAND.${responseType}`) }}
+ :
+
+
+ {{ responseTime }}
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js b/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js
index 473cb03cb..a9c68470a 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/sla/sla.routes.js
@@ -1,18 +1,14 @@
import { frontendURL } from '../../../../helper/URLHelper';
-const SettingsContent = () => import('../Wrapper.vue');
+const SettingsWrapper = () => import('../SettingsWrapper.vue');
const Index = () => import('./Index.vue');
export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/sla'),
- component: SettingsContent,
- props: {
- headerTitle: 'SLA.HEADER',
- icon: 'document-list-clock',
- showNewButton: true,
- },
+ component: SettingsWrapper,
+ props: {},
children: [
{
path: '',
diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json
index 7bac92334..91d4104c4 100644
--- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json
+++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json
@@ -2,6 +2,8 @@
"add-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17ZM12 7a.75.75 0 0 1 .75.75v3.5h3.5a.75.75 0 0 1 0 1.5h-3.5v3.5a.75.75 0 0 1-1.5 0v-3.5h-3.5a.75.75 0 0 1 0-1.5h3.5v-3.5A.75.75 0 0 1 12 7Z",
"add-outline": "M11.75 3a.75.75 0 0 1 .743.648l.007.102.001 7.25h7.253a.75.75 0 0 1 .102 1.493l-.102.007h-7.253l.002 7.25a.75.75 0 0 1-1.493.101l-.007-.102-.002-7.249H3.752a.75.75 0 0 1-.102-1.493L3.752 11h7.25L11 3.75a.75.75 0 0 1 .75-.75Z",
"add-solid": "M11.883 3.007 12 3a1 1 0 0 1 .993.883L13 4v7h7a1 1 0 0 1 .993.883L21 12a1 1 0 0 1-.883.993L20 13h-7v7a1 1 0 0 1-.883.993L12 21a1 1 0 0 1-.993-.883L11 20v-7H4a1 1 0 0 1-.993-.883L3 12a1 1 0 0 1 .883-.993L4 11h7V4a1 1 0 0 1 .883-.993L12 3l-.117.007Z",
+ "alarm-on-outline": "m10.95 13.7l-1.425-1.425q-.3-.3-.7-.3t-.7.3q-.3.3-.3.713t.3.712l2.125 2.15q.3.3.7.3t.7-.3l4.25-4.25q.3-.3.3-.712t-.3-.713q-.3-.3-.713-.3t-.712.3L10.95 13.7ZM12 22q-1.875 0-3.512-.713t-2.85-1.924q-1.213-1.213-1.925-2.85T3 13q0-1.875.713-3.513t1.924-2.85q1.213-1.212 2.85-1.924T12 4q1.875 0 3.513.713t2.85 1.925q1.212 1.212 1.925 2.85T21 13q0 1.875-.713 3.513t-1.924 2.85q-1.213 1.212-2.85 1.925T12 22Zm0-9ZM2.05 7.3q-.275-.275-.275-.7t.275-.7L4.9 3.05q.275-.275.7-.275t.7.275q.275.275.275.7t-.275.7L3.45 7.3q-.275.275-.7.275t-.7-.275Zm19.9 0q-.275.275-.7.275t-.7-.275L17.7 4.45q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l2.85 2.85q.275.275.275.7t-.275.7ZM12 20q2.925 0 4.963-2.038T19 13q0-2.925-2.038-4.963T12 6Q9.075 6 7.037 8.038T5 13q0 2.925 2.038 4.963T12 20Z",
+ "alarm-off-outline": "m19.95 17.25l-1.5-1.5q.275-.675.413-1.313T19 13.1q0-2.9-2.05-5T12 6q-.7 0-1.35.113t-1.3.387L7.85 5q.95-.5 1.988-.75T12 4q1.85 0 3.488.7t2.862 1.938q1.225 1.237 1.938 2.887T21 13.1q0 1.125-.275 2.163t-.775 1.987ZM17.7 4.45q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l2.85 2.85q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275L17.7 4.45ZM12 22q-1.85 0-3.488-.7T5.65 19.4q-1.225-1.2-1.938-2.825T3 13.1q0-1.55.463-2.912T4.8 7.7l-.85-.85l-.5.5q-.275.275-.7.275t-.7-.275q-.275-.275-.275-.7t.275-.7l.5-.5L1.4 4.3q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l18.4 18.4q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275l-2.45-2.45q-1.125.825-2.487 1.288T12 22Zm0-1.975q1.05 0 2.05-.325t1.85-.9L6.2 9.15q-.575.875-.887 1.888T5 13.1q0 2.9 2.05 4.913T12 20.025Zm-.95-6.05Zm2.85-2.85Z",
"alert-outline": "M12 1.996a7.49 7.49 0 0 1 7.496 7.25l.004.25v4.097l1.38 3.156a1.25 1.25 0 0 1-1.145 1.75L15 18.502a3 3 0 0 1-5.995.177L9 18.499H4.275a1.251 1.251 0 0 1-1.147-1.747L4.5 13.594V9.496c0-4.155 3.352-7.5 7.5-7.5ZM13.5 18.5l-3 .002a1.5 1.5 0 0 0 2.993.145l.006-.147ZM12 3.496c-3.32 0-6 2.674-6 6v4.41L4.656 17h14.697L18 13.907V9.509l-.004-.225A5.988 5.988 0 0 0 12 3.496Z",
"alternate-email-outline": "M12 22q-2.05 0-3.875-.788t-3.188-2.15q-1.362-1.362-2.15-3.187T2 12q0-2.075.788-3.888t2.15-3.174Q6.3 3.575 8.124 2.788T12 2q2.075 0 3.888.788t3.174 2.15q1.363 1.362 2.15 3.175T22 12v1.45q0 1.475-1.012 2.513T18.5 17q-.9 0-1.675-.4t-1.275-1.05q-.675.675-1.588 1.063T12 17q-2.075 0-3.538-1.463T7 12q0-2.075 1.463-3.538T12 7q2.075 0 3.538 1.463T17 12v1.45q0 .725.45 1.137T18.5 15q.6 0 1.05-.413T20 13.45V12q0-3.275-2.363-5.638T12 4Q8.725 4 6.362 6.363T4 12q0 3.275 2.363 5.638T12 20h5v2h-5Zm0-7q1.25 0 2.125-.875T15 12q0-1.25-.875-2.125T12 9q-1.25 0-2.125.875T9 12q0 1.25.875 2.125T12 15Z",
"archive-outline": "M19.25 3c.966 0 1.75.784 1.75 1.75v2c0 .698-.408 1.3-1 1.581v9.919A3.75 3.75 0 0 1 16.25 22h-8.5A3.75 3.75 0 0 1 4 18.25V8.332A1.75 1.75 0 0 1 3 6.75v-2C3 3.784 3.784 3 4.75 3h14.5Zm-.75 5.5h-13v9.75a2.25 2.25 0 0 0 2.25 2.25h8.5a2.25 2.25 0 0 0 2.25-2.25V8.5Zm-8.5 3h4a.75.75 0 0 1 .102 1.493L14 13h-4a.75.75 0 0 1-.102-1.493L10 11.5h4-4Zm9.25-7H4.75a.25.25 0 0 0-.25.25v2c0 .138.112.25.25.25h14.5a.25.25 0 0 0 .25-.25v-2a.25.25 0 0 0-.25-.25Z",
@@ -121,6 +123,7 @@
"heart-broken-outline": "M10.725 19.85q-2.9-2.55-4.625-4.225T3.463 12.75q-.913-1.2-1.188-2.162T2 8.5q0-2.3 1.6-3.9T7.5 3q1.125 0 2.175.412T11.55 4.6l-1.175 4.125q-.125.5.163.888t.787.387H13l-.65 6.35q-.025.2.163.225t.237-.15L14.6 10.3q.15-.5-.15-.9t-.8-.4H12l1.775-5.3q.625-.35 1.313-.525T16.5 3q2.3 0 3.9 1.6T22 8.5q0 1.125-.288 2.113t-1.2 2.212Q19.6 14.05 17.9 15.713t-4.55 4.137q-.55.475-1.312.475t-1.313-.475ZM4 8.5q0 .75.263 1.475t.937 1.65q.675.925 1.825 2.175t2.925 3.05q.1.1.213.063t.137-.163l.475-4.75q-1.3 0-2.075-1.012T8.275 8.75l.95-3.325q-.4-.2-.838-.313T7.5 5Q6.05 5 5.025 6.025T4 8.5Zm16 0q0-1.45-1.025-2.475T16.5 5q-.275 0-.55.038t-.55.137L14.8 7q1.125.3 1.662 1.338t.188 2.162l-1.3 4.275q-.05.125.088.213t.262-.038q1.375-1.4 2.225-2.325t1.313-1.613q.462-.687.612-1.25T20 8.5Zm-3.35 2ZM8.275 8.75Z",
"image-outline": "M17.75 3A3.25 3.25 0 0 1 21 6.25v11.5A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h11.5Zm.58 16.401-5.805-5.686a.75.75 0 0 0-.966-.071l-.084.07-5.807 5.687c.182.064.378.099.582.099h11.5c.203 0 .399-.035.58-.099l-5.805-5.686L18.33 19.4ZM17.75 4.5H6.25A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .208.036.408.103.594l5.823-5.701a2.25 2.25 0 0 1 3.02-.116l.128.116 5.822 5.702c.067-.186.104-.386.104-.595V6.25a1.75 1.75 0 0 0-1.75-1.75Zm-2.498 2a2.252 2.252 0 1 1 0 4.504 2.252 2.252 0 0 1 0-4.504Zm0 1.5a.752.752 0 1 0 0 1.504.752.752 0 0 0 0-1.504Z",
"info-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5Zm-.004 7a.75.75 0 0 1 .744.648l.007.102.003 5.502a.75.75 0 0 1-1.493.102l-.007-.101-.003-5.502a.75.75 0 0 1 .75-.75ZM12 7.003a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997Z",
+ "information-outline": "M12 17q.425 0 .713-.288T13 16v-4q0-.425-.288-.713T12 11q-.425 0-.713.288T11 12v4q0 .425.288.713T12 17Zm0-8q.425 0 .713-.288T13 8q0-.425-.288-.713T12 7q-.425 0-.713.288T11 8q0 .425.288.713T12 9Zm0 13q-2.075 0-3.9-.788t-3.175-2.137q-1.35-1.35-2.137-3.175T2 12q0-2.075.788-3.9t2.137-3.175q1.35-1.35 3.175-2.137T12 2q2.075 0 3.9.788t3.175 2.137q1.35 1.35 2.138 3.175T22 12q0 2.075-.788 3.9t-2.137 3.175q-1.35 1.35-3.175 2.138T12 22Zm0-2q3.35 0 5.675-2.325T20 12q0-3.35-2.325-5.675T12 4Q8.65 4 6.325 6.325T4 12q0 3.35 2.325 5.675T12 20Zm0-8Z",
"key-outline": "M15 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0Zm-2.5-4C9.424 2 7 4.424 7 7.5c0 .397.04.796.122 1.175c.058.27-.008.504-.142.638l-4.54 4.54A1.5 1.5 0 0 0 2 14.915V16.5A1.5 1.5 0 0 0 3.5 18h2A1.5 1.5 0 0 0 7 16.5V16h1a1 1 0 0 0 1-1v-1h1a1 1 0 0 0 1-1v-.18c.493.134 1.007.18 1.5.18c3.076 0 5.5-2.424 5.5-5.5S15.576 2 12.5 2ZM8 7.5C8 4.976 9.976 3 12.5 3S17 4.976 17 7.5S15.024 12 12.5 12c-.66 0-1.273-.095-1.776-.347A.5.5 0 0 0 10 12.1v.9H9a1 1 0 0 0-1 1v1H7a1 1 0 0 0-1 1v.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-1.586a.5.5 0 0 1 .146-.353l4.541-4.541c.432-.432.522-1.044.412-1.556A4.619 4.619 0 0 1 8 7.5Z",
"keyboard-outline": "M19.745 5a2.25 2.25 0 0 1 2.25 2.25v9.505a2.25 2.25 0 0 1-2.25 2.25H4.25A2.25 2.25 0 0 1 2 16.755V7.25A2.25 2.25 0 0 1 4.25 5h15.495Zm0 1.5H4.25a.75.75 0 0 0-.75.75v9.505c0 .414.336.75.75.75h15.495a.75.75 0 0 0 .75-.75V7.25a.75.75 0 0 0-.75-.75Zm-12.995 8h10.5a.75.75 0 0 1 .102 1.493L17.25 16H6.75a.75.75 0 0 1-.102-1.493l.102-.007h10.5-10.5ZM16.5 11a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm-5.995 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm-3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm6 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2ZM6 8a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm2.995 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z",
"keyboard-double-arrow-right-outline": "M5.7 17.3q-.275-.275-.275-.7t.275-.7L9.575 12L5.7 8.1q-.275-.275-.288-.687T5.7 6.7q.275-.275.7-.275t.7.275l4.6 4.6q.15.15.213.325t.062.375q0 .2-.062.375t-.213.325l-4.6 4.6q-.275.275-.687.288T5.7 17.3Zm6.6 0q-.275-.275-.275-.7t.275-.7l3.875-3.9L12.3 8.1q-.275-.275-.288-.687T12.3 6.7q.275-.275.7-.275t.7.275l4.6 4.6q.15.15.213.325t.062.375q0 .2-.063.375t-.212.325l-4.6 4.6q-.275.275-.687.288T12.3 17.3Z",
diff --git a/app/models/contact.rb b/app/models/contact.rb
index a60e9f4d2..95ee69c75 100644
--- a/app/models/contact.rb
+++ b/app/models/contact.rb
@@ -169,7 +169,7 @@ class Contact < ApplicationRecord
end
def self.from_email(email)
- find_by(email: email.downcase)
+ find_by(email: email&.downcase)
end
private
diff --git a/app/models/user.rb b/app/models/user.rb
index 3fdaf7f25..faadb3271 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -157,7 +157,7 @@ class User < ApplicationRecord
end
def self.from_email(email)
- find_by(email: email.downcase)
+ find_by(email: email&.downcase)
end
private
diff --git a/app/views/layouts/vueapp.html.erb b/app/views/layouts/vueapp.html.erb
index 2e49a7b96..a80f23cce 100644
--- a/app/views/layouts/vueapp.html.erb
+++ b/app/views/layouts/vueapp.html.erb
@@ -44,6 +44,7 @@
vapidPublicKey: new Uint8Array(<%= Base64.urlsafe_decode64(@global_config['VAPID_PUBLIC_KEY']).bytes %>),
<% end %>
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
+ helpUrls: <%= feature_help_urls.to_json.html_safe %>,
selectedLocale: '<%= I18n.locale %>'
}
window.globalConfig = <%= raw @global_config.to_json %>
diff --git a/config/features.yml b/config/features.yml
index f0477c297..37439c31f 100644
--- a/config/features.yml
+++ b/config/features.yml
@@ -3,37 +3,45 @@
enabled: true
- name: channel_email
enabled: true
+ help_url: https://chwt.app/hc/email
- name: channel_facebook
enabled: true
+ help_url: https://chwt.app/hc/fb
- name: channel_twitter
enabled: true
- name: ip_lookup
enabled: false
- name: disable_branding
enabled: false
- premium: true
+ premium: true
- name: email_continuity_on_api_channel
enabled: false
- name: help_center
enabled: true
+ help_url: https://chwt.app/hc/help-center
- name: agent_bots
enabled: false
+ help_url: https://chwt.app/hc/agent-bots
- name: macros
enabled: true
- name: agent_management
enabled: true
- name: team_management
enabled: true
+ help_url: https://chwt.app/hc/teams
- name: inbox_management
enabled: true
- name: labels
enabled: true
+ help_url: https://chwt.app/hc/labels
- name: custom_attributes
enabled: true
+ help_url: https://chwt.app/hc/custom-attributes
- name: automations
enabled: true
- name: canned_responses
enabled: true
+ help_url: https://chwt.app/hc/canned
- name: integrations
enabled: true
- name: voice_recorder
@@ -44,8 +52,10 @@
enabled: true
- name: campaigns
enabled: true
+ help_url: https://chwt.app/hc/campaigns
- name: reports
enabled: true
+ help_url: https://chwt.app/hc/reports
- name: crm
enabled: true
- name: auto_resolve_conversations
@@ -62,6 +72,7 @@
premium: true
- name: message_reply_to
enabled: false
+ help_url: https://chwt.app/hc/reply-to
- name: insert_article_in_reply
enabled: false
- name: inbox_view
diff --git a/config/routes.rb b/config/routes.rb
index cfa14c854..6be458e07 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -144,6 +144,11 @@ Rails.application.routes.draw do
get :download
end
end
+ resources :applied_slas, only: [:index] do
+ collection do
+ get :metrics
+ end
+ end
resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy]
resources :custom_filters, only: [:index, :show, :create, :update, :destroy]
resources :inboxes, only: [:index, :show, :create, :update, :destroy] do
diff --git a/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb b/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb
new file mode 100644
index 000000000..5eaaf2d68
--- /dev/null
+++ b/enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb
@@ -0,0 +1,58 @@
+class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAccountsController
+ include Sift
+ include DateRangeHelper
+
+ RESULTS_PER_PAGE = 25
+
+ before_action :set_applied_slas, only: [:index, :metrics]
+ before_action :set_current_page, only: [:index]
+ before_action :paginate_slas, only: [:index]
+ before_action :check_admin_authorization?
+
+ sort_on :created_at, type: :datetime
+
+ def index; end
+
+ def metrics
+ @total_applied_slas = total_applied_slas
+ @number_of_sla_breaches = number_of_sla_breaches
+ @hit_rate = hit_rate
+ end
+
+ private
+
+ def total_applied_slas
+ @total_applied_slas ||= @applied_slas.count
+ end
+
+ def number_of_sla_breaches
+ @number_of_sla_breaches ||= @applied_slas.missed.count
+ end
+
+ def hit_rate
+ number_of_sla_breaches.zero? ? '100%' : "#{hit_rate_percentage}%"
+ end
+
+ def hit_rate_percentage
+ ((total_applied_slas - number_of_sla_breaches) / total_applied_slas.to_f * 100).round(2)
+ end
+
+ def set_applied_slas
+ initial_query = Current.account.applied_slas.includes(:conversation)
+ @applied_slas = initial_query
+ .filter_by_date_range(range)
+ .filter_by_inbox_id(params[:inbox_id])
+ .filter_by_team_id(params[:team_id])
+ .filter_by_sla_policy_id(params[:sla_policy_id])
+ .filter_by_label_list(params[:label_list])
+ .filter_by_assigned_agent_id(params[:assigned_agent_id])
+ end
+
+ def paginate_slas
+ @applied_slas = @applied_slas.page(@current_page).per(RESULTS_PER_PAGE)
+ end
+
+ def set_current_page
+ @current_page = params[:page] || 1
+ end
+end
diff --git a/enterprise/app/models/applied_sla.rb b/enterprise/app/models/applied_sla.rb
index c2dde8377..4558d5d23 100644
--- a/enterprise/app/models/applied_sla.rb
+++ b/enterprise/app/models/applied_sla.rb
@@ -29,6 +29,17 @@ class AppliedSla < ApplicationRecord
enum sla_status: { active: 0, hit: 1, missed: 2 }
+ scope :filter_by_date_range, ->(range) { where(created_at: range) if range.present? }
+ scope :filter_by_inbox_id, ->(inbox_id) { where(inbox_id: inbox_id) if inbox_id.present? }
+ scope :filter_by_team_id, ->(team_id) { where(team_id: team_id) if team_id.present? }
+ scope :filter_by_sla_policy_id, ->(sla_policy_id) { where(sla_policy_id: sla_policy_id) if sla_policy_id.present? }
+ scope :filter_by_label_list, ->(label_list) { joins(:conversation).where(conversations: { cached_label_list: label_list }) if label_list.present? }
+ scope :filter_by_assigned_agent_id, lambda { |assigned_agent_id|
+ if assigned_agent_id.present?
+ joins(:conversation).where(conversations: { assigned_agent_id: assigned_agent_id })
+ end
+ }
+ scope :missed, -> { where(sla_status: :missed) }
private
def ensure_account_id
diff --git a/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder b/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder
new file mode 100644
index 000000000..e9a905d56
--- /dev/null
+++ b/enterprise/app/views/api/v1/accounts/applied_slas/index.json.jbuilder
@@ -0,0 +1,14 @@
+json.array! @applied_slas do |applied_sla|
+ json.id applied_sla.id
+ json.sla_policy_id applied_sla.sla_policy_id
+ json.conversation_id applied_sla.conversation_id
+ json.sla_status applied_sla.sla_status
+ json.created_at applied_sla.created_at
+ json.updated_at applied_sla.updated_at
+ json.conversation do
+ json.partial! 'api/v1/models/conversation', conversation: applied_sla.conversation
+ end
+ json.sla_events applied_sla.sla_events do |sla_event|
+ json.partial! 'api/v1/models/sla_event', formats: [:json], sla_event: sla_event
+ end
+end
diff --git a/enterprise/app/views/api/v1/accounts/applied_slas/metrics.json.jbuilder b/enterprise/app/views/api/v1/accounts/applied_slas/metrics.json.jbuilder
new file mode 100644
index 000000000..13f184845
--- /dev/null
+++ b/enterprise/app/views/api/v1/accounts/applied_slas/metrics.json.jbuilder
@@ -0,0 +1,3 @@
+json.total_applied_slas @total_applied_slas
+json.number_of_sla_breaches @number_of_sla_breaches
+json.hit_rate @hit_rate
diff --git a/enterprise/app/views/api/v1/models/_sla_event.json.jbuilder b/enterprise/app/views/api/v1/models/_sla_event.json.jbuilder
new file mode 100644
index 000000000..e51defc0a
--- /dev/null
+++ b/enterprise/app/views/api/v1/models/_sla_event.json.jbuilder
@@ -0,0 +1,5 @@
+json.id sla_event.id
+json.event_type sla_event.event_type
+json.meta sla_event.meta
+json.updated_at sla_event.updated_at.to_i
+json.created_at sla_event.created_at.to_i
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
new file mode 100644
index 000000000..e10d08f85
--- /dev/null
+++ b/spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
@@ -0,0 +1,188 @@
+require 'rails_helper'
+
+RSpec.describe 'Applied SLAs API', type: :request do
+ let(:account) { create(:account) }
+ let(:administrator) { create(:user, account: account, role: :administrator) }
+ let(:agent1) { create(:user, account: account, role: :agent) }
+ let(:agent2) { create(:user, account: account, role: :agent) }
+ let(:conversation1) { create(:conversation, account: account, assignee: agent1) }
+ let(:conversation2) { create(:conversation, account: account, assignee: agent2) }
+ let(:conversation3) { create(:conversation, account: account, assignee: agent2) }
+ let(:sla_policy1) { create(:sla_policy, account: account) }
+ let(:sla_policy2) { create(:sla_policy, account: account) }
+
+ before do
+ AppliedSla.destroy_all
+ end
+
+ describe 'GET /api/v1/accounts/{account.id}/applied_slas/metrics' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ get "/api/v1/accounts/#{account.id}/applied_slas/metrics"
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ it 'returns the sla metrics' do
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, sla_status: 'missed')
+
+ get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body).to include('total_applied_slas' => 1)
+ expect(body).to include('number_of_sla_breaches' => 1)
+ expect(body).to include('hit_rate' => '0.0%')
+ end
+
+ it 'filters sla metrics based on a date range' do
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago)
+
+ get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
+ params: { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s },
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body).to include('total_applied_slas' => 1)
+ expect(body).to include('number_of_sla_breaches' => 0)
+ expect(body).to include('hit_rate' => '100%')
+ end
+
+ it 'filters sla metrics based on a date range and agent ids' do
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed')
+
+ get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
+ params: { agent_ids: [agent2.id] },
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body).to include('total_applied_slas' => 3)
+ expect(body).to include('number_of_sla_breaches' => 1)
+ expect(body).to include('hit_rate' => '66.67%')
+ end
+
+ it 'filters sla metrics based on sla policy ids' do
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, sla_status: 'missed')
+ create(:applied_sla, sla_policy: sla_policy2, conversation: conversation2, sla_status: 'missed')
+
+ get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
+ params: { sla_policy_id: sla_policy1.id },
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body).to include('total_applied_slas' => 2)
+ expect(body).to include('number_of_sla_breaches' => 1)
+ expect(body).to include('hit_rate' => '50.0%')
+ end
+
+ it 'filters sla metrics based on labels' do
+ conversation2.update_labels('label1')
+ conversation3.update_labels('label1')
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed')
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago)
+
+ get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
+ params: { label_list: ['label1'] },
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body).to include('total_applied_slas' => 2)
+ expect(body).to include('number_of_sla_breaches' => 1)
+ expect(body).to include('hit_rate' => '50.0%')
+ 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
+ get "/api/v1/accounts/#{account.id}/applied_slas"
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ it 'returns the applied slas' do
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2)
+ get "/api/v1/accounts/#{account.id}/applied_slas",
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body.size).to eq(2)
+ expect(body.first).to include('id')
+ expect(body.first).to include('sla_policy_id' => sla_policy1.id)
+ expect(body.first).to include('conversation_id' => conversation1.id)
+ end
+
+ it 'filters applied slas based on a date range' do
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago)
+
+ get "/api/v1/accounts/#{account.id}/applied_slas",
+ params: { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s },
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body.size).to eq(1)
+ end
+
+ it 'filters applied slas based on a date range and agent ids' do
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago)
+
+ get "/api/v1/accounts/#{account.id}/applied_slas",
+ params: { agent_ids: [agent2.id] },
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body.size).to eq(3)
+ end
+
+ it 'filters applied slas based on sla policy ids' do
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2)
+ create(:applied_sla, sla_policy: sla_policy2, conversation: conversation2)
+
+ get "/api/v1/accounts/#{account.id}/applied_slas",
+ params: { sla_policy_id: sla_policy1.id },
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body.size).to eq(2)
+ end
+
+ it 'filters applied slas based on labels' do
+ conversation2.update_labels('label1')
+ conversation3.update_labels('label1')
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago)
+ create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago)
+
+ get "/api/v1/accounts/#{account.id}/applied_slas",
+ params: { label_list: ['label1'] },
+ headers: administrator.create_new_auth_token
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body)
+
+ expect(body.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/tailwind.config.js b/tailwind.config.js
index aa7a2a14a..a34650fb5 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -26,6 +26,11 @@ module.exports = {
'./app/views/**/*.html.erb',
],
theme: {
+ extend: {
+ fontFamily: {
+ inter: ['Inter', ...defaultTheme.fontFamily.sans],
+ },
+ },
fontSize: {
...defaultTheme.fontSize,
xxs: '0.625rem',