From 7fe8799edcbe792549168f3e93aba3bd04ebab39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Wed, 28 Oct 2020 11:15:20 +0000 Subject: [PATCH] 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 --- lib/cash-in/cash-in-atomic.js | 2 +- lib/cash-in/cash-in-low.js | 9 +- lib/cash-in/cash-in-tx.js | 6 +- lib/coupon-manager.js | 24 +++ lib/new-admin/graphql/schema.js | 20 +++ lib/plugins.js | 55 ++++++- lib/routes.js | 37 ++++- migrations/1603886141913-coupon-codes.js | 19 +++ .../1604419505567-add-discount-to-txs.js | 14 ++ migrations/1604934042127-clean-bills.js | 15 ++ migrations/1606910357208-change-trades.js | 16 ++ .../src/pages/LoyaltyPanel/CouponCodes.js | 138 ++++++++++++++++++ .../pages/LoyaltyPanel/CouponCodes.styles.js | 37 +++++ .../pages/LoyaltyPanel/CouponCodesModal.js | 125 ++++++++++++++++ .../pages/LoyaltyPanel/IndividualDiscounts.js | 7 + .../pages/LoyaltyPanel/LoyaltyDiscounts.js | 7 + .../src/pages/LoyaltyPanel/LoyaltyPanel.js | 97 ++++++++++++ new-lamassu-admin/src/routing/routes.js | 7 + 18 files changed, 622 insertions(+), 13 deletions(-) create mode 100644 lib/coupon-manager.js create mode 100644 migrations/1603886141913-coupon-codes.js create mode 100644 migrations/1604419505567-add-discount-to-txs.js create mode 100644 migrations/1604934042127-clean-bills.js create mode 100644 migrations/1606910357208-change-trades.js create mode 100644 new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.js create mode 100644 new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.styles.js create mode 100644 new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodesModal.js create mode 100644 new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscounts.js create mode 100644 new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyDiscounts.js create mode 100644 new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyPanel.js diff --git a/lib/cash-in/cash-in-atomic.js b/lib/cash-in/cash-in-atomic.js index ea551d5c..079dc25c 100644 --- a/lib/cash-in/cash-in-atomic.js +++ b/lib/cash-in/cash-in-atomic.js @@ -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) diff --git a/lib/cash-in/cash-in-low.js b/lib/cash-in/cash-in-low.js index ccb9ab01..59b3b1d8 100644 --- a/lib/cash-in/cash-in-low.js +++ b/lib/cash-in/cash-in-low.js @@ -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 +} diff --git a/lib/cash-in/cash-in-tx.js b/lib/cash-in/cash-in-tx.js index 82d09bb6..286408a4 100644 --- a/lib/cash-in/cash-in-tx.js +++ b/lib/cash-in/cash-in-tx.js @@ -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({}) diff --git a/lib/coupon-manager.js b/lib/coupon-manager.js new file mode 100644 index 00000000..efc065be --- /dev/null +++ b/lib/coupon-manager.js @@ -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 } diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js index 7886288a..529da459 100644 --- a/lib/new-admin/graphql/schema.js +++ b/lib/new-admin/graphql/schema.js @@ -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) } } diff --git a/lib/plugins.js b/lib/plugins.js index fe3d4e16..ae5bad51 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -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('') diff --git a/lib/routes.js b/lib/routes.js index 5b3fbc25..e57f6a23 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -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) diff --git a/migrations/1603886141913-coupon-codes.js b/migrations/1603886141913-coupon-codes.js new file mode 100644 index 00000000..eb3527c2 --- /dev/null +++ b/migrations/1603886141913-coupon-codes.js @@ -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() +} diff --git a/migrations/1604419505567-add-discount-to-txs.js b/migrations/1604419505567-add-discount-to-txs.js new file mode 100644 index 00000000..00572698 --- /dev/null +++ b/migrations/1604419505567-add-discount-to-txs.js @@ -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() +} diff --git a/migrations/1604934042127-clean-bills.js b/migrations/1604934042127-clean-bills.js new file mode 100644 index 00000000..bee5cf50 --- /dev/null +++ b/migrations/1604934042127-clean-bills.js @@ -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() +} diff --git a/migrations/1606910357208-change-trades.js b/migrations/1606910357208-change-trades.js new file mode 100644 index 00000000..756b24e1 --- /dev/null +++ b/migrations/1606910357208-change-trades.js @@ -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() +} diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.js b/new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.js new file mode 100644 index 00000000..16ebfbd4 --- /dev/null +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.js @@ -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 => ( + <> + {t.discount} % in commissions + + ) + }, + { + header: 'Delete', + width: 100, + textAlign: 'center', + size: 'sm', + view: t => ( + { + softDeleteCoupon({ variables: { couponId: t.id } }) + }}> + + + ) + } + ] + + return ( + <> + + {!loading && !R.isEmpty(couponResponse.coupons) && ( + + + Add new coupon + + + )} + {!loading && !R.isEmpty(couponResponse.coupons) && ( + + )} + {!loading && R.isEmpty(couponResponse.coupons) && ( + +

Currently, there are no active coupon codes on your network.

+ +
+ )} + + + ) +} +export default Coupons diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.styles.js b/new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.styles.js new file mode 100644 index 00000000..fab71b00 --- /dev/null +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodes.styles.js @@ -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 + } +} diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodesModal.js b/new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodesModal.js new file mode 100644 index 00000000..b80f0537 --- /dev/null +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/CouponCodesModal.js @@ -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 && ( + +

Add coupon code discount

+

Coupon code name

+ +
+

Define discount rate

+ +

+ The discount rate inserted will be applied to the commissions of + all transactions performed with this respective coupon code. +

+

+ (It should be a number between 0 (zero) and 100 (one hundred)). +

+
+
+
+ + + % + +
+
+ +
+
+ )} + + ) +} + +export default CouponCodesModal diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscounts.js b/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscounts.js new file mode 100644 index 00000000..86abb547 --- /dev/null +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscounts.js @@ -0,0 +1,7 @@ +import React from 'react' + +const IndividualDiscounts = () => { + return <> +} + +export default IndividualDiscounts diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyDiscounts.js b/new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyDiscounts.js new file mode 100644 index 00000000..cf4e9335 --- /dev/null +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyDiscounts.js @@ -0,0 +1,7 @@ +import React from 'react' + +const LoyaltyDiscounts = () => { + return <> +} + +export default LoyaltyDiscounts diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyPanel.js b/new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyPanel.js new file mode 100644 index 00000000..0b624386 --- /dev/null +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/LoyaltyPanel.js @@ -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 }) => ( + + + + {innerRoutes.map(({ route, component: Page, key }) => ( + + + + ))} + +) + +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 ( + <> + + + it.label} + onClick={onClick} + /> +
+ +
+
+ + ) +} + +export default LoyaltyPanel diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index a9580c9e..3eb0f98b 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -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',