feat: add search functionality to customer page

This commit is contained in:
Sérgio Salgado 2021-05-31 15:58:24 +01:00 committed by Josh Harvey
parent b99f98982b
commit 2b93f016ac
7 changed files with 151 additions and 24 deletions

View file

@ -450,7 +450,7 @@ function batch () {
* *
* @returns {array} Array of customers with it's transactions aggregations * @returns {array} Array of customers with it's transactions aggregations
*/ */
function getCustomersList () { function getCustomersList (phone = null, name = null, address = null, id = null) {
const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override, const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration, phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at, id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
@ -474,8 +474,12 @@ function getCustomersList () {
from cash_out_txs where confirmed_at is not null) t on c.id = t.customer_id from cash_out_txs where confirmed_at is not null) t on c.id = t.customer_id
where c.id != $1 where c.id != $1
) as cl where rn = 1 ) as cl where rn = 1
and ($3 is null or phone = $3)
and ($4 is null or concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $4)
and ($5 is null or id_card_data::json->>'address' = $5)
and ($6 is null or id_card_data::json->>'documentNumber' = $6)
limit $2` limit $2`
return db.any(sql, [ anonymous.uuid, NUM_RESULTS ]) return db.any(sql, [ anonymous.uuid, NUM_RESULTS, phone, name, address, id ])
.then(customers => Promise.all(_.map(customer => { .then(customers => Promise.all(_.map(customer => {
return populateOverrideUsernames(customer) return populateOverrideUsernames(customer)
.then(camelize) .then(camelize)

View file

@ -27,4 +27,15 @@ function transaction() {
return db.any(sql) return db.any(sql)
} }
module.exports = { transaction } function customer() {
const sql = `select distinct * from (
select 'phone' as type, phone as value from customers where phone is not null union
select 'name' as type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') as value from customers where concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') is not null union
select 'address' as type, id_card_data::json->>'address' as value from customers where id_card_data::json->>'address' is not null union
select 'id' as type, id_card_data::json->>'documentNumber' as value from customers where id_card_data::json->>'documentNumber' is not null
) f`
return db.any(sql)
}
module.exports = { transaction, customer }

View file

@ -1,13 +1,15 @@
const anonymous = require('../../../constants').anonymousCustomer const anonymous = require('../../../constants').anonymousCustomer
const customers = require('../../../customers') const customers = require('../../../customers')
const filters = require('../../filters')
const resolvers = { const resolvers = {
Customer: { Customer: {
isAnonymous: parent => (parent.customerId === anonymous.uuid) isAnonymous: parent => (parent.customerId === anonymous.uuid)
}, },
Query: { Query: {
customers: () => customers.getCustomersList(), customers: (...[, { phone, name, address, id }]) => customers.getCustomersList(phone, name, address, id),
customer: (...[, { customerId }]) => customers.getCustomerById(customerId) customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
customerFilters: () => filters.customer()
}, },
Mutation: { Mutation: {
setCustomer: (root, { customerId, customerInput }, context, info) => { setCustomer: (root, { customerId, customerInput }, context, info) => {

View file

@ -56,8 +56,9 @@ const typeDef = gql`
} }
type Query { type Query {
customers: [Customer] @auth customers(phone: String, name: String, address: String, id: String): [Customer] @auth
customer(customerId: ID!): Customer @auth customer(customerId: ID!): Customer @auth
customerFilters: [Filter] @auth
} }
type Mutation { type Mutation {

View file

@ -17,13 +17,16 @@ const TitleSection = ({
error, error,
labels, labels,
button, button,
children children,
appendix,
appendixClassName
}) => { }) => {
const classes = useStyles() const classes = useStyles()
return ( return (
<div className={classnames(classes.titleWrapper, className)}> <div className={classnames(classes.titleWrapper, className)}>
<div className={classes.titleAndButtonsContainer}> <div className={classes.titleAndButtonsContainer}>
<Title>{title}</Title> <Title>{title}</Title>
{appendix && <div className={appendixClassName}>{appendix}</div>}
{error && ( {error && (
<ErrorMessage className={classes.error}>Failed to save</ErrorMessage> <ErrorMessage className={classes.error}>Failed to save</ErrorMessage>
)} )}

View file

@ -1,12 +1,33 @@
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React from 'react' import React, { useState } from 'react'
import { useHistory } from 'react-router-dom' import { useHistory } from 'react-router-dom'
import Chip from 'src/components/Chip'
import SearchBox from 'src/components/SearchBox'
import TitleSection from 'src/components/layout/TitleSection'
import { P } from 'src/components/typography'
import baseStyles from 'src/pages/Logs.styles'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { fromNamespace, namespaces } from 'src/utils/config' import { fromNamespace, namespaces } from 'src/utils/config'
import { chipStyles } from '../Transactions/Transactions.styles'
import CustomersList from './CustomersList' import CustomersList from './CustomersList'
import styles from './CustomersList.styles'
const GET_CUSTOMER_FILTERS = gql`
query filters {
customerFilters {
type
value
}
}
`
const GET_CUSTOMERS = gql` const GET_CUSTOMERS = gql`
{ {
@ -28,26 +49,119 @@ const GET_CUSTOMERS = gql`
} }
` `
const useStyles = makeStyles(styles)
const useChipStyles = makeStyles(chipStyles)
const useBaseStyles = makeStyles(baseStyles)
const Customers = () => { const Customers = () => {
const classes = useStyles()
const chipClasses = useChipStyles()
const baseStyles = useBaseStyles()
const history = useHistory() const history = useHistory()
const { data: customersResponse, loading } = useQuery(GET_CUSTOMERS)
const handleCustomerClicked = customer => const handleCustomerClicked = customer =>
history.push(`/compliance/customer/${customer.id}`) history.push(`/compliance/customer/${customer.id}`)
const [filteredCustomers, setFilteredCustomers] = useState([])
const [variables, setVariables] = useState({})
const [filters, setFilters] = useState([])
const {
data: customersResponse,
loading: customerLoading,
refetch
} = useQuery(GET_CUSTOMERS, {
variables,
onCompleted: data => setFilteredCustomers(R.path(['customers'])(data))
})
const { data: filtersResponse, loading: loadingFilters } = useQuery(
GET_CUSTOMER_FILTERS
)
const configData = R.path(['config'])(customersResponse) ?? [] const configData = R.path(['config'])(customersResponse) ?? []
const locale = configData && fromNamespace(namespaces.LOCALE, configData) const locale = configData && fromNamespace(namespaces.LOCALE, configData)
const customersData = R.sortWith([R.descend(R.prop('lastActive'))])( const customersData = R.sortWith([R.descend(R.prop('lastActive'))])(
R.path(['customers'])(customersResponse) ?? [] filteredCustomers ?? []
) )
const onFilterChange = filters => {
const filtersObject = R.compose(
R.mergeAll,
R.map(f => ({
[f.type]: f.value
}))
)(filters)
setFilters(filters)
setVariables({
phone: filtersObject.phone,
name: filtersObject.name,
address: filtersObject.address,
id: filtersObject.id
})
refetch && refetch()
}
const onFilterDelete = filter =>
setFilters(
R.filter(f => !R.whereEq(R.pick(['type', 'value'], f), filter))(filters)
)
const filterOptions = R.path(['customerFilters'])(filtersResponse)
return ( return (
<CustomersList <>
data={customersData} <TitleSection
locale={locale} title="Customers"
onClick={handleCustomerClicked} appendix={
loading={loading} <div>
/> <SearchBox
loading={loadingFilters}
filters={filters}
options={filterOptions}
inputPlaceholder={'Search customers'}
onChange={onFilterChange}
/>
</div>
}
appendixClassName={baseStyles.buttonsWrapper}
labels={[
{ label: 'Cash-in', icon: <TxInIcon /> },
{ label: 'Cash-out', icon: <TxOutIcon /> }
]}
/>
{filters.length > 0 && (
<>
<P className={classes.text}>{'Filters:'}</P>
<div>
{filters.map((f, idx) => (
<Chip
key={idx}
classes={chipClasses}
label={`${f.type}: ${f.value}`}
onDelete={() => onFilterDelete(f)}
deleteIcon={<CloseIcon className={classes.button} />}
/>
))}
<Chip
classes={chipClasses}
label={`Delete filters`}
onDelete={() => setFilters([])}
deleteIcon={<CloseIcon className={classes.button} />}
/>
</div>
</>
)}
<CustomersList
data={customersData}
locale={locale}
onClick={handleCustomerClicked}
loading={customerLoading}
/>
</>
) )
} }

View file

@ -4,7 +4,6 @@ import * as R from 'ramda'
import React from 'react' import React from 'react'
import { MainStatus } from 'src/components/Status' import { MainStatus } from 'src/components/Status'
import TitleSection from 'src/components/layout/TitleSection'
import DataTable from 'src/components/tables/DataTable' import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
@ -74,13 +73,6 @@ const CustomersList = ({ data, locale, onClick, loading }) => {
return ( return (
<> <>
<TitleSection
title="Customers"
labels={[
{ label: 'Cash-in', icon: <TxInIcon /> },
{ label: 'Cash-out', icon: <TxOutIcon /> }
]}
/>
<DataTable <DataTable
loading={loading} loading={loading}
emptyText="No customers so far" emptyText="No customers so far"