diff --git a/components/Transaction/Invoice.tsx b/components/Transaction/Invoice.tsx
new file mode 100644
index 00000000..ed0bb51b
--- /dev/null
+++ b/components/Transaction/Invoice.tsx
@@ -0,0 +1,60 @@
+import React from 'react'
+import { XEC_TX_EXPLORER_URL, BCH_TX_EXPLORER_URL, NETWORK_TICKERS_FROM_ID, XEC_NETWORK_ID } from 'constants/index'
+import moment from 'moment'
+import logoImageSource from 'assets/logo.png'
+import Image from 'next/image'
+
+const Receipt = React.forwardRef((props, ref) => {
+ const { data } = props
+ const {
+ invoiceNumber,
+ amount,
+ recipientName,
+ recipientAddress,
+ description,
+ customerName,
+ customerAddress,
+ createdAt,
+ transaction
+ } = data
+ const formattedDate = new Date(createdAt).toLocaleString('en-US', {
+ month: 'short',
+ day: '2-digit',
+ year: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ hour12: true
+ }).replace(',', '')
+ const transactionNetworkId = transaction.networkId
+ const url = transactionNetworkId === XEC_NETWORK_ID ? XEC_TX_EXPLORER_URL : BCH_TX_EXPLORER_URL
+ return (
+
+
+
+
+
+
+
INVOICE
+
#{ invoiceNumber }
+
+
+
Generated at: { formattedDate }
+
Transaction ID: {transaction.hash}
+
Transaction Date & Time: { moment(transaction.timestamp * 1000).tz('utc').format('lll') }
+
+
Senders
+
{ customerName } - { customerAddress }
+
+
Recipients
+
{ recipientName } - { recipientAddress }
+
+
+
+
Notes: { description }
+
Amount: { amount } { NETWORK_TICKERS_FROM_ID[transactionNetworkId]}
+
+
+ )
+})
+
+export default Receipt
diff --git a/components/Transaction/InvoiceModal.tsx b/components/Transaction/InvoiceModal.tsx
index 82fcb639..134b8100 100644
--- a/components/Transaction/InvoiceModal.tsx
+++ b/components/Transaction/InvoiceModal.tsx
@@ -1,26 +1,22 @@
-import React, { useState, useEffect, ReactElement } from 'react'
+import React, { useState, useEffect, ReactElement, useRef } from 'react'
import style from './transaction.module.css'
import Button from 'components/Button'
import { CreateInvoicePOSTParameters } from 'utils/validators'
import axios from 'axios'
import { Prisma } from '@prisma/client'
-
-export interface InvoiceData {
- id?: string
- invoiceNumber: Prisma.Decimal
- amount: number
- recipientName: string
- recipientAddress: string
- description: string
- customerName: string
- customerAddress: string
-}
+import { useReactToPrint } from 'react-to-print'
+import PrintableReceipt from './Invoice'
+import XECIcon from 'assets/xec-logo.png'
+import BCHIcon from 'assets/bch-logo.png'
+import { XEC_NETWORK_ID } from 'constants/index'
+import Image from 'next/image'
+import { InvoiceWithTransaction } from 'services/invoiceService'
interface InvoiceModalProps {
isOpen: boolean
onClose: () => void
transaction: any
- invoiceData: InvoiceData | null
+ invoiceData: InvoiceWithTransaction | null
mode: 'create' | 'edit' | 'view'
}
@@ -31,25 +27,52 @@ export default function InvoiceModal ({
transaction,
mode
}: InvoiceModalProps): ReactElement | null {
- const [formData, setFormData] = useState({
+ const contentRef = useRef(null)
+ const handlePrint = useReactToPrint({
+ contentRef,
+ onBeforePrint: async () => {
+ return await new Promise((resolve) => {
+ setLoading(true)
+ setTimeout(() => {
+ setLoading(false)
+ resolve()
+ }, 1000)
+ })
+ }
+ })
+
+ const [formData, setFormData] = useState({
+ id: '',
invoiceNumber: '',
- amount: Number(transaction?.amount),
+ amount: transaction?.amount ?? new Prisma.Decimal(0),
recipientName: '',
- recipientAddress: transaction?.address?.address,
+ recipientAddress: transaction?.address ?? '',
description: '',
customerName: '',
- customerAddress: ''
+ customerAddress: '',
+ userId: transaction?.userId ?? '',
+ transaction: transaction ?? null,
+ transactionId: transaction?.id ?? null,
+ createdAt: new Date(),
+ updatedAt: new Date()
})
+ const [loading, setLoading] = useState(false)
useEffect(() => {
setFormData(invoiceData ?? {
invoiceNumber: '',
- amount: Number(transaction?.amount),
+ amount: transaction?.amount,
recipientName: '',
- recipientAddress: transaction?.address?.address,
+ recipientAddress: transaction?.address,
description: '',
customerName: '',
- customerAddress: ''
+ customerAddress: '',
+ userId: transaction?.userId ?? '',
+ transaction: transaction ?? null,
+ transactionId: transaction?.id ?? null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ id: ''
})
}, [transaction, mode, invoiceData])
@@ -63,26 +86,34 @@ export default function InvoiceModal ({
const handleModalClose = (): void => {
setFormData({
invoiceNumber: '',
- amount: 0,
+ amount: new Prisma.Decimal(0),
recipientName: '',
recipientAddress: '',
description: '',
customerName: '',
- customerAddress: ''
+ customerAddress: '',
+ userId: transaction?.userId ?? '',
+ transaction: transaction ?? null,
+ transactionId: transaction?.id ?? null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ id: ''
})
onClose()
}
async function handleSubmit (e: React.FormEvent): Promise {
e.preventDefault()
-
+ setLoading(true)
if (mode === 'edit') {
await updateInvoice()
} else {
await createInvoice()
}
+ setLoading(false)
onClose()
}
+
async function createInvoice (): Promise {
const payload: CreateInvoicePOSTParameters = {
...formData,
@@ -133,12 +164,17 @@ export default function InvoiceModal ({
/>
-
+
+
+
+ { transaction.networkId === XEC_NETWORK_ID ? : }
+
+
@@ -193,13 +229,13 @@ export default function InvoiceModal ({
/>
-
{!isReadOnly && (
- Submit
+ Submit
)}
@@ -228,12 +264,17 @@ export default function InvoiceModal ({
Customer Address: {formData.customerAddress}
-
-
Close
+
+ Cancel
+ Download as PDF
}
+
)
diff --git a/components/Transaction/PaybuttonTransactions.tsx b/components/Transaction/PaybuttonTransactions.tsx
index 94b5685f..fbd9535a 100644
--- a/components/Transaction/PaybuttonTransactions.tsx
+++ b/components/Transaction/PaybuttonTransactions.tsx
@@ -5,17 +5,11 @@ import BCHIcon from 'assets/bch-logo.png'
import EyeIcon from 'assets/eye-icon.png'
import CheckIcon from 'assets/check-icon.png'
import XIcon from 'assets/x-icon.png'
-import Plus from 'assets/plus.png'
-import Pencil from 'assets/pencil.png'
-import FileText from 'assets/file-text.png'
import TableContainerGetter, { DataGetterReturn } from '../TableContainer/TableContainerGetter'
import { compareNumericString } from 'utils/index'
import moment from 'moment-timezone'
import { XEC_TX_EXPLORER_URL, BCH_TX_EXPLORER_URL } from 'constants/index'
-import InvoiceModal, { InvoiceData } from './InvoiceModal'
-import style from './transaction.module.css'
-import { TransactionWithAddressAndPricesAndInvoices } from 'services/transactionService'
interface IProps {
addressSyncing: {
@@ -43,58 +37,9 @@ function fetchTransactionsByPaybuttonId (paybuttonId: string): (page: number, pa
}
}
-const fetchNextInvoiceNumberByUserId = async (): Promise => {
- const response = await fetch('/api/invoices/invoiceNumber/', {
- headers: {
- Timezone: moment.tz.guess()
- }
- })
- const result = await response?.json()
- return result?.invoiceNumber
-}
-
export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = moment.tz.guess() }: IProps): JSX.Element => {
- const [isModalOpen, setIsModalOpen] = useState(false)
- const [invoiceData, setInvoiceData] = useState(null)
- const [invoiceDataTransaction, setInvoiceDataTransaction] = useState(null)
- const [localRefreshCount, setLocalRefreshCount] = useState(tableRefreshCount)
-
- const [invoiceMode, setInvoiceMode] = useState<'create' | 'edit' | 'view'>('create')
-
- const onCreateInvoice = async (transaction: TransactionWithAddressAndPricesAndInvoices): Promise => {
- const nextInvoiceNumber = await fetchNextInvoiceNumberByUserId()
- const invoiceData = {
- invoiceNumber: nextInvoiceNumber ?? '',
- amount: Number(transaction.amount),
- recipientName: '',
- recipientAddress: transaction.address.address,
- description: '',
- customerName: '',
- customerAddress: ''
- }
- setInvoiceDataTransaction(transaction)
- setInvoiceData(invoiceData)
- setInvoiceMode('create')
- setIsModalOpen(true)
- }
-
- const onEditInvoice = (invoiceData: InvoiceData): void => {
- setInvoiceData(invoiceData)
- setInvoiceMode('edit')
- setIsModalOpen(true)
- }
+ const [localRefreshCount] = useState(tableRefreshCount)
- const onSeeInvoice = (invoiceData: InvoiceData): void => {
- setInvoiceData(invoiceData)
- setInvoiceMode('view')
- setIsModalOpen(true)
- }
-
- const handleCloseModal = (): void => {
- setIsModalOpen(false)
- setInvoiceData(null)
- setLocalRefreshCount(prev => prev + 1)
- }
const columns = useMemo(
() => [
{
@@ -167,76 +112,13 @@ export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = mom
)
}
- },
- {
- Header: () => (Actions
),
- id: 'actions',
- Cell: (cellProps) => {
- const invoices = cellProps.row.original.invoices
- const hasInvoice = invoices?.length > 0
-
- return (
-
- {!hasInvoice
- ? (
-
-
{
- onCreateInvoice(cellProps.row.original).catch(console.error)
- }}
- title="Create Invoice"
- className={style.create_invoice}
- style={{ background: 'none', border: 'none', cursor: 'pointer' }}
- >
-
-
-
-
New button
-
- )
- : (
- <>
-
- onEditInvoice(cellProps.row.original.invoices[0])}
- title="Edit Invoice"
- className={style.edit_invoice}
- style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '5px' }}
- >
-
-
-
-
- onSeeInvoice(cellProps.row.original.invoices[0])}
- title="See Invoice"
- className={style.see_invoice}
- style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '5px' }}
- >
-
-
-
-
- >
- )}
-
- )
- }
}
-
],
[]
)
return (
<>
-
>
)
}
diff --git a/components/Transaction/transaction.module.css b/components/Transaction/transaction.module.css
index dff9198f..ecd8a7cc 100644
--- a/components/Transaction/transaction.module.css
+++ b/components/Transaction/transaction.module.css
@@ -221,6 +221,11 @@ body[data-theme='dark'] .form_ctn select:not([multiple]) {
box-shadow: none;
}
+.form_ctn input:disabled {
+ background-color: var(--secondary-bg-color) !important;
+ color: var(--primary-text-color);
+}
+
.form_ctn .delete_btn {
background-color: unset;
border: none;
@@ -351,7 +356,7 @@ body[data-theme='dark'] .edit_invoice img {
filter: invert(1);
}
-body[data-theme='dark'] .see_invoice img {
+body[data-theme='dark'] .view_invoice img {
filter: invert(1);
}
@@ -365,7 +370,7 @@ body[data-theme='dark'] .see_invoice img {
brightness(100%) contrast(102%);
}
-.see_invoice_ctn:hover .see_invoice {
+.view_invoice_ctn:hover .view_invoice {
filter: invert(60%) sepia(14%) saturate(5479%) hue-rotate(195deg)
brightness(100%) contrast(102%);
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 90552444..6a632246 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,7 @@
"react-select": "^5.8.3",
"react-table": "^7.8.0",
"react-timezone-select": "^3.2.8",
+ "react-to-print": "^3.1.0",
"simpledotcss": "^2.1.0",
"socket.io": "^4.4.1",
"socket.io-client": "^4.7.1",
diff --git a/pages/api/invoices/index.ts b/pages/api/invoices/index.ts
index a63deb1a..374e7961 100644
--- a/pages/api/invoices/index.ts
+++ b/pages/api/invoices/index.ts
@@ -1,6 +1,7 @@
import { setSession } from 'utils/setSession'
import { RESPONSE_MESSAGES } from 'constants/index'
import { CreateInvoiceParams, UpdateInvoiceParams, createInvoice, updateInvoice } from 'services/invoiceService'
+import { Decimal } from '@prisma/client/runtime/library'
export default async (
req: any,
@@ -11,8 +12,15 @@ export default async (
if (req.method === 'POST') {
try {
const createInvoiceParams: CreateInvoiceParams = {
- ...req.body,
- userId: session.userId
+ userId: session.userId,
+ transactionId: req.body.transaction?.id,
+ invoiceNumber: req.body.invoiceNumber,
+ amount: new Decimal(req.body.amount),
+ description: req.body.description,
+ recipientName: req.body.recipientName,
+ recipientAddress: req.body.recipientAddress,
+ customerName: req.body.customerName,
+ customerAddress: req.body.customerAddress
}
const invoice = await createInvoice(createInvoiceParams)
res.status(200).json({
@@ -30,7 +38,11 @@ export default async (
} else if (req.method === 'PUT') {
try {
const updateInvoiceParams: UpdateInvoiceParams = {
- ...req.body
+ description: req.body.description,
+ recipientName: req.body.recipientName,
+ recipientAddress: req.body.recipientAddress,
+ customerName: req.body.customerName,
+ customerAddress: req.body.customerAddress
}
const invoiceId = req.query.invoiceId as string
const invoice = await updateInvoice(session.userId,
diff --git a/pages/payments/index.tsx b/pages/payments/index.tsx
index 79290036..bc2b1cb0 100644
--- a/pages/payments/index.tsx
+++ b/pages/payments/index.tsx
@@ -10,15 +10,22 @@ import Link from 'next/link'
import XECIcon from 'assets/xec-logo.png'
import BCHIcon from 'assets/bch-logo.png'
import EyeIcon from 'assets/eye-icon.png'
-import { formatQuoteValue, compareNumericString, removeUnserializableFields } from 'utils/index'
+import { formatQuoteValue, compareNumericString, removeUnserializableFields, removeDateFields } from 'utils/index'
import { XEC_NETWORK_ID, BCH_TX_EXPLORER_URL, XEC_TX_EXPLORER_URL, NETWORK_TICKERS_FROM_ID, DECIMALS } from 'constants/index'
import moment from 'moment-timezone'
import TopBar from 'components/TopBar'
import { fetchUserWithSupertokens, UserWithSupertokens } from 'services/userService'
-import { UserProfile } from '@prisma/client'
+import { Organization, UserProfile } from '@prisma/client'
import Button from 'components/Button'
import style from './payments.module.css'
import SettingsIcon from '../../assets/settings-slider-icon.png'
+import Plus from 'assets/plus.png'
+import Pencil from 'assets/pencil.png'
+import FileText from 'assets/file-text.png'
+import InvoiceModal from 'components/Transaction/InvoiceModal'
+import { TransactionWithAddressAndPricesAndInvoices } from 'services/transactionService'
+import { fetchOrganizationForUser } from 'services/organizationService'
+import { InvoiceWithTransaction } from 'services/invoiceService'
export const getServerSideProps: GetServerSideProps = async (context) => {
// this runs on the backend, so we must call init on supertokens-node SDK
@@ -39,12 +46,18 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
if (session === undefined) return
const userId = session.getUserId()
const user = await fetchUserWithSupertokens(userId)
- removeUnserializableFields(user.userProfile)
+ const organization = await fetchOrganizationForUser(userId)
+ removeUnserializableFields(user.userProfile)
+ let serializableOrg = null
+ if (organization !== null) {
+ serializableOrg = removeDateFields(organization)
+ }
return {
props: {
user,
- userId
+ userId,
+ organization: serializableOrg
}
}
}
@@ -52,9 +65,10 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
interface PaybuttonsProps {
user: UserWithSupertokens
userId: string
+ organization: Organization
}
-export default function Payments ({ user, userId }: PaybuttonsProps): React.ReactElement {
+export default function Payments ({ user, userId, organization }: PaybuttonsProps): React.ReactElement {
const timezone = user?.userProfile.preferredTimezone === '' ? moment.tz.guess() : user?.userProfile?.preferredTimezone
const [selectedCurrencyCSV, setSelectedCurrencyCSV] = useState('')
const [paybuttonNetworks, setPaybuttonNetworks] = useState>(new Set())
@@ -64,7 +78,59 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
const [showFilters, setShowFilters] = useState(false)
const [tableLoading, setTableLoading] = useState(true)
const [refreshCount, setRefreshCount] = useState(0)
+ const [invoiceData, setInvoiceData] = useState(null)
+ const [invoiceDataTransaction, setInvoiceDataTransaction] = useState(null)
+ const [invoiceMode, setInvoiceMode] = useState<'create' | 'edit' | 'view'>('create')
+ const [isModalOpen, setIsModalOpen] = useState(false)
+ const fetchNextInvoiceNumberByUserId = async (): Promise => {
+ const response = await fetch('/api/invoices/invoiceNumber/', {
+ headers: {
+ Timezone: moment.tz.guess()
+ }
+ })
+ const result = await response?.json()
+ return result?.invoiceNumber
+ }
+ const handleCloseModal = (): void => {
+ setIsModalOpen(false)
+ setInvoiceData(null)
+ setRefreshCount(prev => prev + 1)
+ }
+ const onCreateInvoice = async (transaction: TransactionWithAddressAndPricesAndInvoices): Promise => {
+ const nextInvoiceNumber = await fetchNextInvoiceNumberByUserId()
+ const invoiceData = {
+ invoiceNumber: nextInvoiceNumber ?? '',
+ amount: transaction.amount,
+ recipientName: '',
+ recipientAddress: transaction.address ?? '',
+ description: '',
+ customerName: organization?.name ?? '',
+ customerAddress: '',
+ userId: '',
+ transaction,
+ transactionId: transaction.id,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ id: ''
+ }
+ setInvoiceDataTransaction(transaction)
+ setInvoiceData(invoiceData)
+ setInvoiceMode('create')
+ setIsModalOpen(true)
+ }
+
+ const onEditInvoice = (invoiceData: InvoiceWithTransaction): void => {
+ setInvoiceData(invoiceData)
+ setInvoiceMode('edit')
+ setIsModalOpen(true)
+ }
+
+ const onViewInvoice = (invoiceData: InvoiceWithTransaction): void => {
+ setInvoiceData(invoiceData)
+ setInvoiceMode('view')
+ setIsModalOpen(true)
+ }
useEffect(() => {
setRefreshCount(prev => prev + 1)
}, [selectedButtonIds])
@@ -77,6 +143,7 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
return await res.json()
}
}
+
const getDataAndSetUpCurrencyCSV = async (): Promise => {
const paybuttons = await fetchPaybuttons()
const networkIds: Set = new Set()
@@ -212,6 +279,70 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
)
}
+ },
+ {
+ Header: () => (Invoice
),
+ id: 'actions',
+ disableSortBy: true,
+ Cell: (cellProps) => {
+ const transaction = cellProps.row.original
+ const invoices = transaction.invoices ?? []
+ const hasInvoice = invoices.filter(i => i !== null).length > 0
+ let invoice = {} as InvoiceWithTransaction
+ if (hasInvoice) {
+ invoice = {
+ transaction,
+ ...transaction.invoices[0]
+ }
+ }
+
+ return (
+
+ {!hasInvoice
+ ? (
+
+
{
+ onCreateInvoice(transaction).catch(console.error)
+ }}
+ title="Create Invoice"
+ className={style.create_invoice}
+ style={{ background: 'none', border: 'none', cursor: 'pointer' }}
+ >
+
+
+
+
New button
+
+ )
+ : (
+ <>
+
+ onEditInvoice(invoice)}
+ title="Edit Invoice"
+ className={style.edit_invoice}
+ style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '5px' }}
+ >
+
+
+
+
+ onViewInvoice(invoice)}
+ title="View Invoice"
+ className={style.view_invoice}
+ style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '5px' }}
+ >
+
+
+
+
+ >
+ )}
+
+ )
+ }
}
],
[]
@@ -357,6 +488,13 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
tableRefreshCount={refreshCount}
emptyMessage={tableLoading ? 'Loading...' : 'No Payments to show yet'}
/>
+
>
)
}
diff --git a/pages/payments/payments.module.css b/pages/payments/payments.module.css
index 4e50b3e2..cd955824 100644
--- a/pages/payments/payments.module.css
+++ b/pages/payments/payments.module.css
@@ -59,7 +59,7 @@ body[data-theme='dark'] .show_filters_button img {
.filter_button:active {
transform: scale(0.95);
- }
+}
.active {
background-color: var(--accent-color);
@@ -80,10 +80,250 @@ body[data-theme='dark'] .show_filters_button img {
.wallet_label {
margin-top: 20px !important;
- }
+}
- .filter_btns {
+.filter_btns {
display: flex;
align-items: center;
gap: 10px;
- }
\ No newline at end of file
+}
+
+.form_ctn_outer {
+ background-color: rgba(255, 255, 255, 0.3);
+ backdrop-filter: blur(5px);
+ position: fixed;
+ top: 0;
+ left: 0px;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 99999;
+}
+
+.form_ctn_inner {
+ width: 100%;
+ max-width: 600px;
+ align-self: middle;
+}
+
+.form_ctn_inner h4 {
+ margin: 0;
+ margin-left: 5px;
+ margin-bottom: 5px;
+}
+
+.form_ctn {
+ background-color: var(--secondary-bg-color);
+ width: 100%;
+ padding: 25px;
+ border-radius: 10px;
+ border: 1px solid var(--secondary-text-color);
+ overflow-y: auto;
+ max-height: 90vh;
+}
+
+body[data-theme='dark'] .form_ctn_outer {
+ background-color: rgba(0, 0, 0, 0.3);
+}
+
+.form_ctn p {
+ margin-top: 0;
+}
+
+.form_ctn form {
+ display: flex;
+ flex-direction: column;
+}
+
+.form_ctn label {
+ margin-bottom: 5px;
+}
+
+body[data-theme='dark'] .form_ctn select {
+ color: #fff;
+}
+
+.form_ctn select {
+ background-color: var(--secondary-bg-color);
+}
+
+.form_ctn input,
+.form_ctn textarea {
+ border: 1px solid var(--secondary-text-color) !important;
+ width: 100%;
+}
+
+body[data-theme='dark'] .form_ctn input,
+body[data-theme='dark'] .form_ctn textarea,
+body[data-theme='dark'] .form_ctn select {
+ background-color: var(--primary-bg-color);
+ border-color: #898EA4 !important;
+}
+
+body[data-theme='dark'] .form_ctn select:not([multiple]) {
+ background-image: linear-gradient(45deg,transparent 49%,#fff 51%),linear-gradient(135deg,#fff 51%,transparent 49%);
+}
+
+.labelMargin {
+ margin-top: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.form_ctn button:last-child {
+ margin-right: 0px;
+ box-shadow: none;
+}
+
+.form_ctn .delete_btn {
+ background-color: unset;
+ border: none;
+ margin-right: 0px;
+ margin-left: 10px;
+ color: var(--primary-text-color);
+ border-radius: 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ opacity: 0.6;
+ position: relative;
+ transition: all ease-in-out 100ms;
+}
+
+.form_ctn .delete_btn:hover {
+ color: red;
+ opacity: 1;
+}
+
+.form_ctn .delete_btn div {
+ width: 15px;
+ border-radius: 0px !important;
+ position: absolute;
+ right: 0px;
+ top: 12px;
+ opacity: -10;
+ transition: all ease-in-out 100ms;
+}
+
+.form_ctn .delete_btn:hover div {
+ right: -18px;
+ opacity: 1;
+}
+
+.form_ctn .delete_confirm_btn {
+ padding-left: 20px;
+ padding-right: 20px;
+ border: 1px solid var(--primary-text-color);
+ background-color: var(--primary-bg-color);
+ color: red;
+ transition: all ease-in-out 100ms;
+ font-weight: 600;
+}
+
+.delete_button_form_ctn {
+ word-break: normal;
+}
+
+.form_ctn .delete_confirm_btn:hover {
+ border: 1px solid var(--primary-text-color);
+ background-color: rgb(149, 0, 0);
+ color: var(--secondary-bg-color);
+}
+
+.tooltiptext {
+ visibility: hidden;
+ opacity: 0;
+ background-color: var(--secondary-bg-color);
+ color: var(--primary-text-color);
+ text-align: center;
+ padding: 2px 0px;
+ border-radius: 5px;
+ position: absolute;
+ z-index: 1;
+ bottom: -30px;
+ width: 80px;
+ right: -5px;
+ flex-basis: 1;
+ font-size: 12px;
+ z-index: 99999999;
+ box-sizing: border-box;
+ transition: all 100ms ease-in-out;
+}
+
+.tooltiptext::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ width: 0;
+ height: 0;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ transform: translate(-50%, -9px);
+ border-bottom: 7px solid var(--secondary-bg-color);
+}
+
+.create_invoice:hover .tooltiptext {
+ visibility: visible;
+ bottom: -35px;
+ opacity: 1;
+}
+
+.create_invoice_ctn {
+ position: relative;
+ position: sticky;
+ bottom: 0;
+ float: right;
+}
+
+.create_invoice {
+ background-color: var(--secondary-bg-color);
+ font-size: 44px;
+ display: inline-block;
+ border-radius: 100px;
+ padding: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all ease-in-out 100ms;
+ position: relative;
+ border: 1px solid var(--secondary-text-color);
+}
+
+.invoice_view_item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px;
+ border-bottom: 1px solid var(--primary-text-color);
+}
+
+body[data-theme='dark'] .create_invoice img {
+ filter: invert(1);
+}
+
+body[data-theme='dark'] .edit_invoice img {
+ filter: invert(1);
+}
+
+body[data-theme='dark'] .view_invoice img {
+ filter: invert(1);
+}
+
+.create_invoice_ctn:hover .create_invoice {
+ filter: invert(60%) sepia(14%) saturate(5479%) hue-rotate(195deg)
+ brightness(100%) contrast(102%);
+}
+
+.edit_invoice_ctn:hover .edit_invoice{
+ filter: invert(60%) sepia(14%) saturate(5479%) hue-rotate(195deg)
+ brightness(100%) contrast(102%);
+}
+
+.view_invoice_ctn:hover .view_invoice {
+ filter: invert(60%) sepia(14%) saturate(5479%) hue-rotate(195deg)
+ brightness(100%) contrast(102%);
+}
+
diff --git a/redis/paymentCache.ts b/redis/paymentCache.ts
index 1b1323cc..c59d6fea 100755
--- a/redis/paymentCache.ts
+++ b/redis/paymentCache.ts
@@ -1,6 +1,12 @@
import { redis } from 'redis/clientInstance'
import { Address, Prisma } from '@prisma/client'
-import { generateTransactionsWithPaybuttonsAndPricesForAddress, getTransactionValue, TransactionsWithPaybuttonsAndPrices, TransactionWithAddressAndPrices } from 'services/transactionService'
+import {
+ generateTransactionsWithPaybuttonsAndPricesForAddress,
+ getTransactionValue,
+ TransactionsWithPaybuttonsAndPrices,
+ TransactionWithAddressAndPrices,
+ TransactionWithAddressAndPricesAndInvoices
+} from 'services/transactionService'
import { fetchAllUserAddresses, AddressPaymentInfo } from 'services/addressService'
import { fetchPaybuttonArrayByUserId } from 'services/paybuttonService'
@@ -80,6 +86,7 @@ export const generatePaymentFromTx = async (tx: TransactionsWithPaybuttonsAndPri
console.warn('Orphan address:', tx.address.address)
}
return {
+ id: tx.id,
timestamp: tx.timestamp,
values,
amount: tx.amount,
@@ -90,6 +97,41 @@ export const generatePaymentFromTx = async (tx: TransactionsWithPaybuttonsAndPri
}
}
+export const generatePaymentFromTxWithInvoices = async (tx: TransactionWithAddressAndPricesAndInvoices, userId?: string): Promise => {
+ const values = getTransactionValue(tx)
+ let buttonDisplayDataList: Array<{ name: string, id: string}> = []
+ if (tx.address.paybuttons !== undefined) {
+ buttonDisplayDataList = tx.address.paybuttons.map(
+ (conn) => {
+ return {
+ name: conn.paybutton.name,
+ id: conn.paybutton.id,
+ providerUserId: conn.paybutton.providerUserId
+ }
+ }
+ )
+ } else {
+ console.warn('Orphan address:', tx.address.address)
+ }
+ let invoices = null
+ if (tx.invoices.length > 0) {
+ invoices = tx.invoices.filter(invoice => {
+ return invoice !== null && invoice.userId === userId
+ })
+ }
+ return {
+ id: tx.id,
+ timestamp: tx.timestamp,
+ values,
+ amount: tx.amount,
+ networkId: tx.address.networkId,
+ hash: tx.hash,
+ buttonDisplayDataList,
+ address: tx.address.address,
+ invoices: invoices ?? []
+ }
+}
+
export const generateAndCacheGroupedPaymentsAndInfoForAddress = async (address: Address): Promise => {
let paymentList: Payment[] = []
let balance = new Prisma.Decimal(0)
diff --git a/redis/types.ts b/redis/types.ts
index a319a2be..4e5037ba 100644
--- a/redis/types.ts
+++ b/redis/types.ts
@@ -1,3 +1,4 @@
+import { Invoice } from '@prisma/client'
import { Decimal } from '@prisma/client/runtime/library'
import { QuoteValues } from 'services/priceService'
@@ -48,6 +49,7 @@ export interface ButtonDisplayData {
}
export interface Payment {
+ id?: string
timestamp: number
values: QuoteValues
amount?: Decimal
@@ -55,6 +57,7 @@ export interface Payment {
hash: string
buttonDisplayDataList: ButtonDisplayData[]
address?: string
+ invoices?: Invoice[]
}
export interface ButtonData {
diff --git a/services/invoiceService.ts b/services/invoiceService.ts
index 1ab5dfc1..f5e102fd 100644
--- a/services/invoiceService.ts
+++ b/services/invoiceService.ts
@@ -1,6 +1,6 @@
import { Decimal } from '@prisma/client/runtime/library'
import prisma from 'prisma/clientInstance'
-import { Invoice } from '@prisma/client'
+import { Invoice, Prisma } from '@prisma/client'
import { RESPONSE_MESSAGES } from 'constants/index'
export interface CreateInvoiceParams {
@@ -22,6 +22,23 @@ export interface UpdateInvoiceParams {
customerName: string
customerAddress: string
}
+const invoiceWithTransaction = Prisma.validator()({
+ include: {
+ transaction: {
+ select: {
+ address: {
+ select: {
+ networkId: true
+ }
+ },
+ timestamp: true,
+ hash: true
+ }
+ }
+ }
+})
+
+export type InvoiceWithTransaction = Prisma.InvoiceGetPayload
export async function createInvoice (params: CreateInvoiceParams): Promise {
return await prisma.invoice.create({
@@ -69,6 +86,7 @@ export async function getInvoiceById (invoiceId: string, userId: string): Promis
export async function updateInvoice (userId: string, invoiceId: string, params: UpdateInvoiceParams): Promise {
const invoice = await getInvoiceById(invoiceId, userId)
+
if (invoice === null) {
throw new Error(RESPONSE_MESSAGES.NO_INVOICE_FOUND_404.message)
}
diff --git a/services/transactionService.ts b/services/transactionService.ts
index f43d0a0f..57a03c06 100644
--- a/services/transactionService.ts
+++ b/services/transactionService.ts
@@ -7,7 +7,7 @@ import _ from 'lodash'
import { CacheSet } from 'redis/index'
import { SimplifiedTransaction } from 'ws-service/types'
import { OpReturnData, parseAddress } from 'utils/validators'
-import { generatePaymentFromTx } from 'redis/paymentCache'
+import { generatePaymentFromTxWithInvoices } from 'redis/paymentCache'
import { ButtonDisplayData, Payment } from 'redis/types'
export function getTransactionValue (transaction: TransactionWithPrices | TransactionsWithPaybuttonsAndPrices | SimplifiedTransaction): QuoteValues {
@@ -663,13 +663,32 @@ export async function getPaymentsByUserIdOrderedByButtonName (
'priceValue', pb.value,
'quoteId', pb.quoteId
)
- ) AS prices
+ ) AS prices,
+ JSON_ARRAYAGG(
+ IF(i.id IS NOT NULL,
+ JSON_OBJECT(
+ 'id', i.id,
+ 'invoiceNumber', i.invoiceNumber,
+ 'userId', i.userId,
+ 'amount', i.amount,
+ 'description', i.description,
+ 'recipientName', i.recipientName,
+ 'recipientAddress', i.recipientAddress,
+ 'customerName', i.customerName,
+ 'customerAddress', i.customerAddress,
+ 'createdAt', i.createdAt,
+ 'updatedAt', i.updatedAt
+ ),
+ NULL
+ )
+ ) AS invoices
FROM \`Transaction\` t
INNER JOIN \`Address\` a ON t.\`addressId\` = a.\`id\`
INNER JOIN \`AddressesOnButtons\` ab ON a.\`id\` = ab.\`addressId\`
INNER JOIN \`Paybutton\` p ON ab.\`paybuttonId\` = p.\`id\`
LEFT JOIN \`PricesOnTransactions\` pt ON t.\`id\` = pt.\`transactionId\`
LEFT JOIN \`Price\` pb ON pt.\`priceId\` = pb.\`id\`
+ LEFT JOIN \`Invoice\` i ON i.\`transactionId\` = t.\`id\`
WHERE t.\`amount\` > 0
AND EXISTS (
SELECT 1
@@ -706,14 +725,23 @@ export async function getPaymentsByUserIdOrderedByButtonName (
id: tx.paybuttonId,
providerUserId: tx.paybuttonProviderUserId
})
+ let invoices = null
+ if (JSON.parse(tx.invoices).length > 0) {
+ invoices = JSON.parse(tx.invoices).filter((invoice: any) => {
+ return invoice !== null && invoice.userId === userId
+ })
+ }
+
if (tx.amount > 0) {
payments.push({
+ id: tx.id,
amount: tx.amount,
timestamp: tx.timestamp,
values: ret,
networkId: tx.networkId,
hash: tx.hash,
- buttonDisplayDataList
+ buttonDisplayDataList,
+ invoices
})
}
})
@@ -783,7 +811,7 @@ export async function fetchAllPaymentsByUserIdWithPagination (
const transactions = await prisma.transaction.findMany({
where,
- include: includePaybuttonsAndPrices,
+ include: includePaybuttonsAndPricesAndInvoices,
orderBy: orderByQuery,
skip: page * Number(pageSize),
take: Number(pageSize)
@@ -793,7 +821,7 @@ export async function fetchAllPaymentsByUserIdWithPagination (
for (let index = 0; index < transactions.length; index++) {
const tx = transactions[index]
if (Number(tx.amount) > 0) {
- const payment = await generatePaymentFromTx(tx)
+ const payment = await generatePaymentFromTxWithInvoices(tx, userId)
transformedData.push(payment)
}
}
diff --git a/yarn.lock b/yarn.lock
index 0a9587ce..15bb3a8c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5420,11 +5420,6 @@ lru-cache@^7.14.1:
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.1.tgz"
integrity sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg==
-lucide-react@^0.510.0:
- version "0.510.0"
- resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.510.0.tgz#933b19d893b30ac5cb355e4b91eeefc13088caa3"
- integrity sha512-p8SQRAMVh7NhsAIETokSqDrc5CHnDLbV29mMnzaXx+Vc/hnqQzwI2r0FMWCcoTXnbw2KEjy48xwpGdEL+ck06Q==
-
luxon@^3.2.1:
version "3.2.1"
resolved "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz"
@@ -6268,6 +6263,11 @@ react-timezone-select@^3.2.8:
spacetime "^7.6.0"
timezone-soft "^1.5.2"
+react-to-print@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/react-to-print/-/react-to-print-3.1.0.tgz#70f2f3e43314ce9e71bb0a69fad47208e89c1320"
+ integrity sha512-hiJZVmJtaRm9EHoUTG2bordyeRxVSGy9oFVV7fSvzOWwctPp6jbz2R6NFkaokaTYBxC7wTM/fMV5eCXsNpEwsA==
+
react-transition-group@^4.3.0, react-transition-group@^4.4.0:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"