Merge branch 'dev' into refact/lam-108/dev/rbf-transactions
This commit is contained in:
commit
83de7a750d
45 changed files with 682 additions and 403 deletions
|
|
@ -45,6 +45,7 @@ keypool=10000
|
||||||
prune=4000
|
prune=4000
|
||||||
daemon=0
|
daemon=0
|
||||||
addresstype=p2sh-segwit
|
addresstype=p2sh-segwit
|
||||||
|
changetype=bech32
|
||||||
walletrbf=1
|
walletrbf=1
|
||||||
bind=0.0.0.0:8332
|
bind=0.0.0.0:8332
|
||||||
rpcport=8333`
|
rpcport=8333`
|
||||||
|
|
|
||||||
|
|
@ -44,5 +44,6 @@ connections=40
|
||||||
keypool=10000
|
keypool=10000
|
||||||
prune=4000
|
prune=4000
|
||||||
daemon=0
|
daemon=0
|
||||||
addresstype=p2sh-segwit`
|
addresstype=p2sh-segwit
|
||||||
|
changetype=bech32`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -683,18 +683,18 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
*/
|
*/
|
||||||
function getCustomerById (id) {
|
function getCustomerById (id) {
|
||||||
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
||||||
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_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,
|
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_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
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,
|
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
|
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, notes
|
||||||
FROM (
|
FROM (
|
||||||
SELECT c.id, c.authorized_override,
|
SELECT c.id, c.authorized_override,
|
||||||
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
|
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
|
||||||
c.suspended_until > now() AS is_suspended,
|
c.suspended_until > now() AS is_suspended,
|
||||||
c.front_camera_path, c.front_camera_override,
|
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_override, c.id_card_data_expiration,
|
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_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
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,
|
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,
|
row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
|
||||||
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY c.id) AS total_txs,
|
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY c.id) AS total_txs,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ function batch (
|
||||||
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired
|
((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
|
FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs
|
||||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||||
INNER JOIN devices d ON txs.device_id = d.device_id
|
LEFT JOIN devices d ON txs.device_id = d.device_id
|
||||||
WHERE txs.created >= $2 AND txs.created <= $3 ${
|
WHERE txs.created >= $2 AND txs.created <= $3 ${
|
||||||
id !== null ? `AND txs.device_id = $6` : ``
|
id !== null ? `AND txs.device_id = $6` : ``
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ function batch (
|
||||||
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
||||||
AND actions.action = 'provisionAddress'
|
AND actions.action = 'provisionAddress'
|
||||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||||
INNER JOIN devices d ON txs.device_id = d.device_id
|
LEFT JOIN devices d ON txs.device_id = d.device_id
|
||||||
WHERE txs.created >= $2 AND txs.created <= $3 ${
|
WHERE txs.created >= $2 AND txs.created <= $3 ${
|
||||||
id !== null ? `AND txs.device_id = $6` : ``
|
id !== null ? `AND txs.device_id = $6` : ``
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +130,10 @@ function simplifiedBatch (data) {
|
||||||
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode).toString()
|
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode).toString()
|
||||||
|
|
||||||
const getProfit = it => {
|
const getProfit = it => {
|
||||||
const getCommissionFee = it => BN(it.commissionPercentage).times(BN(it.fiat))
|
const discountValue = _.isNil(it.discount) ? BN(100) : BN(100).minus(it.discount)
|
||||||
|
const discountPercentage = BN(discountValue).div(100)
|
||||||
|
const commissionPercentage = BN(it.commissionPercentage).times(discountPercentage)
|
||||||
|
const getCommissionFee = it => BN(commissionPercentage).times(BN(it.fiat))
|
||||||
if (!it.cashInFee) return getCommissionFee(it)
|
if (!it.cashInFee) return getCommissionFee(it)
|
||||||
return getCommissionFee(it).plus(BN(it.cashInFee))
|
return getCommissionFee(it).plus(BN(it.cashInFee))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
new-lamassu-admin/package-lock.json
generated
22
new-lamassu-admin/package-lock.json
generated
|
|
@ -6889,11 +6889,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apollo-upload-client": {
|
"apollo-upload-client": {
|
||||||
"version": "16.0.0",
|
"version": "13.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-13.0.0.tgz",
|
||||||
"integrity": "sha512-aLhYucyA0T8aBEQ5g+p13qnR9RUyL8xqb8FSZ7e/Kw2KUOsotLUlFluLobqaE7JSUFwc6sKfXIcwB7y4yEjbZg==",
|
"integrity": "sha512-lJ9/bk1BH1lD15WhWRha2J3+LrXrPIX5LP5EwiOUHv8PCORp4EUrcujrA3rI5hZeZygrTX8bshcuMdpqpSrvtA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"extract-files": "^11.0.0"
|
"@babel/runtime": "^7.9.2",
|
||||||
|
"apollo-link": "^1.2.12",
|
||||||
|
"apollo-link-http-common": "^0.2.14",
|
||||||
|
"extract-files": "^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apollo-utilities": {
|
"apollo-utilities": {
|
||||||
|
|
@ -12617,9 +12620,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-files": {
|
"extract-files": {
|
||||||
"version": "11.0.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz",
|
||||||
"integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ=="
|
"integrity": "sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ=="
|
||||||
},
|
},
|
||||||
"extsprintf": {
|
"extsprintf": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
|
@ -27096,6 +27099,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
|
||||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||||
},
|
},
|
||||||
|
"ua-parser-js": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg=="
|
||||||
|
},
|
||||||
"unfetch": {
|
"unfetch": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"apollo-link": "^1.2.14",
|
"apollo-link": "^1.2.14",
|
||||||
"apollo-link-error": "^1.1.13",
|
"apollo-link-error": "^1.1.13",
|
||||||
"apollo-link-http": "^1.5.17",
|
"apollo-link-http": "^1.5.17",
|
||||||
"apollo-upload-client": "^16.0.0",
|
"apollo-upload-client": "^13.0.0",
|
||||||
"axios": "0.21.1",
|
"axios": "0.21.1",
|
||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
"bignumber.js": "9.0.0",
|
"bignumber.js": "9.0.0",
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
"react-use": "15.3.2",
|
"react-use": "15.3.2",
|
||||||
"react-virtualized": "^9.21.2",
|
"react-virtualized": "^9.21.2",
|
||||||
"sanctuary": "^2.0.1",
|
"sanctuary": "^2.0.1",
|
||||||
|
"ua-parser-js": "^1.0.2",
|
||||||
"uuid": "^7.0.2",
|
"uuid": "^7.0.2",
|
||||||
"yup": "0.32.9"
|
"yup": "0.32.9"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ const Tr = ({
|
||||||
onClick,
|
onClick,
|
||||||
error,
|
error,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
shouldShowError,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
size,
|
size,
|
||||||
|
|
@ -99,7 +100,9 @@ const Tr = ({
|
||||||
<Card className={classnames(classNames, className)} onClick={onClick}>
|
<Card className={classnames(classNames, className)} onClick={onClick}>
|
||||||
<CardContent classes={cardClasses}>
|
<CardContent classes={cardClasses}>
|
||||||
<div className={classes.mainContent}>{children}</div>
|
<div className={classes.mainContent}>{children}</div>
|
||||||
{error && <div className={classes.errorContent}>{errorMessage}</div>}
|
{error && shouldShowError && (
|
||||||
|
<div className={classes.errorContent}>{errorMessage}</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ const styles = {
|
||||||
borderRadius: '4px'
|
borderRadius: '4px'
|
||||||
},
|
},
|
||||||
focus: {
|
focus: {
|
||||||
color: primaryColor,
|
|
||||||
border: '2px solid',
|
border: '2px solid',
|
||||||
borderColor: primaryColor,
|
borderColor: primaryColor,
|
||||||
borderRadius: '4px'
|
borderRadius: '4px',
|
||||||
|
'&:focus': {
|
||||||
|
outline: 'none'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
borderColor: errorColor
|
borderColor: errorColor
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const Row = ({
|
const Row = ({
|
||||||
id,
|
id,
|
||||||
|
index,
|
||||||
elements,
|
elements,
|
||||||
data,
|
data,
|
||||||
width,
|
width,
|
||||||
|
|
@ -48,9 +49,11 @@ const Row = ({
|
||||||
[classes.row]: true,
|
[classes.row]: true,
|
||||||
[classes.expanded]: expanded
|
[classes.expanded]: expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.rowWrapper}>
|
<div className={classes.rowWrapper}>
|
||||||
<div className={classnames({ [classes.before]: expanded && id !== 0 })}>
|
<div
|
||||||
|
className={classnames({ [classes.before]: expanded && index !== 0 })}>
|
||||||
<Tr
|
<Tr
|
||||||
size={size}
|
size={size}
|
||||||
className={classnames(trClasses)}
|
className={classnames(trClasses)}
|
||||||
|
|
@ -58,8 +61,9 @@ const Row = ({
|
||||||
expandable && expandRow(id, data)
|
expandable && expandRow(id, data)
|
||||||
onClick && onClick(data)
|
onClick && onClick(data)
|
||||||
}}
|
}}
|
||||||
error={data.error}
|
error={data.error || data.hasError}
|
||||||
errorMessage={data.errorMessage}>
|
shouldShowError={false}
|
||||||
|
errorMessage={data.errorMessage || data.hasError}>
|
||||||
{elements.map(({ view = it => it?.toString(), ...props }, idx) => (
|
{elements.map(({ view = it => it?.toString(), ...props }, idx) => (
|
||||||
<Td key={idx} {...props}>
|
<Td key={idx} {...props}>
|
||||||
{view(data)}
|
{view(data)}
|
||||||
|
|
@ -142,6 +146,7 @@ const DataTable = ({
|
||||||
width={width}
|
width={width}
|
||||||
size={rowSize}
|
size={rowSize}
|
||||||
id={data[index].id ? data[index].id : index}
|
id={data[index].id ? data[index].id : index}
|
||||||
|
index={index}
|
||||||
expWidth={expWidth}
|
expWidth={expWidth}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={data[index]}
|
data={data[index]}
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ export default {
|
||||||
confirmationCode: {
|
confirmationCode: {
|
||||||
extend: base,
|
extend: base,
|
||||||
fontSize: codeInputFontSize,
|
fontSize: codeInputFontSize,
|
||||||
fontFamily: fontPrimary,
|
fontFamily: fontSecondary,
|
||||||
fontWeight: 900
|
fontWeight: 900
|
||||||
},
|
},
|
||||||
inline: {
|
inline: {
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ const Accounting = () => {
|
||||||
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
||||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||||
|
|
||||||
const loading = operatorLoading && configLoading
|
const loading = operatorLoading || configLoading
|
||||||
|
|
||||||
const operatorData = R.path(['operatorByUsername'], opData)
|
const operatorData = R.path(['operatorByUsername'], opData)
|
||||||
|
|
||||||
|
|
@ -170,19 +170,16 @@ const Accounting = () => {
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!loading && (
|
|
||||||
<>
|
<>
|
||||||
<TitleSection title="Accounting" />
|
<TitleSection title="Accounting" />
|
||||||
<Assets
|
<Assets
|
||||||
balance={
|
balance={operatorData.fiatBalances[operatorData.preferredFiatCurrency]}
|
||||||
operatorData.fiatBalances[operatorData.preferredFiatCurrency]
|
|
||||||
}
|
|
||||||
hedgingReserve={operatorData.hedgingReserve ?? 0}
|
hedgingReserve={operatorData.hedgingReserve ?? 0}
|
||||||
currency={operatorData.preferredFiatCurrency}
|
currency={operatorData.preferredFiatCurrency}
|
||||||
/>
|
/>
|
||||||
<H4 className={classes.tableTitle}>Fiat balance history</H4>
|
<H4 className={classes.tableTitle}>Fiat balance history</H4>
|
||||||
<DataTable
|
<DataTable
|
||||||
loading={false}
|
loading={loading}
|
||||||
emptyText="No transactions so far"
|
emptyText="No transactions so far"
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={operatorData.fundings ?? []}
|
data={operatorData.fundings ?? []}
|
||||||
|
|
@ -190,7 +187,6 @@ const Accounting = () => {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Accounting
|
export default Accounting
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { Button } from 'src/components/buttons'
|
||||||
import { TextInput } from 'src/components/inputs/formik'
|
import { TextInput } from 'src/components/inputs/formik'
|
||||||
import Sidebar from 'src/components/layout/Sidebar'
|
import Sidebar from 'src/components/layout/Sidebar'
|
||||||
import { Info2, P } from 'src/components/typography'
|
import { Info2, P } from 'src/components/typography'
|
||||||
|
import { ReactComponent as CameraIcon } from 'src/styling/icons/ID/photo/zodiac.svg'
|
||||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||||
import { ReactComponent as CompleteStageIconSpring } from 'src/styling/icons/stage/spring/complete.svg'
|
import { ReactComponent as CompleteStageIconSpring } from 'src/styling/icons/stage/spring/complete.svg'
|
||||||
import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg'
|
import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg'
|
||||||
|
|
@ -70,8 +71,18 @@ const QrCodeComponent = ({ classes, qrCode, name, count, onPaired }) => {
|
||||||
Scan QR code with your new cryptomat
|
Scan QR code with your new cryptomat
|
||||||
</Info2>
|
</Info2>
|
||||||
<div className={classes.qrCodeWrapper}>
|
<div className={classes.qrCodeWrapper}>
|
||||||
<div>
|
<div className={classes.qrCodeImageWrapper}>
|
||||||
<QRCode size={240} fgColor={primaryColor} value={qrCode} />
|
<QRCode
|
||||||
|
size={280}
|
||||||
|
fgColor={primaryColor}
|
||||||
|
includeMargin
|
||||||
|
value={qrCode}
|
||||||
|
className={classes.qrCodeBorder}
|
||||||
|
/>
|
||||||
|
<div className={classes.qrCodeScanMessage}>
|
||||||
|
<CameraIcon />
|
||||||
|
<P noMargin>Snap a picture and scan</P>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.qrTextWrapper}>
|
<div className={classes.qrTextWrapper}>
|
||||||
<div className={classes.qrTextInfoWrapper}>
|
<div className={classes.qrTextInfoWrapper}>
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,23 @@ const styles = {
|
||||||
},
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
color: errorColor
|
color: errorColor
|
||||||
|
},
|
||||||
|
qrCodeImageWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: `5px solid ${primaryColor}`,
|
||||||
|
padding: 5,
|
||||||
|
borderRadius: 15
|
||||||
|
},
|
||||||
|
qrCodeScanMessage: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: [[0, 0, 20, 20]],
|
||||||
|
'& > p': {
|
||||||
|
marginLeft: 10
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import base64 from 'base-64'
|
import base64 from 'base-64'
|
||||||
|
import { Form, Formik } from 'formik'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import React, { useContext, useState } from 'react'
|
import React, { useContext, useState } from 'react'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
@ -120,6 +121,9 @@ const Input2FAState = ({ state, dispatch }) => {
|
||||||
<TL1 className={classes.info}>
|
<TL1 className={classes.info}>
|
||||||
Enter your two-factor authentication code
|
Enter your two-factor authentication code
|
||||||
</TL1>
|
</TL1>
|
||||||
|
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
|
||||||
|
<Formik onSubmit={() => {}} initialValues={{}}>
|
||||||
|
<Form>
|
||||||
<CodeInput
|
<CodeInput
|
||||||
name="2fa"
|
name="2fa"
|
||||||
value={state.twoFAField}
|
value={state.twoFAField}
|
||||||
|
|
@ -128,6 +132,9 @@ const Input2FAState = ({ state, dispatch }) => {
|
||||||
error={invalidToken}
|
error={invalidToken}
|
||||||
shouldAutoFocus
|
shouldAutoFocus
|
||||||
/>
|
/>
|
||||||
|
<button onClick={handleSubmit} className={classes.enterButton} />
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
<div className={classes.twofaFooter}>
|
<div className={classes.twofaFooter}>
|
||||||
{errorMessage && <P className={classes.errorMessage}>{errorMessage}</P>}
|
{errorMessage && <P className={classes.errorMessage}>{errorMessage}</P>}
|
||||||
<Button onClick={handleSubmit} buttonClassName={classes.loginButton}>
|
<Button onClick={handleSubmit} buttonClassName={classes.loginButton}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
import { makeStyles, Grid } from '@material-ui/core'
|
import { makeStyles, Grid } from '@material-ui/core'
|
||||||
import Paper from '@material-ui/core/Paper'
|
import Paper from '@material-ui/core/Paper'
|
||||||
|
import { Form, Formik } from 'formik'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import QRCode from 'qrcode.react'
|
import QRCode from 'qrcode.react'
|
||||||
import React, { useReducer, useState } from 'react'
|
import React, { useReducer, useState } from 'react'
|
||||||
|
|
@ -101,6 +102,20 @@ const Reset2FA = () => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (twoFAConfirmation.length !== 6) {
|
||||||
|
setInvalidToken(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reset2FA({
|
||||||
|
variables: {
|
||||||
|
token: token,
|
||||||
|
userID: state.userID,
|
||||||
|
code: twoFAConfirmation
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
|
|
@ -152,6 +167,9 @@ const Reset2FA = () => {
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.confirm2FAInput}>
|
<div className={classes.confirm2FAInput}>
|
||||||
|
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
|
||||||
|
<Formik onSubmit={() => {}} initialValues={{}}>
|
||||||
|
<Form>
|
||||||
<CodeInput
|
<CodeInput
|
||||||
name="2fa"
|
name="2fa"
|
||||||
value={twoFAConfirmation}
|
value={twoFAConfirmation}
|
||||||
|
|
@ -160,25 +178,19 @@ const Reset2FA = () => {
|
||||||
error={invalidToken}
|
error={invalidToken}
|
||||||
shouldAutoFocus
|
shouldAutoFocus
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className={classes.enterButton}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.twofaFooter}>
|
<div className={classes.twofaFooter}>
|
||||||
{getErrorMsg() && (
|
{getErrorMsg() && (
|
||||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={handleSubmit}
|
||||||
if (twoFAConfirmation.length !== 6) {
|
|
||||||
setInvalidToken(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reset2FA({
|
|
||||||
variables: {
|
|
||||||
token: token,
|
|
||||||
userID: state.userID,
|
|
||||||
code: twoFAConfirmation
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
buttonClassName={classes.loginButton}>
|
buttonClassName={classes.loginButton}>
|
||||||
Done
|
Done
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useMutation, useQuery, useLazyQuery } from '@apollo/react-hooks'
|
import { useMutation, useQuery, useLazyQuery } from '@apollo/react-hooks'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import base64 from 'base-64'
|
import base64 from 'base-64'
|
||||||
|
import { Form, Formik } from 'formik'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import QRCode from 'qrcode.react'
|
import QRCode from 'qrcode.react'
|
||||||
import React, { useContext, useState } from 'react'
|
import React, { useContext, useState } from 'react'
|
||||||
|
|
@ -125,6 +126,14 @@ const Setup2FAState = ({ state, dispatch }) => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (twoFAConfirmation.length !== 6) {
|
||||||
|
setInvalidToken(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setup2FA(mutationOptions)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
secret &&
|
secret &&
|
||||||
otpauth && (
|
otpauth && (
|
||||||
|
|
@ -159,6 +168,9 @@ const Setup2FAState = ({ state, dispatch }) => {
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.confirm2FAInput}>
|
<div className={classes.confirm2FAInput}>
|
||||||
|
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
|
||||||
|
<Formik onSubmit={() => {}} initialValues={{}}>
|
||||||
|
<Form>
|
||||||
<CodeInput
|
<CodeInput
|
||||||
name="2fa"
|
name="2fa"
|
||||||
value={twoFAConfirmation}
|
value={twoFAConfirmation}
|
||||||
|
|
@ -167,20 +179,15 @@ const Setup2FAState = ({ state, dispatch }) => {
|
||||||
error={invalidToken}
|
error={invalidToken}
|
||||||
shouldAutoFocus
|
shouldAutoFocus
|
||||||
/>
|
/>
|
||||||
|
<button onClick={handleSubmit} className={classes.enterButton} />
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.twofaFooter}>
|
<div className={classes.twofaFooter}>
|
||||||
{getErrorMsg() && (
|
{getErrorMsg() && (
|
||||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button onClick={handleSubmit} buttonClassName={classes.loginButton}>
|
||||||
onClick={() => {
|
|
||||||
if (twoFAConfirmation.length !== 6) {
|
|
||||||
setInvalidToken(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setup2FA(mutationOptions)
|
|
||||||
}}
|
|
||||||
buttonClassName={classes.loginButton}>
|
|
||||||
Done
|
Done
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,9 @@ const styles = {
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
color: errorColor
|
color: errorColor
|
||||||
|
},
|
||||||
|
enterButton: {
|
||||||
|
display: 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
81
new-lamassu-admin/src/pages/Customers/CustomerPhotos.js
Normal file
81
new-lamassu-admin/src/pages/Customers/CustomerPhotos.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
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 }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [photosDialog, setPhotosDialog] = useState(false)
|
||||||
|
const [photoClickedIndex, setPhotoClickIndex] = useState(null)
|
||||||
|
const orderedPhotosData = !R.isNil(photoClickedIndex)
|
||||||
|
? R.compose(R.flatten, R.reverse, R.splitAt(photoClickedIndex))(photosData)
|
||||||
|
: photosData
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={classes.header}>
|
||||||
|
<H3 className={classes.title}>{'Photos & files'}</H3>
|
||||||
|
</div>
|
||||||
|
<div className={classes.photosChipList}>
|
||||||
|
{photosData.map((elem, idx) => (
|
||||||
|
<PhotoCard
|
||||||
|
idx={idx}
|
||||||
|
date={elem.date}
|
||||||
|
src={`${URI}/${elem.photoDir}/${elem.path}`}
|
||||||
|
setPhotosDialog={setPhotosDialog}
|
||||||
|
setPhotoClickIndex={setPhotoClickIndex}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<InformativeDialog
|
||||||
|
open={photosDialog}
|
||||||
|
title={`Photo roll`}
|
||||||
|
data={<PhotosCarousel photosData={orderedPhotosData} />}
|
||||||
|
onDissmised={() => {
|
||||||
|
setPhotosDialog(false)
|
||||||
|
setPhotoClickIndex(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PhotoCard = ({
|
||||||
|
idx,
|
||||||
|
date,
|
||||||
|
src,
|
||||||
|
setPhotosDialog,
|
||||||
|
setPhotoClickIndex
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
className={classes.photoCardChip}
|
||||||
|
onClick={() => {
|
||||||
|
setPhotoClickIndex(idx)
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
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,
|
||||||
|
borderTopLeftRadius: 4,
|
||||||
|
borderTopRightRadius: 4
|
||||||
|
},
|
||||||
|
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
|
||||||
|
|
@ -24,6 +24,7 @@ import { fromNamespace, namespaces } from 'src/utils/config'
|
||||||
|
|
||||||
import CustomerData from './CustomerData'
|
import CustomerData from './CustomerData'
|
||||||
import CustomerNotes from './CustomerNotes'
|
import CustomerNotes from './CustomerNotes'
|
||||||
|
import CustomerPhotos from './CustomerPhotos'
|
||||||
import styles from './CustomerProfile.styles'
|
import styles from './CustomerProfile.styles'
|
||||||
import {
|
import {
|
||||||
CustomerDetails,
|
CustomerDetails,
|
||||||
|
|
@ -31,7 +32,7 @@ import {
|
||||||
CustomerSidebar,
|
CustomerSidebar,
|
||||||
Wizard
|
Wizard
|
||||||
} from './components'
|
} from './components'
|
||||||
import { getFormattedPhone, getName } from './helper'
|
import { getFormattedPhone, getName, formatPhotosData } from './helper'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
|
@ -367,12 +368,24 @@ const CustomerProfile = memo(() => {
|
||||||
const isCustomerData = clickedItem === 'customerData'
|
const isCustomerData = clickedItem === 'customerData'
|
||||||
const isOverview = clickedItem === 'overview'
|
const isOverview = clickedItem === 'overview'
|
||||||
const isNotes = clickedItem === 'notes'
|
const isNotes = clickedItem === 'notes'
|
||||||
|
const isPhotos = clickedItem === 'photos'
|
||||||
|
|
||||||
const loading = customerLoading && configLoading
|
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
|
||||||
|
|
||||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||||
|
|
||||||
const classes = useStyles({ blocked })
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -406,29 +419,26 @@ const CustomerProfile = memo(() => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Label1 className={classes.actionLabel}>Actions</Label1>
|
<Label1 className={classes.actionLabel}>Actions</Label1>
|
||||||
<div>
|
<div className={classes.actionBar}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className={classes.customerManualDataEntry}
|
className={classes.actionButton}
|
||||||
color="primary"
|
color="primary"
|
||||||
Icon={DataIcon}
|
Icon={DataIcon}
|
||||||
InverseIcon={DataReversedIcon}
|
InverseIcon={DataReversedIcon}
|
||||||
onClick={() => setWizard(true)}>
|
onClick={() => setWizard(true)}>
|
||||||
{`Manual data entry`}
|
{`Manual data entry`}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className={classes.customerDiscount}
|
className={classes.actionButton}
|
||||||
color="primary"
|
color="primary"
|
||||||
Icon={Discount}
|
Icon={Discount}
|
||||||
InverseIcon={DiscountReversedIcon}
|
InverseIcon={DiscountReversedIcon}
|
||||||
onClick={() => {}}>
|
onClick={() => {}}>
|
||||||
{`Add individual discount`}
|
{`Add individual discount`}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{isSuspended && (
|
{isSuspended && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className={classes.actionButton}
|
||||||
color="primary"
|
color="primary"
|
||||||
Icon={AuthorizeIcon}
|
Icon={AuthorizeIcon}
|
||||||
InverseIcon={AuthorizeReversedIcon}
|
InverseIcon={AuthorizeReversedIcon}
|
||||||
|
|
@ -442,7 +452,7 @@ const CustomerProfile = memo(() => {
|
||||||
)}
|
)}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="primary"
|
color="primary"
|
||||||
className={classes.customerBlock}
|
className={classes.actionButton}
|
||||||
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
||||||
InverseIcon={
|
InverseIcon={
|
||||||
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
||||||
|
|
@ -458,7 +468,7 @@ const CustomerProfile = memo(() => {
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="primary"
|
color="primary"
|
||||||
className={classes.retrieveInformation}
|
className={classes.actionButton}
|
||||||
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
||||||
InverseIcon={
|
InverseIcon={
|
||||||
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
||||||
|
|
@ -488,6 +498,7 @@ const CustomerProfile = memo(() => {
|
||||||
justifyContent="space-between">
|
justifyContent="space-between">
|
||||||
<CustomerDetails
|
<CustomerDetails
|
||||||
customer={customerData}
|
customer={customerData}
|
||||||
|
photosData={photosData}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
setShowCompliance={() => setShowCompliance(!showCompliance)}
|
setShowCompliance={() => setShowCompliance(!showCompliance)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -524,6 +535,11 @@ const CustomerProfile = memo(() => {
|
||||||
timezone={timezone}></CustomerNotes>
|
timezone={timezone}></CustomerNotes>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isPhotos && (
|
||||||
|
<div>
|
||||||
|
<CustomerPhotos photosData={photosData} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{wizard && (
|
{wizard && (
|
||||||
<Wizard
|
<Wizard
|
||||||
|
|
|
||||||
|
|
@ -15,29 +15,16 @@ export default {
|
||||||
customerDetails: {
|
customerDetails: {
|
||||||
marginBottom: 18
|
marginBottom: 18
|
||||||
},
|
},
|
||||||
customerBlock: props => ({
|
actionButton: {
|
||||||
|
margin: [[0, 0, 4, 0]],
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
margin: [[0, 0, 4, 0]],
|
justifyContent: 'center'
|
||||||
padding: [[0, props.blocked ? 35 : 48, 0]]
|
|
||||||
}),
|
|
||||||
customerDiscount: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
margin: [[0, 0, 4, 0]],
|
|
||||||
padding: [[0, 23.5, 0]]
|
|
||||||
},
|
},
|
||||||
customerManualDataEntry: {
|
actionBar: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'column',
|
||||||
margin: [[8, 0, 4, 0]],
|
width: 219
|
||||||
padding: [[0, 40.5, 0]]
|
|
||||||
},
|
|
||||||
retrieveInformation: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
margin: [[0, 0, 4, 0]],
|
|
||||||
padding: [[0, 32.5, 0]]
|
|
||||||
},
|
},
|
||||||
panels: {
|
panels: {
|
||||||
display: 'flex'
|
display: 'flex'
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ import PhotosCard from './PhotosCard'
|
||||||
|
|
||||||
const useStyles = makeStyles(mainStyles)
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
|
||||||
const CustomerDetails = memo(
|
const CustomerDetails = memo(({ customer, photosData, locale }) => {
|
||||||
({ txData, customer, locale, setShowCompliance }) => {
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
|
const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
|
||||||
|
|
@ -45,27 +44,14 @@ const CustomerDetails = memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
<PhotosCard
|
<PhotosCard photosData={photosData} />
|
||||||
frontCameraData={R.pick(['frontCameraPath', 'frontCameraAt'])(
|
|
||||||
customer
|
|
||||||
)}
|
|
||||||
txPhotosData={
|
|
||||||
txData &&
|
|
||||||
R.map(R.pick(['id', 'txCustomerPhotoPath', 'txCustomerPhotoAt']))(
|
|
||||||
txData
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Box display="flex" flexDirection="column">
|
<Box display="flex" flexDirection="column">
|
||||||
<div className={classes.name}>
|
<div className={classes.name}>
|
||||||
<IdIcon className={classes.idIcon} />
|
<IdIcon className={classes.idIcon} />
|
||||||
<H2 noMargin>
|
<H2 noMargin>
|
||||||
{name.length
|
{name.length
|
||||||
? name
|
? name
|
||||||
: getFormattedPhone(
|
: getFormattedPhone(R.path(['phone'])(customer), locale.country)}
|
||||||
R.path(['phone'])(customer),
|
|
||||||
locale.country
|
|
||||||
)}
|
|
||||||
</H2>
|
</H2>
|
||||||
</div>
|
</div>
|
||||||
<Box display="flex" mt="auto">
|
<Box display="flex" mt="auto">
|
||||||
|
|
@ -93,7 +79,6 @@ const CustomerDetails = memo(
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
export default CustomerDetails
|
export default CustomerDetails
|
||||||
|
|
|
||||||
|
|
@ -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 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 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 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'
|
import styles from './CustomerSidebar.styles.js'
|
||||||
|
|
||||||
|
|
@ -33,6 +35,12 @@ const CustomerSidebar = ({ isSelected, onClick }) => {
|
||||||
display: 'Notes',
|
display: 'Notes',
|
||||||
Icon: NoteIcon,
|
Icon: NoteIcon,
|
||||||
InverseIcon: NoteReversedIcon
|
InverseIcon: NoteReversedIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'photos',
|
||||||
|
display: 'Photos & files',
|
||||||
|
Icon: Photos,
|
||||||
|
InverseIcon: PhotosReversedIcon
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,58 +4,21 @@ import { makeStyles } from '@material-ui/core/styles'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { memo, useState } from 'react'
|
import React, { memo, useState } from 'react'
|
||||||
|
|
||||||
import { Carousel } from 'src/components/Carousel'
|
|
||||||
import { InformativeDialog } from 'src/components/InformativeDialog'
|
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 { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
|
||||||
import { URI } from 'src/utils/apollo'
|
import { URI } from 'src/utils/apollo'
|
||||||
|
|
||||||
import CopyToClipboard from '../../Transactions/CopyToClipboard'
|
|
||||||
|
|
||||||
import styles from './PhotosCard.styles'
|
import styles from './PhotosCard.styles'
|
||||||
|
import PhotosCarousel from './PhotosCarousel'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const Label = ({ children }) => {
|
const PhotosCard = memo(({ photosData }) => {
|
||||||
const classes = useStyles()
|
|
||||||
return <Label1 className={classes.label}>{children}</Label1>
|
|
||||||
}
|
|
||||||
|
|
||||||
const PhotosCard = memo(({ frontCameraData, txPhotosData }) => {
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const [photosDialog, setPhotosDialog] = useState(false)
|
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)
|
const singlePhoto = R.head(photosData)
|
||||||
|
|
||||||
return (
|
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
|
export default PhotosCard
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import typographyStyles from 'src/components/typography/styles'
|
import { zircon, backgroundColor } from 'src/styling/variables'
|
||||||
import { zircon, backgroundColor, offColor } from 'src/styling/variables'
|
|
||||||
|
|
||||||
const { p } = typographyStyles
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
photo: {
|
photo: {
|
||||||
|
|
@ -41,43 +38,5 @@ export default {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
display: 'flex'
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -70,12 +70,7 @@ const TransactionsList = ({ customer, data, loading, locale }) => {
|
||||||
|
|
||||||
const tableElements = [
|
const tableElements = [
|
||||||
{
|
{
|
||||||
header: 'Machine',
|
width: 40,
|
||||||
width: 160,
|
|
||||||
view: R.path(['machineName'])
|
|
||||||
},
|
|
||||||
{
|
|
||||||
width: 125,
|
|
||||||
view: it => (
|
view: it => (
|
||||||
<>
|
<>
|
||||||
{it.txClass === 'cashOut' ? (
|
{it.txClass === 'cashOut' ? (
|
||||||
|
|
@ -86,6 +81,11 @@ const TransactionsList = ({ customer, data, loading, locale }) => {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'Machine',
|
||||||
|
width: 160,
|
||||||
|
view: R.path(['machineName'])
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Transaction ID',
|
header: 'Transaction ID',
|
||||||
width: 145,
|
width: 145,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ import CustomerSidebar from './CustomerSidebar'
|
||||||
import EditableCard from './EditableCard'
|
import EditableCard from './EditableCard'
|
||||||
import Field from './Field'
|
import Field from './Field'
|
||||||
import IdDataCard from './IdDataCard'
|
import IdDataCard from './IdDataCard'
|
||||||
|
import PhotosCarousel from './PhotosCarousel'
|
||||||
import TransactionsList from './TransactionsList'
|
import TransactionsList from './TransactionsList'
|
||||||
import Upload from './Upload'
|
import Upload from './Upload'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
PhotosCarousel,
|
||||||
CustomerDetails,
|
CustomerDetails,
|
||||||
IdDataCard,
|
IdDataCard,
|
||||||
TransactionsList,
|
TransactionsList,
|
||||||
|
|
|
||||||
|
|
@ -209,10 +209,41 @@ const entryType = {
|
||||||
initialValues: { 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 {
|
export {
|
||||||
getAuthorizedStatus,
|
getAuthorizedStatus,
|
||||||
getFormattedPhone,
|
getFormattedPhone,
|
||||||
getName,
|
getName,
|
||||||
entryType,
|
entryType,
|
||||||
customElements
|
customElements,
|
||||||
|
formatPhotosData
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,9 @@ const IndividualDiscounts = () => {
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
const toggleModal = () => setShowModal(!showModal)
|
const toggleModal = () => setShowModal(!showModal)
|
||||||
|
|
||||||
const { data: discountResponse, loading } = useQuery(GET_INDIVIDUAL_DISCOUNTS)
|
const { data: discountResponse, loading: discountLoading } = useQuery(
|
||||||
|
GET_INDIVIDUAL_DISCOUNTS
|
||||||
|
)
|
||||||
const { data: customerData, loading: customerLoading } = useQuery(
|
const { data: customerData, loading: customerLoading } = useQuery(
|
||||||
GET_CUSTOMERS
|
GET_CUSTOMERS
|
||||||
)
|
)
|
||||||
|
|
@ -160,11 +162,12 @@ const IndividualDiscounts = () => {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const isLoading = loading || customerLoading
|
const loading = discountLoading || customerLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isLoading && !R.isEmpty(discountResponse.individualDiscounts) && (
|
{!loading && !R.isEmpty(discountResponse.individualDiscounts) && (
|
||||||
|
<>
|
||||||
<Box
|
<Box
|
||||||
marginBottom={4}
|
marginBottom={4}
|
||||||
marginTop={-7}
|
marginTop={-7}
|
||||||
|
|
@ -175,9 +178,6 @@ const IndividualDiscounts = () => {
|
||||||
Add new code
|
Add new code
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
{!isLoading && !R.isEmpty(discountResponse.individualDiscounts) && (
|
|
||||||
<>
|
|
||||||
<DataTable
|
<DataTable
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={R.path(['individualDiscounts'])(discountResponse)}
|
data={R.path(['individualDiscounts'])(discountResponse)}
|
||||||
|
|
@ -196,7 +196,7 @@ const IndividualDiscounts = () => {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isLoading && R.isEmpty(discountResponse.individualDiscounts) && (
|
{!loading && R.isEmpty(discountResponse.individualDiscounts) && (
|
||||||
<Box display="flex" alignItems="left" flexDirection="column">
|
<Box display="flex" alignItems="left" flexDirection="column">
|
||||||
<Label3>
|
<Label3>
|
||||||
It seems there are no active individual customer discounts on your
|
It seems there are no active individual customer discounts on your
|
||||||
|
|
|
||||||
|
|
@ -81,16 +81,21 @@ const Logs = () => {
|
||||||
|
|
||||||
const deviceId = selected?.deviceId
|
const deviceId = selected?.deviceId
|
||||||
|
|
||||||
const { data: machineResponse } = useQuery(GET_MACHINES)
|
const { data: machineResponse, loading: machinesLoading } = useQuery(
|
||||||
|
GET_MACHINES
|
||||||
|
)
|
||||||
|
|
||||||
const { data: configResponse } = useQuery(GET_DATA)
|
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
||||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||||
|
|
||||||
const { data: logsResponse, loading } = useQuery(GET_MACHINE_LOGS, {
|
const { data: logsResponse, loading: logsLoading } = useQuery(
|
||||||
|
GET_MACHINE_LOGS,
|
||||||
|
{
|
||||||
variables: { deviceId, limit: NUM_LOG_RESULTS },
|
variables: { deviceId, limit: NUM_LOG_RESULTS },
|
||||||
skip: !selected,
|
skip: !selected,
|
||||||
onCompleted: () => setSaveMessage('')
|
onCompleted: () => setSaveMessage('')
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (machineResponse?.machines?.length && !selected) {
|
if (machineResponse?.machines?.length && !selected) {
|
||||||
setSelected(machineResponse?.machines[0])
|
setSelected(machineResponse?.machines[0])
|
||||||
|
|
@ -100,6 +105,8 @@ const Logs = () => {
|
||||||
return R.path(['deviceId'])(selected) === it.deviceId
|
return R.path(['deviceId'])(selected) === it.deviceId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loading = machinesLoading || configLoading || logsLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classes.titleWrapper}>
|
<div className={classes.titleWrapper}>
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ const Transactions = ({ id }) => {
|
||||||
const { data: configData, loading: configLoading } = useQuery(GET_DATA)
|
const { data: configData, loading: configLoading } = useQuery(GET_DATA)
|
||||||
const timezone = R.path(['config', 'locale_timezone'], configData)
|
const timezone = R.path(['config', 'locale_timezone'], configData)
|
||||||
|
|
||||||
const loading = txLoading && configLoading
|
const loading = txLoading || configLoading
|
||||||
|
|
||||||
if (!loading && txResponse) {
|
if (!loading && txResponse) {
|
||||||
txResponse.transactions = txResponse.transactions.splice(0, 5)
|
txResponse.transactions = txResponse.transactions.splice(0, 5)
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ const CashboxHistory = ({ machines, currency }) => {
|
||||||
const { data: configData, loading: configLoading } = useQuery(GET_DATA)
|
const { data: configData, loading: configLoading } = useQuery(GET_DATA)
|
||||||
const timezone = R.path(['config', 'locale_timezone'], configData)
|
const timezone = R.path(['config', 'locale_timezone'], configData)
|
||||||
|
|
||||||
const loading = batchesLoading && configLoading
|
const loading = batchesLoading || configLoading
|
||||||
|
|
||||||
const batches = R.path(['cashboxBatches'])(batchesData)
|
const batches = R.path(['cashboxBatches'])(batchesData)
|
||||||
|
|
||||||
|
|
@ -252,16 +252,13 @@ const CashboxHistory = ({ machines, currency }) => {
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
{!loading && (
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
loading={loading}
|
||||||
name="cashboxHistory"
|
name="cashboxHistory"
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={batches}
|
data={batches}
|
||||||
emptyText="No cashbox batches so far"
|
emptyText="No cashbox batches so far"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,11 @@ const MachineStatus = () => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { state } = useLocation()
|
const { state } = useLocation()
|
||||||
const addedMachineId = state?.id
|
const addedMachineId = state?.id
|
||||||
const { data: machinesResponse, refetch, loading } = useQuery(GET_MACHINES)
|
const {
|
||||||
|
data: machinesResponse,
|
||||||
|
refetch,
|
||||||
|
loading: machinesLoading
|
||||||
|
} = useQuery(GET_MACHINES)
|
||||||
const { data: configResponse, configLoading } = useQuery(GET_DATA)
|
const { data: configResponse, configLoading } = useQuery(GET_DATA)
|
||||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||||
|
|
||||||
|
|
@ -114,6 +118,8 @@ const MachineStatus = () => {
|
||||||
<MachineDetailsRow it={it} onActionSuccess={refetch} timezone={timezone} />
|
<MachineDetailsRow it={it} onActionSuccess={refetch} timezone={timezone} />
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const loading = machinesLoading || configLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classes.titleWrapper}>
|
<div className={classes.titleWrapper}>
|
||||||
|
|
@ -132,7 +138,7 @@ const MachineStatus = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
loading={loading && configLoading}
|
loading={loading}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={machines}
|
data={machines}
|
||||||
Details={InnerMachineDetailsRow}
|
Details={InnerMachineDetailsRow}
|
||||||
|
|
|
||||||
|
|
@ -98,13 +98,13 @@ const Logs = () => {
|
||||||
const [saveMessage, setSaveMessage] = useState(null)
|
const [saveMessage, setSaveMessage] = useState(null)
|
||||||
const [logLevel, setLogLevel] = useState(SHOW_ALL)
|
const [logLevel, setLogLevel] = useState(SHOW_ALL)
|
||||||
|
|
||||||
const { data, loading } = useQuery(GET_SERVER_DATA, {
|
const { data, loading: dataLoading } = useQuery(GET_SERVER_DATA, {
|
||||||
onCompleted: () => setSaveMessage(''),
|
onCompleted: () => setSaveMessage(''),
|
||||||
variables: {
|
variables: {
|
||||||
limit: NUM_LOG_RESULTS
|
limit: NUM_LOG_RESULTS
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const { data: configResponse, configLoading } = useQuery(GET_DATA)
|
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
||||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||||
|
|
||||||
const defaultLogLevels = [
|
const defaultLogLevels = [
|
||||||
|
|
@ -132,6 +132,8 @@ const Logs = () => {
|
||||||
setLogLevel(logLevel)
|
setLogLevel(logLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loading = dataLoading || configLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classes.titleWrapper}>
|
<div className={classes.titleWrapper}>
|
||||||
|
|
@ -206,8 +208,8 @@ const Logs = () => {
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
{loading && configLoading && <H4>{'Loading...'}</H4>}
|
{loading && <H4>{'Loading...'}</H4>}
|
||||||
{!loading && !configLoading && !data?.serverLogs?.length && (
|
{!loading && !data?.serverLogs?.length && (
|
||||||
<H4>{'No activity so far'}</H4>
|
<H4>{'No activity so far'}</H4>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ const SessionManagement = () => {
|
||||||
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
||||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||||
|
|
||||||
const loading = sessionsLoading && configLoading
|
const loading = sessionsLoading || configLoading
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
|
|
@ -107,16 +107,15 @@ const SessionManagement = () => {
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!loading && (
|
|
||||||
<>
|
<>
|
||||||
<TitleSection title="Session Management" />
|
<TitleSection title="Session Management" />
|
||||||
<DataTable
|
<DataTable
|
||||||
|
loading={loading}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={R.path(['sessions'])(tknResponse)}
|
data={R.path(['sessions'])(tknResponse)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SessionManagement
|
export default SessionManagement
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import DataTable from 'src/components/tables/DataTable'
|
||||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
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 { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
import { ReactComponent as CustomerLinkIcon } from 'src/styling/icons/month arrows/right.svg'
|
import { ReactComponent as CustomerLinkIcon } from 'src/styling/icons/month arrows/right.svg'
|
||||||
|
import { ReactComponent as CustomerLinkWhiteIcon } from 'src/styling/icons/month arrows/right_white.svg'
|
||||||
|
import { errorColor } from 'src/styling/variables'
|
||||||
import { formatDate } from 'src/utils/timezones'
|
import { formatDate } from 'src/utils/timezones'
|
||||||
|
|
||||||
import DetailsRow from './DetailsCard'
|
import DetailsRow from './DetailsCard'
|
||||||
|
|
@ -124,13 +126,13 @@ const Transactions = () => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
const [filters, setFilters] = useState([])
|
const [filters, setFilters] = useState([])
|
||||||
const { data: filtersResponse, loading: loadingFilters } = useQuery(
|
const { data: filtersResponse, loading: filtersLoading } = useQuery(
|
||||||
GET_TRANSACTION_FILTERS
|
GET_TRANSACTION_FILTERS
|
||||||
)
|
)
|
||||||
const [variables, setVariables] = useState({ limit: NUM_LOG_RESULTS })
|
const [variables, setVariables] = useState({ limit: NUM_LOG_RESULTS })
|
||||||
const {
|
const {
|
||||||
data: txData,
|
data: txData,
|
||||||
loading: loadingTransactions,
|
loading: transactionsLoading,
|
||||||
refetch,
|
refetch,
|
||||||
startPolling,
|
startPolling,
|
||||||
stopPolling
|
stopPolling
|
||||||
|
|
@ -185,7 +187,11 @@ const Transactions = () => {
|
||||||
<div className={classes.overflowTd}>{getCustomerDisplayName(it)}</div>
|
<div className={classes.overflowTd}>{getCustomerDisplayName(it)}</div>
|
||||||
{!it.isAnonymous && (
|
{!it.isAnonymous && (
|
||||||
<div onClick={() => redirect(it.customerId)}>
|
<div onClick={() => redirect(it.customerId)}>
|
||||||
|
{it.hasError ? (
|
||||||
|
<CustomerLinkWhiteIcon className={classes.customerLinkIcon} />
|
||||||
|
) : (
|
||||||
<CustomerLinkIcon className={classes.customerLinkIcon} />
|
<CustomerLinkIcon className={classes.customerLinkIcon} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -294,6 +300,14 @@ const Transactions = () => {
|
||||||
|
|
||||||
const filterOptions = R.path(['transactionFilters'])(filtersResponse)
|
const filterOptions = R.path(['transactionFilters'])(filtersResponse)
|
||||||
|
|
||||||
|
const loading = transactionsLoading || filtersLoading || configLoading
|
||||||
|
|
||||||
|
const errorLabel = (
|
||||||
|
<svg width={12} height={12}>
|
||||||
|
<rect width={12} height={12} rx={3} fill={errorColor} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classes.titleWrapper}>
|
<div className={classes.titleWrapper}>
|
||||||
|
|
@ -301,7 +315,7 @@ const Transactions = () => {
|
||||||
<Title>Transactions</Title>
|
<Title>Transactions</Title>
|
||||||
<div className={classes.buttonsWrapper}>
|
<div className={classes.buttonsWrapper}>
|
||||||
<SearchBox
|
<SearchBox
|
||||||
loading={loadingFilters}
|
loading={filtersLoading}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
options={filterOptions}
|
options={filterOptions}
|
||||||
inputPlaceholder={'Search Transactions'}
|
inputPlaceholder={'Search Transactions'}
|
||||||
|
|
@ -331,6 +345,10 @@ const Transactions = () => {
|
||||||
<TxOutIcon />
|
<TxOutIcon />
|
||||||
<span>Cash-out</span>
|
<span>Cash-out</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
{errorLabel}
|
||||||
|
<span>Transaction error</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{filters.length > 0 && (
|
{filters.length > 0 && (
|
||||||
|
|
@ -342,7 +360,7 @@ const Transactions = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DataTable
|
<DataTable
|
||||||
loading={loadingTransactions && configLoading}
|
loading={loading}
|
||||||
emptyText="No transactions so far"
|
emptyText="No transactions so far"
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={txList}
|
data={txList}
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,11 @@ const mainStyles = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
},
|
},
|
||||||
|
'& > div': {
|
||||||
|
marginLeft: 24
|
||||||
|
},
|
||||||
'& > div:first-child': {
|
'& > div:first-child': {
|
||||||
marginRight: 24
|
marginLeft: 0
|
||||||
},
|
},
|
||||||
'& span': {
|
'& span': {
|
||||||
extend: label1,
|
extend: label1,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useLazyQuery } from '@apollo/react-hooks'
|
import { useLazyQuery } from '@apollo/react-hooks'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import { Form, Formik } from 'formik'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
|
@ -48,6 +49,14 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (twoFACode.length !== 6) {
|
||||||
|
setInvalidCode(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirm2FA({ variables: { code: twoFACode } })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
showModal && (
|
showModal && (
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -61,6 +70,9 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
|
||||||
To make changes on this user, please confirm this action by entering
|
To make changes on this user, please confirm this action by entering
|
||||||
your two-factor authentication code below.
|
your two-factor authentication code below.
|
||||||
</P>
|
</P>
|
||||||
|
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
|
||||||
|
<Formik onSubmit={() => {}} initialValues={{}}>
|
||||||
|
<Form>
|
||||||
<CodeInput
|
<CodeInput
|
||||||
name="2fa"
|
name="2fa"
|
||||||
value={twoFACode}
|
value={twoFACode}
|
||||||
|
|
@ -70,19 +82,14 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
|
||||||
containerStyle={classes.codeContainer}
|
containerStyle={classes.codeContainer}
|
||||||
shouldAutoFocus
|
shouldAutoFocus
|
||||||
/>
|
/>
|
||||||
|
<button onClick={handleSubmit} className={classes.enterButton} />
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
{getErrorMsg() && (
|
{getErrorMsg() && (
|
||||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||||
)}
|
)}
|
||||||
<div className={classes.footer}>
|
<div className={classes.footer}>
|
||||||
<Button
|
<Button className={classes.submit} onClick={handleSubmit}>
|
||||||
className={classes.submit}
|
|
||||||
onClick={() => {
|
|
||||||
if (twoFACode.length !== 6) {
|
|
||||||
setInvalidCode(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
confirm2FA({ variables: { code: twoFACode } })
|
|
||||||
}}>
|
|
||||||
Confirm
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ const SAVE_ACCOUNTS = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const isConfigurable = it => !R.isNil(it) && !R.contains(it)(['mock-exchange'])
|
const isConfigurable = it =>
|
||||||
|
!R.isNil(it) && !R.contains(it)(['mock-exchange', 'no-exchange'])
|
||||||
|
|
||||||
const ChooseExchange = ({ data: currentData, addData }) => {
|
const ChooseExchange = ({ data: currentData, addData }) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?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">
|
||||||
|
<defs>
|
||||||
|
<circle id="path-1-right" cx="10" cy="10" r="10"></circle>
|
||||||
|
</defs>
|
||||||
|
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="pop-up/action/download-logs/date-range-copy-2" transform="translate(-232.000000, -187.000000)">
|
||||||
|
<g id="icon/sf-contain-b-copy-4" transform="translate(242.000000, 197.000000) scale(-1, 1) rotate(-270.000000) translate(-242.000000, -197.000000) translate(232.000000, 187.000000)">
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlink:href="#path-1-right"></use>
|
||||||
|
</mask>
|
||||||
|
<use id="Mask" fill="#FFFFFF" fill-rule="nonzero" xlink:href="#path-1-right"></use>
|
||||||
|
<g id="icon/sf-small/wizzard" mask="url(#mask-2)" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<g transform="translate(6.666667, 6.000000)" id="Group">
|
||||||
|
<g>
|
||||||
|
<polyline id="Path-3" stroke="#1B2559" stroke-width="2" points="0 4.83333333 3.33333333 8.16666667 6.66666667 4.83333333"></polyline>
|
||||||
|
<line x1="3.33333333" y1="0.25" x2="3.33333333" y2="6.5" id="Path-4" stroke="#1B2559" stroke-width="2"></line>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -77,7 +77,6 @@
|
||||||
"socket.io-client": "^2.0.3",
|
"socket.io-client": "^2.0.3",
|
||||||
"talisman": "^0.20.0",
|
"talisman": "^0.20.0",
|
||||||
"twilio": "^3.6.1",
|
"twilio": "^3.6.1",
|
||||||
"ua-parser-js": "^0.7.22",
|
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"web3": "^0.20.6",
|
"web3": "^0.20.6",
|
||||||
"winston": "^2.4.2",
|
"winston": "^2.4.2",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue