customer-module-frontend 1.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/context.md +306 -0
- package/.cursor/rules/guardrails.md +35 -0
- package/.env +1 -0
- package/.github/workflows/publish-to-npm-beta.yml +51 -0
- package/.github/workflows/publish-to-npm.yml +58 -0
- package/README.md +73 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/package.json +43 -0
- package/postcss-unwrap-layers.js +31 -0
- package/postcss.config.js +11 -0
- package/public/vite.svg +1 -0
- package/src/App.css +40 -0
- package/src/App.tsx +58 -0
- package/src/assets/accounts.svg +3 -0
- package/src/assets/at_the_rate.svg +10 -0
- package/src/assets/buildings.svg +3 -0
- package/src/assets/chat.svg +3 -0
- package/src/assets/close.svg +3 -0
- package/src/assets/contacts.svg +3 -0
- package/src/assets/conversation.svg +10 -0
- package/src/assets/customers.svg +10 -0
- package/src/assets/details.svg +3 -0
- package/src/assets/domain.svg +10 -0
- package/src/assets/edit.svg +15 -0
- package/src/assets/email.svg +3 -0
- package/src/assets/google.svg +8 -0
- package/src/assets/inbox.svg +0 -0
- package/src/assets/message.svg +3 -0
- package/src/assets/no-data.svg +9 -0
- package/src/assets/open_in_a_new_tab.svg +10 -0
- package/src/assets/phone.svg +3 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/search.svg +3 -0
- package/src/assets/search_typing.svg +4 -0
- package/src/assets/sm_contacts.svg +3 -0
- package/src/assets/sm_inbox.svg +3 -0
- package/src/assets/sm_slider.svg +3 -0
- package/src/assets/status-resolved.svg +3 -0
- package/src/assets/status-snoozed.svg +4 -0
- package/src/assets/status_open.svg +1 -0
- package/src/components/AccountContacts/index.tsx +107 -0
- package/src/components/AccountDetails/index.tsx +102 -0
- package/src/components/AccountsConversation/index.tsx +75 -0
- package/src/components/Avatar/constants.tsx +45 -0
- package/src/components/Avatar/index.tsx +42 -0
- package/src/components/BreadcrumbsSection/index.tsx +16 -0
- package/src/components/Card/index.tsx +31 -0
- package/src/components/Card/types.ts +10 -0
- package/src/components/ContactConversation/Converation.tsx +14 -0
- package/src/components/ContactConversation/index.tsx +81 -0
- package/src/components/ContactDetails/index.tsx +111 -0
- package/src/components/Contacts/EditContact.tsx +213 -0
- package/src/components/Contacts/constants/index.tsx +24 -0
- package/src/components/Contacts/index.tsx +171 -0
- package/src/components/ConversationBox/constants.tsx +99 -0
- package/src/components/ConversationBox/index.tsx +147 -0
- package/src/components/ConversationBox/types.ts +20 -0
- package/src/components/CustomersLayout/index.tsx +20 -0
- package/src/components/DetailsCard/index.tsx +31 -0
- package/src/components/DetailsCard/types.ts +10 -0
- package/src/components/EmptyData/NoDataFound.tsx +31 -0
- package/src/components/Header/index.tsx +55 -0
- package/src/components/Icon/index.tsx +93 -0
- package/src/components/Icon/types.ts +13 -0
- package/src/components/Listing/AccountTable.tsx +47 -0
- package/src/components/Listing/ContactTable.tsx +76 -0
- package/src/components/RightPanel/AccountPanel.tsx +123 -0
- package/src/components/RightPanel/ContactPanel.tsx +142 -0
- package/src/components/RightPanel/index.tsx +167 -0
- package/src/components/Search/SearchDialog.tsx +150 -0
- package/src/components/TabsSection/index.tsx +49 -0
- package/src/constants/index.tsx +645 -0
- package/src/hooks/useBreadcrumb.tsx +93 -0
- package/src/index.css +315 -0
- package/src/main.tsx +14 -0
- package/src/pages/AccountDetail.tsx +68 -0
- package/src/pages/Accounts.tsx +12 -0
- package/src/pages/ContactDetail.tsx +55 -0
- package/src/pages/Contacts.tsx +12 -0
- package/src/stores/count.tsx +17 -0
- package/src/types/index.ts +0 -0
- package/tailwind.config.js +179 -0
- package/tsconfig.app.json +36 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +31 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Outlet, useLocation } from "react-router-dom";
|
|
2
|
+
import Header from "../Header";
|
|
3
|
+
import TabsSection from "../TabsSection";
|
|
4
|
+
import BreadcrumbsSection from "../BreadcrumbsSection";
|
|
5
|
+
|
|
6
|
+
export default function CustomersLayout() {
|
|
7
|
+
const location = useLocation();
|
|
8
|
+
const isDetailsPage = location.pathname.includes("/accounts/") ||
|
|
9
|
+
location.pathname.includes("/contacts/");
|
|
10
|
+
|
|
11
|
+
console.log('isDetailsPage', isDetailsPage);
|
|
12
|
+
return (
|
|
13
|
+
<Header>
|
|
14
|
+
<div className="cm:pt-2 cm:px-12">
|
|
15
|
+
{isDetailsPage ? <BreadcrumbsSection /> : <TabsSection />}
|
|
16
|
+
</div>
|
|
17
|
+
<Outlet />
|
|
18
|
+
</Header>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { DetailsCardProps } from './types';
|
|
3
|
+
|
|
4
|
+
const DetailsCard: React.FC<DetailsCardProps> = ({
|
|
5
|
+
header,
|
|
6
|
+
children,
|
|
7
|
+
className = '',
|
|
8
|
+
maxWidth = 'cm:max-w-[360px]',
|
|
9
|
+
maxHeight = 'cm:h-full'
|
|
10
|
+
}) => {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className={`cm:bg-slate-surface-white cm:border cm:border-slate-border-light cm:rounded-[12px] cm:relative cm:w-full ${maxWidth} ${maxHeight} ${className}`}
|
|
14
|
+
>
|
|
15
|
+
<div className="cm:flex cm:flex-col cm:items-start cm:overflow-clip cm:relative cm:rounded-[inherit] cm:w-full">
|
|
16
|
+
{/* Header Section */}
|
|
17
|
+
<div className="cm:border-b cm:border-slate-border-mild cm:box-border cm:flex cm:h-12 cm:items-center cm:pl-[21px] cm:pr-[25px] cm:py-[14px] cm:relative cm:shrink-0 cm:w-full">
|
|
18
|
+
{header}
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{/* Content Section */}
|
|
22
|
+
<div className="cm:box-border cm:flex cm:flex-col cm:items-start cm:overflow-clip cm:px-[21px] cm:py-5 cm:relative cm:shrink-0 cm:w-full">
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default DetailsCard;
|
|
31
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import NoDataImage from '../../assets/no-data.svg';
|
|
3
|
+
|
|
4
|
+
export interface NoDataFoundProps {
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const NoDataFound: React.FC<NoDataFoundProps> = ({ title, description }) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className="cm:flex cm:flex-col cm:items-center cm:justify-center cm:w-full cm:h-full cm:py-12">
|
|
12
|
+
<div className="cm:flex cm:flex-col cm:items-center cm:gap-3">
|
|
13
|
+
<img
|
|
14
|
+
src={NoDataImage}
|
|
15
|
+
alt="No data"
|
|
16
|
+
className="cm:w-[100px] cm:h-[100px]"
|
|
17
|
+
/>
|
|
18
|
+
<div className="cm:flex cm:flex-col cm:items-center cm:gap-1">
|
|
19
|
+
<p className="cm:text-body-sm cm:text-slate-text-title cm:text-center">
|
|
20
|
+
{title}
|
|
21
|
+
</p>
|
|
22
|
+
<p className="cm:text-body-meduim cm:text-slate-text-subtle cm:text-center">
|
|
23
|
+
{description}
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default NoDataFound;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import SearchDialog from "../Search/SearchDialog";
|
|
4
|
+
import { TextField } from "hiver-ui-kit-extended";
|
|
5
|
+
import searchIcon from "@assets/search.svg";
|
|
6
|
+
import Icon from "../Icon";
|
|
7
|
+
interface LayoutProps {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|
12
|
+
const [isSearchDialogOpen, setIsSearchDialogOpen] = React.useState(false);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="cm:min-h-screen cm:bg-slate-surface-white">
|
|
16
|
+
{/* Header */}
|
|
17
|
+
<header className="cm:border-b cm:border-slate-border-light cm:bg-white cm:px-12 cm:h-14 cm:flex cm:items-center cm:justify-between">
|
|
18
|
+
{/* Right: Search Bar */}
|
|
19
|
+
|
|
20
|
+
<div className="cm:flex cm:items-center cm:gap-3">
|
|
21
|
+
<div className="cm:text-label-medium cm:text-slate-text-title">
|
|
22
|
+
Customers
|
|
23
|
+
</div>
|
|
24
|
+
<Icon
|
|
25
|
+
name="customers"
|
|
26
|
+
size={16}
|
|
27
|
+
type="active"
|
|
28
|
+
color="#0F172A"
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className="cm:w-[300px]" onClick={()=> setIsSearchDialogOpen(true)}>
|
|
33
|
+
<TextField
|
|
34
|
+
size="small"
|
|
35
|
+
onChange={()=>setIsSearchDialogOpen(true)}
|
|
36
|
+
placeholder="Search"
|
|
37
|
+
icon={searchIcon}
|
|
38
|
+
iconPosition="left"
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
</div>
|
|
43
|
+
</header>
|
|
44
|
+
|
|
45
|
+
{/* Main Content */}
|
|
46
|
+
<main>{children}</main>
|
|
47
|
+
<SearchDialog
|
|
48
|
+
open={isSearchDialogOpen}
|
|
49
|
+
onClose={() => setIsSearchDialogOpen(false)}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default Layout;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { IconProps } from './types';
|
|
3
|
+
|
|
4
|
+
const icons = import.meta.glob('/src/assets/**/*.svg', {
|
|
5
|
+
query: '?raw',
|
|
6
|
+
import: 'default',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const strokeWidths: Record<string, string> = { '14': '1.1', '16': '1.3' };
|
|
10
|
+
|
|
11
|
+
const Icon: React.FC<IconProps> = ({
|
|
12
|
+
name,
|
|
13
|
+
size = 16,
|
|
14
|
+
type = 'active',
|
|
15
|
+
folder = '',
|
|
16
|
+
disabled = false,
|
|
17
|
+
color,
|
|
18
|
+
fill,
|
|
19
|
+
className,
|
|
20
|
+
onClick,
|
|
21
|
+
}) => {
|
|
22
|
+
const [svgContent, setSvgContent] = useState<string>('');
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const loadIcon = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const basePath = folder
|
|
28
|
+
? `/src/assets/${folder}/${name}.svg`
|
|
29
|
+
: `/src/assets/${name}.svg`;
|
|
30
|
+
|
|
31
|
+
const loader = icons[basePath];
|
|
32
|
+
if (!loader) {
|
|
33
|
+
console.error(`[Icon] Icon '${name}' not found at ${basePath}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const raw = (await loader()) as string;
|
|
38
|
+
const sizeStr = String(size);
|
|
39
|
+
const stroke = color ?? `var(--p-slate-icons-${disabled ? 'disabled' : type})`;
|
|
40
|
+
const strokeWidth = strokeWidths[sizeStr] || '1.3';
|
|
41
|
+
|
|
42
|
+
let processed = raw
|
|
43
|
+
.replace(/stroke=".*?"/g, `stroke="${stroke}"`)
|
|
44
|
+
.replace(/stroke-width=".*?"/g, `stroke-width="${strokeWidth}"`)
|
|
45
|
+
.replace(/<svg([^>]*?)>/, (_, attrs: string) => {
|
|
46
|
+
let newAttrs = attrs;
|
|
47
|
+
|
|
48
|
+
// Update or add width
|
|
49
|
+
if (/width=".*?"/.test(newAttrs)) {
|
|
50
|
+
newAttrs = newAttrs.replace(/width=".*?"/, `width="${sizeStr}"`);
|
|
51
|
+
} else {
|
|
52
|
+
newAttrs = `${newAttrs} width="${sizeStr}"`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Update or add height
|
|
56
|
+
if (/height=".*?"/.test(newAttrs)) {
|
|
57
|
+
newAttrs = newAttrs.replace(/height=".*?"/, `height="${sizeStr}"`);
|
|
58
|
+
} else {
|
|
59
|
+
newAttrs = `${newAttrs} height="${sizeStr}"`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return `<svg${newAttrs} style="max-width: ${sizeStr}px;">`;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Handle fill for rect elements
|
|
66
|
+
if (fill) {
|
|
67
|
+
processed = processed.replace(/<rect([^>]*?)\/?>/g, (_match, rectAttrs: string) => {
|
|
68
|
+
if (/fill="/.test(rectAttrs)) {
|
|
69
|
+
return `<rect${rectAttrs.replace(/fill=".*?"/, `fill="${fill}"`)} />`;
|
|
70
|
+
}
|
|
71
|
+
return `<rect${rectAttrs} fill="${fill}" />`;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setSvgContent(processed);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`[Icon] Error loading icon '${name}':`, error);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
loadIcon();
|
|
82
|
+
}, [name, size, type, disabled, color, fill, folder]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<span
|
|
86
|
+
className={`cm:flex cm:items-center cm:justify-center cm:w-max ${disabled ? 'cm:cursor-not-allowed' : 'cm:cursor-inherit'} ${className || ''}`}
|
|
87
|
+
onClick={onClick}
|
|
88
|
+
dangerouslySetInnerHTML={{ __html: svgContent }}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default Icon;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type IconType = 'active' | 'inactive' | 'disabled';
|
|
2
|
+
|
|
3
|
+
export interface IconProps {
|
|
4
|
+
name: string;
|
|
5
|
+
size?: number;
|
|
6
|
+
type?: IconType;
|
|
7
|
+
folder?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
color?: string;
|
|
10
|
+
fill?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
onClick?: (event: React.MouseEvent<HTMLSpanElement>) => void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DataTable } from "hiver-ui-kit-extended";
|
|
2
|
+
import { accountData } from "../../constants/index";
|
|
3
|
+
import { useNavigate } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
// Transform account results to table data
|
|
6
|
+
const tableData = accountData.data.results.map((account) => ({
|
|
7
|
+
id: account.id,
|
|
8
|
+
name: account.name,
|
|
9
|
+
domain: account.domains[0]?.name || account.generated_name,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const columns = [
|
|
13
|
+
{
|
|
14
|
+
id: "name",
|
|
15
|
+
field: "name",
|
|
16
|
+
label: "Account name",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "domain",
|
|
20
|
+
field: "domain",
|
|
21
|
+
label: "Domain",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export default function AccountTable() {
|
|
26
|
+
|
|
27
|
+
const navigate = useNavigate();
|
|
28
|
+
|
|
29
|
+
const handleAccountClick = (event: any) => {
|
|
30
|
+
const data = event.data;
|
|
31
|
+
navigate(`/accounts/${data.id}`);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="cm:px-12">
|
|
36
|
+
<DataTable
|
|
37
|
+
scrollable={true}
|
|
38
|
+
scrollHeight="32.75rem"
|
|
39
|
+
value={tableData}
|
|
40
|
+
columns={columns}
|
|
41
|
+
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
onRowClick={handleAccountClick}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DataTable } from "hiver-ui-kit-extended";
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { contactsData } from "../../constants/index";
|
|
4
|
+
import Avatar from "../Avatar/index";
|
|
5
|
+
|
|
6
|
+
// Name cell with avatar
|
|
7
|
+
const NameCell = ({ name }: { name: string }) => (
|
|
8
|
+
<div className="flex items-center gap-2">
|
|
9
|
+
<Avatar name={name} labelName={name} />
|
|
10
|
+
<span className="text-sm text-slate-900">{name}</span>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Email cell with link styling
|
|
15
|
+
const EmailCell = ({ email }: { email: string }) => (
|
|
16
|
+
<span className="text-sm !text-primary-text-default" style={{ color: '#0C3E9D' }}>{email}</span>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Phone cell
|
|
20
|
+
const PhoneCell = ({ phone }: { phone: string }) => (
|
|
21
|
+
<span className="text-sm text-slate-900" style={{ color: '#334155' }}>{phone}</span>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Account cell
|
|
25
|
+
const AccountCell = ({ account }: { account: string }) => (
|
|
26
|
+
<span className="text-sm text-slate-900">{account}</span>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Transform data with custom renderers
|
|
30
|
+
const tableData = contactsData.data.results.map((result) => ({
|
|
31
|
+
...result,
|
|
32
|
+
nameDisplay: <NameCell name={result.name} />,
|
|
33
|
+
emailDisplay: <EmailCell email={result.email} />,
|
|
34
|
+
phoneDisplay: <PhoneCell phone={result.phone_number || ""} />,
|
|
35
|
+
accountDisplay: <AccountCell account={result.company?.name || ""} />,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const columns = [
|
|
39
|
+
{
|
|
40
|
+
id: "name",
|
|
41
|
+
field: "nameDisplay",
|
|
42
|
+
label: "Name",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "email",
|
|
46
|
+
field: "emailDisplay",
|
|
47
|
+
label: "Email",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "phone",
|
|
51
|
+
field: "phoneDisplay",
|
|
52
|
+
label: "Phone Number",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "account",
|
|
56
|
+
field: "accountDisplay",
|
|
57
|
+
label: "Account",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export default function ContactTable() {
|
|
62
|
+
const navigate = useNavigate();
|
|
63
|
+
|
|
64
|
+
const handleContactClick = (event: any) => {
|
|
65
|
+
const data = event.data;
|
|
66
|
+
navigate(`/contacts/${data.id}`);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="cm:px-12">
|
|
71
|
+
<DataTable scrollable={true} scrollHeight="32.75rem" value={tableData} columns={columns}
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
onRowClick={handleContactClick} />
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ConversationBox from '../ConversationBox';
|
|
3
|
+
import Icon from '../Icon';
|
|
4
|
+
|
|
5
|
+
export interface AccountInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
domain: string;
|
|
8
|
+
contactCount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Conversation {
|
|
12
|
+
id: string;
|
|
13
|
+
type: 'chat' | 'email';
|
|
14
|
+
status: 'open' | 'pending' | 'closed';
|
|
15
|
+
senderName: string;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
heading?: string;
|
|
18
|
+
messagePreview: string;
|
|
19
|
+
mailboxName?: string;
|
|
20
|
+
assignedAgent?: {
|
|
21
|
+
name: string;
|
|
22
|
+
avatarColor?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AccountPanelProps {
|
|
27
|
+
account: AccountInfo;
|
|
28
|
+
conversations: Conversation[];
|
|
29
|
+
onConversationClick?: (conversationId: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const AccountPanel: React.FC<AccountPanelProps> = ({
|
|
33
|
+
account,
|
|
34
|
+
conversations,
|
|
35
|
+
onConversationClick,
|
|
36
|
+
}) => {
|
|
37
|
+
return (
|
|
38
|
+
<div className="cm:flex cm:flex-col cm:gap-3 cm:w-full">
|
|
39
|
+
{/* Account Header */}
|
|
40
|
+
<div className="cm:flex cm:flex-col cm:gap-4 cm:px-4 cm:pt-6 cm:pb-4">
|
|
41
|
+
{/* Icon and Account Name */}
|
|
42
|
+
<div className="cm:flex cm:gap-3 cm:items-center">
|
|
43
|
+
<div className="cm:w-7 cm:h-7 cm:rounded-full cm:flex cm:items-center cm:justify-center cm:shrink-0 cm:bg-slate-100">
|
|
44
|
+
<Icon name="buildings" size={14} color="#94A3B8" />
|
|
45
|
+
</div>
|
|
46
|
+
<h3 className="cm:text-base cm:font-medium cm:text-slate-text-title cm:leading-5">
|
|
47
|
+
{account.name}
|
|
48
|
+
</h3>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Account Details */}
|
|
52
|
+
<div className="cm:flex cm:flex-col cm:gap-1">
|
|
53
|
+
{/* Domain */}
|
|
54
|
+
<AccountDetailRow
|
|
55
|
+
icon="domain"
|
|
56
|
+
label="Domain"
|
|
57
|
+
value={account.domain}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
{/* Contact Count */}
|
|
61
|
+
<AccountDetailRow
|
|
62
|
+
icon="contacts"
|
|
63
|
+
label="Contact"
|
|
64
|
+
value={String(account.contactCount)}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Divider */}
|
|
70
|
+
<div className="cm:h-px cm:bg-slate-border-light" />
|
|
71
|
+
|
|
72
|
+
{/* Conversations Section */}
|
|
73
|
+
<div className="cm:flex cm:flex-col cm:gap-3">
|
|
74
|
+
<h4 className="cm:px-4 cm:text-sm cm:font-medium cm:text-slate-text-title cm:leading-5 cm:text-left">
|
|
75
|
+
Conversations from {account.name}
|
|
76
|
+
</h4>
|
|
77
|
+
|
|
78
|
+
{/* Conversations List */}
|
|
79
|
+
<div className="cm:flex cm:flex-col cm:gap-2 cm:px-4">
|
|
80
|
+
{conversations.map((conversation) => (
|
|
81
|
+
<ConversationBox
|
|
82
|
+
key={conversation.id}
|
|
83
|
+
type={conversation.type}
|
|
84
|
+
status={conversation.status}
|
|
85
|
+
size="small"
|
|
86
|
+
senderName={conversation.senderName}
|
|
87
|
+
timestamp={conversation.timestamp}
|
|
88
|
+
heading={conversation.heading}
|
|
89
|
+
messagePreview={conversation.messagePreview}
|
|
90
|
+
mailboxName={conversation.mailboxName}
|
|
91
|
+
assignedAgent={conversation.assignedAgent}
|
|
92
|
+
onClick={() => onConversationClick?.(conversation.id)}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
interface AccountDetailRowProps {
|
|
102
|
+
icon: string;
|
|
103
|
+
label: string;
|
|
104
|
+
value: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const AccountDetailRow: React.FC<AccountDetailRowProps> = ({ icon, label, value }) => {
|
|
108
|
+
return (
|
|
109
|
+
<div className="cm:flex cm:gap-3 cm:items-center cm:h-[38px]">
|
|
110
|
+
<div className="cm:flex cm:gap-2 cm:items-center cm:min-w-[90px]">
|
|
111
|
+
<Icon name={icon} size={14} color="#64758b" />
|
|
112
|
+
<span className="cm:text-sm cm:text-slate-text-subtle cm:leading-5">
|
|
113
|
+
{label}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
<span className="cm:text-sm cm:text-slate-text-body cm:leading-5 cm:truncate">
|
|
117
|
+
{value}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default AccountPanel;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ConversationBox from '../ConversationBox';
|
|
3
|
+
import Icon from '../Icon';
|
|
4
|
+
import Avatar from '../Avatar';
|
|
5
|
+
import PrimaryContactsDialog from '../Contacts';
|
|
6
|
+
|
|
7
|
+
export interface ContactInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
email: string;
|
|
10
|
+
phone: string;
|
|
11
|
+
account: string;
|
|
12
|
+
avatarColor?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Conversation {
|
|
16
|
+
id: string;
|
|
17
|
+
type: 'chat' | 'email';
|
|
18
|
+
status: 'open' | 'pending' | 'closed';
|
|
19
|
+
senderName: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
heading?: string;
|
|
22
|
+
messagePreview: string;
|
|
23
|
+
mailboxName?: string;
|
|
24
|
+
assignedAgent?: {
|
|
25
|
+
name: string;
|
|
26
|
+
avatarColor?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ContactPanelProps {
|
|
31
|
+
contact: ContactInfo;
|
|
32
|
+
conversations: Conversation[];
|
|
33
|
+
onConversationClick?: (conversationId: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ContactPanel: React.FC<ContactPanelProps> = ({
|
|
37
|
+
contact,
|
|
38
|
+
conversations,
|
|
39
|
+
onConversationClick,
|
|
40
|
+
}) => {
|
|
41
|
+
return (
|
|
42
|
+
<div className="cm:flex cm:flex-col cm:gap-3 cm:w-full cm:max-w-[330px]">
|
|
43
|
+
{/* Contact Header */}
|
|
44
|
+
<div className="cm:flex cm:flex-col cm:gap-4 cm:px-4 cm:pt-6 cm:pb-5">
|
|
45
|
+
{/* Avatar and Name */}
|
|
46
|
+
|
|
47
|
+
<div className="cm:flex cm:items-center cm:justify-between cm:px-0 cm:rounded-[6px] cm:shrink-0">
|
|
48
|
+
<div className="cm:flex cm:gap-3 cm:items-center">
|
|
49
|
+
<Avatar
|
|
50
|
+
name={contact.name}
|
|
51
|
+
labelName={contact.name}
|
|
52
|
+
size="default"
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<h3 className="cm:text-base cm:font-medium cm:text-slate-text-title cm:leading-5">
|
|
56
|
+
{contact.name}
|
|
57
|
+
</h3>
|
|
58
|
+
</div>
|
|
59
|
+
{/* <EditContact /> */}
|
|
60
|
+
<PrimaryContactsDialog />
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Contact Details */}
|
|
64
|
+
<div className="cm:flex cm:flex-col cm:gap-1">
|
|
65
|
+
{/* Email */}
|
|
66
|
+
<ContactDetailRow
|
|
67
|
+
icon="email"
|
|
68
|
+
label="Email"
|
|
69
|
+
value={contact.email}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
{/* Phone */}
|
|
73
|
+
<ContactDetailRow
|
|
74
|
+
icon="phone"
|
|
75
|
+
label="Phone"
|
|
76
|
+
value={contact.phone}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
{/* Account */}
|
|
80
|
+
<ContactDetailRow
|
|
81
|
+
icon="buildings"
|
|
82
|
+
label="Account"
|
|
83
|
+
value={contact.account}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Divider */}
|
|
89
|
+
<div className="cm:h-px cm:bg-slate-border-light" />
|
|
90
|
+
|
|
91
|
+
{/* Conversations Section */}
|
|
92
|
+
<div className="cm:flex cm:flex-col cm:gap-3 ">
|
|
93
|
+
<h4 className="cm:text-sm cm:font-medium cm:text-slate-text-title cm:leading-5 cm:text-left cm:px-4">
|
|
94
|
+
Conversations with {contact.name}
|
|
95
|
+
</h4>
|
|
96
|
+
|
|
97
|
+
{/* Conversations List */}
|
|
98
|
+
<div className="cm:flex cm:flex-col cm:gap-3 cm:px-4">
|
|
99
|
+
{conversations.map((conversation) => (
|
|
100
|
+
<ConversationBox
|
|
101
|
+
key={conversation.id}
|
|
102
|
+
type={conversation.type}
|
|
103
|
+
status={conversation.status}
|
|
104
|
+
size="small"
|
|
105
|
+
senderName={conversation.senderName}
|
|
106
|
+
timestamp={conversation.timestamp}
|
|
107
|
+
heading={conversation.heading}
|
|
108
|
+
messagePreview={conversation.messagePreview}
|
|
109
|
+
mailboxName={conversation.mailboxName}
|
|
110
|
+
assignedAgent={conversation.assignedAgent}
|
|
111
|
+
onClick={() => onConversationClick?.(conversation.id)}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
interface ContactDetailRowProps {
|
|
121
|
+
icon: string;
|
|
122
|
+
label: string;
|
|
123
|
+
value: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ContactDetailRow: React.FC<ContactDetailRowProps> = ({ icon, label, value }) => {
|
|
127
|
+
return (
|
|
128
|
+
<div className="cm:flex cm:gap-3 cm:items-center cm:h-[38px]">
|
|
129
|
+
<div className="cm:flex cm:gap-2 cm:items-center cm:w-[90px] cm:shrink-0">
|
|
130
|
+
<Icon name={icon} size={14} color="#64758b" />
|
|
131
|
+
<span className="cm:text-sm cm:text-slate-text-subtle cm:leading-5">
|
|
132
|
+
{label}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
<span className="cm:text-sm cm:text-slate-text-body cm:leading-5 cm:truncate">
|
|
136
|
+
{value}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default ContactPanel;
|