From 768b5a30e1c2ff02782e431d2696b9d57dafe7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Thu, 22 Jul 2021 17:19:45 +0100 Subject: [PATCH] feat: individual discounts creation form feat: individual discounts deletion fix: discounts mapping from db --- lib/loyalty.js | 16 +- .../graphql/resolvers/loyalty.resolver.js | 2 +- lib/new-admin/graphql/types/loyalty.type.js | 6 +- .../1626891847835-add-individual-discounts.js | 2 +- .../LoyaltyPanel/IndividualDiscount.styles.js | 53 +++++ .../LoyaltyPanel/IndividualDiscountModal.js | 183 +++++++++++++++++ .../pages/LoyaltyPanel/IndividualDiscounts.js | 190 +++++++++++++++++- .../src/pages/LoyaltyPanel/PromoCodes.js | 2 +- 8 files changed, 435 insertions(+), 19 deletions(-) create mode 100644 new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscount.styles.js create mode 100644 new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscountModal.js diff --git a/lib/loyalty.js b/lib/loyalty.js index ec61c594..370c25b1 100644 --- a/lib/loyalty.js +++ b/lib/loyalty.js @@ -1,5 +1,6 @@ const db = require('./db') const uuid = require('uuid') +const _ = require('lodash/fp') function getAvailablePromoCodes () { const sql = `SELECT * FROM coupons WHERE soft_deleted=false` @@ -28,12 +29,23 @@ function getNumberOfAvailablePromoCodes () { function getAvailableIndividualDiscounts () { const sql = `SELECT * from individual_discounts WHERE soft_deleted=false` - return db.any(sql) + return db.any(sql).then(res => _.map(it => ({ + id: it.id, + idType: it.identification, + value: it.value, + discount: it.discount, + softDeleted: it.soft_deleted + }), res)) } function createIndividualDiscount (idType, value, discount) { + const idTypes = { + phone: 'phone', + idNumber: 'id_number' + } + const sql = `INSERT INTO individual_discounts (id, identification, value, discount) VALUES ($1, $2, $3, $4) RETURNING *` - return db.one(sql, [uuid.v4(), idType, value, discount]) + return db.one(sql, [uuid.v4(), idTypes[idType], value, discount]) } function deleteIndividualDiscount (id) { diff --git a/lib/new-admin/graphql/resolvers/loyalty.resolver.js b/lib/new-admin/graphql/resolvers/loyalty.resolver.js index 9a621451..bebf4c08 100644 --- a/lib/new-admin/graphql/resolvers/loyalty.resolver.js +++ b/lib/new-admin/graphql/resolvers/loyalty.resolver.js @@ -8,7 +8,7 @@ const resolvers = { Mutation: { createPromoCode: (...[, { code, discount }]) => loyalty.createPromoCode(code, discount), deletePromoCode: (...[, { codeId }]) => loyalty.deletePromoCode(codeId), - createIndividualDiscount: (...[, { identificationType, value, discount }]) => loyalty.createIndividualDiscount(identificationType, value, discount), + createIndividualDiscount: (...[, { idType, value, discount }]) => loyalty.createIndividualDiscount(idType, value, discount), deleteIndividualDiscount: (...[, { discountId }]) => loyalty.deleteIndividualDiscount(discountId) } } diff --git a/lib/new-admin/graphql/types/loyalty.type.js b/lib/new-admin/graphql/types/loyalty.type.js index ba84e032..c5d8862e 100644 --- a/lib/new-admin/graphql/types/loyalty.type.js +++ b/lib/new-admin/graphql/types/loyalty.type.js @@ -3,14 +3,14 @@ const { gql } = require('apollo-server-express') const typeDef = gql` type IndividualDiscount { id: ID! - identificationType: DiscountIdentificationType + idType: DiscountIdentificationType value: String! discount: Int } enum DiscountIdentificationType { phone - idCard + idNumber } type PromoCode { @@ -27,7 +27,7 @@ const typeDef = gql` type Mutation { createPromoCode(code: String!, discount: Int!): PromoCode @auth deletePromoCode(codeId: ID!): PromoCode @auth - createIndividualDiscount(identificationType: DiscountIdentificationType!, value: String!, discount: Int!): IndividualDiscount @auth + createIndividualDiscount(idType: DiscountIdentificationType!, value: String!, discount: Int!): IndividualDiscount @auth deleteIndividualDiscount(discountId: ID!): IndividualDiscount @auth } ` diff --git a/migrations/1626891847835-add-individual-discounts.js b/migrations/1626891847835-add-individual-discounts.js index 037ec658..28539a4d 100644 --- a/migrations/1626891847835-add-individual-discounts.js +++ b/migrations/1626891847835-add-individual-discounts.js @@ -10,7 +10,7 @@ exports.up = function (next) { discount SMALLINT NOT NULL, soft_deleted BOOLEAN DEFAULT false )`, - `CREATE UNIQUE INDEX uq_individual_discount ON individual_discounts (value) WHERE NOT soft_deleted` + `CREATE UNIQUE INDEX uq_individual_discount ON individual_discounts (identification, value) WHERE NOT soft_deleted` ] db.multi(sql, next) diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscount.styles.js b/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscount.styles.js new file mode 100644 index 00000000..ff7c95cb --- /dev/null +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscount.styles.js @@ -0,0 +1,53 @@ +import { spacer, errorColor } from 'src/styling/variables' + +const styles = { + identification: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + '& > *:first-child': { + marginRight: 10 + } + }, + form: { + display: 'flex', + flexDirection: 'column', + height: '100%', + '& > *': { + marginBottom: 20 + } + }, + radioGroup: { + display: 'flex', + flexDirection: 'row', + '& > *': { + marginLeft: 15 + }, + '& > *:first-child': { + marginLeft: 0 + } + }, + discountRateWrapper: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center' + }, + discountInput: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center' + }, + footer: { + display: 'flex', + flexDirection: 'row', + margin: [['auto', 0, spacer * 3, 0]] + }, + submit: { + margin: [['auto', 0, 0, 'auto']] + }, + error: { + color: errorColor + } +} + +export default styles diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscountModal.js b/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscountModal.js new file mode 100644 index 00000000..c9cc9f9a --- /dev/null +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscountModal.js @@ -0,0 +1,183 @@ +import { makeStyles } from '@material-ui/core/styles' +import classNames from 'classnames' +import { Form, Formik, Field } from 'formik' +import React from 'react' +import * as Yup from 'yup' + +import ErrorMessage from 'src/components/ErrorMessage' +import Modal from 'src/components/Modal' +import { Tooltip } from 'src/components/Tooltip' +import { Button } from 'src/components/buttons' +import { + NumberInput, + RadioGroup, + TextInput +} from 'src/components/inputs/formik' +import { H3, TL1, P } from 'src/components/typography' + +import styles from './IndividualDiscount.styles' + +const useStyles = makeStyles(styles) + +const initialValues = { + idType: '', + value: '', + discount: '' +} + +const validationSchema = Yup.object().shape({ + idType: Yup.string() + .required('An identification type is required!') + .trim(), + value: Yup.string() + .required('A value is required!') + .trim() + .min(3, 'Value should have at least 3 characters!') + .max(20, 'Value should have a maximum of 20 characters!'), + discount: Yup.number() + .required('A discount rate is required!') + .min(0, 'Discount rate should be a positive number!') + .max(100, 'Discount rate should have a maximum value of 100%!') +}) + +const radioOptions = [ + { + code: 'phone', + display: 'Phone number' + }, + { + code: 'idNumber', + display: 'ID card number' + } +] + +const getErrorMsg = (formikErrors, formikTouched, mutationError) => { + if (!formikErrors || !formikTouched) return null + if (mutationError) return 'Internal server error' + if (formikErrors.idType && formikTouched.idType) return formikErrors.idType + if (formikErrors.value && formikTouched.value) return formikErrors.value + if (formikErrors.discount && formikTouched.discount) + return formikErrors.discount + return null +} + +const IndividualDiscountModal = ({ + showModal, + setShowModal, + onClose, + creationError, + addDiscount +}) => { + const classes = useStyles() + + const handleAddDiscount = (idType, value, discount) => { + addDiscount({ + variables: { + idType: idType, + value: value, + discount: parseInt(discount) + } + }) + setShowModal(false) + } + + const idTypeClass = (formikErrors, formikTouched) => ({ + [classes.error]: formikErrors.idType && formikTouched.idType + }) + + return ( + <> + {showModal && ( + + { + handleAddDiscount(idType, value, discount) + }}> + {({ values, errors, touched }) => ( +
+
+

+ Select customer identification option +

+ +
+ +
+
+

Define discount rate

+ +

+ This is a percentage discount off of your existing + commission rates for a customer entering this code at + the machine. +

+

+ For instance, if you charge 8% commissions, and this + code is set for 50%, then you'll instead be charging 4% + on transactions using the code. +

+
+
+
+ + + % + +
+
+
+ {getErrorMsg(errors, touched, creationError) && ( + + {getErrorMsg(errors, touched, creationError)} + + )} + +
+ + )} +
+
+ )} + + ) +} + +export default IndividualDiscountModal diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscounts.js b/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscounts.js index e1c7806a..e874a797 100644 --- a/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscounts.js +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/IndividualDiscounts.js @@ -1,18 +1,186 @@ -import { Box } from '@material-ui/core' -import React from 'react' +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 { Button } from 'src/components/buttons' -import { Label3 } from 'src/components/typography' +import { DeleteDialog } from 'src/components/DeleteDialog' +import { Link, Button, IconButton } from 'src/components/buttons' +import DataTable from 'src/components/tables/DataTable' +import { Label3, TL1 } from 'src/components/typography' +import { ReactComponent as CardIdIcon } from 'src/styling/icons/ID/card/zodiac.svg' +import { ReactComponent as PhoneIdIcon } from 'src/styling/icons/ID/phone/zodiac.svg' +import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' + +import styles from './IndividualDiscount.styles' +import IndividualDiscountModal from './IndividualDiscountModal' + +const useStyles = makeStyles(styles) + +const GET_INDIVIDUAL_DISCOUNTS = gql` + query individualDiscounts { + individualDiscounts { + id + idType + value + discount + } + } +` + +const DELETE_DISCOUNT = gql` + mutation deleteIndividualDiscount($discountId: ID!) { + deleteIndividualDiscount(discountId: $discountId) { + id + } + } +` + +const CREATE_DISCOUNT = gql` + mutation createIndividualDiscount( + $idType: DiscountIdentificationType! + $value: String! + $discount: Int! + ) { + createIndividualDiscount( + idType: $idType + value: $value + discount: $discount + ) { + id + } + } +` const IndividualDiscounts = () => { + const classes = useStyles() + + const [deleteDialog, setDeleteDialog] = useState(false) + const [toBeDeleted, setToBeDeleted] = useState() + + const [errorMsg, setErrorMsg] = useState('') + const [showModal, setShowModal] = useState(false) + const toggleModal = () => setShowModal(!showModal) + + const { data: discountResponse, loading } = useQuery(GET_INDIVIDUAL_DISCOUNTS) + + const [createDiscount, { error: creationError }] = useMutation( + CREATE_DISCOUNT, + { + refetchQueries: () => ['individualDiscounts'] + } + ) + + const [deleteDiscount] = useMutation(DELETE_DISCOUNT, { + onError: ({ message }) => { + const errorMessage = message ?? 'Error while deleting row' + setErrorMsg(errorMessage) + }, + onCompleted: () => setDeleteDialog(false), + refetchQueries: () => ['individualDiscounts'] + }) + + const elements = [ + { + header: 'Identification', + width: 312, + textAlign: 'left', + size: 'sm', + view: t => ( +
+ {t.idType === 'phone' ? : } + {t.value} +
+ ) + }, + { + header: 'Name', + width: 300, + textAlign: 'left', + size: 'sm', + view: t => <>{'-'} + }, + { + header: 'Discount rate', + width: 220, + textAlign: 'left', + size: 'sm', + view: t => ( + <> + {t.discount} % + + ) + }, + { + header: 'Revoke', + width: 100, + textAlign: 'center', + size: 'sm', + view: t => ( + { + setDeleteDialog(true) + setToBeDeleted({ variables: { discountId: t.id } }) + }}> + + + ) + } + ] + return ( - - - It seems there are no active individual customer discounts on your - network. - - - + <> + {!loading && !R.isEmpty(discountResponse.individualDiscounts) && ( + + + Add new code + + + )} + {!loading && !R.isEmpty(discountResponse.individualDiscounts) && ( + <> + + { + setDeleteDialog(false) + setErrorMsg(null) + }} + onConfirmed={() => { + setErrorMsg(null) + deleteDiscount(toBeDeleted) + }} + errorMessage={errorMsg} + /> + + )} + {!loading && R.isEmpty(discountResponse.individualDiscounts) && ( + + + It seems there are no active individual customer discounts on your + network. + + + + )} + { + setShowModal(false) + }} + creationError={creationError} + addDiscount={createDiscount} + /> + ) } diff --git a/new-lamassu-admin/src/pages/LoyaltyPanel/PromoCodes.js b/new-lamassu-admin/src/pages/LoyaltyPanel/PromoCodes.js index 22d91322..5e78024c 100644 --- a/new-lamassu-admin/src/pages/LoyaltyPanel/PromoCodes.js +++ b/new-lamassu-admin/src/pages/LoyaltyPanel/PromoCodes.js @@ -133,7 +133,7 @@ const PromoCodes = () => { {!loading && !R.isEmpty(codeResponse.promoCodes) && (