Skip to content

Commit

Permalink
Add orders report (#2578)
Browse files Browse the repository at this point in the history
* Add data table for orders

* Cleanup

* Fix nav decimals and add download button

* Fix date showing incorrect format

* Update nav per share format

* Fix nav index

* Fix format and add date header

* Fix sorting

* Remove date on top

* Fix warning and filter order only if it has date
  • Loading branch information
kattylucy authored Jan 20, 2025
1 parent 001f3df commit d902694
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 13 deletions.
37 changes: 24 additions & 13 deletions centrifuge-app/src/components/Report/DataFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function DataFilter({ poolId }: ReportFilterProps) {
{ label: 'Token price', value: 'token-price' },
{ label: 'Asset list', value: 'asset-list' },
{ label: 'Investor list', value: 'investor-list' },
{ label: 'Orders', value: 'orders' },
]

return (
Expand Down Expand Up @@ -228,19 +229,29 @@ export function DataFilter({ poolId }: ReportFilterProps) {
/>
)}

<DateInput label="From" value={startDate} max={endDate} onChange={(e) => setStartDate(e.target.value)} />
<DateInput label="To" value={endDate} min={startDate} onChange={(e) => setEndDate(e.target.value)} />
{report !== 'orders' && (
<>
<DateInput label="From" value={startDate} max={endDate} onChange={(e) => setStartDate(e.target.value)} />
<DateInput label="To" value={endDate} min={startDate} onChange={(e) => setEndDate(e.target.value)} />
</>
)}
</Grid>
<Box display="flex" alignItems="center" justifyContent="space-between" margin={2}>
<Box display="flex" alignItems="center">
<Text variant="body3" style={{ marginRight: 4, fontWeight: 600 }}>
{formatDate(startDate)}
</Text>
<Text variant="body3">-</Text>
<Text variant="body3" style={{ marginLeft: 4, fontWeight: 600 }}>
{formatDate(endDate)}
</Text>
</Box>
<Grid
gridTemplateColumns={[report === 'orders' ? '130px' : '1fr 130px']}
margin={2}
justifyContent={report === 'orders' ? 'end' : 'space-between'}
>
{report !== 'orders' && (
<Box display="flex" alignItems="center" justifyContent="flex-start">
<Text variant="body3" style={{ marginRight: 4, fontWeight: 600 }}>
{formatDate(startDate)}
</Text>
<Text variant="body3">-</Text>
<Text variant="body3" style={{ marginLeft: 4, fontWeight: 600 }}>
{formatDate(endDate)}
</Text>
</Box>
)}
<AnchorButton
disabled={!csvData}
download={csvData?.fileName}
Expand All @@ -251,7 +262,7 @@ export function DataFilter({ poolId }: ReportFilterProps) {
>
Download
</AnchorButton>
</Box>
</Grid>
</Stack>
)
}
Expand Down
151 changes: 151 additions & 0 deletions centrifuge-app/src/components/Report/Orders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
import { Box, Text } from '@centrifuge/fabric'
import { useContext, useEffect, useMemo } from 'react'
import { TableDataRow } from '.'
import { formatDateAndTime } from '../../../src/utils/date'
import { formatBalance } from '../../../src/utils/formatting'
import { getCSVDownloadUrl } from '../../../src/utils/getCSVDownloadUrl'
import { usePoolOrdersByPoolId } from '../../../src/utils/usePools'
import { DataTable, SortableTableHeader } from '../DataTable'
import { ReportContext } from './ReportContext'
import { convertCSV } from './utils'

const noop = (v: any) => v

