From f1546be521d4f5bc0b25f153463886366e579c7d Mon Sep 17 00:00:00 2001 From: trevor-anderson Date: Fri, 3 Mar 2023 08:24:42 -0500 Subject: [PATCH] feat: add 'tableProps' to core item list views --- src/pages/Contacts/ListView/ListItem.tsx | 13 ++ src/pages/Contacts/ListView/ListView.tsx | 11 +- src/pages/Contacts/ListView/tableProps.tsx | 52 ++++++ src/pages/Invoices/ListView/ListItem.tsx | 20 +-- src/pages/Invoices/ListView/ListView.tsx | 32 ++-- src/pages/Invoices/ListView/tableProps.tsx | 96 +++++++++++ src/pages/WorkOrders/ListView/ListItem.tsx | 10 +- src/pages/WorkOrders/ListView/ListView.tsx | 34 ++-- src/pages/WorkOrders/ListView/tableProps.tsx | 159 +++++++++++++++++++ 9 files changed, 371 insertions(+), 56 deletions(-) create mode 100644 src/pages/Contacts/ListView/tableProps.tsx create mode 100644 src/pages/Invoices/ListView/tableProps.tsx create mode 100644 src/pages/WorkOrders/ListView/tableProps.tsx diff --git a/src/pages/Contacts/ListView/ListItem.tsx b/src/pages/Contacts/ListView/ListItem.tsx index 7a23a32d..97de6718 100644 --- a/src/pages/Contacts/ListView/ListItem.tsx +++ b/src/pages/Contacts/ListView/ListItem.tsx @@ -19,6 +19,19 @@ export const ContactsListItem = ({ onClick={onClick} itemID={contact.id} divider={false} + sx={({ palette }) => ({ + ...(palette.mode === "dark" + ? { + backgroundColor: palette.background.paper + } + : { + border: `2px solid ${palette.divider}` + }), + borderRadius: "0.5rem", + "& .MuiButtonBase-root": { + borderRadius: "0.5rem" + } + })} {...props} > { @@ -26,11 +27,15 @@ export const ContactsListView = () => { viewBasePath="/home/contacts" lists={[{ items: Object.values(MOCK_CONTACTS) as any }]} renderItem={renderContactsListItem} + tableProps={{ + ...contactTableProps, + rows: Object.values(MOCK_CONTACTS) + }} sx={{ - "& ul.core-items-list": { + "& .MuiList-root": { width: "100%", display: "grid", - gridGap: "0.5rem", + gridGap: "1rem", gridTemplateColumns: "repeat( auto-fit, minmax( 16rem, 1fr ))", gridTemplateRows: "repeat( auto-fit, 1fr )" } diff --git a/src/pages/Contacts/ListView/tableProps.tsx b/src/pages/Contacts/ListView/tableProps.tsx new file mode 100644 index 00000000..71520c66 --- /dev/null +++ b/src/pages/Contacts/ListView/tableProps.tsx @@ -0,0 +1,52 @@ +import { DataGrid } from "@mui/x-data-grid"; +import { ContactAvatar } from "@components"; +import { prettifyStr } from "@utils"; +import type { Contact } from "@types"; + +type ColumnFieldKeys = "contact" | keyof Contact; +type ColumnConfig = React.ComponentProps["columns"][number]; + +const COLUMNS = Object.fromEntries( + Object.entries( + { + contact: { + headerName: "Contact", + valueGetter: ({ row: contact }) => contact?.profile?.displayName || contact.handle, + valueFormatter: ({ value }) => value, // <-- necessary for export/print on cols with renderCell + renderCell: ({ row: contact }) => ( + + ) + }, + handle: { + headerName: "Handle" + }, + email: { + headerName: "Email" + }, + phone: { + headerName: "Phone", + valueFormatter: ({ value }) => prettifyStr.phone(value), + valueParser: (value) => prettifyStr.phone(value), + minWidth: 125, + headerAlign: "center", + align: "center" + } + } as Record> + // Map each column entry to an object with "field" and some defaults: + ).map(([columnFieldKey, columnConfig]) => [ + columnFieldKey, + { + field: columnFieldKey, + type: "string", + editable: false, + flex: 1, + minWidth: columnConfig.type === "date" ? 100 : columnConfig.type === "dateTime" ? 160 : 150, + maxWidth: 600, + ...columnConfig // <-- explicit configs override above defaults + } + ]) +) as Record; + +export const contactTableProps: Omit, "rows"> = { + columns: Object.values(COLUMNS) +}; diff --git a/src/pages/Invoices/ListView/ListItem.tsx b/src/pages/Invoices/ListView/ListItem.tsx index b14dd1b7..d5d6a37f 100644 --- a/src/pages/Invoices/ListView/ListItem.tsx +++ b/src/pages/Invoices/ListView/ListItem.tsx @@ -7,18 +7,18 @@ import { formatNum } from "@utils"; import type { Invoice } from "@types"; export const InvoicesListItem = ({ - parentListName, + listName, item, onClick, ...props }: { - parentListName?: "Inbox" | "Sent"; + listName?: "Inbox" | "Sent"; item?: Invoice; onClick?: React.ComponentProps["onClick"]; }) => { - if (!parentListName || !item || !onClick) return null; + if (!listName || !item || !onClick) return null; - const isInboxList = parentListName === "Inbox"; + const isInboxList = listName === "Inbox"; const { createdBy, assignedTo, status, amount, workOrder, createdAt } = item; const userToDisplay = isInboxList ? createdBy : assignedTo; const prettyCreatedAt = createdAt.toLocaleDateString("en-us", { day: "numeric", month: "short" }); @@ -36,7 +36,7 @@ export const InvoicesListItem = ({ user={userToDisplay} onClick={handleClickDiv} itemID={item.id} - parentListName={parentListName} + listName={listName} {...props} > - {formatNum.toCurrencyStr(amount)} - - } + primary={{formatNum.toCurrencyStr(amount)}} /> { @@ -16,7 +12,6 @@ export const InvoicesListView = () => { notifyOnNetworkStatusChange: true, skip: true // TODO <-- skip for now, turn off later }); - const [listVisibility, handleChangeListVisibility] = useInboxListVisToggleBtns(); // FIXME // const { createdByUser, assignedToUser } = invoiceListSettingsStore.useFilterAndSort( @@ -30,25 +25,30 @@ export const InvoicesListView = () => { - } lists={[ { listName: "Inbox", - isListVisible: listVisibility.isInboxVisible, items: MOCK_INVOICES.myInvoices.assignedToUser as any }, { listName: "Sent", - isListVisible: listVisibility.isSentVisible, items: MOCK_INVOICES.myInvoices.createdByUser as any } ]} renderItem={renderInvoicesListItem} + tableProps={{ + ...invoiceTableProps, + rows: [ + ...MOCK_INVOICES.myInvoices.createdByUser.map((inv) => ({ + isItemOwnedByUser: true, + ...inv + })), + ...MOCK_INVOICES.myInvoices.assignedToUser.map((inv) => ({ + isItemOwnedByUser: false, + ...inv + })) + ] + }} /> ); }; diff --git a/src/pages/Invoices/ListView/tableProps.tsx b/src/pages/Invoices/ListView/tableProps.tsx new file mode 100644 index 00000000..cb6d33a1 --- /dev/null +++ b/src/pages/Invoices/ListView/tableProps.tsx @@ -0,0 +1,96 @@ +import { DataGrid } from "@mui/x-data-grid"; +import { Link } from "@components"; +import { getDateAndTime, formatNum } from "@utils"; +import type { Invoice } from "@types"; + +type ColumnFieldKeys = "listName" | keyof Invoice; +type ColumnConfig = React.ComponentProps["columns"][number]; + +const COLUMNS = Object.fromEntries( + Object.entries( + { + listName: { + headerName: "Sent/Received", + valueGetter: ({ row }) => (row.isItemOwnedByUser === true ? "Sent" : "Received"), + minWidth: 115, + headerAlign: "left", + align: "center" + }, + createdBy: { + headerName: "Created By", + valueGetter: ({ row }) => row.createdBy.profile?.displayName || row.createdBy.handle, + flex: 1, + minWidth: 175 + }, + assignedTo: { + headerName: "Assigned To", + valueGetter: ({ row }) => row.assignedTo.profile?.displayName || row.assignedTo.handle, + 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 }) => row?.workOrder?.id, + valueFormatter: ({ value }) => value, // <-- necessary for export/print on cols with renderCell + renderCell: ({ value: workOrderID, row }) => + workOrderID && ( + ) => event.stopPropagation()} + state={{ + isItemOwnedByUser: row.isItemOwnedByUser // Invoice INBOX = WorkOrder SENT + }} + style={{ fontSize: "0.925rem", 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, columnConfig]) => [ + columnFieldKey, + { + field: columnFieldKey, + type: "string", + editable: false, + minWidth: columnConfig.type === "date" ? 100 : columnConfig.type === "dateTime" ? 160 : 150, + maxWidth: 600, + ...columnConfig // <-- explicit configs override above defaults + } + ]) +) as Record; + +export const invoiceTableProps: Omit, "rows"> = { + columns: Object.values(COLUMNS) +}; diff --git a/src/pages/WorkOrders/ListView/ListItem.tsx b/src/pages/WorkOrders/ListView/ListItem.tsx index 405f9c81..80109a09 100644 --- a/src/pages/WorkOrders/ListView/ListItem.tsx +++ b/src/pages/WorkOrders/ListView/ListItem.tsx @@ -6,18 +6,18 @@ import { CoreListItemLayout } from "@layouts"; import type { WorkOrder } from "@types"; export const WorkOrdersListItem = ({ - parentListName, + listName, item, onClick, ...props }: { - parentListName?: "Inbox" | "Sent"; + listName?: "Inbox" | "Sent"; item?: WorkOrder; onClick?: React.ComponentProps["onClick"]; }) => { - if (!parentListName || !item || !onClick) return null; + if (!listName || !item || !onClick) return null; - const isInboxList = parentListName === "Inbox"; + const isInboxList = listName === "Inbox"; const { createdBy, assignedTo, status, location, description, createdAt } = item; const userToDisplay = isInboxList ? createdBy : assignedTo; const prettyCreatedAt = createdAt.toLocaleDateString("en-us", { day: "numeric", month: "short" }); @@ -27,7 +27,7 @@ export const WorkOrdersListItem = ({ user={userToDisplay} onClick={onClick} itemID={item.id} - parentListName={parentListName} + listName={listName} {...props} > diff --git a/src/pages/WorkOrders/ListView/ListView.tsx b/src/pages/WorkOrders/ListView/ListView.tsx index e9f03706..5a82bddd 100644 --- a/src/pages/WorkOrders/ListView/ListView.tsx +++ b/src/pages/WorkOrders/ListView/ListView.tsx @@ -1,13 +1,9 @@ import { useQuery } from "@apollo/client/react/hooks"; -import { WorkOrdersListItem } from "./ListItem"; import { Loading, Error } from "@components"; import { QUERIES } from "@graphql"; -import { - CoreItemsListView, - InboxListVisToggleBtns, - useInboxListVisToggleBtns, - type ListViewRenderItemFn -} from "@layouts"; +import { CoreItemsListView, type ListViewRenderItemFn } from "@layouts"; +import { WorkOrdersListItem } from "./ListItem"; +import { workOrderTableProps } from "./tableProps"; import { MOCK_WORK_ORDERS } from "@/__tests__/mockItems"; // FIXME rm import, use only in test files export const WorkOrdersListView = () => { @@ -16,7 +12,6 @@ export const WorkOrdersListView = () => { notifyOnNetworkStatusChange: true, skip: true // TODO <-- skip for now, turn off later }); - const [listVisibility, handleChangeListVisibility] = useInboxListVisToggleBtns(); // FIXME // const { createdByUser, assignedToUser } = woListSettingsStore.useFilterAndSort( @@ -26,31 +21,34 @@ export const WorkOrdersListView = () => { if (loading || networkStatus === 4) return ; if (error) return ; - // const renderItem: ListViewRenderItemFn = (props) => ; - return ( - } lists={[ { listName: "Inbox", - isListVisible: listVisibility.isInboxVisible, items: MOCK_WORK_ORDERS.myWorkOrders.assignedToUser as any }, { listName: "Sent", - isListVisible: listVisibility.isSentVisible, items: MOCK_WORK_ORDERS.myWorkOrders.createdByUser as any } ]} renderItem={renderWorkOrdersListItem} + tableProps={{ + ...workOrderTableProps, + rows: [ + ...MOCK_WORK_ORDERS.myWorkOrders.createdByUser.map((wo) => ({ + isItemOwnedByUser: true, + ...wo + })), + ...MOCK_WORK_ORDERS.myWorkOrders.assignedToUser.map((wo) => ({ + isItemOwnedByUser: false, + ...wo + })) + ] + }} /> ); }; diff --git a/src/pages/WorkOrders/ListView/tableProps.tsx b/src/pages/WorkOrders/ListView/tableProps.tsx new file mode 100644 index 00000000..0340cfb4 --- /dev/null +++ b/src/pages/WorkOrders/ListView/tableProps.tsx @@ -0,0 +1,159 @@ +import { DataGrid } from "@mui/x-data-grid"; +import { WorkOrderCategoryChip } from "@components"; +import { prettifyStr, getDateAndTime } from "@utils"; +import type { WorkOrder } from "@types"; + +type ColumnFieldKeys = + | "listName" + | Exclude + | keyof WorkOrder["location"]; +type ColumnConfig = React.ComponentProps["columns"][number]; + +const COLUMNS = Object.fromEntries( + Object.entries( + { + listName: { + headerName: "Sent/Received", + valueGetter: ({ row }) => (row.isItemOwnedByUser === true ? "Sent" : "Received"), + minWidth: 115, + headerAlign: "left", + align: "center" + }, + createdBy: { + headerName: "Created By", + valueGetter: ({ row }) => row.createdBy.profile?.displayName || row.createdBy.handle, + flex: 1, + minWidth: 175 + }, + assignedTo: { + headerName: "Assigned To", + valueGetter: ({ row }) => row?.assignedTo?.profile?.displayName || row?.assignedTo?.handle, + flex: 1, + minWidth: 175 + }, + streetLine1: { + headerName: "Street Address", + valueGetter: ({ row }) => row.location.streetLine1, + flex: 1, + minWidth: 175 + }, + streetLine2: { + headerName: "Street Line 2", + valueGetter: ({ row }) => row.location?.streetLine2, + flex: 0.75 + }, + city: { + headerName: "City", + valueGetter: ({ row }) => row.location.city, + flex: 0.75 + }, + region: { + headerName: "State", // TODO "region" vs "state" col name, use state only in US + valueGetter: ({ row }) => row.location.region, + flex: 0.75, + minWidth: 125 + }, + country: { + headerName: "Country", + valueGetter: ({ row }) => row.location?.country, + flex: 0.5, + minWidth: 80 + }, + status: { + headerName: "Status", + valueGetter: ({ row }) => row.status.replace(/_/g, " "), + flex: 0.5, + minWidth: 115, + headerAlign: "center", + align: "center" + }, + priority: { + headerName: "Priority", + flex: 0.5, + minWidth: 115, + headerAlign: "center", + align: "center" + }, + category: { + headerName: "Category", + renderCell: ({ value }) => + value && , + valueFormatter: ({ value }) => value, // <-- necessary for export/print on cols with renderCell + flex: 0.75, + minWidth: 160, + headerAlign: "center", + align: "center" + }, + description: { + headerName: "Description" + }, + entryContact: { + headerName: "Name" + }, + entryContactPhone: { + headerName: "Phone", + valueFormatter: ({ value }) => prettifyStr.phone(value), + valueParser: (value) => prettifyStr.phone(value), + minWidth: 125, + headerAlign: "center", + align: "center" + }, + dueDate: { + headerName: "Due Date", + type: "date", + headerAlign: "center", + align: "center" + }, + scheduledDateTime: { + headerName: "Scheduled Date/Time", + type: "dateTime", + valueFormatter: ({ value }) => getDateAndTime(value) + }, + contractorNotes: { + headerName: "Notes" + }, + createdAt: { + headerName: "Created", + type: "dateTime", + valueFormatter: ({ value }) => getDateAndTime(value) + }, + updatedAt: { + headerName: "Last Updated", + type: "dateTime", + valueFormatter: ({ value }) => getDateAndTime(value) + } + } as Record> + // Map each column entry to an object with "field" and some defaults: + ).map(([columnFieldKey, columnConfig]) => [ + columnFieldKey, + { + field: columnFieldKey, + type: "string", + editable: false, + minWidth: columnConfig.type === "date" ? 100 : columnConfig.type === "dateTime" ? 160 : 150, + maxWidth: 600, + ...columnConfig // <-- explicit configs override above defaults + } + ]) +) as Record; + +export const workOrderTableProps: Omit, "rows"> = { + columns: Object.values(COLUMNS), + experimentalFeatures: { columnGrouping: true }, + columnGroupingModel: [ + { + groupId: "Address", + children: [ + COLUMNS.streetLine1, + COLUMNS.streetLine2, + COLUMNS.city, + COLUMNS.region, + COLUMNS.country + ].map(({ field }) => ({ field })) + }, + { + groupId: "Entry Contact", + children: [COLUMNS.entryContact, COLUMNS.entryContactPhone].map(({ field }) => ({ field })) + } + ] +};