feat: add edit, delete and image input

This commit is contained in:
José Oliveira 2021-10-27 23:54:50 +01:00
parent 421543f0c7
commit 3de7ae2fe9
12 changed files with 545 additions and 139 deletions

View file

@ -138,6 +138,103 @@ async function updateCustomer (id, data, userToken) {
return getCustomerById(id) 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) => { const invalidateCustomerNotifications = (id, data) => {
if (data.authorized_override !== 'verified') return Promise.resolve() 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 ($6 IS NULL OR id_card_data::json->>'address' = $6)
AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7) AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7)
limit $3` 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 => { .then(customers => Promise.all(_.map(customer => {
return populateOverrideUsernames(customer) return populateOverrideUsernames(customer)
.then(camelize) .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 * and with aggregate columns based on their
* transactions * transactions
* *
* @returns {array} Array of customers with it's transactions aggregations * @returns {array} A single customer instance with non edited
*/ */
function getCustomerById (id) { function getCustomerById (id) {
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',') const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
@ -560,6 +657,17 @@ function getCustomerById (id) {
.then(camelize) .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 {String} id customer id
* @param {Object} patch customer update record * @param {Object} patch customer update record
@ -768,7 +876,7 @@ 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` const sql = `SELECT * FROM custom_field_definitions WHERE label=$1 LIMIT 1`
return db.oneOrNone(sql, [label]) return db.oneOrNone(sql, [label])
.then(res => db.tx(t => { .then(res => db.tx(t => {
@ -791,12 +899,12 @@ function addCustomField(customerId, label, value) {
) )
} }
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` 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]) 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` const sql = `SELECT * FROM customer_custom_field_pairs WHERE custom_field_id=$1`
return db.any(sql, [fieldId]) return db.any(sql, [fieldId])
.then(res => db.tx(t => { .then(res => db.tx(t => {
@ -827,5 +935,8 @@ module.exports = {
addCustomField, addCustomField,
saveCustomField, saveCustomField,
removeCustomField, removeCustomField,
edit,
getManuallyEditedData,
deleteEditedData,
updateTxCustomerPhoto updateTxCustomerPhoto
} }

View file

@ -19,7 +19,14 @@ const resolvers = {
}, },
addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value), addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value),
saveCustomField: (...[, { customerId, fieldId, newValue }]) => customers.saveCustomField(customerId, fieldId, newValue), 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)
}
} }
} }

View file

@ -12,6 +12,7 @@ const typeDef = gql`
authorizedOverride: String authorizedOverride: String
daysSuspended: Int daysSuspended: Int
isSuspended: Boolean isSuspended: Boolean
frontCamera: String
frontCameraPath: String frontCameraPath: String
frontCameraAt: Date frontCameraAt: Date
frontCameraOverride: String frontCameraOverride: String
@ -21,6 +22,7 @@ const typeDef = gql`
idCardData: JSONObject idCardData: JSONObject
idCardDataOverride: String idCardDataOverride: String
idCardDataExpiration: Date idCardDataExpiration: Date
idCardPhoto: String
idCardPhotoPath: String idCardPhotoPath: String
idCardPhotoOverride: String idCardPhotoOverride: String
usSsn: String usSsn: String
@ -65,6 +67,13 @@ const typeDef = gql`
subscriberInfo: Boolean subscriberInfo: Boolean
} }
input CustomerEdit {
frontCamera: String
idCardData: JSONObject
idCardPhoto: String
usSsn: String
}
type Query { type Query {
customers(phone: String, name: String, address: String, id: String): [Customer] @auth customers(phone: String, name: String, address: String, id: String): [Customer] @auth
customer(customerId: ID!): Customer @auth customer(customerId: ID!): Customer @auth
@ -76,6 +85,8 @@ const typeDef = gql`
addCustomField(customerId: ID!, label: String!, value: String!): CustomerCustomField @auth addCustomField(customerId: ID!, label: String!, value: String!): CustomerCustomField @auth
saveCustomField(customerId: ID!, fieldId: ID!, value: String!): CustomerCustomField @auth saveCustomField(customerId: ID!, fieldId: ID!, value: String!): CustomerCustomField @auth
removeCustomField(customerId: ID!, fieldId: ID!): CustomerCustomField @auth removeCustomField(customerId: ID!, fieldId: ID!): CustomerCustomField @auth
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
} }
` `

