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 CASH_OUT_DISPENSE_READY = 'cash_out_dispense_ready'
|
||||||
const CONFIRMATION_CODE = 'sms_code'
|
const CONFIRMATION_CODE = 'sms_code'
|
||||||
|
const RECEIPT = 'sms_receipt'
|
||||||
|
|
||||||
const WALLET_SCORE_THRESHOLD = 9
|
const WALLET_SCORE_THRESHOLD = 9
|
||||||
|
|
||||||
|
|
@ -37,5 +38,6 @@ module.exports = {
|
||||||
CONFIRMATION_CODE,
|
CONFIRMATION_CODE,
|
||||||
CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES,
|
CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES,
|
||||||
CASH_OUT_MAXIMUM_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 = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
customMessages: () => customSms.getCustomMessages()
|
SMSNotices: () => smsNotices.getSMSNotices()
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createCustomMessage: (...[, { event, message }]) => customSms.createCustomMessage(event, message),
|
editSMSNotice: (...[, { id, event, message }]) => smsNotices.editSMSNotice(id, event, message),
|
||||||
editCustomMessage: (...[, { id, event, message }]) => customSms.editCustomMessage(id, event, message),
|
enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id),
|
||||||
deleteCustomMessage: (...[, { id }]) => customSms.deleteCustomMessage(id)
|
disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,29 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const { gql } = require('apollo-server-express')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type CustomMessage {
|
type SMSNotice {
|
||||||
id: ID!
|
id: ID!
|
||||||
event: CustomMessageEvent!
|
event: SMSNoticeEvent!
|
||||||
message: String!
|
message: String!
|
||||||
|
messageName: String!
|
||||||
|
enabled: Boolean!
|
||||||
|
allowToggle: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CustomMessageEvent {
|
enum SMSNoticeEvent {
|
||||||
smsCode
|
smsCode
|
||||||
cashOutDispenseReady
|
cashOutDispenseReady
|
||||||
|
smsReceipt
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
customMessages: [CustomMessage] @auth
|
SMSNotices: [SMSNotice] @auth
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
createCustomMessage(event: CustomMessageEvent!, message: String!): CustomMessage @auth
|
editSMSNotice(id: ID!, event: SMSNoticeEvent!, message: String!): SMSNotice @auth
|
||||||
editCustomMessage(id: ID!, event: CustomMessageEvent!, message: String!): CustomMessage @auth
|
enableSMSNotice(id: ID!): SMSNotice @auth
|
||||||
deleteCustomMessage(id: ID!): CustomMessage @auth
|
disableSMSNotice(id: ID!): SMSNotice @auth
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -772,7 +772,8 @@ function plugins (settings, deviceId) {
|
||||||
? '123'
|
? '123'
|
||||||
: randomCode()
|
: 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 => {
|
.then(smsObj => {
|
||||||
const rec = {
|
const rec = {
|
||||||
sms: smsObj
|
sms: smsObj
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const NAME = 'FakeWallet'
|
||||||
const SECONDS = 1000
|
const SECONDS = 1000
|
||||||
const PUBLISH_TIME = 3 * SECONDS
|
const PUBLISH_TIME = 3 * SECONDS
|
||||||
const AUTHORIZE_TIME = PUBLISH_TIME + 5 * SECONDS
|
const AUTHORIZE_TIME = PUBLISH_TIME + 5 * SECONDS
|
||||||
const CONFIRM_TIME = AUTHORIZE_TIME + 120 * SECONDS
|
const CONFIRM_TIME = AUTHORIZE_TIME + 10 * SECONDS
|
||||||
|
|
||||||
let t0
|
let t0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,6 @@ router.patch('/:id/block', triggerBlock)
|
||||||
router.patch('/:id/suspend', triggerSuspend)
|
router.patch('/:id/suspend', triggerSuspend)
|
||||||
router.patch('/:id/photos/idcarddata', updateIdCardData)
|
router.patch('/:id/photos/idcarddata', updateIdCardData)
|
||||||
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
|
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
|
||||||
router.patch('/:id/smsreceipt', sendSmsReceipt)
|
router.post('/:id/smsreceipt', sendSmsReceipt)
|
||||||
|
|
||||||
module.exports = router
|
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 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 _ = require('lodash/fp')
|
||||||
|
|
||||||
const customSms = require('./custom-sms')
|
const smsNotices = require('./sms-notices')
|
||||||
|
const { RECEIPT } = require('./constants')
|
||||||
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}]`
|
|
||||||
})
|
|
||||||
|
|
||||||
function getSms (event, phone, content) {
|
function getSms (event, phone, content) {
|
||||||
return customSms.getCustomMessage(event)
|
return smsNotices.getSMSNotice(event)
|
||||||
.then(msg => {
|
.then(msg => {
|
||||||
if (!_.isNil(msg)) {
|
if (!_.isNil(msg)) {
|
||||||
var accMsg = msg.message
|
var accMsg = msg.message
|
||||||
|
|
@ -22,11 +21,6 @@ function getSms (event, phone, content) {
|
||||||
body: messageContent
|
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`)
|
message = message.concat(`Address: ${data.address}\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = {
|
const timestamp = dateFormat(new Date(), 'UTC:HH:MM Z')
|
||||||
sms: {
|
const postReceiptSmsPromise = getSms(RECEIPT, data.customerPhone, { timestamp })
|
||||||
toNumber: data.customerPhone,
|
|
||||||
body: message
|
return Promise.all([smsNotices.getSMSNotice(RECEIPT), postReceiptSmsPromise])
|
||||||
}
|
.then(([res, postReceiptSms]) => ({
|
||||||
}
|
sms: {
|
||||||
return request
|
toNumber: data.customerPhone,
|
||||||
|
body: res.enabled ? message.concat('\n\n', postReceiptSms.body) : message
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
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 Notifications from 'src/pages/Notifications/Notifications'
|
||||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||||
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||||
import CustomSMS from 'src/pages/OperatorInfo/CustomSMS/CustomSMS'
|
|
||||||
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||||
|
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
||||||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||||
import ServerLogs from 'src/pages/ServerLogs'
|
import ServerLogs from 'src/pages/ServerLogs'
|
||||||
import Services from 'src/pages/Services/Services'
|
import Services from 'src/pages/Services/Services'
|
||||||
|
|
@ -174,11 +174,11 @@ const getLamassuRoutes = () => [
|
||||||
component: ReceiptPrinting
|
component: ReceiptPrinting
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'custom-sms',
|
key: 'sms-notices',
|
||||||
label: 'Custom SMS',
|
label: 'SMS notices',
|
||||||
route: '/settings/operator-info/custom-sms',
|
route: '/settings/operator-info/sms-notices',
|
||||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: CustomSMS
|
component: SMSNotices
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'coin-atm-radar',
|
key: 'coin-atm-radar',
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||||
import Notifications from 'src/pages/Notifications/Notifications'
|
import Notifications from 'src/pages/Notifications/Notifications'
|
||||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||||
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||||
import CustomSMS from 'src/pages/OperatorInfo/CustomSMS/CustomSMS'
|
|
||||||
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||||
|
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
||||||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||||
import ServerLogs from 'src/pages/ServerLogs'
|
import ServerLogs from 'src/pages/ServerLogs'
|
||||||
import Services from 'src/pages/Services/Services'
|
import Services from 'src/pages/Services/Services'
|
||||||
|
|
@ -169,11 +169,11 @@ const getPazuzRoutes = () => [
|
||||||
component: ReceiptPrinting
|
component: ReceiptPrinting
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'custom-sms',
|
key: 'sms-notices',
|
||||||
label: 'Custom SMS',
|
label: 'SMS notices',
|
||||||
route: '/settings/operator-info/custom-sms',
|
route: '/settings/operator-info/sms-notices',
|
||||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: CustomSMS
|
component: SMSNotices
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'coin-atm-radar',
|
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