feat(whatsapp): persist phone number health
This commit is contained in:
@@ -686,36 +686,93 @@
|
||||
"WIDGET_FEATURES": "Widget features",
|
||||
"ACCOUNT_HEALTH": {
|
||||
"TITLE": "Manage your WhatsApp account",
|
||||
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||
"DESCRIPTION": "Review your WhatsApp phone number health, messaging capacity, and linked Meta business details.",
|
||||
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||
"NO_DATA": "Health data is not available",
|
||||
"ERRORS": {
|
||||
"AUTHORIZATION": {
|
||||
"EMBEDDED_TITLE": "WhatsApp connection needs to be refreshed",
|
||||
"EMBEDDED_DESCRIPTION": "Meta could not validate the connection for this inbox. Go to Configuration to reconfigure it using Embedded Signup.",
|
||||
"MANUAL_TITLE": "WhatsApp access token needs attention",
|
||||
"MANUAL_DESCRIPTION": "Meta could not validate the access token for this inbox. Go to Configuration to verify or replace it."
|
||||
},
|
||||
"GENERIC_TITLE": "WhatsApp health data is unavailable",
|
||||
"GENERIC_DESCRIPTION": "Meta could not return the current health details. Review the error below and try again later.",
|
||||
"GO_TO_CONFIGURATION": "Go to Configuration"
|
||||
},
|
||||
"SECTIONS": {
|
||||
"PHONE_NUMBER": "Phone number",
|
||||
"HEALTH_AND_CAPACITY": "Health and capacity",
|
||||
"BUSINESS_ACCOUNT": "Business account"
|
||||
},
|
||||
"FIELDS": {
|
||||
"DISPLAY_PHONE_NUMBER": {
|
||||
"LABEL": "Display phone number",
|
||||
"TOOLTIP": "Phone number displayed to customers"
|
||||
},
|
||||
"VERIFIED_NAME": {
|
||||
"LABEL": "Business name",
|
||||
"TOOLTIP": "Business name verified by WhatsApp"
|
||||
"LABEL": "WhatsApp display name",
|
||||
"TOOLTIP": "Display name associated with this phone number on WhatsApp"
|
||||
},
|
||||
"PHONE_NUMBER_ID": {
|
||||
"LABEL": "Phone number ID",
|
||||
"TOOLTIP": "Meta identifier for this WhatsApp phone number"
|
||||
},
|
||||
"COEXISTENCE": {
|
||||
"LABEL": "Coexistence",
|
||||
"TOOLTIP": "This number is connected to both the WhatsApp Business app and Cloud API"
|
||||
},
|
||||
"DISPLAY_NAME_STATUS": {
|
||||
"LABEL": "Display name status",
|
||||
"TOOLTIP": "Status of your business name verification"
|
||||
"TOOLTIP": "Review status of the WhatsApp display name"
|
||||
},
|
||||
"LAST_ONBOARDED_TIME": {
|
||||
"LABEL": "Last onboarded",
|
||||
"TOOLTIP": "Most recent time Meta onboarded this phone number"
|
||||
},
|
||||
"PHONE_NUMBER_STATUS": {
|
||||
"LABEL": "Phone number status",
|
||||
"TOOLTIP": "Current connection status of this phone number on WhatsApp"
|
||||
},
|
||||
"QUALITY_RATING": {
|
||||
"LABEL": "Quality rating",
|
||||
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||
"TOOLTIP": "Quality rating Meta assigns to this phone number based on recent messaging signals"
|
||||
},
|
||||
"MESSAGING_LIMIT_TIER": {
|
||||
"LABEL": "Messaging limit tier",
|
||||
"TOOLTIP": "Daily messaging limit for your account"
|
||||
"TOOLTIP": "Number of customers this phone number can initiate conversations with in 24 hours"
|
||||
},
|
||||
"ACCOUNT_MODE": {
|
||||
"LABEL": "Account mode",
|
||||
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||
"TOOLTIP": "Current operating mode of this WhatsApp phone number"
|
||||
},
|
||||
"CODE_VERIFICATION_STATUS": {
|
||||
"LABEL": "Code verification",
|
||||
"TOOLTIP": "Verification-code status reported by Meta for this phone number"
|
||||
},
|
||||
"THROUGHPUT_LEVEL": {
|
||||
"LABEL": "Throughput",
|
||||
"TOOLTIP": "Message throughput level assigned to this phone number"
|
||||
},
|
||||
"BUSINESS_ACCOUNT_NAME": {
|
||||
"LABEL": "WhatsApp Business Account name",
|
||||
"TOOLTIP": "Name of the WhatsApp Business Account linked to this phone number"
|
||||
},
|
||||
"BUSINESS_ACCOUNT_ID": {
|
||||
"LABEL": "WhatsApp Business Account ID",
|
||||
"TOOLTIP": "Meta identifier for the linked WhatsApp Business Account"
|
||||
},
|
||||
"BUSINESS_PORTFOLIO_NAME": {
|
||||
"LABEL": "Business portfolio name",
|
||||
"TOOLTIP": "Name of the Meta business portfolio that owns the WhatsApp Business Account"
|
||||
},
|
||||
"BUSINESS_PORTFOLIO_ID": {
|
||||
"LABEL": "Business portfolio ID",
|
||||
"TOOLTIP": "Meta identifier for the business portfolio that owns the WhatsApp Business Account"
|
||||
}
|
||||
},
|
||||
"VALUES": {
|
||||
"NOT_AVAILABLE": "Not available",
|
||||
"TIERS": {
|
||||
"TIER_250": "250 customers per 24h",
|
||||
"TIER_1000": "1K customers per 24h",
|
||||
@@ -727,6 +784,16 @@
|
||||
},
|
||||
"STATUSES": {
|
||||
"APPROVED": "Approved",
|
||||
"CONNECTED": "Connected",
|
||||
"VERIFIED": "Verified",
|
||||
"STANDARD": "Standard",
|
||||
"ACTIVE": "Active",
|
||||
"EXPIRED": "Expired",
|
||||
"FLAGGED": "Flagged",
|
||||
"RESTRICTED": "Restricted",
|
||||
"BANNED": "Banned",
|
||||
"DISCONNECTED": "Disconnected",
|
||||
"UNKNOWN": "Not available",
|
||||
"PENDING_REVIEW": "Pending Review",
|
||||
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||
"REJECTED": "Rejected",
|
||||
@@ -746,7 +813,11 @@
|
||||
"REGISTER_SUCCESS": "Webhook registered successfully",
|
||||
"REGISTER_ERROR": "Failed to register webhook. Please try again.",
|
||||
"CONFIGURED_SUCCESS": "Webhook configured successfully",
|
||||
"URL_MISMATCH": "Webhook URL mismatch"
|
||||
"URL_MISMATCH": "Webhook URL mismatch",
|
||||
"CONFIGURED_URL": "Configured URL",
|
||||
"EXPECTED_URL": "Expected URL",
|
||||
"COPY_URL": "Copy webhook URL",
|
||||
"COPY_SUCCESS": "Webhook URL copied to clipboard"
|
||||
}
|
||||
},
|
||||
"SETTINGS": "Settings",
|
||||
|
||||
@@ -411,6 +411,7 @@ export default {
|
||||
return (
|
||||
this.isAWhatsAppCloudChannel &&
|
||||
this.isEmbeddedSignupWhatsApp &&
|
||||
this.healthError?.type !== 'authorization' &&
|
||||
this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.WHATSAPP_MANUAL_TRANSFER
|
||||
@@ -580,11 +581,27 @@ export default {
|
||||
const response = await InboxHealthAPI.getHealthStatus(this.inbox.id);
|
||||
this.healthData = response.data;
|
||||
} catch (error) {
|
||||
this.healthError = error.message || 'Failed to fetch health data';
|
||||
const apiError = error.response?.data?.error;
|
||||
this.healthData = null;
|
||||
this.healthError =
|
||||
typeof apiError === 'object'
|
||||
? apiError
|
||||
: {
|
||||
type: 'generic',
|
||||
message: apiError || error.message,
|
||||
};
|
||||
} finally {
|
||||
this.isLoadingHealth = false;
|
||||
}
|
||||
},
|
||||
goToWhatsAppConfiguration() {
|
||||
const configurationTabIndex = this.tabs.findIndex(
|
||||
tab => tab.key === 'configuration'
|
||||
);
|
||||
if (configurationTabIndex !== -1) {
|
||||
this.onTabChange(configurationTabIndex);
|
||||
}
|
||||
},
|
||||
async registerWebhook() {
|
||||
if (!this.inbox) return;
|
||||
|
||||
@@ -1402,8 +1419,11 @@ export default {
|
||||
<div v-if="selectedTabKey === 'whatsapp-health'">
|
||||
<AccountHealth
|
||||
:health-data="healthData"
|
||||
:health-error="healthError"
|
||||
:is-embedded-signup="isEmbeddedSignupWhatsApp"
|
||||
:is-registering-webhook="isRegisteringWebhook"
|
||||
@register-webhook="registerWebhook"
|
||||
@go-to-configuration="goToWhatsAppConfiguration"
|
||||
/>
|
||||
</div>
|
||||
<WhatsappManualMigrationDialog
|
||||
|
||||
+401
-106
@@ -1,24 +1,34 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
|
||||
import ButtonV4 from 'next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
const props = defineProps({
|
||||
healthData: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
healthError: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isEmbeddedSignup: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isRegisteringWebhook: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['registerWebhook']);
|
||||
const emit = defineEmits(['registerWebhook', 'goToConfiguration']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, te, locale } = useI18n();
|
||||
|
||||
const QUALITY_COLORS = {
|
||||
GREEN: 'text-n-teal-11',
|
||||
@@ -29,8 +39,17 @@ const QUALITY_COLORS = {
|
||||
|
||||
const STATUS_COLORS = {
|
||||
APPROVED: 'text-n-teal-11',
|
||||
CONNECTED: 'text-n-teal-11',
|
||||
VERIFIED: 'text-n-teal-11',
|
||||
STANDARD: 'text-n-teal-11',
|
||||
ACTIVE: 'text-n-teal-11',
|
||||
PENDING_REVIEW: 'text-n-amber-11',
|
||||
AVAILABLE_WITHOUT_REVIEW: 'text-n-teal-11',
|
||||
EXPIRED: 'text-n-amber-11',
|
||||
FLAGGED: 'text-n-amber-11',
|
||||
RESTRICTED: 'text-n-ruby-9',
|
||||
BANNED: 'text-n-ruby-9',
|
||||
DISCONNECTED: 'text-n-ruby-9',
|
||||
REJECTED: 'text-n-ruby-9',
|
||||
DECLINED: 'text-n-ruby-9',
|
||||
};
|
||||
@@ -40,85 +59,267 @@ const MODE_COLORS = {
|
||||
SANDBOX: 'text-n-slate-11',
|
||||
};
|
||||
|
||||
const healthItems = computed(() => {
|
||||
const formatStatusDisplay = status => {
|
||||
const translationKey = `INBOX_MGMT.ACCOUNT_HEALTH.VALUES.STATUSES.${status}`;
|
||||
|
||||
if (te(translationKey)) {
|
||||
return t(translationKey);
|
||||
}
|
||||
|
||||
return status
|
||||
.toLowerCase()
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const formatDateTime = value => {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
|
||||
return new Intl.DateTimeFormat(locale.value, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const healthSections = computed(() => {
|
||||
if (!props.healthData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const {
|
||||
id: phoneNumberId,
|
||||
display_phone_number: displayPhoneNumber,
|
||||
verified_name: verifiedName,
|
||||
name_status: nameStatus,
|
||||
quality_rating: qualityRating,
|
||||
messaging_limit_tier: messagingLimitTier,
|
||||
account_mode: accountMode,
|
||||
status: phoneNumberStatus,
|
||||
code_verification_status: codeVerificationStatus,
|
||||
throughput_level: throughputLevel,
|
||||
last_onboarded_time: lastOnboardedTime,
|
||||
is_on_biz_app: isOnBizApp,
|
||||
platform_type: platformType,
|
||||
business_account_id: businessAccountId,
|
||||
business_account_name: businessAccountName,
|
||||
business_portfolio_id: businessPortfolioId,
|
||||
business_portfolio_name: businessPortfolioName,
|
||||
} = props.healthData;
|
||||
|
||||
const notAvailable = t('INBOX_MGMT.ACCOUNT_HEALTH.VALUES.NOT_AVAILABLE');
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'displayPhoneNumber',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.LABEL'),
|
||||
value: displayPhoneNumber || 'N/A',
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.TOOLTIP'
|
||||
),
|
||||
show: true,
|
||||
key: 'phoneNumber',
|
||||
title: t('INBOX_MGMT.ACCOUNT_HEALTH.SECTIONS.PHONE_NUMBER'),
|
||||
items: [
|
||||
{
|
||||
key: 'displayPhoneNumber',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.LABEL'
|
||||
),
|
||||
value: displayPhoneNumber || notAvailable,
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.TOOLTIP'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'verifiedName',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.LABEL'),
|
||||
value: verifiedName || notAvailable,
|
||||
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.TOOLTIP'),
|
||||
},
|
||||
{
|
||||
key: 'phoneNumberId',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_ID.LABEL'),
|
||||
value: phoneNumberId || notAvailable,
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_ID.TOOLTIP'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'coexistence',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.COEXISTENCE.LABEL'),
|
||||
value: 'ACTIVE',
|
||||
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.COEXISTENCE.TOOLTIP'),
|
||||
type: 'status',
|
||||
show: isOnBizApp === true && platformType === 'CLOUD_API',
|
||||
},
|
||||
{
|
||||
key: 'displayNameStatus',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.LABEL'
|
||||
),
|
||||
value: nameStatus || 'UNKNOWN',
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.TOOLTIP'
|
||||
),
|
||||
type: 'status',
|
||||
},
|
||||
{
|
||||
key: 'lastOnboardedTime',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.LAST_ONBOARDED_TIME.LABEL'
|
||||
),
|
||||
value: lastOnboardedTime
|
||||
? formatDateTime(lastOnboardedTime)
|
||||
: notAvailable,
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.LAST_ONBOARDED_TIME.TOOLTIP'
|
||||
),
|
||||
show: Boolean(lastOnboardedTime),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'verifiedName',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.LABEL'),
|
||||
value: verifiedName || 'N/A',
|
||||
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.TOOLTIP'),
|
||||
show: true,
|
||||
key: 'healthAndCapacity',
|
||||
title: t('INBOX_MGMT.ACCOUNT_HEALTH.SECTIONS.HEALTH_AND_CAPACITY'),
|
||||
items: [
|
||||
{
|
||||
key: 'phoneNumberStatus',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_STATUS.LABEL'
|
||||
),
|
||||
value: phoneNumberStatus || 'UNKNOWN',
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_STATUS.TOOLTIP'
|
||||
),
|
||||
type: 'status',
|
||||
},
|
||||
{
|
||||
key: 'qualityRating',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.LABEL'),
|
||||
value: qualityRating || 'UNKNOWN',
|
||||
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.TOOLTIP'),
|
||||
type: 'quality',
|
||||
},
|
||||
{
|
||||
key: 'messagingLimitTier',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.LABEL'
|
||||
),
|
||||
value: messagingLimitTier || 'UNKNOWN',
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.TOOLTIP'
|
||||
),
|
||||
type: 'tier',
|
||||
},
|
||||
{
|
||||
key: 'accountMode',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.LABEL'),
|
||||
value: accountMode || 'UNKNOWN',
|
||||
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.TOOLTIP'),
|
||||
type: 'mode',
|
||||
},
|
||||
{
|
||||
key: 'codeVerificationStatus',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.CODE_VERIFICATION_STATUS.LABEL'
|
||||
),
|
||||
value: codeVerificationStatus || 'UNKNOWN',
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.CODE_VERIFICATION_STATUS.TOOLTIP'
|
||||
),
|
||||
type: 'status',
|
||||
},
|
||||
{
|
||||
key: 'throughputLevel',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.THROUGHPUT_LEVEL.LABEL'),
|
||||
value: throughputLevel || 'UNKNOWN',
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.THROUGHPUT_LEVEL.TOOLTIP'
|
||||
),
|
||||
type: 'status',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'displayNameStatus',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.LABEL'),
|
||||
value: nameStatus || 'UNKNOWN',
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.TOOLTIP'
|
||||
),
|
||||
show: true,
|
||||
type: 'status',
|
||||
key: 'businessAccount',
|
||||
title: t('INBOX_MGMT.ACCOUNT_HEALTH.SECTIONS.BUSINESS_ACCOUNT'),
|
||||
items: [
|
||||
{
|
||||
key: 'businessAccountName',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_NAME.LABEL'
|
||||
),
|
||||
value: businessAccountName || notAvailable,
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_NAME.TOOLTIP'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'businessAccountId',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_ID.LABEL'
|
||||
),
|
||||
value: businessAccountId || notAvailable,
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_ID.TOOLTIP'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'businessPortfolioName',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_NAME.LABEL'
|
||||
),
|
||||
value: businessPortfolioName || notAvailable,
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_NAME.TOOLTIP'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'businessPortfolioId',
|
||||
label: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_ID.LABEL'
|
||||
),
|
||||
value: businessPortfolioId || notAvailable,
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_ID.TOOLTIP'
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'qualityRating',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.LABEL'),
|
||||
value: qualityRating || 'UNKNOWN',
|
||||
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.TOOLTIP'),
|
||||
show: true,
|
||||
type: 'quality',
|
||||
},
|
||||
{
|
||||
key: 'messagingLimitTier',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.LABEL'),
|
||||
value: messagingLimitTier || 'UNKNOWN',
|
||||
tooltip: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.TOOLTIP'
|
||||
),
|
||||
show: true,
|
||||
type: 'tier',
|
||||
},
|
||||
{
|
||||
key: 'accountMode',
|
||||
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.LABEL'),
|
||||
value: accountMode || 'UNKNOWN',
|
||||
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.TOOLTIP'),
|
||||
show: true,
|
||||
type: 'mode',
|
||||
},
|
||||
];
|
||||
].map(section => ({
|
||||
...section,
|
||||
items: section.items.filter(item => item.show !== false),
|
||||
}));
|
||||
});
|
||||
|
||||
const errorState = computed(() => {
|
||||
if (!props.healthError) return null;
|
||||
|
||||
if (props.healthError.type === 'authorization') {
|
||||
return {
|
||||
title: props.isEmbeddedSignup
|
||||
? t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.EMBEDDED_TITLE')
|
||||
: t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.MANUAL_TITLE'),
|
||||
description: props.isEmbeddedSignup
|
||||
? t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.EMBEDDED_DESCRIPTION'
|
||||
)
|
||||
: t(
|
||||
'INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.MANUAL_DESCRIPTION'
|
||||
),
|
||||
showConfigurationAction: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.GENERIC_TITLE'),
|
||||
description: t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.GENERIC_DESCRIPTION'),
|
||||
showConfigurationAction: false,
|
||||
};
|
||||
});
|
||||
|
||||
const handleGoToSettings = () => {
|
||||
const { business_id: businessId } = props.healthData || {};
|
||||
const { business_portfolio_id: businessPortfolioId } = props.healthData || {};
|
||||
|
||||
if (businessId) {
|
||||
// WhatsApp Business Manager URL with specific business ID and phone numbers tab
|
||||
const whatsappBusinessUrl = `https://business.facebook.com/latest/whatsapp_manager/phone_numbers/?business_id=${businessId}&tab=phone-numbers`;
|
||||
if (businessPortfolioId) {
|
||||
const whatsappBusinessUrl = `https://business.facebook.com/latest/whatsapp_manager/phone_numbers/?business_id=${businessPortfolioId}&tab=phone-numbers`;
|
||||
window.open(whatsappBusinessUrl, '_blank');
|
||||
} else {
|
||||
// Fallback to general WhatsApp Business Manager if business_id is not available
|
||||
const fallbackUrl = 'https://business.facebook.com/';
|
||||
window.open(fallbackUrl, '_blank');
|
||||
}
|
||||
@@ -130,9 +331,6 @@ const getQualityRatingTextColor = rating =>
|
||||
const formatTierDisplay = tier =>
|
||||
t(`INBOX_MGMT.ACCOUNT_HEALTH.VALUES.TIERS.${tier}`) || tier;
|
||||
|
||||
const formatStatusDisplay = status =>
|
||||
t(`INBOX_MGMT.ACCOUNT_HEALTH.VALUES.STATUSES.${status}`) || status;
|
||||
|
||||
const formatModeDisplay = mode =>
|
||||
t(`INBOX_MGMT.ACCOUNT_HEALTH.VALUES.MODES.${mode}`) || mode;
|
||||
|
||||
@@ -163,6 +361,15 @@ const webhookUrlMismatch = computed(
|
||||
const handleRegisterWebhook = () => {
|
||||
emit('registerWebhook');
|
||||
};
|
||||
|
||||
const handleGoToConfiguration = () => {
|
||||
emit('goToConfiguration');
|
||||
};
|
||||
|
||||
const handleCopyWebhookUrl = async url => {
|
||||
await copyTextToClipboard(url);
|
||||
useAlert(t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_SUCCESS'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -192,55 +399,62 @@ const handleRegisterWebhook = () => {
|
||||
</ButtonV4>
|
||||
</div>
|
||||
|
||||
<div v-if="healthData" class="grid grid-cols-1 gap-4 xs:grid-cols-2">
|
||||
<div
|
||||
v-for="item in healthItems"
|
||||
:key="item.key"
|
||||
class="flex flex-col gap-2 p-4 rounded-lg border border-n-weak bg-n-solid-1"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-body-main font-medium text-n-slate-11">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<Icon
|
||||
v-tooltip.top="item.tooltip"
|
||||
icon="i-lucide-info"
|
||||
class="flex-shrink-0 w-4 h-4 cursor-help text-n-slate-9"
|
||||
/>
|
||||
<div v-if="healthData" class="space-y-6">
|
||||
<section v-for="section in healthSections" :key="section.key">
|
||||
<h3 class="mb-3 text-heading-4 text-n-slate-12">
|
||||
{{ section.title }}
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-4 xs:grid-cols-2">
|
||||
<div
|
||||
v-for="item in section.items"
|
||||
:key="item.key"
|
||||
class="flex flex-col gap-2 p-4 rounded-lg border border-n-weak bg-n-solid-1"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-body-main font-medium text-n-slate-11">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<Icon
|
||||
v-tooltip.top="item.tooltip"
|
||||
icon="i-lucide-info"
|
||||
class="flex-shrink-0 w-4 h-4 cursor-help text-n-slate-9"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
v-if="item.type === 'quality'"
|
||||
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
|
||||
:class="getQualityRatingTextColor(item.value)"
|
||||
>
|
||||
{{ item.value }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="item.type === 'status'"
|
||||
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
|
||||
:class="getStatusTextColor(item.value)"
|
||||
>
|
||||
{{ formatStatusDisplay(item.value) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="item.type === 'mode'"
|
||||
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
|
||||
:class="getModeStatusTextColor(item.value)"
|
||||
>
|
||||
{{ formatModeDisplay(item.value) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="item.type === 'tier'"
|
||||
class="text-label text-n-slate-12"
|
||||
>
|
||||
{{ formatTierDisplay(item.value) }}
|
||||
</span>
|
||||
<span v-else class="text-label text-n-slate-12">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
v-if="item.type === 'quality'"
|
||||
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
|
||||
:class="getQualityRatingTextColor(item.value)"
|
||||
>
|
||||
{{ item.value }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="item.type === 'status'"
|
||||
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
|
||||
:class="getStatusTextColor(item.value)"
|
||||
>
|
||||
{{ formatStatusDisplay(item.value) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="item.type === 'mode'"
|
||||
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
|
||||
:class="getModeStatusTextColor(item.value)"
|
||||
>
|
||||
{{ formatModeDisplay(item.value) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="item.type === 'tier'"
|
||||
class="text-label text-n-slate-12"
|
||||
>
|
||||
{{ formatTierDisplay(item.value) }}
|
||||
</span>
|
||||
<span v-else class="text-label text-n-slate-12">{{
|
||||
item.value
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Webhook configuration card -->
|
||||
<div
|
||||
@@ -289,6 +503,87 @@ const handleRegisterWebhook = () => {
|
||||
{{ t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.REGISTER_BUTTON') }}
|
||||
</ButtonV4>
|
||||
</div>
|
||||
<div v-if="webhookConfigured" class="pt-1 space-y-2">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span class="flex-shrink-0 text-label-small text-n-slate-10">
|
||||
{{ t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.CONFIGURED_URL') }}
|
||||
</span>
|
||||
<span
|
||||
v-tooltip.top="webhookUrl"
|
||||
class="flex-1 min-w-0 font-mono text-label-small truncate text-n-slate-12"
|
||||
>
|
||||
{{ webhookUrl }}
|
||||
</span>
|
||||
<ButtonV4
|
||||
v-tooltip.top="t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_URL')"
|
||||
type="button"
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-copy"
|
||||
class="flex-shrink-0"
|
||||
:aria-label="t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_URL')"
|
||||
@click="handleCopyWebhookUrl(webhookUrl)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="webhookUrlMismatch"
|
||||
class="flex items-center gap-3 min-w-0"
|
||||
>
|
||||
<span class="flex-shrink-0 text-label-small text-n-slate-10">
|
||||
{{ t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.EXPECTED_URL') }}
|
||||
</span>
|
||||
<span
|
||||
v-tooltip.top="healthData.expected_webhook_url"
|
||||
class="flex-1 min-w-0 font-mono text-label-small truncate text-n-slate-12"
|
||||
>
|
||||
{{ healthData.expected_webhook_url }}
|
||||
</span>
|
||||
<ButtonV4
|
||||
v-tooltip.top="t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_URL')"
|
||||
type="button"
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-copy"
|
||||
class="flex-shrink-0"
|
||||
:aria-label="t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_URL')"
|
||||
@click="handleCopyWebhookUrl(healthData.expected_webhook_url)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorState" class="pt-2">
|
||||
<div
|
||||
class="flex flex-col gap-4 p-5 rounded-lg border border-n-weak bg-n-solid-1"
|
||||
>
|
||||
<div class="flex gap-3 items-start">
|
||||
<Icon
|
||||
icon="i-lucide-alert-triangle"
|
||||
class="flex-shrink-0 mt-0.5 w-5 h-5 text-n-amber-11"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-heading-4 text-n-slate-12">
|
||||
{{ errorState.title }}
|
||||
</h3>
|
||||
<p class="mt-1 text-body-main text-n-slate-11">
|
||||
{{ errorState.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="healthError.message"
|
||||
class="p-3 rounded-lg bg-n-alpha-1 text-label-small text-n-slate-11"
|
||||
>
|
||||
<p class="break-words">{{ healthError.message }}</p>
|
||||
</div>
|
||||
<div v-if="errorState.showConfigurationAction">
|
||||
<ButtonV4 sm solid blue @click="handleGoToConfiguration">
|
||||
{{ t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.GO_TO_CONFIGURATION') }}
|
||||
</ButtonV4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user