From c808ca3be9fcb494e3b2c4460a8b8510b672ec2f Mon Sep 17 00:00:00 2001 From: Liordino Neto Date: Thu, 6 Feb 2020 20:36:56 -0300 Subject: [PATCH] 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 --- lib/customers.js | 74 ++++++- lib/new-admin/admin-server.js | 7 + lib/new-admin/graphql/schema.js | 38 +++- lib/new-admin/transactions.js | 45 ++++- new-lamassu-admin/package-lock.json | 6 +- new-lamassu-admin/package.json | 2 +- .../src/components/fake-table/Table.js | 6 +- .../src/components/tables/DataTable.js | 11 +- .../src/pages/Customers/CustomerProfile.js | 180 ++++++++++++++++++ .../src/pages/Customers/Customers.js | 93 +-------- .../src/pages/Customers/Customers.styles.js | 138 +++++++++++++- .../src/pages/Customers/CustomersList.js | 86 +++++++++ .../Customers/components/CustomerDetails.js | 91 +++++++++ .../src/pages/Customers/components/Field.js | 21 ++ .../Customers/components/FrontCameraPhoto.js | 30 +++ .../Customers/components/IdCardPhotoCard.js | 55 ++++++ .../pages/Customers/components/IdDataCard.js | 77 ++++++++ .../pages/Customers/components/PhoneCard.js | 42 ++++ .../Customers/components/TransactionsList.js | 88 +++++++++ .../src/pages/Customers/components/index.js | 13 ++ .../components/propertyCard/PropertyCard.js | 95 +++++++++ .../propertyCard/PropertyCard.styles.js | 74 +++++++ .../components/propertyCard/index.js | 13 ++ .../pages/Customers/components/variables.js | 4 + .../src/pages/Customers/index.js | 3 +- new-lamassu-admin/src/routing/routes.js | 7 +- .../styling/icons/ID/photo/crossed-camera.svg | 16 ++ new-lamassu-admin/src/styling/variables.js | 1 + new-lamassu-admin/src/utils/nullCheck.js | 5 + 29 files changed, 1215 insertions(+), 106 deletions(-) create mode 100644 new-lamassu-admin/src/pages/Customers/CustomerProfile.js create mode 100644 new-lamassu-admin/src/pages/Customers/CustomersList.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/CustomerDetails.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/Field.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/FrontCameraPhoto.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/IdCardPhotoCard.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/IdDataCard.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/PhoneCard.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/TransactionsList.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/index.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.styles.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/propertyCard/index.js create mode 100644 new-lamassu-admin/src/pages/Customers/components/variables.js create mode 100644 new-lamassu-admin/src/styling/icons/ID/photo/crossed-camera.svg create mode 100644 new-lamassu-admin/src/utils/nullCheck.js diff --git a/lib/customers.js b/lib/customers.js index e9081686..fb5fa474 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -85,6 +85,30 @@ function update (id, data, userToken, txId) { .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 * @@ -377,21 +401,24 @@ function batch () { }, customers))) } +// TODO: getCustomersList and getCustomerById are very similar, so this should be refactored + /** * Query all customers, ordered by last activity * and with aggregate columns based on their * transactions * - * @returns {array} Array of customers with it's + * @returns {array} Array of customers with it's transactions aggregations */ function getCustomersList () { - const sql = `select name, phone, total_txs, total_spent, - coalesce(tx_created, customer_created) as last_active, - fiat as last_tx_fiat, fiat_code as last_tx_fiat_code, - tx_class as last_tx_class + 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.name, c.phone, c.created as customer_created, - t.tx_class, t.fiat, t.fiat_code, t.created as tx_created, + 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, 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 @@ -410,6 +437,37 @@ function getCustomersList () { }, 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 {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 } diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index 150c81dd..9c4e3751 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -2,10 +2,12 @@ const fs = require('fs') const path = require('path') const express = require('express') const https = require('https') +const serveStatic = require('serve-static') const cors = require('cors') const helmet = require('helmet') const cookieParser = require('cookie-parser') const { ApolloServer, AuthenticationError } = require('apollo-server-express') +const _ = require('lodash/fp') const T = require('../time') const options = require('../options') @@ -15,6 +17,8 @@ const { typeDefs, resolvers } = require('./graphql/schema') const devMode = require('minimist')(process.argv.slice(2)).dev const NEVER = new Date(Date.now() + 100 * T.years) +const idPhotoCardBasedir = _.get('idPhotoCardDir', options) +const frontCameraBasedir = _.get('frontCameraDir', options) const hostname = options.hostname if (!hostname) { @@ -55,6 +59,9 @@ apolloServer.applyMiddleware({ // cors on app for /api/register endpoint. 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) => { const otp = req.query.otp diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js index 24feee74..355d820d 100644 --- a/lib/new-admin/graphql/schema.js +++ b/lib/new-admin/graphql/schema.js @@ -61,8 +61,37 @@ const typeDefs = gql` } type Customer { - name: String + id: ID! + name: String! + authorizedOverride: String + frontCameraPath: 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 totalSpent: String lastActive: Date @@ -160,6 +189,7 @@ const typeDefs = gql` cryptoCurrencies: [CryptoCurrency] machines: [Machine] customers: [Customer] + customer(customerId: ID!): Customer machineLogs(deviceId: ID!): [MachineLog] funding: [CoinFunds] serverVersion: String! @@ -187,6 +217,7 @@ const typeDefs = gql` machineAction(deviceId:ID!, action: MachineAction!): Machine machineSupportLogs(deviceId: ID!): SupportLogsResponse serverSupportLogs: SupportLogsResponse + setCustomer(customerId: ID!, customerInput: CustomerInput): Customer saveConfig(config: JSONObject): JSONObject createPairingTotem(name: String!): String saveAccount(account: JSONObject): [JSONObject] @@ -201,6 +232,9 @@ const resolvers = { JSON: GraphQLJSON, JSONObject: GraphQLJSONObject, Date: GraphQLDateTime, + Customer: { + transactions: parent => transactions.getCustomerTransactions(parent.id) + }, Query: { countries: () => countries, currencies: () => currencies, @@ -209,6 +243,7 @@ const resolvers = { cryptoCurrencies: () => coins, machines: () => machineLoader.getMachineNames(), customers: () => customers.getCustomersList(), + customer: (...[, { customerId }]) => customers.getCustomerById(customerId), funding: () => funding.getFunding(), machineLogs: (...[, { deviceId }]) => logs.simpleGetMachineLogs(deviceId), serverVersion: () => serverVersion, @@ -225,6 +260,7 @@ const resolvers = { serverSupportLogs: () => serverLogs.insert(), saveAccount: (...[, { account }]) => settingsLoader.saveAccounts([account]), saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts), + setCustomer: (...[, { customerId, customerInput } ]) => customers.updateCustomer(customerId, customerInput), saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config) .then(it => { notify() diff --git a/lib/new-admin/transactions.js b/lib/new-admin/transactions.js index 2ad38e12..57131786 100644 --- a/lib/new-admin/transactions.js +++ b/lib/new-admin/transactions.js @@ -61,6 +61,49 @@ function batch () { .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) { const packager = _.flow(_.compact, _.map(camelize), addNames) @@ -107,4 +150,4 @@ function cancel (txId) { .then(() => single(txId)) } -module.exports = { batch, single, cancel } +module.exports = { batch, getCustomerTransactions, single, cancel } diff --git a/new-lamassu-admin/package-lock.json b/new-lamassu-admin/package-lock.json index 38c2ad52..483b4abf 100644 --- a/new-lamassu-admin/package-lock.json +++ b/new-lamassu-admin/package-lock.json @@ -15049,9 +15049,9 @@ } }, "libphonenumber-js": { - "version": "1.7.49", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.7.49.tgz", - "integrity": "sha512-AthHsii6+s+TBNMCUvKRzjscxMJAUD9rjDYZNj8rCVKBX9w1TzRbsmv+f4/pSuoHeKoNI64rcOV0Xb+7hoHudw==", + "version": "1.7.50", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.7.50.tgz", + "integrity": "sha512-FmdA2WvwdTgu1X05zBnAE+3UAA09o3hFxEaqR0J+x7tGPAt1AD7Dj54L58PTJodrFBve/AIThFtC/UGqfSLbBw==", "requires": { "minimist": "^1.2.5", "xml2js": "^0.4.17" diff --git a/new-lamassu-admin/package.json b/new-lamassu-admin/package.json index 687183bc..c0beaa39 100644 --- a/new-lamassu-admin/package.json +++ b/new-lamassu-admin/package.json @@ -18,7 +18,7 @@ "fuse.js": "^3.4.6", "graphql": "^14.5.8", "jss-plugin-extend": "^10.0.0", - "libphonenumber-js": "^1.7.49", + "libphonenumber-js": "^1.7.50", "moment": "2.24.0", "qrcode.react": "0.9.3", "ramda": "^0.26.1", diff --git a/new-lamassu-admin/src/components/fake-table/Table.js b/new-lamassu-admin/src/components/fake-table/Table.js index 5f3cdd90..b5a84760 100644 --- a/new-lamassu-admin/src/components/fake-table/Table.js +++ b/new-lamassu-admin/src/components/fake-table/Table.js @@ -91,12 +91,14 @@ const Tr = ({ error, errorMessage, children, className }) => { const cardClasses = { root: classes.cardContentRoot } const classNames = { [classes.tr]: true, - [classes.trError]: error + [classes.trError]: error, + [classes.card]: true, + className } return ( <> - +
{children}
{error &&
{errorMessage}
} diff --git a/new-lamassu-admin/src/components/tables/DataTable.js b/new-lamassu-admin/src/components/tables/DataTable.js index e1608f34..dcb8f91b 100644 --- a/new-lamassu-admin/src/components/tables/DataTable.js +++ b/new-lamassu-admin/src/components/tables/DataTable.js @@ -33,12 +33,15 @@ const Row = ({ expanded, expandRow, expWidth, - expandable + expandable, + onClick }) => { const classes = useStyles() return ( - <> +
onClick && onClick(data)}> )} - +
) } @@ -76,6 +79,7 @@ const DataTable = ({ Details, className, expandable, + onClick, ...props }) => { const [expanded, setExpanded] = useState(null) @@ -114,6 +118,7 @@ const DataTable = ({ expanded={index === expanded} expandRow={expandRow} expandable={expandable} + onClick={onClick} /> diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js new file mode 100644 index 00000000..7bec278b --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -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 ( + <> + } + aria-label="breadcrumb"> + history.push('/compliance/customers')}> + Customers + + + {R.path(['name'])(customerData)} + + +
+
+ +
+ Actions + + updateCustomer({ + authorizedOverride: blocked + ? OVERRIDE_AUTHORIZED + : OVERRIDE_REJECTED + }) + }> + {`${blocked ? 'Authorize' : 'Block'} customer`} + +
+
+
+ + + +
+
+ + + ) +}) + +export default CustomerProfile diff --git a/new-lamassu-admin/src/pages/Customers/Customers.js b/new-lamassu-admin/src/pages/Customers/Customers.js index e629eb1d..9f421323 100644 --- a/new-lamassu-admin/src/pages/Customers/Customers.js +++ b/new-lamassu-admin/src/pages/Customers/Customers.js @@ -1,23 +1,15 @@ import { useQuery } from '@apollo/react-hooks' -import { makeStyles } from '@material-ui/core/styles' import { gql } from 'apollo-boost' -import { parsePhoneNumberFromString } from 'libphonenumber-js' -import moment from 'moment' +import { useHistory } from 'react-router-dom' 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) +import CustomersList from './CustomersList' const GET_CUSTOMERS = gql` { customers { + id name phone totalTxs @@ -31,82 +23,17 @@ const GET_CUSTOMERS = gql` ` const Customers = () => { - const classes = useStyles() - + const history = useHistory() const { data: customersResponse } = useQuery(GET_CUSTOMERS) - const elements = [ - { - 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 ? ( -
- {`${Number.parseFloat(it.lastTxFiat)} ${it.lastTxFiatCode} `} - {it.lastTxClass === 'cashOut' ? : } -
- ) : null - } - ] + const handleCustomerClicked = customer => + history.push(`/compliance/customer/${customer.id}`) - return ( - <> -
-
- Customers -
-
-
- - Cash-out -
-
- - Cash-in -
-
-
- - + const customersData = R.sortWith([R.descend('lastActive')])( + R.path(['customers'])(customersResponse) ?? [] ) + + return } export default Customers diff --git a/new-lamassu-admin/src/pages/Customers/Customers.styles.js b/new-lamassu-admin/src/pages/Customers/Customers.styles.js index 48db31ee..2f8bc796 100644 --- a/new-lamassu-admin/src/pages/Customers/Customers.styles.js +++ b/new-lamassu-admin/src/pages/Customers/Customers.styles.js @@ -1,13 +1,91 @@ import typographyStyles from 'src/components/typography/styles' import baseStyles from 'src/pages/Logs.styles' +import { zircon, primaryColor, comet } from 'src/styling/variables' const { label1 } = typographyStyles -const { titleWrapper, titleAndButtonsContainer, buttonsWrapper } = baseStyles +const { titleWrapper, titleAndButtonsContainer } = baseStyles const mainStyles = { + rightAligned: { + display: 'flex', + flexFlow: 'column nowrap', + right: 0 + }, + actionButton: { + height: 28 + }, titleWrapper, 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: { display: 'flex', flexDirection: 'row', @@ -22,6 +100,62 @@ const mainStyles = { extend: label1, 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 } } diff --git a/new-lamassu-admin/src/pages/Customers/CustomersList.js b/new-lamassu-admin/src/pages/Customers/CustomersList.js new file mode 100644 index 00000000..97d80980 --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/CustomersList.js @@ -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' ? ( + + ) : ( + + )} + + ) + } + ] + + return ( + <> +
+
+ Customers +
+
+
+ + Cash-out +
+
+ + Cash-in +
+
+
+ + + ) +} + +export default CustomersList diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomerDetails.js b/new-lamassu-admin/src/pages/Customers/components/CustomerDetails.js new file mode 100644 index 00000000..8e1fad4a --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/CustomerDetails.js @@ -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' ? ( + + ) : ( + + )} + + ) + } + ] + + return ( +
+ +
+
+

{R.path(['name'])(customer)}

+
+
+ {elements.map(({ size, header }, idx) => ( +
+ {header} +
+ ))} +
+
+ {elements.map(({ size, value }, idx) => ( +
+ {value} +
+ ))} +
+
+
+ ) +}) + +export default CustomerDetails diff --git a/new-lamassu-admin/src/pages/Customers/components/Field.js b/new-lamassu-admin/src/pages/Customers/components/Field.js new file mode 100644 index 00000000..1ef4191b --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/Field.js @@ -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 ( +
+ {label} + {display} +
+ ) +}) + +export default Field diff --git a/new-lamassu-admin/src/pages/Customers/components/FrontCameraPhoto.js b/new-lamassu-admin/src/pages/Customers/components/FrontCameraPhoto.js new file mode 100644 index 00000000..1af1f31e --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/FrontCameraPhoto.js @@ -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 ( + + {frontCameraPath ? ( + + ) : ( + + )} + + ) +}) + +export default FrontCameraPhoto diff --git a/new-lamassu-admin/src/pages/Customers/components/IdCardPhotoCard.js b/new-lamassu-admin/src/pages/Customers/components/IdCardPhotoCard.js new file mode 100644 index 00000000..db7db6eb --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/IdCardPhotoCard.js @@ -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 ( + + updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED }) + } + reject={() => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED })}> +
+ {customerData.idCardPhotoPath ? ( + + ) : ( + + )} + +
+
+ ) +}) + +export default IdCardPhotoCard diff --git a/new-lamassu-admin/src/pages/Customers/components/IdDataCard.js b/new-lamassu-admin/src/pages/Customers/components/IdDataCard.js new file mode 100644 index 00000000..7a0ead02 --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/IdDataCard.js @@ -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 ( + + updateCustomer({ idCardDataOverride: OVERRIDE_AUTHORIZED }) + } + reject={() => updateCustomer({ idCardDataOverride: OVERRIDE_REJECTED })}> +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ) +}) + +export default IdDataCard diff --git a/new-lamassu-admin/src/pages/Customers/components/PhoneCard.js b/new-lamassu-admin/src/pages/Customers/components/PhoneCard.js new file mode 100644 index 00000000..e75cf196 --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/PhoneCard.js @@ -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 ( + updateCustomer({ smsOverride: OVERRIDE_AUTHORIZED })} + reject={() => updateCustomer({ smsOverride: OVERRIDE_REJECTED })}> + + + ) +}) + +export default PhoneCard diff --git a/new-lamassu-admin/src/pages/Customers/components/TransactionsList.js b/new-lamassu-admin/src/pages/Customers/components/TransactionsList.js new file mode 100644 index 00000000..63c21279 --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/TransactionsList.js @@ -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' ? ( + + ) : ( + + )} + {it.txClass === 'cashOut' ? 'Cach-out' : 'Cash-in'} + + ) + }, + { + header: 'Transaction ID', + width: 414, + view: it => ( + {it.id} + ) + }, + { + header: 'Cash', + width: 146, + view: it => ( + <> + {`${Number.parseFloat(it.fiat)} `} + {it.fiatCode} + + ) + }, + { + header: 'Crypto', + width: 142, + view: it => ( + <> + {`${toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode).toFormat( + 5 + )} `} + {it.cryptoCode} + + ) + }, + { + 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 ( + <> +
+
+

All transactions from this customer

+
+
+ + + ) +} + +export default TransactionsList diff --git a/new-lamassu-admin/src/pages/Customers/components/index.js b/new-lamassu-admin/src/pages/Customers/components/index.js new file mode 100644 index 00000000..9b8f8946 --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/index.js @@ -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 +} diff --git a/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.js b/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.js new file mode 100644 index 00000000..b0601e56 --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.js @@ -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 = () => ( + authorize()}> + Authorize + + ) + + const RejectButton = () => ( + reject()}> + Reject + + ) + + const authorizedAsString = + state === OVERRIDE_PENDING + ? 'Pending' + : state === OVERRIDE_REJECTED + ? 'Rejected' + : 'Accepted' + + return ( + +
+

{title}

+
+ {authorizedAsString} +
+
+ + {children} + +
+ {state !== OVERRIDE_AUTHORIZED && AuthorizeButton()} + {state !== OVERRIDE_REJECTED && RejectButton()} +
+
+ ) + } +) + +export { + PropertyCard, + OVERRIDE_PENDING, + OVERRIDE_AUTHORIZED, + OVERRIDE_REJECTED +} diff --git a/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.styles.js b/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.styles.js new file mode 100644 index 00000000..e46c12e2 --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.styles.js @@ -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 } diff --git a/new-lamassu-admin/src/pages/Customers/components/propertyCard/index.js b/new-lamassu-admin/src/pages/Customers/components/propertyCard/index.js new file mode 100644 index 00000000..3002e26a --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/propertyCard/index.js @@ -0,0 +1,13 @@ +import { + PropertyCard, + OVERRIDE_PENDING, + OVERRIDE_AUTHORIZED, + OVERRIDE_REJECTED +} from './PropertyCard' + +export { + PropertyCard, + OVERRIDE_PENDING, + OVERRIDE_AUTHORIZED, + OVERRIDE_REJECTED +} diff --git a/new-lamassu-admin/src/pages/Customers/components/variables.js b/new-lamassu-admin/src/pages/Customers/components/variables.js new file mode 100644 index 00000000..f591c38a --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/variables.js @@ -0,0 +1,4 @@ +const IMAGES_URI = + process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : '' + +export { IMAGES_URI } diff --git a/new-lamassu-admin/src/pages/Customers/index.js b/new-lamassu-admin/src/pages/Customers/index.js index 950b526a..069aafbb 100644 --- a/new-lamassu-admin/src/pages/Customers/index.js +++ b/new-lamassu-admin/src/pages/Customers/index.js @@ -1,3 +1,4 @@ import Customers from './Customers' +import CustomerProfile from './CustomerProfile' -export default Customers +export { Customers, CustomerProfile } diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index 6ee06e6d..1acd9be0 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -4,7 +4,7 @@ import { Route, Redirect, Switch } from 'react-router-dom' import AuthRegister from 'src/pages/AuthRegister' 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 Locales from 'src/pages/Locales' import MachineLogs from 'src/pages/MachineLogs' @@ -124,6 +124,11 @@ const tree = [ label: 'Customers', route: '/compliance/customers', component: Customers + }, + { + key: 'customer', + route: '/compliance/customer/:id', + component: CustomerProfile } ] } diff --git a/new-lamassu-admin/src/styling/icons/ID/photo/crossed-camera.svg b/new-lamassu-admin/src/styling/icons/ID/photo/crossed-camera.svg new file mode 100644 index 00000000..4a51ad4b --- /dev/null +++ b/new-lamassu-admin/src/styling/icons/ID/photo/crossed-camera.svg @@ -0,0 +1,16 @@ + + + + icon/crossed-camera + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/new-lamassu-admin/src/styling/variables.js b/new-lamassu-admin/src/styling/variables.js index 249a7976..4446657f 100644 --- a/new-lamassu-admin/src/styling/variables.js +++ b/new-lamassu-admin/src/styling/variables.js @@ -122,6 +122,7 @@ export { comet, spring2, spring3, + spring4, tomato, pumpkin, mistyRose, diff --git a/new-lamassu-admin/src/utils/nullCheck.js b/new-lamassu-admin/src/utils/nullCheck.js new file mode 100644 index 00000000..f0780779 --- /dev/null +++ b/new-lamassu-admin/src/utils/nullCheck.js @@ -0,0 +1,5 @@ +const ifNotNull = (value, valueIfNotNull) => { + return value === null ? '' : valueIfNotNull +} + +export { ifNotNull }