Merge branch 'main' of github.com:hainatravel/global-sales
commit
1fd2a13a40
@ -0,0 +1,104 @@
|
||||
import { create } from "zustand";
|
||||
import { VonageClient } from "@vonage/client-sdk";
|
||||
import { fetchJSON } from "@/utils/request";
|
||||
import { prepareUrl, isNotEmpty } from "@/utils/commons";
|
||||
import { VONAGE_URL, DATETIME_FORMAT } from "@/config";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const callCenterStore = create((set, get) => ({
|
||||
client: new VonageClient({ apiUrl: "https://api-ap-3.vonage.com", websocketUrl: "wss://ws-ap-3.vonage.com" }),
|
||||
call_id: 0,
|
||||
loading: false,
|
||||
logs: "",
|
||||
|
||||
//初始化 Vonage
|
||||
init_vonage: user_id => {
|
||||
const { client, log } = get();
|
||||
set({ loading: true });
|
||||
const fetchUrl = prepareUrl(VONAGE_URL + "/jwt")
|
||||
.append("user_id", user_id)
|
||||
.build();
|
||||
return fetchJSON(fetchUrl).then(json => {
|
||||
if (json.status === 200) {
|
||||
let jwt = json.token;
|
||||
|
||||
client
|
||||
.createSession(jwt)
|
||||
.then(sessionId => {
|
||||
log("Id of created session: ", sessionId);
|
||||
})
|
||||
.catch(error => {
|
||||
log("Error creating session: ", error);
|
||||
});
|
||||
|
||||
client.on("sessionError", reason => {
|
||||
// After creating a session
|
||||
log("Session error reason: ", reason);
|
||||
});
|
||||
|
||||
client.on("legStatusUpdate", (callId, legId, status) => {
|
||||
// After creating a session
|
||||
log({ callId, legId, status });
|
||||
});
|
||||
|
||||
client.on("callInvite", (callId, from, channelType) => {
|
||||
log({ callId, from, channelType }); // Answer / Reject Call
|
||||
});
|
||||
|
||||
client.on("callHangup", (callId, callQuality, reason) => {
|
||||
log(`Call ${callId} has hung up, callQuality:${callQuality}, reason:${reason}`);
|
||||
set({ call_id: 0 });
|
||||
});
|
||||
|
||||
client.on("sessionError", error => {
|
||||
log({ error });
|
||||
});
|
||||
} else {
|
||||
throw new Error("请求jwt失败");
|
||||
}
|
||||
set({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
log: (...message) => {
|
||||
const { logs } = get();
|
||||
console.log(message);
|
||||
set({ logs: [...logs, dayjs().format(DATETIME_FORMAT) + " : " + JSON.stringify(message)] });
|
||||
},
|
||||
|
||||
// 创建一个语音通话
|
||||
make_call: phone_number => {
|
||||
const { client, log } = get();
|
||||
if (!isNotEmpty(phone_number)) {
|
||||
log("请输入电话号码");
|
||||
return;
|
||||
}
|
||||
log("开始拨号:" + phone_number);
|
||||
if (client) {
|
||||
set({ loading: true });
|
||||
client
|
||||
.serverCall({ to: phone_number })
|
||||
.then(callId => {
|
||||
log("Id of created call: ", callId);
|
||||
set({ call_id: callId });
|
||||
set({ loading: false });
|
||||
})
|
||||
.catch(error => {
|
||||
log("Error making call: ", error);
|
||||
set({ loading: false });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 挂断语音通话
|
||||
hang_up: () => {
|
||||
const { client, call_id, log } = get();
|
||||
log("挂断电话");
|
||||
if (call_id) {
|
||||
client.hangup(call_id);
|
||||
set({ call_id: 0 });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default callCenterStore;
|
@ -0,0 +1,61 @@
|
||||
import { useCallback, useState, useEffect } from "react";
|
||||
import { Grid, Divider, Layout, Flex, Spin, Input, Col, Row, List, Typography } from "antd";
|
||||
import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined } from "@ant-design/icons";
|
||||
import { useParams, useHref, useNavigate } from "react-router-dom";
|
||||
import { isEmpty } from "@/utils/commons";
|
||||
|
||||
import callCenterStore from "@/stores/CallCenterStore";
|
||||
import useAuthStore from "@/stores/AuthStore";
|
||||
|
||||
const CallCenter = props => {
|
||||
const href = useHref();
|
||||
const navigate = useNavigate();
|
||||
const { phonenumber } = useParams();
|
||||
const [init_vonage, make_call, hang_up, logs, call_id, loading] = callCenterStore(state => [state.init_vonage, state.make_call, state.hang_up, state.logs, state.call_id, state.loading]);
|
||||
const [loginUser] = useAuthStore(state => [state.loginUser]);
|
||||
const [phone_number, setPhone_number] = useState(phonenumber);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginUser.userId === -1 && href.indexOf("/p/") === -1) {
|
||||
navigate("/p/dingding/login?origin_url=" + href);
|
||||
} else {
|
||||
init_vonage(loginUser.userId);
|
||||
}
|
||||
}, [href, navigate, init_vonage, loginUser]);
|
||||
|
||||
const oncall = () => {
|
||||
if (isEmpty(call_id)) {
|
||||
make_call(phone_number);
|
||||
} else {
|
||||
hang_up();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<Row gutter={16}>
|
||||
<Col md={24} lg={8} xxl={9}></Col>
|
||||
<Col md={24} lg={8} xxl={6}>
|
||||
<Input.Search
|
||||
type="tel"
|
||||
size="large"
|
||||
defaultValue={phone_number}
|
||||
placeholder="电话号码"
|
||||
prefix={<AudioOutlined />}
|
||||
suffix={loading ? <Spin /> : ""}
|
||||
enterButton={call_id ? "挂断" : "拨号"}
|
||||
onSearch={oncall}
|
||||
onChange={e => {
|
||||
setPhone_number(e.target.value);
|
||||
}}></Input.Search>
|
||||
</Col>
|
||||
<Col md={24} lg={8} xxl={9}></Col>
|
||||
</Row>
|
||||
|
||||
<Divider plain orientation="left" className="mb-0"></Divider>
|
||||
<List header={<Typography.Text strong>Console Logs</Typography.Text>} bordered dataSource={logs} renderItem={item => <List.Item>{item}</List.Item>} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CallCenter;
|
@ -1,190 +1,231 @@
|
||||
import { LinkOutlined, MailOutlined, PhoneOutlined, UserOutlined, WhatsAppOutlined } from '@ant-design/icons'
|
||||
import { App, Button, Card, Empty, Flex, Select, Spin, Typography, Divider, Modal } from 'antd'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { LinkOutlined, MailOutlined, PhoneOutlined, UserOutlined, WhatsAppOutlined } from "@ant-design/icons";
|
||||
import { App, Button, Card, Empty, Flex, Select, Spin, Typography, Divider, Modal } from "antd";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { copy, isEmpty } from '@/utils/commons'
|
||||
import { Conditional } from '@/components/Conditional'
|
||||
import useConversationStore from '@/stores/ConversationStore'
|
||||
import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions } from '@/stores/OrderStore'
|
||||
import useAuthStore from '@/stores/AuthStore'
|
||||
import QuotesHistory from './QuotesHistory'
|
||||
import ConversationBind from './../ConversationBind';
|
||||
import ConversationsNewItem from './../ConversationsNewItem';
|
||||
import { useConversationNewItem } from '@/hooks/useConversation';
|
||||
import { copy, isEmpty } from "@/utils/commons";
|
||||
import { Conditional } from "@/components/Conditional";
|
||||
import useConversationStore from "@/stores/ConversationStore";
|
||||
import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions } from "@/stores/OrderStore";
|
||||
import useAuthStore from "@/stores/AuthStore";
|
||||
import QuotesHistory from "./QuotesHistory";
|
||||
import ConversationBind from "./../ConversationBind";
|
||||
import ConversationsNewItem from "./../ConversationsNewItem";
|
||||
import { useConversationNewItem } from "@/hooks/useConversation";
|
||||
|
||||
const CustomerProfile = (() => {
|
||||
const { notification, message } = App.useApp()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const orderCommentRef = useRef(null)
|
||||
const currentOrder = useConversationStore((state) => state.currentConversation?.coli_sn || '')
|
||||
const currentConversationID = useConversationStore((state) => state.currentConversation?.sn || '')
|
||||
const [updateCurrentConversation] = useConversationStore(((state) => [state.updateCurrentConversation]));
|
||||
const loginUser = useAuthStore((state) => state.loginUser)
|
||||
const { orderDetail, customerDetail, lastQuotation, quotationList,
|
||||
fetchOrderDetail, setOrderPropValue, appendOrderComment
|
||||
} = useOrderStore()
|
||||
const CustomerProfile = () => {
|
||||
const { notification, message } = App.useApp();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const orderCommentRef = useRef(null);
|
||||
const currentOrder = useConversationStore(state => state.currentConversation?.coli_sn || "");
|
||||
const currentConversationID = useConversationStore(state => state.currentConversation?.sn || "");
|
||||
const [updateCurrentConversation] = useConversationStore(state => [state.updateCurrentConversation]);
|
||||
const loginUser = useAuthStore(state => state.loginUser);
|
||||
const { orderDetail, customerDetail, lastQuotation, quotationList, fetchOrderDetail, setOrderPropValue, appendOrderComment } = useOrderStore();
|
||||
|
||||
const orderLabelOptions = copy(OrderLabelDefaultOptions)
|
||||
orderLabelOptions.unshift({ value: 0, label: '未设置', disabled: true, })
|
||||
const navigate = useNavigate();
|
||||
const orderLabelOptions = copy(OrderLabelDefaultOptions);
|
||||
orderLabelOptions.unshift({ value: 0, label: "未设置", disabled: true });
|
||||
|
||||
const orderStatusOptions = copy(OrderStatusDefaultOptions)
|
||||
const orderStatusOptions = copy(OrderStatusDefaultOptions);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentOrder) {
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
fetchOrderDetail(currentOrder)
|
||||
.finally(() => setLoading(false))
|
||||
.catch(reason => {
|
||||
notification.error({
|
||||
message: '查询出错',
|
||||
message: "查询出错",
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
placement: "top",
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [currentOrder])
|
||||
}, [currentOrder]);
|
||||
|
||||
let regularText = ''
|
||||
if (orderDetail.buytime > 0) regularText = '(R' + orderDetail.buytime + ')'
|
||||
let regularText = "";
|
||||
if (orderDetail.buytime > 0) regularText = "(R" + orderDetail.buytime + ")";
|
||||
|
||||
const { openOrderContactConversation } = useConversationNewItem();
|
||||
const [newChatModalVisible, setNewChatModalVisible] = useState(false);
|
||||
const [newChatFormValues, setNewChatFormValues] = useState({});
|
||||
const handleNewChat = async (values) => {
|
||||
const handleNewChat = async values => {
|
||||
const newContact = { wa_id: values.wa_id };
|
||||
openOrderContactConversation(newContact.wa_id);
|
||||
setNewChatModalVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (currentOrder) {
|
||||
return (
|
||||
<div className='divide-x-0 divide-y divide-dashed divide-gray-300'>
|
||||
<div className="divide-x-0 divide-y divide-dashed divide-gray-300">
|
||||
<Spin spinning={loading}>
|
||||
<Card className='p-2 '
|
||||
<Card
|
||||
className="p-2 "
|
||||
bordered={false}
|
||||
title={orderDetail.order_no}
|
||||
actions={[
|
||||
<Select key={'orderlabel'} size='small'
|
||||
<Select
|
||||
key={"orderlabel"}
|
||||
size="small"
|
||||
style={{
|
||||
width: '100%'
|
||||
width: "100%",
|
||||
}}
|
||||
variant='borderless'
|
||||
onSelect={(value) => {
|
||||
setOrderPropValue(currentOrder, 'orderlabel', value)
|
||||
variant="borderless"
|
||||
onSelect={value => {
|
||||
setOrderPropValue(currentOrder, "orderlabel", value)
|
||||
.then(() => {
|
||||
message.success('设置成功')
|
||||
message.success("设置成功");
|
||||
})
|
||||
.catch(reason => {
|
||||
notification.error({
|
||||
message: '设置出错',
|
||||
message: "设置出错",
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
placement: "top",
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}}
|
||||
value={orderDetail.tags}
|
||||
options={orderLabelOptions}
|
||||
/>,
|
||||
<Select key={'orderstatus'} size='small'
|
||||
<Select
|
||||
key={"orderstatus"}
|
||||
size="small"
|
||||
style={{
|
||||
width: '100%'
|
||||
width: "100%",
|
||||
}}
|
||||
variant='borderless'
|
||||
onSelect={(value) => {
|
||||
setOrderPropValue(currentOrder,'orderstatus', value)
|
||||
variant="borderless"
|
||||
onSelect={value => {
|
||||
setOrderPropValue(currentOrder, "orderstatus", value)
|
||||
.then(() => {
|
||||
message.success('设置成功')
|
||||
message.success("设置成功");
|
||||
})
|
||||
.catch(reason => {
|
||||
notification.error({
|
||||
message: '设置出错',
|
||||
message: "设置出错",
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
placement: "top",
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}}
|
||||
value={orderDetail.states}
|
||||
options={orderStatusOptions}
|
||||
/>
|
||||
]}
|
||||
>
|
||||
/>,
|
||||
]}>
|
||||
<Flex gap={10}>
|
||||
<Flex vertical={true} justify='space-between'>
|
||||
<Typography.Text ><UserOutlined className=' pr-1' />{customerDetail.name + regularText}</Typography.Text>
|
||||
<Typography.Text ><PhoneOutlined className=' pr-1' />{customerDetail.phone}</Typography.Text>
|
||||
<Typography.Text ><MailOutlined className=' pr-1' />{customerDetail.email}</Typography.Text>
|
||||
<Typography.Text >
|
||||
<WhatsAppOutlined className='pr-1' />
|
||||
<Button type='link' size={'small'} onClick={() => {
|
||||
<Flex vertical={true} justify="space-between">
|
||||
<Typography.Text>
|
||||
<UserOutlined className=" pr-1" />
|
||||
{customerDetail.name + regularText}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<PhoneOutlined className=" pr-1" />
|
||||
<Button
|
||||
type="link"
|
||||
size={"small"}
|
||||
onClick={() => {
|
||||
navigate(`/callcenter/call/` + customerDetail.phone);
|
||||
}}>
|
||||
{customerDetail.phone}
|
||||
</Button>
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<MailOutlined className=" pr-1" />
|
||||
{customerDetail.email}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<WhatsAppOutlined className="pr-1" />
|
||||
<Button
|
||||
type="link"
|
||||
size={"small"}
|
||||
onClick={() => {
|
||||
setNewChatModalVisible(true);
|
||||
setNewChatFormValues(prev => ({...prev, phone_number: customerDetail.whatsapp_phone_number, is_current_order: true, }))
|
||||
}} >{customerDetail.whatsapp_phone_number}</Button>
|
||||
setNewChatFormValues(prev => ({ ...prev, phone_number: customerDetail.whatsapp_phone_number, is_current_order: true }));
|
||||
}}>
|
||||
{customerDetail.whatsapp_phone_number}
|
||||
</Button>
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Divider orientation='left'><Typography.Text strong>最新报价</Typography.Text></Divider>
|
||||
<Flex vertical={true} className='p-2 '>
|
||||
<Divider orientation="left">
|
||||
<Typography.Text strong>最新报价</Typography.Text>
|
||||
</Divider>
|
||||
<Flex vertical={true} className="p-2 ">
|
||||
<Conditional
|
||||
condition={quotationList.length > 0}
|
||||
whenFalse={<Empty description={<span>暂无报价</span>}></Empty>}
|
||||
whenTrue={
|
||||
<>
|
||||
<p className='m-0 py-2 line-clamp-2 '><a target='_blank' href={lastQuotation.letterurl}><LinkOutlined /> {lastQuotation.lettertitle}</a></p>
|
||||
<Flex justify={'space-between'} >
|
||||
<p className="m-0 py-2 line-clamp-2 ">
|
||||
<a target="_blank" href={lastQuotation.letterurl}>
|
||||
<LinkOutlined />
|
||||
{lastQuotation.lettertitle}
|
||||
</a>
|
||||
</p>
|
||||
<Flex justify={"space-between"}>
|
||||
<QuotesHistory dataSource={quotationList} />
|
||||
</Flex>
|
||||
</>
|
||||
}/>
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Divider orientation='left'><Typography.Text strong>表单信息</Typography.Text></Divider>
|
||||
<p className='p-2 overflow-auto m-0 break-words whitespace-pre-wrap' dangerouslySetInnerHTML={{__html: orderDetail.order_detail}}></p>
|
||||
<Modal title='添加备注' open={isModalOpen}
|
||||
<Divider orientation="left">
|
||||
<Typography.Text strong>表单信息</Typography.Text>
|
||||
</Divider>
|
||||
<p className="p-2 overflow-auto m-0 break-words whitespace-pre-wrap" dangerouslySetInnerHTML={{ __html: orderDetail.order_detail }}></p>
|
||||
<Modal
|
||||
title="添加备注"
|
||||
open={isModalOpen}
|
||||
onOk={() => {
|
||||
const orderCommnet = orderCommentRef.current.value
|
||||
const orderCommnet = orderCommentRef.current.value;
|
||||
if (isEmpty(orderCommnet)) {
|
||||
message.warning('请输入备注后再提交。')
|
||||
message.warning("请输入备注后再提交。");
|
||||
} else {
|
||||
appendOrderComment(loginUser.userId, currentOrder, orderCommnet)
|
||||
.then(() => {
|
||||
message.success('添加成功')
|
||||
setIsModalOpen(false)
|
||||
message.success("添加成功");
|
||||
setIsModalOpen(false);
|
||||
})
|
||||
.catch(reason => {
|
||||
notification.error({
|
||||
message: '添加出错',
|
||||
message: "添加出错",
|
||||
description: reason.message,
|
||||
placement: 'top',
|
||||
placement: "top",
|
||||
duration: 60,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
orderCommentRef.current.value = ''
|
||||
orderCommentRef.current.value = "";
|
||||
}}
|
||||
onCancel={() => {setIsModalOpen(false)}}>
|
||||
<textarea ref={orderCommentRef} className='w-full' rows={4}></textarea>
|
||||
onCancel={() => {
|
||||
setIsModalOpen(false);
|
||||
}}>
|
||||
<textarea ref={orderCommentRef} className="w-full" rows={4}></textarea>
|
||||
</Modal>
|
||||
<Button size={'small'} onClick={() => {
|
||||
setIsModalOpen(true)
|
||||
}}>添加备注</Button>
|
||||
<Button
|
||||
size={"small"}
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
}}>
|
||||
添加备注
|
||||
</Button>
|
||||
</Spin>
|
||||
<ConversationsNewItem initialValues={newChatFormValues} open={newChatModalVisible} onCreate={handleNewChat} onCancel={() => setNewChatModalVisible(false)} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Empty
|
||||
description={<span>暂无相关订单</span>}
|
||||
>
|
||||
<ConversationBind currentConversationID={currentConversationID} onBoundSuccess={(coli_sn) => updateCurrentConversation({coli_sn})} />
|
||||
<Empty description={<span>暂无相关订单</span>}>
|
||||
<ConversationBind currentConversationID={currentConversationID} onBoundSuccess={coli_sn => updateCurrentConversation({ coli_sn })} />
|
||||
</Empty>
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default CustomerProfile
|
||||
export default CustomerProfile;
|
||||
|
Loading…
Reference in New Issue