const Orders = ({ pool }: { pool: Pool }) => {
const { setCsvData, setStartDate } = useContext(ReportContext)
const orders = usePoolOrdersByPoolId(pool.id)

useEffect(() => {
if (!orders?.length) return
const dateStrings = orders?.map((order) => order.closedAt).filter(Boolean)
const oldestTimestamp = Math.min(...dateStrings.map((date) => new Date(date).getTime()))
const oldestDate = new Date(oldestTimestamp).toISOString().split('T')[0]
setStartDate(oldestDate)
}, [setStartDate, orders])

const columnsConfig = [
{
align: 'left',
header: 'Epoch',
formatter: noop,
sortable: true,
},
{
align: 'left',
header: 'Date & Time',
sortable: true,
formatter: (v: any) => (v ? formatDateAndTime(v) : '-'),
width: '200px',
},
{
align: 'left',
header: 'NAV',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol) : '-'),
},
{
align: 'left',
header: 'Nav per share',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 6, 6) : '-'),
},
{
align: 'left',
header: 'Investments locked',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
{
align: 'left',
header: 'Investments executed',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
{
align: 'left',
header: 'Redemptions locked',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
{
align: 'left',
header: 'Redemptions executed',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
{
align: 'left',
header: 'Paid fees',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
]

const columns = columnsConfig.map((col, index) => ({
align: col.align,
header: col.sortable ? <SortableTableHeader label={col.header} /> : col.header,
cell: (row: TableDataRow) => {
return <Text variant="body3">{col.formatter((row.value as any)[index])}</Text>
},
sortKey: col.sortable ? `value[${index}]` : undefined,
width: col.width,
}))

const data = useMemo(() => {
if (!orders?.length) return []
else {
return orders.map((order) => {
const epoch = order.epochId.split('-')
return {
name: '',
value: [
epoch[1],
order.closedAt,
order.netAssetValue,
order.tokenPrice,
order.sumOutstandingInvestOrders,
order.sumFulfilledInvestOrders,
order.sumOutstandingRedeemOrders,
order.sumFulfilledRedeemOrders,
order.paidFees,
],
heading: false,
}
})
}
}, [orders])

const dataUrl = useMemo(() => {
if (!data.length) {
return
}

const formatted = data.map(({ value: values }) => convertCSV(values, columnsConfig))

return getCSVDownloadUrl(formatted)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])

useEffect(() => {
setCsvData(
dataUrl
? {
dataUrl,
fileName: `${pool.id}-orders.csv`,
}
: undefined
)

return () => setCsvData(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataUrl, pool.id])

return (
<Box paddingX={2}>
<DataTable data={data} columns={columns} scrollable defaultSortKey="value[1]" defaultSortOrder="desc" />
</Box>
)
}

export default Orders
1 change: 1 addition & 0 deletions centrifuge-app/src/components/Report/ReportContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type Report =
| 'balance-sheet'
| 'cash-flow-statement'
| 'profit-and-loss'
| 'orders'

export type ReportContextType = {
csvData?: CsvDataProps
Expand Down
2 changes: 2 additions & 0 deletions centrifuge-app/src/components/Report/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FeeTransactions } from './FeeTransactions'
import { InvestorList } from './InvestorList'
import { InvestorTransactions } from './InvestorTransactions'
import { OracleTransactions } from './OracleTransactions'
import Orders from './Orders'
import { PoolBalance } from './PoolBalance'
import { ProfitAndLoss } from './ProfitAndLoss'
import { ReportContext } from './ReportContext'
Expand Down Expand Up @@ -38,6 +39,7 @@ export function ReportComponent({ pool }: { pool: Pool }) {
{report === 'cash-flow-statement' && <CashflowStatement pool={pool} />}
{report === 'oracle-tx' && <OracleTransactions pool={pool} />}
{report === 'profit-and-loss' && <ProfitAndLoss pool={pool} />}
{report === 'orders' && <Orders pool={pool} />}
</Box>
</Box>
)
Expand Down
13 changes: 13 additions & 0 deletions centrifuge-app/src/utils/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ export function formatDate(timestamp: number | string | Date, options?: Intl.Dat
})
}

export function formatDateAndTime(timestamp: number | string | Date, options?: Intl.DateTimeFormatOptions) {
return new Date(timestamp).toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: 'UTC',
...options,
})
}

export function formatDateTechnical(timestamp: number | string) {
return new Date(timestamp).toLocaleDateString('en-US')
}
Expand Down
5 changes: 5 additions & 0 deletions centrifuge-app/src/utils/usePools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ export function usePoolOrders(poolId: string) {
return result
}

export function usePoolOrdersByPoolId(poolId: string) {
const [result] = useCentrifugeQuery(['poolOrdersByPoolId', poolId], (cent) => cent.pools.getPoolOrdersById([poolId]))
return result
}

