diff --git a/app/livechat/server/api/v1/contact.js b/app/livechat/server/api/v1/contact.js index 10880b2829135..f98b721c55fad 100644 --- a/app/livechat/server/api/v1/contact.js +++ b/app/livechat/server/api/v1/contact.js @@ -1,11 +1,13 @@ import { Match, check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/Livechat'; +import { Contacts } from '../../lib/Contacts'; import { LivechatVisitors, } from '../../../../models'; + API.v1.addRoute('omnichannel/contact', { authRequired: true }, { post() { try { @@ -15,15 +17,11 @@ API.v1.addRoute('omnichannel/contact', { authRequired: true }, { name: String, email: Match.Maybe(String), phone: Match.Maybe(String), - livechatData: Match.Maybe(Object), + customFields: Match.Maybe(Object), contactManager: Match.Maybe(Object), }); - const contactParams = this.bodyParams; - if (this.bodyParams.phone) { - contactParams.phone = { number: this.bodyParams.phone }; - } - const contact = Livechat.registerGuest(contactParams); + const contact = Contacts.registerContact(this.bodyParams); return API.v1.success({ contact }); } catch (e) { @@ -40,3 +38,31 @@ API.v1.addRoute('omnichannel/contact', { authRequired: true }, { return API.v1.success({ contact }); }, }); + + +API.v1.addRoute('omnichannel/contact.search', { authRequired: true }, { + get() { + try { + check(this.queryParams, { + email: Match.Maybe(String), + phone: Match.Maybe(String), + }); + + const { email, phone } = this.queryParams; + + if (!email && !phone) { + throw new Meteor.Error('error-invalid-params'); + } + + const query = Object.assign({}, { + ...email && { visitorEmails: { address: email } }, + ...phone && { phone: { phoneNumber: phone } }, + }); + + const contact = Promise.await(LivechatVisitors.findOne(query)); + return API.v1.success({ contact }); + } catch (e) { + return API.v1.failure(e); + } + }, +}); diff --git a/app/livechat/server/lib/Contacts.js b/app/livechat/server/lib/Contacts.js new file mode 100644 index 0000000000000..7e0f94e10d7c8 --- /dev/null +++ b/app/livechat/server/lib/Contacts.js @@ -0,0 +1,65 @@ +import { check } from 'meteor/check'; +import s from 'underscore.string'; + +import { + LivechatVisitors, + LivechatCustomField, +} from '../../../models'; + + +export const Contacts = { + + registerContact({ token, name, email, phone, username, customFields = {}, contactManager = {} } = {}) { + check(token, String); + + let contactId; + const updateUser = { + $set: { + token, + }, + }; + + const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); + + if (user) { + contactId = user._id; + } else { + if (!username) { + username = LivechatVisitors.getNextVisitorUsername(); + } + + let existingUser = null; + + if (s.trim(email) !== '' && (existingUser = LivechatVisitors.findOneGuestByEmailAddress(email))) { + contactId = existingUser._id; + } else { + const userData = { + username, + ts: new Date(), + }; + + contactId = LivechatVisitors.insert(userData); + } + } + + updateUser.$set.name = name; + updateUser.$set.phone = (phone && [{ phoneNumber: phone }]) || null; + updateUser.$set.visitorEmails = (email && [{ address: email }]) || null; + + const allowedCF = LivechatCustomField.find({ scope: 'visitor' }).map(({ _id }) => _id); + + const livechatData = Object.keys(customFields) + .filter((key) => allowedCF.includes(key) && customFields[key] !== '' && customFields[key] !== undefined) + .reduce((obj, key) => { + obj[key] = customFields[key]; + return obj; + }, {}); + + updateUser.$set.livechatData = livechatData; + updateUser.$set.contactManager = (contactManager?.username && { username: contactManager.username }) || null; + + LivechatVisitors.updateById(contactId, updateUser); + + return contactId; + }, +}; diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index dbd53debd3c02..198b5da2eed1b 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -192,7 +192,7 @@ export const Livechat = { return true; }, - registerGuest({ token, name, email, department, phone, username, livechatData, contactManager, connectionData } = {}) { + registerGuest({ token, name, email, department, phone, username, connectionData } = {}) { check(token, String); let userId; @@ -200,7 +200,6 @@ export const Livechat = { $set: { token, }, - $unset: { }, }; const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); @@ -235,45 +234,29 @@ export const Livechat = { } } - if (name) { - updateUser.$set.name = name; - } - if (phone) { updateUser.$set.phone = [ { phoneNumber: phone.number }, ]; - } else { - updateUser.$unset.phone = 1; } if (email && email.trim() !== '') { updateUser.$set.visitorEmails = [ { address: email }, ]; - } else { - updateUser.$unset.visitorEmails = 1; } - if (livechatData) { - updateUser.$set.livechatData = livechatData; - } else { - updateUser.$unset.livechatData = 1; - } - - if (contactManager) { - updateUser.$set.contactManager = contactManager; - } else { - updateUser.$unset.contactManager = 1; + if (name) { + updateUser.$set.name = name; } if (!department) { - updateUser.$unset.department = 1; + Object.assign(updateUser, { $unset: { department: 1 } }); } else { const dep = LivechatDepartment.findOneByIdOrName(department); updateUser.$set.department = dep && dep._id; } - if (_.isEmpty(updateUser.$unset)) { delete updateUser.$unset; } + LivechatVisitors.updateById(userId, updateUser); return userId; diff --git a/client/omnichannel/directory/ContactForm.js b/client/omnichannel/directory/ContactForm.js index a70031b22d6ce..fb018ac6b77ad 100644 --- a/client/omnichannel/directory/ContactForm.js +++ b/client/omnichannel/directory/ContactForm.js @@ -1,5 +1,5 @@ import React, { useState, useMemo } from 'react'; -import { Field, TextInput, Icon, ButtonGroup, Button, Box } from '@rocket.chat/fuselage'; +import { Field, TextInput, ButtonGroup, Button, Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSubscription } from 'use-subscription'; @@ -95,7 +95,7 @@ export function ContactNewEdit({ id, data, reload, close }) { const [nameError, setNameError] = useState(); const [emailError, setEmailError] = useState(); - const [phoneError] = useState(); + const [phoneError, setPhoneError] = useState(); const { value: allCustomFields, phase: state } = useEndpointData('livechat/custom-fields'); @@ -118,6 +118,27 @@ export function ContactNewEdit({ id, data, reload, close }) { ? jsonConverterToValidFormat(allCustomFields.customFields) : {}), [allCustomFields]); const saveContact = useEndpointAction('POST', 'omnichannel/contact'); + const emailAlreadyExistsAction = useEndpointAction('GET', `omnichannel/contact.search?email=${ email }`); + const phoneAlreadyExistsAction = useEndpointAction('GET', `omnichannel/contact.search?phone=${ phone }`); + + const checkEmailExists = useMutableCallback(async () => { + if (!isEmail(email)) { return; } + const { contact } = await emailAlreadyExistsAction(); + if (!contact || (id && contact._id === id)) { + return setEmailError(null); + } + setEmailError(t('Email_already_exists')); + }); + + const checkPhoneExists = useMutableCallback(async () => { + if (!phone) { return; } + const { contact } = await phoneAlreadyExistsAction(); + if (!contact || (id && contact._id === id)) { + return setPhoneError(null); + } + setPhoneError(t('Phone_already_exists')); + }); + const dispatchToastMessage = useToastMessageDispatch(); @@ -125,8 +146,11 @@ export function ContactNewEdit({ id, data, reload, close }) { setNameError(!name ? t('The_field_is_required', t('Name')) : ''); }, [t, name]); useComponentDidUpdate(() => { - setEmailError(email && !isEmail(email) ? t('Validate_email_address') : undefined); + setEmailError(email && !isEmail(email) ? t('Validate_email_address') : null); }, [t, email]); + useComponentDidUpdate(() => { + !phone && setPhoneError(null); + }, [phone]); const handleSave = useMutableCallback(async (e) => { e.preventDefault(); @@ -146,9 +170,11 @@ export function ContactNewEdit({ id, data, reload, close }) { const payload = { name, - email, - phone, }; + payload.phone = phone; + payload.email = email; + payload.customFields = livechatData || {}; + payload.contactManager = username ? { username } : {}; if (id) { payload._id = id; @@ -156,8 +182,6 @@ export function ContactNewEdit({ id, data, reload, close }) { } else { payload.token = createToken(); } - if (livechatData) { payload.livechatData = livechatData; } - if (username) { payload.contactManager = { username }; } try { await saveContact(payload); @@ -169,7 +193,7 @@ export function ContactNewEdit({ id, data, reload, close }) { } }); - const formIsValid = name && !emailError; + const formIsValid = name && !emailError && !phoneError; if ([state].includes(AsyncStatePhase.LOADING)) { @@ -190,7 +214,7 @@ export function ContactNewEdit({ id, data, reload, close }) { {t('Email')} - }/> + {t(emailError)} @@ -199,7 +223,7 @@ export function ContactNewEdit({ id, data, reload, close }) { {t('Phone')} - + {t(phoneError)} diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 871acce9b9bae..f4ae1a419ed31 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1539,6 +1539,7 @@ "error-invalid-method": "Invalid method", "error-invalid-name": "Invalid name", "error-invalid-password": "Invalid password", + "error-invalid-params": "Invalid params", "error-invalid-permission": "Invalid permission", "error-invalid-priority": "Invalid priority", "error-invalid-redirectUri": "Invalid redirectUri", @@ -2921,6 +2922,7 @@ "Permissions": "Permissions", "Personal_Access_Tokens": "Personal Access Tokens", "Phone": "Phone", + "Phone_already_exists": "Phone already exists", "Phone_number": "Phone number", "Pin": "Pin", "Pin_Message": "Pin Message", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 4d0e1d47c3eb8..76b039d91a3a7 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -2519,6 +2519,7 @@ "Permissions": "Permissões", "Personal_Access_Tokens": "Tokens de acesso pessoal", "Phone": "Telefone", + "Phone_already_exists": "Telefone já cadastrado", "Phone_number": "Telefone", "Pin": "Fixar", "Pin_Message": "Fixar Mensagem",