chore: new call button
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<Button
|
||||
icon="i-ri-phone-fill"
|
||||
color="slate"
|
||||
size="sm"
|
||||
:tooltip="$t('CALL_BUTTON.TOOLTIP')"
|
||||
class="!h-7 !bg-n-solid-3 dark:!bg-n-black/30 !outline-n-weak !text-n-slate-11"
|
||||
@click="openCallModal"
|
||||
/>
|
||||
<div v-if="showCallModal" class="fixed z-50 bg-n-alpha-black1 backdrop-blur-[4px] flex items-start pt-[clamp(3rem,15vh,12rem)] justify-center inset-0">
|
||||
<CallModal @close="showCallModal = false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import CallModal from './CallModal.vue';
|
||||
|
||||
const showCallModal = ref(false);
|
||||
|
||||
const openCallModal = () => {
|
||||
showCallModal.value = true;
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<div class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl">
|
||||
<div class="px-4 py-3 flex items-center">
|
||||
<h3 class="text-base font-medium">{{ $t('CALL_MODAL.START_CALL') }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Inbox Selector (First) -->
|
||||
<div class="flex items-center flex-1 w-full gap-3 px-4 py-3 overflow-y-visible">
|
||||
<label class="mb-0.5 text-sm font-medium text-n-slate-11 whitespace-nowrap">
|
||||
{{ $t('CALL_MODAL.VIA') }}
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="selectedInbox"
|
||||
class="flex items-center gap-1.5 rounded-md bg-n-alpha-2 truncate ltr:pl-3 rtl:pr-3 ltr:pr-1 rtl:pl-1 h-7 min-w-0"
|
||||
>
|
||||
<span class="text-sm truncate text-n-slate-12 flex items-center gap-2">
|
||||
<span class="i-ri-phone-fill text-n-slate-11"></span>
|
||||
{{ selectedInbox.name }} - {{ selectedInbox.phoneNumber }}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="i-lucide-x"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="flex-shrink-0"
|
||||
@click="selectedInbox = null"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-on-click-outside="() => showInboxDropdown = false"
|
||||
class="relative flex items-center h-7"
|
||||
>
|
||||
<Button
|
||||
:label="$t('CALL_MODAL.SELECT_INBOX')"
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="slate"
|
||||
class="hover:!no-underline"
|
||||
@click="showInboxDropdown = !showInboxDropdown"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="voiceInboxesList.length > 0 && showInboxDropdown"
|
||||
:menu-items="voiceInboxesList"
|
||||
class="left-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
|
||||
@action="selectInbox($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Selector -->
|
||||
<ContactSelector
|
||||
:contacts="contacts"
|
||||
:selected-contact="selectedContact"
|
||||
:show-contacts-dropdown="showContactsDropdown"
|
||||
:is-loading="isSearching"
|
||||
:is-creating-contact="false"
|
||||
:contact-id="null"
|
||||
:contactable-inboxes-list="[]"
|
||||
:show-inboxes-dropdown="false"
|
||||
:has-errors="false"
|
||||
@search-contacts="handleContactSearch"
|
||||
@set-selected-contact="handleSelectedContact"
|
||||
@clear-selected-contact="clearSelectedContact"
|
||||
@update-dropdown="handleDropdownUpdate"
|
||||
/>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center justify-end w-full h-[3.25rem] gap-2 px-4 py-3">
|
||||
<Button
|
||||
:label="$t('CALL_MODAL.CANCEL')"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!text-xs font-medium"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('CALL_MODAL.CALL')"
|
||||
icon="i-ri-phone-fill"
|
||||
size="sm"
|
||||
class="!text-xs font-medium"
|
||||
:disabled="!selectedInbox || !selectedContact || isLoading"
|
||||
:is-loading="isLoading"
|
||||
@click="makeCall"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import ContactAPI from 'dashboard/api/contacts';
|
||||
import VoiceAPI from 'dashboard/api/channels/voice';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import ContactSelector from 'dashboard/components-next/NewConversation/components/ContactSelector.vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const selectedContact = ref(null);
|
||||
const selectedInbox = ref(null);
|
||||
const showContactsDropdown = ref(false);
|
||||
const showInboxDropdown = ref(false);
|
||||
const contacts = ref([]);
|
||||
const isSearching = ref(false);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||
|
||||
const voiceInboxesList = computed(() => {
|
||||
return inboxes.value
|
||||
.filter(inbox => inbox.channel_type === INBOX_TYPES.VOICE)
|
||||
.map(inbox => ({
|
||||
id: inbox.id,
|
||||
title: `${inbox.name}`,
|
||||
subtitle: inbox.phone_number,
|
||||
label: `${inbox.name} - ${inbox.phone_number}`,
|
||||
action: 'select-inbox',
|
||||
value: inbox.id,
|
||||
sourceId: inbox.id,
|
||||
phoneNumber: inbox.phone_number,
|
||||
name: inbox.name,
|
||||
icon: 'i-ri-phone-fill',
|
||||
}));
|
||||
});
|
||||
|
||||
// Auto-select the first available voice inbox
|
||||
watch(voiceInboxesList, (newList) => {
|
||||
if (newList.length > 0 && !selectedInbox.value) {
|
||||
selectedInbox.value = newList[0];
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const selectInbox = item => {
|
||||
const inbox = voiceInboxesList.value.find(i => i.value === item.value);
|
||||
if (inbox) {
|
||||
selectedInbox.value = inbox;
|
||||
showInboxDropdown.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectedContact = ({ value, action, ...rest }) => {
|
||||
// If this is a direct call to a phone number
|
||||
if (action === 'create' && value.match(/^\+?[0-9\s\-()]+$/)) {
|
||||
selectedContact.value = {
|
||||
id: 'direct-call',
|
||||
name: t('CALL_MODAL.CALL_DIRECTLY'),
|
||||
sourceId: 'direct-call',
|
||||
phoneNumber: value,
|
||||
action: 'contact',
|
||||
};
|
||||
} else {
|
||||
// For existing contacts, make sure we're capturing their ID properly
|
||||
console.log('Contact selected from dropdown:', { value, action, ...rest });
|
||||
selectedContact.value = {
|
||||
...rest,
|
||||
sourceId: rest.id || rest.value || value // Make sure we have the ID in sourceId
|
||||
};
|
||||
}
|
||||
showContactsDropdown.value = false;
|
||||
};
|
||||
|
||||
const handleDropdownUpdate = (type, value) => {
|
||||
showContactsDropdown.value = value;
|
||||
};
|
||||
|
||||
const clearSelectedContact = () => {
|
||||
selectedContact.value = null;
|
||||
};
|
||||
|
||||
// This function gets called from the ContactSelector component
|
||||
const handleContactSearch = value => {
|
||||
showContactsDropdown.value = true;
|
||||
// Pass all the needed keys for search when using the value sent directly
|
||||
debouncedSearchContacts(value);
|
||||
};
|
||||
|
||||
const debouncedSearchContacts = debounce(async query => {
|
||||
if (!query || query.length < 2) {
|
||||
contacts.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
isSearching.value = true;
|
||||
try {
|
||||
// Use the simple search endpoint since it's more reliable for this use case
|
||||
const { data } = await ContactAPI.search(query);
|
||||
|
||||
console.log('Search response:', data); // Log the search response
|
||||
|
||||
// Ensure contacts.value is an array and convert to camelCase
|
||||
const searchResults = data?.payload ? camelcaseKeys(data.payload, { deep: true }) : [];
|
||||
|
||||
// Filter to only include contacts with phone numbers
|
||||
const contactsWithPhone = searchResults.filter(contact => contact.phoneNumber);
|
||||
|
||||
// Map the contacts to ensure they have sourceId set to ID for consistency
|
||||
contacts.value = contactsWithPhone.map(contact => ({
|
||||
...contact,
|
||||
sourceId: contact.id, // Make sure sourceId is set
|
||||
value: contact.id // Make sure value is set for TagInput
|
||||
}));
|
||||
|
||||
// If it looks like a phone number, add option to call directly
|
||||
if (query.match(/^\+?[0-9\s\-()]+$/) && !contacts.value.some(c => c.phoneNumber === query)) {
|
||||
contacts.value.push({
|
||||
id: 'direct-call',
|
||||
name: t('CALL_MODAL.CALL_DIRECTLY'),
|
||||
phoneNumber: query,
|
||||
sourceId: 'direct-call',
|
||||
value: 'direct-call'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Processed contacts for dropdown:', contacts.value);
|
||||
} catch (error) {
|
||||
console.error('Error searching contacts:', error);
|
||||
contacts.value = []; // Ensure this is always an array
|
||||
useAlert(t('CALL_MODAL.CONTACT_SEARCH_ERROR'));
|
||||
} finally {
|
||||
isSearching.value = false;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const makeCall = async () => {
|
||||
if (!selectedInbox.value || !selectedContact.value) {
|
||||
useAlert(t('CALL_MODAL.VALIDATION_ERROR'));
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const isDirect = selectedContact.value.sourceId === 'direct-call';
|
||||
const contactId = isDirect ? null : (selectedContact.value.sourceId || selectedContact.value.id);
|
||||
|
||||
if (contactId) {
|
||||
console.log('Making call to contact ID:', contactId, 'with full contact:', selectedContact.value);
|
||||
// Use VoiceAPI.initiateCall instead of direct axios call
|
||||
await VoiceAPI.initiateCall(contactId);
|
||||
} else {
|
||||
// For direct phone number calls
|
||||
const phoneNumber = selectedContact.value.phoneNumber;
|
||||
|
||||
if (!phoneNumber) {
|
||||
throw new Error('Phone number is required for direct calls');
|
||||
}
|
||||
|
||||
// First create a contact with this phone number
|
||||
console.log('Creating new contact with phone number:', phoneNumber);
|
||||
const contactPayload = {
|
||||
phone_number: phoneNumber,
|
||||
inbox_id: selectedInbox.value.sourceId,
|
||||
name: `Phone: ${phoneNumber}`,
|
||||
};
|
||||
|
||||
const contactResponse = await ContactAPI.create(contactPayload);
|
||||
console.log('Created contact:', contactResponse.data);
|
||||
|
||||
// Then initiate call to the newly created contact
|
||||
if (contactResponse.data && contactResponse.data.payload && contactResponse.data.payload.contact) {
|
||||
const newContactId = contactResponse.data.payload.contact.id;
|
||||
console.log('Using new contact ID:', newContactId);
|
||||
await VoiceAPI.initiateCall(newContactId);
|
||||
} else {
|
||||
throw new Error('Failed to create contact for direct call');
|
||||
}
|
||||
}
|
||||
|
||||
useAlert(t('CALL_MODAL.SUCCESS_MESSAGE'));
|
||||
emit('close');
|
||||
} catch (error) {
|
||||
console.error('Error making call:', error);
|
||||
|
||||
let errorMessage = t('CALL_MODAL.ERROR_MESSAGE');
|
||||
|
||||
// Simple error handling - just show server message if available
|
||||
if (error.response && error.response.data && error.response.data.error) {
|
||||
errorMessage = error.response.data.error;
|
||||
}
|
||||
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// The first inbox will be selected automatically via the watch
|
||||
// This ensures it works even if voiceInboxesList is populated after mounting
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user