feat: Contact details screen
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import { computed, watch, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
const props = defineProps({
|
||||
contactId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const allLabels = useMapGetter('labels/getLabels');
|
||||
const contactLabels = useMapGetter('contactLabels/getContactLabels');
|
||||
|
||||
const savedLabels = computed(() => {
|
||||
const availableContactLabels = contactLabels.value(props.contactId);
|
||||
return allLabels.value.filter(({ title }) =>
|
||||
availableContactLabels.includes(title)
|
||||
);
|
||||
});
|
||||
|
||||
const fetchLabels = async contactId => {
|
||||
if (!contactId) {
|
||||
return;
|
||||
}
|
||||
store.dispatch('contactLabels/get', contactId);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.contactId,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
fetchLabels(newVal);
|
||||
}
|
||||
}
|
||||
);
|
||||
onMounted(() => {
|
||||
if (route.params.contactId) {
|
||||
fetchLabels(route.params.contactId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div
|
||||
v-for="label in savedLabels"
|
||||
:key="label.id"
|
||||
class="bg-n-alpha-2 rounded-md flex items-center h-7 w-fit py-1 ltr:pl-1 rtl:pr-1 ltr:pr-1.5 rtl:pl-1.5"
|
||||
>
|
||||
<div
|
||||
class="w-2 h-2 m-1 rounded-sm"
|
||||
:style="{ backgroundColor: label.color }"
|
||||
/>
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{ label.title }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center gap-1 px-2 py-1 rounded-md outline-dashed h-[26px] outline-1 outline-n-slate-6 hover:bg-n-alpha-2"
|
||||
>
|
||||
<span class="i-lucide-plus" />
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t('CONTACTS_LAYOUT.DETAILS.TAG_BUTTON') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
|
||||
@@ -21,6 +22,8 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['toggle']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
@@ -39,6 +42,16 @@ const handleFormUpdate = debounce(
|
||||
400,
|
||||
false
|
||||
);
|
||||
|
||||
const onClickViewDetails = () => {
|
||||
store.dispatch('contacts/show', { id: props.id });
|
||||
router.push({
|
||||
name: 'contacts_dashboard_edit_index',
|
||||
params: {
|
||||
contactId: props.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,15 +77,16 @@ const handleFormUpdate = debounce(
|
||||
<span v-if="email" class="text-sm text-n-slate-11">{{
|
||||
email
|
||||
}}</span>
|
||||
<div v-if="phoneNumber" class="w-px h-3 bg-n-slate-6" />
|
||||
<div v-if="email" class="w-px h-3 bg-n-slate-6" />
|
||||
<span v-if="phoneNumber" class="text-sm text-n-slate-11">{{
|
||||
phoneNumber
|
||||
}}</span>
|
||||
<div v-if="phoneNumber || email" class="w-px h-3 bg-n-slate-6" />
|
||||
<div v-if="phoneNumber" class="w-px h-3 bg-n-slate-6" />
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.CARD.VIEW_DETAILS')"
|
||||
variant="link"
|
||||
size="xs"
|
||||
@click="onClickViewDetails"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +117,7 @@ const handleFormUpdate = debounce(
|
||||
v-show="isExpanded"
|
||||
class="w-full overflow-visible transition-all duration-200"
|
||||
>
|
||||
<div class="border-t border-n-strong">
|
||||
<div class="p-6 border-t border-n-strong">
|
||||
<ContactsForm
|
||||
:contact-data="{
|
||||
id,
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedContact: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['goToContactsList']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const deleteContact = async id => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
await store.dispatch('contacts/delete', id);
|
||||
useAlert(t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.API.ERROR_MESSAGE'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogConfirm = async () => {
|
||||
emit('goToContactsList');
|
||||
await deleteContact(route.params.contactId || props.selectedContact.id);
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({ dialogRef });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="alert"
|
||||
:title="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.TITLE')"
|
||||
:description="
|
||||
t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.DESCRIPTION', {
|
||||
contactName: props.selectedContact.name,
|
||||
})
|
||||
"
|
||||
:confirm-button-label="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.CONFIRM')"
|
||||
@confirm="handleDialogConfirm"
|
||||
/>
|
||||
</template>
|
||||
@@ -11,6 +11,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isDetailsView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
@@ -18,8 +22,8 @@ const emit = defineEmits(['update']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const FORM_CONFIG = {
|
||||
FIRST_NAME: { field: 'name' },
|
||||
LAST_NAME: { field: 'name' },
|
||||
FIRST_NAME: { field: 'firstName' },
|
||||
LAST_NAME: { field: 'lastName' },
|
||||
EMAIL_ADDRESS: { field: 'email' },
|
||||
PHONE_NUMBER: { field: 'phone_number' },
|
||||
CITY: { field: 'additional_attributes.city' },
|
||||
@@ -40,6 +44,8 @@ const defaultState = {
|
||||
id: 0,
|
||||
name: '',
|
||||
email: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone_number: '',
|
||||
additional_attributes: {
|
||||
description: '',
|
||||
@@ -60,9 +66,14 @@ const defaultState = {
|
||||
const state = reactive({ ...defaultState });
|
||||
|
||||
const updateState = () => {
|
||||
const [firstName = '', lastName = ''] = (props.contactData.name || '').split(
|
||||
' '
|
||||
);
|
||||
Object.assign(state, {
|
||||
id: props.contactData.id,
|
||||
name: props.contactData.name,
|
||||
firstName,
|
||||
lastName,
|
||||
email: props.contactData.email,
|
||||
phone_number: props.contactData.phoneNumber,
|
||||
additional_attributes: {
|
||||
@@ -109,6 +120,9 @@ const getFormBinding = key => {
|
||||
// Example 1 (nested): field = 'contact.name' returns state.contact.name = "John Doe"
|
||||
// Example 2 (root): field = 'email' returns state.email = "john@example.com"
|
||||
get: () => {
|
||||
if (field === 'firstName' || field === 'lastName') {
|
||||
return state[field];
|
||||
}
|
||||
const [base, nested] = field.split('.');
|
||||
return nested ? state[base][nested] : state[base];
|
||||
},
|
||||
@@ -117,11 +131,17 @@ const getFormBinding = key => {
|
||||
// Example 1 (nested): field = 'contact.name', value = "Jane Doe" → state.contact.name = "Jane Doe"
|
||||
// Example 2 (root): field = 'email', value = "jane@example.com" → state.email = "jane@example.com"
|
||||
set: value => {
|
||||
const [base, nested] = field.split('.');
|
||||
if (nested) {
|
||||
state[base][nested] = value;
|
||||
if (field === 'firstName' || field === 'lastName') {
|
||||
state[field] = value;
|
||||
// Combine first and last name into the name field
|
||||
state.name = `${state.firstName} ${state.lastName}`.trim();
|
||||
} else {
|
||||
state[base] = value;
|
||||
const [base, nested] = field.split('.');
|
||||
if (nested) {
|
||||
state[base][nested] = value;
|
||||
} else {
|
||||
state[base] = value;
|
||||
}
|
||||
}
|
||||
emit('update', state);
|
||||
},
|
||||
@@ -132,8 +152,8 @@ watch(() => props.contactData, updateState, { immediate: true, deep: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-start gap-2 px-6 py-5">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
<span class="py-1 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.TITLE') }}
|
||||
</span>
|
||||
@@ -144,20 +164,26 @@ watch(() => props.contactData, updateState, { immediate: true, deep: true });
|
||||
v-model="state.additional_attributes.country"
|
||||
:options="countryOptions"
|
||||
:placeholder="item.placeholder"
|
||||
class="[&>div>button]:bg-n-alpha-black2 [&>div>button]:!outline-transparent [&>div>button]:h-8"
|
||||
class="[&>div>button]:h-8"
|
||||
:class="{
|
||||
'[&>div>button]:bg-n-alpha-black2 [&>div>button]:!outline-transparent':
|
||||
!isDetailsView,
|
||||
}"
|
||||
@update:model-value="emit('update', state)"
|
||||
/>
|
||||
<Input
|
||||
v-else
|
||||
v-model="getFormBinding(item.key).value"
|
||||
:placeholder="item.placeholder"
|
||||
custom-input-class="!border-transparent h-8 !py-1"
|
||||
:custom-input-class="`h-8 !pt-1 !pb-1 ${
|
||||
!isDetailsView && '!border-transparent'
|
||||
}`"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-2 px-6 pb-5">
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
<span class="py-1 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.CARD.SOCIAL_MEDIA.TITLE') }}
|
||||
</span>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -29,9 +33,48 @@ defineProps({
|
||||
type: Number,
|
||||
default: 15,
|
||||
},
|
||||
isDetailView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selectedContact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['filter', 'more', 'message', 'update:currentPage']);
|
||||
const emit = defineEmits([
|
||||
'filter',
|
||||
'more',
|
||||
'message',
|
||||
'update:currentPage',
|
||||
'goToContactsList',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const selectedContactName = computed(() => {
|
||||
return props.selectedContact?.name;
|
||||
});
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
const items = [
|
||||
{
|
||||
label: t('CONTACTS_LAYOUT.HEADER.BREADCRUMB.CONTACTS'),
|
||||
link: '#',
|
||||
},
|
||||
];
|
||||
if (props.selectedContact) {
|
||||
items.push({
|
||||
label: selectedContactName.value,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
const handleBreadcrumbClick = () => {
|
||||
emit('goToContactsList');
|
||||
};
|
||||
|
||||
const updateCurrentPage = page => {
|
||||
emit('update:currentPage', page);
|
||||
@@ -43,11 +86,19 @@ const updateCurrentPage = page => {
|
||||
<header class="sticky top-0 z-10 px-6 lg:px-0">
|
||||
<div class="w-full max-w-[900px] mx-auto">
|
||||
<div class="flex items-center justify-between w-full h-20 gap-2">
|
||||
<span class="text-xl font-medium text-n-slate-12">
|
||||
<span
|
||||
v-if="!isDetailView"
|
||||
class="text-xl font-medium text-n-slate-12"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
<Breadcrumb
|
||||
v-else
|
||||
:items="breadcrumbItems"
|
||||
@click="handleBreadcrumbClick"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="!isDetailView" class="flex items-center gap-2">
|
||||
<Input
|
||||
:placeholder="$t('CONTACTS_LAYOUT.HEADER.SEARCH_PLACEHOLDER')"
|
||||
:custom-input-class="['h-8 ltr:!pl-8 rtl:!pr-8']"
|
||||
@@ -61,12 +112,14 @@ const updateCurrentPage = page => {
|
||||
</Input>
|
||||
</div>
|
||||
<Button
|
||||
v-if="!isDetailView"
|
||||
icon="i-lucide-list-filter"
|
||||
size="sm"
|
||||
color="slate"
|
||||
@click="emit('filter')"
|
||||
/>
|
||||
<Button
|
||||
v-if="!isDetailView"
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
size="sm"
|
||||
color="slate"
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
|
||||
import EditableAvatar from 'dashboard/components-next/avatar/EditableAvatar.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ContactLabels from 'dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue';
|
||||
import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue';
|
||||
import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedContact: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['goToContactsList']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const confirmDeleteContactDialogRef = ref(null);
|
||||
|
||||
const selectedContactData = computed(() => {
|
||||
if (!props.selectedContact) return {};
|
||||
|
||||
const {
|
||||
phone_number: phoneNumber = {},
|
||||
additional_attributes: additionalAttributes = {},
|
||||
} = props.selectedContact;
|
||||
|
||||
return {
|
||||
...props.selectedContact,
|
||||
phoneNumber,
|
||||
additionalAttributes,
|
||||
};
|
||||
});
|
||||
|
||||
const createdAt = computed(() => {
|
||||
return props.selectedContact?.created_at
|
||||
? dynamicTime(props.selectedContact.created_at)
|
||||
: '';
|
||||
});
|
||||
|
||||
const lastActivityAt = computed(() => {
|
||||
return props.selectedContact?.last_activity_at
|
||||
? dynamicTime(props.selectedContact.last_activity_at)
|
||||
: '';
|
||||
});
|
||||
|
||||
const updateContact = async updatedData => {
|
||||
await store.dispatch('contacts/update', updatedData);
|
||||
await store.dispatch(
|
||||
'contacts/fetchContactableInbox',
|
||||
props.selectedContact?.id
|
||||
);
|
||||
};
|
||||
|
||||
const handleFormUpdate = debounce(
|
||||
updatedData => updateContact(updatedData),
|
||||
500,
|
||||
false
|
||||
);
|
||||
|
||||
const openConfirmDeleteContactDialog = () => {
|
||||
confirmDeleteContactDialogRef.value?.dialogRef.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start gap-8">
|
||||
<div class="flex flex-col items-start gap-3">
|
||||
<EditableAvatar
|
||||
:src="selectedContact.thumbnail"
|
||||
:name="selectedContact.name"
|
||||
/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
{{ selectedContact.name }}
|
||||
</h3>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ createdAt }} • {{ lastActivityAt }}
|
||||
</span>
|
||||
</div>
|
||||
<ContactLabels :contact-id="selectedContact.id" />
|
||||
</div>
|
||||
<ContactsForm
|
||||
:contact-data="selectedContactData"
|
||||
is-details-view
|
||||
@update="handleFormUpdate"
|
||||
/>
|
||||
<div
|
||||
class="flex items-end justify-end w-full gap-4 border-t h-14 border-n-strong"
|
||||
>
|
||||
<Button
|
||||
color="slate"
|
||||
:label="t('CONTACTS_LAYOUT.DETAILS.MERGE_CONTACT')"
|
||||
/>
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
|
||||
color="ruby"
|
||||
@click="openConfirmDeleteContactDialog"
|
||||
/>
|
||||
</div>
|
||||
<ConfirmContactDeleteDialog
|
||||
ref="confirmDeleteContactDialogRef"
|
||||
:selected-contact="selectedContact"
|
||||
@go-to-contacts-list="emit('goToContactsList')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,14 +8,6 @@ defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: value => {
|
||||
return value.every(
|
||||
item =>
|
||||
typeof item.label === 'string' &&
|
||||
(item.link === undefined || typeof item.link === 'string') &&
|
||||
(item.count === undefined || typeof item.count === 'number')
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -391,7 +391,10 @@
|
||||
"HEADER": {
|
||||
"TITLE": "Contacts",
|
||||
"SEARCH_PLACEHOLDER": "Search...",
|
||||
"MESSAGE_BUTTON": "Message"
|
||||
"MESSAGE_BUTTON": "Message",
|
||||
"BREADCRUMB": {
|
||||
"CONTACTS": "Contacts"
|
||||
}
|
||||
},
|
||||
"CARD": {
|
||||
"OF": "of",
|
||||
@@ -445,6 +448,20 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"TAG_BUTTON": "tag",
|
||||
"MERGE_CONTACT": "Merge contact",
|
||||
"DELETE_CONTACT": "Delete contact",
|
||||
"DELETE_DIALOG": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"DESCRIPTION": "Are you sure you want to delete this {contactName} contact?",
|
||||
"CONFIRM": "Yes, Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper.js';
|
||||
|
||||
import ContactsPageRouteView from './pages/ContactsPageRouteView.vue';
|
||||
import EditContactsPageRouteView from './pages/EditContactsPageRouteView.vue';
|
||||
|
||||
const contactsRoutes = {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts-new'),
|
||||
component: ContactsPageRouteView,
|
||||
children: [
|
||||
{
|
||||
path: 'ongoing',
|
||||
name: 'contacts_dashboard_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
},
|
||||
],
|
||||
name: 'contacts_dashboard_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts-new/:contactId'),
|
||||
component: EditContactsPageRouteView,
|
||||
name: 'contacts_dashboard_edit_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import ContactsLayout from 'dashboard/components-next/Contacts/ContactsLayout.vue';
|
||||
import ContactsList from 'dashboard/components-next/Contacts/Pages/ContactsList.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
@@ -11,6 +12,8 @@ const contacts = useMapGetter('contacts/getContacts');
|
||||
const uiFlags = useMapGetter('contacts/getUIFlags');
|
||||
const meta = useMapGetter('contacts/getMeta');
|
||||
|
||||
const isFetchingList = computed(() => uiFlags.value.isFetching);
|
||||
|
||||
const currentPage = computed(() => Number(meta.value?.currentPage));
|
||||
const totalItems = computed(() => meta.value?.count);
|
||||
|
||||
@@ -38,7 +41,13 @@ const updateCurrentPage = page => {
|
||||
:total-items="totalItems"
|
||||
@update:current-page="updateCurrentPage"
|
||||
>
|
||||
<ContactsList :contacts="contacts" />
|
||||
<div
|
||||
v-if="isFetchingList"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<ContactsList v-else :contacts="contacts" />
|
||||
</ContactsLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import ContactsLayout from 'dashboard/components-next/Contacts/ContactsLayout.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import ContactDetails from 'dashboard/components-next/Contacts/Pages/ContactDetails.vue';
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const contact = useMapGetter('contacts/getContact');
|
||||
const uiFlags = useMapGetter('contacts/getUIFlags');
|
||||
|
||||
const isFetchingItem = computed(() => uiFlags.value.isFetchingItem);
|
||||
|
||||
const selectedContact = computed(() => contact.value(route.params.contactId));
|
||||
|
||||
const goToContactsList = () => {
|
||||
router.push({
|
||||
name: 'contacts_dashboard_index',
|
||||
});
|
||||
};
|
||||
|
||||
const fetchActiveContact = async () => {
|
||||
if (route.params.contactId) {
|
||||
store.dispatch('contacts/show', { id: route.params.contactId });
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchActiveContact();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background"
|
||||
>
|
||||
<ContactsLayout
|
||||
:button-label="$t('CONTACTS_LAYOUT.HEADER.MESSAGE_BUTTON')"
|
||||
:selected-contact="selectedContact"
|
||||
is-detail-view
|
||||
:show-pagination-footer="false"
|
||||
@go-to-contacts-list="goToContactsList"
|
||||
>
|
||||
<div
|
||||
v-if="isFetchingItem"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<ContactDetails
|
||||
v-else
|
||||
:selected-contact="selectedContact"
|
||||
@go-to-contacts-list="goToContactsList"
|
||||
/>
|
||||
</ContactsLayout>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user