Merge branch 'main' of github.com:hainatravel/global-sales

dev/mobile
Jimmy Liow 2 years ago
commit 6de4189392

@ -0,0 +1,116 @@
import { groupBy } from '@/utils/utils';
import { fetchJSON, postJSON } from '@/utils/request'
import { parseRenderMessageList } from '@/lib/msgUtils';
const API_HOST = 'https://p9axztuwd7x8a7.mycht.cn/whatsapp_callback';
const NAME_SPACE = 'CONVERSATION/';
export const initWebsocket = (socket) => ({
type: NAME_SPACE + 'INIT_WEBSOCKET',
payload: socket,
});
export const closeWebsocket0 = () => ({
type: NAME_SPACE + 'CLOSE_WEBSOCKET',
payload: null,
});
export const closeWebsocket = () => {};
export const receivedNewMessage = (targetId, message) => ({
type: NAME_SPACE + 'RECEIVED_NEW_MESSAGE',
payload: {
targetId,
message,
},
});
export const sentNewMessage = (message) => ({
type: NAME_SPACE + 'SENT_NEW_MESSAGE',
payload: {
targetId: message.conversationid,
message,
},
});
export const addError = (error) => {
return {
type: NAME_SPACE + 'ADD_ERROR',
payload: error,
};
};
export const receivedTemplates = (data) => ({
type: NAME_SPACE + 'SET_TEMPLATE_LIST',
payload: data,
});
export const receivedConversationList = (data) => {
return {
type: NAME_SPACE + 'SET_CONVERSATION_LIST',
payload: data,
};
};
export const addConversationList = (data) => {
return {
type: NAME_SPACE + 'ADD_TO_CONVERSATIONS_LIST',
payload: data,
};
};
// export const updateConversationListItemNew = (message) => ({
// type: NAME_SPACE + 'UPDATE_CONVERSATION_LIST_ITEM_NEW',
// payload: message,
// });
export const receivedCustomerProfile = (data) => ({
type: NAME_SPACE + 'SET_CUSTOMER_ORDER_PROFILE',
payload: data,
});
/**
* @deprecated 在更新list时操作
*/
export const setActiveConversations = (obj) => ({
type: NAME_SPACE + 'SET_ACTIVE_CONVERSATIONS',
payload: obj,
});
export const setCurrentConversation = (obj) => ({
type: NAME_SPACE + 'SET_CURRENT_CONVERSATION',
payload: obj,
});
export const updateMessageItem = (message) => ({
type: NAME_SPACE + 'UPDATE_SENT_MESSAGE_ITEM',
payload: message,
});
export const receivedMessageList = (targetId, data) => ({
type: NAME_SPACE + 'RECEIVED_MESSAGE_LIST',
payload: { targetId, data },
})
export const fetchTemplates = async () => {
const data = await fetchJSON(`${API_HOST}/listtemplates`);
const canUseTemplates = (data?.result?.items || [])
.filter((_t) => _t.status !== 'REJECTED')
.map((ele) => ({ ...ele, components_origin: ele.components, components: groupBy(ele.components, (_c) => _c.type.toLowerCase()) }));
return canUseTemplates;
};
export const fetchConversationsList = async (opisn) => {
const { result: data } = await fetchJSON(`${API_HOST}/getconversations`, { opisn });
const list = data.map((ele) => ({ ...ele, customer_name: ele.whatsapp_name.trim() }));
return list;
};
export const fetchMessages = async (params) => {
const { result } = await fetchJSON(`${API_HOST}/getcusmessages`, params);
return parseRenderMessageList(result);
}
export const fetchCustomerProfile = async (colisn) => {
const { result } = await fetchJSON(`${API_HOST}/getorderinfo`, { colisn });
const data = result?.[0] || {};
return data;
};
export const postConversationItemClose = async (body) => {
const getParams = body ? new URLSearchParams(body).toString() : '';
const { result } = await postJSON(`${API_HOST}/closeconversation?${getParams}`);
return result;
};

