Merge pull request #851 from chaotixkilla/feat-customer-creation-modal

Add customer creation via admin
This commit is contained in:
Rafael Taranto 2022-01-14 13:35:03 +00:00 committed by GitHub
commit a422054375
8 changed files with 190 additions and 14 deletions

View file

@ -49,7 +49,8 @@ const resolvers = {
},
deleteCustomerNote: (...[, { noteId }]) => {
return customerNotes.deleteCustomerNote(noteId)
}
},
createCustomer: (...[, { phoneNumber }]) => customers.add({ phone: phoneNumber })
}
}

View file

@ -103,6 +103,7 @@ const typeDef = gql`
createCustomerNote(customerId: ID!, title: String!, content: String!): Boolean @auth
editCustomerNote(noteId: ID!, newContent: String!): Boolean @auth
deleteCustomerNote(noteId: ID!): Boolean @auth
createCustomer(phoneNumber: String): Customer @auth
}
`

View file

@ -13870,6 +13870,11 @@
"delegate": "^3.1.2"
}
},
"google-libphonenumber": {
"version": "3.2.22",
"resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.22.tgz",
"integrity": "sha512-lzEllxWc05n/HEv75SsDrA7zdEVvQzTZimItZm/TZ5XBs7cmx2NJmSlA5I0kZbdKNu8GFETBhSpo+SOhx0JslA=="
},
"graceful-fs": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz",

View file

@ -26,6 +26,7 @@
"downshift": "3.3.4",
"file-saver": "2.0.2",
"formik": "2.2.0",
"google-libphonenumber": "^3.2.22",
"graphql": "^14.5.8",
"graphql-tag": "^2.10.3",
"jss-plugin-extend": "^10.0.0",

View file

@ -19,14 +19,14 @@ const TitleSection = ({
buttons = [],
children,
appendix,
appendixClassName
appendixRight
}) => {
const classes = useStyles()
return (
<div className={classnames(classes.titleWrapper, className)}>
<div className={classes.titleAndButtonsContainer}>
<Title>{title}</Title>
{appendix && <div className={appendixClassName}>{appendix}</div>}
{!!appendix && appendix}
{error && (
<ErrorMessage className={classes.error}>Failed to save</ErrorMessage>
)}
@ -46,13 +46,14 @@ const TitleSection = ({
</>
)}
</div>
<Box display="flex" flexDirection="row">
<Box display="flex" flexDirection="row" alignItems="center">
{(labels ?? []).map(({ icon, label }, idx) => (
<Box key={idx} display="flex" alignItems="center">
<div className={classes.icon}>{icon}</div>
<Label1 className={classes.label}>{label}</Label1>
</Box>
))}
{appendixRight}
</Box>
{children}
</div>

View file

@ -1,5 +1,5 @@
import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import { useQuery, useMutation } from '@apollo/react-hooks'
import { Box, makeStyles } from '@material-ui/core'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState } from 'react'
@ -7,6 +7,7 @@ import { useHistory } from 'react-router-dom'
import SearchBox from 'src/components/SearchBox'
import SearchFilter from 'src/components/SearchFilter'
import { Link } from 'src/components/buttons'
import TitleSection from 'src/components/layout/TitleSection'
import baseStyles from 'src/pages/Logs.styles'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
@ -14,6 +15,7 @@ import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-ou
import { fromNamespace, namespaces } from 'src/utils/config'
import CustomersList from './CustomersList'
import CreateCustomerModal from './components/CreateCustomerModal'
const GET_CUSTOMER_FILTERS = gql`
query filters {
@ -49,6 +51,14 @@ const GET_CUSTOMERS = gql`
}
`
const CREATE_CUSTOMER = gql`
mutation createCustomer($phoneNumber: String) {
createCustomer(phoneNumber: $phoneNumber) {
phone
}
}
`
const useBaseStyles = makeStyles(baseStyles)
const getFiltersObj = filters =>
@ -64,6 +74,7 @@ const Customers = () => {
const [filteredCustomers, setFilteredCustomers] = useState([])
const [variables, setVariables] = useState({})
const [filters, setFilters] = useState([])
const [showCreationModal, setShowCreationModal] = useState(false)
const {
data: customersResponse,
@ -78,6 +89,11 @@ const Customers = () => {
GET_CUSTOMER_FILTERS
)
const [createNewCustomer] = useMutation(CREATE_CUSTOMER, {
onCompleted: () => setShowCreationModal(false),
refetchQueries: () => ['configAndCustomers']
})
const configData = R.path(['config'])(customersResponse) ?? []
const locale = configData && fromNamespace(namespaces.LOCALE, configData)
const customersData = R.sortWith([
@ -139,7 +155,7 @@ const Customers = () => {
<TitleSection
title="Customers"
appendix={
<div>
<div className={baseStyles.buttonsWrapper}>
<SearchBox
loading={loadingFilters}
filters={filters}
@ -149,7 +165,13 @@ const Customers = () => {
/>
</div>
}
appendixClassName={baseStyles.buttonsWrapper}
appendixRight={
<Box display="flex">
<Link color="primary" onClick={() => setShowCreationModal(true)}>
Add new user
</Link>
</Box>
}
labels={[
{ label: 'Cash-in', icon: <TxInIcon /> },
{ label: 'Cash-out', icon: <TxOutIcon /> }
@ -169,6 +191,12 @@ const Customers = () => {
onClick={handleCustomerClicked}
loading={customerLoading}
/>
<CreateCustomerModal
showModal={showCreationModal}
handleClose={() => setShowCreationModal(false)}
locale={locale}
onSubmit={createNewCustomer}
/>
</>
)
}

View file

@ -19,36 +19,36 @@ const CustomersList = ({ data, locale, onClick, loading }) => {
const elements = [
{
header: 'Phone',
width: 175,
width: 199,
view: it => getFormattedPhone(it.phone, locale.country)
},
{
header: 'Name',
width: 247,
width: 241,
view: getName
},
{
header: 'Total TXs',
width: 130,
width: 126,
textAlign: 'right',
view: it => `${Number.parseInt(it.totalTxs)}`
},
{
header: 'Total spent',
width: 155,
width: 152,
textAlign: 'right',
view: it =>
`${Number.parseFloat(it.totalSpent)} ${it.lastTxFiatCode ?? ''}`
},
{
header: 'Last active',
width: 137,
width: 133,
view: it =>
(it.lastActive && format('yyyy-MM-dd', new Date(it.lastActive))) ?? ''
},
{
header: 'Last transaction',
width: 165,
width: 161,
textAlign: 'right',
view: it => {
const hasLastTx = !R.isNil(it.lastTxFiatCode)

View file

@ -0,0 +1,139 @@
import { makeStyles } from '@material-ui/core/styles'
import { Field, Form, Formik } from 'formik'
import { PhoneNumberFormat, PhoneNumberUtil } from 'google-libphonenumber'
import * as R from 'ramda'
import React from 'react'
import * as Yup from 'yup'
import ErrorMessage from 'src/components/ErrorMessage'
import Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs/formik'
import { H1 } from 'src/components/typography'
import { spacer, primaryColor, fontPrimary } from 'src/styling/variables'
const styles = {
modalTitle: {
marginTop: -5,
color: primaryColor,
fontFamily: fontPrimary
},
footer: {
display: 'flex',
flexDirection: 'row',
margin: [['auto', 0, spacer * 3, 0]]
},
form: {
display: 'flex',
flexDirection: 'column',
height: '100%'
},
submit: {
margin: [['auto', 0, 0, 'auto']]
}
}
const pnUtilInstance = PhoneNumberUtil.getInstance()
const getValidationSchema = countryCodes =>
Yup.object().shape({
phoneNumber: Yup.string()
.required('A phone number is required')
.test('is-valid-number', 'That is not a valid phone number', value => {
try {
const validMap = R.map(it => {
const number = pnUtilInstance.parseAndKeepRawInput(value, it)
return pnUtilInstance.isValidNumber(number)
}, countryCodes)
return R.any(it => it === true, validMap)
} catch (e) {}
})
.trim()
})
const formatPhoneNumber = (countryCodes, numberStr) => {
const matchedCountry = R.find(it => {
const number = pnUtilInstance.parseAndKeepRawInput(numberStr, it)
return pnUtilInstance.isValidNumber(number)
}, countryCodes)
const matchedNumber = pnUtilInstance.parseAndKeepRawInput(
numberStr,
matchedCountry
)
return pnUtilInstance.format(matchedNumber, PhoneNumberFormat.E164)
}
const initialValues = {
phoneNumber: ''
}
const useStyles = makeStyles(styles)
const getErrorMsg = (formikErrors, formikTouched) => {
if (!formikErrors || !formikTouched) return null
if (formikErrors.phoneNumber && formikTouched.phoneNumber)
return formikErrors.phoneNumber
return null
}
const CreateCustomerModal = ({ showModal, handleClose, onSubmit, locale }) => {
const classes = useStyles()
const possibleCountries = R.append(
locale?.country,
R.map(it => it.country, locale?.overrides ?? [])
)
return (
<Modal
closeOnBackdropClick={true}
width={600}
height={300}
handleClose={handleClose}
open={showModal}>
<Formik
validationSchema={getValidationSchema(possibleCountries)}
initialValues={initialValues}
validateOnChange={false}
onSubmit={values => {
onSubmit({
variables: {
phoneNumber: formatPhoneNumber(
possibleCountries,
values.phoneNumber
)
}
})
}}>
{({ errors, touched }) => (
<Form id="customer-registration-form" className={classes.form}>
<H1 className={classes.modalTitle}>Create new customer</H1>
<Field
component={TextInput}
name="phoneNumber"
width={338}
autoFocus
label="Phone number"
/>
<div className={classes.footer}>
{getErrorMsg(errors, touched) && (
<ErrorMessage>{getErrorMsg(errors, touched)}</ErrorMessage>
)}
<Button
type="submit"
form="customer-registration-form"
className={classes.submit}>
Finish
</Button>
</div>
</Form>
)}
</Formik>
</Modal>
)
}
export default CreateCustomerModal