Merge pull request #1099 from chaotixkilla/fix-custom-sms
Get SMS notices closer to spec
This commit is contained in:
commit
26bf036553
19 changed files with 709 additions and 468 deletions
|
|
@ -20,6 +20,7 @@ const MANUAL = 'manual'
|
|||
|
||||
const CASH_OUT_DISPENSE_READY = 'cash_out_dispense_ready'
|
||||
const CONFIRMATION_CODE = 'sms_code'
|
||||
const RECEIPT = 'sms_receipt'
|
||||
|
||||
const WALLET_SCORE_THRESHOLD = 9
|
||||
|
||||
|
|
@ -37,5 +38,6 @@ module.exports = {
|
|||
CONFIRMATION_CODE,
|
||||
CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES,
|
||||
CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES,
|
||||
WALLET_SCORE_THRESHOLD
|
||||
WALLET_SCORE_THRESHOLD,
|
||||
RECEIPT
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const uuid = require('uuid')
|
||||
const db = require('./db')
|
||||
|
||||
const getCustomMessages = () => {
|
||||
const sql = `SELECT * FROM custom_messages ORDER BY created`
|
||||
return db.any(sql).then(res => _.map(
|
||||
it => ({
|
||||
id: it.id,
|
||||
event: _.camelCase(it.event),
|
||||
message: it.message
|
||||
}), res))
|
||||
}
|
||||
|
||||
const createCustomMessage = (event, message) => {
|
||||
const sql = `INSERT INTO custom_messages (id, event, message) VALUES ($1, $2, $3)`
|
||||
return db.none(sql, [uuid.v4(), _.snakeCase(event), message])
|
||||
}
|
||||
|
||||
const editCustomMessage = (id, event, message) => {
|
||||
const sql = `UPDATE custom_messages SET event=$2, message=$3 WHERE id=$1`
|
||||
return db.none(sql, [id, _.snakeCase(event), message])
|
||||
}
|
||||
|
||||
const deleteCustomMessage = id => {
|
||||
const sql = `DELETE FROM custom_messages WHERE id=$1`
|
||||
return db.none(sql, [id])
|
||||
}
|
||||
|
||||
const getCustomMessage = event => {
|
||||
const sql = `SELECT * FROM custom_messages WHERE event=$1 LIMIT 1`
|
||||
return db.oneOrNone(sql, [event])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCustomMessages,
|
||||
createCustomMessage,
|
||||
editCustomMessage,
|
||||
deleteCustomMessage,
|
||||
getCustomMessage
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
const customSms = require('../../../custom-sms')
|
||||
const smsNotices = require('../../../sms-notices')
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
customMessages: () => customSms.getCustomMessages()
|
||||
SMSNotices: () => smsNotices.getSMSNotices()
|
||||
},
|
||||
Mutation: {
|
||||
createCustomMessage: (...[, { event, message }]) => customSms.createCustomMessage(event, message),
|
||||
editCustomMessage: (...[, { id, event, message }]) => customSms.editCustomMessage(id, event, message),
|
||||
deleteCustomMessage: (...[, { id }]) => customSms.deleteCustomMessage(id)
|
||||
editSMSNotice: (...[, { id, event, message }]) => smsNotices.editSMSNotice(id, event, message),
|
||||
enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id),
|
||||
disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
const { gql } = require('apollo-server-express')
|
||||
|
||||
const typeDef = gql`
|
||||
type CustomMessage {
|
||||
type SMSNotice {
|
||||
id: ID!
|
||||
event: CustomMessageEvent!
|
||||
event: SMSNoticeEvent!
|
||||
message: String!
|
||||
messageName: String!
|
||||
enabled: Boolean!
|
||||
allowToggle: Boolean!
|
||||
}
|
||||
|
||||
enum CustomMessageEvent {
|
||||
enum SMSNoticeEvent {
|
||||
smsCode
|
||||
cashOutDispenseReady
|
||||
smsReceipt
|
||||
}
|
||||
|
||||
type Query {
|
||||
customMessages: [CustomMessage] @auth
|
||||
SMSNotices: [SMSNotice] @auth
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createCustomMessage(event: CustomMessageEvent!, message: String!): CustomMessage @auth
|
||||
editCustomMessage(id: ID!, event: CustomMessageEvent!, message: String!): CustomMessage @auth
|
||||
deleteCustomMessage(id: ID!): CustomMessage @auth
|
||||
editSMSNotice(id: ID!, event: SMSNoticeEvent!, message: String!): SMSNotice @auth
|
||||
enableSMSNotice(id: ID!): SMSNotice @auth
|
||||
disableSMSNotice(id: ID!): SMSNotice @auth
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -772,7 +772,8 @@ function plugins (settings, deviceId) {
|
|||
? '123'
|
||||
: randomCode()
|
||||
|
||||
return sms.getSms(CONFIRMATION_CODE, phone, { code })
|
||||
const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
|
||||
return sms.getSms(CONFIRMATION_CODE, phone, { code, timestamp })
|
||||
.then(smsObj => {
|
||||
const rec = {
|
||||
sms: smsObj
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const NAME = 'FakeWallet'
|
|||
const SECONDS = 1000
|
||||
const PUBLISH_TIME = 3 * SECONDS
|
||||
const AUTHORIZE_TIME = PUBLISH_TIME + 5 * SECONDS
|
||||
const CONFIRM_TIME = AUTHORIZE_TIME + 120 * SECONDS
|
||||
const CONFIRM_TIME = AUTHORIZE_TIME + 10 * SECONDS
|
||||
|
||||
let t0
|
||||
|
||||
|
|
|
|||
|
|
@ -189,6 +189,6 @@ router.patch('/:id/block', triggerBlock)
|
|||
router.patch('/:id/suspend', triggerSuspend)
|
||||
router.patch('/:id/photos/idcarddata', updateIdCardData)
|
||||
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
|
||||
router.patch('/:id/smsreceipt', sendSmsReceipt)
|
||||
router.post('/:id/smsreceipt', sendSmsReceipt)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
|||
56
lib/sms-notices.js
Normal file
56
lib/sms-notices.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
const _ = require('lodash/fp')
|
||||
const uuid = require('uuid')
|
||||
const db = require('./db')
|
||||
|
||||
const getSMSNotices = () => {
|
||||
const sql = `SELECT * FROM sms_notices ORDER BY created`
|
||||
return db.any(sql).then(res => _.map(
|
||||
it => ({
|
||||
id: it.id,
|
||||
event: _.camelCase(it.event),
|
||||
message: it.message,
|
||||
messageName: it.message_name,
|
||||
enabled: it.enabled,
|
||||
allowToggle: it.allow_toggle
|
||||
}), res))
|
||||
}
|
||||
|
||||
const createSMSNotice = (event, messageName, message, enabled, allowToggle) => {
|
||||
const sql = `INSERT INTO sms_notices (id, event, message_name, message${enabled ? `, enabled`: ``}${allowToggle ? `, allowToggle`: ``}) VALUES ($1, $2, $3, $4${enabled ? `, $5`: ``}${allowToggle ? `, $6`: ``})`
|
||||
return db.none(sql, [uuid.v4(), _.snakeCase(event), messageName, message, enabled, allowToggle])
|
||||
}
|
||||
|
||||
const editSMSNotice = (id, event, message) => {
|
||||
const sql = `UPDATE sms_notices SET event=$2, message=$3 WHERE id=$1`
|
||||
return db.none(sql, [id, _.snakeCase(event), message])
|
||||
}
|
||||
|
||||
const deleteSMSNotice = id => {
|
||||
const sql = `DELETE FROM sms_notices WHERE id=$1`
|
||||
return db.none(sql, [id])
|
||||
}
|
||||
|
||||
const getSMSNotice = event => {
|
||||
const sql = `SELECT * FROM sms_notices WHERE event=$1 LIMIT 1`
|
||||
return db.oneOrNone(sql, [event])
|
||||
}
|
||||
|
||||
const enableSMSNotice = id => {
|
||||
const sql = `UPDATE sms_notices SET enabled = true WHERE id=$1`
|
||||
return db.oneOrNone(sql, [id])
|
||||
}
|
||||
|
||||
const disableSMSNotice = id => {
|
||||
const sql = `UPDATE sms_notices SET enabled = false WHERE id=$1`
|
||||
return db.oneOrNone(sql, [id])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSMSNotices,
|
||||
createSMSNotice,
|
||||
editSMSNotice,
|
||||
deleteSMSNotice,
|
||||
getSMSNotice,
|
||||
enableSMSNotice,
|
||||
disableSMSNotice
|
||||
}
|
||||
35
lib/sms.js
35
lib/sms.js
|
|
@ -1,17 +1,16 @@
|
|||
|
||||
const dateFormat = require('dateformat')
|
||||
|
||||
const ph = require('./plugin-helper')
|
||||
const argv = require('minimist')(process.argv.slice(2))
|
||||
const { utils: coinUtils } = require('lamassu-coins')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const customSms = require('./custom-sms')
|
||||
|
||||
const getDefaultMessageContent = content => ({
|
||||
smsCode: `Your cryptomat code: ${content.code}`,
|
||||
cashOutDispenseReady: `Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours. [${content.timestamp}]`
|
||||
})
|
||||
const smsNotices = require('./sms-notices')
|
||||
const { RECEIPT } = require('./constants')
|
||||
|
||||
function getSms (event, phone, content) {
|
||||
return customSms.getCustomMessage(event)
|
||||
return smsNotices.getSMSNotice(event)
|
||||
.then(msg => {
|
||||
if (!_.isNil(msg)) {
|
||||
var accMsg = msg.message
|
||||
|
|
@ -22,11 +21,6 @@ function getSms (event, phone, content) {
|
|||
body: messageContent
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
toNumber: phone,
|
||||
body: getDefaultMessageContent(content)[_.camelCase(event)]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -110,13 +104,16 @@ function formatSmsReceipt (data, options) {
|
|||
message = message.concat(`Address: ${data.address}\n`)
|
||||
}
|
||||
|
||||
const request = {
|
||||
sms: {
|
||||
toNumber: data.customerPhone,
|
||||
body: message
|
||||
}
|
||||
}
|
||||
return request
|
||||
const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
|
||||
const postReceiptSmsPromise = getSms(RECEIPT, data.customerPhone, { timestamp })
|
||||
|
||||
return Promise.all([smsNotices.getSMSNotice(RECEIPT), postReceiptSmsPromise])
|
||||
.then(([res, postReceiptSms]) => ({
|
||||
sms: {
|
||||
toNumber: data.customerPhone,
|
||||
body: res.enabled ? message.concat('\n\n', postReceiptSms.body) : message
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
22
migrations/1643996603839-change-custom-sms-to-notices.js
Normal file
22
migrations/1643996603839-change-custom-sms-to-notices.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
var db = require('./db')
|
||||
var smsNotices = require('../lib/sms-notices')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
`ALTER TABLE custom_messages RENAME TO sms_notices`,
|
||||
`ALTER TYPE custom_message_event RENAME TO sms_notice_event`,
|
||||
`ALTER TYPE sms_notice_event ADD VALUE 'sms_receipt'`,
|
||||
`ALTER TABLE sms_notices ADD COLUMN message_name TEXT UNIQUE NOT NULL`,
|
||||
`ALTER TABLE sms_notices ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT true`
|
||||
`ALTER TABLE sms_notices ADD COLUMN allow_toggle BOOLEAN NOT NULL DEFAULT true`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
.then(() => smsNotices.createSMSNotice('sms_code', 'SMS confirmation code', 'Your cryptomat code: #code', true, false))
|
||||
.then(() => smsNotices.createSMSNotice('cash_out_dispense_ready', 'Cash is ready', 'Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours. [#timestamp]', true, false))
|
||||
.then(() => smsNotices.createSMSNotice('sms_receipt', 'SMS receipt', '', true, true))
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
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 { DeleteDialog } from 'src/components/DeleteDialog'
|
||||
import { Link, IconButton } from 'src/components/buttons'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { H4 } from 'src/components/typography'
|
||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
|
||||
import styles from './CustomSMS.styles'
|
||||
import CustomSMSModal from './CustomSMSModal'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const GET_CUSTOM_MESSAGES = gql`
|
||||
query customMessages {
|
||||
customMessages {
|
||||
id
|
||||
event
|
||||
message
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CREATE_CUSTOM_MESSAGE = gql`
|
||||
mutation createCustomMessage($event: CustomMessageEvent!, $message: String!) {
|
||||
createCustomMessage(event: $event, message: $message) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const EDIT_CUSTOM_MESSAGE = gql`
|
||||
mutation editCustomMessage(
|
||||
$id: ID!
|
||||
$event: CustomMessageEvent!
|
||||
$message: String!
|
||||
) {
|
||||
editCustomMessage(id: $id, event: $event, message: $message) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const DELETE_CUSTOM_MESSAGE = gql`
|
||||
mutation deleteCustomMessage($id: ID!) {
|
||||
deleteCustomMessage(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const EVENT_OPTIONS = [
|
||||
{ code: 'smsCode', display: 'On SMS confirmation code' },
|
||||
{ code: 'cashOutDispenseReady', display: 'Cash out dispense ready' }
|
||||
]
|
||||
|
||||
const CustomSMS = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [deleteDialog, setDeleteDialog] = useState(false)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [selectedSMS, setSelectedSMS] = useState(null)
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
|
||||
const { data: messagesData, loading: messagesLoading } = useQuery(
|
||||
GET_CUSTOM_MESSAGES
|
||||
)
|
||||
|
||||
const [createMessage] = useMutation(CREATE_CUSTOM_MESSAGE, {
|
||||
onError: ({ msg }) => setErrorMsg(msg),
|
||||
refetchQueries: () => ['customMessages']
|
||||
})
|
||||
|
||||
const [editMessage] = useMutation(EDIT_CUSTOM_MESSAGE, {
|
||||
onError: ({ msg }) => setErrorMsg(msg),
|
||||
refetchQueries: () => ['customMessages']
|
||||
})
|
||||
|
||||
const [deleteMessage] = useMutation(DELETE_CUSTOM_MESSAGE, {
|
||||
onError: ({ msg }) => setErrorMsg(msg),
|
||||
refetchQueries: () => ['customMessages']
|
||||
})
|
||||
|
||||
const loading = messagesLoading
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedSMS(null)
|
||||
setShowModal(false)
|
||||
setDeleteDialog(false)
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setErrorMsg('')
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Event',
|
||||
width: 600,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: it =>
|
||||
R.find(ite => R.propEq('event', ite.code, it), EVENT_OPTIONS).display
|
||||
},
|
||||
{
|
||||
header: 'Edit',
|
||||
width: 100,
|
||||
size: 'sm',
|
||||
textAlign: 'center',
|
||||
view: it => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSelectedSMS(it)
|
||||
setShowModal(true)
|
||||
}}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Delete',
|
||||
width: 100,
|
||||
size: 'sm',
|
||||
textAlign: 'center',
|
||||
view: it => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSelectedSMS(it)
|
||||
setDeleteDialog(true)
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<H4>Custom SMS message</H4>
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
<Link color="primary" onClick={() => handleOpen()}>
|
||||
Add custom SMS
|
||||
</Link>
|
||||
</Box>
|
||||
</div>
|
||||
{showModal && (
|
||||
<CustomSMSModal
|
||||
showModal={showModal}
|
||||
onClose={handleClose}
|
||||
eventOptions={EVENT_OPTIONS}
|
||||
sms={selectedSMS}
|
||||
creationError={errorMsg}
|
||||
submit={selectedSMS ? editMessage : createMessage}
|
||||
/>
|
||||
)}
|
||||
<DeleteDialog
|
||||
open={deleteDialog}
|
||||
onDismissed={() => {
|
||||
handleClose()
|
||||
}}
|
||||
onConfirmed={() => {
|
||||
handleClose()
|
||||
deleteMessage({
|
||||
variables: {
|
||||
id: selectedSMS.id
|
||||
}
|
||||
})
|
||||
}}
|
||||
errorMessage={errorMsg}
|
||||
/>
|
||||
<DataTable
|
||||
emptyText="No custom SMS so far"
|
||||
elements={elements}
|
||||
loading={loading}
|
||||
data={R.path(['customMessages'])(messagesData)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomSMS
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { spacer } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
header: {
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: 800
|
||||
},
|
||||
form: {
|
||||
'& > *': {
|
||||
marginTop: 20
|
||||
},
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
},
|
||||
footer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [['auto', 0, spacer * 3, 0]]
|
||||
},
|
||||
submit: {
|
||||
margin: [['auto', 0, 0, 'auto']]
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import { Form, Formik, Field } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { Autocomplete, TextInput } from 'src/components/inputs/formik'
|
||||
|
||||
import styles from './CustomSMS.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (mutationError) return 'Internal server error'
|
||||
if (formikErrors.event && formikTouched.event) return formikErrors.event
|
||||
if (formikErrors.message && formikTouched.message) return formikErrors.message
|
||||
return null
|
||||
}
|
||||
|
||||
const prefill = {
|
||||
smsCode: {
|
||||
validator: Yup.string()
|
||||
.required('The message content is required!')
|
||||
.trim()
|
||||
.test({
|
||||
name: 'has-code-tag',
|
||||
message: 'A #code tag is missing from the message!',
|
||||
exclusive: false,
|
||||
test: value => value?.match(/#code/g || [])?.length > 0
|
||||
})
|
||||
.test({
|
||||
name: 'has-single-code-tag',
|
||||
message: 'There should be a single #code tag!',
|
||||
exclusive: false,
|
||||
test: value => value?.match(/#code/g || [])?.length === 1
|
||||
})
|
||||
},
|
||||
cashOutDispenseReady: {
|
||||
validator: Yup.string()
|
||||
.required('The message content is required!')
|
||||
.trim()
|
||||
.test({
|
||||
name: 'has-timestamp-tag',
|
||||
message: 'A #timestamp tag is missing from the message!',
|
||||
exclusive: false,
|
||||
test: value => value?.match(/#timestamp/g || [])?.length > 0
|
||||
})
|
||||
.test({
|
||||
name: 'has-single-timestamp-tag',
|
||||
message: 'There should be a single #timestamp tag!',
|
||||
exclusive: false,
|
||||
test: value => value?.match(/#timestamp/g || [])?.length === 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const CustomSMSModal = ({
|
||||
showModal,
|
||||
onClose,
|
||||
sms,
|
||||
eventOptions,
|
||||
creationError,
|
||||
submit
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [selectedEvent, setSelectedEvent] = useState(sms?.event)
|
||||
|
||||
const initialValues = {
|
||||
event: !R.isNil(sms) ? sms.event : '',
|
||||
message: !R.isNil(sms) ? sms.message : ''
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
event: Yup.string().required('An event is required!'),
|
||||
message:
|
||||
prefill[selectedEvent]?.validator ??
|
||||
Yup.string()
|
||||
.required('The message content is required!')
|
||||
.trim()
|
||||
})
|
||||
|
||||
const handleSubmit = values => {
|
||||
sms
|
||||
? submit({
|
||||
variables: {
|
||||
id: sms.id,
|
||||
event: values.event,
|
||||
message: values.message
|
||||
}
|
||||
})
|
||||
: submit({
|
||||
variables: {
|
||||
event: values.event,
|
||||
message: values.message
|
||||
}
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal
|
||||
title={!R.isNil(sms) ? `Edit custom SMS` : `Add custom SMS`}
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={500}
|
||||
open={true}
|
||||
handleClose={onClose}>
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, errors, touched) =>
|
||||
handleSubmit(values, errors, touched)
|
||||
}>
|
||||
{({ values, errors, touched }) => (
|
||||
<Form id="custom-sms" className={classes.form}>
|
||||
<Field
|
||||
name="event"
|
||||
label="Event"
|
||||
fullWidth
|
||||
onChange={setSelectedEvent(values.event)}
|
||||
options={eventOptions}
|
||||
labelProp="display"
|
||||
valueProp="code"
|
||||
component={Autocomplete}
|
||||
/>
|
||||
<Field
|
||||
name="message"
|
||||
label="Message content"
|
||||
fullWidth
|
||||
multiline={true}
|
||||
rows={6}
|
||||
component={TextInput}
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
{getErrorMsg(errors, touched, creationError) && (
|
||||
<ErrorMessage>
|
||||
{getErrorMsg(errors, touched, creationError)}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
form="custom-sms"
|
||||
className={classes.submit}>
|
||||
{!R.isNil(sms) ? `Confirm` : `Create SMS`}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomSMSModal
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles, Paper } from '@material-ui/core'
|
||||
import { format } from 'date-fns/fp'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { H4, P, Label3 } from 'src/components/typography'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
import { ReactComponent as ExpandIconClosed } from 'src/styling/icons/action/expand/closed.svg'
|
||||
import { ReactComponent as ExpandIconOpen } from 'src/styling/icons/action/expand/open.svg'
|
||||
import { ReactComponent as WhiteLogo } from 'src/styling/icons/menu/logo-white.svg'
|
||||
|
||||
import styles from './SMSNotices.styles'
|
||||
import CustomSMSModal from './SMSNoticesModal'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const GET_SMS_NOTICES = gql`
|
||||
query SMSNotices {
|
||||
SMSNotices {
|
||||
id
|
||||
event
|
||||
message
|
||||
messageName
|
||||
enabled
|
||||
allowToggle
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const EDIT_SMS_NOTICE = gql`
|
||||
mutation editSMSNotice($id: ID!, $event: SMSNoticeEvent!, $message: String!) {
|
||||
editSMSNotice(id: $id, event: $event, message: $message) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ENABLE_SMS_NOTICE = gql`
|
||||
mutation enableSMSNotice($id: ID!) {
|
||||
enableSMSNotice(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const DISABLE_SMS_NOTICE = gql`
|
||||
mutation disableSMSNotice($id: ID!) {
|
||||
disableSMSNotice(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const multiReplace = (str, obj) => {
|
||||
var re = new RegExp(Object.keys(obj).join('|'), 'gi')
|
||||
|
||||
return str.replace(re, function(matched) {
|
||||
return obj[matched.toLowerCase()]
|
||||
})
|
||||
}
|
||||
|
||||
const formatContent = content => {
|
||||
const fragments = R.split(/\n/)(content)
|
||||
return R.map((it, idx) => {
|
||||
if (idx === fragments.length) return <>{it}</>
|
||||
return (
|
||||
<>
|
||||
{it}
|
||||
<br />
|
||||
</>
|
||||
)
|
||||
}, fragments)
|
||||
}
|
||||
|
||||
const TOOLTIPS = {
|
||||
smsCode: ``,
|
||||
cashOutDispenseReady: ``,
|
||||
smsReceipt: formatContent(`The contents of this notice will be appended to the end of the SMS receipt sent, and not replace it.\n
|
||||
To edit the contents of the SMS receipt, please go to the 'Receipt' tab`)
|
||||
}
|
||||
|
||||
const SMSPreview = ({ sms, coords }) => {
|
||||
const classes = useStyles(coords)
|
||||
|
||||
const matches = {
|
||||
'#code': 123,
|
||||
'#timestamp': format('HH:mm', new Date())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.smsPreview}>
|
||||
<div className={classes.smsPreviewContainer}>
|
||||
<div className={classes.smsPreviewIcon}>
|
||||
<WhiteLogo width={22} height={22} />
|
||||
</div>
|
||||
<Paper className={classes.smsPreviewContent}>
|
||||
<P noMargin>
|
||||
{R.isEmpty(sms?.message) ? (
|
||||
<i>No content available</i>
|
||||
) : (
|
||||
formatContent(multiReplace(sms?.message, matches))
|
||||
)}
|
||||
</P>
|
||||
</Paper>
|
||||
<Label3>{format('HH:mm', new Date())}</Label3>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SMSNotices = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [selectedSMS, setSelectedSMS] = useState(null)
|
||||
const [previewOpen, setPreviewOpen] = useState(false)
|
||||
const [previewCoords, setPreviewCoords] = useState({ x: 0, y: 0 })
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
|
||||
const { data: messagesData, loading: messagesLoading } = useQuery(
|
||||
GET_SMS_NOTICES
|
||||
)
|
||||
|
||||
const [editMessage] = useMutation(EDIT_SMS_NOTICE, {
|
||||
onError: ({ msg }) => setErrorMsg(msg),
|
||||
refetchQueries: () => ['SMSNotices']
|
||||
})
|
||||
|
||||
const [enableMessage] = useMutation(ENABLE_SMS_NOTICE, {
|
||||
onError: ({ msg }) => setErrorMsg(msg),
|
||||
refetchQueries: () => ['SMSNotices']
|
||||
})
|
||||
|
||||
const [disableMessage] = useMutation(DISABLE_SMS_NOTICE, {
|
||||
onError: ({ msg }) => setErrorMsg(msg),
|
||||
refetchQueries: () => ['SMSNotices']
|
||||
})
|
||||
|
||||
const loading = messagesLoading
|
||||
|
||||
const handleClose = () => {
|
||||
setShowModal(false)
|
||||
setSelectedSMS(null)
|
||||
}
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Message name',
|
||||
width: 500,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: it =>
|
||||
!R.isEmpty(TOOLTIPS[it.event]) ? (
|
||||
<div className={classes.messageWithTooltip}>
|
||||
{R.prop('messageName', it)}
|
||||
<HoverableTooltip width={250}>
|
||||
<P>{TOOLTIPS[it.event]}</P>
|
||||
</HoverableTooltip>
|
||||
</div>
|
||||
) : (
|
||||
R.prop('messageName', it)
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Edit',
|
||||
width: 100,
|
||||
size: 'sm',
|
||||
textAlign: 'center',
|
||||
view: it => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setPreviewOpen(false)
|
||||
setSelectedSMS(it)
|
||||
setShowModal(true)
|
||||
}}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Enable',
|
||||
width: 100,
|
||||
size: 'sm',
|
||||
textAlign: 'center',
|
||||
view: it => (
|
||||
<Switch
|
||||
disabled={!it.allowToggle}
|
||||
onClick={() => {
|
||||
it.enabled
|
||||
? disableMessage({ variables: { id: it.id } })
|
||||
: enableMessage({ variables: { id: it.id } })
|
||||
}}
|
||||
checked={it.enabled}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
width: 100,
|
||||
size: 'sm',
|
||||
textAlign: 'center',
|
||||
view: it => (
|
||||
<IconButton
|
||||
onClick={e => {
|
||||
setSelectedSMS(it)
|
||||
setPreviewCoords({
|
||||
x: e.currentTarget.getBoundingClientRect().right + 50,
|
||||
y:
|
||||
window.innerHeight -
|
||||
5 -
|
||||
e.currentTarget.getBoundingClientRect().bottom
|
||||
})
|
||||
R.equals(selectedSMS, it)
|
||||
? setPreviewOpen(!previewOpen)
|
||||
: setPreviewOpen(true)
|
||||
}}>
|
||||
{R.equals(selectedSMS, it) && previewOpen ? (
|
||||
<ExpandIconOpen />
|
||||
) : (
|
||||
<ExpandIconClosed />
|
||||
)}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<H4>SMS notices</H4>
|
||||
</div>
|
||||
{showModal && (
|
||||
<CustomSMSModal
|
||||
showModal={showModal}
|
||||
onClose={handleClose}
|
||||
sms={selectedSMS}
|
||||
creationError={errorMsg}
|
||||
submit={editMessage}
|
||||
/>
|
||||
)}
|
||||
{previewOpen && <SMSPreview sms={selectedSMS} coords={previewCoords} />}
|
||||
<DataTable
|
||||
emptyText="No SMS notices so far"
|
||||
elements={elements}
|
||||
loading={loading}
|
||||
data={R.path(['SMSNotices'])(messagesData)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SMSNotices
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import {
|
||||
spacer,
|
||||
fontMonospaced,
|
||||
fontSize5,
|
||||
fontColor
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
header: {
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: 800
|
||||
},
|
||||
form: {
|
||||
'& > *': {
|
||||
marginTop: 20
|
||||
},
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
},
|
||||
footer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [['auto', 0, spacer * 3, 0]]
|
||||
},
|
||||
submit: {
|
||||
margin: [['auto', 0, 0, 'auto']]
|
||||
},
|
||||
smsPreview: {
|
||||
position: 'absolute',
|
||||
left: ({ x }) => x,
|
||||
bottom: ({ y }) => y,
|
||||
width: 350,
|
||||
overflow: 'visible'
|
||||
},
|
||||
smsPreviewContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
'& > *': {
|
||||
marginRight: 10
|
||||
}
|
||||
},
|
||||
smsPreviewIcon: {
|
||||
display: 'flex',
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#16D6D3',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
smsPreviewContent: {
|
||||
width: 225,
|
||||
padding: 15,
|
||||
borderRadius: '15px 15px 15px 0px'
|
||||
},
|
||||
chipButtons: {
|
||||
width: 480,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'space-between',
|
||||
'& > div': {
|
||||
marginTop: 15
|
||||
},
|
||||
'& > div:first-child': {
|
||||
marginTop: 0
|
||||
},
|
||||
'& > div > div': {
|
||||
margin: [[0, 5, 0, 5]]
|
||||
},
|
||||
'& > div > div > span': {
|
||||
lineHeight: '120%',
|
||||
color: fontColor,
|
||||
fontSize: fontSize5,
|
||||
fontFamily: fontMonospaced,
|
||||
fontWeight: 500
|
||||
},
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto'
|
||||
},
|
||||
resetToDefault: {
|
||||
width: 145
|
||||
},
|
||||
messageWithTooltip: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
import { makeStyles, Chip } from '@material-ui/core'
|
||||
import { Form, Formik, Field } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { ActionButton, Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import { Info2 } from 'src/components/typography'
|
||||
import { ReactComponent as DefaultIconReverse } from 'src/styling/icons/button/retry/white.svg'
|
||||
import { ReactComponent as DefaultIcon } from 'src/styling/icons/button/retry/zodiac.svg'
|
||||
import { zircon } from 'src/styling/variables'
|
||||
|
||||
import styles from './SMSNotices.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (mutationError) return 'Internal server error'
|
||||
if (formikErrors.event && formikTouched.event) return formikErrors.event
|
||||
if (formikErrors.message && formikTouched.message) return formikErrors.message
|
||||
return null
|
||||
}
|
||||
|
||||
const PREFILL = {
|
||||
smsCode: {
|
||||
validator: Yup.string()
|
||||
.required('The message content is required!')
|
||||
.trim()
|
||||
.test({
|
||||
name: 'has-code',
|
||||
message: 'The confirmation code is missing from the message!',
|
||||
exclusive: false,
|
||||
test: value => value?.match(/#code/g || [])?.length > 0
|
||||
})
|
||||
.test({
|
||||
name: 'has-single-code',
|
||||
message: 'There should be a single confirmation code!',
|
||||
exclusive: false,
|
||||
test: value => value?.match(/#code/g || [])?.length === 1
|
||||
})
|
||||
},
|
||||
cashOutDispenseReady: {
|
||||
validator: Yup.string()
|
||||
.required('The message content is required!')
|
||||
.trim()
|
||||
},
|
||||
smsReceipt: {
|
||||
validator: Yup.string().trim()
|
||||
}
|
||||
}
|
||||
|
||||
const CHIPS = {
|
||||
smsCode: [
|
||||
{ code: '#code', display: 'Confirmation code', obligatory: true },
|
||||
{ code: '#timestamp', display: 'Timestamp', obligatory: false }
|
||||
],
|
||||
cashOutDispenseReady: [
|
||||
{ code: '#timestamp', display: 'Timestamp', obligatory: false }
|
||||
],
|
||||
smsReceipt: [{ code: '#timestamp', display: 'Timestamp', obligatory: false }]
|
||||
}
|
||||
|
||||
const DEFAULT_MESSAGES = {
|
||||
smsCode: 'Your cryptomat code: #code',
|
||||
cashOutDispenseReady:
|
||||
'Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours. [#timestamp]',
|
||||
smsReceipt: ''
|
||||
}
|
||||
|
||||
const SMSNoticesModal = ({
|
||||
showModal,
|
||||
onClose,
|
||||
sms,
|
||||
creationError,
|
||||
submit
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const initialValues = {
|
||||
event: !R.isNil(sms) ? sms.event : '',
|
||||
message: !R.isNil(sms) ? sms.message : ''
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
event: Yup.string().required('An event is required!'),
|
||||
message:
|
||||
PREFILL[sms?.event]?.validator ??
|
||||
Yup.string()
|
||||
.required('The message content is required!')
|
||||
.trim()
|
||||
})
|
||||
|
||||
const handleSubmit = values => {
|
||||
sms
|
||||
? submit({
|
||||
variables: {
|
||||
id: sms.id,
|
||||
event: values.event,
|
||||
message: values.message
|
||||
}
|
||||
})
|
||||
: submit({
|
||||
variables: {
|
||||
event: values.event,
|
||||
message: values.message
|
||||
}
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal
|
||||
title={`SMS notice - ${sms?.messageName}`}
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={500}
|
||||
open={true}
|
||||
handleClose={onClose}>
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, errors, touched) =>
|
||||
handleSubmit(values, errors, touched)
|
||||
}>
|
||||
{({ values, errors, touched, setFieldValue }) => (
|
||||
<Form id="sms-notice" className={classes.form}>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={DefaultIcon}
|
||||
InverseIcon={DefaultIconReverse}
|
||||
className={classes.resetToDefault}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFieldValue('message', DEFAULT_MESSAGES[sms?.event])
|
||||
}>
|
||||
Reset to default
|
||||
</ActionButton>
|
||||
<Field
|
||||
name="message"
|
||||
label="Message content"
|
||||
fullWidth
|
||||
multiline={true}
|
||||
rows={6}
|
||||
component={TextInput}
|
||||
/>
|
||||
{R.length(CHIPS[sms?.event]) > 0 && (
|
||||
<Info2 noMargin>Values to attach</Info2>
|
||||
)}
|
||||
<div className={classes.chipButtons}>
|
||||
{R.map(
|
||||
it => (
|
||||
<div>
|
||||
{R.map(
|
||||
ite => (
|
||||
<Chip
|
||||
label={ite.display}
|
||||
size="small"
|
||||
style={{ backgroundColor: zircon }}
|
||||
disabled={R.includes(ite.code, values.message)}
|
||||
className={classes.chip}
|
||||
onClick={() => {
|
||||
setFieldValue(
|
||||
'message',
|
||||
values.message.concat(
|
||||
R.last(values.message) === ' ' ? '' : ' ',
|
||||
ite.code
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
),
|
||||
it
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
R.splitEvery(3, CHIPS[sms?.event])
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.footer}>
|
||||
{getErrorMsg(errors, touched, creationError) && (
|
||||
<ErrorMessage>
|
||||
{getErrorMsg(errors, touched, creationError)}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
form="sms-notice"
|
||||
className={classes.submit}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SMSNoticesModal
|
||||
|
|
@ -16,8 +16,8 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
|||
import Notifications from 'src/pages/Notifications/Notifications'
|
||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||
import CustomSMS from 'src/pages/OperatorInfo/CustomSMS/CustomSMS'
|
||||
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
||||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||
import ServerLogs from 'src/pages/ServerLogs'
|
||||
import Services from 'src/pages/Services/Services'
|
||||
|
|
@ -174,11 +174,11 @@ const getLamassuRoutes = () => [
|
|||
component: ReceiptPrinting
|
||||
},
|
||||
{
|
||||
key: 'custom-sms',
|
||||
label: 'Custom SMS',
|
||||
route: '/settings/operator-info/custom-sms',
|
||||
key: 'sms-notices',
|
||||
label: 'SMS notices',
|
||||
route: '/settings/operator-info/sms-notices',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: CustomSMS
|
||||
component: SMSNotices
|
||||
},
|
||||
{
|
||||
key: 'coin-atm-radar',
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
|||
import Notifications from 'src/pages/Notifications/Notifications'
|
||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||
import CustomSMS from 'src/pages/OperatorInfo/CustomSMS/CustomSMS'
|
||||
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
||||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||
import ServerLogs from 'src/pages/ServerLogs'
|
||||
import Services from 'src/pages/Services/Services'
|
||||
|
|
@ -169,11 +169,11 @@ const getPazuzRoutes = () => [
|
|||
component: ReceiptPrinting
|
||||
},
|
||||
{
|
||||
key: 'custom-sms',
|
||||
label: 'Custom SMS',
|
||||
route: '/settings/operator-info/custom-sms',
|
||||
key: 'sms-notices',
|
||||
label: 'SMS notices',
|
||||
route: '/settings/operator-info/sms-notices',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: CustomSMS
|
||||
component: SMSNotices
|
||||
},
|
||||
{
|
||||
key: 'coin-atm-radar',
|
||||
|
|
|
|||
20
new-lamassu-admin/src/styling/icons/menu/logo-white.svg
Normal file
20
new-lamassu-admin/src/styling/icons/menu/logo-white.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7 KiB |
Loading…
Add table
Add a link
Reference in a new issue