feat: add loyalty panel screen and structure

feat: add coupons table

feat: add coupons to schema

fix: coupon schema

feat: coupon table

feat: add coupon top button

feat: add first coupon button

feat: delete coupon

feat: coupon modal

fix: clear discount on modal close

fix: modal input formatting

feat: add new coupons

fix: button positioning

fix: remove loyalty panel sidebar

fix: coupon screen matching specs

fix: coupon modal

feat: send coupon data to machine on poll

fix: available coupons bool

feat: coupon endpoint

feat: transaction discount migration

feat: post-discount rates

refactor: bills

feat: version string

fix: bill saving on db

feat: coupon soft-delete

fix: coupon soft delete

fix: bill receiving

feat: remove cryptoAtoms update during tx

fix: tx trading

fix: bills

feat: start trades rework

fix: remove code

fix: code review
This commit is contained in:
Sérgio Salgado 2020-10-28 11:15:20 +00:00 committed by Josh Harvey
parent 64315cfd80
commit 7fe8799edc
18 changed files with 622 additions and 13 deletions

View file

@ -44,7 +44,7 @@ function insertNewBills (t, billRows, machineTx) {
if (_.isEmpty(bills)) return Promise.resolve([]) if (_.isEmpty(bills)) return Promise.resolve([])
const dbBills = _.map(cashInLow.massage, bills) const dbBills = _.map(cashInLow.massage, bills)
const columns = _.keys(dbBills[0]) const columns = ['id', 'fiat', 'fiat_code', 'crypto_code', 'cash_in_fee', 'cash_in_txs_id', 'device_time']
const sql = pgp.helpers.insert(dbBills, columns, 'bills') const sql = pgp.helpers.insert(dbBills, columns, 'bills')
return t.none(sql) return t.none(sql)

View file

@ -11,6 +11,9 @@ const PENDING_INTERVAL_MS = 60 * T.minutes
const massage = _.flow(_.omit(['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse']), const massage = _.flow(_.omit(['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse']),
convertBigNumFields, _.mapKeys(_.snakeCase)) convertBigNumFields, _.mapKeys(_.snakeCase))
const massageUpdates = _.flow(_.omit(['cryptoAtoms', 'direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse']),
convertBigNumFields, _.mapKeys(_.snakeCase))
module.exports = {toObj, upsert, insert, update, massage, isClearToSend} module.exports = {toObj, upsert, insert, update, massage, isClearToSend}
function convertBigNumFields (obj) { function convertBigNumFields (obj) {
@ -62,7 +65,7 @@ function insert (t, tx) {
function update (t, tx, changes) { function update (t, tx, changes) {
if (_.isEmpty(changes)) return Promise.resolve(tx) if (_.isEmpty(changes)) return Promise.resolve(tx)
const dbChanges = massage(changes) const dbChanges = isFinalTxStage(changes) ? massage(changes) : massageUpdates(changes)
const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') + const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [tx.id]) + ' returning *' pgp.as.format(' where id=$1', [tx.id]) + ' returning *'
@ -136,3 +139,7 @@ function isClearToSend (oldTx, newTx) {
(!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) && (!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) &&
(newTx.created > now - PENDING_INTERVAL_MS) (newTx.created > now - PENDING_INTERVAL_MS)
} }
function isFinalTxStage (txChanges) {
return txChanges.send
}

View file

@ -43,8 +43,8 @@ function post (machineTx, pi) {
}) })
} }
function registerTrades (pi, newBills) { function registerTrades (pi, r) {
_.forEach(bill => pi.buy(bill), newBills) _.forEach(bill => pi.buy(bill, r.tx), r.newBills)
} }
function logAction (rec, tx) { function logAction (rec, tx) {
@ -92,7 +92,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse) {
}) })
} }
registerTrades(pi, r.newBills) registerTrades(pi, r)
if (!cashInLow.isClearToSend(r.dbTx, r.tx)) return Promise.resolve({}) if (!cashInLow.isClearToSend(r.dbTx, r.tx)) return Promise.resolve({})

24
lib/coupon-manager.js Normal file
View file

@ -0,0 +1,24 @@
const db = require('./db')
const uuid = require('uuid')
function getAvailableCoupons () {
const sql = `select * from coupons where soft_deleted=false`
return db.any(sql)
}
function getCoupon (code) {
const sql = `select * from coupons where code=$1 and soft_deleted=false`
return db.oneOrNone(sql, [code])
}
function createCoupon (code, discount) {
const sql = `insert into coupons (id, code, discount) values ($1, $2, $3) returning *`
return db.one(sql, [uuid.v4(), code, discount])
}
function softDeleteCoupon (couponId) {
const sql = `update coupons set soft_deleted=true where id=$1`
return db.none(sql, [couponId])
}
module.exports = { getAvailableCoupons, getCoupon, createCoupon, softDeleteCoupon }

View file

@ -13,6 +13,7 @@ const settingsLoader = require('../../new-settings-loader')
// const tokenManager = require('../../token-manager') // const tokenManager = require('../../token-manager')
const blacklist = require('../../blacklist') const blacklist = require('../../blacklist')
const machineEventsByIdBatch = require("../../postgresql_interface").machineEventsByIdBatch const machineEventsByIdBatch = require("../../postgresql_interface").machineEventsByIdBatch
const couponManager = require('../../coupon-manager')
const serverVersion = require('../../../package.json').version const serverVersion = require('../../../package.json').version
@ -174,6 +175,13 @@ const typeDefs = gql`
ip_address: String ip_address: String
} }
type Coupon {
id: ID!
code: String!
discount: Int!
soft_deleted: Boolean
}
type Transaction { type Transaction {
id: ID! id: ID!
txClass: String! txClass: String!
@ -255,6 +263,13 @@ const typeDefs = gql`
config: JSONObject config: JSONObject
blacklist: [Blacklist] blacklist: [Blacklist]
# userTokens: [UserToken] # userTokens: [UserToken]
coupons: [Coupon]
}
type SupportLogsResponse {
id: ID!
timestamp: Date!
deviceId: ID
} }
enum MachineAction { enum MachineAction {
@ -279,6 +294,8 @@ const typeDefs = gql`
# revokeToken(token: String!): UserToken # revokeToken(token: String!): UserToken
deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist
createCoupon(code: String!, discount: Int!): Coupon
softDeleteCoupon(couponId: ID!): Coupon
} }
` `
@ -329,6 +346,7 @@ const resolvers = {
accounts: () => settingsLoader.loadAccounts(), accounts: () => settingsLoader.loadAccounts(),
blacklist: () => blacklist.getBlacklist(), blacklist: () => blacklist.getBlacklist(),
// userTokens: () => tokenManager.getTokenList() // userTokens: () => tokenManager.getTokenList()
coupons: () => couponManager.getAvailableCoupons()
}, },
Mutation: { Mutation: {
machineAction: (...[, { deviceId, action, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cassette1, cassette2, newName }), machineAction: (...[, { deviceId, action, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cassette1, cassette2, newName }),
@ -351,6 +369,8 @@ const resolvers = {
insertBlacklistRow: (...[, { cryptoCode, address }]) => insertBlacklistRow: (...[, { cryptoCode, address }]) =>
blacklist.insertIntoBlacklist(cryptoCode, address), blacklist.insertIntoBlacklist(cryptoCode, address),
// revokeToken: (...[, { token }]) => tokenManager.revokeToken(token) // revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
createCoupon: (...[, { code, discount }]) => couponManager.createCoupon(code, discount),
softDeleteCoupon: (...[, { couponId }]) => couponManager.softDeleteCoupon(couponId)
} }
} }

View file

@ -190,6 +190,11 @@ function plugins (settings, deviceId) {
.then(row => row.id) .then(row => row.id)
} }
function fetchCurrentAvailableCoupons () {
const sql = `select * from coupons where soft_deleted=false`
return db.any(sql).then(v => v.length)
}
function mapCoinSettings (coinParams) { function mapCoinSettings (coinParams) {
const cryptoCode = coinParams[0] const cryptoCode = coinParams[0]
const cryptoNetwork = coinParams[1] const cryptoNetwork = coinParams[1]
@ -222,11 +227,13 @@ function plugins (settings, deviceId) {
const testnetPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c)) const testnetPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c))
const pingPromise = recordPing(deviceTime, machineVersion, machineModel) const pingPromise = recordPing(deviceTime, machineVersion, machineModel)
const currentConfigVersionPromise = fetchCurrentConfigVersion() const currentConfigVersionPromise = fetchCurrentConfigVersion()
const currentAvailableCouponsPromise = fetchCurrentAvailableCoupons()
const promises = [ const promises = [
buildAvailableCassettes(), buildAvailableCassettes(),
pingPromise, pingPromise,
currentConfigVersionPromise currentConfigVersionPromise,
currentAvailableCouponsPromise
].concat(tickerPromises, balancePromises, testnetPromises) ].concat(tickerPromises, balancePromises, testnetPromises)
return Promise.all(promises) return Promise.all(promises)
@ -236,16 +243,18 @@ function plugins (settings, deviceId) {
const cryptoCodesCount = cryptoCodes.length const cryptoCodesCount = cryptoCodes.length
const tickers = arr.slice(3, cryptoCodesCount + 3) const tickers = arr.slice(3, cryptoCodesCount + 3)
const balances = arr.slice(cryptoCodesCount + 3, 2 * cryptoCodesCount + 3) const balances = arr.slice(cryptoCodesCount + 3, 2 * cryptoCodesCount + 3)
const testNets = arr.slice(2 * cryptoCodesCount + 3) const testNets = arr.slice(2 * cryptoCodesCount + 3, arr.length - 1)
const coinParams = _.zip(cryptoCodes, testNets) const coinParams = _.zip(cryptoCodes, testNets)
const coinsWithoutRate = _.map(mapCoinSettings, coinParams) const coinsWithoutRate = _.map(mapCoinSettings, coinParams)
const areThereAvailableCoupons = arr[arr.length - 1] > 0
return { return {
cassettes, cassettes,
rates: buildRates(tickers), rates: buildRates(tickers),
balances: buildBalances(balances), balances: buildBalances(balances),
coins: _.zipWith(_.assign, coinsWithoutRate, tickers), coins: _.zipWith(_.assign, coinsWithoutRate, tickers),
configVersion configVersion,
areThereAvailableCoupons
} }
}) })
} }
@ -440,18 +449,18 @@ function plugins (settings, deviceId) {
* Trader functions * Trader functions
*/ */
function buy (rec) { function buy (rec, tx) {
return buyAndSell(rec, true) return buyAndSell(rec, true, tx)
} }
function sell (rec) { function sell (rec) {
return buyAndSell(rec, false) return buyAndSell(rec, false)
} }
function buyAndSell (rec, doBuy) { function buyAndSell (rec, doBuy, tx) {
const cryptoCode = rec.cryptoCode const cryptoCode = rec.cryptoCode
const fiatCode = rec.fiatCode const fiatCode = rec.fiatCode
const cryptoAtoms = doBuy ? rec.cryptoAtoms : rec.cryptoAtoms.neg() const cryptoAtoms = doBuy ? fiatToCrypto(tx, rec) : rec.cryptoAtoms.neg()
const market = [fiatCode, cryptoCode].join('') const market = [fiatCode, cryptoCode].join('')
@ -467,6 +476,38 @@ function plugins (settings, deviceId) {
}) })
} }
function truncateCrypto (cryptoAtoms, cryptoCode) {
const DECIMAL_PLACES = 3
if (cryptoAtoms.eq(0)) return cryptoAtoms
const scale = 5 // TODO: change this to coins.displayScale when coins have that attribute
const scaleFactor = BN(10).pow(scale)
return BN(cryptoAtoms).truncated().div(scaleFactor)
.round(DECIMAL_PLACES).times(scaleFactor)
}
function fiatToCrypto (tx, rec) {
const usableFiat = rec.fiat - rec.cashInFee
const commissions = configManager.getCommissions(tx.cryptoCode, deviceId, settings.config)
const tickerRate = BN(tx.rawTickerPrice)
const discount = getDiscountRate(tx.discount, commissions[tx.direction])
const rate = tickerRate.mul(discount).round(5)
const unitScale = coinUtils.getCryptoCurrency(tx.cryptoCode).unitScale
const unitScaleFactor = BN(10).pow(unitScale)
return truncateCrypto(BN(usableFiat).div(rate.div(unitScaleFactor)), tx.cryptoCode)
}
function getDiscountRate (discount, commission) {
const bnDiscount = discount ? BN(discount) : BN(0)
const bnCommission = BN(commission)
const percentageDiscount = BN(1).sub(bnDiscount.div(100))
const percentageCommission = bnCommission.div(100)
return BN(1).add(percentageDiscount.mul(percentageCommission))
}
function consolidateTrades (cryptoCode, fiatCode) { function consolidateTrades (cryptoCode, fiatCode) {
const market = [fiatCode, cryptoCode].join('') const market = [fiatCode, cryptoCode].join('')

View file

@ -25,6 +25,8 @@ const E = require('./error')
const customers = require('./customers') const customers = require('./customers')
const logs = require('./logs') const logs = require('./logs')
const compliance = require('./compliance') const compliance = require('./compliance')
const couponManager = require('./coupon-manager')
const BN = require('./bn')
const version = require('../package.json').version const version = require('../package.json').version
@ -213,6 +215,37 @@ function verifyTx (req, res, next) {
.catch(next) .catch(next)
} }
function verifyCoupon (req, res, next) {
couponManager.getCoupon(req.body.codeInput)
.then(coupon => {
if (!coupon) return next()
const transaction = req.body.tx
const commissions = configManager.getCommissions(transaction.cryptoCode, req.deviceId, req.settings.config)
const tickerRate = BN(transaction.rawTickerPrice)
const discount = getDiscountRate(coupon.discount, commissions[transaction.direction])
const rates = {
[transaction.cryptoCode]: {
[transaction.direction]: (transaction.direction === 'cashIn')
? tickerRate.mul(discount).round(5)
: tickerRate.div(discount).round(5)
}
}
respond(req, res, {
coupon: coupon,
newRates: rates
})
})
.catch(next)
}
function getDiscountRate (discount, commission) {
const percentageDiscount = BN(1).sub(BN(discount).div(100))
const percentageCommission = BN(commission).div(100)
return BN(1).add(percentageDiscount.mul(percentageCommission))
}
function addOrUpdateCustomer (req) { function addOrUpdateCustomer (req) {
const customerData = req.body const customerData = req.body
const machineVersion = req.query.version const machineVersion = req.query.version
@ -450,7 +483,8 @@ const configRequiredRoutes = [
'/event', '/event',
'/phone_code', '/phone_code',
'/customer', '/customer',
'/tx' '/tx',
'/verify_coupon'
] ]
const app = express() const app = express()
@ -477,6 +511,7 @@ app.post('/state', stateChange)
app.post('/verify_user', verifyUser) app.post('/verify_user', verifyUser)
app.post('/verify_transaction', verifyTx) app.post('/verify_transaction', verifyTx)
app.post('/verify_coupon', verifyCoupon)
app.post('/phone_code', getCustomerWithPhoneCode) app.post('/phone_code', getCustomerWithPhoneCode)
app.patch('/customer/:id', updateCustomer) app.patch('/customer/:id', updateCustomer)

View file

@ -0,0 +1,19 @@
var db = require('./db')
exports.up = function (next) {
const sql =
[
`create table coupons (
id uuid primary key,
code text not null,
discount smallint not null,
soft_deleted boolean default false )`,
`create unique index uq_code on coupons using btree(code) where not soft_deleted`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,14 @@
const db = require('./db')
exports.up = function (next) {
var sql = [
'alter table cash_in_txs add column discount smallint',
'alter table cash_out_txs add column discount smallint'
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,15 @@
const db = require('./db')
exports.up = function (next) {
var sql = [
'ALTER TABLE bills DROP COLUMN crypto_atoms',
'ALTER TABLE bills DROP COLUMN cash_in_fee_crypto',
'ALTER TABLE bills DROP COLUMN crypto_atoms_after_fee'
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,16 @@
const db = require('./db')
exports.up = function (next) {
var sql = [
'alter table trades add column tx_in_id uuid unique',
'alter table trades add constraint fk_tx_in foreign key (tx_in_id) references cash_in_txs (id)',
'alter table trades add column tx_out_id uuid unique',
'alter table trades add constraint fk_tx_out foreign key (tx_in_id) references cash_out_txs (id)'
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,138 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Box } from '@material-ui/core'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState } from 'react'
import { Link, Button, IconButton } from 'src/components/buttons'
import TitleSection from 'src/components/layout/TitleSection'
import DataTable from 'src/components/tables/DataTable'
import { H2, TL1 } from 'src/components/typography'
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
import styles from './CouponCodes.styles'
import CouponCodesModal from './CouponCodesModal'
const useStyles = makeStyles(styles)
const GET_COUPONS = gql`
query coupons {
coupons {
id
code
discount
soft_deleted
}
}
`
const SOFT_DELETE_COUPON = gql`
mutation softDeleteCoupon($couponId: ID!) {
softDeleteCoupon(couponId: $couponId) {
id
}
}
`
const CREATE_COUPON = gql`
mutation createCoupon($code: String!, $discount: Int!) {
createCoupon(code: $code, discount: $discount) {
id
code
discount
soft_deleted
}
}
`
const Coupons = () => {
const classes = useStyles()
const [showModal, setShowModal] = useState(false)
const toggleModal = () => setShowModal(!showModal)
const { data: couponResponse, loading } = useQuery(GET_COUPONS)
const [softDeleteCoupon] = useMutation(SOFT_DELETE_COUPON, {
refetchQueries: () => ['coupons']
})
const [createCoupon] = useMutation(CREATE_COUPON, {
refetchQueries: () => ['coupons']
})
const addCoupon = (code, discount) => {
createCoupon({ variables: { code: code, discount: discount } })
}
const elements = [
{
header: 'Coupon Code',
width: 300,
textAlign: 'left',
size: 'sm',
view: t => t.code
},
{
header: 'Discount',
width: 220,
textAlign: 'left',
size: 'sm',
view: t => (
<>
<TL1 inline>{t.discount}</TL1> % in commissions
</>
)
},
{
header: 'Delete',
width: 100,
textAlign: 'center',
size: 'sm',
view: t => (
<IconButton
onClick={() => {
softDeleteCoupon({ variables: { couponId: t.id } })
}}>
<DeleteIcon />
</IconButton>
)
}
]
return (
<>
<TitleSection title="Discount Coupons"></TitleSection>
{!loading && !R.isEmpty(couponResponse.coupons) && (
<Box
marginBottom={4}
marginTop={-5}
className={classes.tableWidth}
display="flex"
justifyContent="flex-end">
<Link color="primary" onClick={toggleModal}>
Add new coupon
</Link>
</Box>
)}
{!loading && !R.isEmpty(couponResponse.coupons) && (
<DataTable
elements={elements}
data={R.path(['coupons'])(couponResponse)}
/>
)}
{!loading && R.isEmpty(couponResponse.coupons) && (
<Box display="flex" alignItems="left" flexDirection="column">
<H2>Currently, there are no active coupon codes on your network.</H2>
<Button onClick={toggleModal}>Add coupon</Button>
</Box>
)}
<CouponCodesModal
showModal={showModal}
toggleModal={toggleModal}
addCoupon={addCoupon}
/>
</>
)
}
export default Coupons

View file

@ -0,0 +1,37 @@
import { spacer, fontPrimary, primaryColor } from 'src/styling/variables'
export default {
footer: {
margin: [['auto', 0, spacer * 3, 'auto']]
},
modalTitle: {
marginTop: -5,
color: primaryColor,
fontFamily: fontPrimary
},
modalLabel1: {
marginTop: 20
},
modalLabel2Wrapper: {
marginTop: 40,
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start'
},
discountInput: {
display: 'flex',
height: '100%',
flexDirection: 'row',
alignItems: 'flex-start'
},
inputLabel: {
color: primaryColor,
fontFamily: fontPrimary,
fontSize: 24,
marginLeft: 8,
marginTop: 15
},
tableWidth: {
width: 620
}
}

View file

@ -0,0 +1,125 @@
import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda'
import React, { useState } from 'react'
import Modal from 'src/components/Modal'
import Tooltip from 'src/components/Tooltip'
import { Button } from 'src/components/buttons'
import { TextInput, NumberInput } from 'src/components/inputs/base'
import { H1, H3, TL1, P } from 'src/components/typography'
import styles from './CouponCodes.styles'
const useStyles = makeStyles(styles)
const CouponCodesModal = ({ showModal, toggleModal, addCoupon }) => {
const classes = useStyles()
const [codeField, setCodeField] = useState('')
const [discountField, setDiscountField] = useState('')
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 (
<>
{showModal && (
<Modal
closeOnBackdropClick={true}
width={600}
height={500}
handleClose={handleClose}
open={true}>
<H1 className={classes.modalTitle}>Add coupon code discount</H1>
<H3 className={classes.modalLabel1}>Coupon code name</H3>
<TextInput
error={invalidCode}
name="coupon-code"
autoFocus
id="coupon-code"
type="text"
size="lg"
width={338}
onChange={handleCodeChange}
value={codeField}
inputProps={{ style: { textTransform: 'uppercase' } }}
/>
<div className={classes.modalLabel2Wrapper}>
<H3 className={classes.modalLabel2}>Define discount rate</H3>
<Tooltip width={304}>
<P>
The discount rate inserted will be applied to the commissions of
all transactions performed with this respective coupon code.
</P>
<P>
(It should be a number between 0 (zero) and 100 (one hundred)).
</P>
</Tooltip>
</div>
<div className={classes.discountInput}>
<NumberInput
error={invalidDiscount}
name="coupon-discount"
id="coupon-discount"
size="lg"
width={50}
onChange={handleDiscountChange}
value={discountField}
decimalScale={0}
className={classes.discountInputField}
/>
<TL1 inline className={classes.inputLabel}>
%
</TL1>
</div>
<div className={classes.footer}>
<Button onClick={handleAddCoupon}>Add coupon</Button>
</div>
</Modal>
)}
</>
)
}
export default CouponCodesModal

View file

@ -0,0 +1,7 @@
import React from 'react'
const IndividualDiscounts = () => {
return <></>
}
export default IndividualDiscounts

View file

@ -0,0 +1,7 @@
import React from 'react'
const LoyaltyDiscounts = () => {
return <></>
}
export default LoyaltyDiscounts

View file

@ -0,0 +1,97 @@
import { makeStyles } from '@material-ui/core'
import Grid from '@material-ui/core/Grid'
import React from 'react'
import {
Route,
Switch,
Redirect,
useLocation,
useHistory
} from 'react-router-dom'
import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection'
import CouponCodes from './CouponCodes'
import IndividualDiscounts from './IndividualDiscounts'
import LoyaltyDiscounts from './LoyaltyDiscounts'
const styles = {
grid: {
flex: 1,
height: '100%'
},
content: {
flex: 1,
marginLeft: 48,
paddingTop: 15
}
}
const useStyles = makeStyles(styles)
const innerRoutes = [
{
key: 'individual-discounts',
label: 'Individual Discounts',
route: '/compliance/loyalty/individual-discounts',
component: IndividualDiscounts
},
{
key: 'loyalty-discounts',
label: 'Loyalty Discounts',
route: '/compliance/loyalty/discounts',
component: LoyaltyDiscounts
},
{
key: 'coupon-codes',
label: 'Coupon Codes',
route: '/compliance/loyalty/coupons',
component: CouponCodes
}
]
const Routes = ({ wizard }) => (
<Switch>
<Redirect
exact
from="/compliance/loyalty"
to="/compliance/loyalty/individual-discounts"
/>
<Route exact path="/" />
{innerRoutes.map(({ route, component: Page, key }) => (
<Route path={route} key={key}>
<Page name={key} wizard={wizard} />
</Route>
))}
</Switch>
)
const LoyaltyPanel = ({ wizard = false }) => {
const classes = useStyles()
const history = useHistory()
const location = useLocation()
const isSelected = it => location.pathname === it.route
const onClick = it => history.push(it.route)
return (
<>
<TitleSection title="Loyalty Panel"></TitleSection>
<Grid container className={classes.grid}>
<Sidebar
data={innerRoutes}
isSelected={isSelected}
displayName={it => it.label}
onClick={onClick}
/>
<div className={classes.content}>
<Routes wizard={wizard} />
</div>
</Grid>
</>
)
}
export default LoyaltyPanel

View file

@ -21,6 +21,7 @@ import ConfigMigration from 'src/pages/ConfigMigration'
import { Customers, CustomerProfile } from 'src/pages/Customers' import { Customers, CustomerProfile } from 'src/pages/Customers'
import Funding from 'src/pages/Funding' import Funding from 'src/pages/Funding'
import Locales from 'src/pages/Locales' import Locales from 'src/pages/Locales'
import Coupons from 'src/pages/LoyaltyPanel/CouponCodes'
import MachineLogs from 'src/pages/MachineLogs' import MachineLogs from 'src/pages/MachineLogs'
import CashCassettes from 'src/pages/Maintenance/CashCassettes' import CashCassettes from 'src/pages/Maintenance/CashCassettes'
import MachineStatus from 'src/pages/Maintenance/MachineStatus' import MachineStatus from 'src/pages/Maintenance/MachineStatus'
@ -208,6 +209,12 @@ const tree = [
route: '/compliance/blacklist', route: '/compliance/blacklist',
component: Blacklist component: Blacklist
}, },
{
key: 'discount-coupons',
label: 'Discount Coupons',
route: '/compliance/loyalty/coupons',
component: Coupons
},
{ {
key: 'customer', key: 'customer',
route: '/compliance/customer/:id', route: '/compliance/customer/:id',