feat(shangwutong): sync classifications and update conversations

This commit is contained in:
2026-09-11 17:08:18 +08:00
parent a06d4f4199
commit 31f157a4f5
28 changed files with 2541 additions and 232 deletions
@@ -0,0 +1,34 @@
/* global axios */
import ApiClient from './ApiClient';
class ShangwutongClassificationsAPI extends ApiClient {
constructor() {
super('inboxes', { accountScoped: true });
this.conversations = new ApiClient('conversations', {
accountScoped: true,
});
}
get(inboxId) {
return axios.get(`${this.url}/${inboxId}/shangwutong/classifications`);
}
sync(inboxId) {
return axios.post(
`${this.url}/${inboxId}/shangwutong/classifications/sync`
);
}
updateConversation(conversationId, payload) {
return axios.patch(
`${this.conversations.url}/${conversationId}/shangwutong-classifications`,
payload
);
}
getConversation(conversationId) {
return axios.get(`${this.conversations.url}/${conversationId}`);
}
}
export default new ShangwutongClassificationsAPI();
@@ -403,6 +403,20 @@
"ASSIGNEE_LABEL": "Assigned Agent",
"SELF_ASSIGN": "Assign to me",
"TEAM_LABEL": "Assigned Team",
"SHANGWUTONG_CLASSIFICATIONS": {
"TITLE": "Shangwutong classifications",
"CONVERSATION_KIND": "Conversation classification",
"CUSTOMER_COLOR": "Customer classification",
"SELECT": "Select",
"NOT_SYNCED": "Sync Shangwutong classifications in inbox settings first",
"SYNC_PENDING": "Shangwutong classification sync is processing",
"SYNC_FAILED": "Shangwutong classification sync failed",
"LOAD_ERROR": "Failed to load Shangwutong classifications",
"UPDATE_QUEUED": "Shangwutong classification update queued",
"UPDATE_CONFIRMED": "Shangwutong classification updated",
"UPDATE_PENDING": "Shangwutong classification is still processing",
"UPDATE_ERROR": "Failed to update Shangwutong classification"
},
"SELECT": {
"PLACEHOLDER": "None"
},
@@ -1225,6 +1225,15 @@
},
"PASSWORD": "Replace password",
"PASSWORD_HELP": "Leave blank to keep the current password. A new password is verified online before replacing the working credential.",
"CLASSIFICATIONS": "Shangwutong classifications",
"CLASSIFICATIONS_HELP": "Sync classification definitions from Shangwutong. GoChat does not predefine or edit them.",
"SYNC_CLASSIFICATIONS": "Sync Shangwutong classifications",
"CLASSIFICATION_SYNCED": "Synced: {conversation} conversation kinds, {customer} customer kinds",
"CLASSIFICATION_SYNC_PENDING": "Classification sync is processing",
"CLASSIFICATION_NOT_SYNCED": "Not synced yet",
"CLASSIFICATION_LOAD_ERROR": "Failed to load Shangwutong classifications",
"CLASSIFICATION_SYNC_QUEUED": "Shangwutong classification sync queued",
"CLASSIFICATION_SYNC_ERROR": "Shangwutong classification sync failed",
"WEBHOOK_URL": "Connector webhook URL",
"WEBHOOK_URL_HELP": "All enabled Shangwutong inboxes must use the same Connector URL."
},
@@ -403,6 +403,20 @@
"ASSIGNEE_LABEL": "已分配的客服代表",
"SELF_ASSIGN": "分配给我",
"TEAM_LABEL": "已分配的团队",
"SHANGWUTONG_CLASSIFICATIONS": {
"TITLE": "商务通分类",
"CONVERSATION_KIND": "对话分类",
"CUSTOMER_COLOR": "客户分类",
"SELECT": "请选择",
"NOT_SYNCED": "请先在收件箱设置中同步商务通分类",
"SYNC_PENDING": "商务通分类同步中",
"SYNC_FAILED": "商务通分类同步失败",
"LOAD_ERROR": "商务通分类加载失败",
"UPDATE_QUEUED": "商务通分类修改已提交",
"UPDATE_CONFIRMED": "商务通分类修改成功",
"UPDATE_PENDING": "商务通分类仍在处理中",
"UPDATE_ERROR": "商务通分类修改失败"
},
"SELECT": {
"PLACEHOLDER": "啥都没有"
},
@@ -1225,6 +1225,15 @@
},
"PASSWORD": "修改密码",
"PASSWORD_HELP": "留空表示保持原密码。新密码在线验证成功后才替换当前可用凭据。",
"CLASSIFICATIONS": "商务通分类",
"CLASSIFICATIONS_HELP": "从商务通账号同步分类定义。GoChat 不预设或编辑商务通分类。",
"SYNC_CLASSIFICATIONS": "同步商务通分类",
"CLASSIFICATION_SYNCED": "已同步:对话分类 {conversation} 个,客户分类 {customer} 个",
"CLASSIFICATION_SYNC_PENDING": "同步任务处理中",
"CLASSIFICATION_NOT_SYNCED": "尚未同步",
"CLASSIFICATION_LOAD_ERROR": "商务通分类加载失败",
"CLASSIFICATION_SYNC_QUEUED": "商务通分类同步任务已提交",
"CLASSIFICATION_SYNC_ERROR": "商务通分类同步失败",
"WEBHOOK_URL": "连接器 Webhook 地址",
"WEBHOOK_URL_HELP": "所有启用的商务通收件箱必须使用同一个连接器地址。"
},
@@ -15,6 +15,7 @@ import ConversationAction from './ConversationAction.vue';
import ConversationParticipant from './ConversationParticipant.vue';
import ContactInfo from './contact/ContactInfo.vue';
import ContactNotes from './contact/ContactNotes.vue';
import ShangwutongClassifications from './ShangwutongClassifications.vue';
import ConversationInfo from './ConversationInfo.vue';
import CustomAttributes from './customAttributes/CustomAttributes.vue';
import SharedFiles from './SharedFiles.vue';
@@ -138,6 +139,12 @@ onMounted(() => {
@close="closeContactPanel"
/>
<ContactInfo :contact="contact" :channel-type="channelType" />
<ShangwutongClassifications
:conversation-id="conversationId"
:inbox-id="inboxId"
:channel-type="channelType"
:conversation-attributes="conversationAdditionalAttributes"
/>
<div class="px-2 pb-8 list-group">
<Draggable
:list="conversationSidebarItems"
@@ -0,0 +1,237 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import { useMapGetter } from 'dashboard/composables/store';
import ShangwutongClassificationsAPI from 'dashboard/api/shangwutongClassifications';
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
inboxId: {
type: [Number, String],
default: undefined,
},
channelType: {
type: String,
default: '',
},
conversationAttributes: {
type: Object,
default: () => ({}),
},
});
const { t } = useI18n();
const currentChat = useMapGetter('getSelectedChat');
const catalog = ref({
conversation_kinds: [],
customer_color_kinds: [],
sync_status: 'never',
});
const selectedChatKind = ref('');
const selectedCustomerColor = ref('');
const isLoading = ref(false);
const saving = ref('');
const isShangwutong = computed(
() =>
props.channelType === 'Channel::Shangwutong' ||
currentChat.value?.meta?.channel === 'Channel::Shangwutong'
);
const isReady = computed(() => catalog.value.sync_status === 'succeeded');
const attributes = computed(() => {
if (Object.keys(props.conversationAttributes || {}).length > 0) {
return props.conversationAttributes;
}
return currentChat.value?.meta?.additional_attributes || {};
});
const resetSelections = () => {
selectedChatKind.value =
attributes.value.swt_chat_kind || attributes.value.swt_chat_kind_id || '';
selectedCustomerColor.value = '';
const currentColor = attributes.value.swt_label_color;
if (currentColor !== undefined && currentColor !== null) {
const match = catalog.value.customer_color_kinds.find(
item =>
item.id === String(currentColor) || item.name === String(currentColor)
);
selectedCustomerColor.value = match?.id || String(currentColor);
}
};
const fetchClassifications = async () => {
if (!isShangwutong.value || !props.inboxId) return;
isLoading.value = true;
try {
const response = await ShangwutongClassificationsAPI.get(props.inboxId);
catalog.value = response.data;
resetSelections();
} catch (error) {
useAlert(
error.message ||
t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.LOAD_ERROR')
);
} finally {
isLoading.value = false;
}
};
const conversationAttributesFrom = response =>
response.data?.conversation?.additional_attributes ||
response.data?.additional_attributes ||
response.data?.meta?.additional_attributes ||
{};
const waitForConfirmation = async (type, value, eventID, attempt = 0) => {
if (attempt >= 10) return null;
await new Promise(resolve => {
setTimeout(resolve, 500);
});
try {
const response = await ShangwutongClassificationsAPI.getConversation(
props.conversationId
);
const updated = conversationAttributesFrom(response);
if (updated.swt_classification_event_id === eventID) {
if (updated.swt_classification_status !== 'succeeded') return false;
return type === 'chat'
? String(updated.swt_chat_kind) === String(value)
: String(updated.swt_label_color) === String(value);
}
} catch (error) {
return null;
}
return waitForConfirmation(type, value, eventID, attempt + 1);
};
const updateClassification = async (type, value) => {
if (!value || saving.value || !isReady.value) return;
saving.value = type;
const payload =
type === 'chat' ? { chat_kind_id: value } : { customer_color_id: value };
try {
const response = await ShangwutongClassificationsAPI.updateConversation(
props.conversationId,
payload
);
const confirmation = await waitForConfirmation(
type,
value,
response.data?.sync_id
);
if (confirmation === true) {
useAlert(
t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.UPDATE_CONFIRMED')
);
} else if (confirmation === false) {
resetSelections();
useAlert(
t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.UPDATE_ERROR')
);
} else {
useAlert(
t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.UPDATE_PENDING')
);
}
} catch (error) {
resetSelections();
useAlert(
error.message ||
t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.UPDATE_ERROR')
);
} finally {
saving.value = '';
}
};
watch(
[() => props.inboxId, () => props.channelType, () => props.conversationId],
fetchClassifications,
{ immediate: true }
);
watch(() => props.conversationAttributes, resetSelections, { deep: true });
</script>
<template>
<template v-if="isShangwutong">
<div class="px-4 py-3 border-b border-n-weak">
<h4 class="text-heading-3 text-n-slate-12">
{{ $t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.TITLE') }}
</h4>
<p v-if="!isReady" class="mt-2 text-body-small text-n-slate-10">
{{
catalog.sync_status === 'pending'
? $t(
'CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.SYNC_PENDING'
)
: catalog.sync_status === 'failed'
? $t(
'CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.SYNC_FAILED'
)
: $t(
'CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.NOT_SYNCED'
)
}}
</p>
<div v-else class="mt-3 space-y-3">
<label class="block text-body-small text-n-slate-11">
{{
$t(
'CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.CONVERSATION_KIND'
)
}}
<select
v-model="selectedChatKind"
class="w-full mt-1"
:disabled="isLoading || !!saving"
@change="updateClassification('chat', selectedChatKind)"
>
<option value="">
{{
$t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.SELECT')
}}
</option>
<option
v-for="item in catalog.conversation_kinds"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</option>
</select>
</label>
<label class="block text-body-small text-n-slate-11">
{{
$t(
'CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.CUSTOMER_COLOR'
)
}}
<select
v-model="selectedCustomerColor"
class="w-full mt-1"
:disabled="isLoading || !!saving"
@change="updateClassification('color', selectedCustomerColor)"
>
<option value="">
{{
$t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.SELECT')
}}
</option>
<option
v-for="item in catalog.customer_color_kinds"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</option>
</select>
</label>
</div>
</div>
</template>
<template v-else />
</template>
@@ -1,231 +1,294 @@
<script>
import { useAlert } from 'dashboard/composables';
import InboxHealthAPI from 'dashboard/api/inboxHealth';
import ShangwutongClassificationsAPI from 'dashboard/api/shangwutongClassifications';
import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFieldSection.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: { SettingsFieldSection, NextButton },
props: {
inbox: { type: Object, required: true },
},
data() {
return {
desiredPresence: 'online',
password: '',
webhookUrl: '',
health: null,
isSaving: false,
isLoadingHealth: false,
};
},
watch: {
inbox: {
immediate: true,
handler(value) {
this.desiredPresence = value.desired_presence || 'online';
this.webhookUrl = value.webhook_url || '';
},
},
},
mounted() {
this.fetchHealth();
},
methods: {
statusLabel(type, value, fallback) {
const status = value || fallback;
const key = `INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_VALUES.${type}.${status.toUpperCase()}`;
return this.$te(key)
? this.$t(key)
: this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_VALUES.UNKNOWN');
},
async fetchHealth() {
this.isLoadingHealth = true;
try {
const response = await InboxHealthAPI.getHealthStatus(
this.inbox.id
);
this.health = response.data;
} catch (error) {
useAlert(
error.message ||
this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')
);
} finally {
this.isLoadingHealth = false;
}
},
async save() {
this.isSaving = true;
const channel = {
desired_presence: this.desiredPresence,
webhook_url: this.webhookUrl.trim(),
};
if (this.password) channel.password = this.password;
try {
await this.$store.dispatch('inboxes/updateInbox', {
id: this.inbox.id,
formData: false,
channel,
});
this.password = '';
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
await this.fetchHealth();
} catch (error) {
useAlert(
error.message ||
this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')
);
} finally {
this.isSaving = false;
}
},
},
components: { SettingsFieldSection, NextButton },
props: {
inbox: { type: Object, required: true },
},
data() {
return {
desiredPresence: 'online',
password: '',
webhookUrl: '',
health: null,
isSaving: false,
isLoadingHealth: false,
classifications: null,
isSyncingClassifications: false,
};
},
watch: {
inbox: {
immediate: true,
handler(value) {
this.desiredPresence = value.desired_presence || 'online';
this.webhookUrl = value.webhook_url || '';
},
},
},
mounted() {
this.fetchHealth();
this.fetchClassifications();
},
methods: {
statusLabel(type, value, fallback) {
const status = value || fallback;
const key = `INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_VALUES.${type}.${status.toUpperCase()}`;
return this.$te(key)
? this.$t(key)
: this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_VALUES.UNKNOWN');
},
async fetchHealth() {
this.isLoadingHealth = true;
try {
const response = await InboxHealthAPI.getHealthStatus(this.inbox.id);
this.health = response.data;
} catch (error) {
useAlert(error.message || this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
} finally {
this.isLoadingHealth = false;
}
},
async fetchClassifications() {
try {
const response = await ShangwutongClassificationsAPI.get(this.inbox.id);
this.classifications = response.data;
} catch (error) {
useAlert(
error.message ||
this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_LOAD_ERROR')
);
}
},
async waitForClassificationSync(attempt = 0) {
if (attempt >= 10) return;
await new Promise(resolve => {
setTimeout(resolve, 500);
});
await this.fetchClassifications();
if (this.classifications?.sync_status === 'pending') {
await this.waitForClassificationSync(attempt + 1);
}
},
async syncClassifications() {
this.isSyncingClassifications = true;
try {
await ShangwutongClassificationsAPI.sync(this.inbox.id);
await this.waitForClassificationSync();
if (this.classifications?.sync_status === 'succeeded') {
useAlert(
this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_SYNCED', {
conversation: this.classifications.conversation_kinds.length,
customer: this.classifications.customer_color_kinds.length,
})
);
} else if (this.classifications?.sync_status !== 'failed') {
useAlert(
this.$t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_SYNC_QUEUED'
)
);
}
} catch (error) {
useAlert(
error.message ||
this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_SYNC_ERROR')
);
} finally {
this.isSyncingClassifications = false;
}
},
async save() {
this.isSaving = true;
const channel = {
desired_presence: this.desiredPresence,
webhook_url: this.webhookUrl.trim(),
};
if (this.password) channel.password = this.password;
try {
await this.$store.dispatch('inboxes/updateInbox', {
id: this.inbox.id,
formData: false,
channel,
});
this.password = '';
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
await this.fetchHealth();
} catch (error) {
useAlert(error.message || this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
} finally {
this.isSaving = false;
}
},
},
};
</script>
<template>
<div class="space-y-6">
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.RUNTIME_STATUS')"
:help-text="
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.RUNTIME_STATUS_HELP')
"
>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
{{
$t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.CONNECTION_STATUS'
)
}}
</p>
<p class="text-body-main text-n-slate-12">
{{
statusLabel(
'CONNECTION',
health?.connection_status || inbox.connection_status,
'pending'
)
}}
</p>
</div>
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
{{
$t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.ACTUAL_PRESENCE'
)
}}
</p>
<p class="text-body-main text-n-slate-12">
{{
statusLabel(
'PRESENCE',
health?.actual_presence || inbox.actual_presence,
'offline'
)
}}
</p>
</div>
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
{{
$t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.CREDENTIAL_STATUS'
)
}}
</p>
<p class="text-body-main text-n-slate-12">
{{
statusLabel(
'CREDENTIAL',
health?.credential_status || inbox.credential_status,
'pending'
)
}}
</p>
</div>
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
{{
$t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.LAST_HEARTBEAT_AT'
)
}}
</p>
<p class="text-body-main text-n-slate-12">
{{
health?.last_heartbeat_at ||
inbox.last_heartbeat_at ||
'—'
}}
</p>
</div>
</div>
<p
v-if="health?.last_error_code"
class="mt-3 text-sm text-n-ruby-11"
>
{{ health.last_error_code }}
</p>
<NextButton
class="mt-3"
:is-loading="isLoadingHealth"
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.REFRESH')"
@click="fetchHealth"
/>
</SettingsFieldSection>
<div class="space-y-6">
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.RUNTIME_STATUS')"
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.RUNTIME_STATUS_HELP')"
>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
{{
$t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.CONNECTION_STATUS'
)
}}
</p>
<p class="text-body-main text-n-slate-12">
{{
statusLabel(
'CONNECTION',
health?.connection_status || inbox.connection_status,
'pending'
)
}}
</p>
</div>
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
{{
$t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.ACTUAL_PRESENCE'
)
}}
</p>
<p class="text-body-main text-n-slate-12">
{{
statusLabel(
'PRESENCE',
health?.actual_presence || inbox.actual_presence,
'offline'
)
}}
</p>
</div>
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
{{
$t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.CREDENTIAL_STATUS'
)
}}
</p>
<p class="text-body-main text-n-slate-12">
{{
statusLabel(
'CREDENTIAL',
health?.credential_status || inbox.credential_status,
'pending'
)
}}
</p>
</div>
<div class="rounded-lg border border-n-weak p-3">
<p class="text-label-small text-n-slate-10">
{{
$t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.STATUS_FIELDS.LAST_HEARTBEAT_AT'
)
}}
</p>
<p class="text-body-main text-n-slate-12">
{{ health?.last_heartbeat_at || inbox.last_heartbeat_at || '—' }}
</p>
</div>
</div>
<p v-if="health?.last_error_code" class="mt-3 text-sm text-n-ruby-11">
{{ health.last_error_code }}
</p>
<NextButton
class="mt-3"
:is-loading="isLoadingHealth"
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.REFRESH')"
@click="fetchHealth"
/>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.DESIRED_PRESENCE')"
:help-text="
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.DESIRED_PRESENCE_HELP')
"
>
<select v-model="desiredPresence" class="w-full">
<option value="online">
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.ONLINE') }}
</option>
<option value="busy">
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.BUSY') }}
</option>
<option value="away">
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.AWAY') }}
</option>
<option value="offline">
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.OFFLINE') }}
</option>
</select>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATIONS')"
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATIONS_HELP')"
>
<div class="flex items-center gap-3">
<NextButton
:is-loading="isSyncingClassifications"
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.SYNC_CLASSIFICATIONS')"
@click="syncClassifications"
/>
<span class="text-body-small text-n-slate-10">
{{
classifications?.sync_status === 'succeeded'
? $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_SYNCED', {
conversation: classifications.conversation_kinds.length,
customer: classifications.customer_color_kinds.length,
})
: classifications?.sync_status === 'pending'
? $t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_SYNC_PENDING'
)
: $t(
'INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_NOT_SYNCED'
)
}}
</span>
</div>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PASSWORD')"
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PASSWORD_HELP')"
>
<input
v-model="password"
type="password"
maxlength="4096"
autocomplete="new-password"
class="w-full"
/>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.DESIRED_PRESENCE')"
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.DESIRED_PRESENCE_HELP')"
>
<select v-model="desiredPresence" class="w-full">
<option value="online">
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.ONLINE') }}
</option>
<option value="busy">
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.BUSY') }}
</option>
<option value="away">
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.AWAY') }}
</option>
<option value="offline">
{{ $t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.OFFLINE') }}
</option>
</select>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.WEBHOOK_URL')"
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.WEBHOOK_URL_HELP')"
>
<input v-model="webhookUrl" type="url" class="w-full" />
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PASSWORD')"
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PASSWORD_HELP')"
>
<input
v-model="password"
type="password"
maxlength="4096"
autocomplete="new-password"
class="w-full"
/>
</SettingsFieldSection>
<div class="flex justify-end">
<NextButton
:is-loading="isSaving"
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
@click="save"
/>
</div>
</div>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.WEBHOOK_URL')"
:help-text="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.WEBHOOK_URL_HELP')"
>
<input v-model="webhookUrl" type="url" class="w-full" />
</SettingsFieldSection>
<div class="flex justify-end">
<NextButton
:is-loading="isSaving"
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
@click="save"
/>
</div>
</div>
</template>