feat: added the compliance/customers route
feat: added customers list page feat: created the Customer type on the gql server and consume it Currently only with the 'name' property feat: added query on gql to get the customers list with the needed props feat: added the currently available props to the front end table fix: consider only sent txs for the aggregations on the customers list fix: replace ExpTable with a non-expandable one fix: remove unused properties from gql and front-end fix: fixed the customers list columns width fix: the last active table column was reading the wrong property chore: remove debug logging fix: use the correct table columns to check for txs that should be considered on the customers list page fix: use the international format for phone numbers feat: added the search box fix: remove ordering from the gql customers list query and moved it to the front-end) fix: removed the search box chore: refactor the customers list table into a new component chore: cleanup code fix: fixed styles from customer list page header feat: created customer profile page and started a transition feature from the customer list refactor: make components out of customers list table and profile page feat: selecting a customer now transitions to its profile page feat: added the customer transactions list table fix: fix tx class button margins fix: fix tx class icon margins on the customer list fix: fixed crypto value style: fixed the table column widths feat: added the requirements column (no data yet, though) feat: added the header with the customer details (no image yet, though) feat: created the skeleton for the properties cards feat: create the breadcrumb on the customer profile page (no link yet) feat: added the children container in the property card feat: added block customer action button feat: added action buttons to the property cards feat: added a children prop to the property card component feat: added extra properties to the customer gql query feat: added override fields to the customers gql query style: added conditional styles to the property card component feat: added children to the customer property cards feat: create the edit button function on the property card feat: add error properties to the txs (from gql) style: fix action left editing action button and right property card margins feat: created a mutation to update a customer feat: added the customer auth override state to the gql query feat: fix the routing to the individual customer profile pages feat: made the 'Customers' label on the breadcrumb work as a link style: fixed the breadcrumb separator style: fixed the customer name style feat: made the action to block and authorize a customer as a toggle feat: removed the 'Super user' switch (left for v2) style: added the crossed camera icon on the photo style: fixed the rejected icons refactor: refactored some styles that were repetitive refactor: created constants for the override possible states feat: created functions for the authorization and blocking of overrides refactor: renamed setOverride to updateCustomer fix: remove current unused features feat: make the property cards fields read-only feat: setup id card photo and front camera photo image servers feat: add id card photo on the corresponding property card feat: add front camera photo on the customer profile header feat: added gql cache to update the front-end after any mutation style: added the crossed camera icon when there's no id card photo refactor: extracted the PropertyCard component to another file fix: deactivated the cache for the transactions (no need for it) refactor: removed unused styles fix: fixed front-camera-photo img path fix: changed gql local data updates from cache to query refetch refactor: move override status constants to the property card class refactor: make the image servers URI a const dependent on the build fix: remove requirements column from customer tx table (left for future version) fix: add aliases to gql query to correctly show errors on tx table style: fix the transaction errors styles feat: add terms and conditions page feat: add modal preview feat: remove preview fix: increase space between switch and fields feat: added the compliance/customers route feat: added customers list page feat: created the Customer type on the gql server and consume it Currently only with the 'name' property feat: added query on gql to get the customers list with the needed props feat: added the currently available props to the front end table fix: consider only sent txs for the aggregations on the customers list fix: replace ExpTable with a non-expandable one fix: remove unused properties from gql and front-end fix: fixed the customers list columns width fix: the last active table column was reading the wrong property chore: remove debug logging fix: use the correct table columns to check for txs that should be considered on the customers list page fix: use the international format for phone numbers feat: added the search box fix: remove ordering from the gql customers list query and moved it to the front-end) fix: removed the search box chore: refactor the customers list table into a new component chore: cleanup code fix: fixed styles from customer list page header fix: removed unused code refactor: move transactions to a custom resolver in the customer's query refactor: break the CustomerProfile component into several smaller ones style: changed the table row error color from red to no change and the error text from tomato to comet fix: removed repeated function (wrong merge) fix: make the updateCustomer function updates only what's explicitly told so style: return with the table row error style refactor: create a function to test if a value is null prior to passing it through another function fix: make t&c changes backwards compatible chore: bump eslint import library to activate rule fix: stop showing object on empty column fix: get machine logs page up-to-date fix: small admin fixes feat: add terms and conditions page feat: add modal preview feat: remove preview fix: increase space between switch and fields feat: added the compliance/customers route feat: added customers list page feat: created the Customer type on the gql server and consume it Currently only with the 'name' property feat: added query on gql to get the customers list with the needed props feat: added the currently available props to the front end table fix: consider only sent txs for the aggregations on the customers list fix: replace ExpTable with a non-expandable one fix: remove unused properties from gql and front-end fix: fixed the customers list columns width fix: the last active table column was reading the wrong property chore: remove debug logging fix: use the correct table columns to check for txs that should be considered on the customers list page fix: use the international format for phone numbers feat: added the search box fix: remove ordering from the gql customers list query and moved it to the front-end) fix: removed the search box chore: refactor the customers list table into a new component chore: cleanup code fix: fixed styles from customer list page header fix: make t&c changes backwards compatible fix: stop showing object on empty column fix: get machine logs page up-to-date feat: add terms and conditions page feat: add modal preview feat: remove preview fix: increase space between switch and fields feat: added the compliance/customers route feat: added customers list page feat: created the Customer type on the gql server and consume it Currently only with the 'name' property feat: added query on gql to get the customers list with the needed props feat: added the currently available props to the front end table fix: consider only sent txs for the aggregations on the customers list fix: replace ExpTable with a non-expandable one fix: remove unused properties from gql and front-end fix: fixed the customers list columns width fix: the last active table column was reading the wrong property chore: remove debug logging fix: use the correct table columns to check for txs that should be considered on the customers list page fix: use the international format for phone numbers feat: added the search box fix: remove ordering from the gql customers list query and moved it to the front-end) fix: removed the search box chore: refactor the customers list table into a new component chore: cleanup code fix: fixed styles from customer list page header fix: make t&c changes backwards compatible fix: stop showing object on empty column fix: get machine logs page up-to-date fix: small admin fixes feat: create add machine page feat: add terms and conditions page feat: add modal preview feat: remove preview fix: increase space between switch and fields feat: added the compliance/customers route feat: added customers list page feat: created the Customer type on the gql server and consume it Currently only with the 'name' property feat: added query on gql to get the customers list with the needed props feat: added the currently available props to the front end table fix: consider only sent txs for the aggregations on the customers list fix: replace ExpTable with a non-expandable one fix: remove unused properties from gql and front-end fix: fixed the customers list columns width fix: the last active table column was reading the wrong property chore: remove debug logging fix: use the correct table columns to check for txs that should be considered on the customers list page fix: use the international format for phone numbers feat: added the search box fix: remove ordering from the gql customers list query and moved it to the front-end) fix: removed the search box chore: refactor the customers list table into a new component chore: cleanup code fix: fixed styles from customer list page header fix: make t&c changes backwards compatible fix: stop showing object on empty column fix: get machine logs page up-to-date feat: create add machine page fix: fixed wrong merging fix: more fixes from last merge fix: export needed functions that wasn't exported from the customers module fix: removed the customer profile route from the header fix: replaced old dataTable with new component feat: added onClick event to new DataTable
This commit is contained in:
parent
840788e044
commit
c808ca3be9
29 changed files with 1215 additions and 106 deletions
|
|
@ -85,6 +85,30 @@ function update (id, data, userToken, txId) {
|
||||||
.then(camelize)
|
.then(camelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update customer record
|
||||||
|
*
|
||||||
|
* @name updateCustomer
|
||||||
|
* @function
|
||||||
|
*
|
||||||
|
* @param {string} id Customer's id
|
||||||
|
* @param {object} data Fields to update
|
||||||
|
*
|
||||||
|
* @returns {Promise} Newly updated Customer
|
||||||
|
*/
|
||||||
|
async function updateCustomer (id, data) {
|
||||||
|
const formattedData = _.pick(
|
||||||
|
['authorized_override', 'id_card_photo_override', 'id_card_data_override', 'sms_override'],
|
||||||
|
_.mapKeys(_.snakeCase, data))
|
||||||
|
|
||||||
|
const sql = Pgp.helpers.update(formattedData, _.keys(formattedData), 'customers') +
|
||||||
|
' where id=$1'
|
||||||
|
|
||||||
|
await db.none(sql, [id])
|
||||||
|
|
||||||
|
return getCustomerById(id)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get customer by id
|
* Get customer by id
|
||||||
*
|
*
|
||||||
|
|
@ -377,21 +401,24 @@ function batch () {
|
||||||
}, customers)))
|
}, customers)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: getCustomersList and getCustomerById are very similar, so this should be refactored
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query all customers, ordered by last activity
|
* Query all customers, 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
|
* @returns {array} Array of customers with it's transactions aggregations
|
||||||
*/
|
*/
|
||||||
function getCustomersList () {
|
function getCustomersList () {
|
||||||
const sql = `select name, phone, total_txs, total_spent,
|
const sql = `select id, name, authorized_override, front_camera_path, phone, sms_override,
|
||||||
coalesce(tx_created, customer_created) as last_active,
|
id_card_data, id_card_data_override, id_card_data_expiration, id_card_photo_path,
|
||||||
fiat as last_tx_fiat, fiat_code as last_tx_fiat_code,
|
id_card_photo_override, total_txs, total_spent, created as last_active,
|
||||||
tx_class as last_tx_class
|
fiat as last_tx_fiat, fiat_code as last_tx_fiat_code, tx_class as last_tx_class
|
||||||
from (
|
from (
|
||||||
select c.name, c.phone, c.created as customer_created,
|
select c.id, c.name, c.authorized_override, c.front_camera_path, c.phone, c.sms_override,
|
||||||
t.tx_class, t.fiat, t.fiat_code, t.created as tx_created,
|
c.id_card_data, c.id_card_data_override, c.id_card_data_expiration, c.id_card_photo_path,
|
||||||
|
c.id_card_photo_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,
|
||||||
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(t.fiat) over (partition by c.id), 0) as total_spent
|
coalesce(sum(t.fiat) over (partition by c.id), 0) as total_spent
|
||||||
|
|
@ -410,6 +437,37 @@ function getCustomersList () {
|
||||||
}, customers)))
|
}, customers)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query all customers, ordered by last activity
|
||||||
|
* and with aggregate columns based on their
|
||||||
|
* transactions
|
||||||
|
*
|
||||||
|
* @returns {array} Array of customers with it's transactions aggregations
|
||||||
|
*/
|
||||||
|
function getCustomerById (id) {
|
||||||
|
const sql = `select id, name, authorized_override, front_camera_path, phone, sms_override,
|
||||||
|
id_card_data, id_card_data_override, id_card_data_expiration, id_card_photo_path,
|
||||||
|
id_card_photo_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
|
||||||
|
from (
|
||||||
|
select c.id, c.name, c.authorized_override, c.front_camera_path, 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, t.tx_class, t.fiat, t.fiat_code, t.created,
|
||||||
|
row_number() over (partition by c.id order by t.created desc) as rn,
|
||||||
|
count(0) over (partition by c.id) as total_txs,
|
||||||
|
sum(t.fiat) over (partition by c.id) as total_spent
|
||||||
|
from customers c inner join (
|
||||||
|
select 'cashIn' as tx_class, id, fiat, fiat_code, created, customer_id
|
||||||
|
from cash_in_txs where send_confirmed = true union
|
||||||
|
select 'cashOut' as tx_class, id, fiat, fiat_code, created, customer_id
|
||||||
|
from cash_out_txs where confirmed_at is not null) t on c.id = t.customer_id
|
||||||
|
where c.id = $1
|
||||||
|
) as cl where rn = 1`
|
||||||
|
return db.oneOrNone(sql, [id])
|
||||||
|
.then(populateOverrideUsernames)
|
||||||
|
.then(camelize)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} id customer id
|
* @param {String} id customer id
|
||||||
* @param {Object} patch customer update record
|
* @param {Object} patch customer update record
|
||||||
|
|
@ -515,4 +573,4 @@ function updateFrontCamera (id, patch) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { add, get, batch, getCustomersList, getById, update, updatePhotoCard, updateFrontCamera }
|
module.exports = { add, get, batch, getCustomersList, getCustomerById, getById, update, updateCustomer, updatePhotoCard, updateFrontCamera }
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
|
const serveStatic = require('serve-static')
|
||||||
const cors = require('cors')
|
const cors = require('cors')
|
||||||
const helmet = require('helmet')
|
const helmet = require('helmet')
|
||||||
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 _ = require('lodash/fp')
|
||||||
|
|
||||||
const T = require('../time')
|
const T = require('../time')
|
||||||
const options = require('../options')
|
const options = require('../options')
|
||||||
|
|
@ -15,6 +17,8 @@ const { typeDefs, resolvers } = require('./graphql/schema')
|
||||||
|
|
||||||
const devMode = require('minimist')(process.argv.slice(2)).dev
|
const devMode = require('minimist')(process.argv.slice(2)).dev
|
||||||
const NEVER = new Date(Date.now() + 100 * T.years)
|
const NEVER = new Date(Date.now() + 100 * T.years)
|
||||||
|
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
|
||||||
|
const frontCameraBasedir = _.get('frontCameraDir', options)
|
||||||
|
|
||||||
const hostname = options.hostname
|
const hostname = options.hostname
|
||||||
if (!hostname) {
|
if (!hostname) {
|
||||||
|
|
@ -55,6 +59,9 @@ apolloServer.applyMiddleware({
|
||||||
// cors on app for /api/register endpoint.
|
// cors on app for /api/register endpoint.
|
||||||
app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3000' }))
|
app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3000' }))
|
||||||
|
|
||||||
|
app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, {index: false}))
|
||||||
|
app.use('/front-camera-photo', serveStatic(frontCameraBasedir, {index: false}))
|
||||||
|
|
||||||
app.get('/api/register', (req, res, next) => {
|
app.get('/api/register', (req, res, next) => {
|
||||||
const otp = req.query.otp
|
const otp = req.query.otp
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,37 @@ const typeDefs = gql`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Customer {
|
type Customer {
|
||||||
name: String
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
authorizedOverride: String
|
||||||
|
frontCameraPath: String
|
||||||
phone: String
|
phone: String
|
||||||
|
smsOverride: String
|
||||||
|
idCardData: JSONObject
|
||||||
|
idCardDataOverride: String
|
||||||
|
idCardDataExpiration: Date
|
||||||
|
idCardPhotoPath: String
|
||||||
|
idCardPhotoOverride: String
|
||||||
|
totalTxs: Int
|
||||||
|
totalSpent: String
|
||||||
|
lastActive: Date
|
||||||
|
lastTxFiat: String
|
||||||
|
lastTxFiatCode: String
|
||||||
|
lastTxClass: String
|
||||||
|
transactions: [Transaction]
|
||||||
|
}
|
||||||
|
|
||||||
|
input CustomerInput {
|
||||||
|
name: String
|
||||||
|
authorizedOverride: String
|
||||||
|
frontCameraPath: String
|
||||||
|
phone: String
|
||||||
|
smsOverride: String
|
||||||
|
idCardData: JSONObject
|
||||||
|
idCardDataOverride: String
|
||||||
|
idCardDataExpiration: Date
|
||||||
|
idCardPhotoPath: String
|
||||||
|
idCardPhotoOverride: String
|
||||||
totalTxs: Int
|
totalTxs: Int
|
||||||
totalSpent: String
|
totalSpent: String
|
||||||
lastActive: Date
|
lastActive: Date
|
||||||
|
|
@ -160,6 +189,7 @@ const typeDefs = gql`
|
||||||
cryptoCurrencies: [CryptoCurrency]
|
cryptoCurrencies: [CryptoCurrency]
|
||||||
machines: [Machine]
|
machines: [Machine]
|
||||||
customers: [Customer]
|
customers: [Customer]
|
||||||
|
customer(customerId: ID!): Customer
|
||||||
machineLogs(deviceId: ID!): [MachineLog]
|
machineLogs(deviceId: ID!): [MachineLog]
|
||||||
funding: [CoinFunds]
|
funding: [CoinFunds]
|
||||||
serverVersion: String!
|
serverVersion: String!
|
||||||
|
|
@ -187,6 +217,7 @@ const typeDefs = gql`
|
||||||
machineAction(deviceId:ID!, action: MachineAction!): Machine
|
machineAction(deviceId:ID!, action: MachineAction!): Machine
|
||||||
machineSupportLogs(deviceId: ID!): SupportLogsResponse
|
machineSupportLogs(deviceId: ID!): SupportLogsResponse
|
||||||
serverSupportLogs: SupportLogsResponse
|
serverSupportLogs: SupportLogsResponse
|
||||||
|
setCustomer(customerId: ID!, customerInput: CustomerInput): Customer
|
||||||
saveConfig(config: JSONObject): JSONObject
|
saveConfig(config: JSONObject): JSONObject
|
||||||
createPairingTotem(name: String!): String
|
createPairingTotem(name: String!): String
|
||||||
saveAccount(account: JSONObject): [JSONObject]
|
saveAccount(account: JSONObject): [JSONObject]
|
||||||
|
|
@ -201,6 +232,9 @@ const resolvers = {
|
||||||
JSON: GraphQLJSON,
|
JSON: GraphQLJSON,
|
||||||
JSONObject: GraphQLJSONObject,
|
JSONObject: GraphQLJSONObject,
|
||||||
Date: GraphQLDateTime,
|
Date: GraphQLDateTime,
|
||||||
|
Customer: {
|
||||||
|
transactions: parent => transactions.getCustomerTransactions(parent.id)
|
||||||
|
},
|
||||||
Query: {
|
Query: {
|
||||||
countries: () => countries,
|
countries: () => countries,
|
||||||
currencies: () => currencies,
|
currencies: () => currencies,
|
||||||
|
|
@ -209,6 +243,7 @@ const resolvers = {
|
||||||
cryptoCurrencies: () => coins,
|
cryptoCurrencies: () => coins,
|
||||||
machines: () => machineLoader.getMachineNames(),
|
machines: () => machineLoader.getMachineNames(),
|
||||||
customers: () => customers.getCustomersList(),
|
customers: () => customers.getCustomersList(),
|
||||||
|
customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
|
||||||
funding: () => funding.getFunding(),
|
funding: () => funding.getFunding(),
|
||||||
machineLogs: (...[, { deviceId }]) => logs.simpleGetMachineLogs(deviceId),
|
machineLogs: (...[, { deviceId }]) => logs.simpleGetMachineLogs(deviceId),
|
||||||
serverVersion: () => serverVersion,
|
serverVersion: () => serverVersion,
|
||||||
|
|
@ -225,6 +260,7 @@ const resolvers = {
|
||||||
serverSupportLogs: () => serverLogs.insert(),
|
serverSupportLogs: () => serverLogs.insert(),
|
||||||
saveAccount: (...[, { account }]) => settingsLoader.saveAccounts([account]),
|
saveAccount: (...[, { account }]) => settingsLoader.saveAccounts([account]),
|
||||||
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
|
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
|
||||||
|
setCustomer: (...[, { customerId, customerInput } ]) => customers.updateCustomer(customerId, customerInput),
|
||||||
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
|
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
|
||||||
.then(it => {
|
.then(it => {
|
||||||
notify()
|
notify()
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,49 @@ function batch () {
|
||||||
.then(packager)
|
.then(packager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCustomerTransactions (customerId) {
|
||||||
|
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']),
|
||||||
|
_.take(NUM_RESULTS), _.map(camelize), addNames)
|
||||||
|
|
||||||
|
const cashInSql = `select 'cashIn' as tx_class, txs.*,
|
||||||
|
c.phone as customer_phone,
|
||||||
|
c.id_card_data_number as customer_id_card_data_number,
|
||||||
|
c.id_card_data_expiration as customer_id_card_data_expiration,
|
||||||
|
c.id_card_data as customer_id_card_data,
|
||||||
|
c.name as customer_name,
|
||||||
|
c.front_camera_path as customer_front_camera_path,
|
||||||
|
c.id_card_photo_path as customer_id_card_photo_path,
|
||||||
|
((not txs.send_confirmed) and (txs.created <= now() - interval $2)) as expired
|
||||||
|
from cash_in_txs as txs
|
||||||
|
left outer join customers c on txs.customer_id = c.id
|
||||||
|
where c.id = $1
|
||||||
|
order by created desc limit $3`
|
||||||
|
|
||||||
|
const cashOutSql = `select 'cashOut' as tx_class,
|
||||||
|
txs.*,
|
||||||
|
actions.tx_hash,
|
||||||
|
c.phone as customer_phone,
|
||||||
|
c.id_card_data_number as customer_id_card_data_number,
|
||||||
|
c.id_card_data_expiration as customer_id_card_data_expiration,
|
||||||
|
c.id_card_data as customer_id_card_data,
|
||||||
|
c.name as customer_name,
|
||||||
|
c.front_camera_path as customer_front_camera_path,
|
||||||
|
c.id_card_photo_path as customer_id_card_photo_path,
|
||||||
|
(extract(epoch from (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $3 as expired
|
||||||
|
from cash_out_txs txs
|
||||||
|
inner join cash_out_actions actions on txs.id = actions.tx_id
|
||||||
|
and actions.action = 'provisionAddress'
|
||||||
|
left outer join customers c on txs.customer_id = c.id
|
||||||
|
where c.id = $1
|
||||||
|
order by created desc limit $2`
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
db.any(cashInSql, [customerId, cashInTx.PENDING_INTERVAL, NUM_RESULTS]),
|
||||||
|
db.any(cashOutSql, [customerId, NUM_RESULTS, REDEEMABLE_AGE])
|
||||||
|
])
|
||||||
|
.then(packager)
|
||||||
|
}
|
||||||
|
|
||||||
function single (txId) {
|
function single (txId) {
|
||||||
const packager = _.flow(_.compact, _.map(camelize), addNames)
|
const packager = _.flow(_.compact, _.map(camelize), addNames)
|
||||||
|
|
||||||
|
|
@ -107,4 +150,4 @@ function cancel (txId) {
|
||||||
.then(() => single(txId))
|
.then(() => single(txId))
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { batch, single, cancel }
|
module.exports = { batch, getCustomerTransactions, single, cancel }
|
||||||
|
|
|
||||||
6
new-lamassu-admin/package-lock.json
generated
6
new-lamassu-admin/package-lock.json
generated
|
|
@ -15049,9 +15049,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"libphonenumber-js": {
|
"libphonenumber-js": {
|
||||||
"version": "1.7.49",
|
"version": "1.7.50",
|
||||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.7.49.tgz",
|
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.7.50.tgz",
|
||||||
"integrity": "sha512-AthHsii6+s+TBNMCUvKRzjscxMJAUD9rjDYZNj8rCVKBX9w1TzRbsmv+f4/pSuoHeKoNI64rcOV0Xb+7hoHudw==",
|
"integrity": "sha512-FmdA2WvwdTgu1X05zBnAE+3UAA09o3hFxEaqR0J+x7tGPAt1AD7Dj54L58PTJodrFBve/AIThFtC/UGqfSLbBw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"xml2js": "^0.4.17"
|
"xml2js": "^0.4.17"
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"fuse.js": "^3.4.6",
|
"fuse.js": "^3.4.6",
|
||||||
"graphql": "^14.5.8",
|
"graphql": "^14.5.8",
|
||||||
"jss-plugin-extend": "^10.0.0",
|
"jss-plugin-extend": "^10.0.0",
|
||||||
"libphonenumber-js": "^1.7.49",
|
"libphonenumber-js": "^1.7.50",
|
||||||
"moment": "2.24.0",
|
"moment": "2.24.0",
|
||||||
"qrcode.react": "0.9.3",
|
"qrcode.react": "0.9.3",
|
||||||
"ramda": "^0.26.1",
|
"ramda": "^0.26.1",
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,14 @@ const Tr = ({ error, errorMessage, children, className }) => {
|
||||||
const cardClasses = { root: classes.cardContentRoot }
|
const cardClasses = { root: classes.cardContentRoot }
|
||||||
const classNames = {
|
const classNames = {
|
||||||
[classes.tr]: true,
|
[classes.tr]: true,
|
||||||
[classes.trError]: error
|
[classes.trError]: error,
|
||||||
|
[classes.card]: true,
|
||||||
|
className
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className={classnames(classNames, classes.card, className)}>
|
<Card className={classnames(classNames, className)}>
|
||||||
<CardContent classes={cardClasses}>
|
<CardContent classes={cardClasses}>
|
||||||
<div className={classes.mainContent}>{children}</div>
|
<div className={classes.mainContent}>{children}</div>
|
||||||
{error && <div className={classes.errorContent}>{errorMessage}</div>}
|
{error && <div className={classes.errorContent}>{errorMessage}</div>}
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,15 @@ const Row = ({
|
||||||
expanded,
|
expanded,
|
||||||
expandRow,
|
expandRow,
|
||||||
expWidth,
|
expWidth,
|
||||||
expandable
|
expandable,
|
||||||
|
onClick
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
|
style={onClick && { cursor: 'pointer' }}
|
||||||
|
onClick={() => onClick && onClick(data)}>
|
||||||
<Tr
|
<Tr
|
||||||
className={classnames(classes.row)}
|
className={classnames(classes.row)}
|
||||||
error={data.error}
|
error={data.error}
|
||||||
|
|
@ -66,7 +69,7 @@ const Row = ({
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,6 +79,7 @@ const DataTable = ({
|
||||||
Details,
|
Details,
|
||||||
className,
|
className,
|
||||||
expandable,
|
expandable,
|
||||||
|
onClick,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [expanded, setExpanded] = useState(null)
|
const [expanded, setExpanded] = useState(null)
|
||||||
|
|
@ -114,6 +118,7 @@ const DataTable = ({
|
||||||
expanded={index === expanded}
|
expanded={index === expanded}
|
||||||
expandRow={expandRow}
|
expandRow={expandRow}
|
||||||
expandable={expandable}
|
expandable={expandable}
|
||||||
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CellMeasurer>
|
</CellMeasurer>
|
||||||
|
|
|
||||||
180
new-lamassu-admin/src/pages/Customers/CustomerProfile.js
Normal file
180
new-lamassu-admin/src/pages/Customers/CustomerProfile.js
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
|
import { gql } from 'apollo-boost'
|
||||||
|
import { useHistory, useParams } from 'react-router-dom'
|
||||||
|
import Breadcrumbs from '@material-ui/core/Breadcrumbs'
|
||||||
|
import NavigateNextIcon from '@material-ui/icons/NavigateNext'
|
||||||
|
|
||||||
|
import {
|
||||||
|
OVERRIDE_AUTHORIZED,
|
||||||
|
OVERRIDE_REJECTED
|
||||||
|
} from 'src/pages/Customers/components/propertyCard'
|
||||||
|
import { ActionButton } from 'src/components/buttons'
|
||||||
|
import { Label1 } from 'src/components/typography'
|
||||||
|
import { ReactComponent as BlockReversedIcon } from 'src/styling/icons/button/block/white.svg'
|
||||||
|
import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/zodiac.svg'
|
||||||
|
import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.svg'
|
||||||
|
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomerDetails,
|
||||||
|
IdDataCard,
|
||||||
|
PhoneCard,
|
||||||
|
IdCardPhotoCard,
|
||||||
|
TransactionsList
|
||||||
|
} from './components'
|
||||||
|
import { mainStyles } from './Customers.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
|
const GET_CUSTOMER = gql`
|
||||||
|
query customer($customerId: ID!) {
|
||||||
|
customer(customerId: $customerId) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
authorizedOverride
|
||||||
|
frontCameraPath
|
||||||
|
phone
|
||||||
|
smsOverride
|
||||||
|
idCardData
|
||||||
|
idCardDataOverride
|
||||||
|
idCardDataExpiration
|
||||||
|
idCardPhotoPath
|
||||||
|
idCardPhotoOverride
|
||||||
|
totalTxs
|
||||||
|
totalSpent
|
||||||
|
lastActive
|
||||||
|
lastTxFiat
|
||||||
|
lastTxFiatCode
|
||||||
|
lastTxClass
|
||||||
|
transactions {
|
||||||
|
txClass
|
||||||
|
id
|
||||||
|
fiat
|
||||||
|
fiatCode
|
||||||
|
cryptoAtoms
|
||||||
|
cryptoCode
|
||||||
|
created
|
||||||
|
errorMessage: error
|
||||||
|
error: errorCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SET_CUSTOMER = gql`
|
||||||
|
mutation setCustomer($customerId: ID!, $customerInput: CustomerInput) {
|
||||||
|
setCustomer(customerId: $customerId, customerInput: $customerInput) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
authorizedOverride
|
||||||
|
frontCameraPath
|
||||||
|
phone
|
||||||
|
smsOverride
|
||||||
|
idCardData
|
||||||
|
idCardDataOverride
|
||||||
|
idCardDataExpiration
|
||||||
|
idCardPhotoPath
|
||||||
|
idCardPhotoOverride
|
||||||
|
totalTxs
|
||||||
|
totalSpent
|
||||||
|
lastActive
|
||||||
|
lastTxFiat
|
||||||
|
lastTxFiatCode
|
||||||
|
lastTxClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const CustomerProfile = memo(() => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const history = useHistory()
|
||||||
|
const { id: customerId } = useParams()
|
||||||
|
|
||||||
|
const { data: customerResponse, refetch: getCustomer } = useQuery(
|
||||||
|
GET_CUSTOMER,
|
||||||
|
{
|
||||||
|
variables: { customerId },
|
||||||
|
fetchPolicy: 'no-cache'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const [setCustomer] = useMutation(SET_CUSTOMER, {
|
||||||
|
onCompleted: () => getCustomer()
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateCustomer = it =>
|
||||||
|
setCustomer({
|
||||||
|
variables: {
|
||||||
|
customerId,
|
||||||
|
customerInput: it
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const customerData = R.path(['customer'])(customerResponse) ?? []
|
||||||
|
|
||||||
|
const transactionsData = R.sortWith([R.descend('created')])(
|
||||||
|
R.path(['transactions'])(customerData) ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
const blocked =
|
||||||
|
R.path(['authorizedOverride'])(customerData) === OVERRIDE_REJECTED
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumbs
|
||||||
|
separator={<NavigateNextIcon fontSize="small" />}
|
||||||
|
aria-label="breadcrumb">
|
||||||
|
<Label1
|
||||||
|
className={classes.labelLink}
|
||||||
|
onClick={() => history.push('/compliance/customers')}>
|
||||||
|
Customers
|
||||||
|
</Label1>
|
||||||
|
<Label1 className={classes.bold}>
|
||||||
|
{R.path(['name'])(customerData)}
|
||||||
|
</Label1>
|
||||||
|
</Breadcrumbs>
|
||||||
|
<div>
|
||||||
|
<div className={classes.header}>
|
||||||
|
<CustomerDetails customer={customerData} />
|
||||||
|
<div className={classes.rightAligned}>
|
||||||
|
<Label1 className={classes.label1}>Actions</Label1>
|
||||||
|
<ActionButton
|
||||||
|
className={classes.actionButton}
|
||||||
|
color="primary"
|
||||||
|
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
||||||
|
InverseIcon={blocked ? AuthorizeReversedIcon : BlockReversedIcon}
|
||||||
|
onClick={() =>
|
||||||
|
updateCustomer({
|
||||||
|
authorizedOverride: blocked
|
||||||
|
? OVERRIDE_AUTHORIZED
|
||||||
|
: OVERRIDE_REJECTED
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
{`${blocked ? 'Authorize' : 'Block'} customer`}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.rowCenterAligned}>
|
||||||
|
<IdDataCard
|
||||||
|
customerData={customerData}
|
||||||
|
updateCustomer={updateCustomer}
|
||||||
|
/>
|
||||||
|
<PhoneCard
|
||||||
|
customerData={customerData}
|
||||||
|
updateCustomer={updateCustomer}
|
||||||
|
/>
|
||||||
|
<IdCardPhotoCard
|
||||||
|
customerData={customerData}
|
||||||
|
updateCustomer={updateCustomer}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TransactionsList data={transactionsData} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default CustomerProfile
|
||||||
|
|
@ -1,23 +1,15 @@
|
||||||
import { useQuery } from '@apollo/react-hooks'
|
import { useQuery } from '@apollo/react-hooks'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { gql } from 'apollo-boost'
|
import { gql } from 'apollo-boost'
|
||||||
import { parsePhoneNumberFromString } from 'libphonenumber-js'
|
import { useHistory } from 'react-router-dom'
|
||||||
import moment from 'moment'
|
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import Title from 'src/components/Title'
|
import CustomersList from './CustomersList'
|
||||||
import DataTable from 'src/components/tables/DataTable'
|
|
||||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
|
||||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
|
||||||
|
|
||||||
import { mainStyles } from './Customers.styles'
|
|
||||||
|
|
||||||
const useStyles = makeStyles(mainStyles)
|
|
||||||
|
|
||||||
const GET_CUSTOMERS = gql`
|
const GET_CUSTOMERS = gql`
|
||||||
{
|
{
|
||||||
customers {
|
customers {
|
||||||
|
id
|
||||||
name
|
name
|
||||||
phone
|
phone
|
||||||
totalTxs
|
totalTxs
|
||||||
|
|
@ -31,82 +23,17 @@ const GET_CUSTOMERS = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const Customers = () => {
|
const Customers = () => {
|
||||||
const classes = useStyles()
|
const history = useHistory()
|
||||||
|
|
||||||
const { data: customersResponse } = useQuery(GET_CUSTOMERS)
|
const { data: customersResponse } = useQuery(GET_CUSTOMERS)
|
||||||
|
|
||||||
const elements = [
|
const handleCustomerClicked = customer =>
|
||||||
{
|
history.push(`/compliance/customer/${customer.id}`)
|
||||||
header: 'Name',
|
|
||||||
width: 277,
|
|
||||||
view: R.path(['name'])
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Phone',
|
|
||||||
width: 166,
|
|
||||||
view: it => parsePhoneNumberFromString(it.phone)?.formatInternational()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Total TXs',
|
|
||||||
width: 174,
|
|
||||||
textAlign: 'right',
|
|
||||||
view: it => `${Number.parseInt(it.totalTxs)}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Total spent',
|
|
||||||
width: 188,
|
|
||||||
textAlign: 'right',
|
|
||||||
view: it =>
|
|
||||||
it.lastTxFiatCode
|
|
||||||
? `${Number.parseFloat(it.totalSpent)} ${it.lastTxFiatCode}`
|
|
||||||
: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Last active',
|
|
||||||
width: 197,
|
|
||||||
view: it =>
|
|
||||||
it.lastActive ? moment.utc(it.lastActive).format('YYYY-MM-D') : null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Last transaction',
|
|
||||||
width: 198,
|
|
||||||
textAlign: 'right',
|
|
||||||
view: it =>
|
|
||||||
it.lastTxFiatCode ? (
|
|
||||||
<div>
|
|
||||||
{`${Number.parseFloat(it.lastTxFiat)} ${it.lastTxFiatCode} `}
|
|
||||||
{it.lastTxClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />}
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
const customersData = R.sortWith([R.descend('lastActive')])(
|
||||||
<>
|
R.path(['customers'])(customersResponse) ?? []
|
||||||
<div className={classes.titleWrapper}>
|
|
||||||
<div className={classes.titleAndButtonsContainer}>
|
|
||||||
<Title>Customers</Title>
|
|
||||||
</div>
|
|
||||||
<div className={classes.headerLabels}>
|
|
||||||
<div>
|
|
||||||
<TxOutIcon />
|
|
||||||
<span>Cash-out</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TxInIcon />
|
|
||||||
<span>Cash-in</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DataTable
|
|
||||||
elements={elements}
|
|
||||||
data={R.sortWith([
|
|
||||||
R.ascend(R.prop('name')),
|
|
||||||
R.descend(R.prop('lastActive'))
|
|
||||||
])(R.path(['customers'])(customersResponse) ?? [])}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return <CustomersList data={customersData} onClick={handleCustomerClicked} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Customers
|
export default Customers
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,91 @@
|
||||||
import typographyStyles from 'src/components/typography/styles'
|
import typographyStyles from 'src/components/typography/styles'
|
||||||
import baseStyles from 'src/pages/Logs.styles'
|
import baseStyles from 'src/pages/Logs.styles'
|
||||||
|
import { zircon, primaryColor, comet } from 'src/styling/variables'
|
||||||
|
|
||||||
const { label1 } = typographyStyles
|
const { label1 } = typographyStyles
|
||||||
const { titleWrapper, titleAndButtonsContainer, buttonsWrapper } = baseStyles
|
const { titleWrapper, titleAndButtonsContainer } = baseStyles
|
||||||
|
|
||||||
const mainStyles = {
|
const mainStyles = {
|
||||||
|
rightAligned: {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'column nowrap',
|
||||||
|
right: 0
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
height: 28
|
||||||
|
},
|
||||||
titleWrapper,
|
titleWrapper,
|
||||||
titleAndButtonsContainer,
|
titleAndButtonsContainer,
|
||||||
buttonsWrapper,
|
header: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
customerDetails: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'row nowrap'
|
||||||
|
},
|
||||||
|
rowCenterAligned: {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'row nowrap',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
rowSpaceBetween: {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'row nowrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'column nowrap',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
width: 144
|
||||||
|
},
|
||||||
|
label1: {
|
||||||
|
fontFamily: 'MuseoSans',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontStretch: 'normal',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
lineHeight: 1.33,
|
||||||
|
letterSpacing: 'normal',
|
||||||
|
color: comet,
|
||||||
|
margin: [[4, 0]]
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
fontFamily: 'MuseoSans',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontStretch: 'normal',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
lineHeight: 1.14,
|
||||||
|
letterSpacing: 'normal',
|
||||||
|
color: primaryColor
|
||||||
|
},
|
||||||
|
bold: {
|
||||||
|
fontWeight: 'bold'
|
||||||
|
},
|
||||||
|
txId: {
|
||||||
|
fontFamily: 'MuseoSans',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis'
|
||||||
|
},
|
||||||
|
txClassIconLeft: {
|
||||||
|
marginRight: 11
|
||||||
|
},
|
||||||
|
txClassIconRight: {
|
||||||
|
marginLeft: 11
|
||||||
|
},
|
||||||
headerLabels: {
|
headerLabels: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -22,6 +100,62 @@ const mainStyles = {
|
||||||
extend: label1,
|
extend: label1,
|
||||||
marginLeft: 6
|
marginLeft: 6
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
photo: {
|
||||||
|
width: 92,
|
||||||
|
height: 92,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: zircon,
|
||||||
|
margin: [[15, 28, 0, 0]],
|
||||||
|
padding: [[30]],
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
idDataCard: {
|
||||||
|
width: 544,
|
||||||
|
height: 240
|
||||||
|
},
|
||||||
|
phoneCard: {
|
||||||
|
width: 253,
|
||||||
|
height: 240
|
||||||
|
},
|
||||||
|
idCardPhotoCard: {
|
||||||
|
width: 378,
|
||||||
|
height: 240,
|
||||||
|
margin: [[32, 0, 0, 0]]
|
||||||
|
},
|
||||||
|
labelLink: {
|
||||||
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
position: 'relative',
|
||||||
|
width: 144,
|
||||||
|
height: 46,
|
||||||
|
padding: [[0, 4, 4, 0]],
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
'& > p:first-child': {
|
||||||
|
height: 16,
|
||||||
|
lineHeight: '16px',
|
||||||
|
paddingLeft: 3,
|
||||||
|
margin: [[0, 0, 5, 0]]
|
||||||
|
},
|
||||||
|
'& > p:last-child': {
|
||||||
|
margin: 0,
|
||||||
|
paddingLeft: 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customerName: {
|
||||||
|
marginBottom: 32
|
||||||
|
},
|
||||||
|
fieldDisplay: {
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis'
|
||||||
|
},
|
||||||
|
idCardPhoto: {
|
||||||
|
maxWidth: 171,
|
||||||
|
maxHeight: 97
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
86
new-lamassu-admin/src/pages/Customers/CustomersList.js
Normal file
86
new-lamassu-admin/src/pages/Customers/CustomersList.js
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import { parsePhoneNumberFromString } from 'libphonenumber-js'
|
||||||
|
import moment from 'moment'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Title from 'src/components/Title'
|
||||||
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
|
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||||
|
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
|
|
||||||
|
import { mainStyles } from './Customers.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
|
const CustomersList = ({ data, onClick }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
header: 'Name',
|
||||||
|
width: 277,
|
||||||
|
view: R.path(['name'])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Phone',
|
||||||
|
width: 186,
|
||||||
|
view: it => parsePhoneNumberFromString(it.phone).formatInternational()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Total TXs',
|
||||||
|
width: 154,
|
||||||
|
textAlign: 'right',
|
||||||
|
view: it => `${Number.parseInt(it.totalTxs)}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Total spent',
|
||||||
|
width: 188,
|
||||||
|
textAlign: 'right',
|
||||||
|
view: it => `${Number.parseFloat(it.totalSpent)} ${it.lastTxFiatCode}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Last active',
|
||||||
|
width: 197,
|
||||||
|
view: it => moment.utc(it.lastActive).format('YYYY-MM-D')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Last transaction',
|
||||||
|
width: 198,
|
||||||
|
textAlign: 'right',
|
||||||
|
view: it => (
|
||||||
|
<>
|
||||||
|
{`${Number.parseFloat(it.lastTxFiat)} ${it.lastTxFiatCode}`}
|
||||||
|
{it.lastTxClass === 'cashOut' ? (
|
||||||
|
<TxOutIcon className={classes.txClassIconRight} />
|
||||||
|
) : (
|
||||||
|
<TxInIcon className={classes.txClassIconRight} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.titleWrapper}>
|
||||||
|
<div className={classes.titleAndButtonsContainer}>
|
||||||
|
<Title>Customers</Title>
|
||||||
|
</div>
|
||||||
|
<div className={classes.headerLabels}>
|
||||||
|
<div>
|
||||||
|
<TxOutIcon />
|
||||||
|
<span>Cash-out</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TxInIcon />
|
||||||
|
<span>Cash-in</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DataTable elements={elements} data={data} onClick={onClick} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomersList
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import moment from 'moment'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
|
import { H2 } from 'src/components/typography'
|
||||||
|
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||||
|
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
|
|
||||||
|
import { mainStyles } from '../Customers.styles'
|
||||||
|
import { ifNotNull } from '../../../utils/nullCheck'
|
||||||
|
|
||||||
|
import FrontCameraPhoto from './FrontCameraPhoto'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
|
const CustomerDetails = memo(({ customer }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
header: 'Transactions',
|
||||||
|
size: 127,
|
||||||
|
value: ifNotNull(
|
||||||
|
customer.totalTxs,
|
||||||
|
`${Number.parseInt(customer.totalTxs)}`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Transaction volume',
|
||||||
|
size: 167,
|
||||||
|
value: ifNotNull(
|
||||||
|
customer.totalSpent,
|
||||||
|
`${Number.parseFloat(customer.totalSpent)} ${customer.lastTxFiatCode}`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Last active',
|
||||||
|
size: 142,
|
||||||
|
value: ifNotNull(
|
||||||
|
customer.lastActive,
|
||||||
|
moment.utc(customer.lastActive).format('YYYY-MM-D')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Last transaction',
|
||||||
|
size: 198,
|
||||||
|
value: ifNotNull(
|
||||||
|
customer.lastTxFiat,
|
||||||
|
<>
|
||||||
|
{`${Number.parseFloat(customer.lastTxFiat)}
|
||||||
|
${customer.lastTxFiatCode}`}
|
||||||
|
{customer.lastTxClass === 'cashOut' ? (
|
||||||
|
<TxOutIcon className={classes.txClassIconRight} />
|
||||||
|
) : (
|
||||||
|
<TxInIcon className={classes.txClassIconRight} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.customerDetails}>
|
||||||
|
<FrontCameraPhoto
|
||||||
|
frontCameraPath={R.path(['frontCameraPath'])(customer)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className={classes.rowCenterAligned}>
|
||||||
|
<H2 className={classes.customerName}>{R.path(['name'])(customer)}</H2>
|
||||||
|
</div>
|
||||||
|
<div className={classes.rowCenterAligned}>
|
||||||
|
{elements.map(({ size, header }, idx) => (
|
||||||
|
<div key={idx} className={classes.label1} style={{ width: size }}>
|
||||||
|
{header}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={classes.rowCenterAligned}>
|
||||||
|
{elements.map(({ size, value }, idx) => (
|
||||||
|
<div key={idx} className={classes.p} style={{ width: size }}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default CustomerDetails
|
||||||
21
new-lamassu-admin/src/pages/Customers/components/Field.js
Normal file
21
new-lamassu-admin/src/pages/Customers/components/Field.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
|
import { Info3, Label1 } from 'src/components/typography'
|
||||||
|
|
||||||
|
import { mainStyles } from '../Customers.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
|
const Field = memo(({ label, display }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.field}>
|
||||||
|
<Label1>{label}</Label1>
|
||||||
|
<Info3 className={classes.fieldDisplay}>{display}</Info3>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Field
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
import { Paper } from '@material-ui/core'
|
||||||
|
|
||||||
|
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
|
||||||
|
|
||||||
|
import { mainStyles } from '../Customers.styles'
|
||||||
|
|
||||||
|
import { IMAGES_URI } from './variables'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
|
const FrontCameraPhoto = memo(({ frontCameraPath }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className={classes.photo} elevation={0}>
|
||||||
|
{frontCameraPath ? (
|
||||||
|
<img
|
||||||
|
src={`${IMAGES_URI}/front-camera-photo/${frontCameraPath}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CrossedCameraIcon />
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default FrontCameraPhoto
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import moment from 'moment'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
|
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
|
||||||
|
import {
|
||||||
|
PropertyCard,
|
||||||
|
OVERRIDE_AUTHORIZED,
|
||||||
|
OVERRIDE_REJECTED
|
||||||
|
} from 'src/pages/Customers/components/propertyCard'
|
||||||
|
|
||||||
|
import { mainStyles } from '../Customers.styles'
|
||||||
|
|
||||||
|
import Field from './Field'
|
||||||
|
import { IMAGES_URI } from './variables'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
|
const IdCardPhotoCard = memo(({ customerData, updateCustomer }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PropertyCard
|
||||||
|
className={classes.idCardPhotoCard}
|
||||||
|
title={'ID card photo'}
|
||||||
|
state={R.path(['idCardPhotoOverride'])(customerData)}
|
||||||
|
authorize={() =>
|
||||||
|
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED })
|
||||||
|
}
|
||||||
|
reject={() => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED })}>
|
||||||
|
<div className={classes.row}>
|
||||||
|
{customerData.idCardPhotoPath ? (
|
||||||
|
<img
|
||||||
|
className={classes.idCardPhoto}
|
||||||
|
src={`${IMAGES_URI}/id-card-photo/${R.path(['idCardPhotoPath'])(
|
||||||
|
customerData
|
||||||
|
)}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CrossedCameraIcon />
|
||||||
|
)}
|
||||||
|
<Field
|
||||||
|
label={'Expiration date'}
|
||||||
|
display={moment
|
||||||
|
.utc(R.path(['idCardDataExpiration'])(customerData))
|
||||||
|
.format('YYYY-MM-D')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PropertyCard>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default IdCardPhotoCard
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import moment from 'moment'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
PropertyCard,
|
||||||
|
OVERRIDE_AUTHORIZED,
|
||||||
|
OVERRIDE_REJECTED
|
||||||
|
} from 'src/pages/Customers/components/propertyCard'
|
||||||
|
|
||||||
|
import { mainStyles } from '../Customers.styles'
|
||||||
|
|
||||||
|
import Field from './Field'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
|
const IdDataCard = memo(({ customerData, updateCustomer }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PropertyCard
|
||||||
|
className={classes.idDataCard}
|
||||||
|
title={'ID data'}
|
||||||
|
state={R.path(['idCardDataOverride'])(customerData)}
|
||||||
|
authorize={() =>
|
||||||
|
updateCustomer({ idCardDataOverride: OVERRIDE_AUTHORIZED })
|
||||||
|
}
|
||||||
|
reject={() => updateCustomer({ idCardDataOverride: OVERRIDE_REJECTED })}>
|
||||||
|
<div className={classes.rowSpaceBetween}>
|
||||||
|
<div className={classes.column}>
|
||||||
|
<Field
|
||||||
|
label={'Name'}
|
||||||
|
display={`${R.path(['idCardData', 'firstName'])(
|
||||||
|
customerData
|
||||||
|
)} ${R.path(['idCardData', 'lastName'])(customerData)}`}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={'Gender'}
|
||||||
|
display={R.path(['idCardData', 'gender'])(customerData)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.column}>
|
||||||
|
<Field
|
||||||
|
label={'ID number'}
|
||||||
|
display={R.path(['idCardData', 'documentNumber'])(customerData)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={'Country'}
|
||||||
|
display={R.path(['idCardData', 'country'])(customerData)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.column}>
|
||||||
|
<Field
|
||||||
|
label={'Age'}
|
||||||
|
display={moment
|
||||||
|
.utc()
|
||||||
|
.diff(
|
||||||
|
moment
|
||||||
|
.utc(R.path(['idCardData', 'dateOfBirth'])(customerData))
|
||||||
|
.format('YYYY-MM-D'),
|
||||||
|
'years'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={'Expiration date'}
|
||||||
|
display={moment
|
||||||
|
.utc(R.path(['idCardData', 'expirationDate'])(customerData))
|
||||||
|
.format('YYYY-MM-D')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PropertyCard>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default IdDataCard
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
import { parsePhoneNumberFromString } from 'libphonenumber-js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
PropertyCard,
|
||||||
|
OVERRIDE_AUTHORIZED,
|
||||||
|
OVERRIDE_REJECTED
|
||||||
|
} from 'src/pages/Customers/components/propertyCard'
|
||||||
|
|
||||||
|
import { mainStyles } from '../Customers.styles'
|
||||||
|
|
||||||
|
import Field from './Field'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
|
const PhoneCard = memo(({ customerData, updateCustomer }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PropertyCard
|
||||||
|
className={classes.phoneCard}
|
||||||
|
title={'Phone'}
|
||||||
|
state={R.path(['smsOverride'])(customerData)}
|
||||||
|
authorize={() => updateCustomer({ smsOverride: OVERRIDE_AUTHORIZED })}
|
||||||
|
reject={() => updateCustomer({ smsOverride: OVERRIDE_REJECTED })}>
|
||||||
|
<Field
|
||||||
|
label={'Phone'}
|
||||||
|
display={
|
||||||
|
R.path(['phone'])(customerData)
|
||||||
|
? parsePhoneNumberFromString(
|
||||||
|
R.path(['phone'])(customerData)
|
||||||
|
).formatInternational()
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PropertyCard>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PhoneCard
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import moment from 'moment'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
|
import { H4, Label2 } from 'src/components/typography'
|
||||||
|
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||||
|
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
|
import { toUnit } from 'src/utils/coin'
|
||||||
|
|
||||||
|
import CopyToClipboard from '../../Transactions/CopyToClipboard'
|
||||||
|
import { mainStyles } from '../Customers.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
|
const TransactionsList = ({ data }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
header: 'Direction',
|
||||||
|
width: 207,
|
||||||
|
view: it => (
|
||||||
|
<>
|
||||||
|
{it.txClass === 'cashOut' ? (
|
||||||
|
<TxOutIcon className={classes.txClassIconLeft} />
|
||||||
|
) : (
|
||||||
|
<TxInIcon className={classes.txClassIconLeft} />
|
||||||
|
)}
|
||||||
|
{it.txClass === 'cashOut' ? 'Cach-out' : 'Cash-in'}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Transaction ID',
|
||||||
|
width: 414,
|
||||||
|
view: it => (
|
||||||
|
<CopyToClipboard className={classes.txId}>{it.id}</CopyToClipboard>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Cash',
|
||||||
|
width: 146,
|
||||||
|
view: it => (
|
||||||
|
<>
|
||||||
|
{`${Number.parseFloat(it.fiat)} `}
|
||||||
|
<Label2 inline>{it.fiatCode}</Label2>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Crypto',
|
||||||
|
width: 142,
|
||||||
|
view: it => (
|
||||||
|
<>
|
||||||
|
{`${toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode).toFormat(
|
||||||
|
5
|
||||||
|
)} `}
|
||||||
|
<Label2 inline>{it.cryptoCode}</Label2>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Date',
|
||||||
|
width: 157,
|
||||||
|
view: it => moment.utc(it.created).format('YYYY-MM-D')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Time (h:m:s)',
|
||||||
|
width: 134,
|
||||||
|
view: it => moment.utc(it.created).format('hh:mm:ss')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.titleWrapper}>
|
||||||
|
<div className={classes.titleAndButtonsContainer}>
|
||||||
|
<H4>All transactions from this customer</H4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DataTable elements={elements} data={data} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransactionsList
|
||||||
13
new-lamassu-admin/src/pages/Customers/components/index.js
Normal file
13
new-lamassu-admin/src/pages/Customers/components/index.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import CustomerDetails from './CustomerDetails'
|
||||||
|
import IdDataCard from './IdDataCard'
|
||||||
|
import IdCardPhotoCard from './IdCardPhotoCard'
|
||||||
|
import PhoneCard from './PhoneCard'
|
||||||
|
import TransactionsList from './TransactionsList'
|
||||||
|
|
||||||
|
export {
|
||||||
|
CustomerDetails,
|
||||||
|
IdDataCard,
|
||||||
|
IdCardPhotoCard,
|
||||||
|
PhoneCard,
|
||||||
|
TransactionsList
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
import { Paper } from '@material-ui/core'
|
||||||
|
|
||||||
|
import { ActionButton } from 'src/components/buttons'
|
||||||
|
import { H3 } from 'src/components/typography'
|
||||||
|
import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.svg'
|
||||||
|
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
|
||||||
|
import { ReactComponent as RejectReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
|
||||||
|
import { ReactComponent as RejectIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
|
||||||
|
|
||||||
|
import { propertyCardStyles } from './PropertyCard.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(propertyCardStyles)
|
||||||
|
|
||||||
|
const OVERRIDE_PENDING = 'automatic'
|
||||||
|
const OVERRIDE_AUTHORIZED = 'verified'
|
||||||
|
const OVERRIDE_REJECTED = 'blocked'
|
||||||
|
|
||||||
|
const PropertyCard = memo(
|
||||||
|
({ className, title, state, authorize, reject, children }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const propertyCardClassNames = {
|
||||||
|
[classes.propertyCard]: true,
|
||||||
|
[classes.propertyCardPending]: state === OVERRIDE_PENDING,
|
||||||
|
[classes.propertyCardRejected]: state === OVERRIDE_REJECTED,
|
||||||
|
[classes.propertyCardAccepted]: state === OVERRIDE_AUTHORIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
const label1ClassNames = {
|
||||||
|
[classes.label1]: true,
|
||||||
|
[classes.label1Pending]: state === OVERRIDE_PENDING,
|
||||||
|
[classes.label1Rejected]: state === OVERRIDE_REJECTED,
|
||||||
|
[classes.label1Accepted]: state === OVERRIDE_AUTHORIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthorizeButton = () => (
|
||||||
|
<ActionButton
|
||||||
|
className={classes.cardActionButton}
|
||||||
|
color="secondary"
|
||||||
|
Icon={AuthorizeIcon}
|
||||||
|
InverseIcon={AuthorizeReversedIcon}
|
||||||
|
onClick={() => authorize()}>
|
||||||
|
Authorize
|
||||||
|
</ActionButton>
|
||||||
|
)
|
||||||
|
|
||||||
|
const RejectButton = () => (
|
||||||
|
<ActionButton
|
||||||
|
className={classes.cardActionButton}
|
||||||
|
color="secondary"
|
||||||
|
Icon={RejectIcon}
|
||||||
|
InverseIcon={RejectReversedIcon}
|
||||||
|
onClick={() => reject()}>
|
||||||
|
Reject
|
||||||
|
</ActionButton>
|
||||||
|
)
|
||||||
|
|
||||||
|
const authorizedAsString =
|
||||||
|
state === OVERRIDE_PENDING
|
||||||
|
? 'Pending'
|
||||||
|
: state === OVERRIDE_REJECTED
|
||||||
|
? 'Rejected'
|
||||||
|
: 'Accepted'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
className={classnames(propertyCardClassNames, className)}
|
||||||
|
elevation={0}>
|
||||||
|
<div className={classes.rowSpaceBetween}>
|
||||||
|
<H3>{title}</H3>
|
||||||
|
<div className={classnames(label1ClassNames)}>
|
||||||
|
{authorizedAsString}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Paper className={classes.cardProperties} elevation={0}>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
<div className={classes.buttonsWrapper}>
|
||||||
|
{state !== OVERRIDE_AUTHORIZED && AuthorizeButton()}
|
||||||
|
{state !== OVERRIDE_REJECTED && RejectButton()}
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export {
|
||||||
|
PropertyCard,
|
||||||
|
OVERRIDE_PENDING,
|
||||||
|
OVERRIDE_AUTHORIZED,
|
||||||
|
OVERRIDE_REJECTED
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import {
|
||||||
|
white,
|
||||||
|
zircon,
|
||||||
|
mistyRose,
|
||||||
|
tomato,
|
||||||
|
spring3,
|
||||||
|
spring4,
|
||||||
|
comet
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
|
const propertyCardStyles = {
|
||||||
|
propertyCard: {
|
||||||
|
margin: [[32, 12, 0, 0]],
|
||||||
|
padding: [[0, 16]],
|
||||||
|
borderRadius: 8
|
||||||
|
},
|
||||||
|
propertyCardPending: {
|
||||||
|
backgroundColor: zircon
|
||||||
|
},
|
||||||
|
propertyCardRejected: {
|
||||||
|
backgroundColor: mistyRose
|
||||||
|
},
|
||||||
|
propertyCardAccepted: {
|
||||||
|
backgroundColor: spring3
|
||||||
|
},
|
||||||
|
label1: {
|
||||||
|
fontFamily: 'MuseoSans',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontStretch: 'normal',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
lineHeight: 1.33,
|
||||||
|
letterSpacing: 'normal',
|
||||||
|
color: comet,
|
||||||
|
margin: [[4, 0]]
|
||||||
|
},
|
||||||
|
label1Pending: {
|
||||||
|
color: comet
|
||||||
|
},
|
||||||
|
label1Rejected: {
|
||||||
|
color: tomato
|
||||||
|
},
|
||||||
|
label1Accepted: {
|
||||||
|
color: spring4
|
||||||
|
},
|
||||||
|
cardActionButton: {
|
||||||
|
height: 28,
|
||||||
|
marginLeft: 12
|
||||||
|
},
|
||||||
|
cardProperties: {
|
||||||
|
borderRadius: 8,
|
||||||
|
width: '100%',
|
||||||
|
height: 'calc(100% - 100px)',
|
||||||
|
padding: [[20]],
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
boxShadow: '0 0 8px 0 rgba(0, 0, 0, 0.04)',
|
||||||
|
border: 'solid 0',
|
||||||
|
backgroundColor: white
|
||||||
|
},
|
||||||
|
rowSpaceBetween: {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'row nowrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
buttonsWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginTop: 16,
|
||||||
|
marginBottom: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { propertyCardStyles }
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import {
|
||||||
|
PropertyCard,
|
||||||
|
OVERRIDE_PENDING,
|
||||||
|
OVERRIDE_AUTHORIZED,
|
||||||
|
OVERRIDE_REJECTED
|
||||||
|
} from './PropertyCard'
|
||||||
|
|
||||||
|
export {
|
||||||
|
PropertyCard,
|
||||||
|
OVERRIDE_PENDING,
|
||||||
|
OVERRIDE_AUTHORIZED,
|
||||||
|
OVERRIDE_REJECTED
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
const IMAGES_URI =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||||
|
|
||||||
|
export { IMAGES_URI }
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
import Customers from './Customers'
|
import Customers from './Customers'
|
||||||
|
import CustomerProfile from './CustomerProfile'
|
||||||
|
|
||||||
export default Customers
|
export { Customers, CustomerProfile }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Route, Redirect, Switch } from 'react-router-dom'
|
||||||
|
|
||||||
import AuthRegister from 'src/pages/AuthRegister'
|
import AuthRegister from 'src/pages/AuthRegister'
|
||||||
import Commissions from 'src/pages/Commissions'
|
import Commissions from 'src/pages/Commissions'
|
||||||
import Customers from 'src/pages/Customers'
|
import { Customers, CustomerProfile } from 'src/pages/Customers'
|
||||||
import Funding from 'src/pages/Funding'
|
import Funding from 'src/pages/Funding'
|
||||||
import Locales from 'src/pages/Locales'
|
import Locales from 'src/pages/Locales'
|
||||||
import MachineLogs from 'src/pages/MachineLogs'
|
import MachineLogs from 'src/pages/MachineLogs'
|
||||||
|
|
@ -124,6 +124,11 @@ const tree = [
|
||||||
label: 'Customers',
|
label: 'Customers',
|
||||||
route: '/compliance/customers',
|
route: '/compliance/customers',
|
||||||
component: Customers
|
component: Customers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'customer',
|
||||||
|
route: '/compliance/customer/:id',
|
||||||
|
component: CustomerProfile
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
|
||||||
|
<title>icon/crossed-camera</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<g id="icon/crossed-camera" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="crossed-camera">
|
||||||
|
<g id="Group-2">
|
||||||
|
<g id="icon/ID/cam/zodiac" transform="translate(0.000000, 3.657143)" fill="#1B2559" fill-rule="nonzero">
|
||||||
|
<path d="M16,19.1876029 C12.4482116,19.1876029 9.56809571,16.389776 9.56809571,12.9394673 C9.56809571,9.48915858 12.4482116,6.69133172 16,6.69133172 C19.5517884,6.69133172 22.4319043,9.48915858 22.4319043,12.9394673 C22.4319043,16.389776 19.5517884,19.1876029 16,19.1876029 Z M16,17.104891 C18.3677075,17.104891 20.2879362,15.239526 20.2879362,12.9394673 C20.2879362,10.6394086 18.3677075,8.77404358 16,8.77404358 C13.6322925,8.77404358 11.7120638,10.6394086 11.7120638,12.9394673 C11.7120638,15.239526 13.6322925,17.104891 16,17.104891 Z M22.7667469,3.30692494 L30.7397807,3.30692494 C31.3318211,3.30692494 31.8117647,3.77315587 31.8117647,4.34828087 L31.8117647,22.0513317 C31.8117647,22.6264567 31.3318211,23.0926877 30.7397807,23.0926877 L1.26021934,23.0926877 C0.6681789,23.0926877 0.188235294,22.6264567 0.188235294,22.0513317 L0.188235294,4.34828087 C0.188235294,3.77315587 0.6681789,3.30692494 1.26021934,3.30692494 L9.23325311,3.30692494 L12.0766705,0.494526627 C12.2782333,0.295162767 12.5538198,0.182857143 12.8414756,0.182857143 L19.1585244,0.182857143 C19.4461802,0.182857143 19.7217667,0.295162767 19.9233295,0.494526627 L22.7667469,3.30692494 Z M29.6677966,5.3896368 L22.3170489,5.3896368 C22.0293931,5.3896368 21.7538065,5.27733118 21.5522438,5.07796732 L18.7088264,2.26556901 L13.2911736,2.26556901 L10.4477562,5.07796732 C10.2461935,5.27733118 9.97060695,5.3896368 9.68295115,5.3896368 L2.33220339,5.3896368 L2.33220339,21.0099758 L29.6677966,21.0099758 L29.6677966,5.3896368 Z" id="Stroke-1"></path>
|
||||||
|
</g>
|
||||||
|
<line x1="32" y1="0" x2="0" y2="32" id="Line" stroke="#FF584A" stroke-width="2" stroke-linecap="square"></line>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -122,6 +122,7 @@ export {
|
||||||
comet,
|
comet,
|
||||||
spring2,
|
spring2,
|
||||||
spring3,
|
spring3,
|
||||||
|
spring4,
|
||||||
tomato,
|
tomato,
|
||||||
pumpkin,
|
pumpkin,
|
||||||
mistyRose,
|
mistyRose,
|
||||||
|
|
|
||||||
5
new-lamassu-admin/src/utils/nullCheck.js
Normal file
5
new-lamassu-admin/src/utils/nullCheck.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const ifNotNull = (value, valueIfNotNull) => {
|
||||||
|
return value === null ? '' : valueIfNotNull
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ifNotNull }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue