({ + resource: "quotes", + syncWithLocation: false, + sorters: { + initial: [ + { + field: "updatedAt", + order: "desc", + }, + ], + }, + filters: { + initial: [ + { + field: "title", + value: "", + operator: "contains", + }, + { + field: "status", + value: undefined, + operator: "in", + }, + ], + permanent: [ + { + field: "company.id", + operator: "eq", + value: params.id, + }, + ], + }, + meta: { + fields: [ + "id", + "title", + "status", + "total", + { company: ["id", "name"] }, + { contact: ["id", "name", "avatarUrl"] }, + { salesOwner: ["id", "name", "avatarUrl"] }, + ], + }, + }); + + const { selectProps: selectPropsUsers } = useSelect({ + resource: "users", + optionLabel: "name", + pagination: { + mode: "off", + }, + meta: { + fields: ["id", "name"], + }, + }); + + const showResetFilters = useMemo(() => { + return filters?.filter((filter) => { + if ("field" in filter && filter.field === "company.id") { + return false; + } + + if (!filter.value) { + return false; + } + + return true; + }); + }, [filters]); + + const hasData = tableProps?.dataSource?.length || 0 > 0; + + return ( ++ + ); +}; + +const statusOptions: { label: string; value: QuoteStatus }[] = [ + { + label: "Draft", + value: "DRAFT", + }, + { + label: "Sent", + value: "SENT", + }, + { + label: "Accepted", + value: "ACCEPTED", + }, +]; diff --git a/examples/app-crm/src/routes/companies/table-view.tsx b/examples/app-crm/src/components/company/table-view.tsx similarity index 66% rename from examples/app-crm/src/routes/companies/table-view.tsx rename to examples/app-crm/src/components/company/table-view.tsx index 8b8be4308646..5f566cb510ae 100644 --- a/examples/app-crm/src/routes/companies/table-view.tsx +++ b/examples/app-crm/src/components/company/table-view.tsx @@ -1,20 +1,18 @@ +import { FC } from "react"; import { DeleteButton, + EditButton, FilterDropdown, - ShowButton, getDefaultSortOrder, useSelect, } from "@refinedev/antd"; import { CrudFilters, CrudSorting, getDefaultFilter } from "@refinedev/core"; -import { Avatar, Select, Space, Table, TableProps, Tooltip } from "antd"; -import { - currencyNumber, - getNameInitials, - getRandomColorFromString, -} from "../../utilities"; -import { Text } from "../../components"; +import { Avatar, Input, Select, Space, Table, TableProps, Tooltip } from "antd"; + +import { Text, CustomAvatar } from ".."; +import { currencyNumber } from "../../utilities"; import { Company } from "../../interfaces/graphql"; -import { FC } from "react"; +import { EyeOutlined, SearchOutlined } from "@ant-design/icons"; type Props = { tableProps: TableProps+ Quotes + + {showResetFilters?.length > 0 && ( + + )} + + } + > + {!hasData && ( ++ + )} + {hasData && ( +No quotes yet + +{" "} + Add quotes + + +
+ )}{" "} +} + filterDropdown={(props) => ( + + + + )} + /> ++ title="Total amount" + dataIndex="total" + sorter + render={(_, record) => { + return ( + {currencyNumber(record.total || 0)} + ); + }} + /> ++ title="Stage" + dataIndex="status" + render={(_, record) => { + if (!record.status) return null; + + return ; + }} + filterDropdown={(props) => ( + + + + )} + /> ++ dataIndex={["salesOwner", "id"]} + title="Participants" + render={(_, record) => { + return ( + + ); + }} + filterDropdown={(props) => { + return ( + + + + ); + }} + /> ++ dataIndex="id" + width={48} + render={(value) => { + return ( + } + /> + ); + }} + /> + ; @@ -27,17 +25,6 @@ export const CompaniesTableView: FC = ({ filters, sorters, }) => { - const { selectProps: selectPropsCompanies } = useSelect({ - resource: "companies", - optionLabel: "name", - pagination: { - mode: "off", - }, - meta: { - fields: ["id", "name"], - }, - }); - const { selectProps: selectPropsUsers } = useSelect({ resource: "users", optionLabel: "name", @@ -82,35 +69,23 @@ export const CompaniesTableView: FC = ({ rowKey="id" > - dataIndex="id" + dataIndex="name" title="Company title" defaultFilteredValue={getDefaultFilter("id", filters)} + filterIcon={ } filterDropdown={(props) => ( - + )} render={(_, record) => { return (- - {getNameInitials({ - name: record.name, - })} - + />= ({ const salesOwner = record.salesOwner; return ( - - {getNameInitials({ - name: salesOwner.name, - })} - + />= ({ + ); +}; + +const SalesOwnerInput = ({ + salesOwner, + onChange, + loading, +}: { + onChange?: (value: string) => void; + salesOwner?: Company["salesOwner"]; + loading?: boolean; +}) => { + const [isEdit, setIsEdit] = useState(false); + + const { selectProps, queryResult } = useSelectdataIndex={"totalRevenue"} title="Open deals amount" - sorter - defaultSortOrder={getDefaultSortOrder("totalRevenue", sorters)} - render={(value) => { - return {currencyNumber(value || 0)} ; + render={(_, company) => { + return ( ++ {currencyNumber( + company?.dealsAggregate?.[0].sum?.value || 0, + )} + + ); }} />@@ -205,21 +174,10 @@ export const CompaniesTableView: FC = ({ title={contact.name} key={contact.id} > - - {getNameInitials({ - name: contact.name, - })} - + /> ); })} @@ -232,7 +190,8 @@ export const CompaniesTableView: FC= ({ title="Actions" render={(value) => ( - } hideText size="small" recordItemId={value} diff --git a/examples/app-crm/src/components/company/title-form/title-form.module.css b/examples/app-crm/src/components/company/title-form/title-form.module.css new file mode 100644 index 000000000000..cbe32bfaa492 --- /dev/null +++ b/examples/app-crm/src/components/company/title-form/title-form.module.css @@ -0,0 +1,31 @@ +.title { + display: block; + height: 2.5rem; + margin: 0 !important; + + .titleEditIcon { + visibility: hidden; + } + + &:hover { + .titleEditIcon { + visibility: visible !important; + } + } +} + +.salesOwnerInput { + display: flex; + align-items: center; + height: 2rem; + + .salesOwnerInputEditIcon { + visibility: hidden; + } + + &:hover { + .salesOwnerInputEditIcon { + visibility: visible !important; + } + } +} diff --git a/examples/app-crm/src/components/company/title-form/title-form.tsx b/examples/app-crm/src/components/company/title-form/title-form.tsx new file mode 100644 index 000000000000..4f2a2b44646f --- /dev/null +++ b/examples/app-crm/src/components/company/title-form/title-form.tsx @@ -0,0 +1,212 @@ +import { HttpError } from "@refinedev/core"; +import { Avatar, Button, Form, Select, Skeleton, Space } from "antd"; +import { useState } from "react"; +import { EditOutlined } from "@ant-design/icons"; + +import { useForm, useSelect } from "@refinedev/antd"; +import { Text } from "../../../components"; +import { getNameInitials, getRandomColorFromString } from "../../../utilities"; +import { SelectOptionWithAvatar } from "../../../components/select-option-with-avatar"; +import { Company, User } from "../../../interfaces/graphql"; + +import styles from "./title-form.module.css"; + +export const CompanyTitleForm = () => { + const { formProps, queryResult, onFinish } = useForm ({ + redirect: false, + meta: { + fields: [ + "id", + "name", + "avatarUrl", + { + salesOwner: ["id", "name", "avatarUrl"], + }, + ], + }, + }); + + const company = queryResult?.data?.data; + const loading = queryResult?.isLoading; + + return ( + + ); +}; + +const TitleInput = ({ + value, + onChange, + loading, +}: { + // value is set by + value?: string; + onChange?: (value: string) => void; + loading?: boolean; +}) => { + return ( + , + }} + > + {loading ? ( + + ) : ( + value + )} + ({ + resource: "users", + optionLabel: "name", + pagination: { + mode: "off", + }, + meta: { + fields: ["id", "name", "avatarUrl"], + }, + }); + + return ( + { + setIsEdit(true); + }} + > ++ ); +}; diff --git a/examples/app-crm/src/components/contact/card-view.tsx b/examples/app-crm/src/components/contact/card-view.tsx new file mode 100644 index 000000000000..7da6d483e170 --- /dev/null +++ b/examples/app-crm/src/components/contact/card-view.tsx @@ -0,0 +1,53 @@ +import { Col, Pagination, Row, type TableProps } from "antd"; + +import { ContactCard } from "./card"; +import { Contact } from "../../interfaces/graphql"; + +type Props = { + tableProps: TableProps+ Sales Owner: + + {loading && ( ++ )} + {!isEdit && !loading && ( + <> + + {getNameInitials(salesOwner?.name || "")} + +{salesOwner?.name} + + } + /> + > + )} + {isEdit && !loading && ( ++ + )} +; + setCurrent: (current: number) => void; + setPageSize: (pageSize: number) => void; +}; + +export const CardView: React.FC = ({ + tableProps: { dataSource, pagination }, + setCurrent, + setPageSize, +}) => { + return ( + ++ ); +}; diff --git a/examples/app-crm/src/components/contact/card/index.tsx b/examples/app-crm/src/components/contact/card/index.tsx index 45e7b274b21d..28e2d6d369a7 100644 --- a/examples/app-crm/src/components/contact/card/index.tsx +++ b/examples/app-crm/src/components/contact/card/index.tsx @@ -1,52 +1,74 @@ import React from "react"; -import { Avatar, Button, Dropdown, MenuProps } from "antd"; -import { EllipsisOutlined, EyeOutlined } from "@ant-design/icons"; -import { MenuInfo } from "rc-menu/lib/interface"; +import { Button, Dropdown, MenuProps } from "antd"; +import { + DeleteOutlined, + EllipsisOutlined, + EyeOutlined, +} from "@ant-design/icons"; +import { useDelete, useGetToPath } from "@refinedev/core"; +import { useNavigate } from "react-router-dom"; import { Text } from "../../text"; import { ContactStatusTag } from "../status-tag"; - +import { CustomAvatar } from "../../custom-avatar"; import { Contact } from "../../../interfaces/graphql"; + import styles from "./index.module.css"; type ContactCardProps = { contact: Contact; - onClick?: (menu: MenuInfo) => void; }; -const items: MenuProps["items"] = [ - { - label: "Show", - key: "show", - icon:+ {dataSource?.map((contact) => ( +
+ ++ + + ))} + { + return ( + + {total}{" "} + contacts in total + + ); + }} + onChange={(page, pageSize) => { + setCurrent(page); + setPageSize(pageSize); + }} + /> + , - }, -]; +export const ContactCard: React.FC = ({ contact }) => { + const { name, email, status, jobTitle, company, avatarUrl, id } = contact; + + const navigate = useNavigate(); + const getToPath = useGetToPath(); + const { mutate: deleteMutate } = useDelete(); + + const items: MenuProps["items"] = [ + { + label: "Show", + key: "show", + icon: , + onClick: () => { + navigate( + getToPath({ + action: "show", + meta: { id }, + }) ?? "", + { + replace: true, + }, + ); + }, + }, + { + label: "Delete", + key: "delete", + danger: true, + icon: , + onClick: () => { + deleteMutate({ + resource: "contacts", + id, + }); + }, + }, + ]; -export const ContactCard: React.FC = ({ - contact, - onClick, -}) => { - const { name, email, status, jobTitle, company, avatarUrl } = contact; return ( onClick?.(e), - }} + menu={{ items }} trigger={["click"]} > } /> -+ = ({ -- {`${jobTitle} at` || "-"} + + {(jobTitle && `${jobTitle} at`) || } = ({ tooltip: true, }} > - diff --git a/examples/app-crm/src/components/contact/comment/comment-form.tsx b/examples/app-crm/src/components/contact/comment/comment-form.tsx index e6764d4ab200..c7fe0751976e 100644 --- a/examples/app-crm/src/components/contact/comment/comment-form.tsx +++ b/examples/app-crm/src/components/contact/comment/comment-form.tsx @@ -1,13 +1,8 @@ -import { - BaseKey, - HttpError, - useGetIdentity, - useInvalidate, - useParsed, -} from "@refinedev/core"; +import { BaseKey, HttpError, useGetIdentity, useParsed } from "@refinedev/core"; import { useForm } from "@refinedev/antd"; -import { Avatar, Form, Input } from "antd"; +import { Form, Input } from "antd"; +import { CustomAvatar } from "../../custom-avatar"; import { ContactNote, User } from "../../../interfaces/graphql"; type FormValues = ContactNote & { @@ -60,18 +55,11 @@ export const ContactCommentForm = () => { padding: "1rem", }} > -{company.name} - {me?.name.charAt(0)} - + />