You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Global-sales/src/views/Conversations/Online/Input/Template.jsx

371 lines
16 KiB
JavaScript

import { useState, useRef, useEffect, memo, useMemo, useCallback } from 'react';
import { App, Popover, Flex, Button, List, Input, Tabs, Tag, Alert, Divider } from 'antd';
import { MessageOutlined, SendOutlined } from '@ant-design/icons';
import useAuthStore from '@/stores/AuthStore';
import useConversationStore from '@/stores/ConversationStore';
import { cloneDeep, getNestedValue, groupBy, objectMapper, removeFormattingChars, sortArrayByOrder, TagColorStyle } from '@/utils/commons';
import { replaceTemplateString } from '@/channel/bubbleMsgUtils';
import { isEmpty } from '@/utils/commons';
import useStyleStore from '@/stores/StyleStore';
const splitTemplate = (template) => {
const placeholders = template.match(/{{(.*?)}}/g) || [];
const keys = placeholders.map((placeholder) => placeholder.slice(2, -2));
const arr = template.split(/{{(.*?)}}/).filter(Boolean);
const obj = arr.reduce((prev, curr, index) => {
if (keys.includes(curr)) {
prev.push({ key: curr });
} else {
prev.push(curr);
}
return prev;
}, []);
return obj;
};
// UTILITY
// MARKETING
const templateCaterogyText = { 'UTILITY': '跟进', 'MARKETING': '营销' }
const templateCaterogyTipText = { 'UTILITY': '触达率高', 'MARKETING': '' }
const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, activeInput }) => {
const currentConversation = useConversationStore((state) => state.currentConversation);
const renderForm = ({ tempItem }, key = 'body') => {
const templateText = tempItem.components?.[key]?.[0]?.text || ''
const tempArr = splitTemplate(templateText)
const keys = (templateText.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''))
const paramsVal = keys.reduce((r, k) => ({ ...r, [k]: getNestedValue(valueMapped, [k]) }), {})
return (
<>
{tempArr.map((ele) =>
typeof ele === 'string' ? (
<span key={ele.trim()} className=' text-wrap'>
{ele.replace(/\n+/g, '\n')}
</span>
) : ele.key.includes('free') || ele.key.includes('detail') ? (
<Input.TextArea
key={ele.key}
rows={2}
onChange={(e) => {
onInput(tempItem, ele.key, e.target.value, paramsVal)
}}
className={` w-11/12 `}
size={'small'}
title={ele.key}
placeholder={`${paramsVal[ele.key] || ele.key} 按Tab键跳到下一个空格\n注意: 模板消息无法输入换行`}
value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''}
// onPressEnter={() => handleSendTemplate(tempItem)}
/>
) : (
<Input
key={ele.key}
onChange={(e) => {
onInput(tempItem, ele.key, e.target.value, paramsVal)
}}
className={ele.key.includes('free') || ele.key.includes('detail') ? `w-full block ` : `w-auto ${paramsVal[ele.key] ? 'max-w-24' : 'max-w-60'}`}
size={'small'}
title={ele.key}
placeholder={`${paramsVal[ele.key] || ele.key} 按Tab键跳到下一个空格`}
value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''}
// onPressEnter={() => handleSendTemplate(tempItem)}
/>
),
)}
</>
)
};
const renderHeader = ({ tempItem }) => {
if (isEmpty(tempItem.components.header)) {
return null;
}
const headerObj = tempItem.components.header[0];
return (
<div className='pb-1'>
{'text' === headerObj.format.toLowerCase() && <div>{renderForm({ tempItem }, 'header')}</div>}
{'image' === headerObj.format.toLowerCase() && <img src={headerObj.example.header_url} height={100}></img>}
{['document', 'video'].includes(headerObj.format.toLowerCase()) && (
<a href={headerObj.example.header_url} target='_blank' key={headerObj.format} rel='noreferrer' className='text-sm'>
[&nbsp;{headerObj.format}&nbsp;]({headerObj.example.header_url})
</a>
)}
</div>
);
}
const renderButtons = ({ tempItem }) => {
if (isEmpty(tempItem.components.buttons)) {
return null;
}
const buttons = tempItem.components.buttons.reduce((r, c) => r.concat(c.buttons), []);
return (
<div className='flex gap-1 pt-1'>
{buttons.map((btn, index) =>
btn.type.toLowerCase() === 'url' ? (
<Button className='text-blue-500' size={'small'} href={btn.url} target={'_blank'} key={btn.url} rel='noreferrer'>
{btn.text}
</Button>
) : btn.type.toLowerCase() === 'phone_number' ? (
<Button className='text-blue-500' size={'small'} key={btn.phone_number} rel='noreferrer'>
{btn.text} ({btn.phone_number})
</Button>
) : (
<Button className='text-blue-500' size={'small'} key={btn.type} rel='noreferrer'>
{btn.text}
</Button>
)
)}
</div>
);
}
return (
<List
className=' h-[90%] overflow-y-auto text-slate-900'
itemLayout='horizontal'
dataSource={dataSource}
rowKey={'key'}
pagination={dataSource.length < 4 ? false : { position: 'bottom', pageSize: 3, align: 'start', size: 'small' }}
renderItem={(item, index) => (
<List.Item key={`${currentConversation.sn}_${item.key}`}>
<List.Item.Meta
className=' '
title={
<Flex justify={'space-between'}>
<span>
{item.components.header?.[0]?.text || (item.displayName)}
<Tag style={{ ...TagColorStyle(item.language.toUpperCase(), true) }} className='ml-1'>
{item.language.toUpperCase()}
</Tag>
{/* <Tag style={{...TagColorStyle(item.category.toUpperCase(), true)}}>{templateCaterogyText[item.category]}</Tag> */}
{templateCaterogyTipText[item.category] && <Tag style={{ ...TagColorStyle(item.category.toUpperCase(), true) }}>{templateCaterogyTipText[item.category]}</Tag>}
</span>
<Button onClick={() => handleSendTemplate(item)} size={'small'} type='link' key={'send'} icon={<SendOutlined />}>
Send
</Button>
</Flex>
}
description={
<>
<div className=' max-h-32 overflow-y-auto divide-dashed divide-x-0 divide-y divide-gray-300'>
{renderHeader({ tempItem: item })}
<div className='text-slate-500 py-1 whitespace-pre-wrap'>{renderForm({ tempItem: item })}</div>
{item.components?.footer?.[0] ? <div className=''>{item.components.footer[0].text || ''}</div> : null}
{renderButtons({ tempItem: item })}
</div>
</>
}
/>
</List.Item>
)}
/>
)
}
const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
const [mobile] = useStyleStore((state) => [state.mobile]);
const searchInputRef = useRef(null);
const { notification } = App.useApp();
const loginUser = useAuthStore((state) => state.loginUser);
const { whatsAppBusiness } = loginUser;
loginUser.usernameEN = loginUser.accountList[0].OPI_NameEN.split(' ')?.[0] || loginUser.username;
const currentConversation = useConversationStore((state) => state.currentConversation);
const templates = useConversationStore((state) => state.templates);
const [openTemplates, setOpenTemplates] = useState(false);
const [dataSource, setDataSource] = useState(templates);
const [templateCMapped, setTemplateCMapped] = useState({});
const [templateLangMapped, setTemplateLangMapped] = useState({});
const [searchContent, setSearchContent] = useState('');
// 用于替换变量: customer, agent
const valueMapped = { ...cloneDeep(currentConversation), ...objectMapper(loginUser, { usernameEN: [{ key: 'agent_name' }, { key: 'your_name' }, { key: 'your_name1' }, { key: 'your_name2' }] }) };
useEffect(() => {
setDataSource([]);
// setDataSource(templates);
const mappedByCategory = groupBy(templates, 'category');
const mappedByLang = groupBy(templates, 'language');
setTemplateCMapped(mappedByCategory);
setTemplateLangMapped(mappedByLang);
return () => {};
}, [templates]);
useEffect(() => {
setActiveInput({});
return () => {};
}, [currentConversation.sn])
const handleSearchTemplates = (val) => {
if (val.toLowerCase().trim() !== '') {
const res = templates.filter(
(item) => item.name.includes(val.toLowerCase().trim()) || item.components_origin.some((itemc) => (itemc?.text || '').toLowerCase().includes(val.toLowerCase().trim()))
);
setDataSource(res);
return false;
}
setDataSource([]);
};
const handleSendTemplate = (fromTemplate) => {
const mergeInput = { ...cloneDeep(valueMapped), ...activeInput[fromTemplate.name] };
let valid = true;
const msgObj = {
type: 'whatsappTemplate',
// statusTitle: 'Ready to send',
template: {
name: fromTemplate.name,
language: { code: fromTemplate.language },
components: sortArrayByOrder(fromTemplate.components_origin.map((citem) => {
const keys = ((citem?.text || '').match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''));
const params = keys.map((v) => ({ type: 'text', text: getNestedValue(mergeInput, [v]) }));
if (citem.type.toLowerCase() === 'header' && (citem?.format || 'text').toLowerCase() !== 'text') {
params[0] = { type: citem.format.toLowerCase(), [citem.format.toLowerCase()]: { link: citem.example.header_url[0] } };
}
const paramText = keys.length ? params.map((p) => p.text) : [];
const fillTemplate = paramText.length ? replaceTemplateString(citem?.text || '', paramText) : citem?.text || '';
valid = keys.length !== paramText.filter((s) => s).length ? false : valid;
return ['body', 'header'].includes(citem.type.toLowerCase()) ? {
type: citem.type.toLowerCase(),
parameters: params,
text: fillTemplate,
} : {...citem, type: citem.type.toLowerCase(),};
}), 'type', ['header', 'body', 'footer', 'buttons'] ),
},
template_origin: fromTemplate,
};
if (valid !== true) {
notification.warning({
message: '提示',
description: '信息未填写完整, 请补充填写',
placement: 'top',
duration: 3,
closeIcon: false,
});
return false;
}
invokeSendMessage(msgObj);
setOpenTemplates(false);
setActiveInput({});
};
const [activeInput, setActiveInput] = useState({});
const onInput = (tempItem, key, val, initObj) => {
const _val = removeFormattingChars(val); // Param text cannot have new-line/tab characters or more than 4 consecutive spaces
setActiveInput((prev) => {
return { ...prev, [tempItem.name]: { ...initObj, ...prev[tempItem.name], [key]: _val } };
});
};
return (
<>
<Popover
overlayClassName={[mobile === false ? 'w-3/5' : 'w-full max-h-full'].join(' ')}
fresh
forceRender
destroyTooltipOnHide={true}
title={
<div className='flex justify-between mt-0 gap-4 items-center'>
<Input.Search prefix={'💬'}
ref={searchInputRef}
onSearch={handleSearchTemplates}
allowClear
value={searchContent}
onChange={(e) => {
setSearchContent(e.target.value);
handleSearchTemplates(e.target.value);
}}
placeholder='搜索名称'
/>
<Button size='small' onClick={() => setOpenTemplates(false)}>&times;</Button>
</div>
}
content={
<>
{/* <div className='flex justify-between mt-2 gap-4 items-center'>
<Input.Search prefix={'🙋'}
ref={searchInputRef}
onSearch={handleSearchTemplates}
allowClear
value={searchContent}
onChange={(e) => {
setSearchContent(e.target.value);
handleSearchTemplates(e.target.value);
}}
placeholder='搜索名称'
/>
<Button size='small' onClick={() => setOpenTemplates(false)}>&times;</Button>
</div> */}
{isEmpty(dataSource) && isEmpty(searchContent) ? (
<Tabs items={[
// { key: 'marketing', label: '首次问候', children: <CategoryList key={'utility-templates'} dataSource={templateCMapped?.MARKETING || []} {...{ handleSendTemplate, activeInput, onInput, valueMapped}} />},
// { key: 'utility', label: '再次沟通', children: <CategoryList key={'utility-templates'} dataSource={templateCMapped?.UTILITY || []} {...{ handleSendTemplate, activeInput, onInput, valueMapped}} />},
...(Object.keys(templateLangMapped).map(lang => ({
key: lang, label: lang.toUpperCase(), children: <CategoryList key={'lang-templates-'+lang} dataSource={templateLangMapped[lang]} {...{ handleSendTemplate, activeInput, onInput, valueMapped}} />
})))
]} defaultActiveKey='utility' tabBarExtraContent={{right: <Alert type='info' message='为提升账号质量, 请尽量使用跟进模板' showIcon className='py-0' />, }} size='small' />
) :
(
// Search result
<CategoryList {...{ handleSendTemplate, activeInput, onInput, valueMapped }} dataSource={dataSource} key='search-templates' />
)}
{/* <List
className='h-4/6 overflow-y-auto text-slate-900'
itemLayout='horizontal'
dataSource={dataSource}
rowKey={'name'}
pagination={dataSource.length < 4 ? false : { position: 'bottom', pageSize: 3, align: 'start', size: 'small' }}
renderItem={(item, index) => (
<List.Item key={`${currentConversation.sn}_${item.name}`}>
<List.Item.Meta
className=' '
title={
<Flex justify={'space-between'}>
<span>{item.components.header?.[0]?.text || item.name}<Tag color='blue' className='ml-1'>{item.language.toUpperCase()}</Tag></span>
<Button onClick={() => handleSendTemplate(item)} size={'small'} type='link' key={'send'} icon={<SendOutlined />}>
Send
</Button>
</Flex>
}
description={
<>
<div className=' max-h-32 overflow-y-auto divide-dashed divide-x-0 divide-y divide-gray-300'>
{renderHeader({ tempItem: item })}
<div className='text-slate-500 py-1 whitespace-pre-wrap'>{renderForm({ tempItem: item })}</div>
{item.components?.footer?.[0] ? <div className=''>{item.components.footer[0].text || ''}</div> : null}
{renderButtons({ tempItem: item })}
</div>
</>
}
/>
</List.Item>
)}
/> */}
</>
}
// title={
// <div className='flex justify-between mt-2 '>
// <div>🙋打招呼</div>
// <Button size='small' onClick={() => setOpenTemplates(false)}>&times;</Button>
// </div>}
trigger='click' arrow={false}
open={openTemplates}
onOpenChange={(v) => {
setOpenTemplates(v);
// setActiveInput({});
}}>
<Button type='text' className='' icon={<MessageOutlined />} size={'middle'} disabled={disabled} />
</Popover>
</>
);
};
export default InputTemplate;