+ {{ $t('INBOX_MGMT.CSAT.MESSAGE_PREVIEW.LABEL') }}
+
- {{ $t('INBOX_MGMT.CSAT.NOTE') }} + {{ + isWhatsAppChannel + ? $t('INBOX_MGMT.CSAT.WHATSAPP_NOTE') + : $t('INBOX_MGMT.CSAT.NOTE') + }}
{{ section.title }}
+- {{ category }} -
+ {{ truncatedContent }} +- {{ truncatedContent }} -
-- - {{ email }} - - • - {{ phone }} -
-{{ $t('SEARCH.EMPTY_STATE', { item: titleCase, query }) }}
diff --git a/app/javascript/dashboard/modules/search/components/SearchTabs.vue b/app/javascript/dashboard/modules/search/components/SearchTabs.vue index 9260796ef..86e40df57 100644 --- a/app/javascript/dashboard/modules/search/components/SearchTabs.vue +++ b/app/javascript/dashboard/modules/search/components/SearchTabs.vue @@ -1,5 +1,8 @@ -
- Sourced from httparty's
releases. Full Changelog: https://github.com/jnunemaker/httparty/compare/v0.23.2...v0.24.0 Full Changelog: https://github.com/jnunemaker/httparty/compare/v0.23.1...v0.23.2 Full Changelog: https://github.com/jnunemaker/httparty/compare/v0.23.0...v0.23.1 Full Changelog: https://github.com/jnunemaker/httparty/compare/v0.22.0...v0.23.0 ... (truncated) Sourced from httparty's
changelog. All notable changes since
0.22 are documented in GitHub Releases. {{ description }} {{ modelDescription }} {{ description }}
{{
- isWhatsAppChannel
+ isAnyWhatsAppChannel
? $t('INBOX_MGMT.CSAT.WHATSAPP_NOTE')
: $t('INBOX_MGMT.CSAT.NOTE')
}}
diff --git a/app/models/inbox.rb b/app/models/inbox.rb
index 94b90fa2a..0a26462ad 100644
--- a/app/models/inbox.rb
+++ b/app/models/inbox.rb
@@ -158,6 +158,10 @@ class Inbox < ApplicationRecord
channel_type == 'Channel::Whatsapp'
end
+ def twilio_whatsapp?
+ channel_type == 'Channel::TwilioSms' && channel.medium == 'whatsapp'
+ end
+
def assignable_agents
(account.users.where(id: members.select(:user_id)) + account.administrators).uniq
end
diff --git a/app/services/csat_survey_service.rb b/app/services/csat_survey_service.rb
index 9afddc6d3..6c8a288b8 100644
--- a/app/services/csat_survey_service.rb
+++ b/app/services/csat_survey_service.rb
@@ -6,6 +6,8 @@ class CsatSurveyService
if whatsapp_channel? && template_available_and_approved?
send_whatsapp_template_survey
+ elsif inbox.twilio_whatsapp? && twilio_template_available_and_approved?
+ send_twilio_whatsapp_template_survey
elsif within_messaging_window?
::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform
else
@@ -45,7 +47,7 @@ class CsatSurveyService
template_config = inbox.csat_config&.dig('template')
return false unless template_config
- template_name = template_config['name'] || Whatsapp::CsatTemplateNameService.csat_template_name(inbox.id)
+ template_name = template_config['name'] || CsatTemplateNameService.csat_template_name(inbox.id)
status_result = inbox.channel.provider_service.get_template_status(template_name)
@@ -55,9 +57,25 @@ class CsatSurveyService
false
end
+ def twilio_template_available_and_approved?
+ template_config = inbox.csat_config&.dig('template')
+ return false unless template_config
+
+ content_sid = template_config['content_sid']
+ return false unless content_sid
+
+ template_service = Twilio::CsatTemplateService.new(inbox.channel)
+ status_result = template_service.get_template_status(content_sid)
+
+ status_result[:success] && status_result[:template][:status] == 'approved'
+ rescue StandardError => e
+ Rails.logger.error "Error checking Twilio CSAT template status: #{e.message}"
+ false
+ end
+
def send_whatsapp_template_survey
template_config = inbox.csat_config&.dig('template')
- template_name = template_config['name'] || Whatsapp::CsatTemplateNameService.csat_template_name(inbox.id)
+ template_name = template_config['name'] || CsatTemplateNameService.csat_template_name(inbox.id)
phone_number = conversation.contact_inbox.source_id
template_info = build_template_info(template_name, template_config)
@@ -95,6 +113,26 @@ class CsatSurveyService
)
end
+ def send_twilio_whatsapp_template_survey
+ template_config = inbox.csat_config&.dig('template')
+ content_sid = template_config['content_sid']
+
+ phone_number = conversation.contact_inbox.source_id
+ content_variables = { '1' => conversation.uuid }
+ message = build_csat_message
+
+ send_service = Twilio::SendOnTwilioService.new(message: message)
+ result = send_service.send_csat_template_message(
+ phone_number: phone_number,
+ content_sid: content_sid,
+ content_variables: content_variables
+ )
+
+ message.update!(source_id: result[:message_id]) if result[:success] && result[:message_id].present?
+ rescue StandardError => e
+ Rails.logger.error "Error sending Twilio WhatsApp CSAT template for conversation #{conversation.id}: #{e.message}"
+ end
+
def create_csat_not_sent_activity_message
content = I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window')
activity_message_params = {
diff --git a/app/services/csat_template_management_service.rb b/app/services/csat_template_management_service.rb
new file mode 100644
index 000000000..064804082
--- /dev/null
+++ b/app/services/csat_template_management_service.rb
@@ -0,0 +1,196 @@
+class CsatTemplateManagementService
+ DEFAULT_BUTTON_TEXT = 'Please rate us'.freeze
+ DEFAULT_LANGUAGE = 'en'.freeze
+
+ def initialize(inbox)
+ @inbox = inbox
+ end
+
+ def template_status
+ template = @inbox.csat_config&.dig('template')
+ return { template_exists: false } unless template
+
+ if @inbox.twilio_whatsapp?
+ get_twilio_template_status(template)
+ else
+ get_whatsapp_template_status(template)
+ end
+ rescue StandardError => e
+ Rails.logger.error "Error fetching CSAT template status: #{e.message}"
+ { service_error: e.message }
+ end
+
+ def create_template(template_params)
+ validate_template_params!(template_params)
+
+ delete_existing_template_if_needed
+
+ result = create_template_via_provider(template_params)
+ update_inbox_csat_config(result) if result[:success]
+
+ result
+ rescue StandardError => e
+ Rails.logger.error "Error creating CSAT template: #{e.message}"
+ { success: false, service_error: 'Template creation failed' }
+ end
+
+ private
+
+ def validate_template_params!(template_params)
+ raise ActionController::ParameterMissing, 'message' if template_params[:message].blank?
+ end
+
+ def create_template_via_provider(template_params)
+ if @inbox.twilio_whatsapp?
+ create_twilio_template(template_params)
+ else
+ create_whatsapp_template(template_params)
+ end
+ end
+
+ def create_twilio_template(template_params)
+ template_config = build_template_config(template_params)
+ template_service = Twilio::CsatTemplateService.new(@inbox.channel)
+ template_service.create_template(template_config)
+ end
+
+ def create_whatsapp_template(template_params)
+ template_config = build_template_config(template_params)
+ Whatsapp::CsatTemplateService.new(@inbox.channel).create_template(template_config)
+ end
+
+ def build_template_config(template_params)
+ {
+ message: template_params[:message],
+ button_text: template_params[:button_text] || DEFAULT_BUTTON_TEXT,
+ base_url: ENV.fetch('FRONTEND_URL', 'http://localhost:3000'),
+ language: template_params[:language] || DEFAULT_LANGUAGE,
+ template_name: CsatTemplateNameService.csat_template_name(@inbox.id)
+ }
+ end
+
+ def update_inbox_csat_config(result)
+ current_config = @inbox.csat_config || {}
+ template_data = build_template_data_from_result(result)
+ updated_config = current_config.merge('template' => template_data)
+ @inbox.update!(csat_config: updated_config)
+ end
+
+ def build_template_data_from_result(result)
+ if @inbox.twilio_whatsapp?
+ build_twilio_template_data(result)
+ else
+ build_whatsapp_cloud_template_data(result)
+ end
+ end
+
+ def build_twilio_template_data(result)
+ {
+ 'friendly_name' => result[:friendly_name],
+ 'content_sid' => result[:content_sid],
+ 'approval_sid' => result[:approval_sid],
+ 'language' => result[:language],
+ 'status' => result[:whatsapp_status] || result[:status],
+ 'created_at' => Time.current.iso8601
+ }.compact
+ end
+
+ def build_whatsapp_cloud_template_data(result)
+ {
+ 'name' => result[:template_name],
+ 'template_id' => result[:template_id],
+ 'language' => result[:language],
+ 'created_at' => Time.current.iso8601
+ }
+ end
+
+ def get_twilio_template_status(template)
+ content_sid = template['content_sid']
+ return { template_exists: false } unless content_sid
+
+ template_service = Twilio::CsatTemplateService.new(@inbox.channel)
+ status_result = template_service.get_template_status(content_sid)
+
+ if status_result[:success]
+ {
+ template_exists: true,
+ friendly_name: template['friendly_name'],
+ content_sid: template['content_sid'],
+ status: status_result[:template][:status],
+ language: template['language']
+ }
+ else
+ {
+ template_exists: false,
+ error: 'Template not found'
+ }
+ end
+ end
+
+ def get_whatsapp_template_status(template)
+ template_name = template['name'] || CsatTemplateNameService.csat_template_name(@inbox.id)
+ status_result = Whatsapp::CsatTemplateService.new(@inbox.channel).get_template_status(template_name)
+
+ if status_result[:success]
+ {
+ template_exists: true,
+ template_name: template_name,
+ status: status_result[:template][:status],
+ template_id: status_result[:template][:id]
+ }
+ else
+ {
+ template_exists: false,
+ error: 'Template not found'
+ }
+ end
+ end
+
+ def delete_existing_template_if_needed
+ template = @inbox.csat_config&.dig('template')
+ return true if template.blank?
+
+ if @inbox.twilio_whatsapp?
+ delete_existing_twilio_template(template)
+ else
+ delete_existing_whatsapp_template(template)
+ end
+ rescue StandardError => e
+ Rails.logger.error "Error during template deletion for inbox #{@inbox.id}: #{e.message}"
+ false
+ end
+
+ def delete_existing_twilio_template(template)
+ content_sid = template['content_sid']
+ return true if content_sid.blank?
+
+ template_service = Twilio::CsatTemplateService.new(@inbox.channel)
+ deletion_result = template_service.delete_template(nil, content_sid)
+
+ if deletion_result[:success]
+ Rails.logger.info "Deleted existing Twilio CSAT template '#{content_sid}' for inbox #{@inbox.id}"
+ true
+ else
+ Rails.logger.warn "Failed to delete existing Twilio CSAT template '#{content_sid}' for inbox #{@inbox.id}: #{deletion_result[:response_body]}"
+ false
+ end
+ end
+
+ def delete_existing_whatsapp_template(template)
+ template_name = template['name']
+ return true if template_name.blank?
+
+ csat_template_service = Whatsapp::CsatTemplateService.new(@inbox.channel)
+ template_status = csat_template_service.get_template_status(template_name)
+ return true unless template_status[:success]
+
+ deletion_result = csat_template_service.delete_template(template_name)
+ if deletion_result[:success]
+ Rails.logger.info "Deleted existing CSAT template '#{template_name}' for inbox #{@inbox.id}"
+ true
+ else
+ Rails.logger.warn "Failed to delete existing CSAT template '#{template_name}' for inbox #{@inbox.id}: #{deletion_result[:response_body]}"
+ false
+ end
+ end
+end
diff --git a/app/services/whatsapp/csat_template_name_service.rb b/app/services/csat_template_name_service.rb
similarity index 97%
rename from app/services/whatsapp/csat_template_name_service.rb
rename to app/services/csat_template_name_service.rb
index 223efda04..ac2500abc 100644
--- a/app/services/whatsapp/csat_template_name_service.rb
+++ b/app/services/csat_template_name_service.rb
@@ -1,4 +1,4 @@
-class Whatsapp::CsatTemplateNameService
+class CsatTemplateNameService
CSAT_BASE_NAME = 'customer_satisfaction_survey'.freeze
# Generates template names like: customer_satisfaction_survey_{inbox_id}_{version_number}
diff --git a/app/services/twilio/csat_template_api_client.rb b/app/services/twilio/csat_template_api_client.rb
new file mode 100644
index 000000000..e5e9b1b75
--- /dev/null
+++ b/app/services/twilio/csat_template_api_client.rb
@@ -0,0 +1,68 @@
+class Twilio::CsatTemplateApiClient
+ def initialize(twilio_channel)
+ @twilio_channel = twilio_channel
+ end
+
+ def create_template(request_body)
+ HTTParty.post(
+ "#{api_base_path}/v1/Content",
+ headers: api_headers,
+ body: request_body.to_json
+ )
+ end
+
+ def submit_for_approval(approval_url, template_name, category)
+ request_body = {
+ name: template_name,
+ category: category
+ }
+
+ HTTParty.post(
+ approval_url,
+ headers: api_headers,
+ body: request_body.to_json
+ )
+ end
+
+ def delete_template(content_sid)
+ HTTParty.delete(
+ "#{api_base_path}/v1/Content/#{content_sid}",
+ headers: api_headers
+ )
+ end
+
+ def fetch_template(content_sid)
+ HTTParty.get(
+ "#{api_base_path}/v1/Content/#{content_sid}",
+ headers: api_headers
+ )
+ end
+
+ def fetch_approval_status(content_sid)
+ HTTParty.get(
+ "#{api_base_path}/v1/Content/#{content_sid}/ApprovalRequests",
+ headers: api_headers
+ )
+ end
+
+ private
+
+ def api_headers
+ {
+ 'Authorization' => "Basic #{encoded_credentials}",
+ 'Content-Type' => 'application/json'
+ }
+ end
+
+ def encoded_credentials
+ if @twilio_channel.api_key_sid.present?
+ Base64.strict_encode64("#{@twilio_channel.api_key_sid}:#{@twilio_channel.auth_token}")
+ else
+ Base64.strict_encode64("#{@twilio_channel.account_sid}:#{@twilio_channel.auth_token}")
+ end
+ end
+
+ def api_base_path
+ 'https://content.twilio.com'
+ end
+end
diff --git a/app/services/twilio/csat_template_service.rb b/app/services/twilio/csat_template_service.rb
new file mode 100644
index 000000000..70be01a69
--- /dev/null
+++ b/app/services/twilio/csat_template_service.rb
@@ -0,0 +1,204 @@
+class Twilio::CsatTemplateService
+ DEFAULT_BUTTON_TEXT = 'Please rate us'.freeze
+ DEFAULT_LANGUAGE = 'en'.freeze
+ TEMPLATE_CATEGORY = 'UTILITY'.freeze
+ TEMPLATE_STATUS_PENDING = 'PENDING'.freeze
+ TEMPLATE_CONTENT_TYPE = 'twilio/call-to-action'.freeze
+
+ def initialize(twilio_channel)
+ @twilio_channel = twilio_channel
+ @api_client = Twilio::CsatTemplateApiClient.new(twilio_channel)
+ end
+
+ def create_template(template_config)
+ base_name = template_config[:template_name]
+ template_name = generate_template_name(base_name)
+ template_config_with_name = template_config.merge(template_name: template_name)
+
+ request_body = build_template_request_body(template_config_with_name)
+
+ # Step 1: Create template
+ response = @api_client.create_template(request_body)
+
+ return process_template_creation_response(response, template_config_with_name) unless response.success? && response['sid']
+
+ # Step 2: Submit for WhatsApp approval using the approval_create URL
+ approval_url = response.dig('links', 'approval_create')
+
+ if approval_url.present?
+ approval_response = submit_for_whatsapp_approval(approval_url, template_config_with_name[:template_name])
+ process_approval_response(approval_response, response, template_config_with_name)
+ else
+ Rails.logger.warn 'No approval_create URL provided in template creation response'
+ # Fallback if no approval URL provided
+ process_template_creation_response(response, template_config_with_name)
+ end
+ end
+
+ def delete_template(_template_name = nil, content_sid = nil)
+ content_sid ||= current_template_sid_from_config
+ return { success: false, error: 'No template to delete' } unless content_sid
+
+ response = @api_client.delete_template(content_sid)
+ { success: response.success?, response_body: response.body }
+ end
+
+ def get_template_status(content_sid)
+ return { success: false, error: 'No content SID provided' } unless content_sid
+
+ template_response = fetch_template_details(content_sid)
+ return template_response unless template_response[:success]
+
+ approval_response = fetch_approval_status(content_sid)
+ build_template_status_response(content_sid, template_response[:data], approval_response)
+ rescue StandardError => e
+ Rails.logger.error "Error fetching Twilio template status: #{e.message}"
+ { success: false, error: e.message }
+ end
+
+ private
+
+ def fetch_template_details(content_sid)
+ response = @api_client.fetch_template(content_sid)
+
+ if response.success?
+ { success: true, data: response }
+ else
+ Rails.logger.error "Failed to get template details: #{response.code} - #{response.body}"
+ { success: false, error: 'Template not found' }
+ end
+ end
+
+ def fetch_approval_status(content_sid)
+ @api_client.fetch_approval_status(content_sid)
+ end
+
+ def build_template_status_response(content_sid, template_response, approval_response)
+ if approval_response.success? && approval_response['whatsapp']
+ build_approved_template_response(content_sid, template_response, approval_response['whatsapp'])
+ else
+ build_pending_template_response(content_sid, template_response)
+ end
+ end
+
+ def build_approved_template_response(content_sid, template_response, whatsapp_data)
+ {
+ success: true,
+ template: {
+ content_sid: content_sid,
+ friendly_name: whatsapp_data['name'] || template_response['friendly_name'],
+ status: whatsapp_data['status'] || 'pending',
+ language: template_response['language'] || 'en'
+ }
+ }
+ end
+
+ def build_pending_template_response(content_sid, template_response)
+ {
+ success: true,
+ template: {
+ content_sid: content_sid,
+ friendly_name: template_response['friendly_name'],
+ status: 'pending',
+ language: template_response['language'] || 'en'
+ }
+ }
+ end
+
+ def generate_template_name(base_name)
+ current_template_name = current_template_name_from_config
+ CsatTemplateNameService.generate_next_template_name(base_name, @twilio_channel.inbox.id, current_template_name)
+ end
+
+ def current_template_name_from_config
+ @twilio_channel.inbox.csat_config&.dig('template', 'friendly_name')
+ end
+
+ def current_template_sid_from_config
+ @twilio_channel.inbox.csat_config&.dig('template', 'content_sid')
+ end
+
+ def template_exists_in_config?
+ content_sid = current_template_sid_from_config
+ friendly_name = current_template_name_from_config
+
+ content_sid.present? && friendly_name.present?
+ end
+
+ def build_template_request_body(template_config)
+ {
+ friendly_name: template_config[:template_name],
+ language: template_config[:language] || DEFAULT_LANGUAGE,
+ variables: {
+ '1' => '12345' # Example conversation UUID
+ },
+ types: {
+ TEMPLATE_CONTENT_TYPE => {
+ body: template_config[:message],
+ actions: [
+ {
+ type: 'URL',
+ title: template_config[:button_text] || DEFAULT_BUTTON_TEXT,
+ url: "#{template_config[:base_url]}/survey/responses/{{1}}"
+ }
+ ]
+ }
+ }
+ }
+ end
+
+ def submit_for_whatsapp_approval(approval_url, template_name)
+ @api_client.submit_for_approval(approval_url, template_name, TEMPLATE_CATEGORY)
+ end
+
+ def process_template_creation_response(response, template_config = {})
+ if response.success? && response['sid']
+ {
+ success: true,
+ content_sid: response['sid'],
+ friendly_name: template_config[:template_name],
+ language: template_config[:language] || DEFAULT_LANGUAGE,
+ status: TEMPLATE_STATUS_PENDING
+ }
+ else
+ Rails.logger.error "Twilio template creation failed: #{response.code} - #{response.body}"
+ {
+ success: false,
+ error: 'Template creation failed',
+ response_body: response.body
+ }
+ end
+ end
+
+ def process_approval_response(approval_response, creation_response, template_config)
+ if approval_response.success?
+ build_successful_approval_response(approval_response, creation_response, template_config)
+ else
+ build_failed_approval_response(approval_response, creation_response, template_config)
+ end
+ end
+
+ def build_successful_approval_response(approval_response, creation_response, template_config)
+ approval_data = approval_response.parsed_response
+ {
+ success: true,
+ content_sid: creation_response['sid'],
+ friendly_name: template_config[:template_name],
+ language: template_config[:language] || DEFAULT_LANGUAGE,
+ status: TEMPLATE_STATUS_PENDING,
+ approval_sid: approval_data['sid'],
+ whatsapp_status: approval_data.dig('whatsapp', 'status') || TEMPLATE_STATUS_PENDING
+ }
+ end
+
+ def build_failed_approval_response(approval_response, creation_response, template_config)
+ Rails.logger.error "Twilio template approval submission failed: #{approval_response.code} - #{approval_response.body}"
+ {
+ success: true,
+ content_sid: creation_response['sid'],
+ friendly_name: template_config[:template_name],
+ language: template_config[:language] || DEFAULT_LANGUAGE,
+ status: 'created'
+ }
+ end
+end
diff --git a/app/services/twilio/send_on_twilio_service.rb b/app/services/twilio/send_on_twilio_service.rb
index d2fe5719b..71e67aabf 100644
--- a/app/services/twilio/send_on_twilio_service.rb
+++ b/app/services/twilio/send_on_twilio_service.rb
@@ -1,4 +1,24 @@
class Twilio::SendOnTwilioService < Base::SendOnChannelService
+ def send_csat_template_message(phone_number:, content_sid:, content_variables: {})
+ send_params = {
+ to: phone_number,
+ content_sid: content_sid
+ }
+
+ send_params[:content_variables] = content_variables.to_json if content_variables.present?
+ send_params[:status_callback] = channel.send(:twilio_delivery_status_index_url) if channel.respond_to?(:twilio_delivery_status_index_url, true)
+
+ # Add messaging service or from number
+ send_params = send_params.merge(channel.send(:send_message_from))
+
+ twilio_message = channel.send(:client).messages.create(**send_params)
+
+ { success: true, message_id: twilio_message.sid }
+ rescue Twilio::REST::TwilioError, Twilio::REST::RestError => e
+ Rails.logger.error "Failed to send Twilio template message: #{e.message}"
+ { success: false, error: e.message }
+ end
+
private
def channel_class
diff --git a/app/services/whatsapp/csat_template_service.rb b/app/services/whatsapp/csat_template_service.rb
index 43009b484..9bdbef8ca 100644
--- a/app/services/whatsapp/csat_template_service.rb
+++ b/app/services/whatsapp/csat_template_service.rb
@@ -19,7 +19,7 @@ class Whatsapp::CsatTemplateService
end
def delete_template(template_name = nil)
- template_name ||= Whatsapp::CsatTemplateNameService.csat_template_name(@whatsapp_channel.inbox.id)
+ template_name ||= CsatTemplateNameService.csat_template_name(@whatsapp_channel.inbox.id)
response = HTTParty.delete(
"#{business_account_path}/message_templates?name=#{template_name}",
headers: api_headers
@@ -51,7 +51,7 @@ class Whatsapp::CsatTemplateService
def generate_template_name(base_name)
current_template_name = current_template_name_from_config
- Whatsapp::CsatTemplateNameService.generate_next_template_name(base_name, @whatsapp_channel.inbox.id, current_template_name)
+ CsatTemplateNameService.generate_next_template_name(base_name, @whatsapp_channel.inbox.id, current_template_name)
end
def current_template_name_from_config
diff --git a/app/services/whatsapp/providers/whatsapp_cloud_service.rb b/app/services/whatsapp/providers/whatsapp_cloud_service.rb
index e13ee8979..6f2ead579 100644
--- a/app/services/whatsapp/providers/whatsapp_cloud_service.rb
+++ b/app/services/whatsapp/providers/whatsapp_cloud_service.rb
@@ -67,7 +67,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
end
def delete_csat_template(template_name = nil)
- template_name ||= Whatsapp::CsatTemplateNameService.csat_template_name(whatsapp_channel.inbox.id)
+ template_name ||= CsatTemplateNameService.csat_template_name(whatsapp_channel.inbox.id)
csat_template_service.delete_template(template_name)
end
diff --git a/spec/controllers/api/v1/accounts/inbox_csat_templates_controller_spec.rb b/spec/controllers/api/v1/accounts/inbox_csat_templates_controller_spec.rb
index 7ca9083c0..d577c1b5d 100644
--- a/spec/controllers/api/v1/accounts/inbox_csat_templates_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/inbox_csat_templates_controller_spec.rb
@@ -9,11 +9,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
end
let(:whatsapp_inbox) { create(:inbox, channel: whatsapp_channel, account: account) }
let(:web_widget_inbox) { create(:inbox, account: account) }
- let(:mock_service) { instance_double(Whatsapp::Providers::WhatsappCloudService) }
+ let(:mock_service) { instance_double(Whatsapp::CsatTemplateService) }
before do
create(:inbox_member, user: agent, inbox: whatsapp_inbox)
- allow(Whatsapp::Providers::WhatsappCloudService).to receive(:new).and_return(mock_service)
+ allow(Whatsapp::CsatTemplateService).to receive(:new).and_return(mock_service)
end
describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/csat_template' do
@@ -32,7 +32,7 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
as: :json
expect(response).to have_http_status(:bad_request)
- expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp channels')
+ expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp and Twilio WhatsApp channels')
end
end
@@ -161,7 +161,7 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
as: :json
expect(response).to have_http_status(:bad_request)
- expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp channels')
+ expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp and Twilio WhatsApp channels')
end
end
@@ -195,11 +195,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
it 'creates template successfully' do
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
- allow(mock_service).to receive(:create_csat_template).and_return({
- success: true,
- template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
- template_id: '987654321'
- })
+ allow(mock_service).to receive(:create_template).and_return({
+ success: true,
+ template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
+ template_id: '987654321'
+ })
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
@@ -222,7 +222,7 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
}
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
- expect(mock_service).to receive(:create_csat_template) do |config|
+ expect(mock_service).to receive(:create_template) do |config|
expect(config[:button_text]).to eq('Please rate us')
expect(config[:language]).to eq('en')
expect(config[:template_name]).to eq("customer_satisfaction_survey_#{whatsapp_inbox.id}")
@@ -249,11 +249,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
}
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
- allow(mock_service).to receive(:create_csat_template).and_return({
- success: false,
- error: 'Template creation failed',
- response_body: whatsapp_error_response.to_json
- })
+ allow(mock_service).to receive(:create_template).and_return({
+ success: false,
+ error: 'Template creation failed',
+ response_body: whatsapp_error_response.to_json
+ })
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
@@ -272,11 +272,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
it 'handles generic API errors' do
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
- allow(mock_service).to receive(:create_csat_template).and_return({
- success: false,
- error: 'Network timeout',
- response_body: nil
- })
+ allow(mock_service).to receive(:create_template).and_return({
+ success: false,
+ error: 'Network timeout',
+ response_body: nil
+ })
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
@@ -289,7 +289,7 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
it 'handles unexpected service errors' do
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
- allow(mock_service).to receive(:create_csat_template)
+ allow(mock_service).to receive(:create_template)
.and_raise(StandardError, 'Unexpected error')
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
@@ -312,10 +312,10 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
allow(mock_service).to receive(:get_template_status)
.with('existing_template')
.and_return({ success: true, template: { id: '111111111' } })
- expect(mock_service).to receive(:delete_csat_template)
+ expect(mock_service).to receive(:delete_template)
.with('existing_template')
.and_return({ success: true })
- expect(mock_service).to receive(:create_csat_template)
+ expect(mock_service).to receive(:create_template)
.and_return({
success: true,
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
@@ -336,13 +336,13 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
})
allow(mock_service).to receive(:get_template_status).and_return({ success: true })
- allow(mock_service).to receive(:delete_csat_template)
+ allow(mock_service).to receive(:delete_template)
.and_return({ success: false, response_body: 'Delete failed' })
- allow(mock_service).to receive(:create_csat_template).and_return({
- success: true,
- template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
- template_id: '333333333'
- })
+ allow(mock_service).to receive(:create_template).and_return({
+ success: true,
+ template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
+ template_id: '333333333'
+ })
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: admin.create_new_auth_token,
@@ -365,11 +365,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
it 'allows access when agent is assigned to inbox' do
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
- allow(mock_service).to receive(:create_csat_template).and_return({
- success: true,
- template_name: 'customer_satisfaction_survey',
- template_id: '444444444'
- })
+ allow(mock_service).to receive(:create_template).and_return({
+ success: true,
+ template_name: 'customer_satisfaction_survey',
+ template_id: '444444444'
+ })
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
headers: agent.create_new_auth_token,
diff --git a/spec/services/twilio/send_on_twilio_service_spec.rb b/spec/services/twilio/send_on_twilio_service_spec.rb
index beae54b57..53dee3816 100644
--- a/spec/services/twilio/send_on_twilio_service_spec.rb
+++ b/spec/services/twilio/send_on_twilio_service_spec.rb
@@ -8,11 +8,8 @@ describe Twilio::SendOnTwilioService do
let(:message_record_double) { double }
let!(:account) { create(:account) }
- let!(:widget_inbox) { create(:inbox, account: account) }
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
- let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
- let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
let!(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: twilio_inbox) }
let(:conversation) { create(:conversation, contact: contact, inbox: twilio_inbox, contact_inbox: contact_inbox) }
@@ -23,6 +20,10 @@ describe Twilio::SendOnTwilioService do
end
describe '#perform' do
+ let!(:widget_inbox) { create(:inbox, account: account) }
+ let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
+ let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
+
context 'without reply' do
it 'if message is private' do
message = create(:message, message_type: 'outgoing', private: true, inbox: twilio_inbox, account: account)
@@ -107,4 +108,146 @@ describe Twilio::SendOnTwilioService do
expect(outgoing_message.reload.status).to eq('failed')
end
end
+
+ describe '#send_csat_template_message' do
+ let(:test_message) { create(:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, conversation: conversation) }
+ let(:service) { described_class.new(message: test_message) }
+ let(:mock_twilio_message) { instance_double(Twilio::REST::Api::V2010::AccountContext::MessageInstance, sid: 'SM123456789') }
+
+ # Test parameters defined using let statements
+ let(:test_params) do
+ {
+ phone_number: '+1234567890',
+ content_sid: 'HX123456789',
+ content_variables: { '1' => 'conversation-uuid-123' }
+ }
+ end
+
+ before do
+ allow(twilio_sms).to receive(:send_message_from).and_return({ from: '+0987654321' })
+ allow(twilio_sms).to receive(:respond_to?).and_return(true)
+ allow(twilio_sms).to receive(:twilio_delivery_status_index_url).and_return('http://localhost:3000/twilio/delivery_status')
+ end
+
+ context 'when template message is sent successfully' do
+ before do
+ allow(messages_double).to receive(:create).and_return(mock_twilio_message)
+ end
+
+ it 'sends template message with correct parameters' do
+ expected_params = {
+ to: test_params[:phone_number],
+ content_sid: test_params[:content_sid],
+ content_variables: test_params[:content_variables].to_json,
+ status_callback: 'http://localhost:3000/twilio/delivery_status',
+ from: '+0987654321'
+ }
+
+ result = service.send_csat_template_message(**test_params)
+
+ expect(messages_double).to have_received(:create).with(expected_params)
+ expect(result).to eq({ success: true, message_id: 'SM123456789' })
+ end
+
+ it 'sends template message without content variables when empty' do
+ expected_params = {
+ to: test_params[:phone_number],
+ content_sid: test_params[:content_sid],
+ status_callback: 'http://localhost:3000/twilio/delivery_status',
+ from: '+0987654321'
+ }
+
+ result = service.send_csat_template_message(
+ phone_number: test_params[:phone_number],
+ content_sid: test_params[:content_sid]
+ )
+
+ expect(messages_double).to have_received(:create).with(expected_params)
+ expect(result).to eq({ success: true, message_id: 'SM123456789' })
+ end
+
+ it 'includes custom status callback when channel supports it' do
+ allow(twilio_sms).to receive(:respond_to?).and_return(true)
+ allow(twilio_sms).to receive(:twilio_delivery_status_index_url).and_return('https://example.com/webhook')
+
+ expected_params = {
+ to: test_params[:phone_number],
+ content_sid: test_params[:content_sid],
+ content_variables: test_params[:content_variables].to_json,
+ status_callback: 'https://example.com/webhook',
+ from: '+0987654321'
+ }
+
+ service.send_csat_template_message(**test_params)
+
+ expect(messages_double).to have_received(:create).with(expected_params)
+ end
+ end
+
+ context 'when Twilio API returns an error' do
+ before do
+ allow(Rails.logger).to receive(:error)
+ end
+
+ it 'handles Twilio::REST::TwilioError' do
+ allow(messages_double).to receive(:create).and_raise(Twilio::REST::TwilioError, 'Invalid phone number')
+
+ result = service.send_csat_template_message(**test_params)
+
+ expect(result).to eq({ success: false, error: 'Invalid phone number' })
+ expect(Rails.logger).to have_received(:error).with('Failed to send Twilio template message: Invalid phone number')
+ end
+
+ it 'handles Twilio API errors' do
+ allow(messages_double).to receive(:create).and_raise(Twilio::REST::TwilioError, 'Content template not found')
+
+ result = service.send_csat_template_message(**test_params)
+
+ expect(result).to eq({ success: false, error: 'Content template not found' })
+ expect(Rails.logger).to have_received(:error).with('Failed to send Twilio template message: Content template not found')
+ end
+ end
+
+ context 'with parameter handling' do
+ before do
+ allow(messages_double).to receive(:create).and_return(mock_twilio_message)
+ end
+
+ it 'handles empty content_variables hash' do
+ expected_params = {
+ to: test_params[:phone_number],
+ content_sid: test_params[:content_sid],
+ status_callback: 'http://localhost:3000/twilio/delivery_status',
+ from: '+0987654321'
+ }
+
+ service.send_csat_template_message(
+ phone_number: test_params[:phone_number],
+ content_sid: test_params[:content_sid],
+ content_variables: {}
+ )
+
+ expect(messages_double).to have_received(:create).with(expected_params)
+ end
+
+ it 'converts content_variables to JSON when present' do
+ variables = { '1' => 'test-uuid', '2' => 'another-value' }
+ expected_params = {
+ to: test_params[:phone_number],
+ content_sid: test_params[:content_sid],
+ content_variables: variables.to_json,
+ status_callback: 'http://localhost:3000/twilio/delivery_status',
+ from: '+0987654321'
+ }
+
+ service.send_csat_template_message(
+ phone_number: test_params[:phone_number],
+ content_sid: test_params[:content_sid],
+ content_variables: variables
+ )
+
+ expect(messages_double).to have_received(:create).with(expected_params)
+ end
+ end
+ end
end
From 1a220b29828a3badb5950c936ba7d17d1e26b93a Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Tue, 13 Jan 2026 18:52:10 +0530
Subject: [PATCH 31/43] chore: Improve compose new conversation form (#13176)
Co-authored-by: Muhsin Keloth
+ {{ description }}
+
@@ -137,4 +136,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
-*Chatwoot* © 2017-2025, Chatwoot Inc - Released under the MIT License.
+*Chatwoot* © 2017-2026, Chatwoot Inc - Released under the MIT License.
From 7ba7bf842eb1573c8b1e7b84c9e948f8be851dcb Mon Sep 17 00:00:00 2001
From: Pranav
Release notes
v0.24.0
What's Changed
@jnunemaker in jnunemaker/httparty#823@jnunemaker in jnunemaker/httparty#828@jnunemaker in jnunemaker/httparty#829@jnunemaker in jnunemaker/httparty#8300.23.2
What's Changed
@baraidrissa in
jnunemaker/httparty#817@rdimartino in jnunemaker/httparty#822New Contributors
@baraidrissa
made their first contribution in jnunemaker/httparty#817@rdimartino made
their first contribution in jnunemaker/httparty#822v0.23.1
v0.23.0
What's Changed
@bestony in jnunemaker/httparty#803@tradesmanhelix
in jnunemaker/httparty#811New Contributors
@ashishra0
made their first contribution with foul mode@bestony made
their first contribution in jnunemaker/httparty#803@tradesmanhelix
made their first contribution in jnunemaker/httparty#811v0.22.0
What's Changed
@xymbol in jnunemaker/httparty#780@aliismayilov
in jnunemaker/httparty#786@olleolleolle
in jnunemaker/httparty#791@ngan in jnunemaker/httparty#796@rhett-inbox in
jnunemaker/httparty#783@y-yagi in jnunemaker/httparty#798@dependabot in jnunemaker/httparty#792Changelog
Changelog
Commits
55ec76e
Release 0.24.0ddfbc8d
Merge pull request #830
from jnunemaker/fix-ssrf-base-uri-bypass0529bcd
fix: prevent SSRF via absolute URL bypassing base_uri
(GHSA-hm5p-x4rq-38w4)05f38fd
Merge pull request #829
from jnunemaker/memory8901c23
feat: stream multipart file uploads to reduce memory usage091bd6a
Merge pull request #828
from jnunemaker/issue-82659c0ac5
feat: set Content-Type for Hash body in requests5c8b45e
Merge pull request #823
from jnunemaker/mixed-encodings6419cb3
Force binary encoding throughoutc74571f
Release 0.23.2
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show {{ title }}
+ {{ modelTitle }}
+
+ {{ title }}
+
+
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Muhsin Keloth
### Why this matters
* Capture the real context behind individual CSAT ratings
* Clarify whether a low score points to a genuine service issue or a
process gap
* Spot recurring themes across conversations and teams
* Make CSAT reviews more useful for leadership reviews and
retrospectives
### How Review Notes work
**View CSAT responses**
Open the CSAT report to see overall metrics, rating distribution, and
individual responses.
**Add a Review Note**
For any CSAT entry, managers can add a Review Note directly below the
customer’s feedback.
**Document internal insights**
Use Review Notes to capture things like:
* Why a score was lower or higher than expected
* Patterns you are seeing across similar cases
* Observations around communication, timelines, or customer expectations
Review Notes are visible only to administrators and people with report
access only. We may expand visibility to agents in the future based on
feedback. However, customers never see them.
Each note clearly shows who added it and when, making it easy to review
context and changes over time.
---
.../csat_survey_responses_controller.rb | 2 +
.../components/ui/DatePicker/DatePicker.vue | 6 +-
.../dashboard/components/widgets/ShowMore.vue | 2 +-
.../dashboard/i18n/locale/en/report.json | 55 +++-
.../settings/reports/CsatResponses.vue | 15 +-
.../reports/components/ConversationCell.vue | 26 --
.../components/Csat/CsatFilterHelpers.js | 36 +++
.../reports/components/Csat/CsatFilters.vue | 267 ++++++++++++++++++
.../reports/components/CsatContactCell.vue | 52 ++++
.../reports/components/CsatEmptyState.vue | 32 +++
.../reports/components/CsatExpandedRow.vue | 162 +++++++++++
.../components/CsatMetricCard.story.vue | 53 ++++
.../reports/components/CsatMetricCard.vue | 41 +++
.../reports/components/CsatMetrics.vue | 180 ++++--------
.../CsatRatingDistribution.story.vue | 75 +++++
.../components/CsatRatingDistribution.vue | 101 +++++++
.../components/CsatReviewNotesPaywall.vue | 28 ++
.../settings/reports/components/CsatTable.vue | 264 +++++++++++------
.../reports/components/CsatTableLoader.vue | 36 +++
.../components/specs/CSATMetrics.spec.js | 42 ++-
.../__snapshots__/CSATMetrics.spec.js.snap | 10 -
.../dashboard/store/modules/csat.js | 11 +
.../store/modules/specs/csat/getters.spec.js | 15 +
.../dashboard/store/mutation-types.js | 1 +
app/models/csat_survey_response.rb | 1 +
app/models/user.rb | 2 +
.../csat_survey_responses/download.csv.erb | 28 +-
.../update.json.jbuilder | 1 +
.../_csat_survey_response.json.jbuilder | 8 +
config/features.yml | 4 +
config/locales/en.yml | 1 +
config/routes.rb | 3 +
...l_observations_to_csat_survey_responses.rb | 5 +
...rvations_audit_to_csat_survey_responses.rb | 6 +
db/schema.rb | 6 +-
.../csat_survey_responses_controller.rb | 12 +
.../enterprise/csat_survey_response_policy.rb | 4 +
.../billing/handle_stripe_event_service.rb | 2 +-
enterprise/config/premium_features.yml | 1 +
.../csat_survey_responses_controller_spec.rb | 85 ++++++
40 files changed, 1376 insertions(+), 305 deletions(-)
delete mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/ConversationCell.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/Csat/CsatFilterHelpers.js
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/Csat/CsatFilters.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatContactCell.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatEmptyState.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatExpandedRow.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetricCard.story.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetricCard.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatRatingDistribution.story.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatRatingDistribution.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatReviewNotesPaywall.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTableLoader.vue
delete mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/specs/__snapshots__/CSATMetrics.spec.js.snap
create mode 100644 app/views/api/v1/accounts/csat_survey_responses/update.json.jbuilder
create mode 100644 db/migrate/20260114192518_add_internal_observations_to_csat_survey_responses.rb
create mode 100644 db/migrate/20260114201315_add_observations_audit_to_csat_survey_responses.rb
create mode 100644 enterprise/app/controllers/enterprise/api/v1/accounts/csat_survey_responses_controller.rb
create mode 100644 spec/enterprise/controllers/enterprise/api/v1/accounts/csat_survey_responses_controller_spec.rb
diff --git a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb
index f5bed6c34..0cde5f5c1 100644
--- a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb
+++ b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb
@@ -50,3 +50,5 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
@current_page = params[:page] || 1
end
end
+
+Api::V1::Accounts::CsatSurveyResponsesController.prepend_mod_with('Api::V1::Accounts::CsatSurveyResponsesController')
diff --git a/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue b/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
index bbb31d72c..886bc30a0 100644
--- a/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
+++ b/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
@@ -206,10 +206,14 @@ const emitDateRange = () => {
emit('dateRangeChanged', [selectedStartDate.value, selectedEndDate.value]);
}
};
+
+const closeDatePicker = () => {
+ showDatePicker.value = false;
+};
-
+ {{ title }}
+
+
-
-
-
+
+
+
+
+
+
+
+ {{ header.column.columnDef.header }}
+
+
+
+
+
+
+
+
+
+ {{ $t('CSAT_REPORTS.NO_FEEDBACK') }}
+
+
+
+
+
+
+
+
+
+
+
+
+