fix: coupon form, styling and server handling
This commit is contained in:
parent
5045821593
commit
9d4c4041dc
7 changed files with 138 additions and 118 deletions
|
|
@ -8,10 +8,13 @@ const E = require('../error')
|
||||||
|
|
||||||
const PENDING_INTERVAL_MS = 60 * T.minutes
|
const PENDING_INTERVAL_MS = 60 * T.minutes
|
||||||
|
|
||||||
const massage = _.flow(_.omit(['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse']),
|
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse']
|
||||||
|
const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms')
|
||||||
|
|
||||||
|
const massage = _.flow(_.omit(massageFields),
|
||||||
convertBigNumFields, _.mapKeys(_.snakeCase))
|
convertBigNumFields, _.mapKeys(_.snakeCase))
|
||||||
|
|
||||||
const massageUpdates = _.flow(_.omit(['cryptoAtoms', 'direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse']),
|
const massageUpdates = _.flow(_.omit(massageUpdateFields),
|
||||||
convertBigNumFields, _.mapKeys(_.snakeCase))
|
convertBigNumFields, _.mapKeys(_.snakeCase))
|
||||||
|
|
||||||
module.exports = {toObj, upsert, insert, update, massage, isClearToSend}
|
module.exports = {toObj, upsert, insert, update, massage, isClearToSend}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ const CRYPTO_CURRENCIES = [
|
||||||
configFile: 'bitcoin.conf',
|
configFile: 'bitcoin.conf',
|
||||||
daemon: 'bitcoind',
|
daemon: 'bitcoind',
|
||||||
defaultPort: 8332,
|
defaultPort: 8332,
|
||||||
unitScale: 8
|
unitScale: 8,
|
||||||
|
displayScale: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cryptoCode: 'ETH',
|
cryptoCode: 'ETH',
|
||||||
|
|
@ -21,7 +22,8 @@ const CRYPTO_CURRENCIES = [
|
||||||
configFile: 'geth.conf',
|
configFile: 'geth.conf',
|
||||||
daemon: 'geth',
|
daemon: 'geth',
|
||||||
defaultPort: 8545,
|
defaultPort: 8545,
|
||||||
unitScale: 18
|
unitScale: 18,
|
||||||
|
displayScale: 15
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cryptoCode: 'LTC',
|
cryptoCode: 'LTC',
|
||||||
|
|
@ -30,7 +32,8 @@ const CRYPTO_CURRENCIES = [
|
||||||
configFile: 'litecoin.conf',
|
configFile: 'litecoin.conf',
|
||||||
daemon: 'litecoind',
|
daemon: 'litecoind',
|
||||||
defaultPort: 9332,
|
defaultPort: 9332,
|
||||||
unitScale: 8
|
unitScale: 8,
|
||||||
|
displayScale: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cryptoCode: 'DASH',
|
cryptoCode: 'DASH',
|
||||||
|
|
@ -39,7 +42,8 @@ const CRYPTO_CURRENCIES = [
|
||||||
configFile: 'dash.conf',
|
configFile: 'dash.conf',
|
||||||
daemon: 'dashd',
|
daemon: 'dashd',
|
||||||
defaultPort: 9998,
|
defaultPort: 9998,
|
||||||
unitScale: 8
|
unitScale: 8,
|
||||||
|
displayScale: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cryptoCode: 'ZEC',
|
cryptoCode: 'ZEC',
|
||||||
|
|
@ -48,7 +52,8 @@ const CRYPTO_CURRENCIES = [
|
||||||
configFile: 'zcash.conf',
|
configFile: 'zcash.conf',
|
||||||
daemon: 'zcashd',
|
daemon: 'zcashd',
|
||||||
defaultPort: 8232,
|
defaultPort: 8232,
|
||||||
unitScale: 8
|
unitScale: 8,
|
||||||
|
displayScale: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cryptoCode: 'BCH',
|
cryptoCode: 'BCH',
|
||||||
|
|
@ -57,7 +62,8 @@ const CRYPTO_CURRENCIES = [
|
||||||
configFile: 'bitcoincash.conf',
|
configFile: 'bitcoincash.conf',
|
||||||
daemon: 'bitcoincashd',
|
daemon: 'bitcoincashd',
|
||||||
defaultPort: 8335,
|
defaultPort: 8335,
|
||||||
unitScale: 8
|
unitScale: 8,
|
||||||
|
displayScale: 5
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ function truncateCrypto (cryptoAtoms, cryptoCode) {
|
||||||
const DECIMAL_PLACES = 3
|
const DECIMAL_PLACES = 3
|
||||||
if (cryptoAtoms.eq(0)) return cryptoAtoms
|
if (cryptoAtoms.eq(0)) return cryptoAtoms
|
||||||
|
|
||||||
const scale = 5 // TODO: change this to coins.displayScale when coins have that attribute
|
const scale = coinUtils.getCryptoCurrency(cryptoCode).displayScale
|
||||||
const scaleFactor = BN(10).pow(scale)
|
const scaleFactor = BN(10).pow(scale)
|
||||||
|
|
||||||
return BN(cryptoAtoms).truncated().div(scaleFactor)
|
return BN(cryptoAtoms).truncated().div(scaleFactor)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ function createCoupon (code, discount) {
|
||||||
return db.one(sql, [uuid.v4(), code, discount])
|
return db.one(sql, [uuid.v4(), code, discount])
|
||||||
}
|
}
|
||||||
|
|
||||||
function softDeleteCoupon (couponId) {
|
function deleteCoupon (couponId) {
|
||||||
const sql = `UPDATE coupons SET soft_deleted=true WHERE id=$1`
|
const sql = `UPDATE coupons SET soft_deleted=true WHERE id=$1`
|
||||||
return db.none(sql, [couponId])
|
return db.none(sql, [couponId])
|
||||||
}
|
}
|
||||||
|
|
@ -26,4 +26,4 @@ function getNumberOfAvailableCoupons () {
|
||||||
return db.one(sql).then(res => res.count)
|
return db.one(sql).then(res => res.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getAvailableCoupons, getCoupon, createCoupon, softDeleteCoupon, getNumberOfAvailableCoupons }
|
module.exports = { getAvailableCoupons, getCoupon, createCoupon, deleteCoupon, getNumberOfAvailableCoupons }
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,12 @@ const Coupons = () => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
|
const [errorMsg, setErrorMsg] = useState(null)
|
||||||
const toggleModal = () => setShowModal(!showModal)
|
const toggleModal = () => setShowModal(!showModal)
|
||||||
|
|
||||||
const { data: couponResponse, loading } = useQuery(GET_COUPONS)
|
const { data: couponResponse, loading } = useQuery(GET_COUPONS)
|
||||||
|
|
||||||
const [softDeleteCoupon] = useMutation(DELETE_COUPON, {
|
const [deleteCoupon] = useMutation(DELETE_COUPON, {
|
||||||
refetchQueries: () => ['coupons']
|
refetchQueries: () => ['coupons']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -59,8 +60,22 @@ const Coupons = () => {
|
||||||
refetchQueries: () => ['coupons']
|
refetchQueries: () => ['coupons']
|
||||||
})
|
})
|
||||||
|
|
||||||
const addCoupon = (code, discount) => {
|
const addCoupon = async (code, discount) => {
|
||||||
createCoupon({ variables: { code: code, discount: discount } })
|
setErrorMsg(null)
|
||||||
|
const res = await createCoupon({
|
||||||
|
variables: { code: code, discount: discount }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.errors) {
|
||||||
|
return setShowModal(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateCouponError = res.errors.some(e => {
|
||||||
|
return e.message.includes('duplicate')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (duplicateCouponError)
|
||||||
|
setErrorMsg('There is already a coupon with that code!')
|
||||||
}
|
}
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
|
|
@ -90,7 +105,7 @@ const Coupons = () => {
|
||||||
view: t => (
|
view: t => (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
softDeleteCoupon({ variables: { couponId: t.id } })
|
deleteCoupon({ variables: { couponId: t.id } })
|
||||||
}}>
|
}}>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
@ -127,7 +142,11 @@ const Coupons = () => {
|
||||||
)}
|
)}
|
||||||
<CouponCodesModal
|
<CouponCodesModal
|
||||||
showModal={showModal}
|
showModal={showModal}
|
||||||
toggleModal={toggleModal}
|
onClose={() => {
|
||||||
|
setErrorMsg(null)
|
||||||
|
setShowModal(false)
|
||||||
|
}}
|
||||||
|
errorMsg={errorMsg}
|
||||||
addCoupon={addCoupon}
|
addCoupon={addCoupon}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { spacer, fontPrimary, primaryColor } from 'src/styling/variables'
|
import {
|
||||||
|
spacer,
|
||||||
|
fontPrimary,
|
||||||
|
primaryColor,
|
||||||
|
errorColor
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
footer: {
|
footer: {
|
||||||
margin: [['auto', 0, spacer * 3, 'auto']]
|
margin: [['auto', 0, spacer * 3, 'auto']]
|
||||||
},
|
},
|
||||||
modalTitle: {
|
|
||||||
marginTop: -5,
|
|
||||||
color: primaryColor,
|
|
||||||
fontFamily: fontPrimary
|
|
||||||
},
|
|
||||||
modalLabel1: {
|
modalLabel1: {
|
||||||
marginTop: 20
|
marginTop: 20
|
||||||
},
|
},
|
||||||
|
|
@ -20,7 +20,6 @@ const styles = {
|
||||||
},
|
},
|
||||||
discountInput: {
|
discountInput: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
height: '100%',
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-start'
|
alignItems: 'flex-start'
|
||||||
},
|
},
|
||||||
|
|
@ -33,6 +32,14 @@ const styles = {
|
||||||
},
|
},
|
||||||
tableWidth: {
|
tableWidth: {
|
||||||
width: 620
|
width: 620
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: errorColor
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,121 +1,106 @@
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import { Form, Formik, Field } from 'formik'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { Tooltip } from 'src/components/Tooltip'
|
import { Tooltip } from 'src/components/Tooltip'
|
||||||
import { Button } from 'src/components/buttons'
|
import { Button } from 'src/components/buttons'
|
||||||
import { TextInput, NumberInput } from 'src/components/inputs/base'
|
import { TextInput, NumberInput } from 'src/components/inputs/formik'
|
||||||
import { H1, H3, TL1, P } from 'src/components/typography'
|
import { H3, TL1, P } from 'src/components/typography'
|
||||||
|
|
||||||
import styles from './CouponCodes.styles'
|
import styles from './CouponCodes.styles'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const CouponCodesModal = ({ showModal, toggleModal, addCoupon }) => {
|
const initialValues = {
|
||||||
|
code: '',
|
||||||
|
discount: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
code: Yup.string()
|
||||||
|
.required()
|
||||||
|
.trim()
|
||||||
|
.max(25),
|
||||||
|
discount: Yup.number()
|
||||||
|
.required()
|
||||||
|
.min(0)
|
||||||
|
.max(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
const CouponCodesModal = ({ showModal, onClose, errorMsg, addCoupon }) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const [codeField, setCodeField] = useState('')
|
const handleAddCoupon = (code, discount) => {
|
||||||
const [discountField, setDiscountField] = useState('')
|
addCoupon(R.toUpper(code), parseInt(discount))
|
||||||
const [invalidCode, setInvalidCode] = useState(false)
|
|
||||||
const [invalidDiscount, setInvalidDiscount] = useState(false)
|
|
||||||
|
|
||||||
const handleCodeChange = event => {
|
|
||||||
if (event.target.value === '') {
|
|
||||||
setInvalidCode(false)
|
|
||||||
}
|
|
||||||
setCodeField(event.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDiscountChange = event => {
|
|
||||||
if (event.target.value === '') {
|
|
||||||
setInvalidDiscount(false)
|
|
||||||
}
|
|
||||||
setDiscountField(event.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setCodeField('')
|
|
||||||
setDiscountField('')
|
|
||||||
setInvalidCode(false)
|
|
||||||
setInvalidDiscount(false)
|
|
||||||
toggleModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddCoupon = () => {
|
|
||||||
if (codeField.trim() === '') {
|
|
||||||
setInvalidCode(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!validDiscount(discountField)) {
|
|
||||||
setInvalidDiscount(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (codeField.trim() !== '' && validDiscount(discountField)) {
|
|
||||||
addCoupon(R.toUpper(codeField.trim()), parseInt(discountField))
|
|
||||||
handleClose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validDiscount = discount => {
|
|
||||||
const parsedDiscount = parseInt(discount)
|
|
||||||
return parsedDiscount >= 0 && parsedDiscount <= 100
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<Modal
|
<Modal
|
||||||
|
title="Add coupon code discount"
|
||||||
closeOnBackdropClick={true}
|
closeOnBackdropClick={true}
|
||||||
width={600}
|
width={600}
|
||||||
height={500}
|
height={500}
|
||||||
handleClose={handleClose}
|
handleClose={onClose}
|
||||||
open={true}>
|
open={true}>
|
||||||
<H1 className={classes.modalTitle}>Add coupon code discount</H1>
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={({ code, discount }, { resetForm }) => {
|
||||||
|
handleAddCoupon(code, discount)
|
||||||
|
resetForm()
|
||||||
|
}}>
|
||||||
|
<Form id="coupon-form" className={classes.form}>
|
||||||
<H3 className={classes.modalLabel1}>Coupon code name</H3>
|
<H3 className={classes.modalLabel1}>Coupon code name</H3>
|
||||||
<TextInput
|
<Field
|
||||||
error={invalidCode}
|
name="code"
|
||||||
name="coupon-code"
|
|
||||||
autoFocus
|
autoFocus
|
||||||
id="coupon-code"
|
|
||||||
type="text"
|
|
||||||
size="lg"
|
size="lg"
|
||||||
|
autoComplete="off"
|
||||||
width={338}
|
width={338}
|
||||||
onChange={handleCodeChange}
|
|
||||||
value={codeField}
|
|
||||||
inputProps={{ style: { textTransform: 'uppercase' } }}
|
inputProps={{ style: { textTransform: 'uppercase' } }}
|
||||||
|
component={TextInput}
|
||||||
/>
|
/>
|
||||||
<div className={classes.modalLabel2Wrapper}>
|
<div className={classes.modalLabel2Wrapper}>
|
||||||
<H3 className={classes.modalLabel2}>Define discount rate</H3>
|
<H3 className={classes.modalLabel2}>Define discount rate</H3>
|
||||||
<Tooltip width={304}>
|
<Tooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
The discount rate inserted will be applied to the commissions of
|
The discount rate inserted will be applied to the
|
||||||
all transactions performed with this respective coupon code.
|
commissions of all transactions performed with this
|
||||||
|
respective coupon code.
|
||||||
</P>
|
</P>
|
||||||
<P>
|
<P>
|
||||||
(It should be a number between 0 (zero) and 100 (one hundred)).
|
(It should be a number between 0 (zero) and 100 (one
|
||||||
|
hundred)).
|
||||||
</P>
|
</P>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.discountInput}>
|
<div className={classes.discountInput}>
|
||||||
<NumberInput
|
<Field
|
||||||
error={invalidDiscount}
|
name="discount"
|
||||||
name="coupon-discount"
|
|
||||||
id="coupon-discount"
|
|
||||||
size="lg"
|
size="lg"
|
||||||
|
autoComplete="off"
|
||||||
width={50}
|
width={50}
|
||||||
onChange={handleDiscountChange}
|
|
||||||
value={discountField}
|
|
||||||
decimalScale={0}
|
decimalScale={0}
|
||||||
className={classes.discountInputField}
|
className={classes.discountInputField}
|
||||||
|
component={NumberInput}
|
||||||
/>
|
/>
|
||||||
<TL1 inline className={classes.inputLabel}>
|
<TL1 inline className={classes.inputLabel}>
|
||||||
%
|
%
|
||||||
</TL1>
|
</TL1>
|
||||||
</div>
|
</div>
|
||||||
|
<span className={classes.error}>{errorMsg}</span>
|
||||||
<div className={classes.footer}>
|
<div className={classes.footer}>
|
||||||
<Button onClick={handleAddCoupon}>Add coupon</Button>
|
<Button type="submit" form="coupon-form">
|
||||||
|
Add coupon
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue