Merge pull request #1890 from RafaelTaranto/feat/async-autocomplete-on-loyalty-page
LAM-1453 feat: async autocomplete on loyalty page
This commit is contained in:
commit
9125b26e88
8 changed files with 153 additions and 105 deletions
|
|
@ -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 (
|
||||
<BaseAsyncAutocomplete
|
||||
{...props}
|
||||
name={name}
|
||||
value={selectedOption}
|
||||
onChange={handleChange}
|
||||
error={!!error}
|
||||
helperText={error || ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const AsyncAutocomplete = AsyncAutocompleteFormik
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ const CustomerProfile = memo(() => {
|
|||
refetch: getCustomer,
|
||||
loading: customerLoading,
|
||||
} = useQuery(GET_CUSTOMER, {
|
||||
notifyOnNetworkStatusChange: true,
|
||||
variables: { customerId },
|
||||
skip: !customerId,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
<Field
|
||||
name="customer"
|
||||
label="Select a customer"
|
||||
component={Autocomplete}
|
||||
component={AsyncAutocomplete}
|
||||
fullWidth
|
||||
options={R.map(it => ({
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<PhoneIdIcon />
|
||||
<span>{t.customer.phone}</span>
|
||||
<span>{row.original.customer.phone}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
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 }) => (
|
||||
<>
|
||||
<TL1 inline>{cell.getValue()}</TL1> %
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
...defaultMaterialTableOpts,
|
||||
columns,
|
||||
data: discounts,
|
||||
state: { isLoading: loading },
|
||||
getRowId: row => row.id,
|
||||
enableRowActions: true,
|
||||
renderRowActionMenuItems: ({ row }) => [
|
||||
<MRT_ActionMenuItem
|
||||
icon={<Delete />}
|
||||
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 => (
|
||||
<>
|
||||
<TL1 inline>{t.discount}</TL1> %
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Revoke',
|
||||
width: 100,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: t => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setDeleteDialog(true)
|
||||
setToBeDeleted({ variables: { discountId: t.id } })
|
||||
}}>
|
||||
<SvgIcon>
|
||||
<DeleteIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{!loading && !R.isEmpty(discountResponse.individualDiscounts) && (
|
||||
{!loading && !R.isEmpty(discounts) && (
|
||||
<>
|
||||
<div className="flex justify-end mb-8 -mt-14">
|
||||
<Link
|
||||
color="primary"
|
||||
onClick={toggleModal}
|
||||
className={classnames({ 'cursor-wait': customerLoading })}
|
||||
disabled={customerLoading}>
|
||||
<Link color="primary" onClick={toggleModal}>
|
||||
Add new code
|
||||
</Link>
|
||||
</div>
|
||||
<DataTable
|
||||
elements={elements}
|
||||
data={R.path(['individualDiscounts'])(discountResponse)}
|
||||
/>
|
||||
<MaterialReactTable table={table} />
|
||||
<DeleteDialog
|
||||
open={deleteDialog}
|
||||
onDismissed={() => {
|
||||
|
|
@ -178,7 +170,7 @@ const IndividualDiscounts = () => {
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{!loading && R.isEmpty(discountResponse.individualDiscounts) && (
|
||||
{!loading && R.isEmpty(discounts) && (
|
||||
<div className="flex items-start flex-col">
|
||||
<Label3>
|
||||
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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const MachineStatus = () => {
|
|||
data: machinesResponse,
|
||||
refetch,
|
||||
loading: machinesLoading,
|
||||
} = useQuery(GET_MACHINES)
|
||||
} = useQuery(GET_MACHINES, { notifyOnNetworkStatusChange: true })
|
||||
const { data: configResponse, configLoading } = useQuery(GET_DATA)
|
||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,11 @@ const GET_CUSTOM_REQUESTS = gql`
|
|||
|
||||
const Triggers = () => {
|
||||
const [wizardType, setWizard] = useState(false)
|
||||
const { data, loading: configLoading, refetch } = useQuery(GET_CONFIG)
|
||||
const {
|
||||
data,
|
||||
loading: configLoading,
|
||||
refetch,
|
||||
} = useQuery(GET_CONFIG, { notifyOnNetworkStatusChange: true })
|
||||
const { data: customInfoReqData, loading: customInfoLoading } =
|
||||
useQuery(GET_CUSTOM_REQUESTS)
|
||||
const [error, setError] = useState(null)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue