feat: scripting-like tags on custom sms content
feat: add created column to custom_messages feat: custom message dynamic validators and testing feat: delete custom sms feat: employ custom sms to existing events
This commit is contained in:
parent
3480bbf8f7
commit
54b73b95b4
10 changed files with 444 additions and 95 deletions
|
|
@ -1,17 +1,50 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
const uuid = require('uuid')
|
const uuid = require('uuid')
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
|
|
||||||
const getCustomMessages = () => {
|
const getCustomMessages = () => {
|
||||||
const sql = `SELECT * FROM custom_messages`
|
const sql = `SELECT * FROM custom_messages ORDER BY created`
|
||||||
return db.any(sql)
|
return db.any(sql).then(res => _.map(
|
||||||
|
it => ({
|
||||||
|
id: it.id,
|
||||||
|
event: _.camelCase(it.event),
|
||||||
|
deviceId: it.device_id,
|
||||||
|
message: it.message
|
||||||
|
}), res))
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCustomMessage = (event, deviceId, message) => {
|
const createCustomMessage = (event, deviceId, message) => {
|
||||||
const sql = `INSERT INTO custom_message (event, device_id, message) VALUES ($2, $3, $4)`
|
const machineId = deviceId === 'ALL_MACHINES' ? null : deviceId
|
||||||
return db.none(sql, [uuid.v4(), event, deviceId, message])
|
const sql = `INSERT INTO custom_messages (id, event, device_id, message) VALUES ($1, $2, $3, $4)`
|
||||||
|
return db.none(sql, [uuid.v4(), _.snakeCase(event), machineId, message])
|
||||||
|
}
|
||||||
|
|
||||||
|
const editCustomMessage = (id, event, deviceId, message) => {
|
||||||
|
const machineId = deviceId === 'ALL_MACHINES' ? null : deviceId
|
||||||
|
const sql = `UPDATE custom_messages SET event=$2, device_id=$3, message=$4 WHERE id=$1`
|
||||||
|
return db.none(sql, [id, _.snakeCase(event), machineId, message])
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteCustomMessage = id => {
|
||||||
|
const sql = `DELETE FROM custom_messages WHERE id=$1`
|
||||||
|
return db.none(sql, [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCommonCustomMessages = event => {
|
||||||
|
const sql = `SELECT * FROM custom_messages WHERE event=$1 AND device_id IS NULL LIMIT 1`
|
||||||
|
return db.oneOrNone(sql, [event])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMachineCustomMessages = (event, deviceId) => {
|
||||||
|
const sql = `SELECT * FROM custom_messages WHERE event=$1 AND device_id=$2 LIMIT 1`
|
||||||
|
return db.oneOrNone(sql, [event, deviceId])
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getCustomMessages,
|
getCustomMessages,
|
||||||
createCustomMessage
|
createCustomMessage,
|
||||||
|
editCustomMessage,
|
||||||
|
deleteCustomMessage,
|
||||||
|
getCommonCustomMessages,
|
||||||
|
getMachineCustomMessages
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ const resolvers = {
|
||||||
customMessages: () => customSms.getCustomMessages()
|
customMessages: () => customSms.getCustomMessages()
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createCustomMessage: (...[, { event, deviceId, message }]) => customSms.createCustomMessage(event, deviceId, message)
|
createCustomMessage: (...[, { event, deviceId, message }]) => customSms.createCustomMessage(event, deviceId, message),
|
||||||
|
editCustomMessage: (...[, { id, event, deviceId, message }]) => customSms.editCustomMessage(id, event, deviceId, message),
|
||||||
|
deleteCustomMessage: (...[, { id }]) => customSms.deleteCustomMessage(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ const typeDef = gql`
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
createCustomMessage(event: CustomMessageEvent!, deviceId: String, message: String!): CustomMessage @auth
|
createCustomMessage(event: CustomMessageEvent!, deviceId: String, message: String!): CustomMessage @auth
|
||||||
|
editCustomMessage(id: ID!, event: CustomMessageEvent!, deviceId: String, message: String!): CustomMessage @auth
|
||||||
|
deleteCustomMessage(id: ID!): CustomMessage @auth
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -361,19 +361,19 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
const phone = tx.phone
|
const phone = tx.phone
|
||||||
const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
|
const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
|
||||||
const rec = {
|
return sms.getCashOutReadySms(deviceId, phone, code)
|
||||||
sms: {
|
.then(msg => {
|
||||||
toNumber: phone,
|
const rec = {
|
||||||
body: `Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours. [${timestamp}]`
|
sms: msg
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return sms.sendMessage(settings, rec)
|
return sms.sendMessage(settings, rec)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const sql = 'update cash_out_txs set notified=$1 where id=$2'
|
const sql = 'update cash_out_txs set notified=$1 where id=$2'
|
||||||
const values = [true, tx.id]
|
const values = [true, tx.id]
|
||||||
|
|
||||||
return db.none(sql, values)
|
return db.none(sql, values)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -723,15 +723,15 @@ function plugins (settings, deviceId) {
|
||||||
? '123'
|
? '123'
|
||||||
: randomCode()
|
: randomCode()
|
||||||
|
|
||||||
const rec = {
|
return sms.getPhoneCodeSms(deviceId, phone, code)
|
||||||
sms: {
|
.then(msg => {
|
||||||
toNumber: phone,
|
const rec = {
|
||||||
body: 'Your cryptomat code: ' + code
|
sms: msg
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return sms.sendMessage(settings, rec)
|
return sms.sendMessage(settings, rec)
|
||||||
.then(() => code)
|
.then(() => code)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sweepHdRow (row) {
|
function sweepHdRow (row) {
|
||||||
|
|
|
||||||
70
lib/sms.js
70
lib/sms.js
|
|
@ -1,6 +1,67 @@
|
||||||
const ph = require('./plugin-helper')
|
const ph = require('./plugin-helper')
|
||||||
const argv = require('minimist')(process.argv.slice(2))
|
const argv = require('minimist')(process.argv.slice(2))
|
||||||
const { utils: coinUtils } = require('lamassu-coins')
|
const { utils: coinUtils } = require('lamassu-coins')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
const customSms = require('./custom-sms')
|
||||||
|
|
||||||
|
function getPhoneCodeSms (deviceId, phone, code) {
|
||||||
|
return Promise.all([
|
||||||
|
customSms.getCommonCustomMessages('sms_code'),
|
||||||
|
customSms.getMachineCustomMessages('sms_code', deviceId)
|
||||||
|
])
|
||||||
|
.then(([commonMsg, machineMsg]) => {
|
||||||
|
if (!_.isNil(machineMsg)) {
|
||||||
|
const messageContent = _.replace('#code', code, machineMsg.message)
|
||||||
|
return {
|
||||||
|
toNumber: phone,
|
||||||
|
body: messageContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isNil(commonMsg)) {
|
||||||
|
const messageContent = _.replace('#code', code, commonMsg.message)
|
||||||
|
return {
|
||||||
|
toNumber: phone,
|
||||||
|
body: messageContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toNumber: phone,
|
||||||
|
body: `Your cryptomat code: ${code}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCashOutReadySms (deviceId, phone, timestamp) {
|
||||||
|
return Promise.all([
|
||||||
|
customSms.getCommonCustomMessages('cash_out_dispense_ready'),
|
||||||
|
customSms.getMachineCustomMessages('cash_out_dispense_ready', deviceId)
|
||||||
|
])
|
||||||
|
.then(([commonMsg, machineMsg]) => {
|
||||||
|
if (!_.isNil(machineMsg)) {
|
||||||
|
const messageContent = _.replace('#timestamp', timestamp, machineMsg.message)
|
||||||
|
return {
|
||||||
|
toNumber: phone,
|
||||||
|
body: messageContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isNil(commonMsg)) {
|
||||||
|
const messageContent = _.replace('#timestamp', timestamp, commonMsg.message)
|
||||||
|
return {
|
||||||
|
toNumber: phone,
|
||||||
|
body: messageContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toNumber: phone,
|
||||||
|
body: `Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours. [${timestamp}]`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function getPlugin (settings) {
|
function getPlugin (settings) {
|
||||||
const pluginCode = argv.mockSms ? 'mock-sms' : 'twilio'
|
const pluginCode = argv.mockSms ? 'mock-sms' : 'twilio'
|
||||||
|
|
@ -91,4 +152,11 @@ function formatSmsReceipt (data, options) {
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { sendMessage, formatSmsReceipt, getLookup, toCryptoUnits }
|
module.exports = {
|
||||||
|
getPhoneCodeSms,
|
||||||
|
getCashOutReadySms,
|
||||||
|
sendMessage,
|
||||||
|
getLookup,
|
||||||
|
formatSmsReceipt,
|
||||||
|
toCryptoUnits
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ exports.up = function (next) {
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
event custom_message_event NOT NULL,
|
event custom_message_event NOT NULL,
|
||||||
device_id TEXT REFERENCES devices(device_id),
|
device_id TEXT REFERENCES devices(device_id),
|
||||||
message TEXT NOT NULL
|
message TEXT NOT NULL,
|
||||||
|
created TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
)`,
|
)`,
|
||||||
`CREATE UNIQUE INDEX uq_custom_message_per_device ON custom_messages (event, device_id)`
|
`CREATE UNIQUE INDEX uq_custom_message_per_device ON custom_messages (event, device_id) WHERE device_id IS NOT NULL`,
|
||||||
|
`CREATE UNIQUE INDEX uq_custom_message_all_devices ON custom_messages (event) WHERE device_id IS NULL`
|
||||||
]
|
]
|
||||||
|
|
||||||
db.multi(sql, next)
|
db.multi(sql, next)
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import { useQuery } from '@apollo/react-hooks'
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
import { makeStyles, Box } from '@material-ui/core'
|
import { makeStyles, Box } from '@material-ui/core'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import { DeleteDialog } from 'src/components/DeleteDialog'
|
||||||
import { Link, IconButton } from 'src/components/buttons'
|
import { Link, IconButton } from 'src/components/buttons'
|
||||||
import DataTable from 'src/components/tables/DataTable'
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
import { H4 } from 'src/components/typography'
|
import { H4 } from 'src/components/typography'
|
||||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
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 { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||||
|
|
||||||
import { global } from '../OperatorInfo.styles'
|
import styles from './CustomSMS.styles'
|
||||||
|
|
||||||
import CustomSMSModal from './CustomSMSModal'
|
import CustomSMSModal from './CustomSMSModal'
|
||||||
|
|
||||||
const useStyles = makeStyles(global)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const GET_CUSTOM_MESSAGES = gql`
|
const GET_CUSTOM_MESSAGES = gql`
|
||||||
query customMessages {
|
query customMessages {
|
||||||
|
|
@ -27,6 +27,44 @@ const GET_CUSTOM_MESSAGES = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const CREATE_CUSTOM_MESSAGE = gql`
|
||||||
|
mutation createCustomMessage(
|
||||||
|
$event: CustomMessageEvent!
|
||||||
|
$deviceId: String!
|
||||||
|
$message: String!
|
||||||
|
) {
|
||||||
|
createCustomMessage(event: $event, deviceId: $deviceId, message: $message) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const EDIT_CUSTOM_MESSAGE = gql`
|
||||||
|
mutation editCustomMessage(
|
||||||
|
$id: ID!
|
||||||
|
$event: CustomMessageEvent!
|
||||||
|
$deviceId: String!
|
||||||
|
$message: String!
|
||||||
|
) {
|
||||||
|
editCustomMessage(
|
||||||
|
id: $id
|
||||||
|
event: $event
|
||||||
|
deviceId: $deviceId
|
||||||
|
message: $message
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DELETE_CUSTOM_MESSAGE = gql`
|
||||||
|
mutation deleteCustomMessage($id: ID!) {
|
||||||
|
deleteCustomMessage(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const GET_MACHINES = gql`
|
const GET_MACHINES = gql`
|
||||||
{
|
{
|
||||||
machines {
|
machines {
|
||||||
|
|
@ -36,43 +74,91 @@ const GET_MACHINES = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const EVENT_OPTIONS = [
|
||||||
|
{ code: 'smsCode', display: 'On SMS confirmation code' },
|
||||||
|
{ code: 'cashOutDispenseReady', display: 'Cash out dispense ready' }
|
||||||
|
]
|
||||||
|
|
||||||
const CustomSMS = () => {
|
const CustomSMS = () => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [deleteDialog, setDeleteDialog] = useState(false)
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
|
const [selectedSMS, setSelectedSMS] = useState(null)
|
||||||
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
|
|
||||||
const { data: messagesData, loading: messagesLoading } = useQuery(
|
const { data: messagesData, loading: messagesLoading } = useQuery(
|
||||||
GET_CUSTOM_MESSAGES
|
GET_CUSTOM_MESSAGES
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data: machinesData, loading: machinesLoading } = useQuery(
|
const { data: machinesData, loading: machinesLoading } = useQuery(
|
||||||
GET_MACHINES
|
GET_MACHINES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 && machinesLoading
|
const loading = messagesLoading && machinesLoading
|
||||||
|
|
||||||
const machineOptions =
|
const machineOptions =
|
||||||
machinesData &&
|
(machinesData &&
|
||||||
R.map(
|
R.map(
|
||||||
it => ({ code: it.deviceId, display: it.name }),
|
it => ({ code: it.deviceId, display: it.name }),
|
||||||
R.path(['machines'])(machinesData)
|
R.path(['machines'])(machinesData)
|
||||||
)
|
)) ??
|
||||||
|
[]
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setSelectedSMS(null)
|
||||||
|
setShowModal(false)
|
||||||
|
setDeleteDialog(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setErrorMsg('')
|
||||||
|
setShowModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
header: 'Message name',
|
header: 'Event',
|
||||||
width: 400,
|
width: 400,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
view: it => it.event
|
view: it =>
|
||||||
|
R.find(ite => R.propEq('event', ite.code, it), EVENT_OPTIONS).display
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Machine',
|
||||||
|
width: 200,
|
||||||
|
size: 'sm',
|
||||||
|
textAlign: 'left',
|
||||||
|
view: it =>
|
||||||
|
R.find(ite => R.propEq('deviceId', ite.code, it), machineOptions)
|
||||||
|
?.display ?? `All Machines`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Edit',
|
header: 'Edit',
|
||||||
width: 120,
|
width: 100,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
view: it => (
|
view: it => (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('edit')
|
setSelectedSMS(it)
|
||||||
|
setShowModal(true)
|
||||||
}}>
|
}}>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
@ -80,13 +166,14 @@ const CustomSMS = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Delete',
|
header: 'Delete',
|
||||||
width: 120,
|
width: 100,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
view: it => (
|
view: it => (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('delete')
|
setSelectedSMS(it)
|
||||||
|
setDeleteDialog(true)
|
||||||
}}>
|
}}>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
@ -96,13 +183,10 @@ const CustomSMS = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classes.headerWithLink}>
|
<div className={classes.header}>
|
||||||
<H4>Custom SMS message</H4>
|
<H4>Custom SMS message</H4>
|
||||||
<Box
|
<Box display="flex" justifyContent="flex-end">
|
||||||
className={classes.tableWidth}
|
<Link color="primary" onClick={() => handleOpen()}>
|
||||||
display="flex"
|
|
||||||
justifyContent="flex-end">
|
|
||||||
<Link color="primary" onClick={() => setShowModal(true)}>
|
|
||||||
Add custom SMS
|
Add custom SMS
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -110,10 +194,29 @@ const CustomSMS = () => {
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<CustomSMSModal
|
<CustomSMSModal
|
||||||
showModal={showModal}
|
showModal={showModal}
|
||||||
onClose={() => setShowModal(false)}
|
onClose={handleClose}
|
||||||
machineOptions={machineOptions}
|
machineOptions={machineOptions}
|
||||||
|
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
|
<DataTable
|
||||||
emptyText="No custom SMS so far"
|
emptyText="No custom SMS so far"
|
||||||
elements={elements}
|
elements={elements}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
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,40 +1,128 @@
|
||||||
|
import { makeStyles } from '@material-ui/core'
|
||||||
import { Form, Formik, Field } from 'formik'
|
import { Form, Formik, Field } from 'formik'
|
||||||
import React from 'react'
|
import * as R from 'ramda'
|
||||||
|
import React, { useState } from 'react'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { Autocomplete } from 'src/components/inputs/formik'
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { Autocomplete, TextInput } from 'src/components/inputs/formik'
|
||||||
|
|
||||||
const EVENT_OPTIONS = [
|
import styles from './CustomSMS.styles'
|
||||||
{ code: 'sms_code', display: 'On SMS confirmation code' },
|
|
||||||
{ code: 'cash_out_dispense_ready', display: 'Cash out dispense ready' }
|
const useStyles = makeStyles(styles)
|
||||||
]
|
|
||||||
|
const ALL_MACHINES = {
|
||||||
|
code: 'ALL_MACHINES',
|
||||||
|
display: 'All Machines'
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {
|
||||||
|
tags: [],
|
||||||
|
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: {
|
||||||
|
tags: [],
|
||||||
|
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 = ({
|
const CustomSMSModal = ({
|
||||||
showModal,
|
showModal,
|
||||||
onClose,
|
onClose,
|
||||||
customMessage,
|
sms,
|
||||||
machineOptions
|
machineOptions,
|
||||||
|
eventOptions,
|
||||||
|
creationError,
|
||||||
|
submit
|
||||||
}) => {
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState(sms?.event)
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
event: '',
|
event: !R.isNil(sms) ? sms.event : '',
|
||||||
device: '',
|
device: !R.isNil(sms)
|
||||||
message: ''
|
? !R.isNil(sms.deviceId)
|
||||||
|
? sms.deviceId
|
||||||
|
: 'ALL_MACHINES'
|
||||||
|
: '',
|
||||||
|
message: !R.isNil(sms) ? sms.message : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationSchema = {
|
const validationSchema = Yup.object().shape({
|
||||||
event: Yup.string().required('An event is required!'),
|
event: Yup.string().required('An event is required!'),
|
||||||
device: Yup.string(),
|
device: Yup.string().required('A machine is required!'),
|
||||||
message: Yup.string()
|
message:
|
||||||
.required('The message content is required!')
|
prefill[selectedEvent]?.validator ??
|
||||||
.trim()
|
Yup.string()
|
||||||
|
.required('The message content is required!')
|
||||||
|
.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = values => {
|
||||||
|
sms
|
||||||
|
? submit({
|
||||||
|
variables: {
|
||||||
|
id: sms.id,
|
||||||
|
event: values.event,
|
||||||
|
deviceId: values.device,
|
||||||
|
message: values.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: submit({
|
||||||
|
variables: {
|
||||||
|
event: values.event,
|
||||||
|
deviceId: values.device,
|
||||||
|
message: values.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<Modal
|
<Modal
|
||||||
title="Add custom SMS"
|
title={!R.isNil(sms) ? `Edit custom SMS` : `Add custom SMS`}
|
||||||
closeOnBackdropClick={true}
|
closeOnBackdropClick={true}
|
||||||
width={600}
|
width={600}
|
||||||
height={500}
|
height={500}
|
||||||
|
|
@ -44,25 +132,54 @@ const CustomSMSModal = ({
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
validationSchema={validationSchema}>
|
validationSchema={validationSchema}
|
||||||
<Form id="custom-sms">
|
onSubmit={(values, errors, touched) =>
|
||||||
<Field
|
handleSubmit(values, errors, touched)
|
||||||
name="event"
|
}>
|
||||||
fullWidth
|
{({ values, errors, touched }) => (
|
||||||
options={EVENT_OPTIONS}
|
<Form id="custom-sms" className={classes.form}>
|
||||||
labelProp="display"
|
<Field
|
||||||
valueProp="code"
|
name="event"
|
||||||
component={Autocomplete}
|
label="Event"
|
||||||
/>
|
fullWidth
|
||||||
<Field
|
onChange={setSelectedEvent(values.event)}
|
||||||
name="device"
|
options={eventOptions}
|
||||||
fullWidth
|
labelProp="display"
|
||||||
options={machineOptions}
|
valueProp="code"
|
||||||
labelProp="display"
|
component={Autocomplete}
|
||||||
valueProp="code"
|
/>
|
||||||
component={Autocomplete}
|
<Field
|
||||||
/>
|
name="device"
|
||||||
</Form>
|
label="Machine"
|
||||||
|
fullWidth
|
||||||
|
options={[ALL_MACHINES].concat(machineOptions)}
|
||||||
|
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>
|
</Formik>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,6 @@ const global = {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
flex: 'wrap'
|
flex: 'wrap'
|
||||||
},
|
},
|
||||||
headerWithLink: {
|
|
||||||
display: 'flex',
|
|
||||||
position: 'relative',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
width: 640
|
|
||||||
},
|
|
||||||
section: {
|
section: {
|
||||||
marginBottom: 52
|
marginBottom: 52
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue