chore: use monorepo organization
This commit is contained in:
parent
deaf7d6ecc
commit
a687827f7e
1099 changed files with 8184 additions and 11535 deletions
70
packages/admin-ui/src/pages/OperatorInfo/CoinATMRadar.jsx
Normal file
70
packages/admin-ui/src/pages/OperatorInfo/CoinATMRadar.jsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import SwitchRow from './components/SwitchRow.jsx'
|
||||
import Header from './components/Header.jsx'
|
||||
|
||||
const GET_CONFIG = gql`
|
||||
query getData {
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
}
|
||||
`
|
||||
|
||||
const CoinATMRadar = memo(({ wizard }) => {
|
||||
const { data } = useQuery(GET_CONFIG)
|
||||
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
refetchQueries: ['getData']
|
||||
})
|
||||
|
||||
const save = it =>
|
||||
saveConfig({
|
||||
variables: { config: toNamespace(namespaces.COIN_ATM_RADAR, it) }
|
||||
})
|
||||
|
||||
const coinAtmRadarConfig =
|
||||
data?.config && fromNamespace(namespaces.COIN_ATM_RADAR, data.config)
|
||||
if (!coinAtmRadarConfig) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title="Coin ATM Radar share settings"
|
||||
articleUrl="https://support.lamassu.is/hc/en-us/articles/360023720472-Coin-ATM-Radar"
|
||||
tooltipText="For details on configuring this panel, please read the relevant knowledgebase article."
|
||||
/>
|
||||
<SwitchRow
|
||||
title={'Share information?'}
|
||||
checked={coinAtmRadarConfig.active}
|
||||
save={value => save({ active: value })}
|
||||
/>
|
||||
<BooleanPropertiesTable
|
||||
editing={wizard}
|
||||
title="Machine info"
|
||||
data={coinAtmRadarConfig}
|
||||
elements={[
|
||||
{
|
||||
name: 'commissions',
|
||||
display: 'Commissions'
|
||||
},
|
||||
{
|
||||
name: 'limitsAndVerification',
|
||||
display: 'Limits and verification'
|
||||
}
|
||||
]}
|
||||
save={save}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default CoinATMRadar
|
||||
253
packages/admin-ui/src/pages/OperatorInfo/ContactInfo.jsx
Normal file
253
packages/admin-ui/src/pages/OperatorInfo/ContactInfo.jsx
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import IconButton from '@mui/material/IconButton'
|
||||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import { Form, Formik, Field as FormikField } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||
import { H4, Info3, Label3 } from 'src/components/typography'
|
||||
import EditIcon from 'src/styling/icons/action/edit/enabled.svg?react'
|
||||
import WarningIcon from 'src/styling/icons/warning-icon/comet.svg?react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Link } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import SwitchRow from './components/SwitchRow.jsx'
|
||||
import InfoMessage from './components/InfoMessage.jsx'
|
||||
import Header from './components/Header.jsx'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
|
||||
const FIELD_WIDTH = 280
|
||||
|
||||
const Field = ({ editing, field, displayValue, ...props }) => {
|
||||
return (
|
||||
<div className="w-70 h-12 p-0 pl-1 pb-1">
|
||||
{!editing && (
|
||||
<>
|
||||
<Label3 noMargin className="h-4 text-[13px] my-[3px]">
|
||||
{field.label}
|
||||
</Label3>
|
||||
<Info3
|
||||
noMargin
|
||||
className="overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{displayValue(field.value)}
|
||||
</Info3>
|
||||
</>
|
||||
)}
|
||||
{editing && (
|
||||
<FormikField
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
component={field.component}
|
||||
placeholder={field.placeholder}
|
||||
type={field.type}
|
||||
label={field.label}
|
||||
width={FIELD_WIDTH}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const GET_CONFIG = gql`
|
||||
query getData {
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
}
|
||||
`
|
||||
|
||||
const ContactInfo = ({ wizard }) => {
|
||||
const [editing, setEditing] = useState(wizard || false)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
onCompleted: () => setEditing(false),
|
||||
refetchQueries: () => ['getData'],
|
||||
onError: e => setError(e)
|
||||
})
|
||||
|
||||
const { data } = useQuery(GET_CONFIG)
|
||||
|
||||
const save = it => {
|
||||
return saveConfig({
|
||||
variables: { config: toNamespace(namespaces.OPERATOR_INFO, it) }
|
||||
})
|
||||
}
|
||||
|
||||
const info =
|
||||
data?.config && fromNamespace(namespaces.OPERATOR_INFO, data.config)
|
||||
|
||||
if (!info) return null
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
active: Yup.boolean(),
|
||||
name: Yup.string(),
|
||||
phone: Yup.string(),
|
||||
email: Yup.string()
|
||||
.email('Please enter a valid email address')
|
||||
.required('An email is required'),
|
||||
website: Yup.string(),
|
||||
companyNumber: Yup.string()
|
||||
})
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Company name',
|
||||
value: info.name ?? '',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'phone',
|
||||
label: 'Phone number',
|
||||
value: info.phone,
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
value: info.email ?? '',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
label: 'Website',
|
||||
value: info.website ?? '',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'companyNumber',
|
||||
label: 'Company registration number',
|
||||
value: info.companyNumber ?? '',
|
||||
component: TextInput
|
||||
}
|
||||
]
|
||||
|
||||
const findField = name => R.find(R.propEq('name', name))(fields)
|
||||
const findValue = name => findField(name).value
|
||||
|
||||
const displayTextValue = value => value
|
||||
|
||||
const form = {
|
||||
initialValues: {
|
||||
active: info.active,
|
||||
name: findValue('name'),
|
||||
phone: findValue('phone'),
|
||||
email: findValue('email'),
|
||||
website: findValue('website'),
|
||||
companyNumber: findValue('companyNumber')
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorMsg = formikErrors =>
|
||||
!R.isNil(formikErrors.email) ? formikErrors.email : null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title="Contact information"
|
||||
articleUrl="https://support.lamassu.is/hc/en-us/articles/360033051732-Enabling-Operator-Info"
|
||||
tooltipText="For details on configuring this panel, please read the relevant knowledgebase article:"
|
||||
/>
|
||||
<SwitchRow checked={info.active} save={save} title="Info card enabled?" />
|
||||
<div>
|
||||
<div className="flex items-center gap-4">
|
||||
<H4>Info card</H4>
|
||||
{!editing && (
|
||||
<IconButton onClick={() => setEditing(true)}>
|
||||
<SvgIcon>
|
||||
<EditIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
enableReinitialize
|
||||
initialValues={form.initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={values => save(validationSchema.cast(values))}
|
||||
onReset={() => {
|
||||
setEditing(false)
|
||||
setError(null)
|
||||
}}>
|
||||
{({ errors }) => (
|
||||
<Form className="flex flex-col gap-7 w-147">
|
||||
<PromptWhenDirty />
|
||||
<div className="flex gap-6 justify-between">
|
||||
<Field
|
||||
field={findField('name')}
|
||||
editing={editing}
|
||||
displayValue={displayTextValue}
|
||||
onFocus={() => setError(null)}
|
||||
/>
|
||||
<Field
|
||||
field={findField('phone')}
|
||||
editing={editing}
|
||||
displayValue={displayTextValue}
|
||||
onFocus={() => setError(null)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<Field
|
||||
field={findField('email')}
|
||||
editing={editing}
|
||||
displayValue={displayTextValue}
|
||||
onFocus={() => setError(null)}
|
||||
/>
|
||||
<Field
|
||||
field={findField('website')}
|
||||
editing={editing}
|
||||
displayValue={displayTextValue}
|
||||
onFocus={() => setError(null)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<Field
|
||||
field={findField('companyNumber')}
|
||||
editing={editing}
|
||||
displayValue={displayTextValue}
|
||||
onFocus={() => setError(null)}
|
||||
/>
|
||||
</div>
|
||||
{editing && !!getErrorMsg(errors) && (
|
||||
<ErrorMessage>{getErrorMsg(errors)}</ErrorMessage>
|
||||
)}
|
||||
{editing && (
|
||||
<div className="flex gap-10">
|
||||
<Link color="primary" type="submit">
|
||||
Save
|
||||
</Link>
|
||||
<Link color="secondary" type="reset">
|
||||
Cancel
|
||||
</Link>
|
||||
{error && <ErrorMessage>Failed to save changes</ErrorMessage>}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
{!wizard && (
|
||||
<>
|
||||
<InfoMessage Icon={WarningIcon}>
|
||||
Sharing your information with your customers through your machines
|
||||
allows them to contact you in case there's a problem with a machine
|
||||
in your network or a transaction.
|
||||
</InfoMessage>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContactInfo
|
||||
67
packages/admin-ui/src/pages/OperatorInfo/MachineScreens.jsx
Normal file
67
packages/admin-ui/src/pages/OperatorInfo/MachineScreens.jsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import * as R from 'ramda'
|
||||
import React, { memo } from 'react'
|
||||
import { H4 } from 'src/components/typography'
|
||||
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import SwitchRow from './components/SwitchRow.jsx'
|
||||
|
||||
const GET_CONFIG = gql`
|
||||
query getData {
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
}
|
||||
`
|
||||
|
||||
const MachineScreens = memo(({ wizard }) => {
|
||||
const { data } = useQuery(GET_CONFIG)
|
||||
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
refetchQueries: () => ['getData']
|
||||
})
|
||||
|
||||
const save = it => {
|
||||
const formatConfig = R.compose(
|
||||
toNamespace(namespaces.MACHINE_SCREENS),
|
||||
toNamespace('rates'),
|
||||
R.mergeRight(ratesScreenConfig)
|
||||
)
|
||||
|
||||
return saveConfig({
|
||||
variables: {
|
||||
config: formatConfig({ active: it })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const machineScreensConfig =
|
||||
data?.config && fromNamespace(namespaces.MACHINE_SCREENS, data.config)
|
||||
|
||||
const ratesScreenConfig =
|
||||
data?.config &&
|
||||
R.compose(
|
||||
fromNamespace('rates'),
|
||||
fromNamespace(namespaces.MACHINE_SCREENS)
|
||||
)(data.config)
|
||||
|
||||
if (!machineScreensConfig) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<H4>Rates screen</H4>
|
||||
<SwitchRow
|
||||
save={save}
|
||||
title="Enable rates screen"
|
||||
checked={ratesScreenConfig.active}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default MachineScreens
|
||||
116
packages/admin-ui/src/pages/OperatorInfo/ReceiptPrinting.jsx
Normal file
116
packages/admin-ui/src/pages/OperatorInfo/ReceiptPrinting.jsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import * as R from 'ramda'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import Header from './components/Header.jsx'
|
||||
import SwitchRow from './components/SwitchRow.jsx'
|
||||
|
||||
const GET_CONFIG = gql`
|
||||
query getData {
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
}
|
||||
`
|
||||
|
||||
const ReceiptPrinting = memo(({ wizard }) => {
|
||||
const { data } = useQuery(GET_CONFIG)
|
||||
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
refetchQueries: () => ['getData']
|
||||
})
|
||||
|
||||
const saveSwitch = object => {
|
||||
return saveConfig({
|
||||
variables: {
|
||||
config: toNamespace(
|
||||
namespaces.RECEIPT,
|
||||
R.mergeRight(receiptPrintingConfig, object)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const save = it =>
|
||||
saveConfig({
|
||||
variables: { config: toNamespace(namespaces.RECEIPT, it) }
|
||||
})
|
||||
|
||||
const receiptPrintingConfig =
|
||||
data?.config && fromNamespace(namespaces.RECEIPT, data.config)
|
||||
if (!receiptPrintingConfig) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title="Receipt printing"
|
||||
tooltipText="For details on configuring this panel, please read the relevant knowledgebase article."
|
||||
articleUrl="https://support.lamassu.is/hc/en-us/articles/360058513951-Receipt-options-printers"
|
||||
/>
|
||||
<SwitchRow
|
||||
title="Enable receipt printing"
|
||||
checked={receiptPrintingConfig.active}
|
||||
save={it => saveSwitch({ active: it })}
|
||||
/>
|
||||
<SwitchRow
|
||||
title="Automatic receipt printing"
|
||||
checked={receiptPrintingConfig.automaticPrint}
|
||||
save={it => saveSwitch({ automaticPrint: it })}
|
||||
/>
|
||||
<SwitchRow
|
||||
title="Offer SMS receipt"
|
||||
checked={receiptPrintingConfig.sms}
|
||||
save={it => saveSwitch({ sms: it })}
|
||||
/>
|
||||
<BooleanPropertiesTable
|
||||
editing={wizard}
|
||||
title={'Visible on the receipt (options)'}
|
||||
data={receiptPrintingConfig}
|
||||
elements={[
|
||||
{
|
||||
name: 'operatorWebsite',
|
||||
display: 'Operator website'
|
||||
},
|
||||
{
|
||||
name: 'operatorEmail',
|
||||
display: 'Operator email'
|
||||
},
|
||||
{
|
||||
name: 'operatorPhone',
|
||||
display: 'Operator phone'
|
||||
},
|
||||
{
|
||||
name: 'companyNumber',
|
||||
display: 'Company registration number'
|
||||
},
|
||||
{
|
||||
name: 'machineLocation',
|
||||
display: 'Machine location'
|
||||
},
|
||||
{
|
||||
name: 'customerNameOrPhoneNumber',
|
||||
display: 'Customer name or phone number (if known)'
|
||||
},
|
||||
{
|
||||
name: 'exchangeRate',
|
||||
display: 'Exchange rate'
|
||||
},
|
||||
{
|
||||
name: 'addressQRCode',
|
||||
display: 'Address QR code'
|
||||
}
|
||||
]}
|
||||
save={save}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default ReceiptPrinting
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import Switch from '@mui/material/Switch'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { H4, P, Label3 } from 'src/components/typography'
|
||||
import EditIcon from 'src/styling/icons/action/edit/enabled.svg?react'
|
||||
import ExpandIconClosed from 'src/styling/icons/action/expand/closed.svg?react'
|
||||
import ExpandIconOpen from 'src/styling/icons/action/expand/open.svg?react'
|
||||
import WhiteLogo from 'src/styling/icons/menu/logo-white.svg?react'
|
||||
|
||||
import { SupportLinkButton } from 'src/components/buttons'
|
||||
import { formatDate } from 'src/utils/timezones'
|
||||
|
||||
import CustomSMSModal from './SMSNoticesModal'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
|
||||
const GET_SMS_NOTICES = gql`
|
||||
query SMSNotices {
|
||||
SMSNotices {
|
||||
id
|
||||
event
|
||||
message
|
||||
messageName
|
||||
enabled
|
||||
allowToggle
|
||||
}
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
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, timezone }) => {
|
||||
const matches = {
|
||||
'#code': 123,
|
||||
'#timestamp': formatDate(new Date(), timezone, 'HH:mm')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute w-88 overflow-visible"
|
||||
style={{ left: coords.x, bottom: coords.y }}>
|
||||
<div className="flex flex-row items-end gap-2">
|
||||
<div className="flex w-9 h-9 rounded-full bg-[#16D6D3] items-center justify-center">
|
||||
<WhiteLogo width={22} height={22} />
|
||||
</div>
|
||||
<Paper className="w-56 p-4 rounded-2xl">
|
||||
<P noMargin>
|
||||
{R.isEmpty(sms?.message) ? (
|
||||
<i>No content available</i>
|
||||
) : (
|
||||
formatContent(multiReplace(sms?.message, matches))
|
||||
)}
|
||||
</P>
|
||||
</Paper>
|
||||
<Label3>{formatDate(new Date(), timezone, 'HH:mm')}</Label3>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SMSNotices = () => {
|
||||
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 timezone = R.path(['config', 'locale_timezone'])(messagesData)
|
||||
|
||||
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="flex flex-row items-center">
|
||||
{R.prop('messageName', it)}
|
||||
<HelpTooltip width={250}>
|
||||
<P>{TOOLTIPS[it.event]}</P>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
) : (
|
||||
R.prop('messageName', it)
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Edit',
|
||||
width: 100,
|
||||
size: 'sm',
|
||||
textAlign: 'center',
|
||||
view: it => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setPreviewOpen(false)
|
||||
setSelectedSMS(it)
|
||||
setShowModal(true)
|
||||
}}>
|
||||
<SvgIcon>
|
||||
<EditIcon />
|
||||
</SvgIcon>
|
||||
</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)
|
||||
}}>
|
||||
<SvgIcon>
|
||||
{R.equals(selectedSMS, it) && previewOpen ? (
|
||||
<ExpandIconOpen />
|
||||
) : (
|
||||
<ExpandIconClosed />
|
||||
)}
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex relative items-center justify-between w-200">
|
||||
<H4>SMS notices</H4>
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details on configuring this panel, please read the relevant
|
||||
knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/115001205591-SMS-Phone-Verification"
|
||||
label="Lamassu Support Article"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
{showModal && (
|
||||
<CustomSMSModal
|
||||
showModal={showModal}
|
||||
onClose={handleClose}
|
||||
sms={selectedSMS}
|
||||
creationError={errorMsg}
|
||||
submit={editMessage}
|
||||
/>
|
||||
)}
|
||||
{previewOpen && (
|
||||
<SMSPreview
|
||||
sms={selectedSMS}
|
||||
coords={previewCoords}
|
||||
timezone={timezone}
|
||||
/>
|
||||
)}
|
||||
<DataTable
|
||||
emptyText="No SMS notices so far"
|
||||
elements={elements}
|
||||
loading={loading}
|
||||
data={R.path(['SMSNotices'])(messagesData)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SMSNotices
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
import Chip from '@mui/material/Chip'
|
||||
import { Form, Formik, Field } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Info2 } from 'src/components/typography'
|
||||
import DefaultIconReverse from 'src/styling/icons/button/retry/white.svg?react'
|
||||
import DefaultIcon from 'src/styling/icons/button/retry/zodiac.svg?react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { ActionButton, Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import { zircon } from 'src/styling/variables'
|
||||
|
||||
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 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="flex flex-col h-full gap-5">
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={DefaultIcon}
|
||||
InverseIcon={DefaultIconReverse}
|
||||
className="w-37"
|
||||
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="w-120">
|
||||
{R.splitEvery(3, CHIPS[sms?.event]).map((it, idx) => (
|
||||
<div key={idx} className="flex gap-2">
|
||||
{it.map((ite, idx2) => (
|
||||
<Chip
|
||||
key={idx2}
|
||||
label={ite.display}
|
||||
size="small"
|
||||
style={{ backgroundColor: zircon }}
|
||||
disabled={R.includes(ite.code, values.message)}
|
||||
className="p-2"
|
||||
onClick={() => {
|
||||
setFieldValue(
|
||||
'message',
|
||||
values.message.concat(
|
||||
R.last(values.message) === ' ' ? '' : ' ',
|
||||
ite.code
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row mt-auto mx-0 mb-6">
|
||||
{getErrorMsg(errors, touched, creationError) && (
|
||||
<ErrorMessage>
|
||||
{getErrorMsg(errors, touched, creationError)}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
form="sms-notice"
|
||||
className="mt-auto ml-auto mr-0 mb-0">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SMSNoticesModal
|
||||
248
packages/admin-ui/src/pages/OperatorInfo/TermsConditions.jsx
Normal file
248
packages/admin-ui/src/pages/OperatorInfo/TermsConditions.jsx
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import IconButton from '@mui/material/IconButton'
|
||||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import classnames from 'classnames'
|
||||
import { Form, Formik, Field as FormikField } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||
import { Info2, Info3, Label3 } from 'src/components/typography'
|
||||
import EditIcon from 'src/styling/icons/action/edit/enabled.svg?react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Link } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import Header from './components/Header.jsx'
|
||||
import SwitchRow from './components/SwitchRow.jsx'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
|
||||
const Field = ({
|
||||
editing,
|
||||
name,
|
||||
width,
|
||||
placeholder,
|
||||
label,
|
||||
value,
|
||||
multiline = false,
|
||||
rows,
|
||||
onFocus,
|
||||
...props
|
||||
}) => {
|
||||
const info3ClassNames = {
|
||||
'overflow-hidden whitespace-nowrap text-ellipsis h-6': !multiline,
|
||||
'wrap-anywhere overflow-y-auto h-32 mt-4 leading-[23px]': multiline
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`w-125 p-0 pl-1 pb-1`}>
|
||||
{!editing && (
|
||||
<>
|
||||
<Label3 noMargin className="h-4 text-[13px] my-[3px] mb-1">
|
||||
{label}
|
||||
</Label3>
|
||||
<Info3 noMargin className={classnames(info3ClassNames)}>
|
||||
{value}
|
||||
</Info3>
|
||||
</>
|
||||
)}
|
||||
{editing && (
|
||||
<FormikField
|
||||
id={name}
|
||||
name={name}
|
||||
component={TextInput}
|
||||
width={width}
|
||||
placeholder={placeholder}
|
||||
type="text"
|
||||
label={label}
|
||||
multiline={multiline}
|
||||
rows={rows}
|
||||
rowsMax="6"
|
||||
onFocus={onFocus}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const GET_CONFIG = gql`
|
||||
query getData {
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
}
|
||||
`
|
||||
|
||||
const TermsConditions = () => {
|
||||
const [error, setError] = useState(null)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
onCompleted: () => {
|
||||
setError(null)
|
||||
setEditing(false)
|
||||
},
|
||||
refetchQueries: () => ['getData'],
|
||||
onError: e => setError(e)
|
||||
})
|
||||
|
||||
const { data } = useQuery(GET_CONFIG)
|
||||
|
||||
const termsAndConditions =
|
||||
data?.config && fromNamespace(namespaces.TERMS_CONDITIONS, data.config)
|
||||
const formData = termsAndConditions ?? {}
|
||||
const showOnScreen = termsAndConditions?.active ?? false
|
||||
const addDelayOnScreen = termsAndConditions?.delay ?? false
|
||||
const tcPhoto = termsAndConditions?.tcPhoto ?? false
|
||||
|
||||
const save = it =>
|
||||
saveConfig({
|
||||
variables: { config: toNamespace(namespaces.TERMS_CONDITIONS, it) }
|
||||
})
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Screen title',
|
||||
value: formData.title ?? '',
|
||||
width: 282
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
label: 'Text content',
|
||||
value: formData.text ?? '',
|
||||
width: 502,
|
||||
multiline: true,
|
||||
rows: 6
|
||||
},
|
||||
{
|
||||
name: 'acceptButtonText',
|
||||
label: 'Accept button text',
|
||||
value: formData.acceptButtonText ?? '',
|
||||
placeholder: 'I accept',
|
||||
width: 282
|
||||
},
|
||||
{
|
||||
name: 'cancelButtonText',
|
||||
label: 'Cancel button text',
|
||||
value: formData.cancelButtonText ?? '',
|
||||
placeholder: 'Cancel',
|
||||
width: 282
|
||||
}
|
||||
]
|
||||
|
||||
const findField = name => R.find(R.propEq('name', name))(fields)
|
||||
const findValue = name => findField(name).value
|
||||
|
||||
const initialValues = {
|
||||
title: findValue('title'),
|
||||
text: findValue('text'),
|
||||
acceptButtonText: findValue('acceptButtonText'),
|
||||
cancelButtonText: findValue('cancelButtonText')
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
title: Yup.string('The screen title must be a string')
|
||||
.required('The screen title is required')
|
||||
.max(50, 'Too long'),
|
||||
text: Yup.string('The text content must be a string').required(
|
||||
'The text content is required'
|
||||
),
|
||||
acceptButtonText: Yup.string('The accept button text must be a string')
|
||||
.required('The accept button text is required')
|
||||
.max(50, 'The accept button text is too long'),
|
||||
cancelButtonText: Yup.string('The cancel button text must be a string')
|
||||
.required('The cancel button text is required')
|
||||
.max(50, 'The cancel button text is too long')
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title="Terms & Conditions"
|
||||
tooltipText="For details on configuring this panel, please read the relevant knowledgebase article:"
|
||||
articleUrl="https://support.lamassu.is/hc/en-us/articles/360015982211-Terms-and-Conditions"
|
||||
/>
|
||||
<SwitchRow
|
||||
title="Show on screen"
|
||||
checked={showOnScreen}
|
||||
save={it => save({ active: it })}
|
||||
/>
|
||||
<SwitchRow
|
||||
title="Capture customer photo on acceptance of Terms & Conditions"
|
||||
checked={tcPhoto}
|
||||
save={it => save({ tcPhoto: it })}
|
||||
/>
|
||||
<SwitchRow
|
||||
title="Add 7 seconds delay on screen"
|
||||
checked={addDelayOnScreen}
|
||||
save={it => save({ delay: it })}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<Info2>Info card</Info2>
|
||||
{!editing && (
|
||||
<IconButton onClick={() => setEditing(true)}>
|
||||
<SvgIcon>
|
||||
<EditIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
enableReinitialize
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={values => save(values)}
|
||||
onReset={() => {
|
||||
setEditing(false)
|
||||
setError(null)
|
||||
}}>
|
||||
{({ errors }) => (
|
||||
<Form className="flex flex-col gap-6">
|
||||
<PromptWhenDirty />
|
||||
{fields.map((f, idx) => (
|
||||
<div className="flex gap-7" key={idx}>
|
||||
<Field
|
||||
editing={editing}
|
||||
name={f.name}
|
||||
width={f.width}
|
||||
placeholder={f.placeholder}
|
||||
label={f.label}
|
||||
value={f.value}
|
||||
multiline={f.multiline}
|
||||
rows={f.rows}
|
||||
onFocus={() => setError(null)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-10">
|
||||
{editing && (
|
||||
<>
|
||||
<Link color="primary" type="submit">
|
||||
Save
|
||||
</Link>
|
||||
<Link color="secondary" type="reset">
|
||||
Cancel
|
||||
</Link>
|
||||
{!R.isEmpty(errors) && (
|
||||
<ErrorMessage>{R.head(R.values(errors))}</ErrorMessage>
|
||||
)}
|
||||
{error && <ErrorMessage>Failed to save changes</ErrorMessage>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TermsConditions
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react'
|
||||
import { H4, P } from 'src/components/typography/index.jsx'
|
||||
import { HelpTooltip } from 'src/components/Tooltip.jsx'
|
||||
import { SupportLinkButton } from 'src/components/buttons/index.js'
|
||||
|
||||
const Header = ({ title, tooltipText, articleUrl }) => (
|
||||
<div className="flex items-center">
|
||||
<H4>{title}</H4>
|
||||
<HelpTooltip width={320}>
|
||||
<P>{tooltipText}</P>
|
||||
<SupportLinkButton
|
||||
link={articleUrl}
|
||||
label="Lamassu Support Article"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Header
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react'
|
||||
|
||||
import { Label1 } from 'src/components/typography'
|
||||
|
||||
const InfoMessage = ({ Icon, children }) => (
|
||||
<div className="flex my-13 gap-4">
|
||||
<Icon />
|
||||
<Label1 className="w-83 text-comet mt-1">{children}</Label1>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default InfoMessage
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import React, { memo } from 'react'
|
||||
import { Label2, P } from 'src/components/typography/index.jsx'
|
||||
import Switch from '@mui/material/Switch'
|
||||
|
||||
const SwitchRow = memo(({ title, disabled = false, checked, save }) => {
|
||||
return (
|
||||
<div className="flex justify-between w-99">
|
||||
<P>{title}</P>
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
checked={checked}
|
||||
onChange={event => save && save(event.target.checked)}
|
||||
/>
|
||||
<Label2>{checked ? 'Yes' : 'No'}</Label2>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default SwitchRow
|
||||
Loading…
Add table
Add a link
Reference in a new issue