View 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()
}

View file

@ -12,7 +12,9 @@ const ActionButton = memo(
const classNames = { const classNames = {
[classes.actionButton]: true, [classes.actionButton]: true,
[classes.primary]: color === 'primary', [classes.primary]: color === 'primary',
[classes.secondary]: color === 'secondary' [classes.secondary]: color === 'secondary',
[classes.spring]: color === 'spring',
[classes.tomato]: color === 'tomato'
} }
return ( return (

View file

@ -5,7 +5,12 @@ import {
subheaderColor, subheaderColor,
subheaderDarkColor, subheaderDarkColor,
offColor, offColor,
offDarkColor offDarkColor,
secondaryColor,
secondaryColorDark,
secondaryColorDarker,
errorColor,
errorColorDarker
} from 'src/styling/variables' } from 'src/styling/variables'
const { p } = typographyStyles const { p } = typographyStyles
@ -68,6 +73,42 @@ export default {
display: 'flex' 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: { actionButtonIcon: {
display: 'flex', display: 'flex',
paddingRight: 7, paddingRight: 7,

View file

@ -57,7 +57,12 @@ const Photo = ({ show, src }) => {
) )
} }
const CustomerData = ({ customer, updateCustomer }) => { const CustomerData = ({
customer,
updateCustomer,
editCustomer,
deleteEditedData
}) => {
const classes = useStyles() const classes = useStyles()
const [listView, setListView] = useState(false) const [listView, setListView] = useState(false)
@ -75,11 +80,14 @@ const CustomerData = ({ customer, updateCustomer }) => {
: 'Failed' : 'Failed'
const customEntries = null // get customer custom entries const customEntries = null // get customer custom entries
const customRequirements = null // get customer custom requirements
const isEven = elem => elem % 2 === 0 const isEven = elem => elem % 2 === 0
const getVisibleCards = _.filter( 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 !== '') const getAvailableFields = _.filter(({ value }) => value !== '')
@ -96,6 +104,12 @@ const CustomerData = ({ customer, updateCustomer }) => {
}), }),
usSsn: Yup.object().shape({ usSsn: Yup.object().shape({
usSsn: Yup.string() 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 = [ const usSsnElements = [
{ {
name: 'us ssn', name: 'usSsn',
label: 'US SSN', label: 'US SSN',
value: `${customer.usSsn ?? ''}`, value: `${customer.usSsn ?? ''}`,
component: TextInput, component: TextInput,
@ -168,6 +182,9 @@ const CustomerData = ({ customer, updateCustomer }) => {
} }
] ]
const idCardPhotoElements = [{ name: 'idCardPhoto' }]
const frontCameraElements = [{ name: 'frontCamera' }]
const initialValues = { const initialValues = {
idScan: { idScan: {
name: '', name: '',
@ -180,19 +197,26 @@ const CustomerData = ({ customer, updateCustomer }) => {
}, },
usSsn: { usSsn: {
usSsn: '' usSsn: ''
},
frontCamera: {
frontCamera: null
},
idCardPhoto: {
idCardPhoto: null
} }
} }
const cards = [ const cards = [
{ {
data: getAvailableFields(idScanElements), fields: getAvailableFields(idScanElements),
title: 'ID Scan', title: 'ID Scan',
titleIcon: <PhoneIcon className={classes.cardIcon} />, titleIcon: <PhoneIcon className={classes.cardIcon} />,
state: R.path(['idCardDataOverride'])(customer), state: R.path(['idCardDataOverride'])(customer),
authorize: () => authorize: () =>
updateCustomer({ idCardDataOverride: OVERRIDE_AUTHORIZED }), updateCustomer({ idCardDataOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ idCardDataOverride: OVERRIDE_REJECTED }), reject: () => updateCustomer({ idCardDataOverride: OVERRIDE_REJECTED }),
save: values => console.log(values), deleteEditedData: () => deleteEditedData({ idCardData: null }),
save: values => editCustomer({ idCardData: values }),
validationSchema: schemas.idScan, validationSchema: schemas.idScan,
initialValues: initialValues.idScan initialValues: initialValues.idScan
}, },
@ -217,17 +241,18 @@ const CustomerData = ({ customer, updateCustomer }) => {
authorize: () => authorize: () =>
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }), updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }), reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }),
save: () => {},
children: <Info3>{sanctionsDisplay}</Info3> children: <Info3>{sanctionsDisplay}</Info3>
}, },
{ {
fields: getAvailableFields(frontCameraElements),
title: 'Front facing camera', title: 'Front facing camera',
titleIcon: <EditIcon className={classes.editIcon} />, titleIcon: <EditIcon className={classes.editIcon} />,
state: R.path(['frontCameraOverride'])(customer), state: R.path(['frontCameraOverride'])(customer),
authorize: () => authorize: () =>
updateCustomer({ frontCameraOverride: OVERRIDE_AUTHORIZED }), updateCustomer({ frontCameraOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ frontCameraOverride: OVERRIDE_REJECTED }), reject: () => updateCustomer({ frontCameraOverride: OVERRIDE_REJECTED }),
save: () => {}, save: values => editCustomer({ frontCamera: values.frontCamera }),
deleteEditedData: () => deleteEditedData({ frontCamera: null }),
children: customer.frontCameraPath ? ( children: customer.frontCameraPath ? (
<Photo <Photo
show={customer.frontCameraPath} show={customer.frontCameraPath}
@ -235,31 +260,40 @@ const CustomerData = ({ customer, updateCustomer }) => {
customer customer
)}`} )}`}
/> />
) : null ) : null,
hasImage: true,
validationSchema: schemas.frontCamera,
initialValues: initialValues.frontCamera
}, },
{ {
fields: getAvailableFields(idCardPhotoElements),
title: 'ID card image', title: 'ID card image',
titleIcon: <EditIcon className={classes.editIcon} />, titleIcon: <EditIcon className={classes.editIcon} />,
state: R.path(['idCardPhotoOverride'])(customer), state: R.path(['idCardPhotoOverride'])(customer),
authorize: () => authorize: () =>
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED }), updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED }), reject: () => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED }),
save: () => {}, save: values => editCustomer({ idCardPhoto: values.idCardPhoto }),
deleteEditedData: () => deleteEditedData({ idCardPhoto: null }),
children: customer.idCardPhotoPath ? ( children: customer.idCardPhotoPath ? (
<Photo <Photo
show={customer.idCardPhotoPath} show={customer.idCardPhotoPath}
src={`${URI}/id-card-photo/${R.path(['idCardPhotoPath'])(customer)}`} 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', title: 'US SSN',
titleIcon: <CardIcon className={classes.cardIcon} />, titleIcon: <CardIcon className={classes.cardIcon} />,
state: R.path(['usSsnOverride'])(customer), state: R.path(['usSsnOverride'])(customer),
authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }), authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }), reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }),
save: () => {}, save: values => editCustomer({ usSsn: values.usSsn }),
deleteEditedData: () => deleteEditedData({ usSsn: null }),
validationSchema: schemas.usSsn, validationSchema: schemas.usSsn,
initialValues: initialValues.usSsn initialValues: initialValues.usSsn
} }
@ -272,11 +306,13 @@ const CustomerData = ({ customer, updateCustomer }) => {
reject, reject,
state, state,
titleIcon, titleIcon,
data, fields,
save, save,
deleteEditedData,
children, children,
validationSchema, validationSchema,
initialValues initialValues,
hasImage
}, },
idx idx
) => { ) => {
@ -288,11 +324,13 @@ const CustomerData = ({ customer, updateCustomer }) => {
reject={reject} reject={reject}
state={state} state={state}
titleIcon={titleIcon} titleIcon={titleIcon}
data={data} hasImage={hasImage}
fields={fields}
children={children} children={children}
validationSchema={validationSchema} validationSchema={validationSchema}
initialValues={initialValues} initialValues={initialValues}
save={save}></EditableCard> save={save}
deleteEditedData={deleteEditedData}></EditableCard>
) )
} }
@ -317,7 +355,7 @@ const CustomerData = ({ customer, updateCustomer }) => {
onClick={() => setListView(true)}></FeatureButton> onClick={() => setListView(true)}></FeatureButton>
</div> </div>
<div> <div>
{!listView && ( {!listView && customer && (
<Grid container> <Grid container>
<Grid container direction="column" item xs={6}> <Grid container direction="column" item xs={6}>
{visibleCards.map((elem, idx) => { {visibleCards.map((elem, idx) => {
@ -333,7 +371,12 @@ const CustomerData = ({ customer, updateCustomer }) => {
)} )}
{customEntries && ( {customEntries && (
<div className={classes.wrapper}> <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>
)} )}
</div> </div>

View file

@ -20,22 +20,30 @@ export default {
marginRight: 12 marginRight: 12
}, },
wrapper: { wrapper: {
display: 'flex' display: 'block',
overflow: 'hidden',
whiteSpace: 'nowrap'
}, },
separator: { separator: {
display: 'flex',
flexBasis: '100%',
justifyContent: 'center',
color: offColor, color: offColor,
margin: [[8, 0, 8, 0]], margin: [[8, 0, 8, 150]],
'&::before, &::after': { position: 'relative',
content: '', display: 'inline-block',
flexGrow: 1, '&:before, &:after': {
content: '""',
position: 'absolute',
background: offColor, background: offColor,
height: 1, top: '50%',
fontSize: 1, width: 1000,
lineHeight: 0, height: 1
margin: [[0, 8, 0, 8]] },
'&:before': {
right: '100%',
marginRight: 15
},
'&:after': {
left: '100%',
marginLeft: 15
} }
} }
} }

View file

@ -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 CustomerProfile = memo(() => {
const history = useHistory() 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, { const [setCustomer] = useMutation(SET_CUSTOMER, {
onCompleted: () => getCustomer() 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 onClickSidebarItem = code => setClickedItem(code)
const configData = R.path(['config'])(customerResponse) ?? [] const configData = R.path(['config'])(customerResponse) ?? []
@ -295,7 +342,9 @@ const CustomerProfile = memo(() => {
<div> <div>
<CustomerData <CustomerData
customer={customerData} customer={customerData}
updateCustomer={updateCustomer}></CustomerData> updateCustomer={updateCustomer}
editCustomer={editCustomer}
deleteEditedData={deleteEditedData}></CustomerData>
</div> </div>
)} )}
</div> </div>

View file

@ -2,6 +2,7 @@ import { CardContent, Card, Grid } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames' import classnames from 'classnames'
import { Form, Formik, Field as FormikField } from 'formik' import { Form, Formik, Field as FormikField } from 'formik'
import * as R from 'ramda'
import { useState, React } from 'react' import { useState, React } from 'react'
import ErrorMessage from 'src/components/ErrorMessage' import ErrorMessage from 'src/components/ErrorMessage'
@ -15,12 +16,16 @@ import {
OVERRIDE_REJECTED, OVERRIDE_REJECTED,
OVERRIDE_PENDING OVERRIDE_PENDING
} from 'src/pages/Customers/components/propertyCard' } 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 EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { ReactComponent as EditReversedIcon } from 'src/styling/icons/action/edit/white.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/white.svg'
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.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 CancelReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
import { ReactComponent as CancelIcon } from 'src/styling/icons/button/cancel/zodiac.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 SaveReversedIcon } from 'src/styling/icons/circle buttons/save/white.svg'
import { ReactComponent as SaveIcon } from 'src/styling/icons/circle buttons/save/zodiac.svg' import { ReactComponent as SaveIcon } from 'src/styling/icons/circle buttons/save/zodiac.svg'
import { comet } from 'src/styling/variables' import { comet } from 'src/styling/variables'
@ -70,7 +75,7 @@ const fieldUseStyles = makeStyles(fieldStyles)
const EditableField = ({ editing, field, size, ...props }) => { const EditableField = ({ editing, field, size, ...props }) => {
const classes = fieldUseStyles() const classes = fieldUseStyles()
console.log('FIELDDDDDDDD', field)
const classNames = { const classNames = {
[classes.field]: true, [classes.field]: true,
[classes.notEditing]: !editing [classes.notEditing]: !editing
@ -90,8 +95,9 @@ const EditableField = ({ editing, field, size, ...props }) => {
<FormikField <FormikField
className={classes.editing} className={classes.editing}
id={field.name} id={field.name}
component={field.component} name={field.name}
value={field.value} value={field.value}
component={field.component}
type={field.type} type={field.type}
width={size} width={size}
{...props} {...props}
@ -103,20 +109,27 @@ const EditableField = ({ editing, field, size, ...props }) => {
} }
const EditableCard = ({ const EditableCard = ({
data, fields,
save, save,
authorize, authorize,
hasImage,
reject, reject,
state, state,
title, title,
titleIcon, titleIcon,
children children,
validationSchema,
initialValues,
deleteEditedData
}) => { }) => {
const classes = useStyles() const classes = useStyles()
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [input, setInput] = useState(null)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const triggerInput = () => input.click()
const label1ClassNames = { const label1ClassNames = {
[classes.label1]: true, [classes.label1]: true,
[classes.label1Pending]: state === OVERRIDE_PENDING, [classes.label1Pending]: state === OVERRIDE_PENDING,
@ -130,9 +143,7 @@ const EditableCard = ({
? { label: 'Rejected', type: 'error' } ? { label: 'Rejected', type: 'error' }
: { label: 'Accepted', type: 'success' } : { label: 'Accepted', type: 'success' }
const editableField = field => { const reader = new FileReader()
return <EditableField field={field} editing={editing} size={180} />
}
return ( return (
<div> <div>
@ -142,39 +153,70 @@ const EditableCard = ({
{titleIcon} {titleIcon}
<H3 className={classes.cardTitle}>{title}</H3> <H3 className={classes.cardTitle}>{title}</H3>
<Tooltip width={304}></Tooltip> <Tooltip width={304}></Tooltip>
{state && (
<div className={classnames(label1ClassNames)}> <div className={classnames(label1ClassNames)}>
<MainStatus statuses={[authorized]} /> <MainStatus statuses={[authorized]} />
</div> </div>
)}
</div> </div>
{children}
<Formik <Formik
validateOnBlur={false} validateOnBlur={false}
validateOnChange={false} validateOnChange={false}
enableReinitialize enableReinitialize
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={values => save(values)} onSubmit={values => save(values)}
onReset={() => { onReset={() => {
setEditing(false) setEditing(false)
setError(false) setError(false)
}}> }}>
{({ values, touched, errors, setFieldValue }) => (
<Form> <Form>
<PromptWhenDirty /> <PromptWhenDirty />
{console.log(values, touched, errors, 'FORMIK STATUS')}
<div className={classes.row}> <div className={classes.row}>
<Grid container> <Grid container>
<Grid container direction="column" item xs={6}> <Grid container direction="column" item xs={6}>
{data?.map((field, idx) => { {!hasImage &&
return idx >= 0 && idx < 4 ? editableField(field) : null fields?.map((field, idx) => {
return idx >= 0 && idx < 4 ? (
<EditableField
field={field}
editing={editing}
size={180}
/>
) : null
})} })}
</Grid> </Grid>
<Grid container direction="column" item xs={6}> <Grid container direction="column" item xs={6}>
{data?.map((field, idx) => { {!hasImage &&
return idx >= 4 ? editableField(field) : null fields?.map((field, idx) => {
return idx >= 4 ? (
<EditableField
field={field}
editing={editing}
size={180}
/>
) : null
})} })}
</Grid> </Grid>
</Grid> </Grid>
</div> </div>
{children}
<div className={classes.edit}> <div className={classes.edit}>
{!editing && ( {!editing && (
<div className={classes.editButton}> <div className={classes.editButton}>
<div className={classes.deleteButton}>
<ActionButton
color="primary"
type="button"
Icon={DeleteIcon}
InverseIcon={DeleteReversedIcon}
onClick={() => deleteEditedData()}>
{`Delete`}
</ActionButton>
</div>
<ActionButton <ActionButton
color="primary" color="primary"
Icon={EditIcon} Icon={EditIcon}
@ -185,8 +227,48 @@ const EditableCard = ({
</div> </div>
)} )}
{editing && ( {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>
<div className={classes.editingButtons}> <div className={classes.editingButtons}>
{data && ( {fields && (
<div className={classes.button}> <div className={classes.button}>
<ActionButton <ActionButton
color="secondary" color="secondary"
@ -209,32 +291,34 @@ const EditableCard = ({
{authorized.label !== 'Accepted' && ( {authorized.label !== 'Accepted' && (
<div className={classes.button}> <div className={classes.button}>
<ActionButton <ActionButton
color="secondary" color="spring"
type="button"
Icon={AuthorizeIcon} Icon={AuthorizeIcon}
InverseIcon={AuthorizeReversedIcon} InverseIcon={AuthorizeIcon}
type="submit"
onClick={() => authorize()}> onClick={() => authorize()}>
{'Authorize'} Authorize
</ActionButton> </ActionButton>
</div> </div>
)} )}
{authorized.label !== 'Rejected' && ( {authorized.label !== 'Rejected' && (
<ActionButton <ActionButton
color="secondary" color="tomato"
Icon={CancelIcon} type="button"
InverseIcon={CancelReversedIcon} Icon={BlockIcon}
type="submit" InverseIcon={BlockIcon}
onClick={() => reject()}> onClick={() => reject()}>
{'Reject'} Reject
</ActionButton> </ActionButton>
)} )}
{error && ( {error && (
<ErrorMessage>Failed to save changes</ErrorMessage> <ErrorMessage>Failed to save changes</ErrorMessage>
)} )}
</div> </div>
</div>
)} )}
</div> </div>
</Form> </Form>
)}
</Formik> </Formik>
</CardContent> </CardContent>
</Card> </Card>

View file

@ -20,6 +20,20 @@ export default {
display: 'flex', display: 'flex',
justifyContent: 'right' justifyContent: 'right'
}, },
deleteButton: {
marginRight: 8
},
editingWrapper: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between'
},
replace: {
marginTop: 30
},
input: {
display: 'none'
},
button: { button: {
marginRight: 8 marginRight: 8
}, },

View file

@ -25,6 +25,7 @@ const white = '#ffffff'
// Error // Error
const tomato = '#ff584a' const tomato = '#ff584a'
const tomato1 = '#E45043'
const mistyRose = '#ffeceb' const mistyRose = '#ffeceb'
const pumpkin = '#ff7311' const pumpkin = '#ff7311'
const linen = '#fbf3ec' const linen = '#fbf3ec'
@ -47,6 +48,7 @@ const offColor = comet
const offDarkColor = comet2 const offDarkColor = comet2
const placeholderColor = comet const placeholderColor = comet
const errorColor = tomato const errorColor = tomato
const errorColorDarker = tomato1
const offErrorColor = mistyRose const offErrorColor = mistyRose
const inputBorderColor = primaryColor const inputBorderColor = primaryColor
@ -148,6 +150,7 @@ export {
linkPrimaryColor, linkPrimaryColor,
linkSecondaryColor, linkSecondaryColor,
errorColor, errorColor,
errorColorDarker,
offErrorColor, offErrorColor,
inputBorderColor, inputBorderColor,
// font sizes // font sizes