chore: clean up workspace and fix backend prisma build
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
|
||||
defineProps({
|
||||
keepAlive: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('campaigns/get');
|
||||
store.dispatch('labels/get');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-surface-1"
|
||||
>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive v-if="keepAlive">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-else />
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,87 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import CampaignLayout from 'dashboard/components-next/Campaigns/CampaignLayout.vue';
|
||||
import CampaignList from 'dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue';
|
||||
import LiveChatCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignDialog.vue';
|
||||
import EditLiveChatCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/EditLiveChatCampaignDialog.vue';
|
||||
import ConfirmDeleteCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/ConfirmDeleteCampaignDialog.vue';
|
||||
import LiveChatCampaignEmptyState from 'dashboard/components-next/Campaigns/EmptyState/LiveChatCampaignEmptyState.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const editLiveChatCampaignDialogRef = ref(null);
|
||||
const confirmDeleteCampaignDialogRef = ref(null);
|
||||
const selectedCampaign = ref(null);
|
||||
|
||||
const uiFlags = useMapGetter('campaigns/getUIFlags');
|
||||
const isFetchingCampaigns = computed(() => uiFlags.value.isFetching);
|
||||
|
||||
const [showLiveChatCampaignDialog, toggleLiveChatCampaignDialog] = useToggle();
|
||||
|
||||
const liveChatCampaigns = computed(
|
||||
() => getters['campaigns/getLiveChatCampaigns'].value
|
||||
);
|
||||
|
||||
const hasNoLiveChatCampaigns = computed(
|
||||
() => liveChatCampaigns.value?.length === 0 && !isFetchingCampaigns.value
|
||||
);
|
||||
|
||||
const handleEdit = campaign => {
|
||||
selectedCampaign.value = campaign;
|
||||
editLiveChatCampaignDialogRef.value.dialogRef.open();
|
||||
};
|
||||
const handleDelete = campaign => {
|
||||
selectedCampaign.value = campaign;
|
||||
confirmDeleteCampaignDialogRef.value.dialogRef.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CampaignLayout
|
||||
:header-title="t('CAMPAIGN.LIVE_CHAT.HEADER_TITLE')"
|
||||
:button-label="t('CAMPAIGN.LIVE_CHAT.NEW_CAMPAIGN')"
|
||||
@click="toggleLiveChatCampaignDialog()"
|
||||
@close="toggleLiveChatCampaignDialog(false)"
|
||||
>
|
||||
<template #action>
|
||||
<LiveChatCampaignDialog
|
||||
v-if="showLiveChatCampaignDialog"
|
||||
@close="toggleLiveChatCampaignDialog(false)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="isFetchingCampaigns"
|
||||
class="flex justify-center items-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<CampaignList
|
||||
v-else-if="!hasNoLiveChatCampaigns"
|
||||
:campaigns="liveChatCampaigns"
|
||||
is-live-chat-type
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<LiveChatCampaignEmptyState
|
||||
v-else
|
||||
:title="t('CAMPAIGN.LIVE_CHAT.EMPTY_STATE.TITLE')"
|
||||
:subtitle="t('CAMPAIGN.LIVE_CHAT.EMPTY_STATE.SUBTITLE')"
|
||||
class="pt-14"
|
||||
/>
|
||||
<EditLiveChatCampaignDialog
|
||||
ref="editLiveChatCampaignDialogRef"
|
||||
:selected-campaign="selectedCampaign"
|
||||
/>
|
||||
<ConfirmDeleteCampaignDialog
|
||||
ref="confirmDeleteCampaignDialogRef"
|
||||
:selected-campaign="selectedCampaign"
|
||||
/>
|
||||
</CampaignLayout>
|
||||
</template>
|
||||
@@ -1,72 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import CampaignLayout from 'dashboard/components-next/Campaigns/CampaignLayout.vue';
|
||||
import CampaignList from 'dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue';
|
||||
import SMSCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignDialog.vue';
|
||||
import ConfirmDeleteCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/ConfirmDeleteCampaignDialog.vue';
|
||||
import SMSCampaignEmptyState from 'dashboard/components-next/Campaigns/EmptyState/SMSCampaignEmptyState.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const selectedCampaign = ref(null);
|
||||
const [showSMSCampaignDialog, toggleSMSCampaignDialog] = useToggle();
|
||||
|
||||
const uiFlags = useMapGetter('campaigns/getUIFlags');
|
||||
const isFetchingCampaigns = computed(() => uiFlags.value.isFetching);
|
||||
|
||||
const confirmDeleteCampaignDialogRef = ref(null);
|
||||
|
||||
const SMSCampaigns = computed(() => getters['campaigns/getSMSCampaigns'].value);
|
||||
|
||||
const hasNoSMSCampaigns = computed(
|
||||
() => SMSCampaigns.value?.length === 0 && !isFetchingCampaigns.value
|
||||
);
|
||||
|
||||
const handleDelete = campaign => {
|
||||
selectedCampaign.value = campaign;
|
||||
confirmDeleteCampaignDialogRef.value.dialogRef.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CampaignLayout
|
||||
:header-title="t('CAMPAIGN.SMS.HEADER_TITLE')"
|
||||
:button-label="t('CAMPAIGN.SMS.NEW_CAMPAIGN')"
|
||||
@click="toggleSMSCampaignDialog()"
|
||||
@close="toggleSMSCampaignDialog(false)"
|
||||
>
|
||||
<template #action>
|
||||
<SMSCampaignDialog
|
||||
v-if="showSMSCampaignDialog"
|
||||
@close="toggleSMSCampaignDialog(false)"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-if="isFetchingCampaigns"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<CampaignList
|
||||
v-else-if="!hasNoSMSCampaigns"
|
||||
:campaigns="SMSCampaigns"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<SMSCampaignEmptyState
|
||||
v-else
|
||||
:title="t('CAMPAIGN.SMS.EMPTY_STATE.TITLE')"
|
||||
:subtitle="t('CAMPAIGN.SMS.EMPTY_STATE.SUBTITLE')"
|
||||
class="pt-14"
|
||||
/>
|
||||
<ConfirmDeleteCampaignDialog
|
||||
ref="confirmDeleteCampaignDialogRef"
|
||||
:selected-campaign="selectedCampaign"
|
||||
/>
|
||||
</CampaignLayout>
|
||||
</template>
|
||||
@@ -1,74 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import CampaignLayout from 'dashboard/components-next/Campaigns/CampaignLayout.vue';
|
||||
import CampaignList from 'dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue';
|
||||
import WhatsAppCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue';
|
||||
import ConfirmDeleteCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/ConfirmDeleteCampaignDialog.vue';
|
||||
import WhatsAppCampaignEmptyState from 'dashboard/components-next/Campaigns/EmptyState/WhatsAppCampaignEmptyState.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const selectedCampaign = ref(null);
|
||||
const [showWhatsAppCampaignDialog, toggleWhatsAppCampaignDialog] = useToggle();
|
||||
|
||||
const uiFlags = useMapGetter('campaigns/getUIFlags');
|
||||
const isFetchingCampaigns = computed(() => uiFlags.value.isFetching);
|
||||
|
||||
const confirmDeleteCampaignDialogRef = ref(null);
|
||||
|
||||
const WhatsAppCampaigns = computed(
|
||||
() => getters['campaigns/getWhatsAppCampaigns'].value
|
||||
);
|
||||
|
||||
const hasNoWhatsAppCampaigns = computed(
|
||||
() => WhatsAppCampaigns.value?.length === 0 && !isFetchingCampaigns.value
|
||||
);
|
||||
|
||||
const handleDelete = campaign => {
|
||||
selectedCampaign.value = campaign;
|
||||
confirmDeleteCampaignDialogRef.value.dialogRef.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CampaignLayout
|
||||
:header-title="t('CAMPAIGN.WHATSAPP.HEADER_TITLE')"
|
||||
:button-label="t('CAMPAIGN.WHATSAPP.NEW_CAMPAIGN')"
|
||||
@click="toggleWhatsAppCampaignDialog()"
|
||||
@close="toggleWhatsAppCampaignDialog(false)"
|
||||
>
|
||||
<template #action>
|
||||
<WhatsAppCampaignDialog
|
||||
v-if="showWhatsAppCampaignDialog"
|
||||
@close="toggleWhatsAppCampaignDialog(false)"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-if="isFetchingCampaigns"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<CampaignList
|
||||
v-else-if="!hasNoWhatsAppCampaigns"
|
||||
:campaigns="WhatsAppCampaigns"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<WhatsAppCampaignEmptyState
|
||||
v-else
|
||||
:title="t('CAMPAIGN.WHATSAPP.EMPTY_STATE.TITLE')"
|
||||
:subtitle="t('CAMPAIGN.WHATSAPP.EMPTY_STATE.SUBTITLE')"
|
||||
class="pt-14"
|
||||
/>
|
||||
<ConfirmDeleteCampaignDialog
|
||||
ref="confirmDeleteCampaignDialogRef"
|
||||
:selected-campaign="selectedCampaign"
|
||||
/>
|
||||
</CampaignLayout>
|
||||
</template>
|
||||
@@ -1,89 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const { uiSettings } = useUISettings();
|
||||
const route = useRoute();
|
||||
|
||||
const assistants = computed(
|
||||
() => store.getters['captainAssistants/getRecords']
|
||||
);
|
||||
|
||||
const isAssistantPresent = assistantId => {
|
||||
return !!assistants.value.find(a => a.id === Number(assistantId));
|
||||
};
|
||||
|
||||
const routeToView = (name, params) => {
|
||||
router.replace({ name, params, replace: true });
|
||||
};
|
||||
|
||||
const generateRouterParams = () => {
|
||||
const { last_active_assistant_id: lastActiveAssistantId } =
|
||||
uiSettings.value || {};
|
||||
|
||||
if (isAssistantPresent(lastActiveAssistantId)) {
|
||||
return {
|
||||
assistantId: lastActiveAssistantId,
|
||||
};
|
||||
}
|
||||
|
||||
if (assistants.value.length > 0) {
|
||||
const { id: assistantId } = assistants.value[0];
|
||||
return { assistantId };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const routeToLastActiveAssistant = () => {
|
||||
const params = generateRouterParams();
|
||||
|
||||
// No assistants found, redirect to create page
|
||||
if (!params) {
|
||||
return routeToView('captain_assistants_create_index', {
|
||||
accountId: route.params.accountId,
|
||||
});
|
||||
}
|
||||
|
||||
const { navigationPath } = route.params;
|
||||
const isAValidRoute = [
|
||||
'captain_assistants_responses_index', // Faq page
|
||||
'captain_assistants_documents_index', // Document page
|
||||
'captain_assistants_scenarios_index', // Scenario page
|
||||
'captain_assistants_playground_index', // Playground page
|
||||
'captain_assistants_inboxes_index', // Inboxes page
|
||||
'captain_tools_index', // Tools page
|
||||
'captain_assistants_settings_index', // Settings page
|
||||
].includes(navigationPath);
|
||||
|
||||
const navigateTo = isAValidRoute
|
||||
? navigationPath
|
||||
: 'captain_assistants_responses_index';
|
||||
|
||||
return routeToView(navigateTo, {
|
||||
accountId: route.params.accountId,
|
||||
...params,
|
||||
});
|
||||
};
|
||||
|
||||
const performRouting = async () => {
|
||||
await store.dispatch('captainAssistants/get');
|
||||
nextTick(() => routeToLastActiveAssistant());
|
||||
};
|
||||
|
||||
onMounted(() => performRouting());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center w-full bg-n-surface-1 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup>
|
||||
import { watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
const route = useRoute();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
watch(
|
||||
() => route.params.assistantId,
|
||||
newAssistantId => {
|
||||
if (
|
||||
newAssistantId &&
|
||||
newAssistantId !== String(uiSettings.value.last_active_assistant_id)
|
||||
) {
|
||||
updateUISettings({
|
||||
last_active_assistant_id: Number(newAssistantId),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full h-full min-h-0">
|
||||
<section class="flex flex-1 h-full px-0 overflow-hidden bg-n-surface-1">
|
||||
<router-view />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,171 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, reactive } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useCompaniesStore } from 'dashboard/stores/companies';
|
||||
|
||||
import CompaniesListLayout from 'dashboard/components-next/Companies/CompaniesListLayout.vue';
|
||||
import CompaniesCard from 'dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue';
|
||||
|
||||
const DEFAULT_SORT_FIELD = 'name';
|
||||
const DEBOUNCE_DELAY = 300;
|
||||
|
||||
const companiesStore = useCompaniesStore();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { updateUISettings, uiSettings } = useUISettings();
|
||||
|
||||
const companies = computed(() => companiesStore.getCompaniesList);
|
||||
const meta = computed(() => companiesStore.getMeta);
|
||||
const uiFlags = computed(() => companiesStore.getUIFlags);
|
||||
|
||||
const searchQuery = computed(() => route.query?.search || '');
|
||||
const searchValue = ref(searchQuery.value);
|
||||
const pageNumber = computed(() => Number(route.query?.page) || 1);
|
||||
|
||||
const parseSortSettings = (sortString = '') => {
|
||||
const hasDescending = sortString.startsWith('-');
|
||||
const sortField = hasDescending ? sortString.slice(1) : sortString;
|
||||
return {
|
||||
sort: sortField || DEFAULT_SORT_FIELD,
|
||||
order: hasDescending ? '-' : '',
|
||||
};
|
||||
};
|
||||
|
||||
const { companies_sort_by: companySortBy = DEFAULT_SORT_FIELD } =
|
||||
uiSettings.value ?? {};
|
||||
const { sort: initialSort, order: initialOrder } =
|
||||
parseSortSettings(companySortBy);
|
||||
|
||||
const sortState = reactive({
|
||||
activeSort: initialSort,
|
||||
activeOrdering: initialOrder,
|
||||
});
|
||||
|
||||
const activeSort = computed(() => sortState.activeSort);
|
||||
const activeOrdering = computed(() => sortState.activeOrdering);
|
||||
|
||||
const isFetchingList = computed(() => uiFlags.value.fetchingList);
|
||||
|
||||
const buildSortAttr = () =>
|
||||
`${sortState.activeOrdering}${sortState.activeSort}`;
|
||||
|
||||
const sortParam = computed(() => buildSortAttr());
|
||||
|
||||
const updateURLParams = (page, search = '', sort = '') => {
|
||||
const query = {
|
||||
...route.query,
|
||||
page: page.toString(),
|
||||
};
|
||||
|
||||
if (search) {
|
||||
query.search = search;
|
||||
} else {
|
||||
delete query.search;
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
query.sort = sort;
|
||||
} else {
|
||||
delete query.sort;
|
||||
}
|
||||
|
||||
router.replace({ query });
|
||||
};
|
||||
|
||||
const fetchCompanies = async (page, search, sort) => {
|
||||
const currentPage = page ?? pageNumber.value;
|
||||
const currentSearch = search ?? searchQuery.value;
|
||||
const currentSort = sort ?? sortParam.value;
|
||||
|
||||
// Only update URL if arguments were explicitly provided
|
||||
if (page !== undefined || search !== undefined || sort !== undefined) {
|
||||
updateURLParams(currentPage, currentSearch, currentSort);
|
||||
}
|
||||
|
||||
if (currentSearch) {
|
||||
await companiesStore.search({
|
||||
search: currentSearch,
|
||||
page: currentPage,
|
||||
sort: currentSort,
|
||||
});
|
||||
} else {
|
||||
await companiesStore.get({
|
||||
page: currentPage,
|
||||
sort: currentSort,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSearch = debounce(query => {
|
||||
searchValue.value = query;
|
||||
fetchCompanies(1, query, sortParam.value);
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
const onPageChange = page => {
|
||||
fetchCompanies(page, searchValue.value, sortParam.value);
|
||||
};
|
||||
|
||||
const handleSort = async ({ sort, order }) => {
|
||||
Object.assign(sortState, { activeSort: sort, activeOrdering: order });
|
||||
|
||||
await updateUISettings({
|
||||
companies_sort_by: buildSortAttr(),
|
||||
});
|
||||
|
||||
fetchCompanies(1, searchValue.value, buildSortAttr());
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
searchValue.value = searchQuery.value;
|
||||
fetchCompanies();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CompaniesListLayout
|
||||
:search-value="searchValue"
|
||||
:header-title="t('COMPANIES.HEADER')"
|
||||
:current-page="pageNumber"
|
||||
:total-items="Number(meta.totalCount || 0)"
|
||||
:active-sort="activeSort"
|
||||
:active-ordering="activeOrdering"
|
||||
:is-fetching-list="isFetchingList"
|
||||
:show-pagination-footer="!!companies.length"
|
||||
@update:current-page="onPageChange"
|
||||
@update:sort="handleSort"
|
||||
@search="onSearch"
|
||||
>
|
||||
<div v-if="isFetchingList" class="flex items-center justify-center p-8">
|
||||
<span class="text-n-slate-11 text-base">{{
|
||||
t('COMPANIES.LOADING')
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="companies.length === 0"
|
||||
class="flex items-center justify-center p-8"
|
||||
>
|
||||
<span class="text-n-slate-11 text-base">{{
|
||||
t('COMPANIES.EMPTY_STATE.TITLE')
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<CompaniesCard
|
||||
v-for="company in companies"
|
||||
:id="company.id"
|
||||
:key="company.id"
|
||||
:name="company.name"
|
||||
:domain="company.domain"
|
||||
:contacts-count="company.contactsCount || 0"
|
||||
:description="company.description"
|
||||
:avatar-url="company.avatarUrl"
|
||||
:updated-at="company.updatedAt"
|
||||
/>
|
||||
</div>
|
||||
</CompaniesListLayout>
|
||||
</template>
|
||||
@@ -1,185 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted, computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import ContactsDetailsLayout from 'dashboard/components-next/Contacts/ContactsDetailsLayout.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import ContactDetails from 'dashboard/components-next/Contacts/Pages/ContactDetails.vue';
|
||||
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
|
||||
import ContactNotes from 'dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue';
|
||||
import ContactHistory from 'dashboard/components-next/Contacts/ContactsSidebar/ContactHistory.vue';
|
||||
import ContactMerge from 'dashboard/components-next/Contacts/ContactsSidebar/ContactMerge.vue';
|
||||
import ContactCustomAttributes from 'dashboard/components-next/Contacts/ContactsSidebar/ContactCustomAttributes.vue';
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const contact = useMapGetter('contacts/getContactById');
|
||||
const uiFlags = useMapGetter('contacts/getUIFlags');
|
||||
|
||||
const activeTab = ref('attributes');
|
||||
const contactMergeRef = ref(null);
|
||||
|
||||
const isFetchingItem = computed(() => uiFlags.value.isFetchingItem);
|
||||
const isMergingContact = computed(() => uiFlags.value.isMerging);
|
||||
const isUpdatingContact = computed(() => uiFlags.value.isUpdating);
|
||||
|
||||
const selectedContact = computed(() => contact.value(route.params.contactId));
|
||||
|
||||
const showSpinner = computed(
|
||||
() => isFetchingItem.value || isMergingContact.value
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const CONTACT_TABS_OPTIONS = [
|
||||
{ key: 'ATTRIBUTES', value: 'attributes' },
|
||||
{ key: 'HISTORY', value: 'history' },
|
||||
{ key: 'NOTES', value: 'notes' },
|
||||
{ key: 'MERGE', value: 'merge' },
|
||||
];
|
||||
|
||||
const tabs = computed(() => {
|
||||
return CONTACT_TABS_OPTIONS.map(tab => ({
|
||||
label: t(`CONTACTS_LAYOUT.SIDEBAR.TABS.${tab.key}`),
|
||||
value: tab.value,
|
||||
}));
|
||||
});
|
||||
|
||||
const activeTabIndex = computed(() => {
|
||||
return CONTACT_TABS_OPTIONS.findIndex(v => v.value === activeTab.value);
|
||||
});
|
||||
|
||||
const goToContactsList = () => {
|
||||
if (window.history.state?.back || window.history.length > 1) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push(`/app/accounts/${route.params.accountId}/contacts?page=1`);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchActiveContact = async () => {
|
||||
if (route.params.contactId) {
|
||||
await store.dispatch('contacts/show', { id: route.params.contactId });
|
||||
await store.dispatch(
|
||||
'contacts/fetchContactableInbox',
|
||||
route.params.contactId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = tab => {
|
||||
activeTab.value = tab.value;
|
||||
};
|
||||
|
||||
const fetchContactNotes = () => {
|
||||
const { contactId } = route.params;
|
||||
if (contactId) store.dispatch('contactNotes/get', { contactId });
|
||||
};
|
||||
|
||||
const fetchContactConversations = () => {
|
||||
const { contactId } = route.params;
|
||||
if (contactId) store.dispatch('contactConversations/get', contactId);
|
||||
};
|
||||
|
||||
const fetchAttributes = () => {
|
||||
store.dispatch('attributes/get');
|
||||
};
|
||||
|
||||
const toggleContactBlock = async isBlocked => {
|
||||
const ALERT_MESSAGES = {
|
||||
success: {
|
||||
block: t('CONTACTS_LAYOUT.HEADER.ACTIONS.BLOCK_SUCCESS_MESSAGE'),
|
||||
unblock: t('CONTACTS_LAYOUT.HEADER.ACTIONS.UNBLOCK_SUCCESS_MESSAGE'),
|
||||
},
|
||||
error: {
|
||||
block: t('CONTACTS_LAYOUT.HEADER.ACTIONS.BLOCK_ERROR_MESSAGE'),
|
||||
unblock: t('CONTACTS_LAYOUT.HEADER.ACTIONS.UNBLOCK_ERROR_MESSAGE'),
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await store.dispatch(`contacts/update`, {
|
||||
...selectedContact.value,
|
||||
blocked: !isBlocked,
|
||||
});
|
||||
useAlert(
|
||||
isBlocked ? ALERT_MESSAGES.success.unblock : ALERT_MESSAGES.success.block
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
isBlocked ? ALERT_MESSAGES.error.unblock : ALERT_MESSAGES.error.block
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchActiveContact();
|
||||
fetchContactNotes();
|
||||
fetchContactConversations();
|
||||
fetchAttributes();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-surface-1"
|
||||
>
|
||||
<ContactsDetailsLayout
|
||||
:button-label="$t('CONTACTS_LAYOUT.HEADER.SEND_MESSAGE')"
|
||||
:selected-contact="selectedContact"
|
||||
is-detail-view
|
||||
:show-pagination-footer="false"
|
||||
:is-updating="isUpdatingContact"
|
||||
@go-to-contacts-list="goToContactsList"
|
||||
@toggle-block="toggleContactBlock"
|
||||
>
|
||||
<div
|
||||
v-if="showSpinner"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<ContactDetails
|
||||
v-else-if="selectedContact"
|
||||
:selected-contact="selectedContact"
|
||||
@go-to-contacts-list="goToContactsList"
|
||||
/>
|
||||
<template #sidebar>
|
||||
<div class="px-6">
|
||||
<TabBar
|
||||
:tabs="tabs"
|
||||
:initial-active-tab="activeTabIndex"
|
||||
class="w-full [&>button]:w-full bg-n-alpha-black2"
|
||||
@tab-changed="handleTabChange"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="isFetchingItem"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<template v-else>
|
||||
<ContactCustomAttributes
|
||||
v-if="activeTab === 'attributes'"
|
||||
:selected-contact="selectedContact"
|
||||
/>
|
||||
<ContactNotes v-if="activeTab === 'notes'" />
|
||||
<ContactHistory v-if="activeTab === 'history'" />
|
||||
<ContactMerge
|
||||
v-if="activeTab === 'merge'"
|
||||
ref="contactMergeRef"
|
||||
:selected-contact="selectedContact"
|
||||
@go-to-contacts-list="goToContactsList"
|
||||
@reset-tab="handleTabChange(CONTACT_TABS_OPTIONS[0])"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</ContactsDetailsLayout>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,516 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted, computed, ref, reactive, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
||||
|
||||
import ContactsListLayout from 'dashboard/components-next/Contacts/ContactsListLayout.vue';
|
||||
import ContactEmptyState from 'dashboard/components-next/Contacts/EmptyState/ContactEmptyState.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import ContactsList from 'dashboard/components-next/Contacts/Pages/ContactsList.vue';
|
||||
import ContactsBulkActionBar from '../components/ContactsBulkActionBar.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import BulkActionsAPI from 'dashboard/api/bulkActions';
|
||||
|
||||
const DEFAULT_SORT_FIELD = 'last_activity_at';
|
||||
const DEBOUNCE_DELAY = 300;
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { updateUISettings, uiSettings } = useUISettings();
|
||||
|
||||
const contacts = useMapGetter('contacts/getContactsList');
|
||||
const uiFlags = useMapGetter('contacts/getUIFlags');
|
||||
const customViewsUiFlags = useMapGetter('customViews/getUIFlags');
|
||||
const segments = useMapGetter('customViews/getContactCustomViews');
|
||||
const appliedFilters = useMapGetter('contacts/getAppliedContactFilters');
|
||||
const meta = useMapGetter('contacts/getMeta');
|
||||
|
||||
const searchQuery = computed(() => route.query?.search);
|
||||
const searchValue = ref(searchQuery.value || '');
|
||||
const pageNumber = computed(() => Number(route.query?.page) || 1);
|
||||
// For infinite scroll in search, track page internally
|
||||
const searchPageNumber = ref(1);
|
||||
const isLoadingMore = ref(false);
|
||||
|
||||
const parseSortSettings = (sortString = '') => {
|
||||
const hasDescending = sortString.startsWith('-');
|
||||
const sortField = hasDescending ? sortString.slice(1) : sortString;
|
||||
return {
|
||||
sort: sortField || DEFAULT_SORT_FIELD,
|
||||
order: hasDescending ? '-' : '',
|
||||
};
|
||||
};
|
||||
|
||||
const { contacts_sort_by: contactSortBy = '' } = uiSettings.value ?? {};
|
||||
const { sort: initialSort, order: initialOrder } =
|
||||
parseSortSettings(contactSortBy);
|
||||
|
||||
const sortState = reactive({
|
||||
activeSort: initialSort,
|
||||
activeOrdering: initialOrder,
|
||||
});
|
||||
|
||||
const activeLabel = computed(() => route.params.label);
|
||||
const activeSegmentId = computed(() => route.params.segmentId);
|
||||
const isFetchingList = computed(
|
||||
() => uiFlags.value.isFetching || customViewsUiFlags.value.isFetching
|
||||
);
|
||||
const currentPage = computed(() => Number(meta.value?.currentPage));
|
||||
const totalItems = computed(() => meta.value?.count);
|
||||
const hasMore = computed(() => meta.value?.hasMore ?? false);
|
||||
const isSearchView = computed(() => !!searchQuery.value);
|
||||
|
||||
const selectedContactIds = ref([]);
|
||||
const isBulkActionLoading = ref(false);
|
||||
const bulkDeleteDialogRef = ref(null);
|
||||
const selectedCount = computed(() => selectedContactIds.value.length);
|
||||
const bulkDeleteDialogTitle = computed(() =>
|
||||
selectedCount.value > 1
|
||||
? t('CONTACTS_BULK_ACTIONS.DELETE_DIALOG.TITLE')
|
||||
: t('CONTACTS_BULK_ACTIONS.DELETE_DIALOG.SINGULAR_TITLE')
|
||||
);
|
||||
const bulkDeleteDialogDescription = computed(() =>
|
||||
selectedCount.value > 1
|
||||
? t('CONTACTS_BULK_ACTIONS.DELETE_DIALOG.DESCRIPTION', {
|
||||
count: selectedCount.value,
|
||||
})
|
||||
: t('CONTACTS_BULK_ACTIONS.DELETE_DIALOG.SINGULAR_DESCRIPTION')
|
||||
);
|
||||
const bulkDeleteDialogConfirmLabel = computed(() =>
|
||||
selectedCount.value > 1
|
||||
? t('CONTACTS_BULK_ACTIONS.DELETE_DIALOG.CONFIRM_MULTIPLE')
|
||||
: t('CONTACTS_BULK_ACTIONS.DELETE_DIALOG.CONFIRM_SINGLE')
|
||||
);
|
||||
const hasSelection = computed(() => selectedCount.value > 0);
|
||||
const activeSegment = computed(() => {
|
||||
if (!activeSegmentId.value) return undefined;
|
||||
return segments.value.find(view => view.id === Number(activeSegmentId.value));
|
||||
});
|
||||
|
||||
const hasContacts = computed(() => contacts.value.length > 0);
|
||||
const isContactIndexView = computed(
|
||||
() => route.name === 'contacts_dashboard_index' && pageNumber.value === 1
|
||||
);
|
||||
const isActiveView = computed(() => route.name === 'contacts_dashboard_active');
|
||||
const hasAppliedFilters = computed(() => {
|
||||
return appliedFilters.value.length > 0;
|
||||
});
|
||||
|
||||
const showEmptyStateLayout = computed(() => {
|
||||
return (
|
||||
!searchQuery.value &&
|
||||
!hasContacts.value &&
|
||||
isContactIndexView.value &&
|
||||
!hasAppliedFilters.value
|
||||
);
|
||||
});
|
||||
const showEmptyText = computed(() => {
|
||||
return (
|
||||
(searchQuery.value ||
|
||||
hasAppliedFilters.value ||
|
||||
!isContactIndexView.value) &&
|
||||
!hasContacts.value
|
||||
);
|
||||
});
|
||||
|
||||
const headerTitle = computed(() => {
|
||||
if (searchQuery.value) return t('CONTACTS_LAYOUT.HEADER.SEARCH_TITLE');
|
||||
if (isActiveView.value) return t('CONTACTS_LAYOUT.HEADER.ACTIVE_TITLE');
|
||||
if (activeSegmentId.value) return activeSegment.value?.name;
|
||||
if (activeLabel.value) return `#${activeLabel.value}`;
|
||||
return t('CONTACTS_LAYOUT.HEADER.TITLE');
|
||||
});
|
||||
|
||||
const emptyStateMessage = computed(() => {
|
||||
if (isActiveView.value)
|
||||
return t('CONTACTS_LAYOUT.EMPTY_STATE.ACTIVE_EMPTY_STATE_TITLE');
|
||||
if (!searchQuery.value || hasAppliedFilters.value)
|
||||
return t('CONTACTS_LAYOUT.EMPTY_STATE.LIST_EMPTY_STATE_TITLE');
|
||||
return t('CONTACTS_LAYOUT.EMPTY_STATE.SEARCH_EMPTY_STATE_TITLE');
|
||||
});
|
||||
|
||||
const visibleContactIds = computed(() =>
|
||||
contacts.value.map(contact => contact.id)
|
||||
);
|
||||
|
||||
const clearSelection = () => {
|
||||
selectedContactIds.value = [];
|
||||
};
|
||||
|
||||
const openBulkDeleteDialog = () => {
|
||||
if (!selectedContactIds.value.length || isBulkActionLoading.value) return;
|
||||
bulkDeleteDialogRef.value?.open?.();
|
||||
};
|
||||
|
||||
const toggleSelectAll = shouldSelect => {
|
||||
selectedContactIds.value = shouldSelect ? [...visibleContactIds.value] : [];
|
||||
};
|
||||
|
||||
const toggleContactSelection = ({ id, value }) => {
|
||||
const isAlreadySelected = selectedContactIds.value.includes(id);
|
||||
const shouldSelect = value ?? !isAlreadySelected;
|
||||
|
||||
if (shouldSelect && !isAlreadySelected) {
|
||||
selectedContactIds.value = [...selectedContactIds.value, id];
|
||||
} else if (!shouldSelect && isAlreadySelected) {
|
||||
selectedContactIds.value = selectedContactIds.value.filter(
|
||||
contactId => contactId !== id
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePageParam = (page, search = '') => {
|
||||
const query = {
|
||||
...route.query,
|
||||
page: page.toString(),
|
||||
...(search ? { search } : {}),
|
||||
};
|
||||
|
||||
if (!search) {
|
||||
delete query.search;
|
||||
}
|
||||
|
||||
router.replace({ query });
|
||||
};
|
||||
|
||||
const buildSortAttr = () =>
|
||||
`${sortState.activeOrdering}${sortState.activeSort}`;
|
||||
|
||||
const getCommonFetchParams = (page = 1) => ({
|
||||
page,
|
||||
sortAttr: buildSortAttr(),
|
||||
label: activeLabel.value,
|
||||
});
|
||||
|
||||
const fetchContacts = async (page = 1) => {
|
||||
clearSelection();
|
||||
await store.dispatch('contacts/clearContactFilters');
|
||||
await store.dispatch('contacts/get', getCommonFetchParams(page));
|
||||
updatePageParam(page);
|
||||
};
|
||||
|
||||
const fetchSavedOrAppliedFilteredContact = async (payload, page = 1) => {
|
||||
if (!activeSegmentId.value && !hasAppliedFilters.value) return;
|
||||
clearSelection();
|
||||
await store.dispatch('contacts/filter', {
|
||||
...getCommonFetchParams(page),
|
||||
queryPayload: payload,
|
||||
});
|
||||
updatePageParam(page);
|
||||
};
|
||||
|
||||
const fetchActiveContacts = async (page = 1) => {
|
||||
clearSelection();
|
||||
await store.dispatch('contacts/clearContactFilters');
|
||||
await store.dispatch('contacts/active', {
|
||||
page,
|
||||
sortAttr: buildSortAttr(),
|
||||
});
|
||||
updatePageParam(page);
|
||||
};
|
||||
|
||||
const searchContacts = debounce(async (value, page = 1, append = false) => {
|
||||
if (!append) {
|
||||
clearSelection();
|
||||
searchPageNumber.value = 1;
|
||||
}
|
||||
await store.dispatch('contacts/clearContactFilters');
|
||||
searchValue.value = value;
|
||||
|
||||
if (!value) {
|
||||
updatePageParam(page);
|
||||
await fetchContacts(page);
|
||||
return;
|
||||
}
|
||||
|
||||
updatePageParam(page, value);
|
||||
await store.dispatch('contacts/search', {
|
||||
...getCommonFetchParams(page),
|
||||
search: encodeURIComponent(value),
|
||||
append,
|
||||
});
|
||||
searchPageNumber.value = page;
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
const loadMoreSearchResults = async () => {
|
||||
if (!hasMore.value || isLoadingMore.value) return;
|
||||
|
||||
isLoadingMore.value = true;
|
||||
const nextPage = searchPageNumber.value + 1;
|
||||
|
||||
await store.dispatch('contacts/search', {
|
||||
...getCommonFetchParams(nextPage),
|
||||
search: encodeURIComponent(searchValue.value),
|
||||
append: true,
|
||||
});
|
||||
|
||||
searchPageNumber.value = nextPage;
|
||||
isLoadingMore.value = false;
|
||||
};
|
||||
|
||||
const fetchContactsBasedOnContext = async page => {
|
||||
clearSelection();
|
||||
updatePageParam(page, searchValue.value);
|
||||
if (isFetchingList.value) return;
|
||||
if (searchQuery.value) {
|
||||
await searchContacts(searchQuery.value, page);
|
||||
return;
|
||||
}
|
||||
// Reset the search value when we change the view
|
||||
searchValue.value = '';
|
||||
// If we're on the active route, fetch active contacts
|
||||
if (isActiveView.value) {
|
||||
await fetchActiveContacts(page);
|
||||
return;
|
||||
}
|
||||
// If there are applied filters or active segment with query
|
||||
if (
|
||||
(hasAppliedFilters.value || activeSegment.value?.query) &&
|
||||
!activeLabel.value
|
||||
) {
|
||||
const queryPayload =
|
||||
activeSegment.value?.query || filterQueryGenerator(appliedFilters.value);
|
||||
await fetchSavedOrAppliedFilteredContact(queryPayload, page);
|
||||
return;
|
||||
}
|
||||
// Default case: fetch regular contacts + label
|
||||
await fetchContacts(page);
|
||||
};
|
||||
|
||||
const assignLabels = async labels => {
|
||||
if (!labels.length || !selectedContactIds.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
isBulkActionLoading.value = true;
|
||||
try {
|
||||
await BulkActionsAPI.create({
|
||||
type: 'Contact',
|
||||
ids: selectedContactIds.value,
|
||||
labels: { add: labels },
|
||||
});
|
||||
useAlert(t('CONTACTS_BULK_ACTIONS.ASSIGN_LABELS_SUCCESS'));
|
||||
clearSelection();
|
||||
await fetchContactsBasedOnContext(pageNumber.value);
|
||||
} catch (error) {
|
||||
useAlert(t('CONTACTS_BULK_ACTIONS.ASSIGN_LABELS_FAILED'));
|
||||
} finally {
|
||||
isBulkActionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteContacts = async () => {
|
||||
if (!selectedContactIds.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
isBulkActionLoading.value = true;
|
||||
try {
|
||||
await BulkActionsAPI.create({
|
||||
type: 'Contact',
|
||||
ids: selectedContactIds.value,
|
||||
action_name: 'delete',
|
||||
});
|
||||
useAlert(t('CONTACTS_BULK_ACTIONS.DELETE_SUCCESS'));
|
||||
clearSelection();
|
||||
await fetchContactsBasedOnContext(pageNumber.value);
|
||||
bulkDeleteDialogRef.value?.close?.();
|
||||
} catch (error) {
|
||||
useAlert(t('CONTACTS_BULK_ACTIONS.DELETE_FAILED'));
|
||||
} finally {
|
||||
isBulkActionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSort = async ({ sort, order }) => {
|
||||
Object.assign(sortState, { activeSort: sort, activeOrdering: order });
|
||||
|
||||
await updateUISettings({
|
||||
contacts_sort_by: buildSortAttr(),
|
||||
});
|
||||
|
||||
if (searchQuery.value) {
|
||||
await searchContacts(searchValue.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isActiveView.value) {
|
||||
await fetchActiveContacts();
|
||||
return;
|
||||
}
|
||||
|
||||
await (activeSegmentId.value || hasAppliedFilters.value
|
||||
? fetchSavedOrAppliedFilteredContact(
|
||||
activeSegmentId.value
|
||||
? activeSegment.value?.query
|
||||
: filterQueryGenerator(appliedFilters.value)
|
||||
)
|
||||
: fetchContacts());
|
||||
};
|
||||
|
||||
const createContact = async contact => {
|
||||
await store.dispatch('contacts/create', contact);
|
||||
};
|
||||
|
||||
watch(
|
||||
contacts,
|
||||
newContacts => {
|
||||
const idsOnPage = newContacts.map(contact => contact.id);
|
||||
selectedContactIds.value = selectedContactIds.value.filter(id =>
|
||||
idsOnPage.includes(id)
|
||||
);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(hasSelection, value => {
|
||||
if (!value) {
|
||||
bulkDeleteDialogRef.value?.close?.();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => uiSettings.value?.contacts_sort_by,
|
||||
newSortBy => {
|
||||
if (newSortBy) {
|
||||
const { sort, order } = parseSortSettings(newSortBy);
|
||||
sortState.activeSort = sort;
|
||||
sortState.activeOrdering = order;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
[activeLabel, activeSegment, isActiveView],
|
||||
() => {
|
||||
fetchContactsBasedOnContext(pageNumber.value);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(searchQuery, value => {
|
||||
if (isFetchingList.value) return;
|
||||
searchValue.value = value || '';
|
||||
// Reset the view if there is search query when we click on the sidebar group
|
||||
if (value === undefined) {
|
||||
if (
|
||||
isActiveView.value ||
|
||||
activeLabel.value ||
|
||||
activeSegment.value ||
|
||||
hasAppliedFilters.value
|
||||
)
|
||||
return;
|
||||
fetchContacts();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!activeSegmentId.value) {
|
||||
if (searchQuery.value) {
|
||||
await searchContacts(searchQuery.value, pageNumber.value);
|
||||
return;
|
||||
}
|
||||
if (isActiveView.value) {
|
||||
await fetchActiveContacts(pageNumber.value);
|
||||
return;
|
||||
}
|
||||
await fetchContacts(pageNumber.value);
|
||||
} else if (activeSegment.value && activeSegmentId.value) {
|
||||
await fetchSavedOrAppliedFilteredContact(
|
||||
activeSegment.value.query,
|
||||
pageNumber.value
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-surface-1"
|
||||
>
|
||||
<ContactsListLayout
|
||||
:search-value="searchValue"
|
||||
:header-title="headerTitle"
|
||||
:current-page="currentPage"
|
||||
:total-items="totalItems"
|
||||
:show-pagination-footer="!isFetchingList && hasContacts && !isSearchView"
|
||||
:active-sort="sortState.activeSort"
|
||||
:active-ordering="sortState.activeOrdering"
|
||||
:active-segment="activeSegment"
|
||||
:segments-id="activeSegmentId"
|
||||
:is-fetching-list="isFetchingList"
|
||||
:has-applied-filters="hasAppliedFilters"
|
||||
:use-infinite-scroll="isSearchView"
|
||||
:has-more="hasMore"
|
||||
:is-loading-more="isLoadingMore"
|
||||
@update:current-page="fetchContactsBasedOnContext"
|
||||
@search="searchContacts"
|
||||
@update:sort="handleSort"
|
||||
@apply-filter="fetchSavedOrAppliedFilteredContact"
|
||||
@clear-filters="fetchContacts"
|
||||
@load-more="loadMoreSearchResults"
|
||||
>
|
||||
<div
|
||||
v-if="isFetchingList && !(isSearchView && hasContacts)"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<ContactsBulkActionBar
|
||||
v-if="hasSelection"
|
||||
:visible-contact-ids="visibleContactIds"
|
||||
:selected-contact-ids="selectedContactIds"
|
||||
:is-loading="isBulkActionLoading"
|
||||
@toggle-all="toggleSelectAll"
|
||||
@clear-selection="clearSelection"
|
||||
@assign-labels="assignLabels"
|
||||
@delete-selected="openBulkDeleteDialog"
|
||||
/>
|
||||
<ContactEmptyState
|
||||
v-if="showEmptyStateLayout"
|
||||
class="pt-14"
|
||||
:title="t('CONTACTS_LAYOUT.EMPTY_STATE.TITLE')"
|
||||
:subtitle="t('CONTACTS_LAYOUT.EMPTY_STATE.SUBTITLE')"
|
||||
:button-label="t('CONTACTS_LAYOUT.EMPTY_STATE.BUTTON_LABEL')"
|
||||
@create="createContact"
|
||||
/>
|
||||
<div
|
||||
v-else-if="showEmptyText"
|
||||
class="flex items-center justify-center py-10"
|
||||
>
|
||||
<span class="text-base text-n-slate-11">
|
||||
{{ emptyStateMessage }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-4 pt-4 pb-6">
|
||||
<ContactsList
|
||||
:contacts="contacts"
|
||||
:selected-contact-ids="selectedContactIds"
|
||||
@toggle-contact="toggleContactSelection"
|
||||
/>
|
||||
<Dialog
|
||||
v-if="selectedCount"
|
||||
ref="bulkDeleteDialogRef"
|
||||
type="alert"
|
||||
:title="bulkDeleteDialogTitle"
|
||||
:description="bulkDeleteDialogDescription"
|
||||
:confirm-button-label="bulkDeleteDialogConfirmLabel"
|
||||
:is-loading="isBulkActionLoading"
|
||||
@confirm="deleteContacts"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ContactsListLayout>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,76 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import UpgradePage from '../components/UpgradePage.vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const accountId = computed(() => store.getters.getCurrentAccountId);
|
||||
const portals = computed(() => store.getters['portals/allPortals']);
|
||||
const isFeatureEnabledonAccount = (id, flag) =>
|
||||
store.getters['accounts/isFeatureEnabledonAccount'](id, flag);
|
||||
|
||||
const isHelpCenterEnabled = computed(() =>
|
||||
isFeatureEnabledonAccount(accountId.value, FEATURE_FLAGS.HELP_CENTER)
|
||||
);
|
||||
|
||||
const selectedPortal = computed(() => {
|
||||
const slug =
|
||||
route.params.portalSlug || uiSettings.value.last_active_portal_slug;
|
||||
if (slug) return store.getters['portals/portalBySlug'](slug);
|
||||
return portals.value[0];
|
||||
});
|
||||
|
||||
const defaultPortalLocale = computed(() =>
|
||||
selectedPortal.value ? selectedPortal.value.meta?.default_locale : ''
|
||||
);
|
||||
const selectedLocaleInPortal = computed(
|
||||
() => route.params.locale || defaultPortalLocale.value
|
||||
);
|
||||
|
||||
const selectedPortalSlug = computed(() =>
|
||||
selectedPortal.value ? selectedPortal.value.slug : ''
|
||||
);
|
||||
|
||||
const fetchPortalAndItsCategories = async () => {
|
||||
await store.dispatch('portals/index');
|
||||
const selectedPortalParam = {
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: selectedLocaleInPortal.value,
|
||||
};
|
||||
store.dispatch('portals/show', selectedPortalParam);
|
||||
store.dispatch('categories/index', selectedPortalParam);
|
||||
store.dispatch('agents/get');
|
||||
};
|
||||
|
||||
onMounted(() => fetchPortalAndItsCategories());
|
||||
|
||||
watch(
|
||||
() => route.params.portalSlug,
|
||||
newSlug => {
|
||||
if (newSlug && newSlug !== uiSettings.value.last_active_portal_slug) {
|
||||
updateUISettings({
|
||||
last_active_portal_slug: newSlug,
|
||||
last_active_locale_code: selectedLocaleInPortal.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full h-full min-h-0">
|
||||
<section
|
||||
v-if="isHelpCenterEnabled"
|
||||
class="flex flex-1 h-full px-0 overflow-hidden bg-n-surface-1"
|
||||
>
|
||||
<router-view />
|
||||
</section>
|
||||
<UpgradePage v-else />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,119 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import ArticleEditor from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { articleSlug, portalSlug } = route.params;
|
||||
|
||||
const articleById = useMapGetter('articles/articleById');
|
||||
|
||||
const article = computed(() => articleById.value(articleSlug));
|
||||
|
||||
const portalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
const portal = computed(() => portalBySlug.value(portalSlug));
|
||||
|
||||
const isUpdating = ref(false);
|
||||
const isSaved = ref(false);
|
||||
|
||||
const articleLink = computed(() => {
|
||||
const { slug: categorySlug, locale: categoryLocale } = article.value.category;
|
||||
const { slug: articleSlugValue } = article.value;
|
||||
const portalCustomDomain = portal.value?.custom_domain;
|
||||
return buildPortalArticleURL(
|
||||
portalSlug,
|
||||
categorySlug,
|
||||
categoryLocale,
|
||||
articleSlugValue,
|
||||
portalCustomDomain
|
||||
);
|
||||
});
|
||||
|
||||
const saveArticle = async ({ ...values }, isAsync = false) => {
|
||||
const actionToDispatch = isAsync ? 'articles/updateAsync' : 'articles/update';
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
await store.dispatch(actionToDispatch, {
|
||||
portalSlug,
|
||||
articleId: articleSlug,
|
||||
...values,
|
||||
});
|
||||
isSaved.value = true;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message || t('HELP_CENTER.EDIT_ARTICLE_PAGE.API.ERROR');
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
isUpdating.value = false;
|
||||
isSaved.value = true;
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
const saveArticleAsync = async ({ ...values }) => {
|
||||
saveArticle({ ...values }, true);
|
||||
};
|
||||
|
||||
const isCategoryArticles = computed(() => {
|
||||
return (
|
||||
route.name === 'portals_categories_articles_index' ||
|
||||
route.name === 'portals_categories_articles_edit' ||
|
||||
route.name === 'portals_categories_index'
|
||||
);
|
||||
});
|
||||
|
||||
const goBackToArticles = () => {
|
||||
const { tab, categorySlug, locale } = route.params;
|
||||
if (isCategoryArticles.value) {
|
||||
router.push({
|
||||
name: 'portals_categories_articles_index',
|
||||
params: { categorySlug, locale },
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
name: 'portals_articles_index',
|
||||
params: { tab, categorySlug, locale },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchArticleDetails = () => {
|
||||
store.dispatch('articles/show', {
|
||||
id: articleSlug,
|
||||
portalSlug,
|
||||
});
|
||||
};
|
||||
|
||||
const previewArticle = () => {
|
||||
window.open(articleLink.value, '_blank');
|
||||
useTrack(PORTALS_EVENTS.PREVIEW_ARTICLE, {
|
||||
status: article.value?.status,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(fetchArticleDetails);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ArticleEditor
|
||||
:article="article"
|
||||
:is-updating="isUpdating"
|
||||
:is-saved="isSaved"
|
||||
@save-article="saveArticle"
|
||||
@save-article-async="saveArticleAsync"
|
||||
@preview-article="previewArticle"
|
||||
@go-back="goBackToArticles"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,116 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
import { getArticleStatus } from 'dashboard/helper/portalHelper.js';
|
||||
import ArticlesPage from 'dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticlesPage.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
const pageNumber = ref(1);
|
||||
|
||||
const articles = useMapGetter('articles/allArticles');
|
||||
const categories = useMapGetter('categories/allCategories');
|
||||
const meta = useMapGetter('articles/getMeta');
|
||||
const portalMeta = useMapGetter('portals/getMeta');
|
||||
const currentUserId = useMapGetter('getCurrentUserID');
|
||||
const getPortalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
const selectedPortalSlug = computed(() => route.params.portalSlug);
|
||||
const selectedCategorySlug = computed(() => route.params.categorySlug);
|
||||
const status = computed(() => getArticleStatus(route.params.tab));
|
||||
|
||||
const author = computed(() =>
|
||||
route.params.tab === 'mine' ? currentUserId.value : null
|
||||
);
|
||||
|
||||
const activeLocale = computed(() => route.params.locale);
|
||||
const portal = computed(() => getPortalBySlug.value(selectedPortalSlug.value));
|
||||
const allowedLocales = computed(() => {
|
||||
if (!portal.value) {
|
||||
return [];
|
||||
}
|
||||
const { allowed_locales: allAllowedLocales } = portal.value.config;
|
||||
return allAllowedLocales.map(locale => {
|
||||
return {
|
||||
id: locale.code,
|
||||
name: allLocales[locale.code],
|
||||
code: locale.code,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const defaultPortalLocale = computed(() => {
|
||||
return portal.value?.meta?.default_locale;
|
||||
});
|
||||
|
||||
const selectedLocaleInPortal = computed(() => {
|
||||
return route.params.locale || defaultPortalLocale.value;
|
||||
});
|
||||
|
||||
const isCategoryArticles = computed(() => {
|
||||
return (
|
||||
route.name === 'portals_categories_articles_index' ||
|
||||
route.name === 'portals_categories_articles_edit' ||
|
||||
route.name === 'portals_categories_index'
|
||||
);
|
||||
});
|
||||
|
||||
const fetchArticles = ({ pageNumber: pageNumberParam } = {}) => {
|
||||
store.dispatch('articles/index', {
|
||||
pageNumber: pageNumberParam || pageNumber.value,
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: activeLocale.value,
|
||||
status: status.value,
|
||||
authorId: author.value,
|
||||
categorySlug: selectedCategorySlug.value,
|
||||
});
|
||||
};
|
||||
|
||||
const onPageChange = pageNumberParam => {
|
||||
fetchArticles({ pageNumber: pageNumberParam });
|
||||
};
|
||||
|
||||
const fetchPortalAndItsCategories = async locale => {
|
||||
await store.dispatch('portals/index');
|
||||
const selectedPortalParam = {
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: locale || selectedLocaleInPortal.value,
|
||||
};
|
||||
store.dispatch('portals/show', selectedPortalParam);
|
||||
store.dispatch('categories/index', selectedPortalParam);
|
||||
store.dispatch('agents/get');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchArticles();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.params,
|
||||
() => {
|
||||
pageNumber.value = 1;
|
||||
fetchArticles();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<ArticlesPage
|
||||
v-if="portal"
|
||||
:articles="articles"
|
||||
:portal-name="portal.name"
|
||||
:categories="categories"
|
||||
:allowed-locales="allowedLocales"
|
||||
:meta="meta"
|
||||
:portal-meta="portalMeta"
|
||||
:is-category-articles="isCategoryArticles"
|
||||
@page-change="onPageChange"
|
||||
@fetch-portal="fetchPortalAndItsCategories"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,94 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
import ArticleEditor from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { portalSlug } = route.params;
|
||||
|
||||
const selectedAuthorId = ref(null);
|
||||
const selectedCategoryId = ref(null);
|
||||
|
||||
const currentUserId = useMapGetter('getCurrentUserID');
|
||||
const categories = useMapGetter('categories/allCategories');
|
||||
|
||||
const categoryId = computed(() => categories.value[0]?.id || null);
|
||||
|
||||
const article = ref({});
|
||||
const isUpdating = ref(false);
|
||||
const isSaved = ref(false);
|
||||
|
||||
const setAuthorId = authorId => {
|
||||
selectedAuthorId.value = authorId;
|
||||
};
|
||||
|
||||
const setCategoryId = newCategoryId => {
|
||||
selectedCategoryId.value = newCategoryId;
|
||||
};
|
||||
|
||||
const createNewArticle = async ({ title, content }) => {
|
||||
if (title) article.value.title = title;
|
||||
if (content) article.value.content = content;
|
||||
|
||||
if (!article.value.title || !article.value.content) return;
|
||||
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
const { locale } = route.params;
|
||||
const articleId = await store.dispatch('articles/create', {
|
||||
portalSlug,
|
||||
content: article.value.content,
|
||||
title: article.value.title,
|
||||
locale: locale,
|
||||
authorId: selectedAuthorId.value || currentUserId.value,
|
||||
categoryId: selectedCategoryId.value || categoryId.value,
|
||||
});
|
||||
|
||||
useTrack(PORTALS_EVENTS.CREATE_ARTICLE, { locale });
|
||||
|
||||
router.replace({
|
||||
name: 'portals_articles_edit',
|
||||
params: {
|
||||
articleSlug: articleId,
|
||||
portalSlug,
|
||||
locale,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message || t('HELP_CENTER.EDIT_ARTICLE_PAGE.API.ERROR');
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const goBackToArticles = () => {
|
||||
const { tab, categorySlug, locale } = route.params;
|
||||
router.push({
|
||||
name: 'portals_articles_index',
|
||||
params: { tab, categorySlug, locale },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ArticleEditor
|
||||
:article="article"
|
||||
:is-updating="isUpdating"
|
||||
:is-saved="isSaved"
|
||||
@save-article="createNewArticle"
|
||||
@go-back="goBackToArticles"
|
||||
@set-author="setAuthorId"
|
||||
@set-category="setCategoryId"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
|
||||
import CategoriesPage from 'dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoriesPage.vue';
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const categories = useMapGetter('categories/allCategories');
|
||||
|
||||
const selectedPortalSlug = computed(() => route.params.portalSlug);
|
||||
const getPortalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
const isFetching = useMapGetter('categories/isFetching');
|
||||
|
||||
const portal = computed(() => getPortalBySlug.value(selectedPortalSlug.value));
|
||||
|
||||
const allowedLocales = computed(() => {
|
||||
if (!portal.value) {
|
||||
return [];
|
||||
}
|
||||
const { allowed_locales: allAllowedLocales } = portal.value.config;
|
||||
return allAllowedLocales.map(locale => {
|
||||
return {
|
||||
id: locale.code,
|
||||
name: allLocales[locale.code],
|
||||
code: locale.code,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const fetchCategoriesByPortalSlugAndLocale = async localeCode => {
|
||||
await store.dispatch('categories/index', {
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: localeCode,
|
||||
});
|
||||
};
|
||||
|
||||
const updateMeta = async localeCode => {
|
||||
return store.dispatch('portals/show', {
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: localeCode,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchCategories = async localeCode => {
|
||||
await fetchCategoriesByPortalSlugAndLocale(localeCode);
|
||||
await updateMeta(localeCode);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategoriesByPortalSlugAndLocale(route.params.locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CategoriesPage
|
||||
:categories="categories"
|
||||
:is-fetching="isFetching"
|
||||
:allowed-locales="allowedLocales"
|
||||
@fetch-categories="fetchCategories"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,76 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const { uiSettings } = useUISettings();
|
||||
const route = useRoute();
|
||||
|
||||
const portals = computed(() => store.getters['portals/allPortals']);
|
||||
|
||||
const isPortalPresent = portalSlug => {
|
||||
return !!portals.value.find(portal => portal.slug === portalSlug);
|
||||
};
|
||||
|
||||
const routeToView = (name, params) => {
|
||||
router.replace({ name, params, replace: true });
|
||||
};
|
||||
|
||||
const generateRouterParams = () => {
|
||||
const {
|
||||
last_active_portal_slug: lastActivePortalSlug,
|
||||
last_active_locale_code: lastActiveLocaleCode,
|
||||
} = uiSettings.value || {};
|
||||
if (isPortalPresent(lastActivePortalSlug)) {
|
||||
return {
|
||||
portalSlug: lastActivePortalSlug,
|
||||
locale: lastActiveLocaleCode,
|
||||
};
|
||||
}
|
||||
|
||||
if (portals.value.length > 0) {
|
||||
const { slug: portalSlug, meta: { default_locale: locale } = {} } =
|
||||
portals.value[0];
|
||||
return { portalSlug, locale };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const routeToLastActivePortal = () => {
|
||||
const params = generateRouterParams();
|
||||
const { navigationPath } = route.params;
|
||||
const isAValidRoute = [
|
||||
'portals_articles_index',
|
||||
'portals_categories_index',
|
||||
'portals_locales_index',
|
||||
'portals_settings_index',
|
||||
].includes(navigationPath);
|
||||
|
||||
const navigateTo = isAValidRoute ? navigationPath : 'portals_articles_index';
|
||||
if (params) {
|
||||
return routeToView(navigateTo, params);
|
||||
}
|
||||
return routeToView('portals_new', {});
|
||||
};
|
||||
|
||||
const performRouting = async () => {
|
||||
await store.dispatch('portals/index');
|
||||
nextTick(() => routeToLastActivePortal());
|
||||
};
|
||||
|
||||
onMounted(() => performRouting());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center w-full bg-n-surface-1 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,34 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
|
||||
import LocalesPage from 'dashboard/components-next/HelpCenter/Pages/LocalePage/LocalesPage.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const getPortalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
const portal = computed(() => getPortalBySlug.value(route.params.portalSlug));
|
||||
|
||||
const allowedLocales = computed(() => {
|
||||
if (!portal.value) {
|
||||
return [];
|
||||
}
|
||||
const { allowed_locales: allAllowedLocales } = portal.value.config;
|
||||
return allAllowedLocales.map(locale => {
|
||||
return {
|
||||
id: locale?.code,
|
||||
name: allLocales[locale?.code],
|
||||
code: locale?.code,
|
||||
articlesCount: locale?.articles_count || 0,
|
||||
categoriesCount: locale?.categories_count || 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LocalesPage :locales="allowedLocales" :portal="portal" />
|
||||
</template>
|
||||
@@ -1,9 +0,0 @@
|
||||
<script setup>
|
||||
import PortalEmptyState from 'dashboard/components-next/HelpCenter/EmptyState/Portal/PortalEmptyState.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full bg-n-background">
|
||||
<PortalEmptyState />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,164 +0,0 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import PortalSettings from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue';
|
||||
|
||||
const SSL_STATUS_FETCH_INTERVAL = 5000;
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
const portals = useMapGetter('portals/allPortals');
|
||||
const isFetching = useMapGetter('portals/isFetchingPortals');
|
||||
const getPortalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
const getNextAvailablePortal = deletedPortalSlug =>
|
||||
portals.value?.find(portal => portal.slug !== deletedPortalSlug) ?? null;
|
||||
|
||||
const getDefaultLocale = slug => {
|
||||
return getPortalBySlug.value(slug)?.meta?.default_locale;
|
||||
};
|
||||
|
||||
const fetchSSLStatus = () => {
|
||||
if (!isOnChatwootCloud.value) return;
|
||||
|
||||
const { portalSlug } = route.params;
|
||||
store.dispatch('portals/sslStatus', {
|
||||
portalSlug,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchPortalAndItsCategories = async (slug, locale) => {
|
||||
const selectedPortalParam = { portalSlug: slug, locale };
|
||||
await Promise.all([
|
||||
store.dispatch('portals/index'),
|
||||
store.dispatch('portals/show', selectedPortalParam),
|
||||
store.dispatch('categories/index', selectedPortalParam),
|
||||
store.dispatch('agents/get'),
|
||||
store.dispatch('inboxes/get'),
|
||||
]);
|
||||
};
|
||||
|
||||
const updateRouteAfterDeletion = async deletedPortalSlug => {
|
||||
const nextPortal = getNextAvailablePortal(deletedPortalSlug);
|
||||
if (nextPortal) {
|
||||
const {
|
||||
slug,
|
||||
meta: { default_locale: defaultLocale },
|
||||
} = nextPortal;
|
||||
await fetchPortalAndItsCategories(slug, defaultLocale);
|
||||
router.push({
|
||||
name: 'portals_articles_index',
|
||||
params: { portalSlug: slug, locale: defaultLocale },
|
||||
});
|
||||
} else {
|
||||
router.push({ name: 'portals_new' });
|
||||
}
|
||||
};
|
||||
|
||||
const refreshPortalRoute = async (newSlug, defaultLocale) => {
|
||||
// This is to refresh the portal route and update the UI settings
|
||||
// If there is slug change, this will be called to refresh the route and UI settings
|
||||
await fetchPortalAndItsCategories(newSlug, defaultLocale);
|
||||
updateUISettings({
|
||||
last_active_portal_slug: newSlug,
|
||||
last_active_locale_code: defaultLocale,
|
||||
});
|
||||
await router.replace({
|
||||
name: 'portals_settings_index',
|
||||
params: { portalSlug: newSlug },
|
||||
});
|
||||
};
|
||||
|
||||
const updatePortalSettings = async portalObj => {
|
||||
const { portalSlug } = route.params;
|
||||
try {
|
||||
const defaultLocale = getDefaultLocale(portalSlug);
|
||||
await store.dispatch('portals/update', {
|
||||
...portalObj,
|
||||
portalSlug: portalSlug || portalObj?.slug,
|
||||
});
|
||||
|
||||
// If there is a slug change, this will refresh the route and update the UI settings
|
||||
if (portalObj?.slug && portalSlug !== portalObj.slug) {
|
||||
await refreshPortalRoute(portalObj.slug, defaultLocale);
|
||||
}
|
||||
useAlert(
|
||||
t('HELP_CENTER.PORTAL_SETTINGS.API.UPDATE_PORTAL.SUCCESS_MESSAGE')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.message ||
|
||||
t('HELP_CENTER.PORTAL_SETTINGS.API.UPDATE_PORTAL.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const deletePortal = async selectedPortalForDelete => {
|
||||
const { slug } = selectedPortalForDelete;
|
||||
try {
|
||||
await store.dispatch('portals/delete', { portalSlug: slug });
|
||||
await updateRouteAfterDeletion(slug);
|
||||
useAlert(
|
||||
t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.message ||
|
||||
t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_ERROR')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendCnameInstructions = async payload => {
|
||||
try {
|
||||
await store.dispatch('portals/sendCnameInstructions', payload);
|
||||
useAlert(
|
||||
t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.SEND_CNAME_INSTRUCTIONS.API.SUCCESS_MESSAGE'
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.message ||
|
||||
t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.SEND_CNAME_INSTRUCTIONS.API.ERROR_MESSAGE'
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePortal = updatePortalSettings;
|
||||
const handleUpdatePortalConfiguration = portalObj => {
|
||||
updatePortalSettings(portalObj);
|
||||
|
||||
// If custom domain is added or updated, fetch SSL status after a delay of 5 seconds (only on Chatwoot cloud)
|
||||
if (portalObj?.custom_domain && isOnChatwootCloud.value) {
|
||||
setTimeout(() => {
|
||||
fetchSSLStatus();
|
||||
}, SSL_STATUS_FETCH_INTERVAL);
|
||||
}
|
||||
};
|
||||
const handleDeletePortal = deletePortal;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PortalSettings
|
||||
:portals="portals"
|
||||
:is-fetching="isFetching"
|
||||
@update-portal="handleUpdatePortal"
|
||||
@update-portal-configuration="handleUpdatePortalConfiguration"
|
||||
@delete-portal="handleDeletePortal"
|
||||
@refresh-status="fetchSSLStatus"
|
||||
@send-cname-instructions="handleSendCnameInstructions"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,107 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||
import AssignmentPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const formRef = ref(null);
|
||||
const uiFlags = useMapGetter('assignmentPolicies/getUIFlags');
|
||||
|
||||
const inboxIdFromQuery = computed(() => {
|
||||
const id = route.query.inboxId;
|
||||
return id ? Number(id) : null;
|
||||
});
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
if (inboxIdFromQuery.value) {
|
||||
return [
|
||||
{
|
||||
label: t('INBOX_MGMT.SETTINGS'),
|
||||
routeName: 'settings_inbox_show',
|
||||
params: { inboxId: inboxIdFromQuery.value },
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.CREATE.HEADER.TITLE'
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.HEADER.TITLE'),
|
||||
routeName: 'agent_assignment_policy_index',
|
||||
},
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.CREATE.HEADER.TITLE'),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const handleBreadcrumbClick = item => {
|
||||
if (item.params) {
|
||||
const accountId = route.params.accountId;
|
||||
const inboxId = item.params.inboxId;
|
||||
// Navigate using explicit path to ensure tab parameter is included
|
||||
router.push(
|
||||
`/app/accounts/${accountId}/settings/inboxes/${inboxId}/collaborators`
|
||||
);
|
||||
} else {
|
||||
router.push({
|
||||
name: item.routeName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async formState => {
|
||||
try {
|
||||
const policy = await store.dispatch('assignmentPolicies/create', formState);
|
||||
useAlert(
|
||||
t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.CREATE.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
formRef.value?.resetForm();
|
||||
|
||||
router.push({
|
||||
name: 'agent_assignment_policy_edit',
|
||||
params: {
|
||||
id: policy.id,
|
||||
},
|
||||
// Pass inboxId to edit page to show link prompt
|
||||
query: inboxIdFromQuery.value ? { inboxId: inboxIdFromQuery.value } : {},
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.CREATE.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout class="w-full max-w-2xl ltr:mr-auto rtl:ml-auto">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 w-full justify-between mb-4 min-h-10">
|
||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<AssignmentPolicyForm
|
||||
ref="formRef"
|
||||
mode="CREATE"
|
||||
:is-loading="uiFlags.isCreating"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@@ -1,293 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { getInboxIconByType } from 'dashboard/helper/inbox';
|
||||
import {
|
||||
ROUND_ROBIN,
|
||||
EARLIEST_CREATED,
|
||||
} from 'dashboard/routes/dashboard/settings/assignmentPolicy/constants';
|
||||
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||
import AssignmentPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue';
|
||||
import ConfirmInboxDialog from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmInboxDialog.vue';
|
||||
import InboxLinkDialog from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/InboxLinkDialog.vue';
|
||||
|
||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const uiFlags = useMapGetter('assignmentPolicies/getUIFlags');
|
||||
const inboxes = useMapGetter('inboxes/getAllInboxes');
|
||||
const inboxUiFlags = useMapGetter('assignmentPolicies/getInboxUiFlags');
|
||||
const selectedPolicyById = useMapGetter(
|
||||
'assignmentPolicies/getAssignmentPolicyById'
|
||||
);
|
||||
|
||||
const routeId = computed(() => route.params.id);
|
||||
const selectedPolicy = computed(() => selectedPolicyById.value(routeId.value));
|
||||
|
||||
const confirmInboxDialogRef = ref(null);
|
||||
// Store the policy linked to the inbox when adding a new inbox
|
||||
const inboxLinkedPolicy = ref(null);
|
||||
|
||||
// Inbox linking prompt from create flow
|
||||
const inboxIdFromQuery = computed(() => {
|
||||
const id = route.query.inboxId;
|
||||
return id ? Number(id) : null;
|
||||
});
|
||||
|
||||
const suggestedInbox = computed(() => {
|
||||
if (!inboxIdFromQuery.value || !inboxes.value) return null;
|
||||
return inboxes.value.find(inbox => inbox.id === inboxIdFromQuery.value);
|
||||
});
|
||||
|
||||
const isLinkingInbox = ref(false);
|
||||
|
||||
const dismissInboxLinkPrompt = () => {
|
||||
router.replace({
|
||||
name: route.name,
|
||||
params: route.params,
|
||||
query: {},
|
||||
});
|
||||
};
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
if (inboxIdFromQuery.value) {
|
||||
return [
|
||||
{
|
||||
label: t('INBOX_MGMT.SETTINGS'),
|
||||
routeName: 'settings_inbox_show',
|
||||
params: { inboxId: inboxIdFromQuery.value },
|
||||
},
|
||||
{ label: t(`${BASE_KEY}.EDIT.HEADER.TITLE`) },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: t(`${BASE_KEY}.INDEX.HEADER.TITLE`),
|
||||
routeName: 'agent_assignment_policy_index',
|
||||
},
|
||||
{ label: t(`${BASE_KEY}.EDIT.HEADER.TITLE`) },
|
||||
];
|
||||
});
|
||||
|
||||
const buildInboxList = allInboxes =>
|
||||
allInboxes?.map(({ name, id, email, phoneNumber, channelType, medium }) => ({
|
||||
name,
|
||||
id,
|
||||
email,
|
||||
phoneNumber,
|
||||
icon: getInboxIconByType(channelType, medium, 'line'),
|
||||
})) || [];
|
||||
|
||||
const policyInboxes = computed(() =>
|
||||
buildInboxList(selectedPolicy.value?.inboxes)
|
||||
);
|
||||
|
||||
const inboxList = computed(() =>
|
||||
buildInboxList(
|
||||
inboxes.value?.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
)
|
||||
);
|
||||
|
||||
const formData = computed(() => ({
|
||||
name: selectedPolicy.value?.name || '',
|
||||
description: selectedPolicy.value?.description || '',
|
||||
enabled: true,
|
||||
assignmentOrder: selectedPolicy.value?.assignmentOrder || ROUND_ROBIN,
|
||||
conversationPriority:
|
||||
selectedPolicy.value?.conversationPriority || EARLIEST_CREATED,
|
||||
fairDistributionLimit: selectedPolicy.value?.fairDistributionLimit || 100,
|
||||
fairDistributionWindow: selectedPolicy.value?.fairDistributionWindow || 3600,
|
||||
}));
|
||||
|
||||
const handleDeleteInbox = async inboxId => {
|
||||
try {
|
||||
await store.dispatch('assignmentPolicies/removeInboxPolicy', {
|
||||
policyId: selectedPolicy.value?.id,
|
||||
inboxId,
|
||||
});
|
||||
useAlert(t(`${BASE_KEY}.EDIT.INBOX_API.REMOVE.SUCCESS_MESSAGE`));
|
||||
} catch {
|
||||
useAlert(t(`${BASE_KEY}.EDIT.INBOX_API.REMOVE.ERROR_MESSAGE`));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBreadcrumbClick = ({ routeName, params }) => {
|
||||
if (params) {
|
||||
const accountId = route.params.accountId;
|
||||
const inboxId = params.inboxId;
|
||||
// Navigate using explicit path to ensure tab parameter is included
|
||||
router.push(
|
||||
`/app/accounts/${accountId}/settings/inboxes/${inboxId}/collaborators`
|
||||
);
|
||||
} else {
|
||||
router.push({ name: routeName });
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigateToInbox = inbox => {
|
||||
router.push({
|
||||
name: 'settings_inbox_show',
|
||||
params: {
|
||||
accountId: route.params.accountId,
|
||||
inboxId: inbox.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setInboxPolicy = async (inboxId, policyId) => {
|
||||
try {
|
||||
await store.dispatch('assignmentPolicies/setInboxPolicy', {
|
||||
inboxId,
|
||||
policyId,
|
||||
});
|
||||
useAlert(t(`${BASE_KEY}.FORM.INBOXES.API.SUCCESS_MESSAGE`));
|
||||
await store.dispatch(
|
||||
'assignmentPolicies/getInboxes',
|
||||
Number(routeId.value)
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
useAlert(t(`${BASE_KEY}.FORM.INBOXES.API.ERROR_MESSAGE`));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddInbox = async inbox => {
|
||||
try {
|
||||
const policy = await store.dispatch('assignmentPolicies/getInboxPolicy', {
|
||||
inboxId: inbox?.id,
|
||||
});
|
||||
|
||||
if (policy?.id !== selectedPolicy.value?.id) {
|
||||
inboxLinkedPolicy.value = {
|
||||
...policy,
|
||||
assignedInboxCount: policy.assignedInboxCount - 1,
|
||||
};
|
||||
confirmInboxDialogRef.value.openDialog(inbox);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// If getInboxPolicy fails, continue to setInboxPolicy
|
||||
}
|
||||
|
||||
await setInboxPolicy(inbox?.id, selectedPolicy.value?.id);
|
||||
};
|
||||
|
||||
const handleLinkSuggestedInbox = async () => {
|
||||
if (!suggestedInbox.value) return;
|
||||
|
||||
isLinkingInbox.value = true;
|
||||
const inbox = {
|
||||
id: suggestedInbox.value.id,
|
||||
name: suggestedInbox.value.name,
|
||||
};
|
||||
|
||||
await handleAddInbox(inbox);
|
||||
|
||||
// Clear the query param after linking
|
||||
router.replace({
|
||||
name: route.name,
|
||||
params: route.params,
|
||||
query: {},
|
||||
});
|
||||
isLinkingInbox.value = false;
|
||||
};
|
||||
|
||||
const handleConfirmAddInbox = async inboxId => {
|
||||
const success = await setInboxPolicy(inboxId, selectedPolicy.value?.id);
|
||||
|
||||
if (success) {
|
||||
// Update the policy to reflect the assigned inbox count change
|
||||
await store.dispatch('assignmentPolicies/updateInboxPolicy', {
|
||||
policy: inboxLinkedPolicy.value,
|
||||
});
|
||||
// Fetch the updated inboxes for the policy after update, to reflect real-time changes
|
||||
store.dispatch(
|
||||
'assignmentPolicies/getInboxes',
|
||||
inboxLinkedPolicy.value?.id
|
||||
);
|
||||
inboxLinkedPolicy.value = null;
|
||||
confirmInboxDialogRef.value.closeDialog();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async formState => {
|
||||
try {
|
||||
await store.dispatch('assignmentPolicies/update', {
|
||||
id: selectedPolicy.value?.id,
|
||||
...formState,
|
||||
});
|
||||
useAlert(t(`${BASE_KEY}.EDIT.API.SUCCESS_MESSAGE`));
|
||||
} catch {
|
||||
useAlert(t(`${BASE_KEY}.EDIT.API.ERROR_MESSAGE`));
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPolicyData = async () => {
|
||||
if (!routeId.value) return;
|
||||
|
||||
// Fetch inboxes if not already loaded (needed for inbox link prompt)
|
||||
if (!inboxes.value?.length) {
|
||||
store.dispatch('inboxes/get');
|
||||
}
|
||||
|
||||
// Fetch policy if not available
|
||||
if (!selectedPolicy.value?.id)
|
||||
await store.dispatch('assignmentPolicies/show', routeId.value);
|
||||
|
||||
await store.dispatch('assignmentPolicies/getInboxes', Number(routeId.value));
|
||||
};
|
||||
|
||||
watch(routeId, fetchPolicyData, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetchingItem"
|
||||
class="w-full max-w-2xl ltr:mr-auto rtl:ml-auto"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 w-full justify-between mb-4 min-h-10">
|
||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<AssignmentPolicyForm
|
||||
:key="routeId"
|
||||
mode="EDIT"
|
||||
:initial-data="formData"
|
||||
:policy-inboxes="policyInboxes"
|
||||
:inbox-list="inboxList"
|
||||
show-inbox-section
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
:is-inbox-loading="inboxUiFlags.isFetching"
|
||||
@submit="handleSubmit"
|
||||
@add-inbox="handleAddInbox"
|
||||
@delete-inbox="handleDeleteInbox"
|
||||
@navigate-to-inbox="handleNavigateToInbox"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ConfirmInboxDialog
|
||||
ref="confirmInboxDialogRef"
|
||||
@add="handleConfirmAddInbox"
|
||||
/>
|
||||
|
||||
<InboxLinkDialog
|
||||
:inbox="suggestedInbox"
|
||||
:is-linking="isLinkingInbox"
|
||||
@link="handleLinkSuggestedInbox"
|
||||
@dismiss="dismissInboxLinkPrompt"
|
||||
/>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@@ -1,128 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||
import AssignmentPolicyCard from 'dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.vue';
|
||||
import ConfirmDeletePolicyDialog from './components/ConfirmDeletePolicyDialog.vue';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const agentAssignmentsPolicies = useMapGetter(
|
||||
'assignmentPolicies/getAssignmentPolicies'
|
||||
);
|
||||
const uiFlags = useMapGetter('assignmentPolicies/getUIFlags');
|
||||
const inboxUiFlags = useMapGetter('assignmentPolicies/getInboxUiFlags');
|
||||
|
||||
const confirmDeletePolicyDialogRef = ref(null);
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
const items = [
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.INDEX.HEADER.TITLE'),
|
||||
routeName: 'assignment_policy_index',
|
||||
},
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.HEADER.TITLE'),
|
||||
},
|
||||
];
|
||||
return items;
|
||||
});
|
||||
|
||||
const handleBreadcrumbClick = item => {
|
||||
router.push({
|
||||
name: item.routeName,
|
||||
});
|
||||
};
|
||||
|
||||
const onClickCreatePolicy = () => {
|
||||
router.push({
|
||||
name: 'agent_assignment_policy_create',
|
||||
});
|
||||
};
|
||||
|
||||
const onClickEditPolicy = id => {
|
||||
router.push({
|
||||
name: 'agent_assignment_policy_edit',
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleFetchInboxes = id => {
|
||||
if (inboxUiFlags.value.isFetching) return;
|
||||
store.dispatch('assignmentPolicies/getInboxes', id);
|
||||
};
|
||||
|
||||
const handleDelete = id => {
|
||||
confirmDeletePolicyDialogRef.value.openDialog(id);
|
||||
};
|
||||
|
||||
const handleDeletePolicy = async policyId => {
|
||||
try {
|
||||
await store.dispatch('assignmentPolicies/delete', policyId);
|
||||
useAlert(
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.DELETE_POLICY.SUCCESS_MESSAGE'
|
||||
)
|
||||
);
|
||||
confirmDeletePolicyDialogRef.value.closeDialog();
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.DELETE_POLICY.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('assignmentPolicies/get');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:no-records-found="agentAssignmentsPolicies.length === 0"
|
||||
:no-records-message="
|
||||
$t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.NO_RECORDS_FOUND')
|
||||
"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 w-full justify-between min-h-10">
|
||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||
<Button icon="i-lucide-plus" md @click="onClickCreatePolicy">
|
||||
{{
|
||||
$t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.HEADER.CREATE_POLICY'
|
||||
)
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-4 pt-4">
|
||||
<AssignmentPolicyCard
|
||||
v-for="policy in agentAssignmentsPolicies"
|
||||
:key="policy.id"
|
||||
v-bind="policy"
|
||||
:is-fetching-inboxes="inboxUiFlags.isFetching"
|
||||
@fetch-inboxes="handleFetchInboxes"
|
||||
@edit="onClickEditPolicy"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<ConfirmDeletePolicyDialog
|
||||
ref="confirmDeletePolicyDialogRef"
|
||||
@delete="handleDeletePolicy"
|
||||
/>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@@ -1,87 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||
import AgentCapacityPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const formRef = ref(null);
|
||||
const uiFlags = useMapGetter('agentCapacityPolicies/getUIFlags');
|
||||
const labelsList = useMapGetter('labels/getLabels');
|
||||
|
||||
const allLabels = computed(() =>
|
||||
labelsList.value?.map(({ title, color, id }) => ({
|
||||
id,
|
||||
name: title,
|
||||
color,
|
||||
}))
|
||||
);
|
||||
|
||||
const breadcrumbItems = computed(() => [
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.INDEX.HEADER.TITLE'),
|
||||
routeName: 'agent_capacity_policy_index',
|
||||
},
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.CREATE.HEADER.TITLE'),
|
||||
},
|
||||
]);
|
||||
|
||||
const handleBreadcrumbClick = item => {
|
||||
router.push({
|
||||
name: item.routeName,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async formState => {
|
||||
try {
|
||||
const policy = await store.dispatch(
|
||||
'agentCapacityPolicies/create',
|
||||
formState
|
||||
);
|
||||
useAlert(
|
||||
t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.CREATE.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
formRef.value?.resetForm();
|
||||
|
||||
router.push({
|
||||
name: 'agent_capacity_policy_edit',
|
||||
params: {
|
||||
id: policy.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.CREATE.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout class="w-full max-w-2xl ltr:mr-auto rtl:ml-auto">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 w-full justify-between mb-4 min-h-10">
|
||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<AgentCapacityPolicyForm
|
||||
ref="formRef"
|
||||
mode="CREATE"
|
||||
:is-loading="uiFlags.isCreating"
|
||||
:label-list="allLabels"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@@ -1,220 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, watch, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import { getInboxIconByType } from 'dashboard/helper/inbox';
|
||||
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||
import AgentCapacityPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue';
|
||||
|
||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const uiFlags = useMapGetter('agentCapacityPolicies/getUIFlags');
|
||||
const usersUiFlags = useMapGetter('agentCapacityPolicies/getUsersUIFlags');
|
||||
const selectedPolicyById = useMapGetter(
|
||||
'agentCapacityPolicies/getAgentCapacityPolicyById'
|
||||
);
|
||||
const agentsList = useMapGetter('agents/getAgents');
|
||||
const labelsList = useMapGetter('labels/getLabels');
|
||||
const inboxes = useMapGetter('inboxes/getAllInboxes');
|
||||
const inboxesUiFlags = useMapGetter('inboxes/getUIFlags');
|
||||
|
||||
const routeId = computed(() => route.params.id);
|
||||
const selectedPolicy = computed(() => selectedPolicyById.value(routeId.value));
|
||||
const selectedPolicyId = computed(() => selectedPolicy.value?.id);
|
||||
|
||||
const breadcrumbItems = computed(() => [
|
||||
{
|
||||
label: t(`${BASE_KEY}.INDEX.HEADER.TITLE`),
|
||||
routeName: 'agent_capacity_policy_index',
|
||||
},
|
||||
{ label: t(`${BASE_KEY}.EDIT.HEADER.TITLE`) },
|
||||
]);
|
||||
|
||||
const buildList = items =>
|
||||
items?.map(({ name, title, id, email, avatarUrl, thumbnail, color }) => ({
|
||||
name: name || title,
|
||||
id,
|
||||
email,
|
||||
avatarUrl: avatarUrl || thumbnail,
|
||||
color,
|
||||
})) || [];
|
||||
|
||||
const policyUsers = computed(() => buildList(selectedPolicy.value?.users));
|
||||
|
||||
const allAgents = computed(() =>
|
||||
buildList(camelcaseKeys(agentsList.value)).filter(
|
||||
agent => !policyUsers.value?.some(user => user.id === agent.id)
|
||||
)
|
||||
);
|
||||
|
||||
const allLabels = computed(() => buildList(labelsList.value));
|
||||
|
||||
const allInboxes = computed(
|
||||
() =>
|
||||
inboxes.value
|
||||
?.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(({ name, id, email, phoneNumber, channelType, medium }) => ({
|
||||
name,
|
||||
id,
|
||||
email,
|
||||
phoneNumber,
|
||||
icon: getInboxIconByType(channelType, medium, 'line'),
|
||||
})) || []
|
||||
);
|
||||
|
||||
const formData = computed(() => ({
|
||||
name: selectedPolicy.value?.name || '',
|
||||
description: selectedPolicy.value?.description || '',
|
||||
exclusionRules: {
|
||||
excludedLabels: [
|
||||
...(selectedPolicy.value?.exclusionRules?.excludedLabels || []),
|
||||
],
|
||||
excludeOlderThanHours:
|
||||
selectedPolicy.value?.exclusionRules?.excludeOlderThanHours || 10,
|
||||
},
|
||||
inboxCapacityLimits:
|
||||
selectedPolicy.value?.inboxCapacityLimits?.map(limit => ({
|
||||
...limit,
|
||||
})) || [],
|
||||
}));
|
||||
|
||||
const handleBreadcrumbClick = ({ routeName }) =>
|
||||
router.push({ name: routeName });
|
||||
|
||||
const handleDeleteUser = async agentId => {
|
||||
try {
|
||||
await store.dispatch('agentCapacityPolicies/removeUser', {
|
||||
policyId: selectedPolicyId.value,
|
||||
userId: agentId,
|
||||
});
|
||||
useAlert(t(`${BASE_KEY}.EDIT.AGENT_API.REMOVE.SUCCESS_MESSAGE`));
|
||||
} catch {
|
||||
useAlert(t(`${BASE_KEY}.EDIT.AGENT_API.REMOVE.ERROR_MESSAGE`));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddUser = async agent => {
|
||||
try {
|
||||
await store.dispatch('agentCapacityPolicies/addUser', {
|
||||
policyId: selectedPolicyId.value,
|
||||
userData: { id: agent.id, capacity: 20 },
|
||||
});
|
||||
useAlert(t(`${BASE_KEY}.EDIT.AGENT_API.ADD.SUCCESS_MESSAGE`));
|
||||
} catch {
|
||||
useAlert(t(`${BASE_KEY}.EDIT.AGENT_API.ADD.ERROR_MESSAGE`));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteInboxLimit = async limitId => {
|
||||
try {
|
||||
await store.dispatch('agentCapacityPolicies/deleteInboxLimit', {
|
||||
policyId: selectedPolicyId.value,
|
||||
limitId,
|
||||
});
|
||||
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.DELETE.SUCCESS_MESSAGE`));
|
||||
} catch {
|
||||
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.DELETE.ERROR_MESSAGE`));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddInboxLimit = async limit => {
|
||||
try {
|
||||
await store.dispatch('agentCapacityPolicies/createInboxLimit', {
|
||||
policyId: selectedPolicyId.value,
|
||||
limitData: {
|
||||
inboxId: limit.inboxId,
|
||||
conversationLimit: limit.conversationLimit,
|
||||
},
|
||||
});
|
||||
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.ADD.SUCCESS_MESSAGE`));
|
||||
} catch {
|
||||
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.ADD.ERROR_MESSAGE`));
|
||||
}
|
||||
};
|
||||
|
||||
const handleLimitChange = async limit => {
|
||||
try {
|
||||
await store.dispatch('agentCapacityPolicies/updateInboxLimit', {
|
||||
policyId: selectedPolicyId.value,
|
||||
limitId: limit.id,
|
||||
limitData: { conversationLimit: limit.conversationLimit },
|
||||
});
|
||||
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.UPDATE.SUCCESS_MESSAGE`));
|
||||
} catch {
|
||||
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.UPDATE.ERROR_MESSAGE`));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async formState => {
|
||||
try {
|
||||
await store.dispatch('agentCapacityPolicies/update', {
|
||||
id: selectedPolicyId.value,
|
||||
...formState,
|
||||
});
|
||||
|
||||
useAlert(t(`${BASE_KEY}.EDIT.API.SUCCESS_MESSAGE`));
|
||||
} catch {
|
||||
useAlert(t(`${BASE_KEY}.EDIT.API.ERROR_MESSAGE`));
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPolicyData = async () => {
|
||||
if (!routeId.value) return;
|
||||
|
||||
// Fetch policy if not available
|
||||
if (!selectedPolicyId.value)
|
||||
await store.dispatch('agentCapacityPolicies/show', routeId.value);
|
||||
|
||||
await store.dispatch('agentCapacityPolicies/getUsers', Number(routeId.value));
|
||||
};
|
||||
|
||||
watch(routeId, fetchPolicyData, { immediate: true });
|
||||
onMounted(() => store.dispatch('agents/get'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetchingItem"
|
||||
class="w-full max-w-2xl ltr:mr-auto rtl:ml-auto"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 w-full justify-between mb-4 min-h-10">
|
||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<AgentCapacityPolicyForm
|
||||
:key="routeId"
|
||||
mode="EDIT"
|
||||
:initial-data="formData"
|
||||
:policy-users="policyUsers"
|
||||
:agent-list="allAgents"
|
||||
:label-list="allLabels"
|
||||
:inbox-list="allInboxes"
|
||||
show-user-section
|
||||
show-inbox-limit-section
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
:is-users-loading="usersUiFlags.isFetching"
|
||||
:is-inboxes-loading="inboxesUiFlags.isFetching"
|
||||
@submit="handleSubmit"
|
||||
@add-user="handleAddUser"
|
||||
@delete-user="handleDeleteUser"
|
||||
@add-inbox-limit="handleAddInboxLimit"
|
||||
@update-inbox-limit="handleLimitChange"
|
||||
@delete-inbox-limit="handleDeleteInboxLimit"
|
||||
/>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@@ -1,126 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||
import AgentCapacityPolicyCard from 'dashboard/components-next/AssignmentPolicy/AgentCapacityPolicyCard/AgentCapacityPolicyCard.vue';
|
||||
import ConfirmDeletePolicyDialog from './components/ConfirmDeletePolicyDialog.vue';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const agentCapacityPolicies = useMapGetter(
|
||||
'agentCapacityPolicies/getAgentCapacityPolicies'
|
||||
);
|
||||
const uiFlags = useMapGetter('agentCapacityPolicies/getUIFlags');
|
||||
const usersUiFlags = useMapGetter('agentCapacityPolicies/getUsersUIFlags');
|
||||
|
||||
const confirmDeletePolicyDialogRef = ref(null);
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
const items = [
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.INDEX.HEADER.TITLE'),
|
||||
routeName: 'assignment_policy_index',
|
||||
},
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.INDEX.HEADER.TITLE'),
|
||||
},
|
||||
];
|
||||
return items;
|
||||
});
|
||||
|
||||
const handleBreadcrumbClick = item => {
|
||||
router.push({
|
||||
name: item.routeName,
|
||||
});
|
||||
};
|
||||
|
||||
const onClickCreatePolicy = () => {
|
||||
router.push({
|
||||
name: 'agent_capacity_policy_create',
|
||||
});
|
||||
};
|
||||
|
||||
const onClickEditPolicy = id => {
|
||||
router.push({
|
||||
name: 'agent_capacity_policy_edit',
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleFetchUsers = id => {
|
||||
if (usersUiFlags.value.isFetching) return;
|
||||
store.dispatch('agentCapacityPolicies/getUsers', id);
|
||||
};
|
||||
|
||||
const handleDelete = id => {
|
||||
confirmDeletePolicyDialogRef.value.openDialog(id);
|
||||
};
|
||||
|
||||
const handleDeletePolicy = async policyId => {
|
||||
try {
|
||||
await store.dispatch('agentCapacityPolicies/delete', policyId);
|
||||
useAlert(
|
||||
t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.DELETE_POLICY.SUCCESS_MESSAGE')
|
||||
);
|
||||
confirmDeletePolicyDialogRef.value.closeDialog();
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.DELETE_POLICY.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('agentCapacityPolicies/get');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:no-records-found="agentCapacityPolicies.length === 0"
|
||||
:no-records-message="
|
||||
$t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.INDEX.NO_RECORDS_FOUND')
|
||||
"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 w-full justify-between min-h-10">
|
||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||
<Button icon="i-lucide-plus" md @click="onClickCreatePolicy">
|
||||
{{
|
||||
$t(
|
||||
'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.INDEX.HEADER.CREATE_POLICY'
|
||||
)
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-4 pt-4">
|
||||
<AgentCapacityPolicyCard
|
||||
v-for="policy in agentCapacityPolicies"
|
||||
:key="policy.id"
|
||||
v-bind="policy"
|
||||
:is-fetching-users="usersUiFlags.isFetching"
|
||||
@fetch-users="handleFetchUsers"
|
||||
@edit="onClickEditPolicy"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<ConfirmDeletePolicyDialog
|
||||
ref="confirmDeletePolicyDialogRef"
|
||||
@delete="handleDeletePolicy"
|
||||
/>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@@ -1,290 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import BaseInfo from 'dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue';
|
||||
import RadioCard from 'dashboard/components-next/AssignmentPolicy/components/RadioCard.vue';
|
||||
import FairDistribution from 'dashboard/components-next/AssignmentPolicy/components/FairDistribution.vue';
|
||||
import DataTable from 'dashboard/components-next/AssignmentPolicy/components/DataTable.vue';
|
||||
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import {
|
||||
OPTIONS,
|
||||
ROUND_ROBIN,
|
||||
EARLIEST_CREATED,
|
||||
DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||
DEFAULT_FAIR_DISTRIBUTION_WINDOW,
|
||||
} from 'dashboard/routes/dashboard/settings/assignmentPolicy/constants';
|
||||
|
||||
const props = defineProps({
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: '',
|
||||
description: '',
|
||||
assignmentOrder: ROUND_ROBIN,
|
||||
conversationPriority: EARLIEST_CREATED,
|
||||
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||
fairDistributionWindow: DEFAULT_FAIR_DISTRIBUTION_WINDOW,
|
||||
}),
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => ['CREATE', 'EDIT'].includes(value),
|
||||
},
|
||||
policyInboxes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
inboxList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showInboxSection: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isInboxLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'submit',
|
||||
'addInbox',
|
||||
'deleteInbox',
|
||||
'navigateToInbox',
|
||||
'validationChange',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const accountId = computed(() => Number(route.params.accountId));
|
||||
const isFeatureEnabledonAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY';
|
||||
|
||||
const state = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: true,
|
||||
assignmentOrder: ROUND_ROBIN,
|
||||
conversationPriority: EARLIEST_CREATED,
|
||||
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||
fairDistributionWindow: DEFAULT_FAIR_DISTRIBUTION_WINDOW,
|
||||
});
|
||||
|
||||
const validationState = ref({
|
||||
isValid: false,
|
||||
});
|
||||
|
||||
const createOption = (
|
||||
type,
|
||||
key,
|
||||
stateKey,
|
||||
disabled = false,
|
||||
disabledMessage = '',
|
||||
disabledLabel = ''
|
||||
) => ({
|
||||
key,
|
||||
label: t(`${BASE_KEY}.FORM.${type}.${key.toUpperCase()}.LABEL`),
|
||||
description: t(`${BASE_KEY}.FORM.${type}.${key.toUpperCase()}.DESCRIPTION`),
|
||||
isActive: state[stateKey] === key,
|
||||
disabled,
|
||||
disabledMessage,
|
||||
disabledLabel,
|
||||
});
|
||||
|
||||
const assignmentOrderOptions = computed(() => {
|
||||
const hasAdvancedAssignment = isFeatureEnabledonAccount.value(
|
||||
accountId.value,
|
||||
'advanced_assignment'
|
||||
);
|
||||
|
||||
return OPTIONS.ORDER.map(key => {
|
||||
const isBalanced = key === 'balanced';
|
||||
const disabled = isBalanced && !hasAdvancedAssignment;
|
||||
const disabledMessage = disabled
|
||||
? t(`${BASE_KEY}.FORM.ASSIGNMENT_ORDER.BALANCED.PREMIUM_MESSAGE`)
|
||||
: '';
|
||||
const disabledLabel = disabled
|
||||
? t(`${BASE_KEY}.FORM.ASSIGNMENT_ORDER.BALANCED.PREMIUM_BADGE`)
|
||||
: '';
|
||||
|
||||
return createOption(
|
||||
'ASSIGNMENT_ORDER',
|
||||
key,
|
||||
'assignmentOrder',
|
||||
disabled,
|
||||
disabledMessage,
|
||||
disabledLabel
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const assignmentPriorityOptions = computed(() =>
|
||||
OPTIONS.PRIORITY.map(key =>
|
||||
createOption('ASSIGNMENT_PRIORITY', key, 'conversationPriority')
|
||||
)
|
||||
);
|
||||
|
||||
const radioSections = computed(() => [
|
||||
{
|
||||
key: 'assignmentOrder',
|
||||
label: t(`${BASE_KEY}.FORM.ASSIGNMENT_ORDER.LABEL`),
|
||||
options: assignmentOrderOptions.value,
|
||||
},
|
||||
{
|
||||
key: 'conversationPriority',
|
||||
label: t(`${BASE_KEY}.FORM.ASSIGNMENT_PRIORITY.LABEL`),
|
||||
options: assignmentPriorityOptions.value,
|
||||
},
|
||||
]);
|
||||
|
||||
const buttonLabel = computed(() =>
|
||||
t(`${BASE_KEY}.${props.mode.toUpperCase()}.${props.mode}_BUTTON`)
|
||||
);
|
||||
|
||||
const handleValidationChange = validation => {
|
||||
validationState.value = validation;
|
||||
emit('validationChange', validation);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(state, {
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: true,
|
||||
assignmentOrder: ROUND_ROBIN,
|
||||
conversationPriority: EARLIEST_CREATED,
|
||||
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||
fairDistributionWindow: DEFAULT_FAIR_DISTRIBUTION_WINDOW,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit('submit', { ...state });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.initialData,
|
||||
newData => {
|
||||
Object.assign(state, newData);
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
resetForm,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="flex flex-col gap-4 divide-y divide-n-weak mb-4">
|
||||
<BaseInfo
|
||||
v-model:policy-name="state.name"
|
||||
v-model:description="state.description"
|
||||
:name-label="t(`${BASE_KEY}.FORM.NAME.LABEL`)"
|
||||
:name-placeholder="t(`${BASE_KEY}.FORM.NAME.PLACEHOLDER`)"
|
||||
:description-label="t(`${BASE_KEY}.FORM.DESCRIPTION.LABEL`)"
|
||||
:description-placeholder="t(`${BASE_KEY}.FORM.DESCRIPTION.PLACEHOLDER`)"
|
||||
@validation-change="handleValidationChange"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
v-for="section in radioSections"
|
||||
:key="section.key"
|
||||
class="py-4 flex flex-col items-start gap-3 w-full"
|
||||
>
|
||||
<WithLabel
|
||||
:label="section.label"
|
||||
name="assignmentPolicy"
|
||||
class="w-full flex items-start flex-col gap-3"
|
||||
>
|
||||
<div class="grid grid-cols-1 xs:grid-cols-2 gap-4 w-full">
|
||||
<RadioCard
|
||||
v-for="option in section.options"
|
||||
:id="option.key"
|
||||
:key="option.key"
|
||||
:label="option.label"
|
||||
:description="option.description"
|
||||
:is-active="option.isActive"
|
||||
:disabled="option.disabled"
|
||||
:disabled-label="option.disabledLabel"
|
||||
:disabled-message="option.disabledMessage"
|
||||
@select="state[section.key] = $event"
|
||||
/>
|
||||
</div>
|
||||
</WithLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 pb-2 flex-col flex gap-4">
|
||||
<div class="flex flex-col items-start gap-1 py-1">
|
||||
<label class="text-sm font-medium text-n-slate-12 py-1">
|
||||
{{ t(`${BASE_KEY}.FORM.FAIR_DISTRIBUTION.LABEL`) }}
|
||||
</label>
|
||||
<p class="mb-0 text-n-slate-11 text-sm">
|
||||
{{ t(`${BASE_KEY}.FORM.FAIR_DISTRIBUTION.DESCRIPTION`) }}
|
||||
</p>
|
||||
</div>
|
||||
<FairDistribution
|
||||
v-model:fair-distribution-limit="state.fairDistributionLimit"
|
||||
v-model:fair-distribution-window="state.fairDistributionWindow"
|
||||
v-model:window-unit="state.windowUnit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
:label="buttonLabel"
|
||||
:disabled="!validationState.isValid || isLoading"
|
||||
:is-loading="isLoading"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="showInboxSection"
|
||||
class="py-4 flex-col flex gap-4 border-t border-n-weak mt-6"
|
||||
>
|
||||
<div class="flex items-end gap-4 w-full justify-between">
|
||||
<div class="flex flex-col items-start gap-1 py-1">
|
||||
<label class="text-sm font-medium text-n-slate-12 py-1">
|
||||
{{ t(`${BASE_KEY}.FORM.INBOXES.LABEL`) }}
|
||||
</label>
|
||||
<p class="mb-0 text-n-slate-11 text-sm">
|
||||
{{ t(`${BASE_KEY}.FORM.INBOXES.DESCRIPTION`) }}
|
||||
</p>
|
||||
</div>
|
||||
<AddDataDropdown
|
||||
:label="t(`${BASE_KEY}.FORM.INBOXES.ADD_BUTTON`)"
|
||||
:search-placeholder="
|
||||
t(`${BASE_KEY}.FORM.INBOXES.DROPDOWN.SEARCH_PLACEHOLDER`)
|
||||
"
|
||||
:items="inboxList"
|
||||
@add="$emit('addInbox', $event)"
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
:items="policyInboxes"
|
||||
:is-fetching="isInboxLoading"
|
||||
:empty-state-message="t(`${BASE_KEY}.FORM.INBOXES.EMPTY_STATE`)"
|
||||
@delete="$emit('deleteInbox', $event)"
|
||||
@navigate="$emit('navigateToInbox', $event)"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@@ -1,214 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BaseInfo from 'dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue';
|
||||
import DataTable from 'dashboard/components-next/AssignmentPolicy/components/DataTable.vue';
|
||||
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ExclusionRules from 'dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue';
|
||||
import InboxCapacityLimits from 'dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue';
|
||||
|
||||
const props = defineProps({
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: false,
|
||||
exclusionRules: {
|
||||
excludedLabels: [],
|
||||
excludeOlderThanHours: 10,
|
||||
},
|
||||
inboxCapacityLimits: [],
|
||||
}),
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => ['CREATE', 'EDIT'].includes(value),
|
||||
},
|
||||
policyUsers: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
agentList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
labelList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
inboxList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showUserSection: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showInboxLimitSection: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isUsersLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isInboxesLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'submit',
|
||||
'addUser',
|
||||
'deleteUser',
|
||||
'validationChange',
|
||||
'deleteInboxLimit',
|
||||
'addInboxLimit',
|
||||
'updateInboxLimit',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY';
|
||||
|
||||
const state = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
exclusionRules: {
|
||||
excludedLabels: [],
|
||||
excludeOlderThanHours: 10,
|
||||
},
|
||||
inboxCapacityLimits: [],
|
||||
});
|
||||
|
||||
const validationState = ref({
|
||||
isValid: false,
|
||||
});
|
||||
|
||||
const buttonLabel = computed(() =>
|
||||
t(`${BASE_KEY}.${props.mode.toUpperCase()}.${props.mode}_BUTTON`)
|
||||
);
|
||||
|
||||
const handleValidationChange = validation => {
|
||||
validationState.value = validation;
|
||||
emit('validationChange', validation);
|
||||
};
|
||||
|
||||
const handleDeleteInboxLimit = id => {
|
||||
emit('deleteInboxLimit', id);
|
||||
};
|
||||
|
||||
const handleAddInboxLimit = limit => {
|
||||
emit('addInboxLimit', limit);
|
||||
};
|
||||
|
||||
const handleLimitChange = limit => {
|
||||
emit('updateInboxLimit', limit);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(state, {
|
||||
name: '',
|
||||
description: '',
|
||||
exclusionRules: {
|
||||
excludedLabels: [],
|
||||
excludeOlderThanHours: 10,
|
||||
},
|
||||
inboxCapacityLimits: [],
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit('submit', { ...state });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.initialData,
|
||||
newData => {
|
||||
Object.assign(state, newData);
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
resetForm,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="flex flex-col gap-4 mb-2 divide-y divide-n-weak">
|
||||
<BaseInfo
|
||||
v-model:policy-name="state.name"
|
||||
v-model:description="state.description"
|
||||
:name-label="t(`${BASE_KEY}.FORM.NAME.LABEL`)"
|
||||
:name-placeholder="t(`${BASE_KEY}.FORM.NAME.PLACEHOLDER`)"
|
||||
:description-label="t(`${BASE_KEY}.FORM.DESCRIPTION.LABEL`)"
|
||||
:description-placeholder="t(`${BASE_KEY}.FORM.DESCRIPTION.PLACEHOLDER`)"
|
||||
@validation-change="handleValidationChange"
|
||||
/>
|
||||
<ExclusionRules
|
||||
v-model:excluded-labels="state.exclusionRules.excludedLabels"
|
||||
v-model:exclude-older-than-minutes="
|
||||
state.exclusionRules.excludeOlderThanHours
|
||||
"
|
||||
:tags-list="labelList"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="buttonLabel"
|
||||
:disabled="!validationState.isValid || isLoading"
|
||||
:is-loading="isLoading"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="showInboxLimitSection || showUserSection"
|
||||
class="flex flex-col gap-4 divide-y divide-n-weak border-t border-n-weak mt-6"
|
||||
>
|
||||
<InboxCapacityLimits
|
||||
v-if="showInboxLimitSection"
|
||||
v-model:inbox-capacity-limits="state.inboxCapacityLimits"
|
||||
:inbox-list="inboxList"
|
||||
:is-fetching="isInboxesLoading"
|
||||
@delete="handleDeleteInboxLimit"
|
||||
@add="handleAddInboxLimit"
|
||||
@update="handleLimitChange"
|
||||
/>
|
||||
<div v-if="showUserSection" class="py-4 flex-col flex gap-4">
|
||||
<div class="flex items-end gap-4 w-full justify-between">
|
||||
<div class="flex flex-col items-start gap-1 py-1">
|
||||
<label class="text-sm font-medium text-n-slate-12 py-1">
|
||||
{{ t(`${BASE_KEY}.FORM.USERS.LABEL`) }}
|
||||
</label>
|
||||
<p class="mb-0 text-n-slate-11 text-sm">
|
||||
{{ t(`${BASE_KEY}.FORM.USERS.DESCRIPTION`) }}
|
||||
</p>
|
||||
</div>
|
||||
<AddDataDropdown
|
||||
:label="t(`${BASE_KEY}.FORM.USERS.ADD_BUTTON`)"
|
||||
:search-placeholder="
|
||||
t(`${BASE_KEY}.FORM.USERS.DROPDOWN.SEARCH_PLACEHOLDER`)
|
||||
"
|
||||
:items="agentList"
|
||||
@add="$emit('addUser', $event)"
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
:items="policyUsers"
|
||||
:is-fetching="isUsersLoading"
|
||||
:empty-state-message="t(`${BASE_KEY}.FORM.USERS.EMPTY_STATE`)"
|
||||
@delete="$emit('deleteUser', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@@ -1,44 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
|
||||
const emit = defineEmits(['delete']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const currentPolicyId = ref(null);
|
||||
|
||||
const openDialog = policyId => {
|
||||
currentPolicyId.value = policyId;
|
||||
dialogRef.value.open();
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogRef.value.close();
|
||||
};
|
||||
|
||||
const handleDialogConfirm = () => {
|
||||
emit('delete', currentPolicyId.value);
|
||||
};
|
||||
|
||||
defineExpose({ openDialog, closeDialog });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="alert"
|
||||
:title="t('ASSIGNMENT_POLICY.DELETE_POLICY.TITLE')"
|
||||
:description="t('ASSIGNMENT_POLICY.DELETE_POLICY.DESCRIPTION')"
|
||||
:confirm-button-label="
|
||||
t('ASSIGNMENT_POLICY.DELETE_POLICY.CONFIRM_BUTTON_LABEL')
|
||||
"
|
||||
:cancel-button-label="
|
||||
t('ASSIGNMENT_POLICY.DELETE_POLICY.CANCEL_BUTTON_LABEL')
|
||||
"
|
||||
@confirm="handleDialogConfirm"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,59 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const currentInbox = ref(null);
|
||||
|
||||
const openDialog = inbox => {
|
||||
currentInbox.value = inbox;
|
||||
dialogRef.value.open();
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogRef.value.close();
|
||||
};
|
||||
|
||||
const handleDialogConfirm = () => {
|
||||
emit('add', currentInbox.value.id);
|
||||
};
|
||||
|
||||
defineExpose({ openDialog, closeDialog });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="alert"
|
||||
:title="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.CONFIRM_ADD_INBOX_DIALOG.TITLE'
|
||||
)
|
||||
"
|
||||
:description="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.CONFIRM_ADD_INBOX_DIALOG.DESCRIPTION',
|
||||
{
|
||||
inboxName: currentInbox?.name,
|
||||
}
|
||||
)
|
||||
"
|
||||
:confirm-button-label="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.CONFIRM_ADD_INBOX_DIALOG.CONFIRM_BUTTON_LABEL'
|
||||
)
|
||||
"
|
||||
:cancel-button-label="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.CONFIRM_ADD_INBOX_DIALOG.CANCEL_BUTTON_LABEL'
|
||||
)
|
||||
"
|
||||
@confirm="handleDialogConfirm"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,116 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { getInboxIconByType } from 'dashboard/helper/inbox';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isLinking: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['link', 'dismiss']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const inboxName = computed(() => props.inbox?.name || '');
|
||||
|
||||
const inboxIcon = computed(() => {
|
||||
if (!props.inbox) return 'i-lucide-inbox';
|
||||
return getInboxIconByType(
|
||||
props.inbox.channelType,
|
||||
props.inbox.medium,
|
||||
'line'
|
||||
);
|
||||
});
|
||||
|
||||
const openDialog = () => {
|
||||
dialogRef.value?.open();
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('link');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit('dismiss');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.inbox,
|
||||
async newInbox => {
|
||||
if (newInbox) {
|
||||
await nextTick();
|
||||
openDialog();
|
||||
} else {
|
||||
closeDialog();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
defineExpose({ openDialog, closeDialog });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="edit"
|
||||
:title="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.INBOX_LINK_PROMPT.TITLE'
|
||||
)
|
||||
"
|
||||
:confirm-button-label="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.INBOX_LINK_PROMPT.LINK_BUTTON'
|
||||
)
|
||||
"
|
||||
:cancel-button-label="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.INBOX_LINK_PROMPT.CANCEL_BUTTON'
|
||||
)
|
||||
"
|
||||
:is-loading="isLinking"
|
||||
@confirm="handleConfirm"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template #description>
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.INBOX_LINK_PROMPT.DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-3 p-3 rounded-xl border border-n-weak bg-n-alpha-1"
|
||||
>
|
||||
<div
|
||||
class="flex-shrink-0 size-10 rounded-lg bg-n-alpha-2 flex items-center justify-center"
|
||||
>
|
||||
<i :class="inboxIcon" class="text-lg text-n-slate-11" />
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-sm font-medium text-n-slate-12 truncate">
|
||||
{{ inboxName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user