feat: individual discounts creation form
feat: individual discounts deletion fix: discounts mapping from db
This commit is contained in:
parent
07f15db851
commit
768b5a30e1
8 changed files with 435 additions and 19 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const uuid = require('uuid')
|
const uuid = require('uuid')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
function getAvailablePromoCodes () {
|
function getAvailablePromoCodes () {
|
||||||
const sql = `SELECT * FROM coupons WHERE soft_deleted=false`
|
const sql = `SELECT * FROM coupons WHERE soft_deleted=false`
|
||||||
|
|
@ -28,12 +29,23 @@ function getNumberOfAvailablePromoCodes () {
|
||||||
|
|
||||||
function getAvailableIndividualDiscounts () {
|
function getAvailableIndividualDiscounts () {
|
||||||
const sql = `SELECT * from individual_discounts WHERE soft_deleted=false`
|
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) {
|
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 *`
|
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) {
|
function deleteIndividualDiscount (id) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const resolvers = {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createPromoCode: (...[, { code, discount }]) => loyalty.createPromoCode(code, discount),
|
createPromoCode: (...[, { code, discount }]) => loyalty.createPromoCode(code, discount),
|
||||||
deletePromoCode: (...[, { codeId }]) => loyalty.deletePromoCode(codeId),
|
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)
|
deleteIndividualDiscount: (...[, { discountId }]) => loyalty.deleteIndividualDiscount(discountId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ const { gql } = require('apollo-server-express')
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type IndividualDiscount {
|
type IndividualDiscount {
|
||||||
id: ID!
|
id: ID!
|
||||||
identificationType: DiscountIdentificationType
|
idType: DiscountIdentificationType
|
||||||
value: String!
|
value: String!
|
||||||
discount: Int
|
discount: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DiscountIdentificationType {
|
enum DiscountIdentificationType {
|
||||||
phone
|
phone
|
||||||
idCard
|
idNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromoCode {
|
type PromoCode {
|
||||||
|
|
@ -27,7 +27,7 @@ const typeDef = gql`
|
||||||
type Mutation {
|
type Mutation {
|
||||||
createPromoCode(code: String!, discount: Int!): PromoCode @auth
|
createPromoCode(code: String!, discount: Int!): PromoCode @auth
|
||||||
deletePromoCode(codeId: ID!): 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
|
deleteIndividualDiscount(discountId: ID!): IndividualDiscount @auth
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ exports.up = function (next) {
|
||||||
discount SMALLINT NOT NULL,
|
discount SMALLINT NOT NULL,
|
||||||
soft_deleted BOOLEAN DEFAULT false
|
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)
|
db.multi(sql, next)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,18 +1,186 @@
|
||||||
import { Box } from '@material-ui/core'
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
import React from 'react'
|
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 { DeleteDialog } from 'src/components/DeleteDialog'
|
||||||
import { Label3 } from 'src/components/typography'
|
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 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 (
|
return (
|
||||||
<Box display="flex" alignItems="left" flexDirection="column">
|
<>
|
||||||
<Label3>
|
{!loading && !R.isEmpty(discountResponse.individualDiscounts) && (
|
||||||
It seems there are no active individual customer discounts on your
|
<Box
|
||||||
network.
|
marginBottom={4}
|
||||||
</Label3>
|
marginTop={-7}
|
||||||
<Button>Add individual discount</Button>
|
className={classes.tableWidth}
|
||||||
</Box>
|
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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ const PromoCodes = () => {
|
||||||
{!loading && !R.isEmpty(codeResponse.promoCodes) && (
|
{!loading && !R.isEmpty(codeResponse.promoCodes) && (
|
||||||
<Box
|
<Box
|
||||||
marginBottom={4}
|
marginBottom={4}
|
||||||
marginTop={-5}
|
marginTop={-7}
|
||||||
className={classes.tableWidth}
|
className={classes.tableWidth}
|
||||||
display="flex"
|
display="flex"
|
||||||
justifyContent="flex-end">
|
justifyContent="flex-end">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue