feat: add photos list and carousel

This commit is contained in:
José Oliveira 2021-12-13 18:20:39 +00:00
parent 9553bf8fc9
commit c93e1028a4
14 changed files with 342 additions and 202 deletions

View file

@ -683,18 +683,18 @@ 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,
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,
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_at, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_at, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, created AS last_active, fiat AS last_tx_fiat,
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, notes
FROM (
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.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.front_camera_path, c.front_camera_override, c.front_camera_at,
c.phone, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions,
c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created, cn.notes,
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,

View file

@ -0,0 +1,68 @@
import { makeStyles, Paper } from '@material-ui/core'
import { format } from 'date-fns/fp'
import * as R from 'ramda'
import { React, useState } from 'react'
import { InformativeDialog } from 'src/components/InformativeDialog'
import { Label2, H3 } from 'src/components/typography'
import { ReactComponent as CameraIcon } from 'src/styling/icons/ID/photo/comet.svg'
import { URI } from 'src/utils/apollo'
import styles from './CustomerPhotos.styles'
import PhotosCarousel from './components/PhotosCarousel'
const useStyles = makeStyles(styles)
const CustomerPhotos = ({ photosData, set }) => {
const classes = useStyles()
const [photosDialog, setPhotosDialog] = useState(false)
return (
<div>
<div className={classes.header}>
<H3 className={classes.title}>{'Photos & files'}</H3>
</div>
<div className={classes.photosChipList}>
{R.map(
it => (
<PhotoCard
date={it.date}
src={`${URI}/${it.photoDir}/${it.path}`}
setPhotosDialog={setPhotosDialog}
/>
),
photosData
)}
</div>
<InformativeDialog
open={photosDialog}
title={`Photo roll`}
data={<PhotosCarousel photosData={photosData} />}
onDissmised={() => {
setPhotosDialog(false)
}}
/>
</div>
)
}
export const PhotoCard = ({ date, src, setPhotosDialog }) => {
const classes = useStyles()
return (
<Paper
className={classes.photoCardChip}
onClick={() => setPhotosDialog(true)}>
<img className={classes.image} src={src} alt="" />
<div className={classes.footer}>
<CameraIcon />
<Label2 className={classes.date}>
{format('yyyy-MM-dd', new Date(date))}
</Label2>
</div>
</Paper>
)
}
export default CustomerPhotos

View file

@ -0,0 +1,35 @@
const styles = {
header: {
display: 'flex',
flexDirection: 'row'
},
title: {
marginTop: 7,
marginRight: 24,
marginBottom: 32
},
photosChipList: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap'
},
image: {
objectFit: 'cover',
objectPosition: 'center',
width: 224,
height: 200
},
photoCardChip: {
margin: [[0, 16, 0, 0]]
},
footer: {
display: 'flex',
flexDirection: 'row',
margin: [[8, 0, 0, 8]]
},
date: {
margin: [[0, 0, 8, 12]]
}
}
export default styles

View file

@ -24,6 +24,7 @@ import { fromNamespace, namespaces } from 'src/utils/config'
import CustomerData from './CustomerData'
import CustomerNotes from './CustomerNotes'
import CustomerPhotos from './CustomerPhotos'
import styles from './CustomerProfile.styles'
import {
CustomerDetails,
@ -31,7 +32,7 @@ import {
CustomerSidebar,
Wizard
} from './components'
import { getFormattedPhone, getName } from './helper'
import { getFormattedPhone, getName, formatPhotosData } from './helper'
const useStyles = makeStyles(styles)
@ -367,6 +368,18 @@ const CustomerProfile = memo(() => {
const isCustomerData = clickedItem === 'customerData'
const isOverview = clickedItem === 'overview'
const isNotes = clickedItem === 'notes'
const isPhotos = clickedItem === 'photos'
const frontCameraData = R.pick(['frontCameraPath', 'frontCameraAt'])(
customerData
)
const txPhotosData =
sortedTransactions &&
R.map(R.pick(['id', 'txCustomerPhotoPath', 'txCustomerPhotoAt']))(
sortedTransactions
)
const photosData = formatPhotosData(R.append(frontCameraData, txPhotosData))
const loading = customerLoading && configLoading
@ -488,6 +501,7 @@ const CustomerProfile = memo(() => {
justifyContent="space-between">
<CustomerDetails
customer={customerData}
photosData={photosData}
locale={locale}
setShowCompliance={() => setShowCompliance(!showCompliance)}
/>
@ -524,6 +538,11 @@ const CustomerProfile = memo(() => {
timezone={timezone}></CustomerNotes>
</div>
)}
{isPhotos && (
<div>
<CustomerPhotos photosData={photosData} />
</div>
)}
</div>
{wizard && (
<Wizard

View file

@ -12,88 +12,73 @@ import PhotosCard from './PhotosCard'
const useStyles = makeStyles(mainStyles)
const CustomerDetails = memo(
({ txData, customer, locale, setShowCompliance }) => {
const classes = useStyles()
const CustomerDetails = memo(({ customer, photosData, locale }) => {
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 (
<Box display="flex">
<PhotosCard
frontCameraData={R.pick(['frontCameraPath', 'frontCameraAt'])(
customer
)}
txPhotosData={
txData &&
R.map(R.pick(['id', 'txCustomerPhotoPath', 'txCustomerPhotoAt']))(
txData
)
}
/>
<Box display="flex" flexDirection="column">
<div className={classes.name}>
<IdIcon className={classes.idIcon} />
<H2 noMargin>
{name.length
? name
: getFormattedPhone(
R.path(['phone'])(customer),
locale.country
)}
</H2>
</div>
<Box display="flex" mt="auto">
{elements.map(({ size, header }, idx) => (
<Label1
noMargin
key={idx}
className={classes.label}
style={{ width: size }}>
{header}
</Label1>
))}
</Box>
<Box display="flex">
{elements.map(({ size, value }, idx) => (
<P
noMargin
key={idx}
className={classes.value}
style={{ width: size }}>
{value}
</P>
))}
</Box>
return (
<Box display="flex">
<PhotosCard photosData={photosData} />
<Box display="flex" flexDirection="column">
<div className={classes.name}>
<IdIcon className={classes.idIcon} />
<H2 noMargin>
{name.length
? name
: getFormattedPhone(R.path(['phone'])(customer), locale.country)}
</H2>
</div>
<Box display="flex" mt="auto">
{elements.map(({ size, header }, idx) => (
<Label1
noMargin
key={idx}
className={classes.label}
style={{ width: size }}>
{header}
</Label1>
))}
</Box>
<Box display="flex">
{elements.map(({ size, value }, idx) => (
<P
noMargin
key={idx}
className={classes.value}
style={{ width: size }}>
{value}
</P>
))}
</Box>
</Box>
)
}
)
</Box>
)
})
export default CustomerDetails

View file

@ -8,6 +8,8 @@ import { ReactComponent as NoteReversedIcon } from 'src/styling/icons/customer-n
import { ReactComponent as NoteIcon } from 'src/styling/icons/customer-nav/note/white.svg'
import { ReactComponent as OverviewReversedIcon } from 'src/styling/icons/customer-nav/overview/comet.svg'
import { ReactComponent as OverviewIcon } from 'src/styling/icons/customer-nav/overview/white.svg'
import { ReactComponent as PhotosReversedIcon } from 'src/styling/icons/customer-nav/photos/comet.svg'
import { ReactComponent as Photos } from 'src/styling/icons/customer-nav/photos/white.svg'
import styles from './CustomerSidebar.styles.js'
@ -33,6 +35,12 @@ const CustomerSidebar = ({ isSelected, onClick }) => {
display: 'Notes',
Icon: NoteIcon,
InverseIcon: NoteReversedIcon
},
{
code: 'photos',
display: 'Photos & files',
Icon: Photos,
InverseIcon: PhotosReversedIcon
}
]

View file

@ -4,58 +4,21 @@ 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 { Info2 } 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'
import PhotosCarousel from './PhotosCarousel'
const useStyles = makeStyles(styles)
const Label = ({ children }) => {
const classes = useStyles()
return <Label1 className={classes.label}>{children}</Label1>
}
const PhotosCard = memo(({ frontCameraData, txPhotosData }) => {
const PhotosCard = memo(({ photosData }) => {
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 (
@ -97,41 +60,4 @@ const PhotosCard = memo(({ frontCameraData, txPhotosData }) => {
)
})
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 (
<>
<Carousel photosData={photosData} slidePhoto={slidePhoto} />
{!isFaceCustomerPhoto && (
<div className={classes.firstRow}>
<Label>Session ID</Label>
<CopyToClipboard>
{photosData && photosData[currentIndex]?.id}
</CopyToClipboard>
</div>
)}
<div className={classes.secondRow}>
<div>
<div>
<Label>Date</Label>
<div>{photosData && photosData[currentIndex]?.date}</div>
</div>
</div>
<div>
<Label>Taken by</Label>
<div>
{!isFaceCustomerPhoto ? 'Acceptance of T&C' : 'Compliance scan'}
</div>
</div>
</div>
</>
)
})
export default PhotosCard

View file

@ -1,7 +1,4 @@
import typographyStyles from 'src/components/typography/styles'
import { zircon, backgroundColor, offColor } from 'src/styling/variables'
const { p } = typographyStyles
import { zircon, backgroundColor } from 'src/styling/variables'
export default {
photo: {
@ -41,43 +38,5 @@ export default {
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
}
}

View file

@ -0,0 +1,56 @@
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 { Label1 } from 'src/components/typography'
import CopyToClipboard from '../../Transactions/CopyToClipboard'
import styles from './PhotosCarousel.styles'
const useStyles = makeStyles(styles)
const PhotosCarousel = memo(({ photosData }) => {
const classes = useStyles()
const [currentIndex, setCurrentIndex] = useState(0)
const Label = ({ children }) => {
const classes = useStyles()
return <Label1 className={classes.label}>{children}</Label1>
}
const isFaceCustomerPhoto = !R.has('id')(photosData[currentIndex])
const slidePhoto = index => setCurrentIndex(index)
return (
<>
<Carousel photosData={photosData} slidePhoto={slidePhoto} />
{!isFaceCustomerPhoto && (
<div className={classes.firstRow}>
<Label>Session ID</Label>
<CopyToClipboard>
{photosData && photosData[currentIndex]?.id}
</CopyToClipboard>
</div>
)}
<div className={classes.secondRow}>
<div>
<div>
<Label>Date</Label>
<div>{photosData && photosData[currentIndex]?.date}</div>
</div>
</div>
<div>
<Label>Taken by</Label>
<div>
{!isFaceCustomerPhoto ? 'Acceptance of T&C' : 'Compliance scan'}
</div>
</div>
</div>
</>
)
})
export default PhotosCarousel

View file

@ -0,0 +1,31 @@
import typographyStyles from 'src/components/typography/styles'
import { offColor } from 'src/styling/variables'
const { p } = typographyStyles
export default {
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
}
}
}
}

View file

@ -5,10 +5,12 @@ import CustomerSidebar from './CustomerSidebar'
import EditableCard from './EditableCard'
import Field from './Field'
import IdDataCard from './IdDataCard'
import PhotosCarousel from './PhotosCarousel'
import TransactionsList from './TransactionsList'
import Upload from './Upload'
export {
PhotosCarousel,
CustomerDetails,
IdDataCard,
TransactionsList,

View file

@ -209,10 +209,41 @@ const entryType = {
initialValues: { entryType: '' }
}
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 formatPhotosData = R.compose(
filterByPhotoAvailable,
addPhotoDir,
standardizeKeys
)
export {
getAuthorizedStatus,
getFormattedPhone,
getName,
entryType,
customElements
customElements,
formatPhotosData
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/customer-nav/photos/comet</title>
<g id="icon/customer-nav/photos/comet" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" stroke="#5F668A" stroke-width="2" x="1" y="1" width="18" height="18" rx="1"></rect>
<circle id="Oval" stroke="#5F668A" stroke-width="2" cx="15" cy="5" r="1"></circle>
<polyline id="Path" stroke="#5F668A" stroke-width="2" stroke-linejoin="round" points="1 19 7 13 13 19"></polyline>
<path d="M13.3333333,14 L18,19 L13.3333333,19 L11,16.5 L13.3333333,14 Z" id="Combined-Shape" stroke="#5F668A" stroke-width="2" stroke-linejoin="round"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 850 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/customer-nav/photos/white</title>
<g id="icon/customer-nav/photos/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" stroke="#FFFFFF" stroke-width="2" x="1" y="1" width="18" height="18" rx="1"></rect>
<circle id="Oval" stroke="#FFFFFF" stroke-width="2" cx="15" cy="5" r="1"></circle>
<polyline id="Path" stroke="#FFFFFF" stroke-width="2" stroke-linejoin="round" points="1 19 7 13 13 19"></polyline>
<path d="M13.3333333,14 L18,19 L13.3333333,19 L11,16.5 L13.3333333,14 Z" id="Combined-Shape" stroke="#FFFFFF" stroke-width="2" stroke-linejoin="round"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 850 B