feat: add search functionality to customer page
This commit is contained in:
parent
b99f98982b
commit
2b93f016ac
7 changed files with 151 additions and 24 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue