Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88a4f695c0 | ||
|
|
e82a886846 | ||
|
|
25c0784f13 | ||
|
|
ce7057bd43 | ||
|
|
900e158509 | ||
|
|
b872c1f87f | ||
|
|
934df34079 | ||
|
|
dcf188f156 | ||
|
|
50ee09a883 | ||
|
|
89eee8dc66 | ||
|
|
ffd3154c94 |
@@ -6,6 +6,14 @@
|
|||||||
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
|
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
|
||||||
"LEARN_MORE": "Learn more about SLA",
|
"LEARN_MORE": "Learn more about SLA",
|
||||||
"LOADING": "Fetching SLAs",
|
"LOADING": "Fetching SLAs",
|
||||||
|
"PAYWALL": {
|
||||||
|
"TITLE": "Upgrade to create SLAs",
|
||||||
|
"AVAILABLE_ON": "SLA management is available on the <span class=\"font-medium text-slate-900\">Business and Enterprise</span> plans only.",
|
||||||
|
"UPGRADE_PROMPT": "Upgrade now to get access to advanced features like team management, automations, custom attributes, and more.",
|
||||||
|
"UPGRADE_NOW": "Upgrade now",
|
||||||
|
"ASK_ADMIN": "Ask your administrator to upgrade and get access to advanced features like team management, automations, custom attributes, and more.",
|
||||||
|
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||||
|
},
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "There are no SLAs available in this account.",
|
"404": "There are no SLAs available in this account.",
|
||||||
"EMPTY": {
|
"EMPTY": {
|
||||||
|
|||||||
@@ -10,44 +10,12 @@
|
|||||||
<SLAListItemLoading v-for="ii in 2" :key="ii" class="mb-3" />
|
<SLAListItemLoading v-for="ii in 2" :key="ii" class="mb-3" />
|
||||||
</template>
|
</template>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div v-if="!records.length" class="w-full min-h-[12rem] relative">
|
<SLAPaywall v-if="isBehindAPaywall" :is-admin="isAdmin" />
|
||||||
<div class="w-full space-y-3">
|
<SLAEmptyState
|
||||||
<SLA-list-item
|
v-else-if="!records.length"
|
||||||
class="opacity-25 dark:opacity-20"
|
@primary-action="openAddPopup"
|
||||||
:sla-name="$t('SLA.LIST.EMPTY.TITLE_1')"
|
/>
|
||||||
:description="$t('SLA.LIST.EMPTY.DESC_1')"
|
<div v-else class="flex flex-col w-full h-full gap-3">
|
||||||
first-response="20m"
|
|
||||||
next-response="1h"
|
|
||||||
resolution-time="24h"
|
|
||||||
has-business-hours
|
|
||||||
/>
|
|
||||||
<SLA-list-item
|
|
||||||
class="opacity-25 dark:opacity-20"
|
|
||||||
:sla-name="$t('SLA.LIST.EMPTY.TITLE_2')"
|
|
||||||
:description="$t('SLA.LIST.EMPTY.DESC_2')"
|
|
||||||
first-response="2h"
|
|
||||||
next-response="4h"
|
|
||||||
resolution-time="4d"
|
|
||||||
has-business-hours
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 flex flex-col items-center justify-center w-full h-full bg-gradient-to-t from-white dark:from-slate-900 to-transparent"
|
|
||||||
>
|
|
||||||
<p class="max-w-xs text-sm font-medium text-center">
|
|
||||||
{{ $t('SLA.LIST.404') }}
|
|
||||||
</p>
|
|
||||||
<woot-button
|
|
||||||
color-scheme="primary"
|
|
||||||
icon="plus-sign"
|
|
||||||
class="px-5 mt-4 rounded-xl"
|
|
||||||
@click="openAddPopup"
|
|
||||||
>
|
|
||||||
{{ $t('SLA.ADD_ACTION_LONG') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="records.length" class="flex flex-col w-full h-full gap-3">
|
|
||||||
<SLA-list-item
|
<SLA-list-item
|
||||||
v-for="sla in records"
|
v-for="sla in records"
|
||||||
:key="sla.title"
|
:key="sla.title"
|
||||||
@@ -80,25 +48,30 @@
|
|||||||
</settings-layout>
|
</settings-layout>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import AddSLA from './AddSLA.vue';
|
||||||
import SettingsLayout from '../SettingsLayout.vue';
|
import SettingsLayout from '../SettingsLayout.vue';
|
||||||
|
import SLAEmptyState from './components/SLAEmptyState.vue';
|
||||||
import SLAHeader from './components/SLAHeader.vue';
|
import SLAHeader from './components/SLAHeader.vue';
|
||||||
import SLAListItem from './components/SLAListItem.vue';
|
import SLAListItem from './components/SLAListItem.vue';
|
||||||
import SLAListItemLoading from './components/SLAListItemLoading.vue';
|
import SLAListItemLoading from './components/SLAListItemLoading.vue';
|
||||||
|
import SLAPaywall from './components/SLAPaywall.vue';
|
||||||
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { convertSecondsToTimeUnit } from '@chatwoot/utils';
|
import { convertSecondsToTimeUnit } from '@chatwoot/utils';
|
||||||
|
|
||||||
import AddSLA from './AddSLA.vue';
|
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AddSLA,
|
AddSLA,
|
||||||
|
SettingsLayout,
|
||||||
|
SLAEmptyState,
|
||||||
SLAHeader,
|
SLAHeader,
|
||||||
SLAListItem,
|
SLAListItem,
|
||||||
SLAListItemLoading,
|
SLAListItemLoading,
|
||||||
SettingsLayout,
|
SLAPaywall,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
mixins: [alertMixin, adminMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: {},
|
loading: {},
|
||||||
@@ -109,8 +82,12 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||||
records: 'sla/getSLA',
|
records: 'sla/getSLA',
|
||||||
uiFlags: 'sla/getUIFlags',
|
uiFlags: 'sla/getUIFlags',
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
getAccount: 'accounts/getAccount',
|
||||||
|
isTrialAccount: 'accounts/isTrialAccount',
|
||||||
}),
|
}),
|
||||||
deleteConfirmText() {
|
deleteConfirmText() {
|
||||||
return this.$t('SLA.DELETE.CONFIRM.YES');
|
return this.$t('SLA.DELETE.CONFIRM.YES');
|
||||||
@@ -121,6 +98,19 @@ export default {
|
|||||||
deleteMessage() {
|
deleteMessage() {
|
||||||
return ` ${this.selectedResponse.name}`;
|
return ` ${this.selectedResponse.name}`;
|
||||||
},
|
},
|
||||||
|
isBehindAPaywall() {
|
||||||
|
if (!this.isOnChatwootCloud) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTrial = this.isTrialAccount(this.accountId);
|
||||||
|
if (isTrial) return false;
|
||||||
|
|
||||||
|
const currentPlan = this.currentPlan();
|
||||||
|
if (!currentPlan) return true;
|
||||||
|
|
||||||
|
return ['Hacker', 'Startup'].includes(currentPlan);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('sla/get');
|
this.$store.dispatch('sla/get');
|
||||||
@@ -129,6 +119,12 @@ export default {
|
|||||||
openAddPopup() {
|
openAddPopup() {
|
||||||
this.showAddPopup = true;
|
this.showAddPopup = true;
|
||||||
},
|
},
|
||||||
|
currentPlan() {
|
||||||
|
const account = this.getAccount(this.accountId);
|
||||||
|
if (!account) return '';
|
||||||
|
|
||||||
|
return account.custom_attributes?.plan_name || '';
|
||||||
|
},
|
||||||
hideAddPopup() {
|
hideAddPopup() {
|
||||||
this.showAddPopup = false;
|
this.showAddPopup = false;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import SLAListItem from './SLAListItem.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full min-h-[12rem] relative">
|
||||||
|
<div class="w-full space-y-3">
|
||||||
|
<SLA-list-item
|
||||||
|
class="opacity-25 dark:opacity-20"
|
||||||
|
:sla-name="$t('SLA.LIST.EMPTY.TITLE_1')"
|
||||||
|
:description="$t('SLA.LIST.EMPTY.DESC_1')"
|
||||||
|
first-response="20m"
|
||||||
|
next-response="1h"
|
||||||
|
resolution-time="24h"
|
||||||
|
has-business-hours
|
||||||
|
/>
|
||||||
|
<SLA-list-item
|
||||||
|
class="opacity-25 dark:opacity-20"
|
||||||
|
:sla-name="$t('SLA.LIST.EMPTY.TITLE_2')"
|
||||||
|
:description="$t('SLA.LIST.EMPTY.DESC_2')"
|
||||||
|
first-response="2h"
|
||||||
|
next-response="4h"
|
||||||
|
resolution-time="4d"
|
||||||
|
has-business-hours
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 flex flex-col items-center justify-center w-full h-full bg-gradient-to-t from-white dark:from-slate-900 to-transparent"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup>
|
||||||
|
import BaseEmptyState from './BaseEmptyState.vue';
|
||||||
|
import { defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['primary-action']);
|
||||||
|
const primaryAction = () => emit('primary-action');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<base-empty-state>
|
||||||
|
<p class="max-w-xs text-sm font-medium text-center">
|
||||||
|
{{ $t('SLA.LIST.404') }}
|
||||||
|
</p>
|
||||||
|
<woot-button
|
||||||
|
color-scheme="primary"
|
||||||
|
icon="plus-sign"
|
||||||
|
class="px-5 mt-4 rounded-xl"
|
||||||
|
@click="primaryAction"
|
||||||
|
>
|
||||||
|
{{ $t('SLA.ADD_ACTION_LONG') }}
|
||||||
|
</woot-button>
|
||||||
|
</base-empty-state>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup>
|
||||||
|
import BaseEmptyState from './BaseEmptyState.vue';
|
||||||
|
import { getCurrentInstance } from 'vue';
|
||||||
|
|
||||||
|
// this is a hack to get the route and router from the parent component
|
||||||
|
// TODO: Move this to a separate util if we need to use this in multiple places
|
||||||
|
// https://github.com/vuejs/vue-router/issues/3760
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
const route = proxy.$route;
|
||||||
|
const router = proxy.$router;
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
isAdmin: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function routeToBilling() {
|
||||||
|
const accountId = route.params.accountId;
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: 'billing_settings_index',
|
||||||
|
params: { accountId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<base-empty-state>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center max-w-md px-6 pt-6 pb-3 bg-white border shadow rounded-xl border-slate-100"
|
||||||
|
>
|
||||||
|
<div class="flex items-center w-full gap-2">
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center w-6 h-6 rounded-full bg-woot-100"
|
||||||
|
>
|
||||||
|
<fluent-icon size="14" class="text-woot-500" icon="lock-closed" />
|
||||||
|
</span>
|
||||||
|
<span class="text-base font-medium text-slate-900">
|
||||||
|
{{ $t('SLA.PAYWALL.TITLE') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-sm font-normal tracking-[0.5%] mt-4"
|
||||||
|
v-html="$t('SLA.PAYWALL.AVAILABLE_ON')"
|
||||||
|
/>
|
||||||
|
<template v-if="isAdmin">
|
||||||
|
<p class="text-sm font-normal tracking-[0.5%]">
|
||||||
|
{{ $t('SLA.PAYWALL.UPGRADE_PROMPT') }}
|
||||||
|
</p>
|
||||||
|
<woot-button
|
||||||
|
color-scheme="primary"
|
||||||
|
class="w-full mt-2 text-center rounded-xl"
|
||||||
|
size="expanded"
|
||||||
|
is-expanded
|
||||||
|
@click="routeToBilling"
|
||||||
|
>
|
||||||
|
{{ $t('SLA.PAYWALL.UPGRADE_NOW') }}
|
||||||
|
</woot-button>
|
||||||
|
<p class="mt-2 text-xs tracking-tighter text-center">
|
||||||
|
{{ $t('SLA.PAYWALL.CANCEL_ANYTIME') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p class="text-sm font-normal tracking-[0.5%]">
|
||||||
|
{{ $t('SLA.PAYWALL.ASK_ADMIN') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</base-empty-state>
|
||||||
|
</template>
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||||
import * as types from '../mutation-types';
|
import * as types from '../mutation-types';
|
||||||
import AccountAPI from '../../api/account';
|
import AccountAPI from '../../api/account';
|
||||||
|
import { differenceInDays } from 'date-fns';
|
||||||
import EnterpriseAccountAPI from '../../api/enterprise/account';
|
import EnterpriseAccountAPI from '../../api/enterprise/account';
|
||||||
import { throwErrorMessage } from '../utils/api';
|
import { throwErrorMessage } from '../utils/api';
|
||||||
|
|
||||||
const findRecordById = ($state, id) =>
|
const findRecordById = ($state, id) =>
|
||||||
$state.records.find(record => record.id === Number(id)) || {};
|
$state.records.find(record => record.id === Number(id)) || {};
|
||||||
|
|
||||||
|
const TRIAL_PERIOD_DAYS = 15;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
records: [],
|
records: [],
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
@@ -19,11 +22,18 @@ const state = {
|
|||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getAccount: $state => id => {
|
getAccount: $state => id => {
|
||||||
return $state.records.find(record => record.id === Number(id)) || {};
|
return findRecordById($state, id);
|
||||||
},
|
},
|
||||||
getUIFlags($state) {
|
getUIFlags($state) {
|
||||||
return $state.uiFlags;
|
return $state.uiFlags;
|
||||||
},
|
},
|
||||||
|
isTrialAccount: $state => id => {
|
||||||
|
const account = findRecordById($state, id);
|
||||||
|
const createdAt = new Date(account.created_at);
|
||||||
|
const diffDays = differenceInDays(new Date(), createdAt);
|
||||||
|
|
||||||
|
return diffDays <= TRIAL_PERIOD_DAYS;
|
||||||
|
},
|
||||||
isFeatureEnabledonAccount:
|
isFeatureEnabledonAccount:
|
||||||
($state, _, __, rootGetters) => (id, featureName) => {
|
($state, _, __, rootGetters) => (id, featureName) => {
|
||||||
// If a user is SuperAdmin and has access to the account, then they would see all the available features
|
// If a user is SuperAdmin and has access to the account, then they would see all the available features
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Enterprise::Billing::HandleStripeEventService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def features_to_update
|
def features_to_update
|
||||||
%w[help_center campaigns team_management channel_twitter channel_facebook channel_email]
|
%w[help_center campaigns team_management sla channel_twitter channel_facebook channel_email]
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscription
|
def subscription
|
||||||
|
|||||||
Reference in New Issue
Block a user