chore: use monorepo organization

This commit is contained in:
Rafael Taranto 2025-05-12 10:52:54 +01:00
parent deaf7d6ecc
commit a687827f7e
1099 changed files with 8184 additions and 11535 deletions

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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