admin-dashboard-web/src/Pages/CrmTest/CrmTest.js

1789 lines
51 KiB
JavaScript

import {
Button,
Col,
Collapse,
Drawer,
Form,
Input,
Row,
Segmented,
Select,
Space,
Table,
Tabs,
Typography,
notification,
Badge,
Popover,
DatePicker,
TimePicker,
QRCode,
Card,
Empty,
Flex,
Popconfirm,
} from "antd";
import { useTranslation } from "react-i18next";
import {
AppStyle,
Constants,
FormatDatetime,
myFetch,
showInputsInvalidNotification,
showUnkownErrorNotification,
wsConnectionCustomEventName,
} from "../../utils";
import { useEffect, useRef, useState } from "react";
import {
PlusOutlined,
ChromeOutlined,
SearchOutlined,
PhoneOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import { t } from "i18next";
import { useCrmContext } from "../../Contexts/CrmContext";
import "@mdxeditor/editor/style.css";
import { MDXEditor } from "@mdxeditor/editor/MDXEditor";
import { headingsPlugin } from "@mdxeditor/editor/plugins/headings";
import { listsPlugin } from "@mdxeditor/editor/plugins/lists";
import { quotePlugin } from "@mdxeditor/editor/plugins/quote";
import { thematicBreakPlugin } from "@mdxeditor/editor/plugins/thematic-break";
import { toolbarPlugin } from "@mdxeditor/editor/plugins/toolbar";
import { BoldItalicUnderlineToggles } from "@mdxeditor/editor/plugins/toolbar/components/BoldItalicUnderlineToggles";
import { UndoRedo } from "@mdxeditor/editor/plugins/toolbar/components/UndoRedo";
import { linkPlugin } from "@mdxeditor/editor/plugins/link";
import { markdownShortcutPlugin } from "@mdxeditor/editor/plugins/markdown-shortcut";
import { MySupsenseFallback } from "../../Components/MySupsenseFallback";
import { linkDialogPlugin } from "@mdxeditor/editor";
import { tablePlugin } from "@mdxeditor/editor";
import { frontmatterPlugin } from "@mdxeditor/editor";
import { MyAvatar } from "../../Components/MyAvatar";
import { useAppContext } from "../../Contexts/AppContext";
import Highlighter from "react-highlight-words";
import MyModal, {
MyModalCloseCreateButtonFooter,
} from "../../Components/MyModal";
import dayjs from "dayjs";
import Markdown from "react-markdown";
const CRM_TYPE = {
CUSTOMERS: 0,
DMC_PIPELINE: 1,
SETTER_CLOSER: 2,
};
function MyBadge({ count, color = "blue" }) {
return <Badge color={color} count={count} overflowCount={100000} showZero />;
}
const segmentedDmcPipelineBadgeColors = [
"grey",
"yellow",
"purple",
"blue",
"orange",
"green",
"red",
];
const segmentedSetterCloserBadgeColors = [
"grey",
"yellow",
"purple",
"blue",
"orange",
"green",
"red",
];
export default function CrmTest() {
const { t } = useTranslation();
const appContext = useAppContext();
const crmContext = useCrmContext();
const [isRequesting, setIsRequesting] = useState(false);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [notificationApi, notificationContextHolder] =
notification.useNotification();
// used for the segmented component on top of the page
const [selectedSegmentedTypeValue, setSelectedSegmentedTypeValue] =
useState(1);
const filterAssignedEmployeeRef = useRef([]);
const [searchText, setSearchText] = useState("");
const [searchedColumn, setSearchedColumn] = useState("");
const searchInput = useRef(null);
const title =
selectedSegmentedTypeValue === CRM_TYPE.CUSTOMERS
? "crm.customers.pageTitle"
: selectedSegmentedTypeValue === CRM_TYPE.DMC_PIPELINE
? "crm.dmcPipeline.pageTitle"
: "crm.setterCloser.pageTitle";
const segmentedOptions = t(
selectedSegmentedTypeValue === CRM_TYPE.DMC_PIPELINE
? "crm.dmcPipeline.segmentedOptions"
: "crm.setterCloser.segmentedOptions",
{
returnObjects: true,
}
);
const [
selectedDmcPipelineSegmentedValue,
setSelectedDmcPipelineSegmentedValue,
] = useState(0);
const [
selectedSetterCloserSegmentedValue,
setSelectedSetterCloserSegmentedValue,
] = useState(0);
const sorter = (a, b) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
};
const sorterDates = (a, b) => {
return a === Constants.TEXT_EMPTY_PLACEHOLDER
? 0
: new Date(a) - b === Constants.TEXT_EMPTY_PLACEHOLDER
? 0
: new Date(b);
};
const handleSearch = (selectedKeys, confirm, dataIndex) => {
confirm();
setSearchText(selectedKeys[0]);
setSearchedColumn(dataIndex);
};
const handleReset = (clearFilters) => {
clearFilters();
setSearchText("");
};
const getColumnSearchProps = (dataIndex) => ({
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
close,
}) => (
<div
style={{
padding: 8,
}}
onKeyDown={(e) => e.stopPropagation()}
>
<Input
ref={searchInput}
placeholder={`${t("crm.placeholderSearch")} ${dataIndex}`}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => handleSearch(selectedKeys, confirm, dataIndex)}
style={{
marginBottom: 8,
display: "block",
}}
/>
<Space>
<Button
type="primary"
onClick={() => handleSearch(selectedKeys, confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{
width: 90,
}}
>
{t("common.button.search")}
</Button>
<Button
onClick={() => clearFilters && handleReset(clearFilters)}
size="small"
style={{
width: 100,
}}
>
{t("common.button.reset")}
</Button>
<Button
type="link"
size="small"
onClick={() => {
confirm({
closeDropdown: false,
});
setSearchText(selectedKeys[0]);
setSearchedColumn(dataIndex);
}}
>
{t("common.button.filter")}
</Button>
<Button
type="link"
size="small"
onClick={() => {
close();
}}
>
{t("common.button.close")}
</Button>
</Space>
</div>
),
filterIcon: (filtered) => (
<SearchOutlined
style={{
color: filtered ? "#1677ff" : undefined,
}}
/>
),
onFilter: (value, record) =>
record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()),
onFilterDropdownOpenChange: (visible) => {
if (visible) {
setTimeout(() => searchInput.current?.select(), 100);
}
},
render: (text) =>
searchedColumn === dataIndex ? (
<Highlighter
highlightStyle={{
backgroundColor: "#ffc069",
padding: 0,
}}
searchWords={[searchText]}
autoEscape
textToHighlight={text ? text.toString() : ""}
/>
) : (
text
),
});
const getTableContent = () => {
return [
{
title: t("crm.table.assignedEmployee"),
dataIndex: "assignedEmployee",
key: "assignedEmployee",
sorter: (a, b) => sorter(a._assignedEmployee, b._assignedEmployee),
filters: filterAssignedEmployeeRef.current.map((item) => {
return {
text:
appContext.users.find((u) => u.Id === item) !== undefined ? (
<>
<MyAvatar userId={item} allUsers={appContext.users} />{" "}
{appContext.users.find((u) => u.Id === item)?.Username}
</>
) : (
item
),
value: item,
};
}),
onFilter: (value, record) => record._assignedEmployee === value,
ellipsis: true,
},
{
title: t("crm.table.firstName"),
dataIndex: "firstName",
key: "firstName",
sorter: (a, b) => sorter(a.firstName, b.firstName),
...getColumnSearchProps("firstName"),
},
{
title: t("crm.table.lastName"),
dataIndex: "lastName",
key: "lastName",
sorter: (a, b) => sorter(a.lastName, b.lastName),
...getColumnSearchProps("lastName"),
},
{
title: t("crm.table.company"),
dataIndex: "company",
key: "company",
sorter: (a, b) => sorter(a.company, b.company),
...getColumnSearchProps("company"),
},
{
title: t("crm.table.createdAt"),
dataIndex: "createdAt",
key: "createdAt",
sorter: (a, b) => sorterDates(a.createdAt, b.createdAt),
sortDirections: ["descend"],
},
{
title: t("crm.table.telephone"),
dataIndex: "telephone",
key: "telephone",
sorter: (a, b) => sorter(a.telephone, b.telephone),
...getColumnSearchProps("telephone"),
},
{
title: t("crm.table.email"),
dataIndex: "email",
key: "email",
sorter: (a, b) => sorter(a.email, b.email),
...getColumnSearchProps("email"),
},
{
title: t("crm.table.lastContact"),
dataIndex: "lastContact",
key: "lastContact",
sorter: (a, b) => sorterDates(a.lastContact, b.lastContact),
sortDirections: ["descend"],
},
{
title: t("crm.table.updatedAt"),
dataIndex: "updatedAt",
key: "updatedAt",
sorter: (a, b) => sorterDates(a.updatedAt, b.updatedAt),
sortDirections: ["descend"],
},
{
title: t("crm.table.createdBy"),
dataIndex: "createdBy",
key: "createdBy",
sorter: (a, b) => a.createdBy - b.createdBy,
sortDirections: ["descend"],
},
];
};
const getTableItems = () => {
let data = crmContext.customers;
let items = [];
if (selectedSegmentedTypeValue === CRM_TYPE.DMC_PIPELINE) {
data = data.filter((item) => item.Pipeline === 1);
data = data.filter(
(item) => item.DealPhase === selectedDmcPipelineSegmentedValue + 1
);
} else if (selectedSegmentedTypeValue === CRM_TYPE.SETTER_CLOSER) {
data = data.filter((item) => item.Pipeline === 2);
data = data.filter(
(item) => item.DealPhase === selectedSetterCloserSegmentedValue + 1
);
}
data.forEach((item) => {
items.push({
key: item.Id,
_assignedEmployee: item.AssignedEmployee,
assignedEmployee: (
<>
<Popover
placement="right"
trigger="hover"
content={
<MyAvatar
userId={item.AssignedEmployee}
allUsers={appContext.users}
avatarWidth={256}
/>
}
>
<>
<MyAvatar
userId={item.AssignedEmployee}
allUsers={appContext.users}
/>{" "}
{
appContext.users.find((u) => u.Id === item.AssignedEmployee)
?.Username
}
</>
</Popover>
</>
),
id: item.Id,
firstName: item.FirstName,
lastName: item.LastName,
company: item.Company,
createdAt: FormatDatetime(item.CreatedAt),
telephone: item.Telephone,
email: item.Email,
lastContact: FormatDatetime(item.LastContact),
updatedAt: FormatDatetime(item.UpdatedAt),
createdBy: (
<>
<Popover
placement="right"
trigger="hover"
content={
<MyAvatar
userId={item.CreatedBy}
allUsers={appContext.users}
avatarWidth={256}
/>
}
>
<>
<MyAvatar userId={item.CreatedBy} allUsers={appContext.users} />{" "}
{
appContext.users.find((u) => u.Id === item.CreatedBy)
?.Username
}
</>
</Popover>
</>
),
});
});
return items;
};
const getSegmentedLabels = (item, index) => {
return (
<Space>
<span>{item}</span>
<MyBadge
count={
crmContext.customers.filter(
(customer) =>
customer.Pipeline ===
(selectedSegmentedTypeValue === CRM_TYPE.DMC_PIPELINE
? 1
: 2) && customer.DealPhase === index + 1
).length
}
color={
selectedSegmentedTypeValue === CRM_TYPE.DMC_PIPELINE
? segmentedDmcPipelineBadgeColors[index]
: segmentedSetterCloserBadgeColors[index]
}
/>
</Space>
);
};
useEffect(() => {
const customersRequest = () => {
setIsRequesting(true);
myFetch(`/crm/customers`, "GET").then((data) => {
if (data === undefined || data === null) return;
data.forEach((item) => {
if (
!filterAssignedEmployeeRef.current.includes(item.AssignedEmployee)
) {
filterAssignedEmployeeRef.current.push(item.AssignedEmployee);
}
});
crmContext.setCustomers(data);
setIsRequesting(false);
});
};
customersRequest();
const handleCustomersRequest = () => customersRequest();
document.addEventListener(
wsConnectionCustomEventName,
handleCustomersRequest
);
return () =>
document.removeEventListener(
wsConnectionCustomEventName,
handleCustomersRequest
);
}, []);
return (
<>
{notificationContextHolder}
<Segmented
value={selectedSegmentedTypeValue}
onChange={(value) => {
setSelectedSegmentedTypeValue(value);
}}
options={[
{
value: CRM_TYPE.CUSTOMERS,
label: (
<Space>
<span>All Leads</span>
<MyBadge count={crmContext.customers.length} />
</Space>
),
},
{
value: CRM_TYPE.DMC_PIPELINE,
label: (
<Space>
<span>DMC Pipeline</span>
<MyBadge
count={
crmContext.customers.filter((item) => item.Pipeline === 1)
.length
}
/>
</Space>
),
},
{
value: CRM_TYPE.SETTER_CLOSER,
label: (
<Space>
<span>Setter / Closer</span>
<MyBadge
count={
crmContext.customers.filter((item) => item.Pipeline === 2)
.length
}
/>
</Space>
),
},
]}
block
/>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: 6,
}}
>
<Typography.Title level={4}>{t(title)}</Typography.Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
crmContext.openDrawerCustomerId.current = "new";
crmContext.currentDrawerCustomerRef.current = null;
setIsDrawerOpen(true);
}}
>
{t("crm.buttonNew")}
</Button>
</div>
{selectedSegmentedTypeValue !== CRM_TYPE.CUSTOMERS ? (
<Segmented
value={
selectedSegmentedTypeValue === CRM_TYPE.DMC_PIPELINE
? selectedDmcPipelineSegmentedValue
: selectedSetterCloserSegmentedValue
}
onChange={(value) => {
if (selectedSegmentedTypeValue === CRM_TYPE.DMC_PIPELINE) {
setSelectedDmcPipelineSegmentedValue(value);
} else {
setSelectedSetterCloserSegmentedValue(value);
}
}}
options={segmentedOptions.map((item, index) => {
return {
value: index,
label: getSegmentedLabels(item, index),
};
})}
style={{ marginBottom: AppStyle.app.margin }}
/>
) : (
<div style={{ height: 44 }} />
)}
<Table
scroll={{ x: "max-content" }}
columns={getTableContent()}
dataSource={getTableItems()}
loading={isRequesting}
pagination
onRow={(record) => {
return {
onClick: () => {
setIsDrawerOpen(true);
crmContext.openDrawerCustomerId.current = record.id;
},
};
}}
/>
<CustomerDrawer
isOpen={isDrawerOpen}
setIsOpen={setIsDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
notificationApi={notificationApi}
/>
</>
);
}
function CustomerDrawer({ isOpen, setIsOpen, onClose, notificationApi }) {
const { t } = useTranslation();
const appContext = useAppContext();
const crmContext = useCrmContext();
const [formDealInfo] = Form.useForm();
const tabItems = [
{
key: "0",
label: t("crm.tabs.dealInfo"),
children: <TabContentDealInfo form={formDealInfo} />,
},
{
key: "1",
label: t("crm.tabs.activities"),
children: <TabContentActivities notificationApi={notificationApi} />,
},
{
key: "2",
label: t("crm.tabs.notes"),
children: (
<TabContentNotes
notes={
crmContext.currentDrawerCustomerRef.current === null ||
crmContext.currentDrawerCustomerRef.current.Notes === null
? ""
: crmContext.currentDrawerCustomerRef.current.Notes
}
/>
),
},
];
const [activeTab, setActiveTab] = useState(tabItems[0].key);
const customerRequest = () => {
if (!isOpen) return;
myFetch(
`/crm/customer/view/${crmContext.openDrawerCustomerId.current}`,
"GET"
).then((data) => {
const customer = data.Customer;
crmContext.currentDrawerCustomerRef.current = customer;
crmContext.setCurrentDrawerCallProtocols(data.CallProtocols);
formDealInfo.setFieldsValue({
Pipeline: customer.Pipeline,
DealPhase: customer.DealPhase,
FirstName: customer.FirstName,
LastName: customer.LastName,
Telephone: customer.Telephone,
Email: customer.Email,
Company: customer.Company,
ZipCode: customer.ZipCode,
Address: customer.Address,
City: customer.City,
Country: customer.Country,
FederalState: customer.FederalState,
Website: customer.Website,
LeadOrigin: customer.LeadOrigin,
NumberOfEmployees: customer.NumberOfEmployees,
NumberOfJobsSearchedFor: customer.NumberOfJobsSearchedFor,
JobTitle: customer.JobTitle,
NumberOfEmployeesRequired: customer.NumberOfEmployeesRequired,
HowLongHadHeBeenSearching: customer.HowLongHadHeBeenSearching,
Turnover: customer.Turnover,
DateOfcompletion: customer.DateOfcompletion,
OrderVolume: customer.OrderVolume,
NumberOfInstallments: customer.NumberOfInstallments,
AmountsOfTheInstallments: customer.AmountsOfTheInstallments,
BookedPackages: customer.BookedPackages,
AssignedEmployee: customer.AssignedEmployee,
});
});
};
const handleRequestError = (status) => {
if (status === 409) {
notificationApi["error"]({
message: t("crm.tabContent.request.duplicateCompanyError.message"),
description: t(
"crm.tabContent.request.duplicateCompanyError.description"
),
});
} else {
showUnkownErrorNotification(notificationApi, t);
}
setIsOpen(true);
};
useEffect(() => {
// set the active tab to the first one if the drawer is closed
if (!isOpen) {
setActiveTab(tabItems[0].key);
}
if (crmContext.openDrawerCustomerId.current === null) return;
formDealInfo
.validateFields()
.then((values) => {
if (crmContext.openDrawerCustomerId.current === "new") {
if (isOpen) {
formDealInfo.resetFields();
formDealInfo.setFieldsValue({
Pipeline: 1,
DealPhase: 1,
AssignedEmployee: appContext.userId.current,
});
crmContext.setCurrentDrawerCallProtocols([]);
return;
}
let changedFields = [];
let newCustomer = {};
Object.keys(values).forEach((key) => {
if (values[key] !== undefined) {
changedFields.push(key);
}
});
// check notes
if (crmContext.currentDrawerCustomerNotesRef.current !== null) {
changedFields.push("Notes");
}
// check if something has changed (length 2 = only the pipeline, deal phase and assigned employee)
if (changedFields.length === 3) return;
Object.keys(values).forEach((key) => {
if (values[key] !== undefined) {
newCustomer[key] = values[key];
}
});
if (changedFields.includes("Notes")) {
newCustomer.Notes =
crmContext.currentDrawerCustomerNotesRef.current;
}
myFetch(`/crm/customer/create`, "POST", newCustomer)
.then(() => {
crmContext.openDrawerCustomerId.current = false;
handleCloseDrawer();
})
.catch((status) => handleRequestError(status));
return;
}
if (isOpen) {
customerRequest();
} else {
let changedFields = [];
const updatedCustomer = {};
Object.keys(values).forEach((key) => {
if (
values[key] !== crmContext.currentDrawerCustomerRef.current[key]
) {
changedFields.push(key);
}
});
// check notes
if (
crmContext.currentDrawerCustomerNotesRef.current !== null &&
crmContext.currentDrawerCustomerNotesRef.current !==
crmContext.currentDrawerCustomerRef.current.Notes
) {
changedFields.push("Notes");
}
// check if something has changed
if (changedFields.length > 0) {
changedFields.forEach((key) => {
updatedCustomer[key] = values[key];
});
if (changedFields.includes("Notes")) {
updatedCustomer.Notes =
crmContext.currentDrawerCustomerNotesRef.current;
}
myFetch(
`/crm/customer/update/${crmContext.openDrawerCustomerId.current}`,
"POST",
updatedCustomer
)
.then(() => handleCloseDrawer())
.catch((status) => handleRequestError(status));
}
}
})
.catch((err) => console.log("error", err));
const handleCustomerRequest = () => customerRequest();
document.addEventListener(
wsConnectionCustomEventName,
handleCustomerRequest
);
return () =>
document.removeEventListener(
wsConnectionCustomEventName,
handleCustomerRequest
);
}, [isOpen]);
const handleCloseDrawer = () => {
crmContext.openDrawerCustomerId.current = null;
crmContext.currentDrawerCustomerRef.current = null;
crmContext.currentDrawerCustomerNotesRef.current = null;
formDealInfo.resetFields();
};
return (
<Drawer
placement="right"
open={isOpen}
onClose={onClose}
width={720}
extra={
<Space>
<CallProtocolModal
formDealInfo={formDealInfo}
notificationApi={notificationApi}
/>
<Form form={formDealInfo} layout="inline">
<Form.Item name="AssignedEmployee" style={{ margin: 0 }}>
<Select style={{ minWidth: 120 }}>
{appContext.users.map((user) => (
<Select.Option key={user.Id} value={user.Id}>
<Space>
<MyAvatar
userId={user.Id}
allUsers={appContext.users}
avatarWidth={24}
/>
{user.Username}
</Space>
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
<Button
onClick={() => {
onClose();
handleCloseDrawer();
}}
>
{t("crm.buttonUndo")}
</Button>
</Space>
}
>
<Tabs
activeKey={activeTab}
onChange={(activeKey) => setActiveTab(activeKey)}
items={tabItems}
centered
/>
</Drawer>
);
}
function Content({ children }) {
return (
<Row gutter={16}>
<Col xs={24} sm={12}>
{children[0]}
</Col>
<Col xs={24} sm={12}>
{children[1]}
</Col>
</Row>
);
}
function CollapseContainer({ children, label }) {
return (
<Collapse
defaultActiveKey="1"
items={[
{
key: "1",
label: label,
children: children,
},
]}
/>
);
}
function TabContentDealInfo({ form }) {
const pipeline = Form.useWatch("Pipeline", form);
const dealPhase = Form.useWatch("DealPhase", form);
const FormItem = ({ name, label }) => (
<Form.Item name={name} label={typeof label === "string" ? t(label) : label}>
<Input maxLength={250} />
</Form.Item>
);
const ContentBlock = ({ items }) => {
// Split items into chunks of two
const chunks = [];
for (let i = 0; i < items.length; i += 2) {
chunks.push(items.slice(i, i + 2));
}
// Render a Content component for each chunk
return (
<>
{chunks.map((chunk, index) => (
<Content key={index}>
{chunk.map((item) => (
<FormItem key={item.name} {...item} />
))}
</Content>
))}
</>
);
};
const CollapseContainerBlock = ({ label, items }) => (
<CollapseContainer label={t(label)}>
<ContentBlock items={items} />
</CollapseContainer>
);
// set deal phase to 1 when the pipeline changes
// without this the user could set a deal phase that is not available for the selected pipeline
useEffect(() => {
const options = t(
pipeline === 1
? "crm.dmcPipeline.segmentedOptions"
: "crm.setterCloser.segmentedOptions",
{ returnObjects: true }
);
if (dealPhase > options.length) {
form.setFieldsValue({
DealPhase: options.length,
});
}
}, [pipeline]);
return (
<MySupsenseFallback>
<Form form={form} layout="vertical">
<Space direction="vertical" style={{ display: "flex" }}>
<CollapseContainer
label={t("crm.tabContent.dealInfo.collapseDealStatus.label")}
>
<Content>
<Form.Item
name="Pipeline"
label={t("crm.tabContent.dealInfo.collapseDealStatus.pipeline")}
>
<Select
options={[
{
value: 1,
label: t("crm.dmcPipeline.pageTitle"),
},
{
value: 2,
label: t("crm.setterCloser.pageTitle"),
},
]}
/>
</Form.Item>
<Form.Item
name="DealPhase"
label={t(
"crm.tabContent.dealInfo.collapseDealStatus.dealPhase"
)}
>
<Select
options={t(
pipeline === 1
? "crm.dmcPipeline.segmentedOptions"
: "crm.setterCloser.segmentedOptions",
{ returnObjects: true }
).map((item, index) => {
return {
value: index + 1,
label: item,
};
})}
/>
</Form.Item>
</Content>
</CollapseContainer>
<CollapseContainerBlock
label="crm.tabContent.dealInfo.collapseMasterDataOfContact.label"
items={[
{
name: "FirstName",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.firstName",
},
{
name: "LastName",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.lastName",
},
{
name: "Telephone",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.telephone",
},
{
name: "Email",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.email",
},
{
name: "Company",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.company",
},
{
name: "ZipCode",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.zipCode",
},
{
name: "Address",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.address",
},
{
name: "City",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.city",
},
{
name: "Country",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.country",
},
{
name: "FederalState",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.federalState",
},
{
name: "Website",
label: (
<Space>
<span>
{t(
"crm.tabContent.dealInfo.collapseMasterDataOfContact.website"
)}
</span>
<ChromeOutlined
style={{ color: AppStyle.app.primary }}
onClick={() => {
const website = form.getFieldValue("Website");
if (
!website.startsWith("http://") &&
!website.startsWith("https://")
)
return;
window.open(
form.getFieldValue("Website"),
"_blank",
"noopener"
);
}}
/>
</Space>
),
},
{
name: "LeadOrigin",
label:
"crm.tabContent.dealInfo.collapseMasterDataOfContact.leadOrigin",
},
]}
/>
<CollapseContainerBlock
label="crm.tabContent.dealInfo.collapseSetterInfo.label"
items={[
{
name: "NumberOfEmployees",
label:
"crm.tabContent.dealInfo.collapseSetterInfo.numberOfEmployees",
},
{
name: "NumberOfJobsSearchedFor",
label:
"crm.tabContent.dealInfo.collapseSetterInfo.numberOfJobsSearchedFor",
},
{
name: "JobTitle",
label: "crm.tabContent.dealInfo.collapseSetterInfo.jobTitle",
},
{
name: "NumberOfEmployeesRequired",
label:
"crm.tabContent.dealInfo.collapseSetterInfo.numberOfEmployeesRequired",
},
{
name: "HowLongHadHeBeenSearching",
label:
"crm.tabContent.dealInfo.collapseSetterInfo.howLongHadHeBeenSearching",
},
{
name: "Turnover",
label: "crm.tabContent.dealInfo.collapseSetterInfo.turnover",
},
]}
/>
<CollapseContainerBlock
label="crm.tabContent.dealInfo.collapseDealProperties.label"
items={[
{
name: "DateOfcompletion",
label:
"crm.tabContent.dealInfo.collapseDealProperties.dateOfcompletion",
},
{
name: "OrderVolume",
label:
"crm.tabContent.dealInfo.collapseDealProperties.orderVolume",
},
{
name: "NumberOfInstallments",
label:
"crm.tabContent.dealInfo.collapseDealProperties.numberOfInstallments",
},
{
name: "AmountsOfTheInstallments",
label:
"crm.tabContent.dealInfo.collapseDealProperties.amountsOfTheInstallments",
},
{
name: "BookedPackages",
label:
"crm.tabContent.dealInfo.collapseDealProperties.bookedPackages",
},
]}
/>
</Space>
</Form>
</MySupsenseFallback>
);
}
function TabContentActivities({ notificationApi }) {
const { t } = useTranslation();
return (
<MySupsenseFallback>
<Tabs
items={[
{
key: 0,
label: t("crm.tabContent.activities.calls.title"),
children: (
<ActivityCallProtocols notificationApi={notificationApi} />
),
},
{
key: 1,
label: t("crm.tabContent.activities.emails.title"),
disabled: true,
},
]}
/>
</MySupsenseFallback>
);
}
function ActivityCallProtocols({ notificationApi }) {
const { t } = useTranslation();
const appContext = useAppContext();
const crmContext = useCrmContext();
if (crmContext.currentDrawerCallProtocols.length === 0) {
return <Empty />;
}
const sortedCallProtocols = crmContext.currentDrawerCallProtocols.sort(
(a, b) => new Date(b.CalledAt) - new Date(a.CalledAt)
);
return (
<Space direction="vertical" style={{ width: "100%" }}>
{sortedCallProtocols.map((item) => {
const optionsCallResult = t("crm.callProtocolModal.callResult", {
returnObjects: true,
});
const callResult = [];
let alreadyLast = false;
let results = [
"ResultReached",
"ResultWhoPickedUp",
"ResultExpressedInterest",
"ResultScheduled",
];
results.forEach((result, index) => {
if (alreadyLast) return;
callResult.push(
optionsCallResult[index].result[item[result] === 1 ? 0 : 1]
);
if (item[result] === 0) {
// stop if the result is not set
alreadyLast = true;
}
});
const notes = [];
notes.push(`${FormatDatetime(item.CalledAt)} Uhr`);
if (item.Telephone !== "") {
notes.push(`Tel: ${item.Telephone}`);
}
if (item.Notes !== "") {
notes.push(`${t("crm.callProtocolModal.notes")}: ${item.Notes}`);
}
/*
{`${FormatDatetime(item.CalledAt)} Uhr${
item.Telephone !== "" ? ` - Tel: ${item.Telephone}` : ""
} ${
item.Notes !== ""
? `- ${t("crm.callProtocolModal.notes")}: ${(
<>
<Markdown>{item.Notes}</Markdown>
</>
)}`
: ""
}`}
*/
return (
<Card key={item.Id}>
<Card.Meta
avatar={
<MyAvatar
avatarWidth={56}
userId={item.CreatedBy}
allUsers={appContext.users}
/>
}
title={
<Flex justify="space-between">
<Typography.Title level={5} style={{ marginBottom: 0 }}>{`${
t("crm.callProtocolModal.callTypeOptions", {
returnObjects: true,
})[item.CallType - 1]
}: ${callResult.join(", ")}`}</Typography.Title>
<Popconfirm
placement="left"
title={t(
"crm.tabContent.activities.calls.deleteCallProtocolConfirm.title"
)}
cancelText={t("common.button.cancel")}
okText={t("common.button.confirm")}
onConfirm={() => {
myFetch(`/crm/calls/delete/${item.Id}`, "DELETE").catch(
() => showUnkownErrorNotification(notificationApi, t)
);
}}
>
<DeleteOutlined style={{ color: "darkred" }} />
</Popconfirm>
</Flex>
}
description={
<Typography.Paragraph
style={{ marginBottom: 0 }}
ellipsis={{
rows: 2,
expandable: true,
symbol: t("common.text.more"),
}}
>
{notes.join(" - ")}
</Typography.Paragraph>
}
/>
</Card>
);
})}
</Space>
);
}
const typeCallResult = {
step: 0,
answers: [],
finished: false, // if click cancel or answered all questions
};
function CallProtocolModal({ formDealInfo, notificationApi }) {
const { t } = useTranslation();
const crmContext = useCrmContext();
const [isOpen, setIsOpen] = useState(false);
const [form] = Form.useForm();
const [isRequesting, setIsRequesting] = useState(false);
const [callResult, setCallResult] = useState(typeCallResult);
const telephone = Form.useWatch("telephone", form);
// used to check if the customer has changed and reset the form
const lastCustomerIdRef = useRef(null);
const handleCancel = () => setIsOpen(false);
useEffect(() => {
if (isOpen) {
const currentDate = new Date();
// check if the customer has changed and reset the form
if (
lastCustomerIdRef.current !== crmContext.openDrawerCustomerId.current
) {
lastCustomerIdRef.current = crmContext.openDrawerCustomerId.current;
setCallResult(typeCallResult);
form.resetFields();
}
form.setFieldsValue({
callType: 1,
reached: "Ja",
date: dayjs(
`${currentDate.getFullYear()}-${(currentDate.getMonth() + 1)
.toString()
.padStart(2, "0")}-${currentDate
.getDate()
.toString()
.padStart(2, "0")}`,
"YYYY-MM-DD"
),
time: dayjs(
`${currentDate.getHours().toString().padStart(2, "0")}:${currentDate
.getMinutes()
.toString()
.padStart(2, "0")}-${currentDate
.getSeconds()
.toString()
.padStart(2, "0")}`,
"HH:mm:ss"
),
telephone: formDealInfo.getFieldValue("Telephone"),
});
}
}, [isOpen]);
const optionsCallResult = t("crm.callProtocolModal.callResult", {
returnObjects: true,
});
const buttonCallResultTextYes =
optionsCallResult[callResult.step].yes !== undefined
? t("crm.callProtocolModal.callResult", {
returnObjects: true,
})[callResult.step].yes
: t("common.text.yes");
const buttonCallResultTextNo =
optionsCallResult[callResult.step].no !== undefined
? t("crm.callProtocolModal.callResult", {
returnObjects: true,
})[callResult.step].no
: t("common.text.no");
return (
<>
<Button
type="primary"
icon={<PhoneOutlined />}
onClick={() => setIsOpen(true)}
>
{t("crm.buttonCall")}
</Button>
<MyModal
title={t("crm.callProtocolModal.title")}
isOpen={isOpen}
onCancel={handleCancel}
footer={
<MyModalCloseCreateButtonFooter
onCancel={handleCancel}
isCreateButtonLoading={isRequesting}
onCreate={() => {
form
.validateFields()
.then((values) => {
setIsRequesting(true);
// check if all questions are answered and the last answer is not no
if (
callResult.answers.length < optionsCallResult.length &&
callResult.answers[callResult.answers.length - 1] !== 0
) {
setIsRequesting(false);
showInputsInvalidNotification(notificationApi, t);
return;
}
// create datetime by combining date and time
const date = values.date;
const time = values.time;
const datetime = new Date(
date.year(),
date.month(),
date.date(),
time.hour(),
time.minute(),
time.second()
);
myFetch("/crm/calls/create", "POST", {
CustomerId: crmContext.openDrawerCustomerId.current,
CallType: values.callType,
CalledAt: datetime,
Telephone: values.telephone,
Notes: values.notes,
ResultReached:
callResult.answers[0] === null
? 0
: callResult.answers[0],
ResultWhoPickedUp:
callResult.answers[1] === null
? 0
: callResult.answers[1],
ResultExpressedInterest:
callResult.answers[2] === null
? 0
: callResult.answers[2],
ResultScheduled:
callResult.answers[3] === null
? 0
: callResult.answers[3],
})
.then(() => {
setIsRequesting(false);
setIsOpen(false);
setCallResult(typeCallResult);
})
.catch(() => {
setIsRequesting(false);
showUnkownErrorNotification(notificationApi, t);
});
})
.catch(() => {
setIsRequesting(false);
showInputsInvalidNotification(notificationApi, t);
});
}}
/>
}
>
<MySupsenseFallback spinnerCentered={false}>
<Form form={form} layout="vertical" requiredMark={false}>
<Content>
<Form.Item
name="callType"
label={t("crm.callProtocolModal.callType")}
>
<Select>
{t("crm.callProtocolModal.callTypeOptions", {
returnObjects: true,
}).map((item, index) => (
<Select.Option key={index} value={index + 1}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
{callResult.finished ? (
<Form.Item
label={
<div
style={{
display: "flex",
gap: 20,
}}
>
<span>{t("crm.callProtocolModal.callResultText")}</span>
<a
onClick={() =>
setCallResult({
step: 0,
answers: [],
finished: false,
})
}
>
{t("common.button.reset")}
</a>
</div>
}
>
<div
style={{
borderRadius: 6,
border: 1,
paddingTop: 4,
paddingLeft: 10,
paddingRight: 10,
paddingBottom: 4,
borderColor: "#d8d9d8",
borderStyle: "solid",
}}
>
{callResult.answers.map((item, index) => {
return (
<span key={index}>
<span
style={{
color:
optionsCallResult[index].bothValid !==
undefined || item === 1
? "green"
: "red",
}}
>
{
optionsCallResult[index].result[
item === 1 ? 0 : 1
]
}
</span>
{index !== callResult.answers.length - 1 && (
<span>{" > "}</span>
)}
</span>
);
})}
</div>
</Form.Item>
) : (
<Form.Item
name="callProtocol"
label={
t("crm.callProtocolModal.callResult", {
returnObjects: true,
})[callResult.step].question
}
style={{ width: "100%", margin: 0 }}
>
<Space
styles={{ item: { width: "100%" } }}
style={{ width: "100%" }}
>
<Form.Item name={["callProtocol", "yes"]}>
<Button
block
style={{
backgroundColor: "#f5fcdc",
}}
onClick={() => {
if (optionsCallResult.length > callResult.step + 1) {
setCallResult({
...callResult,
step: callResult.step + 1,
answers: [...callResult.answers, 1],
});
} else {
setCallResult({
...callResult,
answers: [...callResult.answers, 1],
finished: true,
});
}
}}
>
{buttonCallResultTextYes}
</Button>
</Form.Item>
<Form.Item name={["callProtocol", "no"]}>
<Button
block
style={{ backgroundColor: "#ebebf1" }}
onClick={() => {
if (optionsCallResult.length > callResult.step + 1) {
setCallResult({
...callResult,
step: callResult.step + 1,
answers: [...callResult.answers, 0],
finished:
optionsCallResult[callResult.step].bothValid !==
undefined
? false
: true,
});
} else {
setCallResult({
...callResult,
answers: [...callResult.answers, 0],
finished: true,
});
}
}}
>
{buttonCallResultTextNo}
</Button>
</Form.Item>
</Space>
</Form.Item>
)}
</Content>
<Content>
<Form.Item
name="date"
label={t("crm.callProtocolModal.date")}
rules={[
{
required: true,
message: t("common.text.pleaseInput"),
},
]}
>
<DatePicker style={{ width: "100%" }} />
</Form.Item>
<Form.Item
name="time"
label="Zeit"
rules={[
{
type: "object",
required: true,
message: t("common.text.pleaseInput"),
},
]}
>
<TimePicker style={{ width: "100%" }} />
</Form.Item>
</Content>
<Form.Item
name="telephone"
label={t("crm.callProtocolModal.telephone")}
rules={[
{
required: true,
message: t("common.text.pleaseInput"),
},
]}
>
<Input />
</Form.Item>
<Form.Item name="notes" label={t("crm.callProtocolModal.notes")}>
<MyMxEditor _key="call_protocol_notes" markdown={""} />
</Form.Item>
<Form.Item label={t("crm.callProtocolModal.infoQrCode")}>
{telephone !== "" ? (
<QRCode value={`tel:${telephone}`} />
) : (
<Empty />
)}
</Form.Item>
</Form>
</MySupsenseFallback>
</MyModal>
</>
);
}
function TabContentNotes({ notes }) {
const crmContext = useCrmContext();
return (
<MySupsenseFallback>
<MyMxEditor
_key={crmContext.currentDrawerCustomerRef.current?.Id}
markdown={notes}
onChange={(value) => {
crmContext.currentDrawerCustomerNotesRef.current = value;
}}
/>
</MySupsenseFallback>
);
}
function MyMxEditor({ _key, markdown, onChange }) {
return (
<MDXEditor
key={_key}
className="mdx-editor"
markdown={markdown}
onChange={onChange}
plugins={[
listsPlugin(),
quotePlugin(),
headingsPlugin(),
linkPlugin(),
linkDialogPlugin(),
tablePlugin(),
thematicBreakPlugin(),
frontmatterPlugin(),
markdownShortcutPlugin(),
toolbarPlugin({
toolbarContents: () => (
<>
<UndoRedo />
<BoldItalicUnderlineToggles />
</>
),
}),
]}
/>
);
}