Merge pull request #909 from josepfo/feat/edit-customer-data
Feat: edit customer data
This commit is contained in:
commit
7087781cfc
23 changed files with 789 additions and 210 deletions
248
lib/customers.js
248
lib/customers.js
|
|
@ -138,6 +138,154 @@ 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',
|
||||||
|
'subscriber_info',
|
||||||
|
'name'
|
||||||
|
]
|
||||||
|
const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, _.omitBy(_.isNil, data)))
|
||||||
|
if (_.isEmpty(filteredData)) return getCustomerById(id)
|
||||||
|
const formattedData = enhanceEditedPhotos(enhanceEditedFields(filteredData, userToken))
|
||||||
|
const defaultDbData = {
|
||||||
|
customer_id: id,
|
||||||
|
created: new Date(),
|
||||||
|
...formattedData
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
return db.none(upsert)
|
||||||
|
.then(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 + '_by'] = userToken
|
||||||
|
fields[field + '_at'] = 'now()^'
|
||||||
|
}, fields)
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add *_path to edited photos fields
|
||||||
|
*
|
||||||
|
* @name enhanceEditedFields
|
||||||
|
* @function
|
||||||
|
*
|
||||||
|
* @param {object} fields Fields to be enhanced
|
||||||
|
* @returns {object} fields enhanced with *_path
|
||||||
|
*/
|
||||||
|
|
||||||
|
function enhanceEditedPhotos (fields) {
|
||||||
|
return _.mapKeys((field) => {
|
||||||
|
if (_.includes(field, ['front_camera', 'id_card_photo'])) {
|
||||||
|
return field + '_path'
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}, 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) {
|
||||||
|
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace customer's compliance photos
|
||||||
|
*
|
||||||
|
* @name save
|
||||||
|
* @function
|
||||||
|
*
|
||||||
|
* @param {string} id Customer's id
|
||||||
|
* @param {File} photo New photo data
|
||||||
|
* @param {string} photoType Photo's compliance type
|
||||||
|
*
|
||||||
|
* @returns {object} path New photo path
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async function updateEditedPhoto (id, photo, photoType) {
|
||||||
|
const newPatch = {}
|
||||||
|
const baseDir = photoType === 'frontCamera' ? frontCameraBaseDir : idPhotoCardBasedir
|
||||||
|
const { createReadStream, filename } = photo
|
||||||
|
const stream = createReadStream()
|
||||||
|
|
||||||
|
const randomString = uuid.v4().toString() + '/'
|
||||||
|
|
||||||
|
// i.e. ..62ed29c5-f37e-4fb7-95bb-c52d4a3738f7/filename.jpg
|
||||||
|
const rpath = path.join(randomString, filename)
|
||||||
|
|
||||||
|
// create the directory tree if needed
|
||||||
|
_.attempt(() => makeDir.sync(path.join(baseDir, randomString)))
|
||||||
|
|
||||||
|
// i.e. ../<lamassu-server-home>/idphotocard/62ed29c5-f37e-4fb7-95bb-c52d4a3738f7/filename.jpg
|
||||||
|
const pathName = path.join(baseDir, rpath)
|
||||||
|
|
||||||
|
await stream.pipe(fs.createWriteStream(pathName))
|
||||||
|
newPatch[photoType] = rpath
|
||||||
|
|
||||||
|
return newPatch
|
||||||
|
}
|
||||||
|
|
||||||
const invalidateCustomerNotifications = (id, data) => {
|
const invalidateCustomerNotifications = (id, data) => {
|
||||||
if (data.authorized_override !== 'verified') return Promise.resolve()
|
if (data.authorized_override !== 'verified') return Promise.resolve()
|
||||||
|
|
||||||
|
|
@ -489,9 +637,8 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
||||||
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created,
|
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created,
|
||||||
row_number() OVER (partition by c.id order by t.created desc) AS rn,
|
row_number() OVER (partition by c.id order by t.created desc) AS rn,
|
||||||
coalesce(sum(case when error_code is null or error_code not in ($1^) then t.fiat else 0 end) over (partition by c.id), 0) as total_spent,
|
|
||||||
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
|
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
|
||||||
ccf.custom_fields
|
coalesce(sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (partition by c.id), 0) AS total_spent, ccf.custom_fields
|
||||||
FROM customers c LEFT OUTER JOIN (
|
FROM customers c LEFT OUTER JOIN (
|
||||||
SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||||
FROM cash_in_txs WHERE send_confirmed = true UNION
|
FROM cash_in_txs WHERE send_confirmed = true UNION
|
||||||
|
|
@ -510,7 +657,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,17 +665,17 @@ 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(',')
|
||||||
const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
|
const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_at, front_camera_path, front_camera_at, front_camera_override,
|
||||||
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
|
phone, sms_override, id_card_data_at, 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_at, id_card_photo_path, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||||
sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat,
|
sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat,
|
||||||
fiat_code as last_tx_fiat_code, tx_class as last_tx_class, subscriber_info, custom_fields
|
fiat_code as last_tx_fiat_code, tx_class as last_tx_class, subscriber_info, custom_fields
|
||||||
from (
|
from (
|
||||||
|
|
@ -536,8 +683,8 @@ function getCustomerById (id) {
|
||||||
greatest(0, date_part('day', c.suspended_until - now())) as days_suspended,
|
greatest(0, date_part('day', c.suspended_until - now())) as days_suspended,
|
||||||
c.suspended_until > now() as is_suspended,
|
c.suspended_until > now() as is_suspended,
|
||||||
c.front_camera_path, c.front_camera_at, c.front_camera_override,
|
c.front_camera_path, c.front_camera_at, c.front_camera_override,
|
||||||
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
|
c.phone, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
|
||||||
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions,
|
||||||
c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created,
|
c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created,
|
||||||
row_number() over (partition by c.id order by t.created desc) as rn,
|
row_number() over (partition by c.id order by t.created desc) as rn,
|
||||||
sum(case when t.id is not null then 1 else 0 end) over (partition by c.id) as total_txs,
|
sum(case when t.id is not null then 1 else 0 end) over (partition by c.id) as total_txs,
|
||||||
|
|
@ -556,10 +703,52 @@ function getCustomerById (id) {
|
||||||
where c.id = $2
|
where c.id = $2
|
||||||
) as cl where rn = 1`
|
) as cl where rn = 1`
|
||||||
return db.oneOrNone(sql, [passableErrorCodes, id])
|
return db.oneOrNone(sql, [passableErrorCodes, id])
|
||||||
|
.then(customerData => {
|
||||||
|
return getEditedData(id)
|
||||||
|
.then(customerEditedData => selectLatestData(customerData, customerEditedData))
|
||||||
|
})
|
||||||
.then(populateOverrideUsernames)
|
.then(populateOverrideUsernames)
|
||||||
.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 edited data
|
||||||
|
*/
|
||||||
|
function getEditedData (id) {
|
||||||
|
const sql = `SELECT * FROM edited_customer_data WHERE customer_id = $1`
|
||||||
|
return db.oneOrNone(sql, [id])
|
||||||
|
.then(_.omitBy(_.isNil))
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLatestData (customerData, customerEditedData) {
|
||||||
|
const defaults = [
|
||||||
|
'front_camera',
|
||||||
|
'id_card_data',
|
||||||
|
'id_card_photo',
|
||||||
|
'us_ssn',
|
||||||
|
'subscriber_info',
|
||||||
|
'name'
|
||||||
|
]
|
||||||
|
_.map(field => {
|
||||||
|
let fieldName = field
|
||||||
|
if (_.includes(field, ['front_camera', 'id_card_photo'])) fieldName = fieldName + '_path'
|
||||||
|
const atField = field + '_at'
|
||||||
|
const byField = field + '_by'
|
||||||
|
if (!_.has(fieldName, customerData) || !_.has(fieldName, customerEditedData)) return
|
||||||
|
if (customerData[atField] < customerEditedData[atField]) {
|
||||||
|
customerData[fieldName] = customerEditedData[fieldName]
|
||||||
|
customerData[atField] = customerEditedData[atField]
|
||||||
|
customerData[byField] = customerEditedData[byField]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, defaults)
|
||||||
|
return customerData
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} id customer id
|
* @param {String} id customer id
|
||||||
* @param {Object} patch customer update record
|
* @param {Object} patch customer update record
|
||||||
|
|
@ -768,35 +957,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`
|
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 => {
|
||||||
if (_.isNil(res)) {
|
if (_.isNil(res)) {
|
||||||
const fieldId = uuid.v4()
|
const fieldId = uuid.v4()
|
||||||
const q1 = t.none(`INSERT INTO custom_field_definitions (id, label) VALUES ($1, $2)`, [fieldId, label])
|
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])
|
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])
|
return t.batch([q1, q2])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isNil(res) && !res.active) {
|
if (!_.isNil(res) && !res.active) {
|
||||||
const q1 = t.none(`UPDATE custom_field_definitions SET active = true WHERE id=$1`, [res.id])
|
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])
|
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])
|
return t.batch([q1, q2])
|
||||||
} else if (!_.isNil(res) && res.active) {
|
} 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])
|
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])
|
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`
|
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 +1016,8 @@ module.exports = {
|
||||||
addCustomField,
|
addCustomField,
|
||||||
saveCustomField,
|
saveCustomField,
|
||||||
removeCustomField,
|
removeCustomField,
|
||||||
|
edit,
|
||||||
|
deleteEditedData,
|
||||||
|
updateEditedPhoto,
|
||||||
updateTxCustomerPhoto
|
updateTxCustomerPhoto
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const helmet = require('helmet')
|
||||||
const nocache = require('nocache')
|
const nocache = require('nocache')
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require('cookie-parser')
|
||||||
const { ApolloServer, AuthenticationError } = require('apollo-server-express')
|
const { ApolloServer, AuthenticationError } = require('apollo-server-express')
|
||||||
|
const { graphqlUploadExpress } = require('graphql-upload')
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
const { asyncLocalStorage, defaultStore } = require('../async-storage')
|
const { asyncLocalStorage, defaultStore } = require('../async-storage')
|
||||||
|
|
@ -47,10 +48,12 @@ app.use(cleanUserSessions(USER_SESSIONS_CLEAR_INTERVAL))
|
||||||
app.use(computeSchema)
|
app.use(computeSchema)
|
||||||
app.use(findOperatorId)
|
app.use(findOperatorId)
|
||||||
app.use(session)
|
app.use(session)
|
||||||
|
app.use(graphqlUploadExpress())
|
||||||
|
|
||||||
const apolloServer = new ApolloServer({
|
const apolloServer = new ApolloServer({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
|
uploads: false,
|
||||||
schemaDirectives: {
|
schemaDirectives: {
|
||||||
auth: AuthDirective
|
auth: AuthDirective
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const customers = require('../../../customers')
|
||||||
const filters = require('../../filters')
|
const filters = require('../../filters')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
|
|
||||||
Customer: {
|
Customer: {
|
||||||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
||||||
},
|
},
|
||||||
|
|
@ -13,13 +14,32 @@ const resolvers = {
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
setCustomer: (root, { customerId, customerInput }, context, info) => {
|
setCustomer: (root, { customerId, customerInput }, context, info) => {
|
||||||
|
// TODO: To be replaced by function that fetchs the token
|
||||||
const token = !!context.req.cookies.lamassu_sid && context.req.session.user.id
|
const token = !!context.req.cookies.lamassu_sid && context.req.session.user.id
|
||||||
if (customerId === anonymous.uuid) return customers.getCustomerById(customerId)
|
if (customerId === anonymous.uuid) return customers.getCustomerById(customerId)
|
||||||
return customers.updateCustomer(customerId, customerInput, token)
|
return customers.updateCustomer(customerId, customerInput, token)
|
||||||
},
|
},
|
||||||
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: async (root, { customerId, customerEdit }, context) => {
|
||||||
|
// TODO: To be replaced by function that fetchs the token
|
||||||
|
const token = !!context.req.cookies.lid && context.req.session.user.id
|
||||||
|
const editedData = await customerEdit
|
||||||
|
return customers.edit(customerId, editedData, token)
|
||||||
|
},
|
||||||
|
replacePhoto: async (root, { customerId, photoType, newPhoto }, context) => {
|
||||||
|
// TODO: To be replaced by function that fetchs the token
|
||||||
|
const token = !!context.req.cookies.lid && context.req.session.user.id
|
||||||
|
const photo = await newPhoto
|
||||||
|
if (!photo) return customers.getCustomerById(customerId)
|
||||||
|
return customers.updateEditedPhoto(customerId, photo, photoType)
|
||||||
|
.then(newPatch => customers.edit(customerId, newPatch, token))
|
||||||
|
},
|
||||||
|
deleteEditedData: (root, { customerId, customerEdit }) => {
|
||||||
|
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
|
||||||
|
return customers.getCustomerById(customerId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
const { GraphQLDateTime } = require('graphql-iso-date')
|
const { GraphQLDateTime } = require('graphql-iso-date')
|
||||||
const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
|
const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
|
||||||
|
const { GraphQLUpload } = require('graphql-upload')
|
||||||
GraphQLDateTime.name = 'Date'
|
GraphQLDateTime.name = 'Date'
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
JSON: GraphQLJSON,
|
JSON: GraphQLJSON,
|
||||||
JSONObject: GraphQLJSONObject,
|
JSONObject: GraphQLJSONObject,
|
||||||
Date: GraphQLDateTime
|
Date: GraphQLDateTime,
|
||||||
|
UploadGQL: GraphQLUpload
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ const typeDef = gql`
|
||||||
authorizedOverride: String
|
authorizedOverride: String
|
||||||
daysSuspended: Int
|
daysSuspended: Int
|
||||||
isSuspended: Boolean
|
isSuspended: Boolean
|
||||||
|
newPhoto: UploadGQL
|
||||||
|
photoType: String
|
||||||
frontCameraPath: String
|
frontCameraPath: String
|
||||||
frontCameraAt: Date
|
frontCameraAt: Date
|
||||||
frontCameraOverride: String
|
frontCameraOverride: String
|
||||||
|
|
@ -21,6 +23,7 @@ const typeDef = gql`
|
||||||
idCardData: JSONObject
|
idCardData: JSONObject
|
||||||
idCardDataOverride: String
|
idCardDataOverride: String
|
||||||
idCardDataExpiration: Date
|
idCardDataExpiration: Date
|
||||||
|
idCardPhoto: UploadGQL
|
||||||
idCardPhotoPath: String
|
idCardPhotoPath: String
|
||||||
idCardPhotoOverride: String
|
idCardPhotoOverride: String
|
||||||
usSsn: String
|
usSsn: String
|
||||||
|
|
@ -65,6 +68,12 @@ const typeDef = gql`
|
||||||
subscriberInfo: Boolean
|
subscriberInfo: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input CustomerEdit {
|
||||||
|
idCardData: JSONObject
|
||||||
|
idCardPhoto: UploadGQL
|
||||||
|
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,9 @@ 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
|
||||||
|
replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ const typeDef = gql`
|
||||||
scalar JSON
|
scalar JSON
|
||||||
scalar JSONObject
|
scalar JSONObject
|
||||||
scalar Date
|
scalar Date
|
||||||
|
scalar UploadGQL
|
||||||
`
|
`
|
||||||
|
|
||||||
module.exports = typeDef
|
module.exports = typeDef
|
||||||
|
|
|
||||||
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_at TIMESTAMPTZ,
|
||||||
|
id_card_data_by UUID REFERENCES users(id),
|
||||||
|
front_camera_path TEXT,
|
||||||
|
front_camera_at TIMESTAMPTZ,
|
||||||
|
front_camera_by UUID REFERENCES users(id),
|
||||||
|
id_card_photo_path TEXT,
|
||||||
|
id_card_photo_at TIMESTAMPTZ,
|
||||||
|
id_card_photo_by UUID REFERENCES users(id),
|
||||||
|
subscriber_info JSON,
|
||||||
|
subscriber_info_at TIMESTAMPTZ,
|
||||||
|
subscriber_info_by UUID REFERENCES users(id),
|
||||||
|
name TEXT,
|
||||||
|
name_at TIMESTAMPTZ,
|
||||||
|
name_by UUID REFERENCES users(id),
|
||||||
|
us_ssn TEXT,
|
||||||
|
us_ssn_at TIMESTAMPTZ,
|
||||||
|
us_ssn_by UUID REFERENCES users(id),
|
||||||
|
created TIMESTAMPTZ NOT NULL DEFAULT now() )`
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
13
new-lamassu-admin/package-lock.json
generated
13
new-lamassu-admin/package-lock.json
generated
|
|
@ -6888,6 +6888,14 @@
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"apollo-upload-client": {
|
||||||
|
"version": "16.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-16.0.0.tgz",
|
||||||
|
"integrity": "sha512-aLhYucyA0T8aBEQ5g+p13qnR9RUyL8xqb8FSZ7e/Kw2KUOsotLUlFluLobqaE7JSUFwc6sKfXIcwB7y4yEjbZg==",
|
||||||
|
"requires": {
|
||||||
|
"extract-files": "^11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"apollo-utilities": {
|
"apollo-utilities": {
|
||||||
"version": "1.3.4",
|
"version": "1.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz",
|
||||||
|
|
@ -12608,6 +12616,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"extract-files": {
|
||||||
|
"version": "11.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz",
|
||||||
|
"integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ=="
|
||||||
|
},
|
||||||
"extsprintf": {
|
"extsprintf": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"apollo-link": "^1.2.14",
|
"apollo-link": "^1.2.14",
|
||||||
"apollo-link-error": "^1.1.13",
|
"apollo-link-error": "^1.1.13",
|
||||||
"apollo-link-http": "^1.5.17",
|
"apollo-link-http": "^1.5.17",
|
||||||
|
"apollo-upload-client": "^16.0.0",
|
||||||
"axios": "0.21.1",
|
"axios": "0.21.1",
|
||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
"bignumber.js": "9.0.0",
|
"bignumber.js": "9.0.0",
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
import typographyStyles from 'src/components/typography/styles'
|
import typographyStyles from 'src/components/typography/styles'
|
||||||
import {
|
import {
|
||||||
white,
|
white,
|
||||||
fontColor,
|
|
||||||
subheaderColor,
|
subheaderColor,
|
||||||
subheaderDarkColor,
|
subheaderDarkColor,
|
||||||
offColor,
|
offColor,
|
||||||
offDarkColor
|
offDarkColor,
|
||||||
|
offDarkerColor,
|
||||||
|
secondaryColor,
|
||||||
|
secondaryColorDark,
|
||||||
|
secondaryColorDarker,
|
||||||
|
errorColor,
|
||||||
|
errorColorDark,
|
||||||
|
errorColorDarker
|
||||||
} from 'src/styling/variables'
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
const { p } = typographyStyles
|
const { p } = typographyStyles
|
||||||
|
|
@ -50,10 +56,45 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
extend: colors(offColor, offDarkColor, white),
|
extend: colors(offColor, offDarkColor, offDarkerColor),
|
||||||
|
color: white,
|
||||||
|
'&:active': {
|
||||||
|
'& $actionButtonIcon': {
|
||||||
|
display: 'flex'
|
||||||
|
},
|
||||||
|
'& $actionButtonIconActive': {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'& $actionButtonIcon': {
|
||||||
|
display: 'none'
|
||||||
|
},
|
||||||
|
'& $actionButtonIconActive': {
|
||||||
|
display: 'flex'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spring: {
|
||||||
|
extend: colors(secondaryColorDark, secondaryColor, secondaryColorDarker),
|
||||||
|
color: white,
|
||||||
|
'&:active': {
|
||||||
|
'& $actionButtonIcon': {
|
||||||
|
display: 'flex'
|
||||||
|
},
|
||||||
|
'& $actionButtonIconActive': {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'& $actionButtonIcon': {
|
||||||
|
display: 'none'
|
||||||
|
},
|
||||||
|
'& $actionButtonIconActive': {
|
||||||
|
display: 'flex'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tomato: {
|
||||||
|
extend: colors(errorColorDark, errorColor, errorColorDarker),
|
||||||
color: white,
|
color: white,
|
||||||
'&:active': {
|
'&:active': {
|
||||||
color: fontColor,
|
|
||||||
'& $actionButtonIcon': {
|
'& $actionButtonIcon': {
|
||||||
display: 'flex'
|
display: 'flex'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import { getName } from './helper.js'
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const IMAGE_WIDTH = 165
|
const IMAGE_WIDTH = 165
|
||||||
const IMAGE_HEIGHT = 45
|
const IMAGE_HEIGHT = 32
|
||||||
const POPUP_IMAGE_WIDTH = 360
|
const POPUP_IMAGE_WIDTH = 360
|
||||||
const POPUP_IMAGE_HEIGHT = 240
|
const POPUP_IMAGE_HEIGHT = 240
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,96 +117,98 @@ const CustomerData = ({ customer, updateCustomer }) => {
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
value: `${getName(customer)}`,
|
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'idNumber',
|
name: 'idNumber',
|
||||||
label: 'ID number',
|
label: 'ID number',
|
||||||
value: R.path(['documentNumber'])(idData) ?? '',
|
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'birthDate',
|
name: 'birthDate',
|
||||||
label: 'Birth Date',
|
label: 'Birth Date',
|
||||||
value:
|
|
||||||
(rawDob &&
|
|
||||||
format('yyyy-MM-dd')(parse(new Date(), 'yyyyMMdd', rawDob))) ??
|
|
||||||
'',
|
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'age',
|
name: 'age',
|
||||||
label: 'Age',
|
label: 'Age',
|
||||||
value:
|
|
||||||
(rawDob &&
|
|
||||||
differenceInYears(
|
|
||||||
parse(new Date(), 'yyyyMMdd', rawDob),
|
|
||||||
new Date()
|
|
||||||
)) ??
|
|
||||||
'',
|
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'gender',
|
name: 'gender',
|
||||||
label: 'Gender',
|
label: 'Gender',
|
||||||
value: R.path(['gender'])(idData) ?? '',
|
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'state',
|
name: 'state',
|
||||||
label: country === 'Canada' ? 'Province' : 'State',
|
label: country === 'Canada' ? 'Province' : 'State',
|
||||||
value: R.path(['state'])(idData) ?? '',
|
|
||||||
component: TextInput
|
component: TextInput
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'expirationDate',
|
name: 'expirationDate',
|
||||||
label: 'Expiration Date',
|
label: 'Expiration Date',
|
||||||
value:
|
|
||||||
(rawExpirationDate &&
|
|
||||||
format('yyyy-MM-dd')(
|
|
||||||
parse(new Date(), 'yyyyMMdd', rawExpirationDate)
|
|
||||||
)) ??
|
|
||||||
'',
|
|
||||||
component: TextInput
|
component: TextInput
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const usSsnElements = [
|
const usSsnElements = [
|
||||||
{
|
{
|
||||||
name: 'us ssn',
|
name: 'usSsn',
|
||||||
label: 'US SSN',
|
label: 'US SSN',
|
||||||
value: `${customer.usSsn ?? ''}`,
|
|
||||||
component: TextInput,
|
component: TextInput,
|
||||||
size: 190
|
size: 190
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const idCardPhotoElements = [{ name: 'idCardPhoto' }]
|
||||||
|
const frontCameraElements = [{ name: 'frontCamera' }]
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
idScan: {
|
idScan: {
|
||||||
name: '',
|
name: getName(customer) ?? '',
|
||||||
idNumber: '',
|
idNumber: R.path(['documentNumber'])(idData) ?? '',
|
||||||
birthDate: '',
|
birthDate:
|
||||||
age: '',
|
(rawDob &&
|
||||||
gender: '',
|
format('yyyy-MM-dd')(parse(new Date(), 'yyyyMMdd', rawDob))) ??
|
||||||
state: '',
|
'',
|
||||||
expirationDate: ''
|
age:
|
||||||
|
(rawDob &&
|
||||||
|
differenceInYears(
|
||||||
|
parse(new Date(), 'yyyyMMdd', rawDob),
|
||||||
|
new Date()
|
||||||
|
)) ??
|
||||||
|
'',
|
||||||
|
gender: R.path(['gender'])(idData) ?? '',
|
||||||
|
state: R.path(['state'])(idData) ?? '',
|
||||||
|
expirationDate:
|
||||||
|
(rawExpirationDate &&
|
||||||
|
format('yyyy-MM-dd')(
|
||||||
|
parse(new Date(), 'yyyyMMdd', rawExpirationDate)
|
||||||
|
)) ??
|
||||||
|
''
|
||||||
},
|
},
|
||||||
usSsn: {
|
usSsn: {
|
||||||
usSsn: ''
|
usSsn: customer.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 +233,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 +252,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 +298,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 +316,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 +347,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 +363,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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,45 @@ const SET_CUSTOMER = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
const EDIT_CUSTOMER = gql`
|
||||||
|
mutation editCustomer($customerId: ID!, $customerEdit: CustomerEdit) {
|
||||||
|
editCustomer(customerId: $customerId, customerEdit: $customerEdit) {
|
||||||
|
id
|
||||||
|
idCardData
|
||||||
|
usSsn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const REPLACE_CUSTOMER_PHOTO = gql`
|
||||||
|
mutation replacePhoto(
|
||||||
|
$customerId: ID!
|
||||||
|
$photoType: String
|
||||||
|
$newPhoto: Upload
|
||||||
|
) {
|
||||||
|
replacePhoto(
|
||||||
|
customerId: $customerId
|
||||||
|
photoType: $photoType
|
||||||
|
newPhoto: $newPhoto
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
newPhoto
|
||||||
|
photoType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DELETE_EDITED_CUSTOMER = gql`
|
||||||
|
mutation deleteEditedData($customerId: ID!, $customerEdit: CustomerEdit) {
|
||||||
|
deleteEditedData(customerId: $customerId, customerEdit: $customerEdit) {
|
||||||
|
id
|
||||||
|
frontCameraPath
|
||||||
|
idCardData
|
||||||
|
idCardPhotoPath
|
||||||
|
usSsn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const CustomerProfile = memo(() => {
|
const CustomerProfile = memo(() => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
@ -133,6 +172,18 @@ const CustomerProfile = memo(() => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, {
|
||||||
|
onCompleted: () => getCustomer()
|
||||||
|
})
|
||||||
|
|
||||||
|
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 +196,31 @@ const CustomerProfile = memo(() => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const replacePhoto = it =>
|
||||||
|
replaceCustomerPhoto({
|
||||||
|
variables: {
|
||||||
|
customerId,
|
||||||
|
newPhoto: it.newPhoto,
|
||||||
|
photoType: it.photoType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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) ?? []
|
||||||
|
|
@ -248,6 +324,7 @@ const CustomerProfile = memo(() => {
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="primary"
|
color="primary"
|
||||||
|
className={classes.retrieveInformation}
|
||||||
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
||||||
InverseIcon={
|
InverseIcon={
|
||||||
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
||||||
|
|
@ -295,7 +372,10 @@ const CustomerProfile = memo(() => {
|
||||||
<div>
|
<div>
|
||||||
<CustomerData
|
<CustomerData
|
||||||
customer={customerData}
|
customer={customerData}
|
||||||
updateCustomer={updateCustomer}></CustomerData>
|
updateCustomer={updateCustomer}
|
||||||
|
replacePhoto={replacePhoto}
|
||||||
|
editCustomer={editCustomer}
|
||||||
|
deleteEditedData={deleteEditedData}></CustomerData>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ export default {
|
||||||
margin: [[8, 0, 4, 0]],
|
margin: [[8, 0, 4, 0]],
|
||||||
padding: [[0, 40.5, 0]]
|
padding: [[0, 40.5, 0]]
|
||||||
},
|
},
|
||||||
|
retrieveInformation: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
margin: [[0, 0, 4, 0]],
|
||||||
|
padding: [[0, 32.5, 0]]
|
||||||
|
},
|
||||||
panels: {
|
panels: {
|
||||||
display: 'flex'
|
display: 'flex'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,10 @@ export default {
|
||||||
color: white,
|
color: white,
|
||||||
backgroundColor: offDarkColor,
|
backgroundColor: offDarkColor,
|
||||||
'&:first-child': {
|
'&:first-child': {
|
||||||
borderRadius: [5, 5, 0, 0]
|
borderRadius: [[5, 5, 0, 0]]
|
||||||
},
|
},
|
||||||
'&:last-child': {
|
'&:last-child': {
|
||||||
borderRadius: [0, 0, 5, 5]
|
borderRadius: [[0, 0, 5, 5]]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
@ -9,20 +10,21 @@ import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||||
import { MainStatus } from 'src/components/Status'
|
import { MainStatus } from 'src/components/Status'
|
||||||
import { Tooltip } from 'src/components/Tooltip'
|
import { Tooltip } from 'src/components/Tooltip'
|
||||||
import { ActionButton } from 'src/components/buttons'
|
import { ActionButton } from 'src/components/buttons'
|
||||||
import { Label1, Info3, H3 } from 'src/components/typography'
|
import { Label1, P, H3 } from 'src/components/typography'
|
||||||
import {
|
import {
|
||||||
OVERRIDE_AUTHORIZED,
|
OVERRIDE_AUTHORIZED,
|
||||||
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 ReplaceReversedIcon } from 'src/styling/icons/button/replace/white.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 { comet } from 'src/styling/variables'
|
import { comet } from 'src/styling/variables'
|
||||||
|
|
||||||
import styles from './EditableCard.styles.js'
|
import styles from './EditableCard.styles.js'
|
||||||
|
|
@ -34,7 +36,8 @@ const fieldStyles = {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: 280,
|
width: 280,
|
||||||
height: 48,
|
height: 48,
|
||||||
padding: [[0, 4, 4, 0]]
|
padding: [[0, 4, 4, 0]],
|
||||||
|
marginTop: 2
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
color: comet,
|
color: comet,
|
||||||
|
|
@ -60,7 +63,8 @@ const fieldStyles = {
|
||||||
editing: {
|
editing: {
|
||||||
'& > div': {
|
'& > div': {
|
||||||
'& > input': {
|
'& > input': {
|
||||||
padding: 0
|
padding: 0,
|
||||||
|
fontSize: 14
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -68,9 +72,8 @@ const fieldStyles = {
|
||||||
|
|
||||||
const fieldUseStyles = makeStyles(fieldStyles)
|
const fieldUseStyles = makeStyles(fieldStyles)
|
||||||
|
|
||||||
const EditableField = ({ editing, field, size, ...props }) => {
|
const EditableField = ({ editing, field, value, size, ...props }) => {
|
||||||
const classes = fieldUseStyles()
|
const classes = fieldUseStyles()
|
||||||
|
|
||||||
const classNames = {
|
const classNames = {
|
||||||
[classes.field]: true,
|
[classes.field]: true,
|
||||||
[classes.notEditing]: !editing
|
[classes.notEditing]: !editing
|
||||||
|
|
@ -81,7 +84,7 @@ const EditableField = ({ editing, field, size, ...props }) => {
|
||||||
{!editing && (
|
{!editing && (
|
||||||
<>
|
<>
|
||||||
<Label1 className={classes.label}>{field.label}</Label1>
|
<Label1 className={classes.label}>{field.label}</Label1>
|
||||||
<Info3>{field.value}</Info3>
|
<P>{value}</P>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{editing && (
|
{editing && (
|
||||||
|
|
@ -90,8 +93,8 @@ const EditableField = ({ editing, field, size, ...props }) => {
|
||||||
<FormikField
|
<FormikField
|
||||||
className={classes.editing}
|
className={classes.editing}
|
||||||
id={field.name}
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
component={field.component}
|
component={field.component}
|
||||||
value={field.value}
|
|
||||||
type={field.type}
|
type={field.type}
|
||||||
width={size}
|
width={size}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -103,27 +106,33 @@ 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,
|
||||||
[classes.label1Rejected]: state === OVERRIDE_REJECTED,
|
[classes.label1Rejected]: state === OVERRIDE_REJECTED,
|
||||||
[classes.label1Accepted]: state === OVERRIDE_AUTHORIZED
|
[classes.label1Accepted]: state === OVERRIDE_AUTHORIZED
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorized =
|
const authorized =
|
||||||
state === OVERRIDE_PENDING
|
state === OVERRIDE_PENDING
|
||||||
? { label: 'Pending', type: 'neutral' }
|
? { label: 'Pending', type: 'neutral' }
|
||||||
|
|
@ -131,111 +140,176 @@ const EditableCard = ({
|
||||||
? { label: 'Rejected', type: 'error' }
|
? { label: 'Rejected', type: 'error' }
|
||||||
: { label: 'Accepted', type: 'success' }
|
: { label: 'Accepted', type: 'success' }
|
||||||
|
|
||||||
const editableField = field => {
|
|
||||||
return <EditableField field={field} editing={editing} size={180} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card className={classes.card}>
|
<Card className={classes.card}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className={classes.cardHeader}>
|
<div className={classes.headerWrapper}>
|
||||||
{titleIcon}
|
<div className={classes.cardHeader}>
|
||||||
<H3 className={classes.cardTitle}>{title}</H3>
|
{titleIcon}
|
||||||
<Tooltip width={304}></Tooltip>
|
<H3 className={classes.cardTitle}>{title}</H3>
|
||||||
<div className={classnames(label1ClassNames)}>
|
<Tooltip width={304}></Tooltip>
|
||||||
<MainStatus statuses={[authorized]} />
|
|
||||||
</div>
|
</div>
|
||||||
|
{state && (
|
||||||
|
<div className={classnames(label1ClassNames)}>
|
||||||
|
<MainStatus statuses={[authorized]} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{children}
|
||||||
<Formik
|
<Formik
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
onSubmit={values => save(values)}
|
validationSchema={validationSchema}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={values => {
|
||||||
|
save(values)
|
||||||
|
setEditing(false)
|
||||||
|
}}
|
||||||
onReset={() => {
|
onReset={() => {
|
||||||
setEditing(false)
|
setEditing(false)
|
||||||
setError(false)
|
setError(false)
|
||||||
}}>
|
}}>
|
||||||
<Form>
|
{({ values, touched, errors, setFieldValue }) => (
|
||||||
<PromptWhenDirty />
|
<Form>
|
||||||
<div className={classes.row}>
|
<PromptWhenDirty />
|
||||||
<Grid container>
|
<div className={classes.row}>
|
||||||
<Grid container direction="column" item xs={6}>
|
<Grid container>
|
||||||
{data?.map((field, idx) => {
|
<Grid container direction="column" item xs={6}>
|
||||||
return idx >= 0 && idx < 4 ? editableField(field) : null
|
{!hasImage &&
|
||||||
})}
|
fields?.map((field, idx) => {
|
||||||
|
return idx >= 0 && idx < 4 ? (
|
||||||
|
<EditableField
|
||||||
|
field={field}
|
||||||
|
value={initialValues[field.name]}
|
||||||
|
editing={editing}
|
||||||
|
size={180}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
<Grid container direction="column" item xs={6}>
|
||||||
|
{!hasImage &&
|
||||||
|
fields?.map((field, idx) => {
|
||||||
|
return idx >= 4 ? (
|
||||||
|
<EditableField
|
||||||
|
field={field}
|
||||||
|
value={initialValues[field.name]}
|
||||||
|
editing={editing}
|
||||||
|
size={180}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid container direction="column" item xs={6}>
|
</div>
|
||||||
{data?.map((field, idx) => {
|
<div className={classes.edit}>
|
||||||
return idx >= 4 ? editableField(field) : null
|
{!editing && (
|
||||||
})}
|
<div className={classes.editButton}>
|
||||||
</Grid>
|
<div className={classes.deleteButton}>
|
||||||
</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}>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="secondary"
|
color="primary"
|
||||||
Icon={SaveIcon}
|
type="button"
|
||||||
InverseIcon={SaveReversedIcon}
|
Icon={DeleteIcon}
|
||||||
type="submit">
|
InverseIcon={DeleteReversedIcon}
|
||||||
Save
|
onClick={() => deleteEditedData()}>
|
||||||
|
{`Delete`}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className={classes.button}>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="secondary"
|
color="primary"
|
||||||
Icon={CancelIcon}
|
Icon={EditIcon}
|
||||||
InverseIcon={CancelReversedIcon}
|
InverseIcon={EditReversedIcon}
|
||||||
type="reset">
|
onClick={() => setEditing(true)}>
|
||||||
Cancel
|
{`Edit`}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
{authorized.label !== 'Accepted' && (
|
)}
|
||||||
<div className={classes.button}>
|
{editing && (
|
||||||
<ActionButton
|
<div className={classes.editingWrapper}>
|
||||||
color="secondary"
|
<div className={classes.replace}>
|
||||||
Icon={AuthorizeIcon}
|
{hasImage && (
|
||||||
InverseIcon={AuthorizeReversedIcon}
|
<ActionButton
|
||||||
type="submit"
|
color="secondary"
|
||||||
onClick={() => authorize()}>
|
type="button"
|
||||||
{'Authorize'}
|
Icon={ReplaceReversedIcon}
|
||||||
</ActionButton>
|
InverseIcon={ReplaceReversedIcon}
|
||||||
|
onClick={() => triggerInput()}>
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
alt=""
|
||||||
|
accept="image/*"
|
||||||
|
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)
|
||||||
|
if (!file) return
|
||||||
|
setFieldValue(R.head(fields).name, file)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Replace
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className={classes.editingButtons}>
|
||||||
{authorized.label !== 'Rejected' && (
|
{fields && (
|
||||||
<ActionButton
|
<div className={classes.button}>
|
||||||
color="secondary"
|
<ActionButton
|
||||||
Icon={CancelIcon}
|
color="secondary"
|
||||||
InverseIcon={CancelReversedIcon}
|
Icon={SaveReversedIcon}
|
||||||
type="submit"
|
InverseIcon={SaveReversedIcon}
|
||||||
onClick={() => reject()}>
|
type="submit">
|
||||||
{'Reject'}
|
Save
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
</div>
|
||||||
{error && (
|
)}
|
||||||
<ErrorMessage>Failed to save changes</ErrorMessage>
|
<div className={classes.button}>
|
||||||
)}
|
<ActionButton
|
||||||
</div>
|
color="secondary"
|
||||||
)}
|
Icon={CancelReversedIcon}
|
||||||
</div>
|
InverseIcon={CancelReversedIcon}
|
||||||
</Form>
|
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>
|
</Formik>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,35 @@ export default {
|
||||||
color: spring4
|
color: spring4
|
||||||
},
|
},
|
||||||
editButton: {
|
editButton: {
|
||||||
marginTop: 30,
|
marginTop: 20,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'right'
|
justifyContent: 'right'
|
||||||
},
|
},
|
||||||
button: {
|
deleteButton: {
|
||||||
marginRight: 8
|
marginRight: 8
|
||||||
},
|
},
|
||||||
|
headerWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
height: 40
|
||||||
|
},
|
||||||
|
editingWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: 20
|
||||||
|
},
|
||||||
|
replace: {
|
||||||
|
marginRight: 5
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
display: 'none'
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: 5
|
||||||
|
},
|
||||||
editingButtons: {
|
editingButtons: {
|
||||||
marginTop: 30,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'right'
|
justifyContent: 'right'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ const TransactionsList = ({ customer, data, loading, locale }) => {
|
||||||
view: R.path(['machineName'])
|
view: R.path(['machineName'])
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Direction',
|
|
||||||
width: 125,
|
width: 125,
|
||||||
view: it => (
|
view: it => (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const spring = '#48f694'
|
||||||
// Secondary
|
// Secondary
|
||||||
const comet = '#5f668a'
|
const comet = '#5f668a'
|
||||||
const comet2 = '#72799d'
|
const comet2 = '#72799d'
|
||||||
|
const comet3 = '#525772'
|
||||||
const spring2 = '#44e188'
|
const spring2 = '#44e188'
|
||||||
const spring3 = '#ecfbef'
|
const spring3 = '#ecfbef'
|
||||||
const spring4 = '#3fd07e'
|
const spring4 = '#3fd07e'
|
||||||
|
|
@ -25,6 +26,8 @@ const white = '#ffffff'
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
const tomato = '#ff584a'
|
const tomato = '#ff584a'
|
||||||
|
const tomato1 = '#E45043'
|
||||||
|
const tomato2 = '#CE463A'
|
||||||
const mistyRose = '#ffeceb'
|
const mistyRose = '#ffeceb'
|
||||||
const pumpkin = '#ff7311'
|
const pumpkin = '#ff7311'
|
||||||
const linen = '#fbf3ec'
|
const linen = '#fbf3ec'
|
||||||
|
|
@ -45,8 +48,11 @@ const disabledColor2 = concrete
|
||||||
const fontColor = primaryColor
|
const fontColor = primaryColor
|
||||||
const offColor = comet
|
const offColor = comet
|
||||||
const offDarkColor = comet2
|
const offDarkColor = comet2
|
||||||
|
const offDarkerColor = comet3
|
||||||
const placeholderColor = comet
|
const placeholderColor = comet
|
||||||
const errorColor = tomato
|
const errorColor = tomato
|
||||||
|
const errorColorDark = tomato1
|
||||||
|
const errorColorDarker = tomato2
|
||||||
const offErrorColor = mistyRose
|
const offErrorColor = mistyRose
|
||||||
const inputBorderColor = primaryColor
|
const inputBorderColor = primaryColor
|
||||||
|
|
||||||
|
|
@ -142,12 +148,15 @@ export {
|
||||||
placeholderColor,
|
placeholderColor,
|
||||||
offColor,
|
offColor,
|
||||||
offDarkColor,
|
offDarkColor,
|
||||||
|
offDarkerColor,
|
||||||
fontColor,
|
fontColor,
|
||||||
disabledColor,
|
disabledColor,
|
||||||
disabledColor2,
|
disabledColor2,
|
||||||
linkPrimaryColor,
|
linkPrimaryColor,
|
||||||
linkSecondaryColor,
|
linkSecondaryColor,
|
||||||
errorColor,
|
errorColor,
|
||||||
|
errorColorDarker,
|
||||||
|
errorColorDark,
|
||||||
offErrorColor,
|
offErrorColor,
|
||||||
inputBorderColor,
|
inputBorderColor,
|
||||||
// font sizes
|
// font sizes
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory'
|
||||||
import { ApolloClient } from 'apollo-client'
|
import { ApolloClient } from 'apollo-client'
|
||||||
import { ApolloLink } from 'apollo-link'
|
import { ApolloLink } from 'apollo-link'
|
||||||
import { onError } from 'apollo-link-error'
|
import { onError } from 'apollo-link-error'
|
||||||
import { HttpLink } from 'apollo-link-http'
|
import { createUploadLink } from 'apollo-upload-client'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useHistory, useLocation } from 'react-router-dom'
|
import { useHistory, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
|
@ -15,6 +15,16 @@ const URI =
|
||||||
const ALT_URI =
|
const ALT_URI =
|
||||||
process.env.NODE_ENV === 'development' ? 'http://localhost:4001' : ''
|
process.env.NODE_ENV === 'development' ? 'http://localhost:4001' : ''
|
||||||
|
|
||||||
|
const uploadLink = createUploadLink({
|
||||||
|
credentials: 'include',
|
||||||
|
uri: `${URI}/graphql`
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadLinkALT = createUploadLink({
|
||||||
|
credentials: 'include',
|
||||||
|
uri: `${ALT_URI}/graphql`
|
||||||
|
})
|
||||||
|
|
||||||
const getClient = (history, location, getUserData, setUserData, setRole) =>
|
const getClient = (history, location, getUserData, setUserData, setRole) =>
|
||||||
new ApolloClient({
|
new ApolloClient({
|
||||||
link: ApolloLink.from([
|
link: ApolloLink.from([
|
||||||
|
|
@ -48,14 +58,8 @@ const getClient = (history, location, getUserData, setUserData, setRole) =>
|
||||||
}),
|
}),
|
||||||
ApolloLink.split(
|
ApolloLink.split(
|
||||||
operation => operation.getContext().clientName === 'pazuz',
|
operation => operation.getContext().clientName === 'pazuz',
|
||||||
new HttpLink({
|
uploadLinkALT,
|
||||||
credentials: 'include',
|
uploadLink
|
||||||
uri: `${ALT_URI}/graphql`
|
|
||||||
}),
|
|
||||||
new HttpLink({
|
|
||||||
credentials: 'include',
|
|
||||||
uri: `${URI}/graphql`
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache(),
|
||||||
|
|
|
||||||
24
package-lock.json
generated
24
package-lock.json
generated
|
|
@ -11479,6 +11479,30 @@
|
||||||
"resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz",
|
||||||
"integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg=="
|
"integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg=="
|
||||||
},
|
},
|
||||||
|
"graphql-upload": {
|
||||||
|
"version": "12.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-12.0.0.tgz",
|
||||||
|
"integrity": "sha512-ovZ3Q7sZ17Bmn8tYl22MfrpNR7nYM/DUszXWgkue7SFIlI9jtqszHAli8id8ZcnGBc9GF0gUTNSskYWW+5aNNQ==",
|
||||||
|
"requires": {
|
||||||
|
"busboy": "^0.3.1",
|
||||||
|
"fs-capacitor": "^6.2.0",
|
||||||
|
"http-errors": "^1.8.0",
|
||||||
|
"isobject": "^4.0.0",
|
||||||
|
"object-path": "^0.11.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fs-capacitor": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-nKcE1UduoSKX27NSZlg879LdQc94OtbOsEmKMN2MBNudXREvijRKx2GEBsTMTfws+BrbkJoEuynbGSVRSpauvw=="
|
||||||
|
},
|
||||||
|
"isobject": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"graphql-ws": {
|
"graphql-ws": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
"graphql-iso-date": "^3.6.1",
|
"graphql-iso-date": "^3.6.1",
|
||||||
"graphql-tools": "^7.0.2",
|
"graphql-tools": "^7.0.2",
|
||||||
"graphql-type-json": "^0.3.1",
|
"graphql-type-json": "^0.3.1",
|
||||||
|
"graphql-upload": "12.0.0",
|
||||||
"helmet": "^3.8.1",
|
"helmet": "^3.8.1",
|
||||||
"inquirer": "^5.2.0",
|
"inquirer": "^5.2.0",
|
||||||
"json2csv": "^5.0.3",
|
"json2csv": "^5.0.3",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue