feat: Add support for sending CSAT surveys via templates (Whatsapp Cloud) (#12787)
This PR enables sending CSAT surveys on WhatsApp using approved WhatsApp message templates, ensuring survey delivery even after the 24-hour session window. The system now automatically creates, updates, and monitors WhatsApp CSAT templates without manual intervention. <img width="1664" height="1792" alt="approved" src="https://github.com/user-attachments/assets/c6efd61e-1d01-4738-abb6-0afc0dace975" /> #### Why this change Previously, WhatsApp CSAT messages failed outside the 24-hour customer window. With this update: - CSAT surveys are delivered reliably using WhatsApp templates - Template creation happens automatically in the background - Users can modify survey content and recreate templates easily - Clear UI states show template approval status #### Screens & States <details> <summary>Default — No template configured yet</summary> <img width="1662" height="1788" alt="default" src="https://github.com/user-attachments/assets/ed26d71b-cf7c-4a26-a2af-da88772c847c" /> </details> <details> <summary>Pending — Template submitted, awaiting Meta approval</summary> <img width="1658" height="1816" alt="pending" src="https://github.com/user-attachments/assets/923b789b-d91b-4364-905d-e56a2b65331a" /> </details> <details> <summary>Approved — Survey will be sent when conversation resolves</summary> <img width="1664" height="1792" alt="approved" src="https://github.com/user-attachments/assets/c6efd61e-1d01-4738-abb6-0afc0dace975" /> </details> <details> <summary>Rejected — Template rejected by Meta</summary> <img width="1672" height="1776" alt="rejected" src="https://github.com/user-attachments/assets/f69a9b0e-be27-4e67-a993-7b8149502c4f" /> </details> <details> <summary>Not Found — Template missing in Meta Platform</summary> <img width="1660" height="1784" alt="not-exist" src="https://github.com/user-attachments/assets/a2a4b4f7-b01a-4424-8fcb-3ed84256e057" /> </details> <details> <summary>Edit Template — Delete & recreate template on change</summary> <img width="2342" height="1778" alt="edit-survey" src="https://github.com/user-attachments/assets/0f999285-0341-4226-84e9-31f0c6446924" /> </details> #### Test Cases **1. First-time CSAT setup on WhatsApp inbox** - Enable CSAT - Enter message + button text - Save - Expected: Template created automatically, UI shows pending state **2. CSAT toggle without changing text** - Existing approved template - Toggle CSAT OFF → ON (no text change) - Expected: No confirmation alert, no template recreation **3. Editing only survey rules** - Modify labels or rule conditions only - Expected: No confirmation alert, template remains unchanged **4. Template text change** - Change survey message or button text - Save - Expected: - Confirmation dialog shown - On confirm → previous template deleted, new one created - On cancel → revert to previous values **5. Language change** - Change template language (e.g., en → es) - Expected: Confirmation dialog + new template on confirm **6. Sending survey** - Template approved → always send template - Template pending → send free-form within 24 hours only - Template rejected/missing → fallback to free-form (if within window) - Outside 24 hours & no approved template → activity log only **7. Non-WhatsApp inbox** - Enable CSAT for email/web inbox - Expected: No template logic triggered Fixes https://linear.app/chatwoot/issue/CW-6188/support-for-sending-csat-surveys-via-approved-whatsapp --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
@@ -32,6 +32,16 @@ class Inboxes extends CacheEnabledApiClient {
|
||||
syncTemplates(inboxId) {
|
||||
return axios.post(`${this.url}/${inboxId}/sync_templates`);
|
||||
}
|
||||
|
||||
createCSATTemplate(inboxId, template) {
|
||||
return axios.post(`${this.url}/${inboxId}/csat_template`, {
|
||||
template,
|
||||
});
|
||||
}
|
||||
|
||||
getCSATTemplateStatus(inboxId) {
|
||||
return axios.get(`${this.url}/${inboxId}/csat_template`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inboxes();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2.5 text-n-slate-12 max-w-80">
|
||||
<div class="p-3 rounded-xl bg-n-alpha-2">
|
||||
<span
|
||||
v-dompurify-html="message.content"
|
||||
class="text-sm font-medium prose prose-bubble"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button :label="buttonText" slate class="!text-n-blue-text w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -859,6 +859,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
max-height: none !important;
|
||||
min-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
> .ProseMirror {
|
||||
|
||||
@@ -140,6 +140,11 @@ export const FORMATTING = {
|
||||
nodes: [],
|
||||
menu: ['strong', 'em', 'link', 'undo', 'redo'],
|
||||
},
|
||||
'Context::Plain': {
|
||||
marks: [],
|
||||
nodes: [],
|
||||
menu: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Editor menu options for Full Editor
|
||||
|
||||
@@ -808,6 +808,35 @@
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"BUTTON_TEXT": {
|
||||
"LABEL": "Button text",
|
||||
"PLACEHOLDER": "Please rate us"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Language",
|
||||
"PLACEHOLDER": "Select template language"
|
||||
},
|
||||
"MESSAGE_PREVIEW": {
|
||||
"LABEL": "Message preview",
|
||||
"TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
|
||||
},
|
||||
"TEMPLATE_STATUS": {
|
||||
"APPROVED": "Approved by WhatsApp",
|
||||
"PENDING": "Pending WhatsApp approval",
|
||||
"REJECTED": "Meta rejected the template",
|
||||
"DEFAULT": "Needs WhatsApp approval",
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
"DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
|
||||
"CONFIRM": "Create new template",
|
||||
"CANCEL": "Go back"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -819,6 +848,7 @@
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
|
||||
@@ -3,15 +3,22 @@ import { reactive, onMounted, ref, defineProps, watch, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useInbox } from 'dashboard/composables/useInbox';
|
||||
import { CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import SectionLayout from 'dashboard/routes/dashboard/settings/account/components/SectionLayout.vue';
|
||||
import CSATDisplayTypeSelector from './components/CSATDisplayTypeSelector.vue';
|
||||
import CSATTemplate from 'dashboard/components-next/message/bubbles/Template/CSAT.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
import FilterSelect from 'dashboard/components-next/filter/inputs/FilterSelect.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Switch from 'next/switch/Switch.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
|
||||
import ConfirmTemplateUpdateDialog from './components/ConfirmTemplateUpdateDialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
inbox: { type: Object, required: true },
|
||||
@@ -21,6 +28,10 @@ const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const labels = useMapGetter('labels/getLabels');
|
||||
|
||||
const { isAWhatsAppCloudChannel: isWhatsAppChannel } = useInbox(
|
||||
props.inbox?.id
|
||||
);
|
||||
|
||||
const isUpdating = ref(false);
|
||||
const selectedLabelValues = ref([]);
|
||||
const currentLabel = ref('');
|
||||
@@ -29,7 +40,19 @@ const state = reactive({
|
||||
csatSurveyEnabled: false,
|
||||
displayType: 'emoji',
|
||||
message: '',
|
||||
templateButtonText: 'Please rate us',
|
||||
surveyRuleOperator: 'contains',
|
||||
templateLanguage: '',
|
||||
});
|
||||
|
||||
const templateStatus = ref(null);
|
||||
const templateLoading = ref(false);
|
||||
const confirmDialog = ref(null);
|
||||
|
||||
const originalTemplateValues = ref({
|
||||
message: '',
|
||||
templateButtonText: '',
|
||||
templateLanguage: '',
|
||||
});
|
||||
|
||||
const filterTypes = [
|
||||
@@ -51,6 +74,59 @@ const labelOptions = computed(() =>
|
||||
: []
|
||||
);
|
||||
|
||||
const languageOptions = computed(() =>
|
||||
languages.map(({ name, id }) => ({ label: `${name} (${id})`, value: id }))
|
||||
);
|
||||
|
||||
const messagePreviewData = computed(() => ({
|
||||
content: state.message || t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER'),
|
||||
}));
|
||||
|
||||
const shouldShowTemplateStatus = computed(
|
||||
() => templateStatus.value && !templateLoading.value
|
||||
);
|
||||
|
||||
const templateApprovalStatus = computed(() => {
|
||||
const statusMap = {
|
||||
APPROVED: {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.APPROVED'),
|
||||
icon: 'i-lucide-circle-check',
|
||||
color: 'text-n-teal-11',
|
||||
},
|
||||
PENDING: {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.PENDING'),
|
||||
icon: 'i-lucide-clock',
|
||||
color: 'text-n-amber-11',
|
||||
},
|
||||
REJECTED: {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.REJECTED'),
|
||||
icon: 'i-lucide-circle-x',
|
||||
color: 'text-n-ruby-10',
|
||||
},
|
||||
};
|
||||
|
||||
// Handle template not found case
|
||||
if (templateStatus.value?.error === 'TEMPLATE_NOT_FOUND') {
|
||||
return {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.NOT_FOUND'),
|
||||
icon: 'i-lucide-alert-triangle',
|
||||
color: 'text-n-ruby-10',
|
||||
};
|
||||
}
|
||||
|
||||
// Handle existing template with status
|
||||
if (templateStatus.value?.template_exists && templateStatus.value.status) {
|
||||
return statusMap[templateStatus.value.status] || statusMap.PENDING;
|
||||
}
|
||||
|
||||
// Default case - no template exists
|
||||
return {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.DEFAULT'),
|
||||
icon: 'i-lucide-stamp',
|
||||
color: 'text-n-slate-11',
|
||||
};
|
||||
});
|
||||
|
||||
const initializeState = () => {
|
||||
if (!props.inbox) return;
|
||||
|
||||
@@ -63,21 +139,63 @@ const initializeState = () => {
|
||||
const {
|
||||
display_type: displayType = CSAT_DISPLAY_TYPES.EMOJI,
|
||||
message = '',
|
||||
button_text: buttonText = 'Please rate us',
|
||||
language = 'en',
|
||||
survey_rules: surveyRules = {},
|
||||
} = csat_config;
|
||||
|
||||
state.displayType = displayType;
|
||||
state.message = message;
|
||||
state.templateButtonText = buttonText;
|
||||
state.templateLanguage = language;
|
||||
state.surveyRuleOperator = surveyRules.operator || 'contains';
|
||||
|
||||
selectedLabelValues.value = Array.isArray(surveyRules.values)
|
||||
? [...surveyRules.values]
|
||||
: [];
|
||||
|
||||
// Store original template values for change detection
|
||||
if (isWhatsAppChannel.value) {
|
||||
originalTemplateValues.value = {
|
||||
message: state.message,
|
||||
templateButtonText: state.templateButtonText,
|
||||
templateLanguage: state.templateLanguage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const checkTemplateStatus = async () => {
|
||||
if (!isWhatsAppChannel.value) return;
|
||||
|
||||
try {
|
||||
templateLoading.value = true;
|
||||
const response = await store.dispatch('inboxes/getCSATTemplateStatus', {
|
||||
inboxId: props.inbox.id,
|
||||
});
|
||||
|
||||
// Handle case where template doesn't exist
|
||||
if (!response.template_exists && response.error === 'Template not found') {
|
||||
templateStatus.value = {
|
||||
template_exists: false,
|
||||
error: 'TEMPLATE_NOT_FOUND',
|
||||
};
|
||||
} else {
|
||||
templateStatus.value = response;
|
||||
}
|
||||
} catch (error) {
|
||||
templateStatus.value = {
|
||||
template_exists: false,
|
||||
error: 'API_ERROR',
|
||||
};
|
||||
} finally {
|
||||
templateLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initializeState();
|
||||
if (!labels.value?.length) store.dispatch('labels/get');
|
||||
if (isWhatsAppChannel.value) checkTemplateStatus();
|
||||
});
|
||||
|
||||
watch(() => props.inbox, initializeState, { immediate: true });
|
||||
@@ -105,6 +223,49 @@ const removeLabel = label => {
|
||||
}
|
||||
};
|
||||
|
||||
// Check if template-related fields have changed
|
||||
const hasTemplateChanges = () => {
|
||||
if (!isWhatsAppChannel.value) return false;
|
||||
|
||||
const original = originalTemplateValues.value;
|
||||
return (
|
||||
original.message !== state.message ||
|
||||
original.templateButtonText !== state.templateButtonText ||
|
||||
original.templateLanguage !== state.templateLanguage
|
||||
);
|
||||
};
|
||||
|
||||
// Check if there's an existing template
|
||||
const hasExistingTemplate = () => {
|
||||
const { template_exists, error } = templateStatus.value || {};
|
||||
return template_exists && !error;
|
||||
};
|
||||
|
||||
// Check if we should create a template
|
||||
const shouldCreateTemplate = () => {
|
||||
// Create template if no existing template
|
||||
if (!hasExistingTemplate()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create template if there are changes to template fields
|
||||
return hasTemplateChanges();
|
||||
};
|
||||
|
||||
// Build template config for saving
|
||||
const buildTemplateConfig = () => {
|
||||
if (!hasExistingTemplate()) return null;
|
||||
|
||||
const { template_name, template_id, template, status } =
|
||||
templateStatus.value || {};
|
||||
return {
|
||||
name: template_name,
|
||||
template_id,
|
||||
language: template?.language || state.templateLanguage,
|
||||
status,
|
||||
};
|
||||
};
|
||||
|
||||
const updateInbox = async attributes => {
|
||||
const payload = {
|
||||
id: props.inbox.id,
|
||||
@@ -115,31 +276,103 @@ const updateInbox = async attributes => {
|
||||
return store.dispatch('inboxes/updateInbox', payload);
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
const createTemplate = async () => {
|
||||
if (!isWhatsAppChannel.value) return null;
|
||||
|
||||
const response = await store.dispatch('inboxes/createCSATTemplate', {
|
||||
inboxId: props.inbox.id,
|
||||
template: {
|
||||
message: state.message,
|
||||
button_text: state.templateButtonText,
|
||||
language: state.templateLanguage,
|
||||
},
|
||||
});
|
||||
useAlert(t('INBOX_MGMT.CSAT.TEMPLATE_CREATION.SUCCESS_MESSAGE'));
|
||||
return response.template;
|
||||
};
|
||||
|
||||
const performSave = async () => {
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
let newTemplateData = null;
|
||||
|
||||
// For WhatsApp channels, create template first if needed
|
||||
if (
|
||||
isWhatsAppChannel.value &&
|
||||
state.csatSurveyEnabled &&
|
||||
shouldCreateTemplate()
|
||||
) {
|
||||
try {
|
||||
newTemplateData = await createTemplate();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.response?.data?.error ||
|
||||
t('INBOX_MGMT.CSAT.TEMPLATE_CREATION.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const csatConfig = {
|
||||
display_type: state.displayType,
|
||||
message: state.message,
|
||||
button_text: state.templateButtonText,
|
||||
language: state.templateLanguage,
|
||||
survey_rules: {
|
||||
operator: state.surveyRuleOperator,
|
||||
values: selectedLabelValues.value,
|
||||
},
|
||||
};
|
||||
|
||||
// Use new template data if created, otherwise preserve existing template information
|
||||
if (newTemplateData) {
|
||||
csatConfig.template = {
|
||||
name: newTemplateData.name,
|
||||
template_id: newTemplateData.template_id,
|
||||
language: newTemplateData.language,
|
||||
status: newTemplateData.status,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
} else {
|
||||
const templateConfig = buildTemplateConfig();
|
||||
if (templateConfig) {
|
||||
csatConfig.template = templateConfig;
|
||||
}
|
||||
}
|
||||
|
||||
await updateInbox({
|
||||
csat_survey_enabled: state.csatSurveyEnabled,
|
||||
csat_config: csatConfig,
|
||||
});
|
||||
|
||||
useAlert(t('INBOX_MGMT.CSAT.API.SUCCESS_MESSAGE'));
|
||||
checkTemplateStatus();
|
||||
} catch (error) {
|
||||
useAlert(t('INBOX_MGMT.CSAT.API.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
// Check if we need to show confirmation dialog for WhatsApp template changes
|
||||
if (
|
||||
isWhatsAppChannel.value &&
|
||||
state.csatSurveyEnabled &&
|
||||
hasExistingTemplate() &&
|
||||
hasTemplateChanges()
|
||||
) {
|
||||
confirmDialog.value?.open();
|
||||
return;
|
||||
}
|
||||
|
||||
await performSave();
|
||||
};
|
||||
|
||||
const handleConfirmTemplateUpdate = async () => {
|
||||
// We will delete the template before creating the template
|
||||
await performSave();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -155,7 +388,9 @@ const saveSettings = async () => {
|
||||
</template>
|
||||
|
||||
<div class="grid gap-5">
|
||||
<!-- Show display type only for non-WhatsApp channels -->
|
||||
<WithLabel
|
||||
v-if="!isWhatsAppChannel"
|
||||
:label="$t('INBOX_MGMT.CSAT.DISPLAY_TYPE.LABEL')"
|
||||
name="display_type"
|
||||
>
|
||||
@@ -165,14 +400,97 @@ const saveSettings = async () => {
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel :label="$t('INBOX_MGMT.CSAT.MESSAGE.LABEL')" name="message">
|
||||
<Editor
|
||||
v-model="state.message"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER')"
|
||||
:max-length="200"
|
||||
class="w-full"
|
||||
/>
|
||||
</WithLabel>
|
||||
<template v-if="isWhatsAppChannel">
|
||||
<div
|
||||
class="flex flex-col gap-4 justify-between w-full lg:flex-row lg:gap-6"
|
||||
>
|
||||
<div class="flex flex-col gap-3 basis-3/5">
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.MESSAGE.LABEL')"
|
||||
name="message"
|
||||
>
|
||||
<Editor
|
||||
v-model="state.message"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER')"
|
||||
:max-length="200"
|
||||
channel-type="Context::Plain"
|
||||
class="w-full"
|
||||
/>
|
||||
</WithLabel>
|
||||
<Input
|
||||
v-model="state.templateButtonText"
|
||||
:label="$t('INBOX_MGMT.CSAT.BUTTON_TEXT.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.BUTTON_TEXT.PLACEHOLDER')"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.LANGUAGE.LABEL')"
|
||||
name="language"
|
||||
>
|
||||
<ComboBox
|
||||
v-model="state.templateLanguage"
|
||||
:options="languageOptions"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.LANGUAGE.PLACEHOLDER')"
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<div
|
||||
v-if="shouldShowTemplateStatus"
|
||||
class="flex gap-2 items-center mt-4"
|
||||
>
|
||||
<Icon
|
||||
:icon="templateApprovalStatus.icon"
|
||||
:class="templateApprovalStatus.color"
|
||||
class="size-4"
|
||||
/>
|
||||
<span
|
||||
:class="templateApprovalStatus.color"
|
||||
class="text-sm font-medium"
|
||||
>
|
||||
{{ templateApprovalStatus.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col flex-shrink-0 justify-start items-center p-6 mt-1 rounded-xl basis-2/5 bg-n-slate-2 outline outline-1 outline-n-weak"
|
||||
>
|
||||
<p
|
||||
class="inline-flex items-center text-sm font-medium text-n-slate-11"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.CSAT.MESSAGE_PREVIEW.LABEL') }}
|
||||
<Icon
|
||||
v-tooltip.top-end="
|
||||
$t('INBOX_MGMT.CSAT.MESSAGE_PREVIEW.TOOLTIP')
|
||||
"
|
||||
icon="i-lucide-info"
|
||||
class="flex-shrink-0 mx-1 size-4"
|
||||
/>
|
||||
</p>
|
||||
<CSATTemplate
|
||||
:message="messagePreviewData"
|
||||
:button-text="state.templateButtonText"
|
||||
class="pt-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Non-WhatsApp channels layout -->
|
||||
<template v-else>
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.MESSAGE.LABEL')"
|
||||
name="message"
|
||||
>
|
||||
<Editor
|
||||
v-model="state.message"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER')"
|
||||
:max-length="200"
|
||||
class="w-full"
|
||||
/>
|
||||
</WithLabel>
|
||||
</template>
|
||||
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.SURVEY_RULE.LABEL')"
|
||||
@@ -180,7 +498,7 @@ const saveSettings = async () => {
|
||||
>
|
||||
<div class="mb-4">
|
||||
<span
|
||||
class="inline-flex flex-wrap items-center gap-1.5 text-sm text-n-slate-12"
|
||||
class="inline-flex flex-wrap gap-1.5 items-center text-sm text-n-slate-12"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.CSAT.SURVEY_RULE.DESCRIPTION_PREFIX') }}
|
||||
<FilterSelect
|
||||
@@ -217,7 +535,11 @@ const saveSettings = async () => {
|
||||
</div>
|
||||
</WithLabel>
|
||||
<p class="text-sm italic text-n-slate-11">
|
||||
{{ $t('INBOX_MGMT.CSAT.NOTE') }}
|
||||
{{
|
||||
isWhatsAppChannel
|
||||
? $t('INBOX_MGMT.CSAT.WHATSAPP_NOTE')
|
||||
: $t('INBOX_MGMT.CSAT.NOTE')
|
||||
}}
|
||||
</p>
|
||||
<div>
|
||||
<NextButton
|
||||
@@ -229,5 +551,11 @@ const saveSettings = async () => {
|
||||
</div>
|
||||
</div>
|
||||
</SectionLayout>
|
||||
|
||||
<!-- Template Update Confirmation Dialog -->
|
||||
<ConfirmTemplateUpdateDialog
|
||||
ref="confirmDialog"
|
||||
@confirm="handleConfirmTemplateUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
|
||||
const emit = defineEmits(['confirm']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const handleDialogConfirm = () => {
|
||||
emit('confirm');
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
dialogRef,
|
||||
open: () => dialogRef.value?.open(),
|
||||
close: () => dialogRef.value?.close(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="alert"
|
||||
:title="t('INBOX_MGMT.CSAT.TEMPLATE_UPDATE_DIALOG.TITLE')"
|
||||
:description="t('INBOX_MGMT.CSAT.TEMPLATE_UPDATE_DIALOG.DESCRIPTION')"
|
||||
:confirm-button-label="t('INBOX_MGMT.CSAT.TEMPLATE_UPDATE_DIALOG.CONFIRM')"
|
||||
:cancel-button-label="t('INBOX_MGMT.CSAT.TEMPLATE_UPDATE_DIALOG.CANCEL')"
|
||||
@confirm="handleDialogConfirm"
|
||||
/>
|
||||
</template>
|
||||
@@ -83,6 +83,14 @@ export const getters = {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter out CSAT templates (customer_satisfaction_survey and its versions)
|
||||
if (
|
||||
template.name &&
|
||||
template.name.startsWith('customer_satisfaction_survey')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter out interactive templates (LIST, PRODUCT, CATALOG), location templates, and call permission templates
|
||||
const hasUnsupportedComponents = template.components.some(
|
||||
component =>
|
||||
@@ -344,6 +352,14 @@ export const actions = {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
createCSATTemplate: async (_, { inboxId, template }) => {
|
||||
const response = await InboxesAPI.createCSATTemplate(inboxId, template);
|
||||
return response.data;
|
||||
},
|
||||
getCSATTemplateStatus: async (_, { inboxId }) => {
|
||||
const response = await InboxesAPI.getCSATTemplateStatus(inboxId);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
|
||||
Reference in New Issue
Block a user