diff --git a/lib/customer-notes.js b/lib/customer-notes.js
index e89c457d..b9e4ae81 100644
--- a/lib/customer-notes.js
+++ b/lib/customer-notes.js
@@ -4,27 +4,28 @@ const _ = require('lodash/fp')
const db = require('./db')
const getCustomerNotes = customerId => {
- const sql = `SELECT * FROM customer_notes WHERE customer_id=$1 LIMIT 1`
+ const sql = `SELECT * FROM customer_notes WHERE customer_id=$1`
return db.oneOrNone(sql, [customerId]).then(res => _.mapKeys((_, key) => _.camelize(key), res))
}
-const createCustomerNotes = (customerId, userId, content) => {
- const sql = `INSERT INTO customer_notes (id, customer_id, last_edited_by, last_edited_at, content) VALUES ($1, $2, $3, now(), $4)`
- return db.none(sql, [uuid.v4(), customerId, userId, content])
+const createCustomerNote = (customerId, userId, title, content) => {
+ const sql = `INSERT INTO customer_notes (id, customer_id, last_edited_by, last_edited_at, title, content) VALUES ($1, $2, $3, now(), $4, $5)`
+ return db.none(sql, [uuid.v4(), customerId, userId, title, content])
}
-const updateCustomerNotes = (customerId, userId, content) => {
- const sql = `UPDATE customer_notes SET last_edited_at=now(), last_edited_by=$1, content=$2 WHERE customer_id=$3 RETURNING *`
- return db.any(sql, [userId, content, customerId])
- .then(res => {
- if (_.isEmpty(res)) {
- createCustomerNotes(customerId, userId, content)
- }
- })
+const deleteCustomerNote = noteId => {
+ const sql = `DELETE FROM customer_notes WHERE id=$1`
+ return db.none(sql, [noteId])
+}
+
+const updateCustomerNote = (noteId, userId, content) => {
+ const sql = `UPDATE customer_notes SET last_edited_at=now(), last_edited_by=$1, content=$2 WHERE id=$3`
+ return db.none(sql, [userId, content, noteId])
}
module.exports = {
getCustomerNotes,
- createCustomerNotes,
- updateCustomerNotes
+ createCustomerNote,
+ deleteCustomerNote,
+ updateCustomerNote
}
diff --git a/lib/customers.js b/lib/customers.js
index fc089d05..89ea723f 100644
--- a/lib/customers.js
+++ b/lib/customers.js
@@ -632,7 +632,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, created AS last_active, fiat AS last_tx_fiat,
- fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, content AS notes
+ fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes
FROM (
SELECT c.id, c.authorized_override,
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
@@ -640,7 +640,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
c.front_camera_path, c.front_camera_override,
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
- c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created, cn.content,
+ c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created, cn.notes,
row_number() OVER (partition by c.id order by t.created desc) AS rn,
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
coalesce(sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (partition by c.id), 0) AS total_spent, ccf.custom_fields
@@ -656,7 +656,8 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
) cf GROUP BY cf.customer_id
) ccf ON c.id = ccf.customer_id
LEFT OUTER JOIN (
- SELECT customer_id, content FROM customer_notes
+ SELECT customer_id, coalesce(json_agg(customer_notes.*), '[]'::json) AS notes FROM customer_notes
+ GROUP BY customer_notes.customer_id
) cn ON c.id = cn.customer_id
WHERE c.id != $2
) AS cl WHERE rn = 1
@@ -669,6 +670,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
.then(customers => Promise.all(_.map(customer => {
return populateOverrideUsernames(customer)
.then(camelize)
+ .then(it => ({ ...it, notes: it.notes.map(camelize) }))
}, customers)))
}
@@ -685,7 +687,7 @@ function getCustomerById (id) {
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, created AS last_active, fiat AS last_tx_fiat,
- fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, content AS notes
+ fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, notes
FROM (
SELECT c.id, c.authorized_override,
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
@@ -693,7 +695,7 @@ function getCustomerById (id) {
c.front_camera_path, c.front_camera_override,
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
- c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created, cn.content,
+ c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created, cn.notes,
row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY c.id) AS total_txs,
sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (PARTITION BY c.id) AS total_spent, ccf.custom_fields
@@ -709,7 +711,8 @@ function getCustomerById (id) {
) cf GROUP BY cf.customer_id
) ccf ON c.id = ccf.customer_id
LEFT OUTER JOIN (
- SELECT customer_id, content FROM customer_notes
+ SELECT customer_id, coalesce(json_agg(customer_notes.*), '[]'::json) AS notes FROM customer_notes
+ GROUP BY customer_notes.customer_id
) cn ON c.id = cn.customer_id
WHERE c.id = $2
) AS cl WHERE rn = 1`
@@ -720,6 +723,7 @@ function getCustomerById (id) {
})
.then(populateOverrideUsernames)
.then(camelize)
+ .then(it => ({ ...it, notes: it.notes.map(camelize) }))
}
/**
diff --git a/lib/new-admin/graphql/resolvers/customer.resolver.js b/lib/new-admin/graphql/resolvers/customer.resolver.js
index bc258284..d3563669 100644
--- a/lib/new-admin/graphql/resolvers/customer.resolver.js
+++ b/lib/new-admin/graphql/resolvers/customer.resolver.js
@@ -1,4 +1,4 @@
-const authentication = require('../modules/authentication')
+const authentication = require('../modules/userManagement')
const anonymous = require('../../../constants').anonymousCustomer
const customers = require('../../../customers')
const filters = require('../../filters')
@@ -39,9 +39,16 @@ const resolvers = {
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
return customers.getCustomerById(customerId)
},
- setCustomerNotes: (...[, { customerId, newContent }, context]) => {
+ createCustomerNote: (...[, { customerId, title, content }, context]) => {
const token = authentication.getToken(context)
- return customerNotes.updateCustomerNotes(customerId, token, newContent)
+ return customerNotes.createCustomerNote(customerId, token, title, content)
+ },
+ editCustomerNote: (...[, { noteId, newContent }, context]) => {
+ const token = authentication.getToken(context)
+ return customerNotes.updateCustomerNote(noteId, token, newContent)
+ },
+ deleteCustomerNote: (...[, { noteId }]) => {
+ return customerNotes.deleteCustomerNote(noteId)
}
}
}
diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js
index adc6d2d6..bdbf3a94 100644
--- a/lib/new-admin/graphql/types/customer.type.js
+++ b/lib/new-admin/graphql/types/customer.type.js
@@ -41,7 +41,7 @@ const typeDef = gql`
subscriberInfo: JSONObject
customFields: [CustomerCustomField]
customInfoRequests: [CustomRequestData]
- notes: String
+ notes: [CustomerNote]
}
input CustomerInput {
@@ -76,6 +76,16 @@ const typeDef = gql`
usSsn: String
}
+ type CustomerNote {
+ id: ID
+ customerId: ID
+ created: Date
+ lastEditedAt: Date
+ lastEditedBy: ID
+ title: String
+ content: String
+ }
+
type Query {
customers(phone: String, name: String, address: String, id: String): [Customer] @auth
customer(customerId: ID!): Customer @auth
@@ -90,7 +100,9 @@ const typeDef = gql`
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth
- setCustomerNotes(customerId: ID!, newContent: String!): Customer @auth
+ createCustomerNote(customerId: ID!, title: String!, content: String!): Boolean @auth
+ editCustomerNote(noteId: ID!, newContent: String!): Boolean @auth
+ deleteCustomerNote(noteId: ID!): Boolean @auth
}
`
diff --git a/migrations/1627868356883-customer-custom-notes.js b/migrations/1627868356883-customer-custom-notes.js
index 87a16e5e..4e8497ec 100644
--- a/migrations/1627868356883-customer-custom-notes.js
+++ b/migrations/1627868356883-customer-custom-notes.js
@@ -8,6 +8,7 @@ exports.up = function (next) {
created TIMESTAMPTZ NOT NULL DEFAULT now(),
last_edited_at TIMESTAMPTZ,
last_edited_by UUID REFERENCES users(id),
+ title TEXT NOT NULL DEFAULT ''
content TEXT NOT NULL DEFAULT ''
)`
]
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerNotes.js b/new-lamassu-admin/src/pages/Customers/CustomerNotes.js
new file mode 100644
index 00000000..b7f055ba
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Customers/CustomerNotes.js
@@ -0,0 +1,94 @@
+import { makeStyles } from '@material-ui/core'
+import * as R from 'ramda'
+import { React, useState } from 'react'
+
+import { H3 } from 'src/components/typography'
+
+import styles from './CustomerNotes.styles'
+import NewNoteCard from './components/notes/NewNoteCard'
+import NewNoteModal from './components/notes/NewNoteModal'
+import NoteCard from './components/notes/NoteCard'
+import NoteEdit from './components/notes/NoteEdit'
+
+const useStyles = makeStyles(styles)
+
+const CustomerNotes = ({
+ customer,
+ createNote,
+ deleteNote,
+ editNote,
+ timezone
+}) => {
+ const classes = useStyles()
+ const [openModal, setOpenModal] = useState(false)
+ const [editing, setEditing] = useState(null)
+
+ const customerNotes = R.sort(
+ (a, b) => new Date(b?.created).getTime() - new Date(a?.created).getTime(),
+ customer.notes ?? []
+ )
+
+ const handleModalClose = () => {
+ setOpenModal(false)
+ }
+
+ const handleModalSubmit = it => {
+ createNote(it)
+ return handleModalClose()
+ }
+
+ const cancelNoteEditing = () => {
+ setEditing(null)
+ }
+
+ const submitNoteEditing = it => {
+ if (!R.equals(it.newContent, it.oldContent)) {
+ editNote({
+ noteId: it.noteId,
+ newContent: it.newContent
+ })
+ }
+ setEditing(null)
+ }
+
+ return (
+
+
+
{'Notes'}
+
+ {R.isNil(editing) && (
+
+
+ {R.map(
+ it => (
+
+ ),
+ customerNotes
+ )}
+
+ )}
+ {!R.isNil(editing) && (
+
+ )}
+ {openModal && (
+
+ )}
+
+ )
+}
+
+export default CustomerNotes
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerNotes.styles.js b/new-lamassu-admin/src/pages/Customers/CustomerNotes.styles.js
new file mode 100644
index 00000000..4ba177b2
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Customers/CustomerNotes.styles.js
@@ -0,0 +1,17 @@
+const styles = {
+ header: {
+ display: 'flex',
+ flexDirection: 'row'
+ },
+ title: {
+ marginTop: 7,
+ marginRight: 24
+ },
+ notesChipList: {
+ display: 'flex',
+ flexDirection: 'row',
+ flexWrap: 'wrap'
+ }
+}
+
+export default styles
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
index e0be120c..626c5f38 100644
--- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
+++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
@@ -23,6 +23,7 @@ import { ReactComponent as Discount } from 'src/styling/icons/button/discount/zo
import { fromNamespace, namespaces } from 'src/utils/config'
import CustomerData from './CustomerData'
+import CustomerNotes from './CustomerNotes'
import styles from './CustomerProfile.styles'
import {
CustomerDetails,
@@ -69,7 +70,14 @@ const GET_CUSTOMER = gql`
label
value
}
- notes
+ notes {
+ id
+ customerId
+ title
+ content
+ created
+ lastEditedAt
+ }
transactions {
txClass
id
@@ -195,6 +203,38 @@ const SET_CUSTOMER_CUSTOM_INFO_REQUEST = gql`
}
`
+const CREATE_NOTE = gql`
+ mutation createCustomerNote(
+ $customerId: ID!
+ $title: String!
+ $content: String!
+ ) {
+ createCustomerNote(
+ customerId: $customerId
+ title: $title
+ content: $content
+ )
+ }
+`
+
+const DELETE_NOTE = gql`
+ mutation deleteCustomerNote($noteId: ID!) {
+ deleteCustomerNote(noteId: $noteId)
+ }
+`
+
+const EDIT_NOTE = gql`
+ mutation editCustomerNote($noteId: ID!, $newContent: String!) {
+ editCustomerNote(noteId: $noteId, newContent: $newContent)
+ }
+`
+
+const GET_DATA = gql`
+ query getData {
+ config
+ }
+`
+
const CustomerProfile = memo(() => {
const history = useHistory()
@@ -204,12 +244,15 @@ const CustomerProfile = memo(() => {
const [clickedItem, setClickedItem] = useState('overview')
const { id: customerId } = useParams()
- const { data: customerResponse, refetch: getCustomer, loading } = useQuery(
- GET_CUSTOMER,
- {
- variables: { customerId }
- }
- )
+ const {
+ data: customerResponse,
+ refetch: getCustomer,
+ loading: customerLoading
+ } = useQuery(GET_CUSTOMER, {
+ variables: { customerId }
+ })
+
+ const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, {
onCompleted: () => getCustomer()
@@ -238,6 +281,18 @@ const CustomerProfile = memo(() => {
}
)
+ const [createNote] = useMutation(CREATE_NOTE, {
+ onCompleted: () => getCustomer()
+ })
+
+ const [deleteNote] = useMutation(DELETE_NOTE, {
+ onCompleted: () => getCustomer()
+ })
+
+ const [editNote] = useMutation(EDIT_NOTE, {
+ onCompleted: () => getCustomer()
+ })
+
const updateCustomer = it =>
setCustomer({
variables: {
@@ -271,6 +326,30 @@ const CustomerProfile = memo(() => {
}
})
+ const createCustomerNote = it =>
+ createNote({
+ variables: {
+ customerId,
+ title: it.title,
+ content: it.content
+ }
+ })
+
+ const deleteCustomerNote = it =>
+ deleteNote({
+ variables: {
+ noteId: it.noteId
+ }
+ })
+
+ const editCustomerNote = it =>
+ editNote({
+ variables: {
+ noteId: it.noteId,
+ newContent: it.newContent
+ }
+ })
+
const onClickSidebarItem = code => setClickedItem(code)
const configData = R.path(['config'])(customerResponse) ?? []
@@ -287,6 +366,11 @@ const CustomerProfile = memo(() => {
const isSuspended = customerData.isSuspended
const isCustomerData = clickedItem === 'customerData'
const isOverview = clickedItem === 'overview'
+ const isNotes = clickedItem === 'notes'
+
+ const loading = customerLoading && configLoading
+
+ const timezone = R.path(['config', 'locale_timezone'], configResponse)
const classes = useStyles({ blocked })
@@ -430,6 +514,16 @@ const CustomerProfile = memo(() => {
authorizeCustomRequest={authorizeCustomRequest}>
)}
+ {isNotes && (
+
+
+
+ )}
{wizard && (
{
- const classes = useStyles()
-
- const [editing, setEditing] = useState(false)
- const [setNotes] = useMutation(SAVE_CUSTOMER_NOTES, {
- refetchQueries: () => ['customer']
- })
-
- const initialValues = {
- notes: R.path(['notes'])(customer) ?? ''
- }
-
- const handleConfirm = values => {
- setNotes({
- variables: {
- customerId: customer.id,
- newContent: values.notes
- }
- })
- setEditing(false)
- }
-
- const getFormattedNotes = content => {
- const fragments = R.split(/\n/)(content ?? '')
- return R.map((it, idx) => {
- if (idx === fragments.length) return <>{it}>
- return (
- <>
- {it}
-
- >
- )
- }, fragments)
- }
-
- return (
- setEditing(true)}
- confirm={true}
- isEditing={editing}
- formName="notes-form"
- className={classes.root}
- contentClassName={classes.content}>
- {!editing && (
- {getFormattedNotes(R.path(['notes'])(customer))}
- )}
- {editing && (
- handleConfirm(values)}>
-
-
- )}
-
- )
-}
-
-export default CustomerNotes
diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomerNotes.styles.js b/new-lamassu-admin/src/pages/Customers/components/CustomerNotes.styles.js
deleted file mode 100644
index 67fc82b5..00000000
--- a/new-lamassu-admin/src/pages/Customers/components/CustomerNotes.styles.js
+++ /dev/null
@@ -1,16 +0,0 @@
-const styles = {
- root: {
- height: 'auto'
- },
- content: {
- height: 'auto'
- },
- form: {
- display: 'flex',
- flexBasis: '90%',
- flexShrink: 1,
- marginTop: 8
- }
-}
-
-export default styles
diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.js b/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.js
index 57c430ff..a6190f13 100644
--- a/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.js
+++ b/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.js
@@ -4,6 +4,8 @@ import React from 'react'
import { ReactComponent as CustomerDataReversedIcon } from 'src/styling/icons/customer-nav/data/comet.svg'
import { ReactComponent as CustomerDataIcon } from 'src/styling/icons/customer-nav/data/white.svg'
+import { ReactComponent as NoteReversedIcon } from 'src/styling/icons/customer-nav/note/comet.svg'
+import { ReactComponent as NoteIcon } from 'src/styling/icons/customer-nav/note/white.svg'
import { ReactComponent as OverviewReversedIcon } from 'src/styling/icons/customer-nav/overview/comet.svg'
import { ReactComponent as OverviewIcon } from 'src/styling/icons/customer-nav/overview/white.svg'
@@ -25,6 +27,12 @@ const CustomerSidebar = ({ isSelected, onClick }) => {
display: 'Customer Data',
Icon: CustomerDataIcon,
InverseIcon: CustomerDataReversedIcon
+ },
+ {
+ code: 'notes',
+ display: 'Notes',
+ Icon: NoteIcon,
+ InverseIcon: NoteReversedIcon
}
]
diff --git a/new-lamassu-admin/src/pages/Customers/components/notes/NewNoteCard.js b/new-lamassu-admin/src/pages/Customers/components/notes/NewNoteCard.js
new file mode 100644
index 00000000..72a1e2b9
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Customers/components/notes/NewNoteCard.js
@@ -0,0 +1,24 @@
+import { makeStyles, Paper } from '@material-ui/core'
+import classNames from 'classnames'
+import { React } from 'react'
+
+import { P } from 'src/components/typography'
+import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg'
+
+import styles from './NoteCard.styles'
+
+const useStyles = makeStyles(styles)
+
+const NewNoteCard = ({ setOpenModal }) => {
+ const classes = useStyles()
+ return (
+ setOpenModal(true)}>
+
+
+ Add new
+
+
+ )
+}
+
+export default NewNoteCard
diff --git a/new-lamassu-admin/src/pages/Customers/components/notes/NewNoteModal.js b/new-lamassu-admin/src/pages/Customers/components/notes/NewNoteModal.js
new file mode 100644
index 00000000..eb832607
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Customers/components/notes/NewNoteModal.js
@@ -0,0 +1,81 @@
+import { makeStyles } from '@material-ui/core/styles'
+import { Form, Formik, Field } from 'formik'
+import { React } 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 { TextInput } from 'src/components/inputs/formik'
+
+import styles from './NewNoteModal.styles'
+
+const useStyles = makeStyles(styles)
+
+const initialValues = {
+ title: '',
+ content: ''
+}
+
+const validationSchema = Yup.object().shape({
+ title: Yup.string()
+ .required()
+ .trim()
+ .max(25),
+ content: Yup.string().required()
+})
+
+const NewNoteModal = ({ showModal, onClose, onSubmit, errorMsg }) => {
+ const classes = useStyles()
+
+ return (
+ <>
+
+ {
+ onSubmit({ title, content })
+ }}>
+
+
+
+ >
+ )
+}
+
+export default NewNoteModal
diff --git a/new-lamassu-admin/src/pages/Customers/components/notes/NewNoteModal.styles.js b/new-lamassu-admin/src/pages/Customers/components/notes/NewNoteModal.styles.js
new file mode 100644
index 00000000..a42ecd41
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Customers/components/notes/NewNoteModal.styles.js
@@ -0,0 +1,25 @@
+import { spacer } from 'src/styling/variables'
+
+const styles = {
+ form: {
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100%',
+ '& > *': {
+ marginTop: 20
+ },
+ '& > *:last-child': {
+ marginTop: 'auto'
+ }
+ },
+ submit: {
+ margin: [['auto', 0, 0, 'auto']]
+ },
+ footer: {
+ display: 'flex',
+ flexDirection: 'row',
+ margin: [['auto', 0, spacer * 3, 0]]
+ }
+}
+
+export default styles
diff --git a/new-lamassu-admin/src/pages/Customers/components/notes/NoteCard.js b/new-lamassu-admin/src/pages/Customers/components/notes/NoteCard.js
new file mode 100644
index 00000000..68f779d6
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Customers/components/notes/NoteCard.js
@@ -0,0 +1,55 @@
+import { makeStyles, Paper } from '@material-ui/core'
+import * as R from 'ramda'
+import { React } from 'react'
+
+import { H3, P } from 'src/components/typography'
+import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
+import { formatDate } from 'src/utils/timezones'
+
+import styles from './NoteCard.styles'
+
+const useStyles = makeStyles(styles)
+
+const formatContent = content => {
+ const fragments = R.split(/\n/)(content)
+ return R.map((it, idx) => {
+ if (idx === fragments.length) return <>{it}>
+ return (
+ <>
+ {it}
+
+ >
+ )
+ }, fragments)
+}
+
+const NoteCard = ({ note, deleteNote, handleClick, timezone }) => {
+ const classes = useStyles()
+
+ return (
+
+
handleClick(note)}>
+
+
+
{note?.title}
+
{formatDate(note?.created, timezone, 'yyyy-MM-dd')}
+
+
+ {
+ e.stopPropagation()
+ deleteNote({ noteId: note.id })
+ }}
+ />
+
+
+
+ {formatContent(note?.content)}
+
+
+
+ )
+}
+
+export default NoteCard
diff --git a/new-lamassu-admin/src/pages/Customers/components/notes/NoteCard.styles.js b/new-lamassu-admin/src/pages/Customers/components/notes/NoteCard.styles.js
new file mode 100644
index 00000000..9712fa77
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Customers/components/notes/NoteCard.styles.js
@@ -0,0 +1,93 @@
+import { zircon } from 'src/styling/variables'
+
+const styles = {
+ noteCardWrapper: {
+ flexGrow: 0,
+ flexShrink: 0,
+ flexBasis: `25%`,
+ minWidth: 0,
+ maxWidth: 500,
+ '&:nth-child(4n+1)': {
+ '& > div': {
+ margin: [[0, 10, 0, 0]]
+ }
+ },
+ '&:nth-child(4n)': {
+ '& > div': {
+ margin: [[0, 0, 0, 10]]
+ }
+ },
+ margin: [[10, 0]]
+ },
+ noteCardChip: {
+ height: 200,
+ margin: [[0, 10]],
+ padding: [[10, 10]],
+ cursor: 'pointer'
+ },
+ newNoteCard: {
+ backgroundColor: zircon,
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ noteCardHeader: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ width: '100%'
+ },
+ noteCardTitle: {
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ marginRight: 10
+ },
+ noteCardContent: {
+ display: 'box',
+ lineClamp: 7,
+ boxOrient: 'vertical',
+ margin: [[15, 0]],
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ wordWrap: 'break-word'
+ },
+ editCardChip: {
+ height: 325,
+ padding: 15
+ },
+ editCardHeader: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 15
+ },
+ editCardActions: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ '& > *': {
+ marginRight: 10
+ },
+ '& > *:last-child': {
+ marginRight: 0
+ }
+ },
+ editNotesContent: {
+ '& > div': {
+ '&:after': {
+ borderBottom: 'none'
+ },
+ '&:before': {
+ borderBottom: 'none'
+ },
+ '&:hover:not(.Mui-disabled)::before': {
+ borderBottom: 'none'
+ }
+ }
+ }
+}
+
+export default styles
diff --git a/new-lamassu-admin/src/pages/Customers/components/notes/NoteEdit.js b/new-lamassu-admin/src/pages/Customers/components/notes/NoteEdit.js
new file mode 100644
index 00000000..dcfc36a8
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Customers/components/notes/NoteEdit.js
@@ -0,0 +1,103 @@
+import { makeStyles, Paper } from '@material-ui/core'
+import { formatDurationWithOptions, intervalToDuration } from 'date-fns/fp'
+import { Form, Formik, Field } from 'formik'
+import { React, useRef } from 'react'
+import * as Yup from 'yup'
+
+import { ActionButton } from 'src/components/buttons'
+import { TextInput } from 'src/components/inputs/formik'
+import { P } from 'src/components/typography'
+import { ReactComponent as CancelIconInverse } from 'src/styling/icons/button/cancel/white.svg'
+import { ReactComponent as CancelIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
+import { ReactComponent as SaveIconInverse } from 'src/styling/icons/circle buttons/save/white.svg'
+import { ReactComponent as SaveIcon } from 'src/styling/icons/circle buttons/save/zodiac.svg'
+import { toTimezone } from 'src/utils/timezones'
+
+import styles from './NoteCard.styles'
+
+const useStyles = makeStyles(styles)
+
+const NoteEdit = ({ note, cancel, edit, timezone }) => {
+ const formRef = useRef()
+ const classes = useStyles()
+
+ const validationSchema = Yup.object().shape({
+ content: Yup.string()
+ })
+
+ const initialValues = {
+ content: note.content
+ }
+
+ return (
+
+
+
+ {`Last edited `}
+ {formatDurationWithOptions(
+ { delimited: ', ' },
+ intervalToDuration({
+ start: toTimezone(new Date(note.lastEditedAt), timezone),
+ end: toTimezone(new Date(), timezone)
+ })
+ )}
+ {` ago`}
+
+
+
+ {`Cancel`}
+
+
+ {`Save changes`}
+
+
formRef.current.setFieldValue('content', '')}>
+ {`Clear content`}
+
+
+
+
+ edit({
+ noteId: note.id,
+ newContent: content,
+ oldContent: note.content
+ })
+ }
+ innerRef={formRef}>
+
+
+
+ )
+}
+
+export default NoteEdit
diff --git a/new-lamassu-admin/src/styling/icons/customer-nav/note/comet.svg b/new-lamassu-admin/src/styling/icons/customer-nav/note/comet.svg
new file mode 100644
index 00000000..951e3108
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/customer-nav/note/comet.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/customer-nav/note/white.svg b/new-lamassu-admin/src/styling/icons/customer-nav/note/white.svg
new file mode 100644
index 00000000..def24ed8
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/customer-nav/note/white.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/customer-nav/note/zodiac.svg b/new-lamassu-admin/src/styling/icons/customer-nav/note/zodiac.svg
new file mode 100644
index 00000000..6388cc81
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/customer-nav/note/zodiac.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file