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:
parent
64315cfd80
commit
7fe8799edc
18 changed files with 622 additions and 13 deletions
|
|
@ -44,7 +44,7 @@ function insertNewBills (t, billRows, machineTx) {
|
|||
if (_.isEmpty(bills)) return Promise.resolve([])
|
||||
|
||||
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')
|
||||
|
||||
return t.none(sql)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ const PENDING_INTERVAL_MS = 60 * T.minutes
|
|||
const massage = _.flow(_.omit(['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse']),
|
||||
convertBigNumFields, _.mapKeys(_.snakeCase))
|
||||
|
||||
const massageUpdates = _.flow(_.omit(['cryptoAtoms', 'direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse']),
|
||||
convertBigNumFields, _.mapKeys(_.snakeCase))
|
||||
|
||||
module.exports = {toObj, upsert, insert, update, massage, isClearToSend}
|
||||
|
||||
function convertBigNumFields (obj) {
|
||||
|
|
@ -62,7 +65,7 @@ function insert (t, tx) {
|
|||
function update (t, tx, changes) {
|
||||
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') +
|
||||
pgp.as.format(' where id=$1', [tx.id]) + ' returning *'
|
||||
|
||||
|
|
@ -136,3 +139,7 @@ function isClearToSend (oldTx, newTx) {
|
|||
(!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) &&
|
||||
(newTx.created > now - PENDING_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function isFinalTxStage (txChanges) {
|
||||
return txChanges.send
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ function post (machineTx, pi) {
|
|||
})
|
||||
}
|
||||
|
||||
function registerTrades (pi, newBills) {
|
||||
_.forEach(bill => pi.buy(bill), newBills)
|
||||
function registerTrades (pi, r) {
|
||||
_.forEach(bill => pi.buy(bill, r.tx), r.newBills)
|
||||
}
|
||||
|
||||
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({})
|
||||
|
||||
|
|
|
|||
24
lib/coupon-manager.js
Normal file
24
lib/coupon-manager.js
Normal 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 }
|
||||
|
|
@ -13,6 +13,7 @@ const settingsLoader = require('../../new-settings-loader')
|
|||
// const tokenManager = require('../../token-manager')
|
||||
const blacklist = require('../../blacklist')
|
||||
const machineEventsByIdBatch = require("../../postgresql_interface").machineEventsByIdBatch
|
||||
const couponManager = require('../../coupon-manager')
|
||||
|
||||
const serverVersion = require('../../../package.json').version
|
||||
|
||||
|
|
@ -174,6 +175,13 @@ const typeDefs = gql`
|
|||
ip_address: String
|
||||
}
|
||||
|
||||
type Coupon {
|
||||
id: ID!
|
||||
code: String!
|
||||
discount: Int!
|
||||
soft_deleted: Boolean
|
||||
}
|
||||
|
||||
type Transaction {
|
||||
id: ID!
|
||||
txClass: String!
|
||||
|
|
@ -255,6 +263,13 @@ const typeDefs = gql`
|
|||
config: JSONObject
|
||||
blacklist: [Blacklist]
|
||||
# userTokens: [UserToken]
|
||||
coupons: [Coupon]
|
||||
}
|
||||
|
||||
type SupportLogsResponse {
|
||||
id: ID!
|
||||
timestamp: Date!
|
||||
deviceId: ID
|
||||
}
|
||||
|
||||
enum MachineAction {
|
||||
|
|
@ -279,6 +294,8 @@ const typeDefs = gql`
|
|||
# revokeToken(token: String!): UserToken
|
||||
deleteBlacklistRow(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(),
|
||||
blacklist: () => blacklist.getBlacklist(),
|
||||
// userTokens: () => tokenManager.getTokenList()
|
||||
coupons: () => couponManager.getAvailableCoupons()
|
||||
},
|
||||
Mutation: {
|
||||
machineAction: (...[, { deviceId, action, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cassette1, cassette2, newName }),
|
||||
|
|
@ -351,6 +369,8 @@ const resolvers = {
|
|||
insertBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.insertIntoBlacklist(cryptoCode, address),
|
||||
// revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
|
||||
createCoupon: (...[, { code, discount }]) => couponManager.createCoupon(code, discount),
|
||||
softDeleteCoupon: (...[, { couponId }]) => couponManager.softDeleteCoupon(couponId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,11 @@ function plugins (settings, deviceId) {
|
|||
.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) {
|
||||
const cryptoCode = coinParams[0]
|
||||
const cryptoNetwork = coinParams[1]
|
||||
|
|
@ -222,11 +227,13 @@ function plugins (settings, deviceId) {
|
|||
const testnetPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c))
|
||||
const pingPromise = recordPing(deviceTime, machineVersion, machineModel)
|
||||
const currentConfigVersionPromise = fetchCurrentConfigVersion()
|
||||
const currentAvailableCouponsPromise = fetchCurrentAvailableCoupons()
|
||||
|
||||
const promises = [
|
||||
buildAvailableCassettes(),
|
||||
pingPromise,
|
||||
currentConfigVersionPromise
|
||||
currentConfigVersionPromise,
|
||||
currentAvailableCouponsPromise
|
||||
].concat(tickerPromises, balancePromises, testnetPromises)
|
||||
|
||||
return Promise.all(promises)
|
||||
|
|
@ -236,16 +243,18 @@ function plugins (settings, deviceId) {
|
|||
const cryptoCodesCount = cryptoCodes.length
|
||||
const tickers = arr.slice(3, 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 coinsWithoutRate = _.map(mapCoinSettings, coinParams)
|
||||
const areThereAvailableCoupons = arr[arr.length - 1] > 0
|
||||
|
||||
return {
|
||||
cassettes,
|
||||
rates: buildRates(tickers),
|
||||
balances: buildBalances(balances),
|
||||
coins: _.zipWith(_.assign, coinsWithoutRate, tickers),
|
||||
configVersion
|
||||
configVersion,
|
||||
areThereAvailableCoupons
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -440,18 +449,18 @@ function plugins (settings, deviceId) {
|
|||
* Trader functions
|
||||
*/
|
||||
|
||||
function buy (rec) {
|
||||
return buyAndSell(rec, true)
|
||||
function buy (rec, tx) {
|
||||
return buyAndSell(rec, true, tx)
|
||||
}
|
||||
|
||||
function sell (rec) {
|
||||
return buyAndSell(rec, false)
|
||||
}
|
||||
|
||||
function buyAndSell (rec, doBuy) {
|
||||
function buyAndSell (rec, doBuy, tx) {
|
||||
const cryptoCode = rec.cryptoCode
|
||||
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('')
|
||||
|
||||
|
|
@ -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) {
|
||||
const market = [fiatCode, cryptoCode].join('')
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ const E = require('./error')
|
|||
const customers = require('./customers')
|
||||
const logs = require('./logs')
|
||||
const compliance = require('./compliance')
|
||||
const couponManager = require('./coupon-manager')
|
||||
const BN = require('./bn')
|
||||
|
||||
const version = require('../package.json').version
|
||||
|
||||
|
|
@ -213,6 +215,37 @@ function verifyTx (req, res, 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) {
|
||||
const customerData = req.body
|
||||
const machineVersion = req.query.version
|
||||
|
|
@ -450,7 +483,8 @@ const configRequiredRoutes = [
|
|||
'/event',
|
||||
'/phone_code',
|
||||
'/customer',
|
||||
'/tx'
|
||||
'/tx',
|
||||
'/verify_coupon'
|
||||
]
|
||||
|
||||
const app = express()
|
||||
|
|
@ -477,6 +511,7 @@ app.post('/state', stateChange)
|
|||
|
||||
app.post('/verify_user', verifyUser)
|
||||
app.post('/verify_transaction', verifyTx)
|
||||
app.post('/verify_coupon', verifyCoupon)
|
||||
|
||||
app.post('/phone_code', getCustomerWithPhoneCode)
|
||||
app.patch('/customer/:id', updateCustomer)
|
||||
|
|
|
|||
19
migrations/1603886141913-coupon-codes.js
Normal file
19
migrations/1603886141913-coupon-codes.js
Normal 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()
|
||||
}
|
||||
14
migrations/1604419505567-add-discount-to-txs.js
Normal file
14
migrations/1604419505567-add-discount-to-txs.js
Normal 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()
|
||||
}
|
||||
15
migrations/1604934042127-clean-bills.js
Normal file
15
migrations/1604934042127-clean-bills.js
Normal 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()
|
||||
}
|
||||
16
migrations/1606910357208-change-trades.js
Normal file
16
migrations/1606910357208-change-trades.js
Normal 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()
|
||||
}
|
||||
138
new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.js
Normal file
138
new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.js
Normal 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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
125
new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodesModal.js
Normal file
125
new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodesModal.js
Normal 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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react'
|
||||
|
||||
const IndividualDiscounts = () => {
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default IndividualDiscounts
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react'
|
||||
|
||||
const LoyaltyDiscounts = () => {
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default LoyaltyDiscounts
|
||||
97
new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyPanel.js
Normal file
97
new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyPanel.js
Normal 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
|
||||
|
|
@ -21,6 +21,7 @@ import ConfigMigration from 'src/pages/ConfigMigration'
|
|||
import { Customers, CustomerProfile } from 'src/pages/Customers'
|
||||
import Funding from 'src/pages/Funding'
|
||||
import Locales from 'src/pages/Locales'
|
||||
import Coupons from 'src/pages/LoyaltyPanel/CouponCodes'
|
||||
import MachineLogs from 'src/pages/MachineLogs'
|
||||
import CashCassettes from 'src/pages/Maintenance/CashCassettes'
|
||||
import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||
|
|
@ -208,6 +209,12 @@ const tree = [
|
|||
route: '/compliance/blacklist',
|
||||
component: Blacklist
|
||||
},
|
||||
{
|
||||
key: 'discount-coupons',
|
||||
label: 'Discount Coupons',
|
||||
route: '/compliance/loyalty/coupons',
|
||||
component: Coupons
|
||||
},
|
||||
{
|
||||
key: 'customer',
|
||||
route: '/compliance/customer/:id',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue