From f322b814b12f106c3be44412ce314bf563c8912f Mon Sep 17 00:00:00 2001 From: trevor-anderson Date: Tue, 20 Feb 2024 11:19:46 -0500 Subject: [PATCH] feat: mv pages/Invoices/ListView to pages/InvoicesListView, update ListView args --- src/pages/Invoices/ListView/ListItem.tsx | 117 -------------- src/pages/Invoices/ListView/ListView.tsx | 86 ---------- src/pages/Invoices/ListView/index.ts | 1 - src/pages/Invoices/ListView/tableProps.tsx | 103 ------------ .../InvoicesListView.stories.tsx | 58 +++++++ .../InvoicesListView/InvoicesListView.tsx | 147 ++++++++++++++++++ src/pages/InvoicesListView/index.ts | 2 + src/pages/InvoicesListView/tableProps.tsx | 93 +++++++++++ 8 files changed, 300 insertions(+), 307 deletions(-) delete mode 100644 src/pages/Invoices/ListView/ListItem.tsx delete mode 100644 src/pages/Invoices/ListView/ListView.tsx delete mode 100644 src/pages/Invoices/ListView/index.ts delete mode 100644 src/pages/Invoices/ListView/tableProps.tsx create mode 100644 src/pages/InvoicesListView/InvoicesListView.stories.tsx create mode 100644 src/pages/InvoicesListView/InvoicesListView.tsx create mode 100644 src/pages/InvoicesListView/index.ts create mode 100644 src/pages/InvoicesListView/tableProps.tsx diff --git a/src/pages/Invoices/ListView/ListItem.tsx b/src/pages/Invoices/ListView/ListItem.tsx deleted file mode 100644 index 4c548717..00000000 --- a/src/pages/Invoices/ListView/ListItem.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import dayjs from "dayjs"; -import { styled } from "@mui/material/styles"; -import Text from "@mui/material/Typography"; -import { - CoreListItemLayout, - type CoreListItemLayoutProps, -} from "@layouts/CoreItemsListView/CoreListItemLayout"; -import { coreListItemLayoutClassNames } from "@layouts/CoreItemsListView/classNames"; -import { formatNum } from "@utils/formatNum"; -import type { Invoice } from "@graphql/types"; - -export const InvoicesListItem = ({ listName, item, onClick, ...props }: InvoicesListItemProps) => { - if (!listName || !item || !onClick) return null; - - const isInboxList = listName === "Inbox"; - const { createdBy, assignedTo, status, amount, createdAt } = item; - const userToDisplay = isInboxList ? createdBy : assignedTo; - - const prettyContactName = userToDisplay.profile?.displayName ?? userToDisplay.handle; - const prettyAmount = formatNum.toCurrencyRoundedStr(amount); - const prettyCreatedAt = dayjs(createdAt).format("MMM D"); - const prettyStatus = status.replace(/_/g, " "); - - return ( - -
- {prettyContactName} -
- -
- {prettyAmount} -
- -
- - {prettyCreatedAt} - - - {prettyStatus} - -
-
- ); -}; - -export const invoicesListItemClassNames = { - middleContentContainer: "inv-list-item-middle-content-container", - amountContainer: "inv-list-item-amount-container", - amountText: "inv-list-item-amount-text", - rightContentContainer: "inv-list-item-right-content-container", -}; - -const StyledInvoicesListItem = styled(CoreListItemLayout)(({ theme }) => ({ - // On mobile, shrink the avatar and its container to 2.5rem wide - [`& .${coreListItemLayoutClassNames.leftContentContainer}`]: { - ...(theme.variables.isMobilePageLayout && { - width: "2.5rem !important", - minWidth: "2.5rem !important", - maxWidth: "2.5rem !important", - }), - - [`& .${coreListItemLayoutClassNames.avatar}`]: { - ...(theme.variables.isMobilePageLayout && { - height: "2.5rem !important", - width: "2.5rem !important", - }), - }, - }, - - // INVOICE LIST ITEM CHILDREN - - [`& .${coreListItemLayoutClassNames.childrenContentContainer}`]: { - [`& > .${invoicesListItemClassNames.middleContentContainer}`]: { - maxHeight: "2.5rem", // ensures no 3rd line of text is partially visible - justifyContent: "center !important", - - "& > .MuiTypography-root": { - whiteSpace: "normal", - }, - }, - - [`& > .${invoicesListItemClassNames.amountContainer}`]: { - width: "4rem", - flexShrink: 0, - textAlign: "right", - marginLeft: "auto !important", - justifyContent: "center !important", - }, - - [`& > .${invoicesListItemClassNames.rightContentContainer}`]: { - flexShrink: 0, - ...(theme.variables.isMobilePageLayout - ? { - width: "4rem !important", - minWidth: "4rem !important", - maxWidth: "4rem !important", - } - : { - width: "4.5rem !important", - minWidth: "4.5rem !important", - maxWidth: "4.5rem !important", - }), - }, - }, -})); - -export type InvoicesListItemProps = { - listName?: "Inbox" | "Sent"; - item?: Invoice; - onClick?: CoreListItemLayoutProps["onClick"]; -}; diff --git a/src/pages/Invoices/ListView/ListView.tsx b/src/pages/Invoices/ListView/ListView.tsx deleted file mode 100644 index 3d18fc54..00000000 --- a/src/pages/Invoices/ListView/ListView.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useQuery } from "@apollo/client/react/hooks"; -import { CreateItemButton } from "@components/Buttons/CreateItemButton"; -import { EmptyListFallback, type EmptyListFallbackProps } from "@components/HelpInfo"; -import { FileInvoiceDollarIcon } from "@components/Icons/FileInvoiceDollarIcon"; -import { Error } from "@components/Indicators/Error"; -import { Loading } from "@components/Indicators/Loading"; -import { QUERIES } from "@graphql/queries"; -import { CoreItemsListView, type ListViewRenderItemFn } from "@layouts/CoreItemsListView"; -import { InvoicesListItem } from "./ListItem"; -import { invoiceTableProps } from "./tableProps"; - -export const InvoicesListView = () => { - const { data, loading, error } = useQuery(QUERIES.MY_INVOICES); - - return loading ? ( - - ) : error ? ( - - ) : ( - - } - lists={[ - { - listName: "Inbox", - items: data?.myInvoices.assignedToUser ?? [], - emptyListFallback: ( - - ), - }, - { - listName: "Sent", - items: data?.myInvoices.createdByUser ?? [], - emptyListFallback: ( - - ), - }, - ]} - tableProps={{ - ...invoiceTableProps, - rows: [ - ...(data?.myInvoices.createdByUser.map((inv) => ({ - isItemOwnedByUser: true, - ...inv, - })) ?? []), - ...(data?.myInvoices.assignedToUser.map((inv) => ({ - isItemOwnedByUser: false, - ...inv, - })) ?? []), - ], - noRowsOverlayProps: { - backgroundIcon: , - }, - }} - sx={(theme) => ({ - "& a": { - color: `${theme.palette.secondary.main} !important`, // "View Work Order" links - }, - })} - /> - ); -}; - -const InvoicesEmptyListFallback = ({ - backgroundIcon = , - ...props -}: Partial) => ( - -); - -const renderInvoicesListItem: ListViewRenderItemFn = (props) => ; diff --git a/src/pages/Invoices/ListView/index.ts b/src/pages/Invoices/ListView/index.ts deleted file mode 100644 index 3e3f99fc..00000000 --- a/src/pages/Invoices/ListView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { InvoicesListView } from "./ListView"; diff --git a/src/pages/Invoices/ListView/tableProps.tsx b/src/pages/Invoices/ListView/tableProps.tsx deleted file mode 100644 index c9f32b44..00000000 --- a/src/pages/Invoices/ListView/tableProps.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { ContactAvatar } from "@components/Avatar/ContactAvatar"; -import { Link } from "@components/Navigation/Link"; -import { getDateAndTime } from "@utils/dateTime"; -import { formatNum } from "@utils/formatNum"; -import type { Invoice } from "@graphql/types"; -import type { DataGridProps, GridColDef } from "@mui/x-data-grid"; -import type { Except } from "type-fest"; - -type ColumnFieldKeys = "listName" | keyof Invoice; - -const COLUMNS = Object.fromEntries( - Object.entries( - { - listName: { - headerName: "Sent/Received", - valueGetter: ({ row: inv }) => (inv.isItemOwnedByUser === true ? "Sent" : "Received"), - minWidth: 115, - headerAlign: "left", - align: "center", - }, - createdBy: { - headerName: "Created By", - valueGetter: ({ row: inv }) => inv.createdBy.profile?.displayName || inv.createdBy.handle, - valueFormatter: ({ value }) => value, // <-- necessary for export/print on cols with renderCell - renderCell: ({ row: inv }) => ( - - ), - flex: 1, - minWidth: 175, - }, - assignedTo: { - headerName: "Assigned To", - valueGetter: ({ row: inv }) => inv.assignedTo.profile?.displayName || inv.assignedTo.handle, - valueFormatter: ({ value }) => value, // <-- necessary for export/print on cols with renderCell - renderCell: ({ row: inv }) => ( - - ), - flex: 1, - minWidth: 175, - }, - amount: { - headerName: "Amount", - valueFormatter: ({ value }) => formatNum.toCurrencyStr(value), - flex: 0.5, - minWidth: 100, - headerAlign: "right", - align: "right", - }, - status: { - headerName: "Status", - flex: 0.5, - minWidth: 115, - headerAlign: "center", - align: "center", - }, - workOrder: { - headerName: "Work Order", - flex: 0.5, - align: "center", - headerAlign: "center", - valueGetter: ({ row: inv }) => inv?.workOrder?.id, - valueFormatter: ({ value }) => value, // <-- necessary for export/print on cols with renderCell - renderCell: ({ value: workOrderID, row: _inv }) => - workOrderID && ( - ) => event.stopPropagation()} - style={{ fontSize: "0.875rem", lineHeight: "1.25rem" }} - > - View Work Order - - ), - }, - createdAt: { - headerName: "Created", - type: "dateTime", - valueFormatter: ({ value }) => getDateAndTime(value), - flex: 1, - }, - updatedAt: { - headerName: "Last Updated", - type: "dateTime", - valueFormatter: ({ value }) => getDateAndTime(value), - flex: 1, - }, - } as Record> - // Map each column entry to an object with "field" and some defaults: - ).map(([columnFieldKey, GridColDef]) => [ - columnFieldKey, - { - field: columnFieldKey, - type: "string", - editable: false, - minWidth: GridColDef.type === "date" ? 100 : GridColDef.type === "dateTime" ? 160 : 150, - maxWidth: 600, - ...GridColDef, // <-- explicit configs override above defaults - }, - ]) -) as Record; - -export const invoiceTableProps: Except = { - columns: Object.values(COLUMNS), -}; diff --git a/src/pages/InvoicesListView/InvoicesListView.stories.tsx b/src/pages/InvoicesListView/InvoicesListView.stories.tsx new file mode 100644 index 00000000..9016ec19 --- /dev/null +++ b/src/pages/InvoicesListView/InvoicesListView.stories.tsx @@ -0,0 +1,58 @@ +import { + withMockApolloDecorator, + withHomePageLayoutDecorator, + type MockApolloDecoratorArgs, +} from "@/../.storybook/decorators"; +import { QUERIES } from "@/graphql/queries"; +import { MOCK_INVOICES } from "@/tests/mockItems/mockInvoices"; +import { InvoicesListView } from "./InvoicesListView"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Pages/InvoicesListView", + component: InvoicesListView, + decorators: [withHomePageLayoutDecorator, withMockApolloDecorator], + parameters: { + layout: "fullscreen", + }, +} satisfies Meta; + +export default meta; + +/////////////////////////////////////////////////////////// +// STORIES + +type Story = StoryObj; + +export const WithMockInvoices = { + args: { + _mock_apollo_decorator_args: { + mocks: [ + { + request: { query: QUERIES.MY_INVOICES }, + result: { data: MOCK_INVOICES }, + }, + ], + }, + }, +} satisfies Story; + +export const EmptyList = { + args: { + _mock_apollo_decorator_args: { + mocks: [ + { + request: { query: QUERIES.MY_INVOICES }, + result: { + data: { + myInvoices: { + createdByUser: [], + assignedToUser: [], + }, + }, + }, + }, + ], + }, + }, +} satisfies Story; diff --git a/src/pages/InvoicesListView/InvoicesListView.tsx b/src/pages/InvoicesListView/InvoicesListView.tsx new file mode 100644 index 00000000..fc8f2045 --- /dev/null +++ b/src/pages/InvoicesListView/InvoicesListView.tsx @@ -0,0 +1,147 @@ +import { CreateItemButton } from "@/components/Buttons/CreateItemButton"; +import { FileInvoiceDollarIcon } from "@/components/Icons/FileInvoiceDollarIcon"; +import { InvoiceListItemButton } from "@/components/List/listItems/InvoiceListItem"; +import { QUERIES } from "@/graphql/queries"; +import { + CoreItemsListView, + LIST_VIEW_LIST_NAMES, + TABLE_VIEW_DATA_SETS, + type ListViewRenderItemFn, +} from "@/layouts/CoreItemsListView"; +import { APP_PATHS } from "@/routes/appPaths"; +import { invoiceTableProps, type InvoiceTableRowData } from "./tableProps"; +import type { Invoice, MyInvoicesQueryReturnType } from "@/graphql/types"; + +type MyInvoicesQueryData = { myInvoices: MyInvoicesQueryReturnType }; + +const getListsAndTablePropsFromMyInvoices = (data: MyInvoicesQueryData) => { + const invoicesCreatedByUser = data?.myInvoices?.createdByUser ?? []; + const invoicesAssignedToUser = data?.myInvoices?.assignedToUser ?? []; + return { + lists: [ + { + listName: LIST_VIEW_LIST_NAMES.INBOX, + items: invoicesAssignedToUser, + listComponentProps: { + EmptyPlaceholder: { + text: "Your Invoice Inbox is Empty", + tooltip: "Invoices you receive from others will appear here", + backgroundIcon: , + }, + }, + }, + { + listName: LIST_VIEW_LIST_NAMES.SENT, + items: invoicesCreatedByUser, + listComponentProps: { + EmptyPlaceholder: { + text: "Your List of Sent Invoices is Empty", + tooltip: "Invoices you send to others will appear here", + backgroundIcon: , + }, + }, + }, + ], + tableProps: { + ...invoiceTableProps, + backgroundIcon: , + rows: [ + ...invoicesCreatedByUser.map((inv) => ({ + dataSet: TABLE_VIEW_DATA_SETS.SENT, + ...inv, + })), + ...invoicesAssignedToUser.map((inv) => ({ + dataSet: TABLE_VIEW_DATA_SETS.RECEIVED, + ...inv, + })), + ], + }, + }; +}; + +export const InvoicesListView = () => ( + + listQuery={QUERIES.MY_INVOICES} + getListsAndTableProps={getListsAndTablePropsFromMyInvoices} + headerLabel="Invoices" + headerComponents={ + + } + itemViewBasePath={APP_PATHS.INVOICES_LIST_VIEW} + listViewSettingsStoreKey="invoices" + renderItem={renderInvoicesListItem} + /> +); + +// Exported as "Component" for react-router-dom lazy loading +export const Component = InvoicesListView; + +const renderInvoicesListItem: ListViewRenderItemFn = ({ listName, item, onClick }) => ( + +); + +// return loading ? ( +// +// ) : error ? ( +// +// ) : ( +// +// viewHeader="Invoices" +// viewBasePath={APP_PATHS.INVOICES_LIST_VIEW} +// renderItem={renderInvoicesListItem} +// listViewSettingsStoreKey="invoices" +// headerComponents={ +// +// } +// lists={[ +// { +// listName: LIST_VIEW_LIST_NAMES.INBOX, +// items: data?.myInvoices.assignedToUser ?? [], +// listComponentProps: { +// EmptyPlaceholder: { +// text: "Your Invoice Inbox is Empty", +// tooltip: "Invoices you receive from others will appear here", +// backgroundIcon: , +// }, +// }, +// }, +// { +// listName: LIST_VIEW_LIST_NAMES, +// items: data?.myInvoices.createdByUser ?? [], +// listComponentProps: { +// EmptyPlaceholder: { +// text: "Your List of Sent Invoices is Empty", +// tooltip: "Invoices you send to others will appear here", +// backgroundIcon: , +// }, +// }, +// }, +// ]} +// tableProps={{ +// ...invoiceTableProps, +// rows: [ +// ...(data?.myInvoices.createdByUser.map((inv) => ({ +// listName: "Sent" as const, +// ...inv, +// })) ?? []), +// ...(data?.myInvoices.assignedToUser.map((inv) => ({ +// listName: "Received" as const, +// ...inv, +// })) ?? []), +// ], +// backgroundIcon: , +// }} +// /> +// ); diff --git a/src/pages/InvoicesListView/index.ts b/src/pages/InvoicesListView/index.ts new file mode 100644 index 00000000..7da643cc --- /dev/null +++ b/src/pages/InvoicesListView/index.ts @@ -0,0 +1,2 @@ +export { Component } from "./InvoicesListView"; +// Exported as "Component" for react-router-dom lazy loading diff --git a/src/pages/InvoicesListView/tableProps.tsx b/src/pages/InvoicesListView/tableProps.tsx new file mode 100644 index 00000000..651c712d --- /dev/null +++ b/src/pages/InvoicesListView/tableProps.tsx @@ -0,0 +1,93 @@ +import { ContactAvatar } from "@/components/Avatar/ContactAvatar"; +import { getDataGridColDefs } from "@/components/DataGrid/helpers/getDataGridColDefs"; +import { Link } from "@/components/Navigation/Link"; +import { getItemViewPath } from "@/routes/helpers"; +import { intToCurrencyStr } from "@/utils/formatters/currency"; +import type { Invoice } from "@/graphql/types"; +import type { TableViewDataSetProp } from "@/layouts/CoreItemsListView/types"; +import type { DataGridProps } from "@mui/x-data-grid"; + +/** + * The type of data provided in the InvoicesListView table. + * + * > "dataSet" added in ./InvoicesListView + */ +export type InvoiceTableRowData = Invoice & TableViewDataSetProp; + +const COLUMNS = getDataGridColDefs< + InvoiceTableRowData, + { + omit: "__typename" | "id" | "stripePaymentIntentID"; + } +>({ + dataSet: { + headerName: "Sent/Received", + minWidth: 115, + headerAlign: "left", + align: "center", + }, + createdBy: { + headerName: "Created By", + valueGetter: ({ row: inv }) => inv.createdBy.profile.displayName, + renderCell: ({ row: inv }) => ( + + ), + flex: 1, + minWidth: 175, + }, + assignedTo: { + headerName: "Assigned To", + valueGetter: ({ row: inv }) => inv.assignedTo.profile.displayName, + renderCell: ({ row: inv }) => ( + + ), + flex: 1, + minWidth: 175, + }, + amount: { + headerName: "Amount", + valueFormatter: ({ value }) => intToCurrencyStr(value), + flex: 0.5, + minWidth: 100, + headerAlign: "right", + align: "right", + }, + status: { + headerName: "Status", + flex: 0.5, + minWidth: 115, + headerAlign: "center", + align: "center", + }, + workOrder: { + headerName: "Work Order", + flex: 0.5, + align: "center", + headerAlign: "center", + valueGetter: ({ row: inv }) => inv?.workOrder?.id, + renderCell: ({ value: workOrder }) => + !!workOrder?.id && ( + ) => event.stopPropagation()} + style={{ fontSize: "0.875rem", lineHeight: "1.25rem" }} + > + View Work Order + + ), + }, + createdAt: { + headerName: "Created", + type: "dateTime", + flex: 1, + }, + updatedAt: { + headerName: "Last Updated", + type: "dateTime", + flex: 1, + }, +}); + +export const invoiceTableProps = { + columns: Object.values(COLUMNS), +} as const satisfies Partial>;