diff --git a/packages/admin-ui/src/components/inputs/formik/AsyncAutocomplete.jsx b/packages/admin-ui/src/components/inputs/formik/AsyncAutocomplete.jsx new file mode 100644 index 00000000..06fefacb --- /dev/null +++ b/packages/admin-ui/src/components/inputs/formik/AsyncAutocomplete.jsx @@ -0,0 +1,29 @@ +import React, { useState } from 'react' +import { AsyncAutocomplete as BaseAsyncAutocomplete } from '../base/AsyncAutocomplete' + +const AsyncAutocompleteFormik = ({ field, form, ...props }) => { + const { name } = field + const { touched, errors, setFieldValue } = form + const [selectedOption, setSelectedOption] = useState(null) + + const error = touched[name] && errors[name] + const getOptionId = props.getOptionId || (opt => opt.id) + + const handleChange = (event, newValue) => { + setSelectedOption(newValue) + setFieldValue(name, newValue ? getOptionId(newValue) : '') + } + + return ( + + ) +} + +export const AsyncAutocomplete = AsyncAutocompleteFormik diff --git a/packages/admin-ui/src/components/inputs/formik/index.js b/packages/admin-ui/src/components/inputs/formik/index.js index 82afd511..303b4c3b 100644 --- a/packages/admin-ui/src/components/inputs/formik/index.js +++ b/packages/admin-ui/src/components/inputs/formik/index.js @@ -1,4 +1,5 @@ import Autocomplete from './Autocomplete' +import { AsyncAutocomplete } from './AsyncAutocomplete' import CashCassetteInput from './CashCassetteInput' import Checkbox from './Checkbox' import Dropdown from './Dropdown' @@ -9,6 +10,7 @@ import TextInput from './TextInput' export { Autocomplete, + AsyncAutocomplete, Checkbox, TextInput, NumberInput, diff --git a/packages/admin-ui/src/pages/LoyaltyPanel/IndividualDiscountModal.jsx b/packages/admin-ui/src/pages/LoyaltyPanel/IndividualDiscountModal.jsx index 45f9187a..fec25539 100644 --- a/packages/admin-ui/src/pages/LoyaltyPanel/IndividualDiscountModal.jsx +++ b/packages/admin-ui/src/pages/LoyaltyPanel/IndividualDiscountModal.jsx @@ -1,6 +1,6 @@ import { Form, Formik, Field } from 'formik' -import * as R from 'ramda' import React from 'react' +import { useLazyQuery, gql } from '@apollo/client' import ErrorMessage from '../../components/ErrorMessage' import Modal from '../../components/Modal' import { HelpTooltip } from '../../components/Tooltip' @@ -8,7 +8,18 @@ import { H1, H3, P } from '../../components/typography' import * as Yup from 'yup' import { Button } from '../../components/buttons' -import { NumberInput, Autocomplete } from '../../components/inputs/formik' +import { NumberInput, AsyncAutocomplete } from '../../components/inputs/formik' + +const SEARCH_CUSTOMERS = gql` + query searchCustomers($searchTerm: String!, $limit: Int) { + searchCustomers(searchTerm: $searchTerm, limit: $limit) { + id + name + phone + email + } + } +` const initialValues = { customer: '', @@ -39,8 +50,15 @@ const IndividualDiscountModal = ({ onClose, creationError, addDiscount, - customers, }) => { + const [searchCustomersQuery] = useLazyQuery(SEARCH_CUSTOMERS) + + const searchCustomers = async searchTerm => { + const { data } = await searchCustomersQuery({ + variables: { searchTerm, limit: 20 }, + }) + return data?.searchCustomers || [] + } const handleAddDiscount = (customer, discount) => { addDiscount({ variables: { @@ -77,18 +95,18 @@ const IndividualDiscountModal = ({ ({ - code: it.id, - display: `${it?.idCardData?.firstName ?? ``}${ - it?.idCardData?.firstName && it?.idCardData?.lastName - ? ` ` - : `` - }${it?.idCardData?.lastName ?? ``} (${it.phone})`, - }))(customers)} - labelProp="display" - valueProp="code" + onSearch={searchCustomers} + getOptionLabel={option => { + const name = option.name + const contact = option.phone || option.email + return contact ? `${name} (${contact})` : name + }} + getOptionId={option => option.id} + placeholder="Type to search customers..." + noOptionsText="Type at least 3 characters to search" + minSearchLength={2} />
diff --git a/packages/admin-ui/src/pages/LoyaltyPanel/IndividualDiscounts.jsx b/packages/admin-ui/src/pages/LoyaltyPanel/IndividualDiscounts.jsx index bd151fc4..952b2a84 100644 --- a/packages/admin-ui/src/pages/LoyaltyPanel/IndividualDiscounts.jsx +++ b/packages/admin-ui/src/pages/LoyaltyPanel/IndividualDiscounts.jsx @@ -1,18 +1,20 @@ -import IconButton from '@mui/material/IconButton' -import SvgIcon from '@mui/material/SvgIcon' import { useQuery, useMutation, gql } from '@apollo/client' import * as R from 'ramda' -import React, { useState } from 'react' +import React, { useState, useMemo } from 'react' +import { + MaterialReactTable, + MRT_ActionMenuItem, + useMaterialReactTable, +} from 'material-react-table' +import Delete from '@mui/icons-material/Delete' import { Link, Button } from '../../components/buttons' import { DeleteDialog } from '../../components/DeleteDialog' -import DataTable from '../../components/tables/DataTable' import { Label3, TL1 } from '../../components/typography' import PhoneIdIcon from '../../styling/icons/ID/phone/zodiac.svg?react' -import DeleteIcon from '../../styling/icons/action/delete/enabled.svg?react' +import { defaultMaterialTableOpts } from '../../utils/materialReactTableOpts' import IndividualDiscountModal from './IndividualDiscountModal' -import classnames from 'classnames' const GET_INDIVIDUAL_DISCOUNTS = gql` query individualDiscounts { @@ -44,16 +46,6 @@ const CREATE_DISCOUNT = gql` } ` -const GET_CUSTOMERS = gql` - { - customers { - id - phone - idCardData - } - } -` - const IndividualDiscounts = () => { const [deleteDialog, setDeleteDialog] = useState(false) const [toBeDeleted, setToBeDeleted] = useState() @@ -62,9 +54,11 @@ const IndividualDiscounts = () => { const [showModal, setShowModal] = useState(false) const toggleModal = () => setShowModal(!showModal) - const { data: discountResponse, loading } = useQuery(GET_INDIVIDUAL_DISCOUNTS) - const { data: customerData, loading: customerLoading } = - useQuery(GET_CUSTOMERS) + const { data: discountResponse, loading } = useQuery( + GET_INDIVIDUAL_DISCOUNTS, + { notifyOnNetworkStatusChange: true }, + ) + const discounts = discountResponse?.individualDiscounts || [] const [createDiscount, { error: creationError }] = useMutation( CREATE_DISCOUNT, @@ -82,88 +76,86 @@ const IndividualDiscounts = () => { refetchQueries: () => ['individualDiscounts'], }) - const elements = [ - { - header: 'Identification', - width: 312, - textAlign: 'left', - size: 'sm', - view: t => { - return ( + const columns = useMemo( + () => [ + { + id: 'identification', + header: 'Identification', + size: 312, + accessorFn: row => row.customer.phone, + Cell: ({ row }) => (
- {t.customer.phone} + {row.original.customer.phone}
- ) + ), }, - }, - { - header: 'Name', - width: 300, - textAlign: 'left', - size: 'sm', - view: t => { - const customer = t.customer - if (R.isNil(customer.idCardData)) { - return <>{'-'} - } - - return ( - <>{`${customer.idCardData.firstName ?? ``}${ + { + id: 'name', + header: 'Name', + size: 300, + accessorFn: row => { + const customer = row.customer + if (R.isNil(customer.idCardData)) { + return '-' + } + return `${customer.idCardData.firstName ?? ''}${ customer.idCardData.firstName && customer.idCardData.lastName - ? ` ` - : `` - }${customer.idCardData.lastName ?? ``}`} - ) + ? ' ' + : '' + }${customer.idCardData.lastName ?? ''}` + }, }, + { + id: 'discount', + header: 'Discount rate', + size: 220, + accessorKey: 'discount', + Cell: ({ cell }) => ( + <> + {cell.getValue()} % + + ), + }, + ], + [], + ) + + const table = useMaterialReactTable({ + ...defaultMaterialTableOpts, + columns, + data: discounts, + state: { isLoading: loading }, + getRowId: row => row.id, + enableRowActions: true, + renderRowActionMenuItems: ({ row }) => [ + } + key="delete" + label="Revoke" + onClick={() => { + setDeleteDialog(true) + setToBeDeleted({ variables: { discountId: row.original.id } }) + }} + table={table} + />, + ], + initialState: { + ...defaultMaterialTableOpts.initialState, + columnPinning: { right: ['mrt-row-actions'] }, }, - { - header: 'Discount rate', - width: 220, - textAlign: 'left', - size: 'sm', - view: t => ( - <> - {t.discount} % - - ), - }, - { - header: 'Revoke', - width: 100, - textAlign: 'center', - size: 'sm', - view: t => ( - { - setDeleteDialog(true) - setToBeDeleted({ variables: { discountId: t.id } }) - }}> - - - - - ), - }, - ] + }) return ( <> - {!loading && !R.isEmpty(discountResponse.individualDiscounts) && ( + {!loading && !R.isEmpty(discounts) && ( <>
- + Add new code
- + { @@ -178,7 +170,7 @@ const IndividualDiscounts = () => { /> )} - {!loading && R.isEmpty(discountResponse.individualDiscounts) && ( + {!loading && R.isEmpty(discounts) && (
It seems there are no active individual customer discounts on your @@ -195,7 +187,6 @@ const IndividualDiscounts = () => { }} creationError={creationError} addDiscount={createDiscount} - customers={R.path(['customers'])(customerData)} /> ) diff --git a/packages/server/lib/customers.js b/packages/server/lib/customers.js index 5e768e6d..7b66b8af 100644 --- a/packages/server/lib/customers.js +++ b/packages/server/lib/customers.js @@ -488,7 +488,10 @@ function getSlimCustomerByIdBatch(ids) { const sql = `SELECT id, phone, id_card_data FROM customers WHERE id = ANY($1::uuid[])` - return db.any(sql, [ids]).then(customers => _.map(camelize, customers)) + return db.any(sql, [ids]).then(customers => { + const customersById = _.keyBy('id', _.map(camelize, customers)) + return ids.map(id => customersById[id] || null) + }) } function getCustomersList() {