From c9e4ab6d39abeca97d3b619dd6e5d66adeca47fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salih=20=C3=96zdemir?= Date: Wed, 30 Aug 2023 10:44:23 +0300 Subject: [PATCH] refactor: app crm example (#4871) * chore: delete unused component * chore: add `index.ts` to export route components * refactor: create a custom avatar component to handle all avatars * refactor: calender components * fix: move quotes components under the components/quotes * fix: move company components under the components/company * refactor: seperate contact components * fix: add missing type * fix: add debounce to compony search * fix: calender page style issues * fix: calander show drawer issue * fix: datepicker bg color * fix: padding issues on event timeline * fix: compony create logic * fix: add "see calendar" button * fix: color picker input * fix: add delete button to card * fix: update contact page style issues * fix: contact modal drawer style issues * chore: fix build errorr * feat(app-crm): randomly select user email (#4877) * feat(app-crm) company detail page (#4873) * feat(app-crm): add company detail page * fix(app-crm): typo * feat(app-crm): add company notes * feat(app-crm): add missing fields and filters * feat(app-crm): add resource to useTable * fix(app-crm): disable company list query on create page * feat(app-crm): persist table filters * feat(app-crm): memoize avatar * feat(app-crm): use drag overlay --------- Co-authored-by: Alican Erdurmaz --- examples/app-crm/src/App.tsx | 90 ++-- .../src/components/calendar/calendar-cell.tsx | 47 ++ .../calendar/index.module.css | 0 .../{calender => calendar}/calendar/index.tsx | 51 +- .../categories/index.module.css | 0 .../categories/index.tsx | 11 +- .../{calender => calendar}/form/index.tsx | 27 +- .../app-crm/src/components/calendar/index.ts | 4 + .../manage-categories/index.module.css | 0 .../manage-categories/index.tsx | 0 .../upcoming-events/event/index.tsx | 9 +- .../upcoming-events/index.module.css | 4 +- .../upcoming-events/index.tsx | 14 +- .../company}/card-view.tsx | 161 +++--- .../src/components/company/contacts-table.tsx | 472 ++++++++++++++++++ .../src/components/company/deals-table.tsx | 266 ++++++++++ .../app-crm/src/components/company/index.ts | 8 + .../src/components/company/info-form.tsx | 319 ++++++++++++ .../app-crm/src/components/company/notes.tsx | 315 ++++++++++++ .../src/components/company/quotes-table.tsx | 249 +++++++++ .../company}/table-view.tsx | 101 ++-- .../company/title-form/title-form.module.css | 31 ++ .../company/title-form/title-form.tsx | 212 ++++++++ .../src/components/contact/card-view.tsx | 53 ++ .../src/components/contact/card/index.tsx | 96 ++-- .../contact/comment/comment-form.tsx | 26 +- .../contact/comment/comment-list.tsx | 22 +- .../app-crm/src/components/contact/index.ts | 4 + .../src/components/contact/table-view.tsx | 168 +++++++ .../app-crm/src/components/current-user.tsx | 18 +- .../app-crm/src/components/custom-avatar.tsx | 37 ++ .../app-crm/src/components/dashboard/index.ts | 5 + .../latest-activities/activity/index.tsx | 10 +- .../src/components/deal-kanban-card/index.tsx | 39 +- .../src/components/deal-stage-tag/index.tsx | 11 + examples/app-crm/src/components/index.ts | 8 +- .../src/components/kanban/comment-form.tsx | 18 +- .../src/components/kanban/comment-list.tsx | 15 +- .../src/components/kanban/item/index.tsx | 74 +-- .../src/components/participants/index.tsx | 45 ++ .../components/project-kanban-card/index.tsx | 30 +- .../index.tsx => quotes/form-modal.tsx} | 55 +- .../app-crm/src/components/quotes/index.ts | 6 + .../show => components/quotes}/pdf-export.tsx | 7 +- .../quotes}/products-services.tsx | 7 +- .../quotes/show-description.tsx} | 4 +- .../quotes/status-indicator/index.module.css} | 0 .../quotes/status-indicator/index.tsx} | 4 +- .../index.tsx => quotes/status-tag.tsx} | 14 +- .../components/select-option-with-avatar.tsx | 7 +- .../single-element-form/index.module.css | 29 +- .../components/single-element-form/index.tsx | 27 +- examples/app-crm/src/components/user-tag.tsx | 12 +- examples/app-crm/src/providers/auth.ts | 22 +- .../src/routes/administration/audit-log.tsx | 10 +- .../app-crm/src/routes/calendar/create.tsx | 23 +- examples/app-crm/src/routes/calendar/edit.tsx | 4 +- examples/app-crm/src/routes/calendar/index.ts | 4 + examples/app-crm/src/routes/calendar/show.tsx | 183 ++++--- .../app-crm/src/routes/calendar/wrapper.tsx | 18 +- .../app-crm/src/routes/companies/create.tsx | 57 ++- .../app-crm/src/routes/companies/edit.tsx | 45 ++ .../app-crm/src/routes/companies/index.tsx | 3 +- .../app-crm/src/routes/companies/list.tsx | 219 +++++--- .../app-crm/src/routes/companies/show.tsx | 11 - .../src/routes/contacts/company-create.tsx | 48 -- examples/app-crm/src/routes/contacts/index.ts | 4 + .../src/routes/contacts/show/index.module.css | 35 +- .../src/routes/contacts/show/index.tsx | 165 +++--- .../src/routes/contacts/wrapper/index.tsx | 265 +--------- .../app-crm/src/routes/dashboard/index.tsx | 39 +- examples/app-crm/src/routes/quotes/create.tsx | 10 +- examples/app-crm/src/routes/quotes/edit.tsx | 10 +- examples/app-crm/src/routes/quotes/list.tsx | 94 +--- .../app-crm/src/routes/quotes/show/index.tsx | 41 +- examples/app-crm/src/styles/antd.css | 8 + examples/app-crm/src/styles/index.css | 4 +- .../src/utilities/get-name-initials.ts | 8 +- 78 files changed, 3359 insertions(+), 1213 deletions(-) create mode 100644 examples/app-crm/src/components/calendar/calendar-cell.tsx rename examples/app-crm/src/components/{calender => calendar}/calendar/index.module.css (100%) rename examples/app-crm/src/components/{calender => calendar}/calendar/index.tsx (70%) rename examples/app-crm/src/components/{calender => calendar}/categories/index.module.css (100%) rename examples/app-crm/src/components/{calender => calendar}/categories/index.tsx (93%) rename examples/app-crm/src/components/{calender => calendar}/form/index.tsx (79%) create mode 100644 examples/app-crm/src/components/calendar/index.ts rename examples/app-crm/src/components/{calender => calendar}/manage-categories/index.module.css (100%) rename examples/app-crm/src/components/{calender => calendar}/manage-categories/index.tsx (100%) rename examples/app-crm/src/components/{calender => calendar}/upcoming-events/event/index.tsx (92%) rename examples/app-crm/src/components/{calender => calendar}/upcoming-events/index.module.css (80%) rename examples/app-crm/src/components/{calender => calendar}/upcoming-events/index.tsx (84%) rename examples/app-crm/src/{routes/companies => components/company}/card-view.tsx (65%) create mode 100644 examples/app-crm/src/components/company/contacts-table.tsx create mode 100644 examples/app-crm/src/components/company/deals-table.tsx create mode 100644 examples/app-crm/src/components/company/index.ts create mode 100644 examples/app-crm/src/components/company/info-form.tsx create mode 100644 examples/app-crm/src/components/company/notes.tsx create mode 100644 examples/app-crm/src/components/company/quotes-table.tsx rename examples/app-crm/src/{routes/companies => components/company}/table-view.tsx (66%) create mode 100644 examples/app-crm/src/components/company/title-form/title-form.module.css create mode 100644 examples/app-crm/src/components/company/title-form/title-form.tsx create mode 100644 examples/app-crm/src/components/contact/card-view.tsx create mode 100644 examples/app-crm/src/components/contact/index.ts create mode 100644 examples/app-crm/src/components/contact/table-view.tsx create mode 100644 examples/app-crm/src/components/custom-avatar.tsx create mode 100644 examples/app-crm/src/components/dashboard/index.ts create mode 100644 examples/app-crm/src/components/deal-stage-tag/index.tsx create mode 100644 examples/app-crm/src/components/participants/index.tsx rename examples/app-crm/src/components/{quotes-form-modal/index.tsx => quotes/form-modal.tsx} (76%) create mode 100644 examples/app-crm/src/components/quotes/index.ts rename examples/app-crm/src/{routes/quotes/show => components/quotes}/pdf-export.tsx (98%) rename examples/app-crm/src/{routes/quotes/show => components/quotes}/products-services.tsx (99%) rename examples/app-crm/src/{routes/quotes/show/description.tsx => components/quotes/show-description.tsx} (94%) rename examples/app-crm/src/{routes/quotes/show/status.module.css => components/quotes/status-indicator/index.module.css} (100%) rename examples/app-crm/src/{routes/quotes/show/status.tsx => components/quotes/status-indicator/index.tsx} (97%) rename examples/app-crm/src/components/{quote-status-tag/index.tsx => quotes/status-tag.tsx} (64%) create mode 100644 examples/app-crm/src/routes/calendar/index.ts create mode 100644 examples/app-crm/src/routes/companies/edit.tsx delete mode 100644 examples/app-crm/src/routes/companies/show.tsx delete mode 100644 examples/app-crm/src/routes/contacts/company-create.tsx create mode 100644 examples/app-crm/src/routes/contacts/index.ts diff --git a/examples/app-crm/src/App.tsx b/examples/app-crm/src/App.tsx index 156abf87a143..66ec17f8ee2d 100644 --- a/examples/app-crm/src/App.tsx +++ b/examples/app-crm/src/App.tsx @@ -1,7 +1,6 @@ import { Refine, Authenticated } from "@refinedev/core"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { notificationProvider, ErrorComponent } from "@refinedev/antd"; - import routerProvider, { NavigateToResource, CatchAllNavigate, @@ -16,15 +15,21 @@ import "@refinedev/antd/dist/reset.css"; import { authProvider } from "./providers/auth"; import { dataProvider, liveProvider } from "./providers/data"; import { resources } from "./providers/resources"; +import { themeConfig } from "./providers/antd"; + +import { Layout } from "./components/layout"; import { LoginPage } from "./routes/login"; import { RegisterPage } from "./routes/register"; import { ForgotPasswordPage } from "./routes/forgot-password"; import { UpdatePasswordPage } from "./routes/update-password"; - -import { DashboardPage } from "./routes/dashboard/index"; - -import { CalendarPageWrapper } from "./routes/calendar/wrapper"; +import { DashboardPage } from "./routes/dashboard"; +import { + CalendarPageWrapper, + CalendarShowPage, + CalendarEditPage, + CalendarCreatePage, +} from "./routes/calendar"; import { KanbanPage, KanbanCreatePage, @@ -32,7 +37,11 @@ import { KanbanCreateStage, KanbanEditStage, } from "./routes/scrumboard/kanban"; -import { CompanyListPage, CompanyShowPage } from "./routes/companies"; +import { + CompanyEditPage, + CompanyListPage, + CompanyCreatePage, +} from "./routes/companies"; import { SalesPage, SalesCreatePage, @@ -41,10 +50,12 @@ import { SalesEditStage, SalesCreateDetails, } from "./routes/scrumboard/sales"; -import { ContactsPageWrapper } from "./routes/contacts/wrapper"; -import { ContactCreatePage } from "./routes/contacts/create"; -import { ContactEditPage } from "./routes/contacts/edit"; -import { ContactShowPage } from "./routes/contacts/show"; +import { + ContactsPageWrapper, + ContactCreatePage, + ContactEditPage, + ContactShowPage, +} from "./routes/contacts"; import { QuotesListPage, QuotesCreatePage, @@ -53,12 +64,6 @@ import { } from "./routes/quotes"; import { SettingsPage } from "./routes/administration/settings"; import { AuditLogPage } from "./routes/administration/audit-log"; -import { Layout } from "./components/layout"; -import { themeConfig } from "./providers/antd"; -import { CalendarShowPage } from "./routes/calendar/show"; -import { CalendarEditPage } from "./routes/calendar/edit"; -import { CalendarCreatePage } from "./routes/calendar/create"; -import { CompanyCreatePage } from "./routes/companies/create"; import "./utilities/init-dayjs"; @@ -186,15 +191,23 @@ const App: React.FC = () => { /> - - - } /> + + + + } + > } + path="create" + element={} /> - + } + /> { } > - + } @@ -238,13 +251,34 @@ const App: React.FC = () => { > } - /> - + element={ + + + + } + > + + } + /> + } - /> + element={ + + + + } + > + + } + /> + void; +}; + +export const CalendarCell: React.FC = ({ + events, + value, + onClickEvent, +}) => { + const todayEvents = events.filter((event) => { + const startDate = dayjs(event.startDate); + const endDate = dayjs(event.endDate); + + return ( + startDate.isSame(value, "day") || + endDate.isSame(value, "day") || + (startDate.isBefore(value, "day") && endDate.isAfter(value, "day")) + ); + }); + + return ( +
+ {todayEvents.slice(0, 3).map((item) => ( +
onClickEvent?.(item)} key={item.id}> + + + {item.title} + +
+ ))} + {todayEvents.length > 3 && ( + {todayEvents.length - 3} more + )} +
+ ); +}; diff --git a/examples/app-crm/src/components/calender/calendar/index.module.css b/examples/app-crm/src/components/calendar/calendar/index.module.css similarity index 100% rename from examples/app-crm/src/components/calender/calendar/index.module.css rename to examples/app-crm/src/components/calendar/calendar/index.module.css diff --git a/examples/app-crm/src/components/calender/calendar/index.tsx b/examples/app-crm/src/components/calendar/calendar/index.tsx similarity index 70% rename from examples/app-crm/src/components/calender/calendar/index.tsx rename to examples/app-crm/src/components/calendar/calendar/index.tsx index 9d6693e4b12b..45a654d29b26 100644 --- a/examples/app-crm/src/components/calender/calendar/index.tsx +++ b/examples/app-crm/src/components/calendar/calendar/index.tsx @@ -1,11 +1,13 @@ import React from "react"; import { useList } from "@refinedev/core"; -import { Calendar as AntdCalendar, Card, Button, Badge } from "antd"; +import { Calendar as AntdCalendar, Card, Button } from "antd"; import { LeftOutlined, RightOutlined } from "@ant-design/icons"; import dayjs, { Dayjs } from "dayjs"; import { Text } from "../../text"; +import { CalendarCell } from "../calendar-cell"; import { Event } from "../../../interfaces/graphql"; + import styles from "./index.module.css"; type CalendarProps = { @@ -13,51 +15,6 @@ type CalendarProps = { onClickEvent?: (event: Event) => void; }; -type CalendarCellProps = { - value: Dayjs; - events: Event[]; -} & CalendarProps; - -const CalendarCell: React.FC = ({ - events, - value, - onClickEvent, -}) => { - const todayEvents = events.filter((event) => { - const startDate = dayjs(event.startDate); - const endDate = dayjs(event.endDate); - - return ( - startDate.isSame(value, "day") || - endDate.isSame(value, "day") || - (startDate.isBefore(value, "day") && endDate.isAfter(value, "day")) - ); - }); - - return ( -
- {todayEvents.slice(0, 3).map((item) => ( -
onClickEvent?.(item)} key={item.id}> - - - {item.title} - -
- ))} - {todayEvents.length > 3 && ( - {todayEvents.length - 3} more - )} -
- ); -}; - export const Calendar: React.FC = ({ categoryId, onClickEvent, @@ -68,7 +25,7 @@ export const Calendar: React.FC = ({ const { data } = useList({ pagination: { - pageSize: 9999, + mode: "off", }, filters: [ { diff --git a/examples/app-crm/src/components/calender/categories/index.module.css b/examples/app-crm/src/components/calendar/categories/index.module.css similarity index 100% rename from examples/app-crm/src/components/calender/categories/index.module.css rename to examples/app-crm/src/components/calendar/categories/index.module.css diff --git a/examples/app-crm/src/components/calender/categories/index.tsx b/examples/app-crm/src/components/calendar/categories/index.tsx similarity index 93% rename from examples/app-crm/src/components/calender/categories/index.tsx rename to examples/app-crm/src/components/calendar/categories/index.tsx index fb358633fcd8..489958740816 100644 --- a/examples/app-crm/src/components/calender/categories/index.tsx +++ b/examples/app-crm/src/components/calendar/categories/index.tsx @@ -1,18 +1,19 @@ import React from "react"; -import { Button, Card, theme, CardProps, Checkbox, Skeleton } from "antd"; +import { Button, Card, theme, Checkbox, Skeleton } from "antd"; import { useList } from "@refinedev/core"; +import { useModal } from "@refinedev/antd"; import { SettingOutlined, FlagOutlined } from "@ant-design/icons"; +import { CheckboxChangeEvent } from "antd/es/checkbox"; import { Text } from "../../text"; import { CalendarManageCategories } from "../manage-categories"; - import { EventCategory } from "../../../interfaces/graphql"; + import styles from "./index.module.css"; -import { useModal } from "@refinedev/antd"; type CalendarCategoriesProps = { - onChange?: (e: any) => void; -} & CardProps; + onChange?: (e: CheckboxChangeEvent) => void; +}; export const CalendarCategories: React.FC = ({ onChange, diff --git a/examples/app-crm/src/components/calender/form/index.tsx b/examples/app-crm/src/components/calendar/form/index.tsx similarity index 79% rename from examples/app-crm/src/components/calender/form/index.tsx rename to examples/app-crm/src/components/calendar/form/index.tsx index c75c3a15bf6a..ca0b59ed2a9e 100644 --- a/examples/app-crm/src/components/calender/form/index.tsx +++ b/examples/app-crm/src/components/calendar/form/index.tsx @@ -100,7 +100,7 @@ export const CalendarForm: React.FC = ({ noStyle > = ({ required: true, }, ]} - initialValue={"#000000"} + initialValue={"#1677FF"} > } + presets={[ + { + label: "Recommended", + colors: [ + "#F5222D", + "#FA8C16", + "#FADB14", + "#8BBB11", + "#52C41A", + "#13A8A8", + "#1677FF", + "#2F54EB", + "#722ED1", + "#EB2F96", + ], + }, + ]} onChangeComplete={(value) => { return form?.setFieldValue( "color", diff --git a/examples/app-crm/src/components/calendar/index.ts b/examples/app-crm/src/components/calendar/index.ts new file mode 100644 index 000000000000..9b4b720c097f --- /dev/null +++ b/examples/app-crm/src/components/calendar/index.ts @@ -0,0 +1,4 @@ +export * from "./calendar"; +export * from "./upcoming-events"; +export * from "./categories"; +export * from "./calendar-cell"; diff --git a/examples/app-crm/src/components/calender/manage-categories/index.module.css b/examples/app-crm/src/components/calendar/manage-categories/index.module.css similarity index 100% rename from examples/app-crm/src/components/calender/manage-categories/index.module.css rename to examples/app-crm/src/components/calendar/manage-categories/index.module.css diff --git a/examples/app-crm/src/components/calender/manage-categories/index.tsx b/examples/app-crm/src/components/calendar/manage-categories/index.tsx similarity index 100% rename from examples/app-crm/src/components/calender/manage-categories/index.tsx rename to examples/app-crm/src/components/calendar/manage-categories/index.tsx diff --git a/examples/app-crm/src/components/calender/upcoming-events/event/index.tsx b/examples/app-crm/src/components/calendar/upcoming-events/event/index.tsx similarity index 92% rename from examples/app-crm/src/components/calender/upcoming-events/event/index.tsx rename to examples/app-crm/src/components/calendar/upcoming-events/event/index.tsx index 13c045c58dff..e46b64178821 100644 --- a/examples/app-crm/src/components/calender/upcoming-events/event/index.tsx +++ b/examples/app-crm/src/components/calendar/upcoming-events/event/index.tsx @@ -69,16 +69,9 @@ export const CalendarUpcomingEvent: React.FC = ({ >
- {`${renderDate()}, ${renderTime()}`}
- + {title} diff --git a/examples/app-crm/src/components/calender/upcoming-events/index.module.css b/examples/app-crm/src/components/calendar/upcoming-events/index.module.css similarity index 80% rename from examples/app-crm/src/components/calender/upcoming-events/index.module.css rename to examples/app-crm/src/components/calendar/upcoming-events/index.module.css index 06b6363e08e3..03cda6f45b12 100644 --- a/examples/app-crm/src/components/calender/upcoming-events/index.module.css +++ b/examples/app-crm/src/components/calendar/upcoming-events/index.module.css @@ -8,11 +8,11 @@ } .badge { - margin-right: 0.7rem; + margin-right: 1.1rem; } .title { - padding-left: 1rem; + padding-left: 1.5rem; } } diff --git a/examples/app-crm/src/components/calender/upcoming-events/index.tsx b/examples/app-crm/src/components/calendar/upcoming-events/index.tsx similarity index 84% rename from examples/app-crm/src/components/calender/upcoming-events/index.tsx rename to examples/app-crm/src/components/calendar/upcoming-events/index.tsx index a40c80a3a1c1..2e5f53f33d79 100644 --- a/examples/app-crm/src/components/calender/upcoming-events/index.tsx +++ b/examples/app-crm/src/components/calendar/upcoming-events/index.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { Card, theme, CardProps, Skeleton } from "antd"; +import { Card, theme, Skeleton } from "antd"; +import type { CardProps } from "antd"; import { CalendarOutlined } from "@ant-design/icons"; import { useList } from "@refinedev/core"; import dayjs from "dayjs"; @@ -9,11 +10,11 @@ import { CalendarUpcomingEvent } from "./event"; import { Event } from "../../../interfaces/graphql"; -type CalendarUpcomingEventsProps = { limit?: number } & CardProps; +type CalendarUpcomingEventsProps = { limit?: number; cardProps?: CardProps }; export const CalendarUpcomingEvents: React.FC = ({ limit = 5, - ...rest + cardProps, }) => { const { token } = theme.useToken(); const { data, isLoading } = useList({ @@ -44,15 +45,18 @@ export const CalendarUpcomingEvents: React.FC = ({ title={ - + Upcoming events } + headStyle={{ + padding: "0 12px", + }} bodyStyle={{ padding: "0 1rem", }} - {...rest} + {...cardProps} > {isLoading && ( = ({ companies, pagination }) => { + const { edit } = useNavigation(); + const { mutate } = useDelete(); + return ( <> @@ -46,7 +48,6 @@ export const CompaniesCardView: FC = ({ companies, pagination }) => { > = ({ companies, pagination }) => { maxCount={3} size="small" > - {company?.contacts?.nodes?.map( + {company.contacts?.nodes?.map( (contact) => { return ( = ({ companies, pagination }) => { } key={contact.id} > - - {getNameInitials( - { - name: contact.name, - }, - )} - + /> ); }, @@ -108,33 +97,18 @@ export const CompaniesCardView: FC = ({ companies, pagination }) => { Sales owner - - {getNameInitials({ - name: company - ?.salesOwner?.name, - })} - + /> , @@ -148,42 +122,65 @@ export const CompaniesCardView: FC = ({ companies, pagination }) => { position: "relative", }} > - + )} + + } + actions={[]} + extra={ + <> + Total contacts: + + {tableProps?.pagination !== false && + tableProps.pagination?.total} + + + } + > + {!hasData && ( +
+ No contacts yet +
+ )} + {hasData && ( + + + title="Name" + dataIndex="name" + render={(_, record) => { + return ( +
+ + {record?.name} +
+ ); + }} + filterIcon={} + filterDropdown={(props) => ( + + + + )} + /> + } + filterDropdown={(props) => ( + + + + )} + /> + + title="Stage" + dataIndex="status" + render={(_, record) => { + return ; + }} + filterDropdown={(props) => ( + + + + )} + /> + + dataIndex="id" + width={112} + render={(value, record) => { + return ( + +
+ )} +
+ ); +}; + +type ContactFormValues = { + contacts: ContactCreateInput[]; +}; + +const ContactForm = () => { + const { id = "" } = useParams(); + + const { data } = useOne({ + id, + resource: "companies", + meta: { + fields: [ + "id", + "name", + { + salesOwner: ["id"], + }, + ], + }, + }); + + const [form] = Form.useForm(); + const contacts = Form.useWatch("contacts", form); + + const { mutateAsync } = useCreateMany< + Contact, + HttpError, + ContactCreateInput + >(); + + const handleOnFinish = async (args: ContactFormValues) => { + form.validateFields(); + + const contacts = args.contacts.map((contact) => ({ + ...contact, + companyId: id, + salesOwnerId: data?.data.salesOwner?.id || "", + })); + + await mutateAsync({ + resource: "contacts", + values: contacts, + successNotification: false, + }); + + form.resetFields(); + }; + + const { hasContacts } = useMemo(() => { + const hasContacts = contacts?.length > 0; + + return { + hasContacts, + }; + }, [contacts]); + + return ( +
+ + {(fields, { add, remove }) => { + return ( +
+ {fields.map(({ key, name, ...restField }) => { + return ( + + + + + } + placeholder="Contact name" + /> + + + + + + } + placeholder="Contact email" + /> + + + + +
+ ); + }} +
+ {hasContacts && ( +
+ + form.submit()} + /> +
+ )} +
+ ); +}; + +const statusOptions: { + label: string; + value: Contact["status"]; +}[] = [ + { + label: "New", + value: "NEW", + }, + { + label: "Qualified", + value: "QUALIFIED", + }, + { + label: "Unqualified", + value: "UNQUALIFIED", + }, + { + label: "Won", + value: "WON", + }, + { + label: "Negotiation", + value: "NEGOTIATION", + }, + { + label: "Lost", + value: "LOST", + }, + { + label: "Interested", + value: "INTERESTED", + }, + { + label: "Contacted", + value: "CONTACTED", + }, + { + label: "Churned", + value: "CHURNED", + }, +]; diff --git a/examples/app-crm/src/components/company/deals-table.tsx b/examples/app-crm/src/components/company/deals-table.tsx new file mode 100644 index 000000000000..821b33258354 --- /dev/null +++ b/examples/app-crm/src/components/company/deals-table.tsx @@ -0,0 +1,266 @@ +import { FC, useMemo } from "react"; +import { + EditButton, + FilterDropdown, + useSelect, + useTable, +} from "@refinedev/antd"; +import { useParams } from "react-router-dom"; +import { Button, Card, Input, Select, Skeleton, Space, Table } from "antd"; +import { Deal, DealAggregateResponse } from "../../interfaces/graphql"; +import { DealStageTag, Participants, Text } from "../../components"; +import { + AuditOutlined, + ExportOutlined, + PlusCircleOutlined, + SearchOutlined, +} from "@ant-design/icons"; +import { currencyNumber } from "../../utilities"; +import { useCustom, useNavigation } from "@refinedev/core"; +import { Link } from "react-router-dom"; + +type Props = { + style?: React.CSSProperties; +}; + +export const CompanyDealsTable: FC = ({ style }) => { + const { listUrl } = useNavigation(); + const params = useParams(); + + const { tableProps, filters, setFilters } = useTable({ + resource: "deals", + syncWithLocation: false, + sorters: { + initial: [ + { + field: "updatedAt", + order: "desc", + }, + ], + }, + filters: { + initial: [ + { + field: "title", + value: "", + operator: "contains", + }, + { + field: "stage.title", + value: "", + operator: "contains", + }, + ], + permanent: [ + { + field: "company.id", + operator: "eq", + value: params.id, + }, + ], + }, + meta: { + fields: [ + "id", + "title", + "value", + { stage: ["id", "title"] }, + { dealOwner: ["id", "name", "avatarUrl"] }, + { dealContact: ["id", "name", "avatarUrl"] }, + ], + }, + }); + + const { data: aggregate, isLoading: isLoadingDealAggregate } = useCustom<{ + dealAggregate: DealAggregateResponse[]; + }>({ + method: "post", + url: "/graphql", + meta: { + rawQuery: `query dealAggregate { + dealAggregate(filter: {companyId:{eq: ${Number(params.id)}}}) { + sum { + id + value + } + } + } + `, + }, + }); + + const { selectProps: selectPropsUsers } = useSelect({ + resource: "users", + optionLabel: "name", + pagination: { + mode: "off", + }, + meta: { + fields: ["id", "name"], + }, + }); + + const hasData = tableProps.loading + ? true + : tableProps?.dataSource?.length || 0 > 0; + + 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]); + + return ( + + + Deals + + {showResetFilters?.length > 0 && ( + + )} + + } + extra={ + <> + Total deal amount: + {isLoadingDealAggregate ? ( + + ) : ( + + {currencyNumber( + aggregate?.data.dealAggregate?.[0]?.sum + ?.value || 0, + )} + + )} + + } + > + {!hasData && ( + + No deals yet + + {" "} + Add deals through sales pipeline + + + )} + + {hasData && ( + + } + filterDropdown={(props) => ( + + + + )} + /> + + title="Deal amount" + dataIndex="value" + sorter + render={(_, record) => { + return ( + {currencyNumber(record.value || 0)} + ); + }} + /> + + title="Stage" + dataIndex={["stage", "title"]} + render={(_, record) => { + if (!record.stage) return null; + + return ; + }} + filterIcon={} + filterDropdown={(props) => ( + + + + )} + /> + + dataIndex={["dealOwnerId"]} + title="Participants" + render={(_, record) => { + return ( + + ); + }} + filterDropdown={(props) => { + return ( + +
+ )} +
+ ); +}; diff --git a/examples/app-crm/src/components/company/index.ts b/examples/app-crm/src/components/company/index.ts new file mode 100644 index 000000000000..912cc21d8f82 --- /dev/null +++ b/examples/app-crm/src/components/company/index.ts @@ -0,0 +1,8 @@ +export * from "./card-view"; +export * from "./table-view"; +export * from "./contacts-table"; +export * from "./deals-table"; +export * from "./quotes-table"; +export * from "./info-form"; +export * from "./title-form/title-form"; +export * from "./notes"; diff --git a/examples/app-crm/src/components/company/info-form.tsx b/examples/app-crm/src/components/company/info-form.tsx new file mode 100644 index 000000000000..6e571abb141f --- /dev/null +++ b/examples/app-crm/src/components/company/info-form.tsx @@ -0,0 +1,319 @@ +import { useState } from "react"; +import { useShow } from "@refinedev/core"; +import { Card, Input, InputNumber, Select, Space } from "antd"; +import { + ApiOutlined, + BankOutlined, + ColumnWidthOutlined, + DollarOutlined, + EnvironmentOutlined, + ShopOutlined, +} from "@ant-design/icons"; +import { + BusinessType, + Company, + CompanySize, + Industry, +} from "../../interfaces/graphql"; +import { SingleElementForm } from "../../components/single-element-form"; +import { Text } from "../../components/text"; +import { currencyNumber } from "../../utilities"; + +export const CompanyInfoForm = () => { + const [activeForm, setActiveForm] = useState< + | "totalRevenue" + | "industry" + | "companySize" + | "businessType" + | "country" + | "website" + >(); + + const { queryResult } = useShow({ + meta: { + fields: [ + "id", + "totalRevenue", + "industry", + "companySize", + "businessType", + "country", + "website", + ], + }, + }); + + const data = queryResult?.data?.data; + const { + totalRevenue, + industry, + companySize, + businessType, + country, + website, + } = data || {}; + + const getActiveForm = (args: { formName: keyof Company }) => { + const { formName } = args; + + if (activeForm === formName) { + return "form"; + } + + if (!data?.[formName]) { + return "empty"; + } + + return "view"; + }; + + const loading = queryResult?.isLoading; + + return ( + + + Company info + + } + headStyle={{ + padding: "1rem", + }} + bodyStyle={{ + padding: "0", + }} + style={{ + maxWidth: "500px", + }} + > + } + state={getActiveForm({ formName: "companySize" })} + itemProps={{ + name: "companySize", + label: "Company size", + }} + view={{companySize}} + onClick={() => setActiveForm("companySize")} + onUpdate={() => setActiveForm(undefined)} + onCancel={() => setActiveForm(undefined)} + > + + + } + state={getActiveForm({ formName: "businessType" })} + itemProps={{ + name: "businessType", + label: "Business type", + }} + view={{businessType}} + onClick={() => setActiveForm("businessType")} + onUpdate={() => setActiveForm(undefined)} + onCancel={() => setActiveForm(undefined)} + > + + + } + state={getActiveForm({ formName: "website" })} + itemProps={{ + name: "website", + label: "Website", + }} + view={{website}} + onClick={() => setActiveForm("website")} + onUpdate={() => setActiveForm(undefined)} + onCancel={() => setActiveForm(undefined)} + > + + + + ); +}; + +const companySizeOptions: { + label: string; + value: CompanySize; +}[] = [ + { + label: "Enterprise", + value: "ENTERPRISE", + }, + { + label: "Large", + value: "LARGE", + }, + { + label: "Medium", + value: "MEDIUM", + }, + { + label: "Small", + value: "SMALL", + }, +]; + +const industryOptions: { + label: string; + value: Industry; +}[] = [ + { label: "Aerospace", value: "AEROSPACE" }, + { label: "Agriculture", value: "AGRICULTURE" }, + { label: "Automotive", value: "AUTOMOTIVE" }, + { label: "Chemicals", value: "CHEMICALS" }, + { label: "Construction", value: "CONSTRUCTION" }, + { label: "Defense", value: "DEFENSE" }, + { label: "Education", value: "EDUCATION" }, + { label: "Energy", value: "ENERGY" }, + { label: "Financial Services", value: "FINANCIAL_SERVICES" }, + { label: "Food and Beverage", value: "FOOD_AND_BEVERAGE" }, + { label: "Government", value: "GOVERNMENT" }, + { label: "Healthcare", value: "HEALTHCARE" }, + { label: "Hospitality", value: "HOSPITALITY" }, + { label: "Industrial Manufacturing", value: "INDUSTRIAL_MANUFACTURING" }, + { label: "Insurance", value: "INSURANCE" }, + { label: "Life Sciences", value: "LIFE_SCIENCES" }, + { label: "Logistics", value: "LOGISTICS" }, + { label: "Media", value: "MEDIA" }, + { label: "Mining", value: "MINING" }, + { label: "Nonprofit", value: "NONPROFIT" }, + { label: "Other", value: "OTHER" }, + { label: "Pharmaceuticals", value: "PHARMACEUTICALS" }, + { label: "Professional Services", value: "PROFESSIONAL_SERVICES" }, + { label: "Real Estate", value: "REAL_ESTATE" }, + { label: "Retail", value: "RETAIL" }, + { label: "Technology", value: "TECHNOLOGY" }, + { label: "Telecommunications", value: "TELECOMMUNICATIONS" }, + { label: "Transportation", value: "TRANSPORTATION" }, + { label: "Utilities", value: "UTILITIES" }, +]; + +const businessTypeOptions: { + label: string; + value: BusinessType; +}[] = [ + { + label: "B2B", + value: "B2B", + }, + { + label: "B2C", + value: "B2C", + }, + { + label: "B2G", + value: "B2G", + }, +]; diff --git a/examples/app-crm/src/components/company/notes.tsx b/examples/app-crm/src/components/company/notes.tsx new file mode 100644 index 000000000000..af4b20b02739 --- /dev/null +++ b/examples/app-crm/src/components/company/notes.tsx @@ -0,0 +1,315 @@ +import { FC } from "react"; +import { + useGetIdentity, + HttpError, + useParsed, + useList, + useInvalidate, +} from "@refinedev/core"; +import { useForm, DeleteButton } from "@refinedev/antd"; +import { Form, Space, Typography, Input, Button, Card } from "antd"; +import { useParams } from "react-router-dom"; +import dayjs from "dayjs"; +import { Text } from "../text"; +import { CustomAvatar } from "../custom-avatar"; +import { CompanyNote, User } from "../../interfaces/graphql"; +import { TextIcon } from "../icon"; + +type Props = { + style?: React.CSSProperties; +}; + +export const CompanyNotes: FC = ({ style }) => { + return ( + + + Notes + + } + style={style} + > + + + + ); +}; + +export const CompanyNoteForm = () => { + const { id: companyId } = useParsed(); + + const { data: me } = useGetIdentity(); + + const { formProps, onFinish, form } = useForm({ + action: "create", + resource: "companyNotes", + queryOptions: { + enabled: false, + }, + redirect: false, + mutationMode: "optimistic", + }); + + const handleOnFinish = async (values: CompanyNote) => { + if (!companyId) { + return; + } + + const note = values.note.trim(); + if (!note) { + return; + } + + try { + await onFinish({ + ...values, + companyId: companyId, + }); + + form.resetFields(); + } catch (error) {} + }; + + return ( +
+ +
handleOnFinish(values as CompanyNote)} + > + .<,-]+$/i, + ), + message: "Please enter a note", + }, + ]} + > + + +
+
+ ); +}; + +export const CompanyNoteList = () => { + const params = useParams(); + + const invalidate = useInvalidate(); + + const { data: notes } = useList({ + resource: "companyNotes", + sorters: [ + { + field: "updatedAt", + order: "desc", + }, + ], + filters: [{ field: "company.id", operator: "eq", value: params.id }], + meta: { + fields: [ + "id", + "note", + "createdAt", + { createdBy: ["id", "name", "updatedAt", "avatarUrl"] }, + ], + }, + }); + + const { formProps, setId, id, saveButtonProps } = useForm< + CompanyNote, + HttpError, + CompanyNote + >({ + resource: "companyNotes", + action: "edit", + queryOptions: { + enabled: false, + }, + mutationMode: "optimistic", + onMutationSuccess: () => { + setId(undefined); + invalidate({ + invalidates: ["list"], + resource: "companyNotes", + }); + }, + }); + + const { data: me } = useGetIdentity(); + + return ( + + {notes?.data?.map((item) => { + const isMe = me?.id === item.createdBy.id; + + return ( +
+ + +
+
+ + {item.createdBy.name} + + + {dayjs(item.createdAt).format( + "MMMM D, YYYY - h:ma", + )} + +
+ + {id === item.id ? ( +
+ .<,-]+$/i, + ), + message: "Please enter a note", + }, + ]} + > + + +
+ ) : ( + + {item.note} + + )} + + {isMe && !id && ( + + setId(item.id)} + > + Edit + + + + )} + + {id === item.id && ( + + + + + )} +
+
+ ); + })} +
+ ); +}; diff --git a/examples/app-crm/src/components/company/quotes-table.tsx b/examples/app-crm/src/components/company/quotes-table.tsx new file mode 100644 index 000000000000..1d7c45680a3f --- /dev/null +++ b/examples/app-crm/src/components/company/quotes-table.tsx @@ -0,0 +1,249 @@ +import { FC, useMemo } from "react"; +import { + FilterDropdown, + ShowButton, + useSelect, + useTable, +} from "@refinedev/antd"; +import { Link, useParams } from "react-router-dom"; +import { Button, Card, Input, Select, Space, Table } from "antd"; +import { Quote, QuoteStatus } from "../../interfaces/graphql"; +import { QuoteStatusTag, Participants, Text } from "../../components"; +import { + ContainerOutlined, + ExportOutlined, + PlusCircleOutlined, + SearchOutlined, +} from "@ant-design/icons"; +import { currencyNumber } from "../../utilities"; +import { useNavigation } from "@refinedev/core"; + +type Props = { + style?: React.CSSProperties; +}; + +export const CompanyQuotesTable: FC = ({ style }) => { + const { listUrl } = useNavigation(); + const params = useParams(); + + const { tableProps, filters, setFilters } = useTable({ + 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 ( + + + Quotes + + {showResetFilters?.length > 0 && ( + + )} + + } + > + {!hasData && ( + + No quotes yet + + {" "} + Add quotes + + + )} + {hasData && ( + + } + 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 ( + +
+ )}{" "} +
+ ); +}; + +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; @@ -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, - })} - + /> = ({ dataIndex={"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 ( +
+ + + {getNameInitials(company?.name || "")} + + + + { + return onFinish?.({ + name: value, + }); + }} + /> + + { + onFinish?.({ + salesOwnerId: value, + }); + }} + /> + + +
+ ); +}; + +const TitleInput = ({ + value, + onChange, + loading, +}: { + // value is set by + value?: string; + onChange?: (value: string) => void; + loading?: boolean; +}) => { + return ( + , + }} + > + {loading ? ( + + ) : ( + value + )} + + ); +}; + +const SalesOwnerInput = ({ + salesOwner, + onChange, + loading, +}: { + onChange?: (value: string) => void; + salesOwner?: Company["salesOwner"]; + loading?: boolean; +}) => { + const [isEdit, setIsEdit] = useState(false); + + const { selectProps, queryResult } = useSelect({ + resource: "users", + optionLabel: "name", + pagination: { + mode: "off", + }, + meta: { + fields: ["id", "name", "avatarUrl"], + }, + }); + + return ( +
{ + setIsEdit(true); + }} + > + + Sales Owner: + + {loading && ( + + )} + {!isEdit && !loading && ( + <> + + {getNameInitials(salesOwner?.name || "")} + + {salesOwner?.name} + + } > + } + placeholder="Search by name" + onChange={debouncedOnChange} + /> + + + onViewChange(e.target.value)} + > + + + + + + + + + ); + }} + contentProps={{ + style: { + marginTop: "28px", + }, + }} + title={ + + } + > + {view === "table" ? ( + + ) : ( + { + setCurrent(page); + setPageSize(pageSize); + }, + }} + /> + )} + + {children} + ); }; diff --git a/examples/app-crm/src/routes/companies/show.tsx b/examples/app-crm/src/routes/companies/show.tsx deleted file mode 100644 index 730bced70503..000000000000 --- a/examples/app-crm/src/routes/companies/show.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import { ListButton } from "@refinedev/antd"; - -export const CompanyShowPage = () => { - return ( -
- company details page - -
- ); -}; diff --git a/examples/app-crm/src/routes/contacts/company-create.tsx b/examples/app-crm/src/routes/contacts/company-create.tsx deleted file mode 100644 index 08da9669add1..000000000000 --- a/examples/app-crm/src/routes/contacts/company-create.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import { useGetIdentity, useGetToPath } from "@refinedev/core"; -import { useForm, useSelect } from "@refinedev/antd"; -import { Button, Form, Input, Modal, Select } from "antd"; -import { PlusCircleOutlined } from "@ant-design/icons"; - -import { Company, User } from "../../interfaces/graphql"; -import { SelectOptionWithAvatar } from "../../components/select-option-with-avatar"; - -export const ContactCompanyCreatePage = () => { - const navigate = useNavigate(); - const getToPath = useGetToPath(); - const { data: user } = useGetIdentity(); - const { formProps, saveButtonProps, onFinish } = useForm({ - redirect: "list", - }); - const { selectProps, queryResult } = useSelect({ - resource: "companies", - optionLabel: "name", - meta: { - fields: ["id", "name", "avatarUrl"], - }, - }); - - return ( - { - navigate( - getToPath({ - action: "list", - }) ?? "", - { - replace: true, - }, - ); - }} - okText="Save" - okButtonProps={{ - ...saveButtonProps, - }} - width={560} - > - comapny create form - - ); -}; diff --git a/examples/app-crm/src/routes/contacts/index.ts b/examples/app-crm/src/routes/contacts/index.ts new file mode 100644 index 000000000000..9b44e51145fc --- /dev/null +++ b/examples/app-crm/src/routes/contacts/index.ts @@ -0,0 +1,4 @@ +export * from "./wrapper"; +export * from "./show"; +export * from "./create"; +export * from "./edit"; diff --git a/examples/app-crm/src/routes/contacts/show/index.module.css b/examples/app-crm/src/routes/contacts/show/index.module.css index a042fea47230..f90b68f8fb55 100644 --- a/examples/app-crm/src/routes/contacts/show/index.module.css +++ b/examples/app-crm/src/routes/contacts/show/index.module.css @@ -1,14 +1,43 @@ +.header { + display: flex; + justify-content: flex-end; + align-items: center; + background-color: #fff; + padding: 16px; +} + .container { + padding: 24px; + + .title { + display: block; + height: 2.5rem; + margin: 0 !important; + + .titleEditIcon { + visibility: hidden; + opacity: 0; + transition: all 0.2s ease-in-out; + } + + &:hover { + .titleEditIcon { + opacity: 1; + visibility: visible !important; + } + } + } + .name { display: flex; align-items: center; - justify-content: start; + justify-content: flex-start; padding: 1rem; } .form { background-color: #fff; - border-radius: .5rem; + border-radius: 0.5rem; margin-bottom: 1.75rem; } @@ -23,4 +52,4 @@ display: flex; justify-content: space-between; } -} \ No newline at end of file +} diff --git a/examples/app-crm/src/routes/contacts/show/index.tsx b/examples/app-crm/src/routes/contacts/show/index.tsx index 71a0c563c6a1..64227ea9c647 100644 --- a/examples/app-crm/src/routes/contacts/show/index.tsx +++ b/examples/app-crm/src/routes/contacts/show/index.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import { - Avatar, Button, Card, Drawer, @@ -20,19 +19,23 @@ import { PhoneOutlined, IdcardOutlined, DeleteOutlined, + CloseOutlined, + EditOutlined, } from "@ant-design/icons"; import { useSelect } from "@refinedev/antd"; import dayjs from "dayjs"; -import { Text } from "../../../components/text"; -import { SingleElementForm } from "../../../components/single-element-form"; -import { ContactStatus } from "../../../components/contact/status"; +import { + Text, + CustomAvatar, + SingleElementForm, + SelectOptionWithAvatar, +} from "../../../components"; +import { ContactStatus, ContactComment } from "../../../components/contact"; import { TextIcon } from "../../../components/icon"; -import { ContactComment } from "../../../components/contact/comment"; -import { SelectOptionWithAvatar } from "../../../components/select-option-with-avatar"; import { Timezone } from "../../../enums/timezone"; - import type { Company, Contact, User } from "../../../interfaces/graphql"; + import styles from "./index.module.css"; const timezoneOptions = Object.keys(Timezone).map((key) => ({ @@ -92,39 +95,86 @@ export const ContactShowPage = () => { optionLabel: "name", }); + const closeModal = () => { + setActiveForm(undefined); + navigate( + getToPath({ + action: "list", + }) ?? "", + { + replace: true, + }, + ); + }; + const { data, isLoading, isError } = queryResult; - const renderContent = () => { - if (isError) { - return null; - } - if (isLoading) { - return ; - } + if (isError) { + closeModal(); + return null; + } - const { - id, - name, - email, - jobTitle, - phone, - timezone, - avatarUrl, - company, - createdAt, - salesOwner, - } = data.data; + if (isLoading) { return ( + + + + ); + } + + const { + id, + name, + email, + jobTitle, + phone, + timezone, + avatarUrl, + company, + createdAt, + salesOwner, + } = data?.data ?? {}; + + return ( + closeModal()} + width={756} + bodyStyle={{ background: "#f5f5f5", padding: 0 }} + headerStyle={{ display: "none" }} + > +
+
- { values: { name: value, }, + successNotification: false, }); }, + triggerType: ["text", "icon"], + icon: ( + + ), }} > {name} @@ -143,7 +200,7 @@ export const ContactShowPage = () => {
} + icon={} state={ activeForm && activeForm === "email" ? "form" @@ -164,7 +221,7 @@ export const ContactShowPage = () => { } + icon={} state={ activeForm && activeForm === "companyId" ? "form" @@ -178,7 +235,10 @@ export const ContactShowPage = () => { }} view={ - + {company.name} } @@ -186,8 +246,6 @@ export const ContactShowPage = () => { onCancel={() => setActiveForm(undefined)} onUpdate={() => { setActiveForm(undefined); - // save sales owner - console.log("salesOwnerId", salesOwnerId); updateMutation({ resource: "contacts", id, @@ -254,7 +312,7 @@ export const ContactShowPage = () => { /> } + icon={} state={ activeForm && activeForm === "jobTitle" ? "form" @@ -274,7 +332,7 @@ export const ContactShowPage = () => { } + icon={} state={ activeForm && activeForm === "phone" ? "form" @@ -295,7 +353,7 @@ export const ContactShowPage = () => { } + icon={} state={ activeForm && activeForm === "timezone" ? "form" @@ -353,16 +411,7 @@ export const ContactShowPage = () => { resource: "contacts", }, { - onSuccess: () => { - navigate( - getToPath({ - action: "list", - }) ?? "", - { - replace: true, - }, - ); - }, + onSuccess: () => closeModal(), }, ); }} @@ -375,28 +424,6 @@ export const ContactShowPage = () => {
- ); - }; - - return ( - { - navigate( - getToPath({ - action: "list", - }) ?? "", - { - replace: true, - }, - ); - }} - width={756} - bodyStyle={{ - background: "#f5f5f5", - }} - > - {renderContent()} ); }; diff --git a/examples/app-crm/src/routes/contacts/wrapper/index.tsx b/examples/app-crm/src/routes/contacts/wrapper/index.tsx index 42aedfb7da2f..0a53f303c48d 100644 --- a/examples/app-crm/src/routes/contacts/wrapper/index.tsx +++ b/examples/app-crm/src/routes/contacts/wrapper/index.tsx @@ -1,268 +1,21 @@ import React from "react"; -import { - CreateButton, - DeleteButton, - FilterDropdown, - SaveButton, - ShowButton, - getDefaultSortOrder, - useSelect, - useTable, -} from "@refinedev/antd"; -import { - Form, - Input, - Radio, - Select, - Space, - Table, - Pagination, - Row, - Col, - TableProps, - Avatar, - Button, -} from "antd"; +import { HttpError } from "@refinedev/core"; +import { CreateButton, SaveButton, useTable } from "@refinedev/antd"; +import { Form, Input, Radio } from "antd"; import { UnorderedListOutlined, AppstoreOutlined, PlusSquareOutlined, SearchOutlined, - PhoneOutlined, } from "@ant-design/icons"; -import { useNavigate } from "react-router-dom"; -import { - CrudFilters, - CrudSorting, - getDefaultFilter, - useGetToPath, -} from "@refinedev/core"; import debounce from "lodash/debounce"; -import { ContactStatusTag } from "../../../components/contact/status-tag"; -import { ContactStatusEnum } from "../../../enums/contact-status"; -import { ContactCard } from "../../../components/contact/card"; -import { Text } from "../../../components"; - +import { TableView, CardView } from "../../../components/contact"; import { Contact } from "../../../interfaces/graphql"; + import styles from "./index.module.css"; type Props = React.PropsWithChildren<{}>; -type TableViewProps = TableProps & { - filters?: CrudFilters; - sorters?: CrudSorting; -}; -type CardViewProps = TableProps & { - setCurrent: (current: number) => void; - setPageSize: (pageSize: number) => void; -}; - -const statusOptions = Object.keys(ContactStatusEnum).map((key) => ({ - label: `${key[0]}${key.slice(1).toLowerCase()}`, - value: ContactStatusEnum[key as keyof typeof ContactStatusEnum], -})); - -const TableView: React.FC = ({ filters, sorters, ...rest }) => { - const { selectProps } = useSelect({ - resource: "companies", - optionLabel: "name", - meta: { - fields: ["id", "name"], - }, - }); - - return ( - { - return ( - - {total}{" "} - contacts in total - - ); - }, - }} - rowKey="id" - > - ( - - - - )} - render={(_, record: Contact) => { - return ( - - - {record.name} - - ); - }} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - render={(value: ContactStatusEnum) => ( - - )} - /> - - title="Actions" - dataIndex="actions" - render={(_, record) => ( - - -
- ); -}; - -const CardView: React.FC = ({ - dataSource, - pagination, - setCurrent, - setPageSize, -}) => { - const navigate = useNavigate(); - const getToPath = useGetToPath(); - - return ( -
- - {dataSource?.map((contact) => ( - - { - if (key === "show") { - navigate( - getToPath({ - action: "show", - meta: { - id: contact.id, - }, - }) ?? "", - { - replace: true, - }, - ); - } - }} - contact={contact} - /> - - ))} - - - { - return ( - - {total}{" "} - contacts in total - - ); - }} - onChange={(page, pageSize) => { - setCurrent(page); - setPageSize(pageSize); - }} - /> -
- ); -}; export const ContactsPageWrapper: React.FC = ({ children }) => { const [type, setType] = React.useState("table"); @@ -274,7 +27,7 @@ export const ContactsPageWrapper: React.FC = ({ children }) => { setPageSize, filters, sorters, - } = useTable({ + } = useTable({ sorters: { initial: [ { @@ -312,7 +65,7 @@ export const ContactsPageWrapper: React.FC = ({ children }) => { }, ], }, - onSearch: (values: any) => { + onSearch: (values) => { return [ { field: "name", @@ -383,15 +136,15 @@ export const ContactsPageWrapper: React.FC = ({ children }) => { {type === "table" ? ( ) : ( )} {children} diff --git a/examples/app-crm/src/routes/dashboard/index.tsx b/examples/app-crm/src/routes/dashboard/index.tsx index 7ba8fdeb0c71..91242a602701 100644 --- a/examples/app-crm/src/routes/dashboard/index.tsx +++ b/examples/app-crm/src/routes/dashboard/index.tsx @@ -1,15 +1,24 @@ import React from "react"; -import { Row, Col } from "antd"; -import { ProjectOutlined, TeamOutlined } from "@ant-design/icons"; +import { Row, Col, Button } from "antd"; +import { + ProjectOutlined, + RightCircleOutlined, + TeamOutlined, +} from "@ant-design/icons"; +import { useNavigation } from "@refinedev/core"; -import { DashboardTotalCountCard } from "../../components/dashboard/total-count-card"; -import { DashboardTasksChart } from "../../components/dashboard/tasks-chart"; -import { DashboardDealsChart } from "../../components/dashboard/deals-chart"; -import { DashboardTotalRevenueChart } from "../../components/dashboard/total-revenue-chart"; -import { CalendarUpcomingEvents } from "../../components/calender/upcoming-events"; -import { DashboardLatestActivities } from "../../components/dashboard/latest-activities"; +import { + DashboardTotalCountCard, + DashboardTasksChart, + DashboardDealsChart, + DashboardTotalRevenueChart, + DashboardLatestActivities, +} from "../../components/dashboard"; +import { CalendarUpcomingEvents } from "../../components/calendar"; export const DashboardPage: React.FC = () => { + const { list } = useNavigation(); + return ( @@ -57,7 +66,19 @@ export const DashboardPage: React.FC = () => { - + list("events")} + icon={} + > + See calendar + + ), + }} + /> diff --git a/examples/app-crm/src/routes/quotes/create.tsx b/examples/app-crm/src/routes/quotes/create.tsx index 684e8396870f..d69baf46046d 100644 --- a/examples/app-crm/src/routes/quotes/create.tsx +++ b/examples/app-crm/src/routes/quotes/create.tsx @@ -1,5 +1,11 @@ +import { FC, PropsWithChildren } from "react"; import { QuotesFormModal } from "../../components"; -export const QuotesCreatePage = () => { - return ; +export const QuotesCreatePage: FC = ({ children }) => { + return ( + <> + + {children} + + ); }; diff --git a/examples/app-crm/src/routes/quotes/edit.tsx b/examples/app-crm/src/routes/quotes/edit.tsx index fe8936ea58e6..a2013a950245 100644 --- a/examples/app-crm/src/routes/quotes/edit.tsx +++ b/examples/app-crm/src/routes/quotes/edit.tsx @@ -1,5 +1,11 @@ +import { FC, PropsWithChildren } from "react"; import { QuotesFormModal } from "../../components"; -export const QuotesEditPage = () => { - return ; +export const QuotesEditPage: FC = ({ children }) => { + return ( + <> + + {children} + + ); }; diff --git a/examples/app-crm/src/routes/quotes/list.tsx b/examples/app-crm/src/routes/quotes/list.tsx index dbc7f8773230..87fcb347a733 100644 --- a/examples/app-crm/src/routes/quotes/list.tsx +++ b/examples/app-crm/src/routes/quotes/list.tsx @@ -1,4 +1,5 @@ import { FC, PropsWithChildren } from "react"; +import { HttpError, getDefaultFilter } from "@refinedev/core"; import { CreateButton, DeleteButton, @@ -10,17 +11,13 @@ import { useSelect, useTable, } from "@refinedev/antd"; -import { Avatar, Form, Input, Select, Space, Table, Tooltip } from "antd"; -import { PlusCircleOutlined, PlusSquareOutlined } from "@ant-design/icons"; -import { Quote, QuoteFilter, QuoteStatus } from "../../interfaces/graphql"; -import { - currencyNumber, - getNameInitials, - getRandomColorFromString, -} from "../../utilities"; -import { Text, QuoteStatusTag } from "../../components"; +import { Form, Input, Select, Space, Table } from "antd"; +import { PlusSquareOutlined } from "@ant-design/icons"; import dayjs from "dayjs"; -import { HttpError, getDefaultFilter } from "@refinedev/core"; +import { Text, QuoteStatusTag, CustomAvatar } from "../../components"; +import { currencyNumber } from "../../utilities"; +import { Quote, QuoteFilter, QuoteStatus } from "../../interfaces/graphql"; +import { Participants } from "../../components/participants"; const statusOptions: { label: string; value: QuoteStatus }[] = [ { @@ -43,6 +40,7 @@ export const QuotesListPage: FC = ({ children }) => { HttpError, QuoteFilter >({ + resource: "quotes", onSearch: (values) => { return [ { @@ -81,7 +79,7 @@ export const QuotesListPage: FC = ({ children }) => { "status", "total", "createdAt", - { company: ["id", "name"] }, + { company: ["id", "name", "avatarUrl"] }, { contact: ["id", "name", "avatarUrl"] }, { salesOwner: ["id", "name", "avatarUrl"] }, ], @@ -202,25 +200,11 @@ export const QuotesListPage: FC = ({ children }) => { render={(_, record) => { return ( - - {getNameInitials({ - name: record.company.name, - })} - - + + {record.company.name} @@ -230,6 +214,7 @@ export const QuotesListPage: FC = ({ children }) => { { return ( = ({ children }) => { )} render={(value) => { - return ; + return ; }} /> @@ -279,50 +264,11 @@ export const QuotesListPage: FC = ({ children }) => { ); }} render={(_, record) => { - const salesOwnerName = record.salesOwner?.name; - const contactName = record.contact?.name; - return ( - - - - {getNameInitials({ - name: salesOwnerName, - })} - - - - - - {getNameInitials({ - name: contactName, - })} - - - + ); }} /> diff --git a/examples/app-crm/src/routes/quotes/show/index.tsx b/examples/app-crm/src/routes/quotes/show/index.tsx index df87fef194f0..1e94672fb306 100644 --- a/examples/app-crm/src/routes/quotes/show/index.tsx +++ b/examples/app-crm/src/routes/quotes/show/index.tsx @@ -1,17 +1,21 @@ +import { useState } from "react"; import { useOne } from "@refinedev/core"; import { Link, useParams } from "react-router-dom"; -import { Avatar, Button, Space } from "antd"; +import { Button, Space } from "antd"; import { EditOutlined, LeftOutlined } from "@ant-design/icons"; -import { FullScreenLoading, QuotesFormModal, Text } from "../../../components"; -import { Status } from "./status"; -import { ProductsServices } from "./products-services"; -import { getNameInitials, getRandomColorFromString } from "../../../utilities"; + +import { FullScreenLoading, Text, CustomAvatar } from "../../../components"; +import { + QuotesFormModal, + ProductsServices, + ShowDescription, + PdfExport, + StatusIndicator, +} from "../../../components/quotes"; + import { Quote } from "../../../interfaces/graphql"; import styles from "./index.module.css"; -import { useState } from "react"; -import { Description } from "./description"; -import { PdfExport } from "./pdf-export"; export const QuotesShowPage = () => { const [editModalVisible, setEditModalVisible] = useState(false); @@ -41,7 +45,7 @@ export const QuotesShowPage = () => { ], }, { - company: ["id", "name", "country", "website"], + company: ["id", "name", "country", "website", "avatarUrl"], }, { salesOwner: ["id", "name"], @@ -86,7 +90,7 @@ export const QuotesShowPage = () => {
- {
- - {getNameInitials({ - name: company?.name ?? "", - })} - + />
{company.name} {company.country} @@ -131,7 +128,7 @@ export const QuotesShowPage = () => {
- +
{editModalVisible && ( diff --git a/examples/app-crm/src/styles/antd.css b/examples/app-crm/src/styles/antd.css index 74ed82fd6609..685aefdcaea9 100644 --- a/examples/app-crm/src/styles/antd.css +++ b/examples/app-crm/src/styles/antd.css @@ -74,3 +74,11 @@ color: rgba(0, 0, 0, 0.45) !important; margin-inline-end: auto !important; } + +.ant-color-picker { + .ant-popover-content { + .ant-collapse-header { + display: none; + } + } +} diff --git a/examples/app-crm/src/styles/index.css b/examples/app-crm/src/styles/index.css index 88e4ff29b467..0d3f1f42f6cf 100644 --- a/examples/app-crm/src/styles/index.css +++ b/examples/app-crm/src/styles/index.css @@ -41,7 +41,9 @@ line-height: 1.11; } } -.ant-text, + +.ant-typography, +.ant-text, .anticon { &.primary { color: rgba(0, 0, 0, 0.85); diff --git a/examples/app-crm/src/utilities/get-name-initials.ts b/examples/app-crm/src/utilities/get-name-initials.ts index 153fd40af59a..ce1da1e9774f 100644 --- a/examples/app-crm/src/utilities/get-name-initials.ts +++ b/examples/app-crm/src/utilities/get-name-initials.ts @@ -1,10 +1,4 @@ -export const getNameInitials = ({ - name, - count = 2, -}: { - name: string; - count?: number; -}) => { +export const getNameInitials = (name: string, count = 2) => { const initials = name .split(" ") .map((n) => n[0])