feat: customer notes migration
feat: customer notes backend operations feat: add customer note mutation feat: add editing capabilities to PropertyCard feat: connect customer notes backend to frontend fix: customer note form and static content styling fix: SQL uppercasing fix: set default value for notes content fix: SQL after dev rebase refactor: move get current user token to separate method
This commit is contained in:
parent
f14674c4f3
commit
dcd3259484
11 changed files with 268 additions and 38 deletions
30
lib/customer-notes.js
Normal file
30
lib/customer-notes.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
const uuid = require('uuid')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
const getCustomerNotes = customerId => {
|
||||||
|
const sql = `SELECT * FROM customer_notes WHERE customer_id=$1 LIMIT 1`
|
||||||
|
return db.oneOrNone(sql, [customerId]).then(res => _.mapKeys((_, key) => _.camelize(key), res))
|
||||||
|
}
|
||||||
|
|
||||||
|
const createCustomerNotes = (customerId, userId, content) => {
|
||||||
|
const sql = `INSERT INTO customer_notes (id, customer_id, last_edited_by, last_edited_at, content) VALUES ($1, $2, $3, now(), $4)`
|
||||||
|
return db.none(sql, [uuid.v4(), customerId, userId, content])
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCustomerNotes = (customerId, userId, content) => {
|
||||||
|
const sql = `UPDATE customer_notes SET last_edited_at=now(), last_edited_by=$1, content=$2 WHERE customer_id=$3 RETURNING *`
|
||||||
|
return db.any(sql, [userId, content, customerId])
|
||||||
|
.then(res => {
|
||||||
|
if (_.isEmpty(res)) {
|
||||||
|
createCustomerNotes(customerId, userId, content)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCustomerNotes,
|
||||||
|
createCustomerNotes,
|
||||||
|
updateCustomerNotes
|
||||||
|
}
|
||||||
|
|
@ -632,7 +632,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
|
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
|
||||||
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||||
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, custom_fields
|
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, content AS notes
|
||||||
FROM (
|
FROM (
|
||||||
SELECT c.id, c.authorized_override,
|
SELECT c.id, c.authorized_override,
|
||||||
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
|
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
|
||||||
|
|
@ -640,7 +640,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
c.front_camera_path, c.front_camera_override,
|
c.front_camera_path, 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_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_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, cn.content,
|
||||||
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,
|
||||||
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
|
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
|
||||||
|
|
@ -655,6 +655,9 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
LEFT OUTER JOIN customer_custom_field_pairs ccfp ON cfd.id = ccfp.custom_field_id
|
LEFT OUTER JOIN customer_custom_field_pairs ccfp ON cfd.id = ccfp.custom_field_id
|
||||||
) cf GROUP BY cf.customer_id
|
) cf GROUP BY cf.customer_id
|
||||||
) ccf ON c.id = ccf.customer_id
|
) ccf ON c.id = ccf.customer_id
|
||||||
|
LEFT OUTER JOIN (
|
||||||
|
SELECT customer_id, content FROM customer_notes
|
||||||
|
) cn ON c.id = cn.customer_id
|
||||||
WHERE c.id != $2
|
WHERE c.id != $2
|
||||||
) AS cl WHERE rn = 1
|
) AS cl WHERE rn = 1
|
||||||
AND ($4 IS NULL OR phone = $4)
|
AND ($4 IS NULL OR phone = $4)
|
||||||
|
|
@ -678,35 +681,38 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
*/
|
*/
|
||||||
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_at, front_camera_path, front_camera_at, front_camera_override,
|
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
|
||||||
phone, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
|
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
|
||||||
id_card_photo_at, id_card_photo_path, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
id_card_photo_path, id_card_photo_override, 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, content AS notes
|
||||||
from (
|
FROM (
|
||||||
select c.id, c.authorized_override,
|
SELECT c.id, c.authorized_override,
|
||||||
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_override,
|
||||||
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.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
|
||||||
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.id_card_photo_path, c.id_card_photo_override, c.us_ssn, 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, cn.content,
|
||||||
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,
|
||||||
sum(case when error_code is null or error_code not in ($1^) then t.fiat else 0 end) over (partition by c.id) as total_spent, ccf.custom_fields
|
sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (PARTITION BY c.id) 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
|
||||||
select 'cashOut' as tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
SELECT 'cashOut' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||||
from cash_out_txs where confirmed_at is not null) t on c.id = t.customer_id
|
FROM cash_out_txs WHERE confirmed_at IS NOT NULL) t ON c.id = t.customer_id
|
||||||
LEFT OUTER JOIN (
|
LEFT OUTER JOIN (
|
||||||
SELECT cf.customer_id, json_agg(json_build_object('id', cf.custom_field_id, 'label', cf.label, 'value', cf.value)) AS custom_fields FROM (
|
SELECT cf.customer_id, json_agg(json_build_object('id', cf.custom_field_id, 'label', cf.label, 'value', cf.value)) AS custom_fields FROM (
|
||||||
SELECT ccfp.custom_field_id, ccfp.customer_id, cfd.label, ccfp.value FROM custom_field_definitions cfd
|
SELECT ccfp.custom_field_id, ccfp.customer_id, cfd.label, ccfp.value FROM custom_field_definitions cfd
|
||||||
LEFT OUTER JOIN customer_custom_field_pairs ccfp ON cfd.id = ccfp.custom_field_id
|
LEFT OUTER JOIN customer_custom_field_pairs ccfp ON cfd.id = ccfp.custom_field_id
|
||||||
) cf GROUP BY cf.customer_id
|
) cf GROUP BY cf.customer_id
|
||||||
) ccf ON c.id = ccf.customer_id
|
) ccf ON c.id = ccf.customer_id
|
||||||
where c.id = $2
|
LEFT OUTER JOIN (
|
||||||
) as cl where rn = 1`
|
SELECT customer_id, content FROM customer_notes
|
||||||
|
) cn ON c.id = cn.customer_id
|
||||||
|
WHERE c.id = $2
|
||||||
|
) AS cl WHERE rn = 1`
|
||||||
return db.oneOrNone(sql, [passableErrorCodes, id])
|
return db.oneOrNone(sql, [passableErrorCodes, id])
|
||||||
.then(customerData => {
|
.then(customerData => {
|
||||||
return getEditedData(id)
|
return getEditedData(id)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { ApolloError } = require('apollo-server-express')
|
const { ApolloError, AuthenticationError } = require('apollo-server-express')
|
||||||
|
|
||||||
class InvalidCredentialsError extends ApolloError {
|
class InvalidCredentialsError extends ApolloError {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
|
|
@ -29,6 +29,7 @@ class InvalidUrlError extends ApolloError {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
AuthenticationError,
|
||||||
InvalidCredentialsError,
|
InvalidCredentialsError,
|
||||||
UserAlreadyExistsError,
|
UserAlreadyExistsError,
|
||||||
InvalidTwoFactorError,
|
InvalidTwoFactorError,
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,13 @@ const reset2FA = (token, userID, code, context) => {
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getToken = context => {
|
||||||
|
if (_.isNil(context.req.cookies.lid) || _.isNil(context.req.session.user.id))
|
||||||
|
throw new authErrors.AuthenticationError('Authentication failed')
|
||||||
|
|
||||||
|
return context.req.session.user.id
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
getUserData,
|
getUserData,
|
||||||
|
|
@ -259,5 +266,6 @@ module.exports = {
|
||||||
createRegisterToken,
|
createRegisterToken,
|
||||||
register,
|
register,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
reset2FA
|
reset2FA,
|
||||||
|
getToken
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
const authentication = require('../modules/authentication')
|
||||||
const anonymous = require('../../../constants').anonymousCustomer
|
const anonymous = require('../../../constants').anonymousCustomer
|
||||||
const customers = require('../../../customers')
|
const customers = require('../../../customers')
|
||||||
const filters = require('../../filters')
|
const filters = require('../../filters')
|
||||||
|
const customerNotes = require('../../../customer-notes')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
|
|
||||||
|
|
@ -14,8 +16,7 @@ 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 = authentication.getToken(context)
|
||||||
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)
|
||||||
},
|
},
|
||||||
|
|
@ -23,14 +24,12 @@ const resolvers = {
|
||||||
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) => {
|
editCustomer: async (root, { customerId, customerEdit }, context) => {
|
||||||
// TODO: To be replaced by function that fetchs the token
|
const token = authentication.getToken(context)
|
||||||
const token = !!context.req.cookies.lid && context.req.session.user.id
|
|
||||||
const editedData = await customerEdit
|
const editedData = await customerEdit
|
||||||
return customers.edit(customerId, editedData, token)
|
return customers.edit(customerId, editedData, token)
|
||||||
},
|
},
|
||||||
replacePhoto: async (root, { customerId, photoType, newPhoto }, context) => {
|
replacePhoto: async (root, { customerId, photoType, newPhoto }, context) => {
|
||||||
// TODO: To be replaced by function that fetchs the token
|
const token = authentication.getToken(context)
|
||||||
const token = !!context.req.cookies.lid && context.req.session.user.id
|
|
||||||
const photo = await newPhoto
|
const photo = await newPhoto
|
||||||
if (!photo) return customers.getCustomerById(customerId)
|
if (!photo) return customers.getCustomerById(customerId)
|
||||||
return customers.updateEditedPhoto(customerId, photo, photoType)
|
return customers.updateEditedPhoto(customerId, photo, photoType)
|
||||||
|
|
@ -39,6 +38,10 @@ const resolvers = {
|
||||||
deleteEditedData: (root, { customerId, customerEdit }) => {
|
deleteEditedData: (root, { customerId, customerEdit }) => {
|
||||||
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
|
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
|
||||||
return customers.getCustomerById(customerId)
|
return customers.getCustomerById(customerId)
|
||||||
|
},
|
||||||
|
setCustomerNotes: (...[, { customerId, newContent }, context]) => {
|
||||||
|
const token = authentication.getToken(context)
|
||||||
|
return customerNotes.updateCustomerNotes(customerId, token, newContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ const typeDef = gql`
|
||||||
subscriberInfo: JSONObject
|
subscriberInfo: JSONObject
|
||||||
customFields: [CustomerCustomField]
|
customFields: [CustomerCustomField]
|
||||||
customInfoRequests: [CustomRequestData]
|
customInfoRequests: [CustomRequestData]
|
||||||
|
notes: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input CustomerInput {
|
input CustomerInput {
|
||||||
|
|
@ -89,6 +90,7 @@ const typeDef = gql`
|
||||||
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
||||||
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
||||||
replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth
|
replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth
|
||||||
|
setCustomerNotes(customerId: ID!, newContent: String!): Customer @auth
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
20
migrations/1627868356883-customer-custom-notes.js
Normal file
20
migrations/1627868356883-customer-custom-notes.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
var db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
var sql = [
|
||||||
|
`CREATE TABLE customer_notes (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
customer_id UUID NOT NULL REFERENCES customers(id),
|
||||||
|
created TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
last_edited_at TIMESTAMPTZ,
|
||||||
|
last_edited_by UUID REFERENCES users(id),
|
||||||
|
content TEXT NOT NULL DEFAULT ''
|
||||||
|
)`
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
@ -69,6 +69,7 @@ const GET_CUSTOMER = gql`
|
||||||
label
|
label
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
notes
|
||||||
transactions {
|
transactions {
|
||||||
txClass
|
txClass
|
||||||
id
|
id
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { useMutation } from '@apollo/react-hooks'
|
||||||
|
import { makeStyles } from '@material-ui/core'
|
||||||
|
import { Formik, Form, Field } from 'formik'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
|
import { TextInput } from 'src/components/inputs/formik'
|
||||||
|
import { Info3 } from 'src/components/typography'
|
||||||
|
|
||||||
|
import styles from './CustomerNotes.styles'
|
||||||
|
import { PropertyCard } from './propertyCard'
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
notes: Yup.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const SAVE_CUSTOMER_NOTES = gql`
|
||||||
|
mutation setCustomerNotes($customerId: ID!, $newContent: String!) {
|
||||||
|
setCustomerNotes(customerId: $customerId, newContent: $newContent) {
|
||||||
|
notes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const CustomerNotes = ({ customer }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [editing, setEditing] = useState(false)
|
||||||
|
const [setNotes] = useMutation(SAVE_CUSTOMER_NOTES, {
|
||||||
|
refetchQueries: () => ['customer']
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
notes: R.path(['notes'])(customer) ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = values => {
|
||||||
|
setNotes({
|
||||||
|
variables: {
|
||||||
|
customerId: customer.id,
|
||||||
|
newContent: values.notes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setEditing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFormattedNotes = content => {
|
||||||
|
const fragments = R.split(/\n/)(content ?? '')
|
||||||
|
return R.map((it, idx) => {
|
||||||
|
if (idx === fragments.length) return <>{it}</>
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{it}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}, fragments)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PropertyCard
|
||||||
|
title={'Notes'}
|
||||||
|
edit={() => setEditing(true)}
|
||||||
|
confirm={true}
|
||||||
|
isEditing={editing}
|
||||||
|
formName="notes-form"
|
||||||
|
className={classes.root}
|
||||||
|
contentClassName={classes.content}>
|
||||||
|
{!editing && (
|
||||||
|
<Info3>{getFormattedNotes(R.path(['notes'])(customer))}</Info3>
|
||||||
|
)}
|
||||||
|
{editing && (
|
||||||
|
<Formik
|
||||||
|
validateOnBlur={false}
|
||||||
|
validateOnChange={false}
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={values => handleConfirm(values)}>
|
||||||
|
<Form id="notes-form" className={classes.form}>
|
||||||
|
<Field
|
||||||
|
name="notes"
|
||||||
|
fullWidth
|
||||||
|
multiline={true}
|
||||||
|
rows={6}
|
||||||
|
component={TextInput}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
)}
|
||||||
|
</PropertyCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomerNotes
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
const styles = {
|
||||||
|
root: {
|
||||||
|
height: 'auto'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
height: 'auto'
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
display: 'flex',
|
||||||
|
flexBasis: '90%',
|
||||||
|
flexShrink: 1,
|
||||||
|
marginTop: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default styles
|
||||||
|
|
@ -10,6 +10,8 @@ import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/butto
|
||||||
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
|
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
|
||||||
import { ReactComponent as RejectReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
|
import { ReactComponent as RejectReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
|
||||||
import { ReactComponent as RejectIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
|
import { ReactComponent as RejectIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
|
||||||
|
import { ReactComponent as EditReversedIcon } from 'src/styling/icons/button/edit/white.svg'
|
||||||
|
import { ReactComponent as EditIcon } from 'src/styling/icons/button/edit/zodiac.svg'
|
||||||
|
|
||||||
import { propertyCardStyles } from './PropertyCard.styles'
|
import { propertyCardStyles } from './PropertyCard.styles'
|
||||||
|
|
||||||
|
|
@ -20,7 +22,19 @@ const OVERRIDE_AUTHORIZED = 'verified'
|
||||||
const OVERRIDE_REJECTED = 'blocked'
|
const OVERRIDE_REJECTED = 'blocked'
|
||||||
|
|
||||||
const PropertyCard = memo(
|
const PropertyCard = memo(
|
||||||
({ className, title, state, authorize, reject, children }) => {
|
({
|
||||||
|
className,
|
||||||
|
contentClassName,
|
||||||
|
title,
|
||||||
|
state,
|
||||||
|
authorize,
|
||||||
|
reject,
|
||||||
|
edit,
|
||||||
|
confirm,
|
||||||
|
isEditing,
|
||||||
|
formName,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const label1ClassNames = {
|
const label1ClassNames = {
|
||||||
|
|
@ -52,6 +66,29 @@ const PropertyCard = memo(
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const EditButton = () => (
|
||||||
|
<ActionButton
|
||||||
|
className={classes.cardActionButton}
|
||||||
|
color="secondary"
|
||||||
|
Icon={EditIcon}
|
||||||
|
InverseIcon={EditReversedIcon}
|
||||||
|
onClick={() => edit()}>
|
||||||
|
Edit
|
||||||
|
</ActionButton>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ConfirmButton = () => (
|
||||||
|
<ActionButton
|
||||||
|
className={classes.cardActionButton}
|
||||||
|
type="submit"
|
||||||
|
form={formName}
|
||||||
|
color="secondary"
|
||||||
|
Icon={AuthorizeIcon}
|
||||||
|
InverseIcon={AuthorizeReversedIcon}>
|
||||||
|
Confirm
|
||||||
|
</ActionButton>
|
||||||
|
)
|
||||||
|
|
||||||
const authorized =
|
const authorized =
|
||||||
state === OVERRIDE_PENDING
|
state === OVERRIDE_PENDING
|
||||||
? { label: 'Pending', type: 'neutral' }
|
? { label: 'Pending', type: 'neutral' }
|
||||||
|
|
@ -64,14 +101,22 @@ const PropertyCard = memo(
|
||||||
className={classnames(classes.propertyCard, className)}
|
className={classnames(classes.propertyCard, className)}
|
||||||
elevation={0}>
|
elevation={0}>
|
||||||
<H3 className={classes.propertyCardTopRow}>{title}</H3>
|
<H3 className={classes.propertyCardTopRow}>{title}</H3>
|
||||||
<div className={classes.propertyCardBottomRow}>
|
<div
|
||||||
|
className={classnames(
|
||||||
|
classes.propertyCardBottomRow,
|
||||||
|
contentClassName
|
||||||
|
)}>
|
||||||
|
{state && (
|
||||||
<div className={classnames(label1ClassNames)}>
|
<div className={classnames(label1ClassNames)}>
|
||||||
<MainStatus statuses={[authorized]} />
|
<MainStatus statuses={[authorized]} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
<div className={classes.buttonsWrapper}>
|
<div className={classes.buttonsWrapper}>
|
||||||
{authorize && state !== OVERRIDE_AUTHORIZED && AuthorizeButton()}
|
{authorize && state !== OVERRIDE_AUTHORIZED && AuthorizeButton()}
|
||||||
{reject && state !== OVERRIDE_REJECTED && RejectButton()}
|
{reject && state !== OVERRIDE_REJECTED && RejectButton()}
|
||||||
|
{edit && !isEditing && EditButton()}
|
||||||
|
{confirm && isEditing && ConfirmButton()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue