feat: add edit, delete and image input
This commit is contained in:
parent
421543f0c7
commit
3de7ae2fe9
12 changed files with 545 additions and 139 deletions
153
lib/customers.js
153
lib/customers.js
|
|
@ -138,6 +138,103 @@ async function updateCustomer (id, data, userToken) {
|
|||
return getCustomerById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all customer record
|
||||
*
|
||||
* @name save
|
||||
* @function
|
||||
*
|
||||
* @param {string} id Customer's id
|
||||
* @param {object} data Fields to update
|
||||
*
|
||||
* @returns {Promise} Newly updated Customer
|
||||
*/
|
||||
|
||||
function edit (id, data, userToken) {
|
||||
const defaults = [
|
||||
'front_camera',
|
||||
'id_card_data',
|
||||
'id_card_photo',
|
||||
'us_ssn',
|
||||
'subcriber_info',
|
||||
'name'
|
||||
]
|
||||
const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, _.omitBy(_.isNil, data)))
|
||||
if (_.isEmpty(filteredData)) return getCustomerById(id)
|
||||
if (filteredData.front_camera_data || filteredData.id_card_photo_data) return getCustomerById(id)
|
||||
const formattedData = enhanceEditedFields(filteredData, userToken)
|
||||
const defaultDbData = {
|
||||
customer_id: id,
|
||||
created: new Date(),
|
||||
...formattedData
|
||||
}
|
||||
console.log(formattedData, 'FORMATED', defaultDbData, 'DEFAULT DB')
|
||||
const cs = new Pgp.helpers.ColumnSet(_.keys(defaultDbData),
|
||||
{ table: 'edited_customer_data' })
|
||||
const onConflict = ' ON CONFLICT (customer_id) DO UPDATE SET ' +
|
||||
cs.assignColumns({ from: 'EXCLUDED', skip: ['customer_id', 'created'] })
|
||||
const upsert = Pgp.helpers.insert(defaultDbData, cs) + onConflict
|
||||
db.none(upsert)
|
||||
.then(res => {
|
||||
console.log(res)
|
||||
return getCustomerById(id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add *edited_by and *edited_at fields with acting user's token
|
||||
* and date of override respectively before saving to db.
|
||||
*
|
||||
* @name enhanceEditedFields
|
||||
* @function
|
||||
*
|
||||
* @param {object} fields Fields to be enhanced
|
||||
* @param {string} userToken Acting user's token
|
||||
* @returns {object} fields enhanced with *_by and *_at fields
|
||||
*/
|
||||
|
||||
function enhanceEditedFields (fields, userToken) {
|
||||
if (!userToken) return fields
|
||||
_.mapKeys((field) => {
|
||||
fields[field + '_edited_by'] = userToken
|
||||
fields[field + '_edited_at'] = 'now()^'
|
||||
}, fields)
|
||||
return fields
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the edited data from the db record
|
||||
*
|
||||
* @name enhanceOverrideFields
|
||||
* @function
|
||||
*
|
||||
* @param {string} id Customer's id
|
||||
* @param {object} data Fields to be deleted
|
||||
*
|
||||
* @returns {Promise} Newly updated Customer
|
||||
*
|
||||
*/
|
||||
|
||||
function deleteEditedData (id, data) {
|
||||
// NOT IMPLEMENTING THIS FEATURE FOR NOW
|
||||
// const defaults = [
|
||||
// 'front_camera',
|
||||
// 'id_card_data',
|
||||
// 'id_card_photo',
|
||||
// 'us_ssn',
|
||||
// 'subcriber_info',
|
||||
// 'name'
|
||||
// ]
|
||||
// const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, data))
|
||||
// if (_.isEmpty(filteredData)) return getCustomerById(id)
|
||||
|
||||
// const cs = new Pgp.helpers.ColumnSet(_.keys(filteredData),
|
||||
// { table: 'edited_customer_data' })
|
||||
// const update = Pgp.helpers.update(filteredData, cs)
|
||||
// db.none(update)
|
||||
return getCustomerById(id)
|
||||
}
|
||||
|
||||
const invalidateCustomerNotifications = (id, data) => {
|
||||
if (data.authorized_override !== 'verified') return Promise.resolve()
|
||||
|
||||
|
|
@ -510,7 +607,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
|||
AND ($6 IS NULL OR id_card_data::json->>'address' = $6)
|
||||
AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7)
|
||||
limit $3`
|
||||
return db.any(sql, [passableErrorCodes, anonymous.uuid, NUM_RESULTS, phone, name, address, id ])
|
||||
return db.any(sql, [ passableErrorCodes, anonymous.uuid, NUM_RESULTS, phone, name, address, id ])
|
||||
.then(customers => Promise.all(_.map(customer => {
|
||||
return populateOverrideUsernames(customer)
|
||||
.then(camelize)
|
||||
|
|
@ -518,11 +615,11 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
|||
}
|
||||
|
||||
/**
|
||||
* Query all customers, ordered by last activity
|
||||
* Query a specific customer, ordered by last activity
|
||||
* and with aggregate columns based on their
|
||||
* transactions
|
||||
*
|
||||
* @returns {array} Array of customers with it's transactions aggregations
|
||||
* @returns {array} A single customer instance with non edited
|
||||
*/
|
||||
function getCustomerById (id) {
|
||||
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
||||
|
|
@ -560,6 +657,17 @@ function getCustomerById (id) {
|
|||
.then(camelize)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the specific customer manually edited data
|
||||
*
|
||||
* @param {String} id customer id
|
||||
*
|
||||
* @returns {array} A single customer instance with the most recent data
|
||||
*/
|
||||
function getManuallyEditedData (id) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} id customer id
|
||||
* @param {Object} patch customer update record
|
||||
|
|
@ -768,35 +876,35 @@ function updateFrontCamera (id, patch) {
|
|||
})
|
||||
}
|
||||
|
||||
function addCustomField(customerId, label, value) {
|
||||
function addCustomField (customerId, label, value) {
|
||||
const sql = `SELECT * FROM custom_field_definitions WHERE label=$1 LIMIT 1`
|
||||
return db.oneOrNone(sql, [label])
|
||||
.then(res => db.tx(t => {
|
||||
if (_.isNil(res)) {
|
||||
const fieldId = uuid.v4()
|
||||
const q1 = t.none(`INSERT INTO custom_field_definitions (id, label) VALUES ($1, $2)`, [fieldId, label])
|
||||
const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, fieldId, value])
|
||||
return t.batch([q1, q2])
|
||||
}
|
||||
if (_.isNil(res)) {
|
||||
const fieldId = uuid.v4()
|
||||
const q1 = t.none(`INSERT INTO custom_field_definitions (id, label) VALUES ($1, $2)`, [fieldId, label])
|
||||
const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, fieldId, value])
|
||||
return t.batch([q1, q2])
|
||||
}
|
||||
|
||||
if (!_.isNil(res) && !res.active) {
|
||||
const q1 = t.none(`UPDATE custom_field_definitions SET active = true WHERE id=$1`, [res.id])
|
||||
const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value])
|
||||
return t.batch([q1, q2])
|
||||
} else if (!_.isNil(res) && res.active) {
|
||||
const q1 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value])
|
||||
return t.batch([q1])
|
||||
}
|
||||
})
|
||||
if (!_.isNil(res) && !res.active) {
|
||||
const q1 = t.none(`UPDATE custom_field_definitions SET active = true WHERE id=$1`, [res.id])
|
||||
const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value])
|
||||
return t.batch([q1, q2])
|
||||
} else if (!_.isNil(res) && res.active) {
|
||||
const q1 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value])
|
||||
return t.batch([q1])
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function saveCustomField(customerId, fieldId, newValue) {
|
||||
function saveCustomField (customerId, fieldId, newValue) {
|
||||
const sql = `UPDATE customer_custom_field_pairs SET value=$1 WHERE customer_id=$2 AND custom_field_id=$3`
|
||||
return db.none(sql, [newValue, customerId, fieldId])
|
||||
}
|
||||
|
||||
function removeCustomField(customerId, fieldId) {
|
||||
function removeCustomField (customerId, fieldId) {
|
||||
const sql = `SELECT * FROM customer_custom_field_pairs WHERE custom_field_id=$1`
|
||||
return db.any(sql, [fieldId])
|
||||
.then(res => db.tx(t => {
|
||||
|
|
@ -827,5 +935,8 @@ module.exports = {
|
|||
addCustomField,
|
||||
saveCustomField,
|
||||
removeCustomField,
|
||||
edit,
|
||||
getManuallyEditedData,
|
||||
deleteEditedData,
|
||||
updateTxCustomerPhoto
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,14 @@ const resolvers = {
|
|||
},
|
||||
addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value),
|
||||
saveCustomField: (...[, { customerId, fieldId, newValue }]) => customers.saveCustomField(customerId, fieldId, newValue),
|
||||
removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId)
|
||||
removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId),
|
||||
editCustomer: (root, { customerId, customerEdit }, context) => {
|
||||
const token = !!context.req.cookies.lid && context.req.session.user.id
|
||||
return customers.edit(customerId, customerEdit, token)
|
||||
},
|
||||
deleteEditedData: (root, { customerId, customerEdit }) => {
|
||||
return customers.deleteEditedData(customerId, customerEdit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const typeDef = gql`
|
|||
authorizedOverride: String
|
||||
daysSuspended: Int
|
||||
isSuspended: Boolean
|
||||
frontCamera: String
|
||||
frontCameraPath: String
|
||||
frontCameraAt: Date
|
||||
frontCameraOverride: String
|
||||
|
|
@ -21,6 +22,7 @@ const typeDef = gql`
|
|||
idCardData: JSONObject
|
||||
idCardDataOverride: String
|
||||
idCardDataExpiration: Date
|
||||
idCardPhoto: String
|
||||
idCardPhotoPath: String
|
||||
idCardPhotoOverride: String
|
||||
usSsn: String
|
||||
|
|
@ -65,6 +67,13 @@ const typeDef = gql`
|
|||
subscriberInfo: Boolean
|
||||
}
|
||||
|
||||
input CustomerEdit {
|
||||
frontCamera: String
|
||||
idCardData: JSONObject
|
||||
idCardPhoto: String
|
||||
usSsn: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
customers(phone: String, name: String, address: String, id: String): [Customer] @auth
|
||||
customer(customerId: ID!): Customer @auth
|
||||
|
|
@ -76,6 +85,8 @@ const typeDef = gql`
|
|||
addCustomField(customerId: ID!, label: String!, value: String!): CustomerCustomField @auth
|
||||
saveCustomField(customerId: ID!, fieldId: ID!, value: String!): CustomerCustomField @auth
|
||||
removeCustomField(customerId: ID!, fieldId: ID!): CustomerCustomField @auth
|
||||
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
||||
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
33
migrations/1635159374499-editable-customer-data.js
Normal file
33
migrations/1635159374499-editable-customer-data.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
`CREATE TABLE edited_customer_data (
|
||||
customer_id uuid PRIMARY KEY REFERENCES customers(id),
|
||||
id_card_data JSON,
|
||||
id_card_data_edited_at TIMESTAMPTZ,
|
||||
id_card_data_edited_by UUID REFERENCES users(id),
|
||||
front_camera_path TEXT,
|
||||
front_camera_edited_at TIMESTAMPTZ,
|
||||
front_camera_edited_by UUID REFERENCES users(id),
|
||||
id_card_photo_path TEXT,
|
||||
id_card_photo_edited_at TIMESTAMPTZ,
|
||||
id_card_photo_edited_by UUID REFERENCES users(id),
|
||||
subscriber_info JSON,
|
||||
subscriber_info_edited_at TIMESTAMPTZ,
|
||||
subscriber_info_edited_by UUID REFERENCES users(id),
|
||||
name TEXT,
|
||||
name_info_edited_at TIMESTAMPTZ,
|
||||
name_info_edited_by UUID REFERENCES users(id),
|
||||
us_ssn TEXT,
|
||||
us_ssn_edited_at TIMESTAMPTZ,
|
||||
us_ssn_edited_by UUID REFERENCES users(id),
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT now() )`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ const ActionButton = memo(
|
|||
const classNames = {
|
||||
[classes.actionButton]: true,
|
||||
[classes.primary]: color === 'primary',
|
||||
[classes.secondary]: color === 'secondary'
|
||||
[classes.secondary]: color === 'secondary',
|
||||
[classes.spring]: color === 'spring',
|
||||
[classes.tomato]: color === 'tomato'
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ import {
|
|||
subheaderColor,
|
||||
subheaderDarkColor,
|
||||
offColor,
|
||||
offDarkColor
|
||||
offDarkColor,
|
||||
secondaryColor,
|
||||
secondaryColorDark,
|
||||
secondaryColorDarker,
|
||||
errorColor,
|
||||
errorColorDarker
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const { p } = typographyStyles
|
||||
|
|
@ -68,6 +73,42 @@ export default {
|
|||
display: 'flex'
|
||||
}
|
||||
},
|
||||
spring: {
|
||||
extend: colors(secondaryColor, secondaryColorDark, secondaryColorDarker),
|
||||
color: white,
|
||||
'&:active': {
|
||||
'& $actionButtonIcon': {
|
||||
display: 'flex'
|
||||
},
|
||||
'& $actionButtonIconActive': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
'& $actionButtonIcon': {
|
||||
display: 'none'
|
||||
},
|
||||
'& $actionButtonIconActive': {
|
||||
display: 'flex'
|
||||
}
|
||||
},
|
||||
tomato: {
|
||||
extend: colors(errorColor, errorColorDarker, errorColor),
|
||||
color: white,
|
||||
'&:active': {
|
||||
'& $actionButtonIcon': {
|
||||
display: 'flex'
|
||||
},
|
||||
'& $actionButtonIconActive': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
'& $actionButtonIcon': {
|
||||
display: 'none'
|
||||
},
|
||||
'& $actionButtonIconActive': {
|
||||
display: 'flex'
|
||||
}
|
||||
},
|
||||
actionButtonIcon: {
|
||||
display: 'flex',
|
||||
paddingRight: 7,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,12 @@ const Photo = ({ show, src }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const CustomerData = ({ customer, updateCustomer }) => {
|
||||
const CustomerData = ({
|
||||
customer,
|
||||
updateCustomer,
|
||||
editCustomer,
|
||||
deleteEditedData
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const [listView, setListView] = useState(false)
|
||||
|
||||
|
|
@ -75,11 +80,14 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
: 'Failed'
|
||||
|
||||
const customEntries = null // get customer custom entries
|
||||
const customRequirements = null // get customer custom requirements
|
||||
|
||||
const isEven = elem => elem % 2 === 0
|
||||
|
||||
const getVisibleCards = _.filter(
|
||||
elem => !_.isEmpty(elem.data) || !_.isNil(elem.children)
|
||||
elem =>
|
||||
!_.isEmpty(elem.fields) ||
|
||||
(!_.isNil(elem.children) && !_.isNil(elem.state))
|
||||
)
|
||||
|
||||
const getAvailableFields = _.filter(({ value }) => value !== '')
|
||||
|
|
@ -96,6 +104,12 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
}),
|
||||
usSsn: Yup.object().shape({
|
||||
usSsn: Yup.string()
|
||||
}),
|
||||
idCardPhoto: Yup.object().shape({
|
||||
idCardPhoto: Yup.mixed()
|
||||
}),
|
||||
frontCamera: Yup.object().shape({
|
||||
frontCamera: Yup.mixed()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +174,7 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
|
||||
const usSsnElements = [
|
||||
{
|
||||
name: 'us ssn',
|
||||
name: 'usSsn',
|
||||
label: 'US SSN',
|
||||
value: `${customer.usSsn ?? ''}`,
|
||||
component: TextInput,
|
||||
|
|
@ -168,6 +182,9 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
}
|
||||
]
|
||||
|
||||
const idCardPhotoElements = [{ name: 'idCardPhoto' }]
|
||||
const frontCameraElements = [{ name: 'frontCamera' }]
|
||||
|
||||
const initialValues = {
|
||||
idScan: {
|
||||
name: '',
|
||||
|
|
@ -180,19 +197,26 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
},
|
||||
usSsn: {
|
||||
usSsn: ''
|
||||
},
|
||||
frontCamera: {
|
||||
frontCamera: null
|
||||
},
|
||||
idCardPhoto: {
|
||||
idCardPhoto: null
|
||||
}
|
||||
}
|
||||
|
||||
const cards = [
|
||||
{
|
||||
data: getAvailableFields(idScanElements),
|
||||
fields: getAvailableFields(idScanElements),
|
||||
title: 'ID Scan',
|
||||
titleIcon: <PhoneIcon className={classes.cardIcon} />,
|
||||
state: R.path(['idCardDataOverride'])(customer),
|
||||
authorize: () =>
|
||||
updateCustomer({ idCardDataOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ idCardDataOverride: OVERRIDE_REJECTED }),
|
||||
save: values => console.log(values),
|
||||
deleteEditedData: () => deleteEditedData({ idCardData: null }),
|
||||
save: values => editCustomer({ idCardData: values }),
|
||||
validationSchema: schemas.idScan,
|
||||
initialValues: initialValues.idScan
|
||||
},
|
||||
|
|
@ -217,17 +241,18 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
authorize: () =>
|
||||
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }),
|
||||
save: () => {},
|
||||
children: <Info3>{sanctionsDisplay}</Info3>
|
||||
},
|
||||
{
|
||||
fields: getAvailableFields(frontCameraElements),
|
||||
title: 'Front facing camera',
|
||||
titleIcon: <EditIcon className={classes.editIcon} />,
|
||||
state: R.path(['frontCameraOverride'])(customer),
|
||||
authorize: () =>
|
||||
updateCustomer({ frontCameraOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ frontCameraOverride: OVERRIDE_REJECTED }),
|
||||
save: () => {},
|
||||
save: values => editCustomer({ frontCamera: values.frontCamera }),
|
||||
deleteEditedData: () => deleteEditedData({ frontCamera: null }),
|
||||
children: customer.frontCameraPath ? (
|
||||
<Photo
|
||||
show={customer.frontCameraPath}
|
||||
|
|
@ -235,31 +260,40 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
customer
|
||||
)}`}
|
||||
/>
|
||||
) : null
|
||||
) : null,
|
||||
hasImage: true,
|
||||
validationSchema: schemas.frontCamera,
|
||||
initialValues: initialValues.frontCamera
|
||||
},
|
||||
{
|
||||
fields: getAvailableFields(idCardPhotoElements),
|
||||
title: 'ID card image',
|
||||
titleIcon: <EditIcon className={classes.editIcon} />,
|
||||
state: R.path(['idCardPhotoOverride'])(customer),
|
||||
authorize: () =>
|
||||
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED }),
|
||||
save: () => {},
|
||||
save: values => editCustomer({ idCardPhoto: values.idCardPhoto }),
|
||||
deleteEditedData: () => deleteEditedData({ idCardPhoto: null }),
|
||||
children: customer.idCardPhotoPath ? (
|
||||
<Photo
|
||||
show={customer.idCardPhotoPath}
|
||||
src={`${URI}/id-card-photo/${R.path(['idCardPhotoPath'])(customer)}`}
|
||||
/>
|
||||
) : null
|
||||
) : null,
|
||||
hasImage: true,
|
||||
validationSchema: schemas.idCardPhoto,
|
||||
initialValues: initialValues.idCardPhoto
|
||||
},
|
||||
{
|
||||
data: getAvailableFields(usSsnElements),
|
||||
fields: getAvailableFields(usSsnElements),
|
||||
title: 'US SSN',
|
||||
titleIcon: <CardIcon className={classes.cardIcon} />,
|
||||
state: R.path(['usSsnOverride'])(customer),
|
||||
authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }),
|
||||
save: () => {},
|
||||
save: values => editCustomer({ usSsn: values.usSsn }),
|
||||
deleteEditedData: () => deleteEditedData({ usSsn: null }),
|
||||
validationSchema: schemas.usSsn,
|
||||
initialValues: initialValues.usSsn
|
||||
}
|
||||
|
|
@ -272,11 +306,13 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
reject,
|
||||
state,
|
||||
titleIcon,
|
||||
data,
|
||||
fields,
|
||||
save,
|
||||
deleteEditedData,
|
||||
children,
|
||||
validationSchema,
|
||||
initialValues
|
||||
initialValues,
|
||||
hasImage
|
||||
},
|
||||
idx
|
||||
) => {
|
||||
|
|
@ -288,11 +324,13 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
reject={reject}
|
||||
state={state}
|
||||
titleIcon={titleIcon}
|
||||
data={data}
|
||||
hasImage={hasImage}
|
||||
fields={fields}
|
||||
children={children}
|
||||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
save={save}></EditableCard>
|
||||
save={save}
|
||||
deleteEditedData={deleteEditedData}></EditableCard>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +355,7 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
onClick={() => setListView(true)}></FeatureButton>
|
||||
</div>
|
||||
<div>
|
||||
{!listView && (
|
||||
{!listView && customer && (
|
||||
<Grid container>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
{visibleCards.map((elem, idx) => {
|
||||
|
|
@ -333,7 +371,12 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
|||
)}
|
||||
{customEntries && (
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.separator}>{'Custom data entry'}</div>
|
||||
<span className={classes.separator}>Custom data entry</span>
|
||||
</div>
|
||||
)}
|
||||
{customRequirements && (
|
||||
<div className={classes.wrapper}>
|
||||
<span className={classes.separator}>Custom requirements</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,22 +20,30 @@ export default {
|
|||
marginRight: 12
|
||||
},
|
||||
wrapper: {
|
||||
display: 'flex'
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
separator: {
|
||||
display: 'flex',
|
||||
flexBasis: '100%',
|
||||
justifyContent: 'center',
|
||||
color: offColor,
|
||||
margin: [[8, 0, 8, 0]],
|
||||
'&::before, &::after': {
|
||||
content: '',
|
||||
flexGrow: 1,
|
||||
margin: [[8, 0, 8, 150]],
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
'&:before, &:after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
background: offColor,
|
||||
height: 1,
|
||||
fontSize: 1,
|
||||
lineHeight: 0,
|
||||
margin: [[0, 8, 0, 8]]
|
||||
top: '50%',
|
||||
width: 1000,
|
||||
height: 1
|
||||
},
|
||||
'&:before': {
|
||||
right: '100%',
|
||||
marginRight: 15
|
||||
},
|
||||
'&:after': {
|
||||
left: '100%',
|
||||
marginLeft: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,29 @@ const SET_CUSTOMER = gql`
|
|||
}
|
||||
}
|
||||
`
|
||||
const EDIT_CUSTOMER = gql`
|
||||
mutation editCustomer($customerId: ID!, $customerEdit: CustomerEdit) {
|
||||
editCustomer(customerId: $customerId, customerEdit: $customerEdit) {
|
||||
id
|
||||
frontCamera
|
||||
idCardData
|
||||
idCardPhoto
|
||||
usSsn
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const DELETE_EDITED_CUSTOMER = gql`
|
||||
mutation deleteEditedData($customerId: ID!, $customerEdit: CustomerEdit) {
|
||||
deleteEditedData(customerId: $customerId, customerEdit: $customerEdit) {
|
||||
id
|
||||
frontCamera
|
||||
idCardData
|
||||
idCardPhoto
|
||||
usSsn
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CustomerProfile = memo(() => {
|
||||
const history = useHistory()
|
||||
|
|
@ -133,6 +156,14 @@ const CustomerProfile = memo(() => {
|
|||
}
|
||||
)
|
||||
|
||||
const [editCustomerData] = useMutation(EDIT_CUSTOMER, {
|
||||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
||||
const [deleteCustomerEditedData] = useMutation(DELETE_EDITED_CUSTOMER, {
|
||||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
||||
const [setCustomer] = useMutation(SET_CUSTOMER, {
|
||||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
|
@ -145,6 +176,22 @@ const CustomerProfile = memo(() => {
|
|||
}
|
||||
})
|
||||
|
||||
const editCustomer = it =>
|
||||
editCustomerData({
|
||||
variables: {
|
||||
customerId,
|
||||
customerEdit: it
|
||||
}
|
||||
})
|
||||
|
||||
const deleteEditedData = it =>
|
||||
deleteCustomerEditedData({
|
||||
variables: {
|
||||
customerId,
|
||||
customerEdit: it
|
||||
}
|
||||
})
|
||||
|
||||
const onClickSidebarItem = code => setClickedItem(code)
|
||||
|
||||
const configData = R.path(['config'])(customerResponse) ?? []
|
||||
|
|
@ -295,7 +342,9 @@ const CustomerProfile = memo(() => {
|
|||
<div>
|
||||
<CustomerData
|
||||
customer={customerData}
|
||||
updateCustomer={updateCustomer}></CustomerData>
|
||||
updateCustomer={updateCustomer}
|
||||
editCustomer={editCustomer}
|
||||
deleteEditedData={deleteEditedData}></CustomerData>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { CardContent, Card, Grid } from '@material-ui/core'
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import { Form, Formik, Field as FormikField } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import { useState, React } from 'react'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
|
|
@ -15,12 +16,16 @@ import {
|
|||
OVERRIDE_REJECTED,
|
||||
OVERRIDE_PENDING
|
||||
} from 'src/pages/Customers/components/propertyCard'
|
||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
import { ReactComponent as DeleteReversedIcon } from 'src/styling/icons/action/delete/white.svg'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
import { ReactComponent as EditReversedIcon } from 'src/styling/icons/action/edit/white.svg'
|
||||
import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.svg'
|
||||
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
|
||||
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/white.svg'
|
||||
import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/white.svg'
|
||||
import { ReactComponent as CancelReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
|
||||
import { ReactComponent as CancelIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
|
||||
import { ReactComponent as ReplaceReversedIcon } from 'src/styling/icons/button/replace/white.svg'
|
||||
import { ReactComponent as ReplaceIcon } from 'src/styling/icons/button/replace/zodiac.svg'
|
||||
import { ReactComponent as SaveReversedIcon } from 'src/styling/icons/circle buttons/save/white.svg'
|
||||
import { ReactComponent as SaveIcon } from 'src/styling/icons/circle buttons/save/zodiac.svg'
|
||||
import { comet } from 'src/styling/variables'
|
||||
|
|
@ -70,7 +75,7 @@ const fieldUseStyles = makeStyles(fieldStyles)
|
|||
|
||||
const EditableField = ({ editing, field, size, ...props }) => {
|
||||
const classes = fieldUseStyles()
|
||||
|
||||
console.log('FIELDDDDDDDD', field)
|
||||
const classNames = {
|
||||
[classes.field]: true,
|
||||
[classes.notEditing]: !editing
|
||||
|
|
@ -90,8 +95,9 @@ const EditableField = ({ editing, field, size, ...props }) => {
|
|||
<FormikField
|
||||
className={classes.editing}
|
||||
id={field.name}
|
||||
component={field.component}
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
component={field.component}
|
||||
type={field.type}
|
||||
width={size}
|
||||
{...props}
|
||||
|
|
@ -103,20 +109,27 @@ const EditableField = ({ editing, field, size, ...props }) => {
|
|||
}
|
||||
|
||||
const EditableCard = ({
|
||||
data,
|
||||
fields,
|
||||
save,
|
||||
authorize,
|
||||
hasImage,
|
||||
reject,
|
||||
state,
|
||||
title,
|
||||
titleIcon,
|
||||
children
|
||||
children,
|
||||
validationSchema,
|
||||
initialValues,
|
||||
deleteEditedData
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [input, setInput] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const triggerInput = () => input.click()
|
||||
|
||||
const label1ClassNames = {
|
||||
[classes.label1]: true,
|
||||
[classes.label1Pending]: state === OVERRIDE_PENDING,
|
||||
|
|
@ -130,9 +143,7 @@ const EditableCard = ({
|
|||
? { label: 'Rejected', type: 'error' }
|
||||
: { label: 'Accepted', type: 'success' }
|
||||
|
||||
const editableField = field => {
|
||||
return <EditableField field={field} editing={editing} size={180} />
|
||||
}
|
||||
const reader = new FileReader()
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -142,99 +153,172 @@ const EditableCard = ({
|
|||
{titleIcon}
|
||||
<H3 className={classes.cardTitle}>{title}</H3>
|
||||
<Tooltip width={304}></Tooltip>
|
||||
<div className={classnames(label1ClassNames)}>
|
||||
<MainStatus statuses={[authorized]} />
|
||||
</div>
|
||||
{state && (
|
||||
<div className={classnames(label1ClassNames)}>
|
||||
<MainStatus statuses={[authorized]} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
enableReinitialize
|
||||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={values => save(values)}
|
||||
onReset={() => {
|
||||
setEditing(false)
|
||||
setError(false)
|
||||
}}>
|
||||
<Form>
|
||||
<PromptWhenDirty />
|
||||
<div className={classes.row}>
|
||||
<Grid container>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
{data?.map((field, idx) => {
|
||||
return idx >= 0 && idx < 4 ? editableField(field) : null
|
||||
})}
|
||||
{({ values, touched, errors, setFieldValue }) => (
|
||||
<Form>
|
||||
<PromptWhenDirty />
|
||||
{console.log(values, touched, errors, 'FORMIK STATUS')}
|
||||
<div className={classes.row}>
|
||||
<Grid container>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
{!hasImage &&
|
||||
fields?.map((field, idx) => {
|
||||
return idx >= 0 && idx < 4 ? (
|
||||
<EditableField
|
||||
field={field}
|
||||
editing={editing}
|
||||
size={180}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
</Grid>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
{!hasImage &&
|
||||
fields?.map((field, idx) => {
|
||||
return idx >= 4 ? (
|
||||
<EditableField
|
||||
field={field}
|
||||
editing={editing}
|
||||
size={180}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
{data?.map((field, idx) => {
|
||||
return idx >= 4 ? editableField(field) : null
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
{children}
|
||||
<div className={classes.edit}>
|
||||
{!editing && (
|
||||
<div className={classes.editButton}>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={EditIcon}
|
||||
InverseIcon={EditReversedIcon}
|
||||
onClick={() => setEditing(true)}>
|
||||
{`Edit`}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
{editing && (
|
||||
<div className={classes.editingButtons}>
|
||||
{data && (
|
||||
<div className={classes.button}>
|
||||
</div>
|
||||
<div className={classes.edit}>
|
||||
{!editing && (
|
||||
<div className={classes.editButton}>
|
||||
<div className={classes.deleteButton}>
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
Icon={SaveIcon}
|
||||
InverseIcon={SaveReversedIcon}
|
||||
type="submit">
|
||||
Save
|
||||
color="primary"
|
||||
type="button"
|
||||
Icon={DeleteIcon}
|
||||
InverseIcon={DeleteReversedIcon}
|
||||
onClick={() => deleteEditedData()}>
|
||||
{`Delete`}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.button}>
|
||||
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
Icon={CancelIcon}
|
||||
InverseIcon={CancelReversedIcon}
|
||||
type="reset">
|
||||
Cancel
|
||||
color="primary"
|
||||
Icon={EditIcon}
|
||||
InverseIcon={EditReversedIcon}
|
||||
onClick={() => setEditing(true)}>
|
||||
{`Edit`}
|
||||
</ActionButton>
|
||||
</div>
|
||||
{authorized.label !== 'Accepted' && (
|
||||
<div className={classes.button}>
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
Icon={AuthorizeIcon}
|
||||
InverseIcon={AuthorizeReversedIcon}
|
||||
type="submit"
|
||||
onClick={() => authorize()}>
|
||||
{'Authorize'}
|
||||
</ActionButton>
|
||||
)}
|
||||
{editing && (
|
||||
<div className={classes.editingWrapper}>
|
||||
<div className={classes.replace}>
|
||||
{hasImage && (
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
type="button"
|
||||
Icon={ReplaceIcon}
|
||||
InverseIcon={ReplaceReversedIcon}
|
||||
onClick={() => triggerInput()}>
|
||||
{
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
alt=""
|
||||
className={classes.input}
|
||||
ref={fileInput => setInput(fileInput)}
|
||||
onChange={event => {
|
||||
// need to store it locally if we want to display it even after saving to db
|
||||
|
||||
const file = R.head(event.target.files)
|
||||
|
||||
reader.onloadend = () => {
|
||||
// use a regex to remove data url part
|
||||
setFieldValue(
|
||||
R.head(fields).name,
|
||||
reader.result
|
||||
.replace('data:', '')
|
||||
.replace(/^.+,/, '')
|
||||
)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
event.target.value = null
|
||||
}}
|
||||
/>
|
||||
Replace
|
||||
</div>
|
||||
}
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{authorized.label !== 'Rejected' && (
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
Icon={CancelIcon}
|
||||
InverseIcon={CancelReversedIcon}
|
||||
type="submit"
|
||||
onClick={() => reject()}>
|
||||
{'Reject'}
|
||||
</ActionButton>
|
||||
)}
|
||||
{error && (
|
||||
<ErrorMessage>Failed to save changes</ErrorMessage>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
<div className={classes.editingButtons}>
|
||||
{fields && (
|
||||
<div className={classes.button}>
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
Icon={SaveIcon}
|
||||
InverseIcon={SaveReversedIcon}
|
||||
type="submit">
|
||||
Save
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.button}>
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
Icon={CancelIcon}
|
||||
InverseIcon={CancelReversedIcon}
|
||||
type="reset">
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
{authorized.label !== 'Accepted' && (
|
||||
<div className={classes.button}>
|
||||
<ActionButton
|
||||
color="spring"
|
||||
type="button"
|
||||
Icon={AuthorizeIcon}
|
||||
InverseIcon={AuthorizeIcon}
|
||||
onClick={() => authorize()}>
|
||||
Authorize
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
{authorized.label !== 'Rejected' && (
|
||||
<ActionButton
|
||||
color="tomato"
|
||||
type="button"
|
||||
Icon={BlockIcon}
|
||||
InverseIcon={BlockIcon}
|
||||
onClick={() => reject()}>
|
||||
Reject
|
||||
</ActionButton>
|
||||
)}
|
||||
{error && (
|
||||
<ErrorMessage>Failed to save changes</ErrorMessage>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,20 @@ export default {
|
|||
display: 'flex',
|
||||
justifyContent: 'right'
|
||||
},
|
||||
deleteButton: {
|
||||
marginRight: 8
|
||||
},
|
||||
editingWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
replace: {
|
||||
marginTop: 30
|
||||
},
|
||||
input: {
|
||||
display: 'none'
|
||||
},
|
||||
button: {
|
||||
marginRight: 8
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const white = '#ffffff'
|
|||
|
||||
// Error
|
||||
const tomato = '#ff584a'
|
||||
const tomato1 = '#E45043'
|
||||
const mistyRose = '#ffeceb'
|
||||
const pumpkin = '#ff7311'
|
||||
const linen = '#fbf3ec'
|
||||
|
|
@ -47,6 +48,7 @@ const offColor = comet
|
|||
const offDarkColor = comet2
|
||||
const placeholderColor = comet
|
||||
const errorColor = tomato
|
||||
const errorColorDarker = tomato1
|
||||
const offErrorColor = mistyRose
|
||||
const inputBorderColor = primaryColor
|
||||
|
||||
|
|
@ -148,6 +150,7 @@ export {
|
|||
linkPrimaryColor,
|
||||
linkSecondaryColor,
|
||||
errorColor,
|
||||
errorColorDarker,
|
||||
offErrorColor,
|
||||
inputBorderColor,
|
||||
// font sizes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue