diff --git a/new-lamassu-admin/src/components/ConfirmDialog.js b/new-lamassu-admin/src/components/ConfirmDialog.js index 7b52d139..da195f55 100644 --- a/new-lamassu-admin/src/components/ConfirmDialog.js +++ b/new-lamassu-admin/src/components/ConfirmDialog.js @@ -9,13 +9,14 @@ import React, { useEffect, useState, memo } from 'react' import { Button, IconButton } from 'src/components/buttons' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' +import { fontSize3 } from 'src/styling/variables' import { TextInput } from './inputs' import { H4, P } from './typography' const useStyles = makeStyles({ label: { - fontSize: 16 + fontSize: fontSize3 }, spacing: { padding: 32 diff --git a/new-lamassu-admin/src/components/Modal.js b/new-lamassu-admin/src/components/Modal.js index 98a5a362..e38214b3 100644 --- a/new-lamassu-admin/src/components/Modal.js +++ b/new-lamassu-admin/src/components/Modal.js @@ -24,6 +24,25 @@ const styles = { borderRadius: 8, outline: 0 }), + infoPanelWrapper: ({ width, infoPanelHeight }) => ({ + width, + height: infoPanelHeight, + marginTop: 16, + display: 'flex', + flexDirection: 'column', + minHeight: infoPanelHeight ?? 200, + maxHeight: '90vh', + overflowY: 'auto', + borderRadius: 8, + outline: 0 + }), + panelContent: { + width: '100%', + display: 'flex', + flexDirection: 'column', + flex: 1, + padding: [[0, 24]] + }, content: ({ small }) => ({ width: '100%', display: 'flex', @@ -48,17 +67,24 @@ const useStyles = makeStyles(styles) const Modal = ({ width, height, + infoPanelHeight, title, small, infoPanel, handleClose, children, + secondaryModal, className, closeOnEscape, closeOnBackdropClick, ...props }) => { - const classes = useStyles({ width, height, small }) + const classes = useStyles({ + width, + height, + small, + infoPanelHeight + }) const TitleCase = small ? H4 : H1 const closeSize = small ? 16 : 20 @@ -84,8 +110,8 @@ const Modal = ({
{children}
{infoPanel && ( - - {infoPanel} + +
{infoPanel}
)} diff --git a/new-lamassu-admin/src/components/editableTable/Row.js b/new-lamassu-admin/src/components/editableTable/Row.js index 2ab3ba7d..01476669 100644 --- a/new-lamassu-admin/src/components/editableTable/Row.js +++ b/new-lamassu-admin/src/components/editableTable/Row.js @@ -1,4 +1,5 @@ import { makeStyles } from '@material-ui/core' +import classnames from 'classnames' import { Field, useFormikContext } from 'formik' import * as R from 'ramda' import React, { useContext } from 'react' @@ -158,7 +159,7 @@ const groupStriped = elements => { ) } -const ERow = ({ editing, disabled }) => { +const ERow = ({ editing, disabled, lastOfGroup }) => { const { errors } = useFormikContext() const { elements, @@ -169,6 +170,8 @@ const ERow = ({ editing, disabled }) => { stripeWhen } = useContext(TableCtx) + const classes = useStyles() + const { values } = useFormikContext() const shouldStripe = stripeWhen && stripeWhen(values) && !editing @@ -187,8 +190,13 @@ const ERow = ({ editing, disabled }) => { it => it.editable === undefined || it.editable ) + const classNames = { + [classes.lastOfGroup]: lastOfGroup + } + return ( diff --git a/new-lamassu-admin/src/components/editableTable/Row.styles.js b/new-lamassu-admin/src/components/editableTable/Row.styles.js index c03d377f..04d9b8d8 100644 --- a/new-lamassu-admin/src/components/editableTable/Row.styles.js +++ b/new-lamassu-admin/src/components/editableTable/Row.styles.js @@ -4,6 +4,9 @@ export default { cancelButton: { marginRight: 20 }, + lastOfGroup: { + marginBottom: 24 + }, extraPaddingLeft: { paddingLeft: 35 }, diff --git a/new-lamassu-admin/src/components/editableTable/Table.js b/new-lamassu-admin/src/components/editableTable/Table.js index da9a094a..094ceca0 100644 --- a/new-lamassu-admin/src/components/editableTable/Table.js +++ b/new-lamassu-admin/src/components/editableTable/Table.js @@ -47,6 +47,8 @@ const ETable = ({ setEditing, stripeWhen, disableRowEdit, + groupBy, + sortBy, createText = 'Add override' }) => { const [editingId, setEditingId] = useState(null) @@ -102,6 +104,8 @@ const ETable = ({ const canAdd = !forceDisable && !editingId && !disableAdd && !adding const showTable = adding || data.length !== 0 + const innerData = sortBy ? R.sortWith(sortBy)(data) : data + const ctxValue = { elements, enableEdit, @@ -159,24 +163,33 @@ const ETable = ({ )} - {data.map((it, idx) => ( - -
- - -
- ))} + {innerData.map((it, idx) => { + const nextElement = innerData[idx + 1] + const isLastOfGroup = + groupBy && + nextElement && + nextElement[groupBy] !== it[groupBy] + + return ( + +
+ + +
+ ) + })} diff --git a/new-lamassu-admin/src/components/inputs/base/Checkbox.js b/new-lamassu-admin/src/components/inputs/base/Checkbox.js index 7430f310..f5a6a819 100644 --- a/new-lamassu-admin/src/components/inputs/base/Checkbox.js +++ b/new-lamassu-admin/src/components/inputs/base/Checkbox.js @@ -4,7 +4,7 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox' import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank' import React from 'react' -import { secondaryColor } from '../../../styling/variables' +import { fontSize2, fontSize3, secondaryColor } from 'src/styling/variables' const useStyles = makeStyles({ root: { @@ -30,9 +30,11 @@ const CheckboxInput = ({ name, onChange, value, label, ...props }) => { value={value} checked={value} icon={ - + } - checkedIcon={} + checkedIcon={} disableRipple {...props} /> diff --git a/new-lamassu-admin/src/components/inputs/base/TextInput.styles.js b/new-lamassu-admin/src/components/inputs/base/TextInput.styles.js index d9077436..1c8db3e5 100644 --- a/new-lamassu-admin/src/components/inputs/base/TextInput.styles.js +++ b/new-lamassu-admin/src/components/inputs/base/TextInput.styles.js @@ -3,7 +3,7 @@ import { secondaryColor } from 'src/styling/variables' export default { size: ({ size }) => ({ - marginTop: size === 'lg' ? -2 : 0, + marginTop: size === 'lg' ? 0 : 2, ...bySize(size) }), bold, diff --git a/new-lamassu-admin/src/components/tables/DataTable.js b/new-lamassu-admin/src/components/tables/DataTable.js index 5563088d..3709f71b 100644 --- a/new-lamassu-admin/src/components/tables/DataTable.js +++ b/new-lamassu-admin/src/components/tables/DataTable.js @@ -47,7 +47,7 @@ const Row = ({ return (
-
+
{ diff --git a/new-lamassu-admin/src/components/tables/DataTable.styles.js b/new-lamassu-admin/src/components/tables/DataTable.styles.js index 26714004..24212f0d 100644 --- a/new-lamassu-admin/src/components/tables/DataTable.styles.js +++ b/new-lamassu-admin/src/components/tables/DataTable.styles.js @@ -12,6 +12,7 @@ export default { padding: 1 }, row: { + border: [[2, 'solid', 'transparent']], borderRadius: 0 }, expanded: { diff --git a/new-lamassu-admin/src/components/typography/index.js b/new-lamassu-admin/src/components/typography/index.js index 31648de4..2cd0a09e 100644 --- a/new-lamassu-admin/src/components/typography/index.js +++ b/new-lamassu-admin/src/components/typography/index.js @@ -66,6 +66,21 @@ function H4({ children, noMargin, className, ...props }) { ) } +function H5({ children, noMargin, className, ...props }) { + const classes = useStyles() + const classNames = { + [classes.h5]: true, + [classes.noMargin]: noMargin, + [className]: !!className + } + + return ( +
+ {children} +
+ ) +} + const P = pBuilder('p') const Info1 = pBuilder('info1') const Info2 = pBuilder('info2') @@ -99,6 +114,7 @@ export { H2, H3, H4, + H5, TL1, TL2, P, diff --git a/new-lamassu-admin/src/components/typography/styles.js b/new-lamassu-admin/src/components/typography/styles.js index 2d5f90d3..0117fc93 100644 --- a/new-lamassu-admin/src/components/typography/styles.js +++ b/new-lamassu-admin/src/components/typography/styles.js @@ -11,7 +11,7 @@ import { } from 'src/styling/variables' const base = { - lineHeight: '110%', + lineHeight: '120%', color: fontColor } @@ -40,6 +40,12 @@ export default { fontFamily: fontPrimary, fontWeight: 700 }, + h5: { + extend: base, + fontSize: fontSize3, + fontFamily: fontPrimary, + fontWeight: 700 + }, p: { extend: base, fontSize: fontSize4, diff --git a/new-lamassu-admin/src/pages/Commissions/helper.js b/new-lamassu-admin/src/pages/Commissions/helper.js index 74e524e1..27c64eaa 100644 --- a/new-lamassu-admin/src/pages/Commissions/helper.js +++ b/new-lamassu-admin/src/pages/Commissions/helper.js @@ -75,7 +75,7 @@ const getOverridesFields = (getData, currency) => { { name: 'fixedFee', display: 'Fixed fee', - width: 140, + width: 144, input: NumberInput, doubleHeader: 'Cash-in only', textAlign: 'right', @@ -87,7 +87,7 @@ const getOverridesFields = (getData, currency) => { { name: 'minimumTx', display: 'Minimun Tx', - width: 140, + width: 144, input: NumberInput, doubleHeader: 'Cash-in only', textAlign: 'right', diff --git a/new-lamassu-admin/src/pages/Customers/CustomersList.styles.js b/new-lamassu-admin/src/pages/Customers/CustomersList.styles.js index ee7a51fd..42e5a15f 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomersList.styles.js +++ b/new-lamassu-admin/src/pages/Customers/CustomersList.styles.js @@ -1,6 +1,6 @@ import typographyStyles from 'src/components/typography/styles' import baseStyles from 'src/pages/Logs.styles' -import { zircon, primaryColor } from 'src/styling/variables' +import { zircon, primaryColor, fontSize4 } from 'src/styling/variables' const { label1 } = typographyStyles const { titleWrapper, titleAndButtonsContainer } = baseStyles @@ -30,7 +30,7 @@ export default { }, p: { fontFamily: 'MuseoSans', - fontSize: 14, + fontSize: fontSize4, fontWeight: 500, fontStretch: 'normal', fontStyle: 'normal', diff --git a/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.styles.js b/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.styles.js index 3fb5ff0c..a41af7a2 100644 --- a/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.styles.js +++ b/new-lamassu-admin/src/pages/Customers/components/propertyCard/PropertyCard.styles.js @@ -5,7 +5,8 @@ import { tomato, spring3, spring4, - comet + comet, + fontSize5 } from 'src/styling/variables' const propertyCardStyles = { @@ -25,7 +26,7 @@ const propertyCardStyles = { }, label1: { fontFamily: 'MuseoSans', - fontSize: 12, + fontSize: fontSize5, fontWeight: 500, fontStretch: 'normal', fontStyle: 'normal', diff --git a/new-lamassu-admin/src/pages/Logs.styles.js b/new-lamassu-admin/src/pages/Logs.styles.js index 1b93191d..15fc8679 100644 --- a/new-lamassu-admin/src/pages/Logs.styles.js +++ b/new-lamassu-admin/src/pages/Logs.styles.js @@ -1,3 +1,5 @@ +import { fontSize5 } from 'src/styling/variables' + export default { titleWrapper: { display: 'flex', @@ -41,7 +43,7 @@ export default { margin: 8, display: 'flex', alignItems: 'center', - fontSize: 12, + fontSize: fontSize5, padding: [[0, 12]] }, shareIcon: { diff --git a/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.styles.js b/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.styles.js index 5d80f71e..fbc07811 100644 --- a/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.styles.js +++ b/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.styles.js @@ -1,13 +1,13 @@ import { fade } from '@material-ui/core/styles/colorManipulator' -import { offColor, comet } from 'src/styling/variables' +import { fontSize4, offColor, comet } from 'src/styling/variables' export default { wrapper: { display: 'flex', marginTop: 24, marginBottom: 32, - fontSize: 14 + fontSize: fontSize4 }, column1: { width: 600 diff --git a/new-lamassu-admin/src/pages/Transactions/Transactions.js b/new-lamassu-admin/src/pages/Transactions/Transactions.js index e200c314..fae7d2e5 100644 --- a/new-lamassu-admin/src/pages/Transactions/Transactions.js +++ b/new-lamassu-admin/src/pages/Transactions/Transactions.js @@ -118,17 +118,17 @@ const Transactions = () => { }, { header: 'Date (UTC)', - view: it => moment.utc(it.created).format('YYYY-MM-D'), + view: it => moment.utc(it.created).format('YYYY-MM-DD'), textAlign: 'right', size: 'sm', - width: 124 + width: 144 }, { header: 'Time (UTC)', view: it => moment.utc(it.created).format('HH:mm:ss'), textAlign: 'right', size: 'sm', - width: 124 + width: 144 } ] diff --git a/new-lamassu-admin/src/pages/Triggers/Triggers.js b/new-lamassu-admin/src/pages/Triggers/Triggers.js index adcaae94..73069943 100644 --- a/new-lamassu-admin/src/pages/Triggers/Triggers.js +++ b/new-lamassu-admin/src/pages/Triggers/Triggers.js @@ -8,10 +8,11 @@ import { v4 } from 'uuid' import Title from 'src/components/Title' import { Link } from 'src/components/buttons' import { Table as EditableTable } from 'src/components/editableTable' +import { fromNamespace, namespaces } from 'src/utils/config' import { mainStyles } from './Triggers.styles' import Wizard from './Wizard' -import { Schema, elements } from './helper' +import { Schema, elements, sortBy } from './helper' const useStyles = makeStyles(mainStyles) @@ -28,6 +29,7 @@ const GET_INFO = gql` ` const Triggers = () => { + const classes = useStyles() const [wizard, setWizard] = useState(false) const [error, setError] = useState(false) @@ -51,7 +53,9 @@ const Triggers = () => { return saveConfig({ variables: { config } }) } - const classes = useStyles() + const currency = R.path(['fiatCurrency'])( + fromNamespace(namespaces.LOCALE)(data?.config) + ) return ( <> @@ -69,13 +73,20 @@ const Triggers = () => { data={triggers} name="triggers" enableEdit + sortBy={sortBy} + groupBy="triggerType" enableDelete save={save} validationSchema={Schema} elements={elements} /> {wizard && ( - setWizard(null)} /> + setWizard(null)} + /> )} ) diff --git a/new-lamassu-admin/src/pages/Triggers/Wizard.js b/new-lamassu-admin/src/pages/Triggers/Wizard.js index ff76c550..48bf83ec 100644 --- a/new-lamassu-admin/src/pages/Triggers/Wizard.js +++ b/new-lamassu-admin/src/pages/Triggers/Wizard.js @@ -1,12 +1,14 @@ import { makeStyles } from '@material-ui/core' -import { Form, Formik } from 'formik' +import { Form, Formik, useFormikContext } from 'formik' import * as R from 'ramda' -import React, { useState, Fragment } from 'react' +import React, { useState, Fragment, useEffect } from 'react' import ErrorMessage from 'src/components/ErrorMessage' import Modal from 'src/components/Modal' import Stepper from 'src/components/Stepper' import { Button } from 'src/components/buttons' +import { H5, Info3 } from 'src/components/typography' +import { comet } from 'src/styling/variables' import { direction, type, requirements } from './helper' @@ -28,6 +30,12 @@ const styles = { height: '100%', display: 'flex', flexDirection: 'column' + }, + infoTitle: { + margin: [[18, 0, 20, 0]] + }, + infoCurrentText: { + color: comet } } @@ -46,9 +54,118 @@ const getStep = step => { } } -const Wizard = ({ machine, onClose, save, error }) => { +const getText = (step, config, currency) => { + switch (step) { + case 1: + return `In ${getDirectionText(config)} transactions` + case 2: + return `if the user ${getTypeText(config, currency)}` + case 3: + return `the user will be ${getRequirementText(config)}.` + default: + return '' + } +} + +const orUnderline = value => { + return R.isEmpty(value) || R.isNil(value) ? '⎼⎼⎼⎼⎼ ' : value +} + +const getDirectionText = config => { + switch (config.cashDirection) { + case 'both': + return 'both cash-in and cash-out' + case 'cashIn': + return 'cash-in' + case 'cashOut': + return 'cash-out' + default: + return orUnderline(null) + } +} + +const getTypeText = (config, currency) => { + switch (config.triggerType) { + case 'txAmount': + return `makes a single transaction over ${orUnderline( + config.threshold + )} ${currency}` + case 'txVolume': + return `makes transactions over ${orUnderline( + config.threshold + )} ${currency} in ${orUnderline(config.days)} days` + case 'txVelocity': + return `makes ${orUnderline( + config.threshold + )} transactions in ${orUnderline(config.days)} days` + case 'consecutiveDays': + return `at least one transaction every day for ${orUnderline( + config.days + )} days` + default: + return '' + } +} + +const getRequirementText = config => { + switch (config.requirement) { + case 'sms': + return 'asked to enter code provided through SMS verification' + case 'idPhoto': + return 'asked to scan a ID with photo' + case 'idData': + return 'asked to scan a ID' + case 'facephoto': + return 'asked to have a photo taken' + case 'sanctions': + return 'matched against the OFAC sanctions list' + case 'superuser': + return '' + case 'suspend': + return 'suspended' + case 'block': + return 'blocked' + default: + return orUnderline(null) + } +} + +const InfoPanel = ({ step, config = {}, liveValues = {}, currency }) => { const classes = useStyles() + const oldText = R.range(1, step) + .map(it => getText(it, config, currency)) + .join(', ') + const newText = getText(step, liveValues, currency) + const isLastStep = step === LAST_STEP + + return ( + <> +
Trigger overview so far
+ + {oldText} + {step !== 1 && ', '} + {newText} + {!isLastStep && '...'} + + + ) +} + +const GetValues = ({ setValues }) => { + const { values } = useFormikContext() + useEffect(() => { + console.log('triggered') + setValues && values && setValues(values) + }, [setValues, values]) + + return null +} + +const Wizard = ({ onClose, save, error, currency }) => { + const classes = useStyles() + + const [liveValues, setLiveValues] = useState({}) const [{ step, config }, setState] = useState({ step: 1 }) @@ -70,33 +187,45 @@ const Wizard = ({ machine, onClose, save, error }) => { } return ( - - - -
- -
- {error && Failed to save} - -
- -
-
+ <> + + } + infoPanelHeight={172} + open={true}> + + +
+ + +
+ {error && Failed to save} + +
+ +
+
+ ) } diff --git a/new-lamassu-admin/src/pages/Triggers/helper.js b/new-lamassu-admin/src/pages/Triggers/helper.js index bebc7dc4..b0957119 100644 --- a/new-lamassu-admin/src/pages/Triggers/helper.js +++ b/new-lamassu-admin/src/pages/Triggers/helper.js @@ -8,6 +8,8 @@ import * as Yup from 'yup' import { TextInput, RadioGroup } from 'src/components/inputs/formik' import Autocomplete from 'src/components/inputs/formik/Autocomplete' import { H4 } from 'src/components/typography' +import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' +import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { errorColor } from 'src/styling/variables' const useStyles = makeStyles({ @@ -32,6 +34,12 @@ const useStyles = makeStyles({ specialGrid: { display: 'grid', gridTemplateColumns: [[182, 162, 141]] + }, + directionIcon: { + marginRight: 2 + }, + directionName: { + marginLeft: 6 } }) @@ -98,8 +106,8 @@ const typeSchema = Yup.object().shape({ const typeOptions = [ { display: 'Transaction amount', code: 'txAmount' }, - { display: 'Transaction velocity', code: 'txVelocity' }, { display: 'Transaction volume', code: 'txVolume' }, + { display: 'Transaction velocity', code: 'txVelocity' }, { display: 'Consecutive days', code: 'consecutiveDays' } ] @@ -131,9 +139,6 @@ const Type = () => { size="lg" name="threshold" options={typeOptions} - labelClassName={classes.radioLabel} - radioClassName={classes.radio} - className={classes.radioGroup} /> ) @@ -199,12 +204,29 @@ const getView = (data, code, compare) => it => { return R.compose(R.prop(code), R.find(R.propEq(compare ?? 'code', it)))(data) } +const DirectionDisplay = ({ code }) => { + const classes = useStyles() + const displayName = getView(directionOptions, 'display')(code) + const showCashIn = code === 'cashIn' || code === 'both' + const showCashOut = code === 'cashOut' || code === 'both' + + return ( +
+ {showCashOut && } + {showCashIn && } + {displayName} +
+ ) +} + const elements = [ { name: 'triggerType', size: 'sm', - width: 271, - input: Autocomplete, + width: 230, + input: ({ field: { value: name } }) => ( + <>{getView(typeOptions, 'display')(name)} + ), view: getView(typeOptions, 'display'), inputProps: { options: typeOptions, @@ -216,8 +238,10 @@ const elements = [ { name: 'requirement', size: 'sm', - width: 271, - input: Autocomplete, + width: 230, + input: ({ field: { value: name } }) => ( + <>{getView(requirementOptions, 'display')(name)} + ), view: getView(requirementOptions, 'display'), inputProps: { options: requirementOptions, @@ -229,14 +253,15 @@ const elements = [ { name: 'threshold', size: 'sm', - width: 271, + width: 260, + textAlign: 'right', input: TextInput }, { name: 'cashDirection', size: 'sm', - width: 200, - view: getView(directionOptions, 'display'), + width: 282, + view: it => , input: Autocomplete, inputProps: { options: directionOptions, @@ -247,4 +272,12 @@ const elements = [ } ] -export { Schema, elements, direction, type, requirements } +const triggerOrder = R.map(R.prop('code'))(typeOptions) +const sortBy = [ + R.comparator( + (a, b) => + triggerOrder.indexOf(a.triggerType) < triggerOrder.indexOf(b.triggerType) + ) +] + +export { Schema, elements, direction, type, requirements, sortBy } diff --git a/new-lamassu-admin/src/styling/variables.js b/new-lamassu-admin/src/styling/variables.js index 4446657f..fecc40d8 100644 --- a/new-lamassu-admin/src/styling/variables.js +++ b/new-lamassu-admin/src/styling/variables.js @@ -63,11 +63,11 @@ const fontPrimary = 'Mont' const fontSecondary = 'MuseoSans' const fontMonospaced = 'BPmono' -let fontSize1 = 24 -let fontSize2 = 20 -let fontSize3 = 16 -let fontSize4 = 14 -let fontSize5 = 12 +let fontSize1 = 25 +let fontSize2 = 21 +let fontSize3 = 17 +let fontSize4 = 15 +let fontSize5 = 13 if (version === 8) { fontSize1 = 32 diff --git a/new-lamassu-admin/todo.md b/new-lamassu-admin/todo.md index fbf11b51..00f7e93a 100644 --- a/new-lamassu-admin/todo.md +++ b/new-lamassu-admin/todo.md @@ -5,7 +5,6 @@ Overall: - validation is bad rn, negatives being allowed - locale based mil separators 1.000 1,000 - Table should be loaded on slow internet (we want to load the table with no data) - - font sizes could be better - tooltip like components should close on esc - saving should be a one time thing. disable buttons so user doesnt spam it - disable edit on non-everrides => overrides @@ -21,9 +20,6 @@ Locale: Notifications: - one of the crypto balance alerts has to be optional because of migration -Machine status: - - font-size of the 'write to confirm' - Server: - Takes too long to load. Investigate @@ -47,3 +43,7 @@ Compliance: Ideas - Transactions could have a link to the customer - Transactions table on customer should have a link to "transactions" + + +Feedback needed + - font sizes could be better (I've bumped all font sizes by 1px, looks pretty good as fonts do a good vertical bump in size. Maybe some of the fonts don't like even values) \ No newline at end of file