export function useOrder(poolId: string, trancheId: string, address?: string) {
const [result] = useCentrifugeQuery(
['order', trancheId, address],
Expand Down
81 changes: 81 additions & 0 deletions centrifuge-js/src/modules/pools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
SubqueryPoolAssetSnapshot,
SubqueryPoolFeeSnapshot,
SubqueryPoolFeeTransaction,
SubqueryPoolOrdersById,
SubqueryPoolSnapshot,
SubqueryTrancheBalances,
SubqueryTrancheSnapshot,
Expand Down Expand Up @@ -3941,6 +3942,85 @@ export function getPoolsModule(inst: Centrifuge) {
)
}

function getPoolOrdersById(args: [poolId: string]) {
const [poolId] = args

const $query = inst.getSubqueryObservable<{
epoches: { nodes: SubqueryPoolOrdersById[] }
}>(
`query($poolId: String!) {
epoches(
filter: {
poolId: { equalTo: $poolId }
}
) {
nodes {
poolId
id
sumPoolFeesPaidAmount
closedAt
epochStates{
nodes{
tokenPrice
sumOutstandingInvestOrders
sumFulfilledInvestOrders
sumOutstandingRedeemOrders
sumFulfilledRedeemOrders
}
}
poolSnapshots{
nodes{
netAssetValue
}
}
}
}
}
`,
{
poolId,
},
false
)

return $query.pipe(
combineLatestWith(getPoolCurrency([poolId])),
map(([data, poolCurrency]) => {
return data?.epoches?.nodes
.map((order) => {
const index = order.epochStates.nodes.length > 1 ? order.epochStates.nodes.length - 1 : 0
const snapshotIndex = order.poolSnapshots.nodes.length > 1 ? order.poolSnapshots.nodes.length - 1 : 0
return {
epochId: order.id,
closedAt: order.closedAt,
paidFees: order.sumPoolFeesPaidAmount
? new CurrencyBalance(order.sumPoolFeesPaidAmount, poolCurrency.decimals)
: null,
tokenPrice: order.epochStates.nodes[index].tokenPrice
? new Price(order.epochStates.nodes[index].tokenPrice)
: null,
sumOutstandingInvestOrders: order.epochStates.nodes[index].sumOutstandingInvestOrders
? new CurrencyBalance(order.epochStates.nodes[index].sumOutstandingInvestOrders, poolCurrency.decimals)
: null,
sumFulfilledInvestOrders: order.epochStates.nodes[index].sumFulfilledInvestOrders
? new CurrencyBalance(order.epochStates.nodes[index].sumFulfilledInvestOrders, poolCurrency.decimals)
: null,
sumOutstandingRedeemOrders: order.epochStates.nodes[index].sumOutstandingRedeemOrders
? new CurrencyBalance(order.epochStates.nodes[index].sumOutstandingRedeemOrders, poolCurrency.decimals)
: null,
sumFulfilledRedeemOrders: order.epochStates.nodes[index].sumFulfilledRedeemOrders
? new CurrencyBalance(order.epochStates.nodes[index].sumFulfilledRedeemOrders, poolCurrency.decimals)
: null,
netAssetValue: order.poolSnapshots.nodes.length
? new CurrencyBalance(order.poolSnapshots.nodes[snapshotIndex].netAssetValue, poolCurrency.decimals)
: null,
}
})
.filter((order) => order.closedAt)
})
)
}

function getLoans(args: [poolId: string]) {
const [poolId] = args
const $api = inst.getApi()
Expand Down Expand Up @@ -4631,6 +4711,7 @@ export function getPoolsModule(inst: Centrifuge) {
getBalances,
getOrder,
getPoolOrders,
getPoolOrdersById,
getPoolAccountOrders,
getPortfolio,
getLoans,
Expand Down
21 changes: 21 additions & 0 deletions centrifuge-js/src/types/subquery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,27 @@ export type SubqueryPoolAssetSnapshot = {
totalRepaidUnscheduled: string | undefined
}

export type SubqueryPoolOrdersById = {
__typename?: 'Epoches'
id: string
sumPoolFeesPaidAmount: string
closedAt: string
epochStates: {
nodes: {
tokenPrice: string
sumOutstandingInvestOrders: string
sumFulfilledInvestOrders: string
sumOutstandingRedeemOrders: string
sumFulfilledRedeemOrders: string
}[]
}
poolSnapshots: {
nodes: {
netAssetValue: string
}[]
}
}

export type PoolFeeTransactionType = 'PROPOSED' | 'ADDED' | 'REMOVED' | 'CHARGED' | 'UNCHARGED' | 'PAID' | 'ACCRUED'

export type SubqueryPoolFeeTransaction = {
Expand Down

0 comments on commit d902694

Please sign in to comment.