feat: async autocomplete on individual discounts
This commit is contained in:
parent
d0aaf6c170
commit
f1e5edd4ac
5 changed files with 146 additions and 103 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 Autocomplete from './Autocomplete'
|
||||||
|
import { AsyncAutocomplete } from './AsyncAutocomplete'
|
||||||
import CashCassetteInput from './CashCassetteInput'
|
import CashCassetteInput from './CashCassetteInput'
|
||||||
import Checkbox from './Checkbox'
|
import Checkbox from './Checkbox'
|
||||||
import Dropdown from './Dropdown'
|
import Dropdown from './Dropdown'
|
||||||
|
|
@ -9,6 +10,7 @@ import TextInput from './TextInput'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
|
AsyncAutocomplete,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
TextInput,
|
TextInput,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Form, Formik, Field } from 'formik'
|
import { Form, Formik, Field } from 'formik'
|
||||||
import * as R from 'ramda'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useLazyQuery, gql } from '@apollo/client'
|
||||||
import ErrorMessage from '../../components/ErrorMessage'
|
import ErrorMessage from '../../components/ErrorMessage'
|
||||||
import Modal from '../../components/Modal'
|
import Modal from '../../components/Modal'
|
||||||
import { HelpTooltip } from '../../components/Tooltip'
|
import { HelpTooltip } from '../../components/Tooltip'
|
||||||
|
|
@ -8,7 +8,18 @@ import { H1, H3, P } from '../../components/typography'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import { Button } from '../../components/buttons'
|
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 = {
|
const initialValues = {
|
||||||
customer: '',
|
customer: '',
|
||||||
|
|
@ -39,8 +50,15 @@ const IndividualDiscountModal = ({
|
||||||
onClose,
|
onClose,
|
||||||
creationError,
|
creationError,
|
||||||
addDiscount,
|
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) => {
|
const handleAddDiscount = (customer, discount) => {
|
||||||
addDiscount({
|
addDiscount({
|
||||||
variables: {
|
variables: {
|
||||||
|
|
@ -77,18 +95,18 @@ const IndividualDiscountModal = ({
|
||||||
<Field
|
<Field
|
||||||
name="customer"
|
name="customer"
|
||||||
label="Select a customer"
|
label="Select a customer"
|
||||||
component={Autocomplete}
|
component={AsyncAutocomplete}
|
||||||
fullWidth
|
fullWidth
|
||||||
options={R.map(it => ({
|
onSearch={searchCustomers}
|
||||||
code: it.id,
|
getOptionLabel={option => {
|
||||||
display: `${it?.idCardData?.firstName ?? ``}${
|
const name = option.name
|
||||||
it?.idCardData?.firstName && it?.idCardData?.lastName
|
const contact = option.phone || option.email
|
||||||
? ` `
|
return contact ? `${name} (${contact})` : name
|
||||||
: ``
|
}}
|
||||||
}${it?.idCardData?.lastName ?? ``} (${it.phone})`,
|
getOptionId={option => option.id}
|
||||||
}))(customers)}
|
placeholder="Type to search customers..."
|
||||||
labelProp="display"
|
noOptionsText="Type at least 3 characters to search"
|
||||||
valueProp="code"
|
minSearchLength={2}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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 { useQuery, useMutation, gql } from '@apollo/client'
|
||||||
import * as R from 'ramda'
|
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 { Link, Button } from '../../components/buttons'
|
||||||
import { DeleteDialog } from '../../components/DeleteDialog'
|
import { DeleteDialog } from '../../components/DeleteDialog'
|
||||||
import DataTable from '../../components/tables/DataTable'
|
|
||||||
import { Label3, TL1 } from '../../components/typography'
|
import { Label3, TL1 } from '../../components/typography'
|
||||||
import PhoneIdIcon from '../../styling/icons/ID/phone/zodiac.svg?react'
|
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 IndividualDiscountModal from './IndividualDiscountModal'
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
const GET_INDIVIDUAL_DISCOUNTS = gql`
|
const GET_INDIVIDUAL_DISCOUNTS = gql`
|
||||||
query individualDiscounts {
|
query individualDiscounts {
|
||||||
|
|
@ -44,16 +46,6 @@ const CREATE_DISCOUNT = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const GET_CUSTOMERS = gql`
|
|
||||||
{
|
|
||||||
customers {
|
|
||||||
id
|
|
||||||
phone
|
|
||||||
idCardData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const IndividualDiscounts = () => {
|
const IndividualDiscounts = () => {
|
||||||
const [deleteDialog, setDeleteDialog] = useState(false)
|
const [deleteDialog, setDeleteDialog] = useState(false)
|
||||||
const [toBeDeleted, setToBeDeleted] = useState()
|
const [toBeDeleted, setToBeDeleted] = useState()
|
||||||
|
|
@ -62,9 +54,11 @@ const IndividualDiscounts = () => {
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
const toggleModal = () => setShowModal(!showModal)
|
const toggleModal = () => setShowModal(!showModal)
|
||||||
|
|
||||||
const { data: discountResponse, loading } = useQuery(GET_INDIVIDUAL_DISCOUNTS)
|
const { data: discountResponse, loading } = useQuery(
|
||||||
const { data: customerData, loading: customerLoading } =
|
GET_INDIVIDUAL_DISCOUNTS,
|
||||||
useQuery(GET_CUSTOMERS)
|
{ notifyOnNetworkStatusChange: true },
|
||||||
|
)
|
||||||
|
const discounts = discountResponse?.individualDiscounts || []
|
||||||
|
|
||||||
const [createDiscount, { error: creationError }] = useMutation(
|
const [createDiscount, { error: creationError }] = useMutation(
|
||||||
CREATE_DISCOUNT,
|
CREATE_DISCOUNT,
|
||||||
|
|
@ -82,88 +76,86 @@ const IndividualDiscounts = () => {
|
||||||
refetchQueries: () => ['individualDiscounts'],
|
refetchQueries: () => ['individualDiscounts'],
|
||||||
})
|
})
|
||||||
|
|
||||||
const elements = [
|
const columns = useMemo(
|
||||||
{
|
() => [
|
||||||
header: 'Identification',
|
{
|
||||||
width: 312,
|
id: 'identification',
|
||||||
textAlign: 'left',
|
header: 'Identification',
|
||||||
size: 'sm',
|
size: 312,
|
||||||
view: t => {
|
accessorFn: row => row.customer.phone,
|
||||||
return (
|
Cell: ({ row }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<PhoneIdIcon />
|
<PhoneIdIcon />
|
||||||
<span>{t.customer.phone}</span>
|
<span>{row.original.customer.phone}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
width: 300,
|
size: 300,
|
||||||
textAlign: 'left',
|
accessorFn: row => {
|
||||||
size: 'sm',
|
const customer = row.customer
|
||||||
view: t => {
|
if (R.isNil(customer.idCardData)) {
|
||||||
const customer = t.customer
|
return '-'
|
||||||
if (R.isNil(customer.idCardData)) {
|
}
|
||||||
return <>{'-'}</>
|
return `${customer.idCardData.firstName ?? ''}${
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>{`${customer.idCardData.firstName ?? ``}${
|
|
||||||
customer.idCardData.firstName && customer.idCardData.lastName
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{!loading && !R.isEmpty(discountResponse.individualDiscounts) && (
|
{!loading && !R.isEmpty(discounts) && (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-end mb-8 -mt-14">
|
<div className="flex justify-end mb-8 -mt-14">
|
||||||
<Link
|
<Link color="primary" onClick={toggleModal}>
|
||||||
color="primary"
|
|
||||||
onClick={toggleModal}
|
|
||||||
className={classnames({ 'cursor-wait': customerLoading })}
|
|
||||||
disabled={customerLoading}>
|
|
||||||
Add new code
|
Add new code
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
<MaterialReactTable table={table} />
|
||||||
elements={elements}
|
|
||||||
data={R.path(['individualDiscounts'])(discountResponse)}
|
|
||||||
/>
|
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
open={deleteDialog}
|
open={deleteDialog}
|
||||||
onDismissed={() => {
|
onDismissed={() => {
|
||||||
|
|
@ -178,7 +170,7 @@ const IndividualDiscounts = () => {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!loading && R.isEmpty(discountResponse.individualDiscounts) && (
|
{!loading && R.isEmpty(discounts) && (
|
||||||
<div className="flex items-start flex-col">
|
<div className="flex items-start flex-col">
|
||||||
<Label3>
|
<Label3>
|
||||||
It seems there are no active individual customer discounts on your
|
It seems there are no active individual customer discounts on your
|
||||||
|
|
@ -195,7 +187,6 @@ const IndividualDiscounts = () => {
|
||||||
}}
|
}}
|
||||||
creationError={creationError}
|
creationError={creationError}
|
||||||
addDiscount={createDiscount}
|
addDiscount={createDiscount}
|
||||||
customers={R.path(['customers'])(customerData)}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -488,7 +488,10 @@ function getSlimCustomerByIdBatch(ids) {
|
||||||
const sql = `SELECT id, phone, id_card_data
|
const sql = `SELECT id, phone, id_card_data
|
||||||
FROM customers
|
FROM customers
|
||||||
WHERE id = ANY($1::uuid[])`
|
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() {
|
function getCustomersList() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue