feat: individual discounts creation form

feat: individual discounts deletion
fix: discounts mapping from db
This commit is contained in:
Sérgio Salgado 2021-07-22 17:19:45 +01:00 committed by Josh Harvey
parent 07f15db851
commit 768b5a30e1
8 changed files with 435 additions and 19 deletions

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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
}
`

View file

@ -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)

View file

@ -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

View file

@ -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 && (
<Modal
title="Add individual customer discount"
closeOnBackdropClick={true}
width={600}
height={500}
handleClose={onClose}
open={true}>
<Formik
validateOnBlur={false}
validateOnChange={false}
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={({ idType, value, discount }) => {
handleAddDiscount(idType, value, discount)
}}>
{({ values, errors, touched }) => (
<Form id="individual-discount-form" className={classes.form}>
<div>
<H3 className={classNames(idTypeClass(errors, touched))}>
Select customer identification option
</H3>
<Field
component={RadioGroup}
name="idType"
className={classes.radioGroup}
options={radioOptions}
/>
</div>
<Field
name="value"
label={`Enter customer ${
values.idType === 'idNumber' ? `ID` : `phone`
} number`}
autoFocus
size="lg"
autoComplete="off"
width={338}
component={TextInput}
/>
<div>
<div className={classes.discountRateWrapper}>
<H3>Define discount rate</H3>
<Tooltip width={304}>
<P>
This is a percentage discount off of your existing
commission rates for a customer entering this code at
the machine.
</P>
<P>
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.
</P>
</Tooltip>
</div>
<div className={classes.discountInput}>
<Field
name="discount"
size="lg"
autoComplete="off"
width={50}
decimalScale={0}
className={classes.discountInputField}
component={NumberInput}
/>
<TL1 inline className={classes.inputLabel}>
%
</TL1>
</div>
</div>
<div className={classes.footer}>
{getErrorMsg(errors, touched, creationError) && (
<ErrorMessage>
{getErrorMsg(errors, touched, creationError)}
</ErrorMessage>
)}
<Button
type="submit"
form="individual-discount-form"
className={classes.submit}>
Add discount
</Button>
</div>
</Form>
)}
</Formik>
</Modal>
)}
</>
)
}
export default IndividualDiscountModal

View file

@ -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 => (
<div className={classes.identification}>
{t.idType === 'phone' ? <PhoneIdIcon /> : <CardIdIcon />}
{t.value}
</div>
)
},
{
header: 'Name',
width: 300,
textAlign: 'left',
size: 'sm',
view: t => <>{'-'}</>
},
{
header: 'Discount rate',
width: 220,
textAlign: 'left',
size: 'sm',
view: t => (
<>
<TL1 inline>{t.discount}</TL1> %
</>
)
},
{
header: 'Revoke',
width: 100,
textAlign: 'center',
size: 'sm',
view: t => (
<IconButton
onClick={() => {
setDeleteDialog(true)
setToBeDeleted({ variables: { discountId: t.id } })
}}>
<DeleteIcon />
</IconButton>
)
}
]
return (
<Box display="flex" alignItems="left" flexDirection="column">
<Label3>
It seems there are no active individual customer discounts on your
network.
</Label3>
<Button>Add individual discount</Button>
</Box>
<>
{!loading && !R.isEmpty(discountResponse.individualDiscounts) && (
<Box
marginBottom={4}
marginTop={-7}
className={classes.tableWidth}
display="flex"
justifyContent="flex-end">
<Link color="primary" onClick={toggleModal}>
Add new code
</Link>
</Box>
)}
{!loading && !R.isEmpty(discountResponse.individualDiscounts) && (
<>
<DataTable
elements={elements}
data={R.path(['individualDiscounts'])(discountResponse)}
/>
<DeleteDialog
open={deleteDialog}
onDismissed={() => {
setDeleteDialog(false)
setErrorMsg(null)
}}
onConfirmed={() => {
setErrorMsg(null)
deleteDiscount(toBeDeleted)
}}
errorMessage={errorMsg}
/>
</>
)}
{!loading && R.isEmpty(discountResponse.individualDiscounts) && (
<Box display="flex" alignItems="left" flexDirection="column">
<Label3>
It seems there are no active individual customer discounts on your
network.
</Label3>
<Button onClick={toggleModal}>Add individual discount</Button>
</Box>
)}
<IndividualDiscountModal
showModal={showModal}
setShowModal={setShowModal}
onClose={() => {
setShowModal(false)
}}
creationError={creationError}
addDiscount={createDiscount}
/>
</>
)
}

View file

@ -133,7 +133,7 @@ const PromoCodes = () => {
{!loading && !R.isEmpty(codeResponse.promoCodes) && (
<Box
marginBottom={4}
marginTop={-5}
marginTop={-7}
className={classes.tableWidth}
display="flex"
justifyContent="flex-end">