feat: async autocomplete on individual discounts

This commit is contained in:
Rafael Taranto 2025-06-23 10:15:42 +01:00
parent d0aaf6c170
commit f1e5edd4ac
5 changed files with 146 additions and 103 deletions

View file

@ -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

View file

@ -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,

View file

@ -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>

View file

@ -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(
() => [
{ {
id: 'identification',
header: 'Identification', header: 'Identification',
width: 312, size: 312,
textAlign: 'left', accessorFn: row => row.customer.phone,
size: 'sm', Cell: ({ row }) => (
view: t => {
return (
<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 => {
const customer = t.customer
if (R.isNil(customer.idCardData)) { if (R.isNil(customer.idCardData)) {
return <>{'-'}</> 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', header: 'Discount rate',
width: 220, size: 220,
textAlign: 'left', accessorKey: 'discount',
size: 'sm', Cell: ({ cell }) => (
view: t => (
<> <>
<TL1 inline>{t.discount}</TL1> % <TL1 inline>{cell.getValue()}</TL1> %
</> </>
), ),
}, },
{ ],
header: 'Revoke', [],
width: 100, )
textAlign: 'center',
size: 'sm', const table = useMaterialReactTable({
view: t => ( ...defaultMaterialTableOpts,
<IconButton columns,
data: discounts,
state: { isLoading: loading },
getRowId: row => row.id,
enableRowActions: true,
renderRowActionMenuItems: ({ row }) => [
<MRT_ActionMenuItem
icon={<Delete />}
key="delete"
label="Revoke"
onClick={() => { onClick={() => {
setDeleteDialog(true) setDeleteDialog(true)
setToBeDeleted({ variables: { discountId: t.id } }) setToBeDeleted({ variables: { discountId: row.original.id } })
}}> }}
<SvgIcon> table={table}
<DeleteIcon /> />,
</SvgIcon> ],
</IconButton> initialState: {
), ...defaultMaterialTableOpts.initialState,
columnPinning: { right: ['mrt-row-actions'] },
}, },
] })
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)}
/> />
</> </>
) )

View file

@ -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() {