@ -33,8 +33,8 @@ export const sentMsgTypeMapped = {
contentToRender: (msg) => {
console.log(msg);
const templateDataMapped = msg.template?.components ? msg.template.components.reduce((r, v) => ({...r, [v.type]: v}), {}) : null;
const templateParam = (templateDataMapped?.body?.parameters || []).map(e => e.text);
const fillTemplate = templateParam.length ? replaceTemplateString(msg.template_origin.components.body?.[0]?.text || '', templateParam) : (msg.template_origin.components.body?.[0]?.text || '');
// const templateParam = (templateDataMapped?.body?.parameters || []).map(e => e.text);
// const fillTemplate = templateParam.length ? replaceTemplateString(msg.template_origin.components.body?.[0]?.text || '', templateParam) : (msg.template_origin.components.body?.[0]?.text || '');
// const footer = msg.template_origin.components?.footer?.[0]?.text || '';
return {
...msg,
@ -42,7 +42,7 @@ export const sentMsgTypeMapped = {
conversationid: msg.id.split('.')[0],
type: 'text',
title: msg.template_origin.components.header?.[0]?.text || '',
text: `${fillTemplate}`, // msg.template_origin.components.body?.[0]?.text || '',
text: templateDataMapped?.body?.text || '', // msg.template_origin.components.body?.[0]?.text || '',
};
},
},
@ -65,12 +65,25 @@ const whatsappMsgMapped = {
console.log('getMsg', result);
return isEmpty(result?.whatsappMessage) ? null : { ...result.whatsappMessage, conversationid: result.conversationid };
},
contentToRender: () => null, // * 仅更新消息状态, 没有输出
contentToUpdate: (msgcontent) => ({ ...msgcontent, id: msgcontent.wamid, status: msgStatusRenderMapped[(msgcontent?.status || 'failed')] }),
contentToRender: (contentObj) => {
if ((contentObj?.status === 'failed' )) {
contentObj = {
...contentObj,
type: 'error',
text: {body: `❌ Message failed to send.` }, // contentObj.errorMessage
id: contentObj.id,
wamid: contentObj.id,
};
return parseRenderMessageItem(contentObj);
}
// * 仅更新消息状态, 没有输出
return null;
},
contentToUpdate: (msgcontent) => ({ ...msgcontent, id: msgcontent.wamid, status: msgStatusRenderMapped[(msgcontent?.status || 'failed')], dateString: msgcontent.status==='failed' ? '发送失败 ❌' : '', }),
},
};
export const msgStatusRenderMapped = {
'accepted': 'sent',
'accepted': 'waiting', // 'sent', // 接口的发送请求
'sent': 'sent',
'delivered': 'received',
'read': 'read',
@ -87,13 +100,14 @@ export const receivedMsgTypeMapped = {
actionId: msgcontent.actionId,
id: msgcontent.wamid,
status: msgStatusRenderMapped[(msgcontent?.status || 'failed')],
conversationid: msgcontent.actionId.split('.')[0], // msgcontent.conversation.id,
conversationid: msgcontent.actionId.split('.')[0], // msgcontent.conversation.sn,
}),
},
'error': {
// 发送消息的同步返回: 发送失败时
getMsg: (result) => result,
contentToRender: () => null,
contentToUpdate: (msgcontent) => ({ ...msgcontent, id: msgcontent.actionId, status: msgcontent?.status || 'failed', dateString: '发送失败 ❌' }),
contentToUpdate: (msgcontent) => ({ ...msgcontent, id: msgcontent.actionId, status: msgcontent?.status || 'failed', dateString: '发送失败 ❌', conversationid: msgcontent.actionId.split('.')[0], }),
},
};
export const whatsappMsgTypeMapped = {
@ -102,8 +116,8 @@ export const whatsappMsgTypeMapped = {
data: (msg) => ({ id: msg.wamid, text: msg.errorCode ? msg.errorMessage : msg.text.body }),
},
text: {
type: (msg) => ({ type: msg.errorCode ? 'system' : 'text' }),
data: (msg) => ({ id: msg.wamid, text: msg.errorCode ? msg.errorMessage : msg.text.body }),
type: 'text',
data: (msg) => ({ id: msg.wamid, text: msg.text.body }),
},
image: {
type: 'photo',
@ -157,6 +171,14 @@ export const whatsappMsgTypeMapped = {
// 'contact-card': 'contact-card',
// 'contact-card-with-photo': 'contact-card-with-photo',
// 'contact-card-with-photo-and-label': 'contact-card-with-photo-and-label',
template: {
type: 'text',
data: (msg) => {
const templateDataMapped = msg.template?.components ? msg.template.components.reduce((r, v) => ({...r, [v.type]: v}), {}) : null;
console.log(msg.template.name, templateDataMapped);
return { id: msg.wamid, text: templateDataMapped?.body?.parameters?.[0]?.text || '', title: msg.template.name }
},
},
};
/**
* render received msg
@ -175,15 +197,29 @@ export const parseRenderMessageItem = (msg) => {
// replyButton: true,
};
};
export const parseRenderMessageList = (messages) => {
/**
* 从数据库读取的记录
*/
export const parseRenderMessageList = (messages, conversationid = null) => {
return messages.map((msg) => {
const msgContent = msg.msgtext_AsJOSN;
const msgType = msgContent.type;
// const parseMethod = msgContent.bizType === 'whatsapp' ? cloneDeep(whatsappMsgTypeMapped) : {};
return {
...(whatsappMsgTypeMapped?.[msg.type]?.data(msg) || {}),
...(whatsappMsgTypeMapped?.[msg.type]?.type(msg) || { type: 'text' }), // type: whatsappMsgTypeMapped?.[msg.type]?.type || 'text',
id: msg.id,
sender: msg.from,
...(whatsappMsgTypeMapped?.[msgType]?.data(msgContent) || {}),
type: msgContent.type,
...(typeof whatsappMsgTypeMapped[msgType].type === 'function' ? whatsappMsgTypeMapped[msgType].type(msg) : { type: whatsappMsgTypeMapped[msgType].type || 'text' }),
date: msgContent?.sendTime || msg.msgtime || '',
sender: msgContent.from,
...(msg.msg_direction === 'outbound'
? {
sender: 'me',
status: msgStatusRenderMapped[msgContent?.status || 'failed'],
dateString: msgStatusRenderMapped[msgContent?.status || 'failed'] === 'failed' ? '发送失败 ❌' : '',
}
: {}),
// conversationid: conversationid,
// title: msg.customerProfile.name,
date: msg.sendTime,
};
});
};
@ -192,6 +228,9 @@ export const parseRenderMessageList = (messages) => {
* WhatsApp Templates params
*/
export const whatsappTemplatesParamMapped = {
/** @deprecated */
'asia_highlights_has_receive_your_inquiry': [['customer_name']],
'hello_from_asia_highlights': [['agent_name']], // todo:
'hello_from_china_highlights': [['agent_name']], // todo:
'use_new_whatsapp': [['agent_name']], // todo:
};

@ -0,0 +1,13 @@
const initialState = {
websocket: null,
errors: [], // 错误信息
conversationsList: [], // 对话列表
templates: [],
customerOrderProfile: {},
activeConversations: {}, // 激活的对话的消息列表: { [conversationId]: [] }
currentConversation: {}, // 当前对话
};
export default initialState;

@ -0,0 +1,118 @@
import initialState from '@/records/ConversationState';
const NAME_SPACE = 'CONVERSATION/';
const ConversationReducer = (state = initialState, action) => {
switch (action.type) {
case NAME_SPACE + 'INIT_WEBSOCKET':
return { ...state, websocket: action.payload };
case NAME_SPACE + 'SET_CONVERSATION_LIST':{
const conversationsMapped = action.payload.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
return { ...state, conversationsList: action.payload, activeConversations: conversationsMapped };
}
case NAME_SPACE + 'ADD_TO_CONVERSATIONS_LIST':{
const { activeConversations } = state;
const conversationsIds = Object.keys(activeConversations);
const newConversations = action.payload.filter((conversation) => !conversationsIds.includes(`${conversation.sn}`));
const newConversationsMapped = newConversations.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
return {
...state,
conversationsList: [...newConversations, ...state.conversationsList],
activeConversations: { ...activeConversations, ...newConversationsMapped },
};
}
case NAME_SPACE + 'SET_TEMPLATE_LIST':
return { ...state, templates: action.payload };
case NAME_SPACE + 'SET_CUSTOMER_ORDER_PROFILE':
return { ...state, customerOrderProfile: action.payload };
case NAME_SPACE + 'SET_CURRENT_CONVERSATION': {
// 清空未读
const { conversationsList } = state;
const targetId = action.payload.sn;
const newConversationList = conversationsList.map((ele) => {
if (String(ele.sn) === String(targetId)) {
return { ...ele, unread_msg_count: 0 };
}
return ele;
});
return { ...state, currentConversation: action.payload, conversationsList: newConversationList };
}
case NAME_SPACE + 'SET_ACTIVE_CONVERSATIONS': {
const { activeConversations } = state;
return { ...state, activeConversations: { ...activeConversations, ...action.payload } };
}
case NAME_SPACE + 'UPDATE_SENT_MESSAGE_ITEM': {
console.log('UPDATE_SENT_MESSAGE_ITEM-----------------------------------------------------------------');
// 更新会话中的消息
const { activeConversations, conversationsList } = state;
const message = action.payload;
const targetId = message.conversationid;
const targetMsgs = (activeConversations[String(targetId)] || []).map((ele) => {
if (ele.id === ele.actionId && ele.actionId === message.actionId) {
return { ...ele, id: message.id, status: message.status, dateString: message.dateString };
} else if (ele.id === message.id) {
return { ...ele, id: message.id, status: message.status, dateString: message.dateString };
}
return ele;
});
// 更新列表的时间
const newConversationList = conversationsList.map((ele) => {
if (String(ele.sn) === String(targetId) && message.type !== 'error') {
return { ...ele, last_received_time: message.date };
}
return ele;
});
return {
...state,
activeConversations: { ...state.activeConversations, [String(targetId)]: targetMsgs },
conversationsList: newConversationList,
};
}
case NAME_SPACE + 'RECEIVED_MESSAGE_LIST': {
const { targetId, data } = action.payload;
return {
...state,
activeConversations: { ...state.activeConversations, [String(targetId)]: data },
};
}
case NAME_SPACE + 'SENT_NEW_MESSAGE':
case NAME_SPACE + 'RECEIVED_NEW_MESSAGE': {
const { activeConversations, conversationsList, currentConversation } = state;
const { targetId, message } = action.payload;
const targetMsgs = activeConversations[String(targetId)] || [];
console.log(activeConversations, String(targetId), 'sent sent sent');
const newConversationList = conversationsList.map((ele) => {
if (String(ele.sn) === String(targetId)) {
return {
...ele,
last_received_time: message.date,
unread_msg_count: String(ele.sn) !== String(currentConversation.sn) ? ele.unread_msg_count + 1 : ele.unread_msg_count,
};
}
return ele;
});
return {
...state,
activeConversations: { ...activeConversations, [String(targetId)]: [...targetMsgs, message] },
conversationsList: newConversationList,
};
}
case NAME_SPACE + 'ADD_ERROR': {
console.log('add error', state.errors, action.payload);
const prelist = state.errors || [];
return { ...state, errors: [...prelist, action.payload] };
}
default:
// throw new Error(`Unknown action: ${action.type}`);
return state;
}
};
export default ConversationReducer;

@ -0,0 +1,12 @@
import {createContext, useContext} from 'react';
export const ConversationContext = createContext();
export const useConversationContext = () => useContext(ConversationContext);
export const ConversationStateContext = createContext();
export const ConversationDispatchContext = createContext();
export const useConversationState = () => useContext(ConversationStateContext);
export const useConversationDispatch = () => useContext(ConversationDispatchContext);

@ -20,13 +20,15 @@ export function fetchText(url) {
})
}
export function fetchJSON(url) {
return fetch(url)
export function fetchJSON(url, data) {
const params = data ? new URLSearchParams(data).toString() : '';
const ifp = url.includes('?') ? '&' : '?';
return fetch(`${url}${ifp}${params}`)
.then(checkStatus)
.then(response => response.json())
.catch(error => {
throw error
})
throw error;
});
}
export function postForm(url, data) {

@ -1,40 +1,29 @@
import { useEffect, useState } from 'react';
import { useParams, useNavigate } from "react-router-dom";
import { Layout, List, Avatar, Flex, Typography, Spin } from 'antd';
import { useEffect } from 'react';
import { Layout, Spin } from 'antd';
import MessagesHeader from './Components/MessagesHeader';
import Messages from './Components/Messages';
import InputBox from './Components/InputBox';
import ConversationsList from './Components/ConversationsList';
import CustomerProfile from './Components/CustomerProfile';
import LocalTimeClock from './Components/LocalTimeClock';
import { useConversationContext } from '@/stores/Conversations/ConversationContext';
import './Conversations.css';
import { useAuthContext } from '@/stores/AuthContext.js';
const { Sider, Content, Header, Footer } = Layout;
/**
*
*/
const ChatWindow = (() => {
const { order_sn } = useParams();
const { loginUser: currentUser } = useAuthContext();
const { errors, sendMessage, currentConversation, customerOrderProfile: orderInfo, getCustomerProfile } = useConversationContext();
const { quotes, contact, last_contact, ...order } = orderInfo;
const ChatWindow = () => {
console.log('chat window;;;;;;;;;;;;;;;;;;;;;;;;');
// console.log(order_sn, currentUser);
useEffect(() => {
if (order_sn) {
getCustomerProfile(order_sn);
}
console.log('chat window 222;;;;;;;;;;;;;;;;;;;;;;;;');
return () => {};
}, [order_sn]);
}, []);
return (
<Spin spinning={false} tip={'正在连接...'} >
<Spin spinning={false} tip={'正在连接...'}>
<Layout className='h-screen chatwindow-wrapper' style={{ maxHeight: 'calc(100% - 198px)', height: 'calc(100% - 198px)' }}>
<Sider width={240} theme={'light'} className='h-full overflow-y-auto' style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}>
<ConversationsList />
@ -43,21 +32,7 @@ const ChatWindow = (() => {
<Content style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}>
<Layout className='h-full'>
<Header className='ant-layout-sider-light ant-card p-1 h-auto'>
<Flex gap={16}>
{currentConversation.customer_name && <Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${currentConversation.customer_name}`} />}
<Flex flex={'1'} justify='space-between'>
<Flex vertical={true} justify='space-between'>
<Typography.Text strong>{currentConversation.customer_name}</Typography.Text>
<Typography.Text>{currentConversation.whatsapp_phone_number}</Typography.Text>
</Flex>
<Flex vertical={true} justify='space-between'>
<Typography.Text>
{/* <LocalTimeClock /> <Typography.Text strong>{order?.location} </Typography.Text> */}
</Typography.Text>
{/* <Typography.Text>{customerDateTime}</Typography.Text> */}
</Flex>
</Flex>
</Flex>
<MessagesHeader />
</Header>
<Content style={{ maxHeight: '74vh', height: '74vh' }}>
<div className='h-full overflow-y-auto'>
@ -65,7 +40,7 @@ const ChatWindow = (() => {
</div>
</Content>
<Footer className='ant-layout-sider-light p-1'>
<InputBox onSend={(v) => sendMessage(v)} />
<InputBox />
</Footer>
</Layout>
</Content>
@ -76,6 +51,6 @@ const ChatWindow = (() => {
</Layout>
</Spin>
);
});
};
export default ChatWindow;

@ -1,20 +1,33 @@
import { useRef, useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom";
import { useParams, useNavigate } from 'react-router-dom';
import { List, Avatar, Flex } from 'antd';
import { useConversationContext } from '@/stores/Conversations/ConversationContext';
import { ChatItem, ChatList } from 'react-chat-elements';
import { useGetJson } from '@/hooks/userFetch';
import { useAuthContext } from '@/stores/AuthContext';
import { useConversationState, useConversationDispatch } from '@/stores/ConversationContext';
import {
fetchCustomerProfile,
receivedCustomerProfile,
setCurrentConversation,
addConversationList,
postConversationItemClose,
fetchMessages, receivedMessageList,
} from '@/actions/ConversationActions';
import { ChatList } from 'react-chat-elements';
import { isEmpty, pick } from '@/utils/utils';
import { v4 as uuid } from 'uuid';
/**
* []
*/
const Conversations = (() => {
const Conversations = () => {
const { order_sn } = useParams();
const navigate = useNavigate();
const { switchConversation, conversationsList, poseConversationItemClose } = useConversationContext();
// console.log(conversationsList);
const { loginUser } = useAuthContext();
const { userId } = loginUser;
const { conversationsList, activeConversations, currentConversation } = useConversationState();
const dispatch = useConversationDispatch();
const [chatlist, setChatlist] = useState([]);
useEffect(() => {
setChatlist(
(conversationsList || []).map((item) => ({
conversationsList.map((item) => ({
...item,
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${item.whatsapp_name.trim() || item.whatsapp_phone_number}`,
id: item.sn,
@ -22,7 +35,7 @@ const Conversations = (() => {
title: item.whatsapp_name.trim() || item.whatsapp_phone_number,
// subtitle: item.whatsapp_phone_number,
// subtitle: item.lastMessage,
date: item.last_received_time, // last_send_time
date: item.last_received_time, // last_send_time // todo: GMT+8
unread: item.unread_msg_count,
showMute: true,
muted: false,
@ -36,23 +49,86 @@ const Conversations = (() => {
return () => {};
}, [conversationsList]);
useEffect(() => {
if (order_sn) {
getCustomerProfile(order_sn);
}
return () => {};
}, [order_sn]);
const getCustomerProfile = async (colisn) => {
const data = await fetchCustomerProfile(colisn);
dispatch(receivedCustomerProfile(data));
if (isEmpty(data)) {
return false;
}
if (!isEmpty(data.conversations)) {
dispatch(addConversationList(data.conversations));
dispatch(setCurrentConversation(data.conversations[0]));
} else {
// reset chat window
if (isEmpty(data.contact?.[0]?.whatsapp_phone_number)) {
dispatch(setCurrentConversation({ sn: '', customer_name: '', coli_sn: '' }));
return false;
}
//
const newChat = {
'sn': uuid(),
// 'opi_sn': 354,
'coli_sn': colisn,
'whatsapp_phone_number': data.contact[0].whatsapp_phone_number,
'last_received_time': '',
'last_send_time': '',
'unread_msg_count': 0,
'whatsapp_name': data.contact[0].name,
'customer_name': data.contact[0].name,
};
dispatch(addConversationList([newChat]));
const newCurrent = pick(newChat, ['sn', 'coli_sn', 'whatsapp_phone_number', 'whatsapp_name', 'customer_name']);
dispatch(setCurrentConversation(newCurrent));
}
};
const switchConversation = async (item) => {
if (currentConversation.sn === item.sn) {
return false;
}
dispatch(setCurrentConversation(item));
if (isEmpty(item.coli_sn) || item.coli_sn === '0') {
dispatch(receivedCustomerProfile({}));
}
const messagesList = activeConversations[`${item.sn}`] || [];
if (isEmpty(messagesList)) {
const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number });
dispatch(receivedMessageList(item.sn, data));
}
};
const onSwitchConversation = (item) => {
switchConversation(item);
if (item.coli_sn) {
navigate(`/order/chat/${item.coli_sn}`, { replace: true });
}
}
switchConversation(item);
};
const handleConversationItemClose = async (item) => {
console.log('invoke close', item);
const data = await postConversationItemClose({ conversationid: item.sn, opisn: userId });
};
return (
<>
<ChatList className='' dataSource={chatlist} onClick={(item) => onSwitchConversation(item)}
<ChatList
className=''
dataSource={chatlist}
onClick={(item) => onSwitchConversation(item)}
// onContextMenu={(item, i, e) => {
// console.log(item, i, e);
// return ( <p>ppp</p> )
// }}
onClickMute={poseConversationItemClose}
onClickMute={handleConversationItemClose}
/>
</>
);
});
};
export default Conversations;

@ -1,6 +1,6 @@
import { Card, Flex, Avatar, Typography, Radio, Button, Table } from 'antd';
import { useAuthContext } from '@/stores/AuthContext.js';
import { useConversationContext } from '@/stores/Conversations/ConversationContext';
import { useConversationState } from '@/stores/ConversationContext';
import { HomeOutlined, LoadingOutlined, SettingFilled, SmileOutlined, SyncOutlined, PhoneOutlined, MailOutlined, WhatsAppOutlined, SmileTwoTone } from '@ant-design/icons';
import CreatePayment from './CreatePayment';
@ -21,8 +21,8 @@ const orderStatus = [
const { Meta } = Card;
const CustomerProfile = (({ customer }) => {
const { errors, customerOrderProfile: orderInfo } = useConversationContext();
const CustomerProfile = (() => {
const { customerOrderProfile: orderInfo } = useConversationState();
const { loginUser: currentUser } = useAuthContext();
const { quotes, contact, last_contact, ...order } = orderInfo;
@ -59,7 +59,8 @@ const CustomerProfile = (({ customer }) => {
<QuotesHistory />
</Flex>
</Flex>
<pre className='p-2 overflow-auto max-h-72 m-0' dangerouslySetInnerHTML={{__html: order?.order_detail}}></pre>
{/* <pre className='p-2 overflow-auto max-h-72 m-0' dangerouslySetInnerHTML={{__html: order?.order_detail}}></pre> */}
<p className='p-2 overflow-auto max-h-72 m-0 break-words whitespace-pre-wrap ' dangerouslySetInnerHTML={{__html: order?.order_detail}}></p>
{/* <Flex vertical={true} className='p-2 '>
<Typography.Text strong>沟通记录</Typography.Text>
<Table size={'small'} columns={[{ title: '进程', dataIndex: 'title' }, { title: '状态', dataIndex: 'title2' },]} />

@ -1,42 +1,56 @@
import React, { useEffect, useState } from 'react';
import { Input, Button, Tabs, List, Space, Popover, Flex } from 'antd';
// import { Input } from 'react-chat-elements';
import { useConversationContext } from '@/stores/Conversations/ConversationContext';
import { useConversationState, useConversationDispatch } from '@/stores/ConversationContext';
import { sentNewMessage } from '@/actions/ConversationActions';
import { useAuthContext } from '@/stores/AuthContext';
import { LikeOutlined, MessageOutlined, StarOutlined, SendOutlined, PlusOutlined, PlusCircleOutlined } from '@ant-design/icons';
import { cloneDeep, getNestedValue, isEmpty } from '@/utils/utils';
import { v4 as uuid } from 'uuid';
import { whatsappTemplatesParamMapped } from '@/lib/msgUtils';
import { whatsappTemplatesParamMapped, sentMsgTypeMapped, replaceTemplateString } from '@/lib/msgUtils';
const InputBox = ({ onSend }) => {
const { currentConversation, templates } = useConversationContext();
const InputBox = () => {
const { loginUser } = useAuthContext();
const { userId } = loginUser;
const { websocket, currentConversation, templates } = useConversationState();
const dispatch = useConversationDispatch();
const [textContent, setTextContent] = useState('');
const talkabled = !isEmpty(currentConversation.sn);
const invokeSendMessage = (msgObj) => {
console.log('sendMessage------------------', msgObj);
const contentToSend = sentMsgTypeMapped[msgObj.type].contentToSend(msgObj);
console.log('content to send-------------------------------------', contentToSend);
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn });
const contentToRender = sentMsgTypeMapped[msgObj.type].contentToRender(msgObj);
console.log(contentToRender, 'contentToRender sendMessage------------------');
dispatch(sentNewMessage(contentToRender));
};
const handleSendText = () => {
if (typeof onSend === 'function' && textContent.trim() !== '') {
if (textContent.trim() !== '') {
const msgObj = {
type: 'text',
text: textContent,
sender: 'me',
to: currentConversation.whatsapp_phone_number,
id: `${currentConversation.id}.${uuid()}`, // Date.now().toString(16),
id: `${currentConversation.sn}.${uuid()}`, // Date.now().toString(16),
date: new Date(),
status: 'waiting',
};
onSend(msgObj);
invokeSendMessage(msgObj);
setTextContent('');
}
};
const handleSendTemplate = (fromTemplate) => {
console.log(fromTemplate, 'fromTemplate');
if (typeof onSend === 'function') {
const _conversation = { ...cloneDeep(currentConversation) };
const msgObj = {
type: 'whatsappTemplate',
to: currentConversation.whatsapp_phone_number,
id: `${currentConversation.id}.${uuid()}`,
id: `${currentConversation.sn}.${uuid()}`,
date: new Date(),
status: 'waiting',
statusTitle: 'Ready to send',
@ -44,32 +58,43 @@ const InputBox = ({ onSend }) => {
template: {
name: fromTemplate.name,
language: { code: fromTemplate.language },
...(fromTemplate.components.body[0]?.example?.body_text?.[0]?.length > 0
? {
components: [
{
'type': 'body',
'parameters': whatsappTemplatesParamMapped[fromTemplate.name].map((v) => ({ type: 'text', text: getNestedValue(_conversation, v) || '' })),
// [
// {
// 'type': 'text',
// 'text': getNestedValue(_conversation, whatsappTemplatesParamMapped[fromTemplate.name][0]) ,
// },
// { // debug:
// 'type': 'text',
// 'text': getNestedValue(_conversation, whatsappTemplatesParamMapped[fromTemplate.name]?.[1] || whatsappTemplatesParamMapped[fromTemplate.name][0]) ,
// },
// ],
},
],
}
: {}),
components: fromTemplate.components_origin.map(citem => {
const params = whatsappTemplatesParamMapped[fromTemplate.name].map((v) => ({ type: 'text', text: getNestedValue(_conversation, v) || '' }));
const paramText = params.map((p) => p.text);
const fillTemplate = paramText.length ? replaceTemplateString(citem?.text || '', paramText) : citem?.text || '';
return {
type: citem.type.toLowerCase(),
parameters: params,
text: fillTemplate,
};
}),
// ...(fromTemplate.components.body[0]?.example?.body_text?.[0]?.length > 0
// ? {
// components: [
// {
// 'type': 'body',
// 'text': fromTemplate.components.body[0]?.text,
// 'parameters': whatsappTemplatesParamMapped[fromTemplate.name].map((v) => ({ type: 'text', text: getNestedValue(_conversation, v) || '' })),
// // [
// // {
// // 'type': 'text',
// // 'text': getNestedValue(_conversation, whatsappTemplatesParamMapped[fromTemplate.name][0]) ,
// // },
// // { // debug:
// // 'type': 'text',
// // 'text': getNestedValue(_conversation, whatsappTemplatesParamMapped[fromTemplate.name]?.[1] || whatsappTemplatesParamMapped[fromTemplate.name][0]) ,
// // },
// // ],
// },
// ],
// }
// : {}),
},
template_origin: fromTemplate,
};
onSend(msgObj);
invokeSendMessage(msgObj);
setOpenTemplates(false);
}
};
const [openTemplates, setOpenTemplates] = useState(false);

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { Typography } from 'antd';
import { useConversationContext } from '@/stores/Conversations/ConversationContext';
import { useConversationState } from '@/stores/ConversationContext';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
@ -9,7 +9,7 @@ dayjs.extend(utc);
dayjs.extend(timezone);
const LocalTimeClock = ((props) => {
const { customerOrderProfile: orderInfo } = useConversationContext();
const { customerOrderProfile: orderInfo } = useConversationState();
const [customerDateTime, setCustomerDateTime] = useState();

@ -1,12 +1,12 @@
import { useEffect, useState, useRef } from 'react';
import { List, Avatar, Timeline, Image } from 'antd';
import { MessageBox } from 'react-chat-elements';
import { useConversationContext } from '@/stores/Conversations/ConversationContext';
import { useConversationState } from '@/stores/ConversationContext';
const Messages = (() => {
const { messages: messagesList } = useConversationContext();
// const messagesList = parseMessage(messages);
// console.log(messagesList);
const { activeConversations, currentConversation } = useConversationState();
const messagesList = activeConversations[currentConversation.sn] || [];
console.log('messagesList----------------------------------------------------', messagesList);
const messagesEndRef = useRef(null);
useEffect(() => {

@ -0,0 +1,27 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useConversationState } from '@/stores/ConversationContext';
import { Flex, Typography, Avatar } from 'antd';
import LocalTimeClock from './LocalTimeClock';
const MessagesHeader = () => {
const { currentConversation } = useConversationState();
return (
<>
<Flex gap={16}>
{currentConversation.customer_name && <Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${currentConversation.customer_name}`} />}
<Flex flex={'1'} justify='space-between'>
<Flex vertical={true} justify='space-between'>
<Typography.Text strong>{currentConversation.customer_name}</Typography.Text>
<Typography.Text>{currentConversation.whatsapp_phone_number}</Typography.Text>
</Flex>
<Flex vertical={true} justify='space-between'>
<Typography.Text>{/* <LocalTimeClock /> <Typography.Text strong>{order?.location} </Typography.Text> */}</Typography.Text>
{/* <Typography.Text>{customerDateTime}</Typography.Text> */}
</Flex>
</Flex>
</Flex>
</>
);
};
export default MessagesHeader;

@ -1,9 +1,9 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { Popover, Flex, Button, List, Popconfirm } from 'antd';
import { useConversationContext } from '@/stores/Conversations/ConversationContext';
import { useConversationState } from '@/stores/ConversationContext';
const QuotesHistory = ((props) => {
const { customerOrderProfile: orderInfo } = useConversationContext();
const { customerOrderProfile: orderInfo } = useConversationState();
const { quotes, ...order } = orderInfo;
const [open, setOpen] = useState(false);

@ -1,30 +1,94 @@
import { useContext } from 'react';
import { ConversationContext, useConversations, } from '@/stores/Conversations/ConversationContext';
import { useContext, useReducer, useEffect } from 'react';
import { ConversationStateContext, ConversationDispatchContext } from '@/stores/ConversationContext';
import ConversationReducer from '@/reducers/ConversationReducer';
import {
initWebsocket,
addError,
fetchConversationsList,
fetchTemplates,
receivedConversationList,
receivedTemplates,
updateMessageItem,
receivedNewMessage,
} from '@/actions/ConversationActions';
import initialState from '@/records/ConversationState';
import { AuthContext } from '@/stores/AuthContext';
import { RealTimeAPI } from '@/lib/realTimeAPI';
import { receivedMsgTypeMapped } from '@/lib/msgUtils';
import { isEmpty } from '@/utils/utils';
// const WS_URL = 'ws://202.103.68.144:8888/whatever/';
// const WS_URL = 'ws://120.79.9.217:10022/whatever/';
const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_callback'; // prod:
export const ConversationProvider = ({ children, loginUser, realtimeAPI }) => {
const ConversationProvider = ({ children }) => {
const { loginUser } = useContext(AuthContext);
const { userId } = loginUser;
console.log('====', loginUser);
const conversations = useConversations({loginUser, realtimeAPI});
return <ConversationContext.Provider value={conversations}>{children}</ConversationContext.Provider>;
};
const realtimeAPI = new RealTimeAPI({ url: `${WS_URL}?opisn=${userId || ''}&_spam=${Date.now().toString()}`, protocol: 'WhatsApp' });
const [state, dispatch] = useReducer(ConversationReducer, { ...initialState, websocket: null });
// export default ConversationProvider;
console.log('ConversationProvider', state, dispatch);
const AuthAndConversationProvider = ({ children }) => {
const { loginUser } = useContext(AuthContext);
const {userId} = loginUser;
const realtimeAPI = new RealTimeAPI({ url: `${WS_URL}?opisn=${userId || ''}&_spam=${Date.now().toString()}`, protocol: 'WhatsApp' });
useEffect(() => {
console.log('invoke provider');
realtimeAPI.onError(() => dispatch(addError('Error')));
realtimeAPI.onMessage(handleMessage);
realtimeAPI.onCompletion(() => dispatch(addError('Connection broken')));
dispatch(initWebsocket(realtimeAPI));
return () => {
realtimeAPI.disconnect();
};
}, []);
useEffect(() => {
fetchConversationsList(userId).then((data) => {
console.log(data, 'llllllllllllllllllllllll');
dispatch(receivedConversationList(data));
}); // todo:
fetchTemplates().then((data) => dispatch(receivedTemplates(data)));
return () => {};
}, []);
const handleMessage = (data) => {
console.log('handleMessage------------------');
console.log(data);
const { errcode, errmsg, result } = data;
if (!result) {
return false;
}
let resultType = result?.action || result.type;
if (errcode !== 0) {
// addError('Error Connecting to Server');
resultType = 'error';
}
console.log(resultType, 'result.type');
const msgObj = receivedMsgTypeMapped[resultType].getMsg(result);
const msgRender = receivedMsgTypeMapped[resultType].contentToRender(msgObj);
const msgUpdate = receivedMsgTypeMapped[resultType].contentToUpdate(msgObj);
console.log('msgRender msgUpdate', msgRender, msgUpdate);
if (['whatsapp.message.updated', 'message', 'error'].includes(resultType)) {
dispatch(updateMessageItem(msgUpdate));
// return false;
}
if (!isEmpty(msgRender)) {
dispatch(receivedNewMessage(msgRender.conversationid, msgRender));
}
console.log('handleMessage*******************');
};
// return <ConversationContext.Provider value={{ ...state, dispatch }}>{children}</ConversationContext.Provider>;
return (
<ConversationProvider loginUser={loginUser} realtimeAPI={realtimeAPI} >
{children}
</ConversationProvider>
<ConversationStateContext.Provider value={{ ...state }}>
<ConversationDispatchContext.Provider value={dispatch}>{children}</ConversationDispatchContext.Provider>
</ConversationStateContext.Provider>
);
};
export default AuthAndConversationProvider;
export default ConversationProvider;

Loading…
Cancel
Save