diff --git a/lib/customers.js b/lib/customers.js index ae76e032..2a1a6302 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -519,7 +519,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null) */ function getCustomerById (id) { const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',') - const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override, + const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override, phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration, id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at, sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat, @@ -528,7 +528,7 @@ function getCustomerById (id) { select c.id, c.authorized_override, greatest(0, date_part('day', c.suspended_until - now())) as days_suspended, c.suspended_until > now() as is_suspended, - c.front_camera_path, c.front_camera_override, + c.front_camera_path, c.front_camera_at, c.front_camera_override, c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration, c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions, c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created, @@ -654,6 +654,57 @@ function updateIdCardData (patch, id) { }) } +/** + * @param {String} imageData customer t&c photo data + * @returns {Promise} new patch to be applied + */ +function updateTxCustomerPhoto (imageData) { + return Promise.resolve(imageData) + .then(imageData => { + const newPatch = {} + const directory = `${operatorDataDir}/customersphotos` + + if (_.isEmpty(imageData)) { + return + } + + // decode the base64 string to binary data + const decodedImageData = Buffer.from(imageData, 'base64') + + // workout the image hash + // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 + const hash = crypto + .createHash('sha256') + .update(imageData) + .digest('hex') + + // workout the image folder + // i.e. 24/0e/85 + const rpath = _.join(path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash))))) + + // i.e. ..///customersphotos/24/0e/85 + const dirname = path.join(directory, rpath) + + // create the directory tree if needed + _.attempt(() => makeDir.sync(dirname)) + + // i.e. ..///customersphotos/24/0e/85/240e85ff2e4bb931f235985dd01....jpg + const filename = path.join(dirname, hash + '.jpg') + + // update db record patch + // i.e. { + // "idCustomerTxPhoto": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", + // "idCustomerTxPhotoAt": "now()" + // } + newPatch.txCustomerPhotoPath = path.join(rpath, hash + '.jpg') + newPatch.txCustomerPhotoAt = 'now()' + + // write image file + return writeFile(filename, decodedImageData) + .then(() => newPatch) + }) +} + function updateFrontCamera (id, patch) { return Promise.resolve(patch) .then(patch => { @@ -704,4 +755,4 @@ function updateFrontCamera (id, patch) { }) } -module.exports = { add, get, batch, getCustomersList, getCustomerById, getById, update, updateCustomer, updatePhotoCard, updateFrontCamera, updateIdCardData } +module.exports = { add, get, batch, getCustomersList, getCustomerById, getById, update, updateCustomer, updatePhotoCard, updateFrontCamera, updateIdCardData, updateTxCustomerPhoto } diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index c2538924..8ebb8faa 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -22,6 +22,7 @@ const { typeDefs, resolvers } = require('./graphql/schema') const devMode = require('minimist')(process.argv.slice(2)).dev const idPhotoCardBasedir = _.get('idPhotoCardDir', options) const frontCameraBasedir = _.get('frontCameraDir', options) +const operatorDataBasedir = _.get('operatorDataDir', options) const hostname = options.hostname if (!hostname) { @@ -87,6 +88,7 @@ app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3001' }) app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, { index: false })) app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false })) +app.use('/operator-data', serveStatic(operatorDataBasedir, { index: false })) // Everything not on graphql or api/register is redirected to the front-end app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html'))) diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js index 6b933489..c6f5f7a7 100644 --- a/lib/new-admin/graphql/types/customer.type.js +++ b/lib/new-admin/graphql/types/customer.type.js @@ -7,6 +7,7 @@ const typeDef = gql` daysSuspended: Int isSuspended: Boolean frontCameraPath: String + frontCameraAt: Date frontCameraOverride: String phone: String isAnonymous: Boolean diff --git a/lib/new-admin/graphql/types/transaction.type.js b/lib/new-admin/graphql/types/transaction.type.js index a50f0494..82d1c16c 100644 --- a/lib/new-admin/graphql/types/transaction.type.js +++ b/lib/new-admin/graphql/types/transaction.type.js @@ -43,6 +43,8 @@ const typeDef = gql` expired: Boolean machineName: String discount: Int + txCustomerPhotoPath: String + txCustomerPhotoAt: Date } type Filter { diff --git a/lib/new-admin/services/transactions.js b/lib/new-admin/services/transactions.js index 44de0ab2..dc8c78b3 100644 --- a/lib/new-admin/services/transactions.js +++ b/lib/new-admin/services/transactions.js @@ -49,6 +49,8 @@ function batch ( array_to_string(array[c.id_card_data::json->>'firstName', c.id_card_data::json->>'lastName'], ' ') AS customer_name, c.front_camera_path AS customer_front_camera_path, c.id_card_photo_path AS customer_id_card_photo_path, + txs.tx_customer_photo_at AS tx_customer_photo_at, + txs.tx_customer_photo_path AS tx_customer_photo_path, ((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs LEFT OUTER JOIN customers c ON txs.customer_id = c.id @@ -76,6 +78,8 @@ function batch ( array_to_string(array[c.id_card_data::json->>'firstName', c.id_card_data::json->>'lastName'], ' ') AS customer_name, c.front_camera_path AS customer_front_camera_path, c.id_card_photo_path AS customer_id_card_photo_path, + txs.tx_customer_photo_at AS tx_customer_photo_at, + txs.tx_customer_photo_path AS tx_customer_photo_path, (extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $1 AS expired FROM (SELECT *, ${CASH_OUT_TRANSACTION_STATES} AS txStatus FROM cash_out_txs) txs INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id @@ -270,4 +274,23 @@ function getTxAssociatedData (txId, txClass) { : db.manyOrNone(actionsSql, [txId]) } -module.exports = { batch, single, cancel, getCustomerTransactionsBatch, getTx, getTxAssociatedData } +function updateTxCustomerPhoto (customerId, txId, direction, data) { + const formattedData = _.mapKeys(_.snakeCase, data) + const cashInSql = 'UPDATE cash_in_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4' + + const cashOutSql = 'UPDATE cash_out_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4' + + return direction === 'cashIn' + ? db.oneOrNone(cashInSql, [formattedData.tx_customer_photo_at, formattedData.tx_customer_photo_path, customerId, txId]) + : db.oneOrNone(cashOutSql, [formattedData.tx_customer_photo_at, formattedData.tx_customer_photo_path, customerId, txId]) +} + +module.exports = { + batch, + single, + cancel, + getCustomerTransactionsBatch, + getTx, + getTxAssociatedData, + updateTxCustomerPhoto +} diff --git a/lib/routes/customerRoutes.js b/lib/routes/customerRoutes.js index 510a6882..714a29cd 100644 --- a/lib/routes/customerRoutes.js +++ b/lib/routes/customerRoutes.js @@ -7,6 +7,7 @@ const compliance = require('../compliance') const complianceTriggers = require('../compliance-triggers') const configManager = require('../new-config-manager') const customers = require('../customers') +const txs = require('../new-admin/services/transactions') const httpError = require('../route-helpers').httpError const notifier = require('../notifier') const respond = require('../respond') @@ -99,10 +100,27 @@ function triggerSuspend (req, res, next) { .catch(next) } +function updateTxCustomerPhoto (req, res, next) { + const customerId = req.params.id + const txId = req.params.txId + const tcPhotoData = req.body.tcPhotoData + const direction = req.body.direction + + Promise.all([customers.getById(customerId), txs.getTx(txId, direction)]) + .then(([customer, tx]) => { + if (!customer || !tx) return + return customers.updateTxCustomerPhoto(tcPhotoData) + .then(newPatch => txs.updateTxCustomerPhoto(customerId, txId, direction, newPatch)) + }) + .then(() => respond(req, res, {})) + .catch(next) +} + router.patch('/:id', updateCustomer) router.patch('/:id/sanctions', triggerSanctions) router.patch('/:id/block', triggerBlock) router.patch('/:id/suspend', triggerSuspend) router.patch('/:id/photos/idcarddata', updateIdCardData) +router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto) module.exports = router diff --git a/migrations/1627563019030-add-customer-tc-photo-path.js b/migrations/1627563019030-add-customer-tc-photo-path.js new file mode 100644 index 00000000..2815f8bd --- /dev/null +++ b/migrations/1627563019030-add-customer-tc-photo-path.js @@ -0,0 +1,13 @@ +const db = require('./db') + +exports.up = function (next) { + const sql = [ + 'ALTER TABLE cash_in_txs ADD COLUMN tx_customer_photo_at TIMESTAMPTZ, ADD COLUMN tx_customer_photo_path TEXT', + 'ALTER TABLE cash_out_txs ADD COLUMN tx_customer_photo_at TIMESTAMPTZ, ADD COLUMN tx_customer_photo_path TEXT' + ] + db.multi(sql, next) +} + +exports.down = function (next) { + next() +} diff --git a/new-lamassu-admin/package-lock.json b/new-lamassu-admin/package-lock.json index 92ddcad7..5dac80f0 100644 --- a/new-lamassu-admin/package-lock.json +++ b/new-lamassu-admin/package-lock.json @@ -6057,6 +6057,11 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -7192,6 +7197,31 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "auto-bind": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-2.1.1.tgz", + "integrity": "sha512-NUwV1i9D3vxxY1KnfZgSZ716d6ovY7o8LfOwLhGIPFBowIb6Ln6DBW64+jCqPzUznel2hRSkQnYQqvh7/ldw8A==", + "requires": { + "@types/react": "^16.8.12" + }, + "dependencies": { + "@types/react": { + "version": "16.14.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.11.tgz", + "integrity": "sha512-Don0MtsZZ3fjwTJ2BsoqkyOy7e176KplEAKOpr/4XDdzinlyJBn9yfsKn5mcSgn4kh1B22+3tBnzBC1z63ybtQ==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "csstype": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", + "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" + } + } + }, "autoprefixer": { "version": "9.8.6", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", @@ -21860,6 +21890,15 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-material-ui-carousel": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/react-material-ui-carousel/-/react-material-ui-carousel-2.2.7.tgz", + "integrity": "sha512-aO42C4oupmIxmJwYaTWrlWaXvVVspKcpEu/5efZ9slteATEsqqPtNAeVaE40Vimw2hZeIh2e8vpRwjq7fSsLxw==", + "requires": { + "auto-bind": "^2.1.1", + "react-swipeable": "^6.1.0" + } + }, "react-number-format": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-4.4.4.tgz", @@ -23328,6 +23367,11 @@ "throttle-debounce": "^2.1.0" } }, + "react-swipeable": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-6.1.2.tgz", + "integrity": "sha512-vfZtOZNivwd/aI+ZZH1Grx0eQBdbV1UI3pB9p65jbW5guHHdSIPpKsND6XmaiZXP5REOOc9Ckfr36ChswPqwsA==" + }, "react-syntax-highlighter": { "version": "12.2.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz", diff --git a/new-lamassu-admin/package.json b/new-lamassu-admin/package.json index c3910b71..041b402c 100644 --- a/new-lamassu-admin/package.json +++ b/new-lamassu-admin/package.json @@ -35,6 +35,7 @@ "react": "^16.12.0", "react-copy-to-clipboard": "^5.0.2", "react-dom": "^16.10.2", + "react-material-ui-carousel": "^2.2.7", "react-number-format": "^4.4.1", "react-otp-input": "^2.3.0", "react-router-dom": "5.1.2", diff --git a/new-lamassu-admin/src/components/Carousel.js b/new-lamassu-admin/src/components/Carousel.js new file mode 100644 index 00000000..f751cd1f --- /dev/null +++ b/new-lamassu-admin/src/components/Carousel.js @@ -0,0 +1,60 @@ +import { makeStyles } from '@material-ui/core/styles' +import React, { memo } from 'react' +import ReactCarousel from 'react-material-ui-carousel' + +import { ReactComponent as LeftArrow } from 'src/styling/icons/arrow/carousel-left-arrow.svg' +import { ReactComponent as RightArrow } from 'src/styling/icons/arrow/carousel-right-arrow.svg' +import { URI } from 'src/utils/apollo' + +const useStyles = makeStyles({ + imgWrapper: { + alignItems: 'center', + justifyContent: 'center', + display: 'flex', + width: 550 + }, + imgInner: { + objectFit: 'cover', + objectPosition: 'center', + width: 550, + marginBottom: 40 + } +}) + +export const Carousel = memo(({ photosData, slidePhoto }) => { + const classes = useStyles() + + return ( + <> + } + NextIcon={} + navButtonsProps={{ + style: { + backgroundColor: 'transparent', + borderRadius: 0, + width: 50, + color: 'transparent', + opacity: 1 + } + }} + autoPlay={false} + indicators={false} + navButtonsAlwaysVisible={true} + next={activeIndex => slidePhoto(activeIndex)} + prev={activeIndex => slidePhoto(activeIndex)}> + {photosData.map((item, i) => ( +
+
+ +
+
+ ))} +
+ + ) +}) diff --git a/new-lamassu-admin/src/components/InformativeDialog.js b/new-lamassu-admin/src/components/InformativeDialog.js new file mode 100644 index 00000000..2e9fddba --- /dev/null +++ b/new-lamassu-admin/src/components/InformativeDialog.js @@ -0,0 +1,64 @@ +import { Dialog, DialogContent, makeStyles } from '@material-ui/core' +import React, { memo } from 'react' + +import { IconButton } from 'src/components/buttons' +import { H1 } from 'src/components/typography' +import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' +import { spacer } from 'src/styling/variables' + +const useStyles = makeStyles({ + closeButton: { + display: 'flex', + padding: [[spacer * 2, spacer * 2, 0, spacer * 2]], + paddingRight: spacer * 1.5, + justifyContent: 'end' + }, + title: { + margin: [[0, spacer * 2, spacer, spacer * 2 + 4]] + } +}) + +export const DialogTitle = ({ children, onClose }) => { + const classes = useStyles() + return ( +
+ {children} + {onClose && ( + + + + )} +
+ ) +} + +export const InformativeDialog = memo( + ({ title = '', open, onDissmised, disabled = false, data, ...props }) => { + const classes = useStyles() + + const innerOnClose = () => { + onDissmised() + } + + return ( + +
+ + + +
+

{title}

+ {data} +
+ ) + } +) diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index 6649acd8..361ae26e 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -35,6 +35,7 @@ const GET_CUSTOMER = gql` id authorizedOverride frontCameraPath + frontCameraAt frontCameraOverride phone isAnonymous @@ -67,6 +68,8 @@ const GET_CUSTOMER = gql` created errorMessage: error error: errorCode + txCustomerPhotoAt + txCustomerPhotoPath } } } @@ -168,6 +171,7 @@ const CustomerProfile = memo(() => { justifyContent="space-between"> setShowCompliance(!showCompliance)} /> diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomerDetails.js b/new-lamassu-admin/src/pages/Customers/components/CustomerDetails.js index 8d215c21..27714ee9 100644 --- a/new-lamassu-admin/src/pages/Customers/components/CustomerDetails.js +++ b/new-lamassu-admin/src/pages/Customers/components/CustomerDetails.js @@ -11,86 +11,99 @@ import { ReactComponent as LawIcon } from 'src/styling/icons/circle buttons/law/ import mainStyles from '../CustomersList.styles' import { getFormattedPhone, getName } from '../helper' -import FrontCameraPhoto from './FrontCameraPhoto' +import PhotosCard from './PhotosCard' const useStyles = makeStyles(mainStyles) -const CustomerDetails = memo(({ customer, locale, setShowCompliance }) => { - const classes = useStyles() +const CustomerDetails = memo( + ({ txData, customer, locale, setShowCompliance }) => { + const classes = useStyles() - const idNumber = R.path(['idCardData', 'documentNumber'])(customer) - const usSsn = R.path(['usSsn'])(customer) + const idNumber = R.path(['idCardData', 'documentNumber'])(customer) + const usSsn = R.path(['usSsn'])(customer) - const elements = [ - { - header: 'Phone number', - size: 172, - value: getFormattedPhone(customer.phone, locale.country) - } - ] + const elements = [ + { + header: 'Phone number', + size: 172, + value: getFormattedPhone(customer.phone, locale.country) + } + ] - if (idNumber) - elements.push({ - header: 'ID number', - size: 172, - value: idNumber - }) + if (idNumber) + elements.push({ + header: 'ID number', + size: 172, + value: idNumber + }) - if (usSsn) - elements.push({ - header: 'US SSN', - size: 127, - value: usSsn - }) + if (usSsn) + elements.push({ + header: 'US SSN', + size: 127, + value: usSsn + }) - const name = getName(customer) + const name = getName(customer) - return ( - - - -
- -

- {name.length - ? name - : getFormattedPhone(R.path(['phone'])(customer), locale.country)} -

- - Compliance details - -
- - {elements.map(({ size, header }, idx) => ( - - {header} - - ))} - - - {elements.map(({ size, value }, idx) => ( -

- {value} -

- ))} + return ( + + + +
+ +

+ {name.length + ? name + : getFormattedPhone( + R.path(['phone'])(customer), + locale.country + )} +

+ + Compliance details + +
+ + {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/PhotosCard.js b/new-lamassu-admin/src/pages/Customers/components/PhotosCard.js new file mode 100644 index 00000000..97674ec8 --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/PhotosCard.js @@ -0,0 +1,137 @@ +import ButtonBase from '@material-ui/core/ButtonBase' +import Paper from '@material-ui/core/Card' +import { makeStyles } from '@material-ui/core/styles' +import * as R from 'ramda' +import React, { memo, useState } from 'react' + +import { Carousel } from 'src/components/Carousel' +import { InformativeDialog } from 'src/components/InformativeDialog' +import { Info2, Label1 } from 'src/components/typography' +import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg' +import { URI } from 'src/utils/apollo' + +import CopyToClipboard from '../../Transactions/CopyToClipboard' + +import styles from './PhotosCard.styles' + +const useStyles = makeStyles(styles) + +const Label = ({ children }) => { + const classes = useStyles() + return {children} +} + +const PhotosCard = memo(({ frontCameraData, txPhotosData }) => { + const classes = useStyles() + + const [photosDialog, setPhotosDialog] = useState(false) + + const mapKeys = pair => { + const [key, value] = pair + if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') { + return ['path', value] + } + if (key === 'txCustomerPhotoAt' || key === 'frontCameraAt') { + return ['date', value] + } + return pair + } + + const addPhotoDir = R.map(it => { + const hasFrontCameraData = R.has('id')(it) + return hasFrontCameraData + ? { ...it, photoDir: 'operator-data/customersphotos' } + : { ...it, photoDir: 'front-camera-photo' } + }) + + const standardizeKeys = R.map( + R.compose(R.fromPairs, R.map(mapKeys), R.toPairs) + ) + + const filterByPhotoAvailable = R.filter( + tx => !R.isNil(tx.date) && !R.isNil(tx.path) + ) + + const photosData = filterByPhotoAvailable( + addPhotoDir(standardizeKeys(R.append(frontCameraData, txPhotosData))) + ) + + const singlePhoto = R.head(photosData) + + return ( + <> + + { + setPhotosDialog(true) + }}> + {singlePhoto ? ( +
+ + +
+ {photosData.length} +
+
+
+ ) : ( + + )} +
+
+ } + onDissmised={() => { + setPhotosDialog(false) + }} + /> + + ) +}) + +export const PhotosCarousel = memo(({ photosData }) => { + const classes = useStyles() + const [currentIndex, setCurrentIndex] = useState(0) + + const isFaceCustomerPhoto = !R.has('id')(photosData[currentIndex]) + + const slidePhoto = index => setCurrentIndex(index) + + return ( + <> + + {!isFaceCustomerPhoto && ( +
+ + + {photosData && photosData[currentIndex]?.id} + +
+ )} +
+
+
+ +
{photosData && photosData[currentIndex]?.date}
+
+
+
+ +
+ {!isFaceCustomerPhoto ? 'Acceptance of T&C' : 'Compliance scan'} +
+
+
+ + ) +}) + +export default PhotosCard diff --git a/new-lamassu-admin/src/pages/Customers/components/PhotosCard.styles.js b/new-lamassu-admin/src/pages/Customers/components/PhotosCard.styles.js new file mode 100644 index 00000000..002c5bfb --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/PhotosCard.styles.js @@ -0,0 +1,83 @@ +import typographyStyles from 'src/components/typography/styles' +import { zircon, backgroundColor, offColor } from 'src/styling/variables' + +const { p } = typographyStyles + +export default { + photo: { + width: 135, + height: 135, + borderRadius: 8, + backgroundColor: zircon, + margin: [[0, 28, 0, 0]], + alignItems: 'center', + justifyContent: 'center', + display: 'flex' + }, + img: { + objectFit: 'cover', + objectPosition: 'center', + width: 135, + height: 135 + }, + container: { + position: 'relative', + '& > img': { + display: 'block' + }, + '& > circle': { + position: 'absolute', + top: '0', + right: '0', + marginRight: 5, + marginTop: 5 + } + }, + circle: { + background: backgroundColor, + borderRadius: '50%', + width: 25, + height: 25, + alignItems: 'center', + justifyContent: 'center', + display: 'flex' + }, + label: { + color: offColor, + margin: [[0, 0, 6, 0]] + }, + firstRow: { + padding: [[8]], + display: 'flex', + flexDirection: 'column' + }, + secondRow: { + extend: p, + display: 'flex', + padding: [[8]], + '& > div': { + display: 'flex', + flexDirection: 'column', + '& > div': { + width: 144, + height: 37, + marginBottom: 15, + marginRight: 55 + } + } + }, + imgWrapper: { + alignItems: 'center', + justifyContent: 'center', + display: 'flex', + width: 550, + height: 550 + }, + imgInner: { + objectFit: 'cover', + objectPosition: 'center', + width: 550, + height: 550, + marginBottom: 40 + } +} diff --git a/new-lamassu-admin/src/styling/icons/arrow/carousel-left-arrow.svg b/new-lamassu-admin/src/styling/icons/arrow/carousel-left-arrow.svg new file mode 100644 index 00000000..b4ccda85 --- /dev/null +++ b/new-lamassu-admin/src/styling/icons/arrow/carousel-left-arrow.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/new-lamassu-admin/src/styling/icons/arrow/carousel-right-arrow.svg b/new-lamassu-admin/src/styling/icons/arrow/carousel-right-arrow.svg new file mode 100644 index 00000000..2f2f4f2f --- /dev/null +++ b/new-lamassu-admin/src/styling/icons/arrow/carousel-right-arrow.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file