From f7969b67018bdf1f3b1652a377d7e31c7939aada Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 23 Oct 2024 14:40:33 +0200 Subject: [PATCH] Add changes to asset detail page --- .../Charts/AssetPerformanceChart.tsx | 183 ++++++++---------- .../src/components/LayoutBase/BasePadding.tsx | 2 +- centrifuge-app/src/components/LoanList.tsx | 4 +- centrifuge-app/src/components/PoolList.tsx | 2 +- .../PoolOverview/TransactionHistory.tsx | 4 +- .../src/pages/Loan/HoldingsValues.tsx | 6 +- centrifuge-app/src/pages/Loan/KeyMetrics.tsx | 6 +- .../src/pages/Loan/MetricsTable.tsx | 12 +- .../src/pages/Loan/PricingValues.tsx | 12 +- .../src/pages/Loan/TransactionTable.tsx | 21 +- centrifuge-app/src/pages/Loan/index.tsx | 125 ++++++------ .../src/pages/Pool/Assets/index.tsx | 5 +- centrifuge-js/src/modules/pools.ts | 1 + fabric/src/components/Card/index.ts | 6 +- fabric/src/theme/tokens/theme.ts | 2 +- 15 files changed, 182 insertions(+), 209 deletions(-) diff --git a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx index 85cf10f542..6bc29f5e73 100644 --- a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx @@ -1,11 +1,11 @@ import { CurrencyBalance, Pool } from '@centrifuge/centrifuge-js' -import { AnchorButton, Box, Card, IconDownload, Shelf, Spinner, Stack, Text } from '@centrifuge/fabric' +import { AnchorButton, Box, Card, IconDownload, Shelf, Spinner, Stack, Tabs, TabsItem, Text } from '@centrifuge/fabric' import * as React from 'react' import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' -import styled, { useTheme } from 'styled-components' +import { useTheme } from 'styled-components' +import { getCSVDownloadUrl } from '../../../src/utils/getCSVDownloadUrl' import { formatDate } from '../../utils/date' import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' -import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { TinlakePool } from '../../utils/tinlake/useTinlakePools' import { useLoan } from '../../utils/useLoans' import { useAssetSnapshots } from '../../utils/usePools' @@ -25,50 +25,13 @@ interface Props { loanId: string } -const FilterButton = styled(Stack)` - &:hover { - cursor: pointer; - } -` - -const filterOptions = [ - { value: 'price', label: 'Price' }, - { value: 'value', label: 'Asset value' }, -] as const - function AssetPerformanceChart({ pool, poolId, loanId }: Props) { const theme = useTheme() const chartColor = theme.colors.accentPrimary const asset = useLoan(poolId, loanId) const assetSnapshots = useAssetSnapshots(poolId, loanId) - const [activeFilter, setActiveFilter] = React.useState<(typeof filterOptions)[number]>(filterOptions[0]) - - React.useEffect(() => { - if (assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') { - setActiveFilter(filterOptions[1]) - } - }, [assetSnapshots]) - - const dataUrl: any = React.useMemo(() => { - if (!assetSnapshots || !assetSnapshots?.length) { - return undefined - } - - const formatted = assetSnapshots.map((assetObject: Record) => { - const keys = Object.keys(assetObject) - const newObj: Record = {} - - keys.forEach((assetKey) => { - newObj[assetKey] = - assetObject[assetKey] instanceof CurrencyBalance ? assetObject[assetKey].toFloat() : assetObject[assetKey] - }) - - return newObj - }) - - return getCSVDownloadUrl(formatted as any) - }, [assetSnapshots]) + const [selectedTabIndex, setSelectedTabIndex] = React.useState(0) const data: ChartData[] = React.useMemo(() => { if (!asset || !assetSnapshots) return [] @@ -157,58 +120,76 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { [data, assetSnapshots] ) + const dataUrl: any = React.useMemo(() => { + if (!assetSnapshots || !assetSnapshots?.length) { + return undefined + } + + const formatted = assetSnapshots.map((assetObject: Record) => { + const keys = Object.keys(assetObject) + const newObj: Record = {} + + keys.forEach((assetKey) => { + newObj[assetKey] = + assetObject[assetKey] instanceof CurrencyBalance ? assetObject[assetKey].toFloat() : assetObject[assetKey] + }) + + return newObj + }) + + return getCSVDownloadUrl(formatted as any) + }, [assetSnapshots]) + if (!assetSnapshots) return return ( - + - - - {asset && 'valuationMethod' in asset.pricing && asset?.pricing.valuationMethod !== 'cash' - ? 'Asset performance' - : 'Cash balance'} - - {!isChartEmpty && ( - - Download - + + + + {asset && 'valuationMethod' in asset.pricing && asset?.pricing.valuationMethod !== 'cash' + ? 'Asset performance' + : 'Cash balance'} + + + ({pool.currency.name ?? 'USD'}) + + + {!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && ( + + + {data.length > 0 && ( + setSelectedTabIndex(index)}> + + Price + + + Asset value + + + )} + + )} - - - {isChartEmpty && No data yet} - - {!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && ( - - - {data.length > 0 && - filterOptions.map((filter, index) => ( - - setActiveFilter(filter)}> - - {filter.label} - - - - {index !== filterOptions.length - 1 && ( - - )} - - ))} - - + + Download + + + + {isChartEmpty && ( + + No data available + )} - + {data?.length ? ( @@ -225,7 +206,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { tickFormatter={(tick: number) => { return new Date(tick).toLocaleString('en-US', { day: 'numeric', month: 'short' }) }} - style={{ fontSize: 8, fill: theme.colors.textSecondary, letterSpacing: '-0.7px' }} + style={{ fontSize: 8, fill: theme.colors.textPrimary, letterSpacing: '-0.7px' }} dy={4} interval={10} angle={-40} @@ -234,9 +215,9 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { formatBalanceAbbreviated(tick, '', 2)} - domain={activeFilter.value === 'price' ? priceRange : [0, 'auto']} + domain={selectedTabIndex === 0 ? priceRange : [0, 'auto']} width={90} /> @@ -249,22 +230,22 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { {payload.map(({ value }, index) => ( <> - {'Value'} - + {'Value'} + {payload[0].payload.historicPV - ? formatBalance(payload[0].payload.historicPV, 'USD', 2) + ? formatBalance(payload[0].payload.historicPV, pool.currency.name, 2) : payload[0].payload.futurePV - ? `~${formatBalance(payload[0].payload.futurePV, 'USD', 2)}` + ? `~${formatBalance(payload[0].payload.futurePV, pool.currency.name, 2)}` : '-'} - {'Price'} - + Price + {payload[0].payload.historicPrice - ? formatBalance(payload[0].payload.historicPrice, 'USD', 6) + ? formatBalance(payload[0].payload.historicPrice, pool.currency.name, 6) : payload[0].payload.futurePrice - ? `~${formatBalance(payload[0].payload.futurePrice, 'USD', 6)}` + ? `~${formatBalance(payload[0].payload.futurePrice, pool.currency.name, 6)}` : '-'} @@ -277,7 +258,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { }} /> - {activeFilter.value === 'price' && ( + {selectedTabIndex === 0 && ( )} - {activeFilter.value === 'price' && ( + {selectedTabIndex === 0 && ( )} - {activeFilter.value === 'value' && ( + {selectedTabIndex === 1 && ( )} - {activeFilter.value === 'value' && ( + {selectedTabIndex === 1 && ( - + ( - + )) diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index 85f45698de..610632ca31 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -286,9 +286,7 @@ export const TransactionHistoryTable = ({ return ( - - Transaction history - + Transaction history {transactions?.length! > 8 && preview && ( diff --git a/centrifuge-app/src/pages/Loan/HoldingsValues.tsx b/centrifuge-app/src/pages/Loan/HoldingsValues.tsx index 8f9146f16a..d3ba229f84 100644 --- a/centrifuge-app/src/pages/Loan/HoldingsValues.tsx +++ b/centrifuge-app/src/pages/Loan/HoldingsValues.tsx @@ -74,11 +74,9 @@ export function HoldingsValues({ pool, transactions, currentFace, pricing }: Pro ] return ( - + - - Holdings - + Holdings diff --git a/centrifuge-app/src/pages/Loan/KeyMetrics.tsx b/centrifuge-app/src/pages/Loan/KeyMetrics.tsx index 3d5bb53459..ef9838db29 100644 --- a/centrifuge-app/src/pages/Loan/KeyMetrics.tsx +++ b/centrifuge-app/src/pages/Loan/KeyMetrics.tsx @@ -155,11 +155,9 @@ export function KeyMetrics({ pool, loan }: Props) { ] return ( - + - - Key metrics - + Key metrics diff --git a/centrifuge-app/src/pages/Loan/MetricsTable.tsx b/centrifuge-app/src/pages/Loan/MetricsTable.tsx index 431ebc3425..dedc335393 100644 --- a/centrifuge-app/src/pages/Loan/MetricsTable.tsx +++ b/centrifuge-app/src/pages/Loan/MetricsTable.tsx @@ -12,7 +12,7 @@ type Props = { export function MetricsTable({ metrics }: Props) { return ( - + {metrics.map(({ label, value }, index) => { const multirow = value && value.length > 20 const asLink = value && /^(https?:\/\/[^\s]+)$/.test(value) @@ -33,25 +33,21 @@ export function MetricsTable({ metrics }: Props) { } : {} - const combinedStyle: React.CSSProperties = { ...defaultStyle, ...multiRowStyle } + const combinedStyle: React.CSSProperties = { ...defaultStyle, ...multiRowStyle, textAlign: 'right' } return ( - + {label} - + {asLink ? {value} : value} diff --git a/centrifuge-app/src/pages/Loan/PricingValues.tsx b/centrifuge-app/src/pages/Loan/PricingValues.tsx index 4d095fa314..9ac7e84218 100644 --- a/centrifuge-app/src/pages/Loan/PricingValues.tsx +++ b/centrifuge-app/src/pages/Loan/PricingValues.tsx @@ -59,11 +59,9 @@ export function PricingValues({ loan, pool }: Props) { const accruedPrice = 'currentPrice' in loan && loan.currentPrice return ( - + - - Pricing - + Pricing , + label: , value: pricing.withLinearPricing ? 'Enabled' : 'Disabled', }, ...(loan.status === 'Active' && loan.outstandingDebt.toDecimal().lte(0) @@ -102,9 +100,7 @@ export function PricingValues({ loan, pool }: Props) { return ( - - Pricing - + Pricing { const assetTransactions = useMemo(() => { const sortedTransactions = transactions?.sort((a, b) => { @@ -77,6 +80,8 @@ export const TransactionTable = ({ .mul((pricing as ExternalPricingInfo).notional.toDecimal()) : null + console.log(isLoanClosed, transaction) + return { type: transaction.type, amount: transaction.amount, @@ -97,7 +102,7 @@ export const TransactionTable = ({ position: array.slice(0, index + 1).reduce((sum, trx) => { if (trx.type === 'BORROWED') { sum = sum.add( - trx.quantity + trx.quantity && (pricing as ExternalPricingInfo)?.notional ? new CurrencyBalance(trx.quantity, 18) .toDecimal() .mul((pricing as ExternalPricingInfo).notional.toDecimal()) @@ -108,7 +113,7 @@ export const TransactionTable = ({ } if (trx.type === 'REPAID') { sum = sum.sub( - trx.quantity + trx.quantity && (pricing as ExternalPricingInfo)?.notional ? new CurrencyBalance(trx.quantity, 18) .toDecimal() .mul((pricing as ExternalPricingInfo).notional.toDecimal()) @@ -120,6 +125,7 @@ export const TransactionTable = ({ return sum }, Dec(0)), realizedProfitFifo: transaction.realizedProfitFifo, + unrealizedProfitAtMarketPrice: transaction.unrealizedProfitAtMarketPrice, } }) }, [transactions, maturityDate, pricing, decimals]) @@ -214,10 +220,15 @@ export const TransactionTable = ({ }, { align: 'left', - header: `Realized P&L`, + header: isLoanClosed ? 'Realized P&L' : 'Unrealized P&L', cell: (row: Row) => - row.realizedProfitFifo - ? `${row.type !== 'REPAID' ? '-' : ''}${formatBalance(row.realizedProfitFifo, undefined, 2, 2)}` + row.realizedProfitFifo || row.unrealizedProfitAtMarketPrice + ? formatBalance( + isLoanClosed ? row.unrealizedProfitAtMarketPrice ?? 0 : row.realizedProfitFifo ?? 0, + undefined, + 2, + 2 + ) : '-', }, ]), diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 39454bb592..9ec2e0eb1a 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -14,9 +14,9 @@ import { } from '@centrifuge/fabric' import * as React from 'react' import { useParams } from 'react-router' -import styled, { useTheme } from 'styled-components' +import styled from 'styled-components' +import { AssetSummary } from '../../../src/components/AssetSummary' import { LoanLabel } from '../../../src/components/LoanLabel' -import { AssetSummary } from '../../components/AssetSummary' import AssetPerformanceChart from '../../components/Charts/AssetPerformanceChart' import { LabelValueStack } from '../../components/LabelValueStack' import { LayoutSection } from '../../components/LayoutBase/LayoutSection' @@ -60,13 +60,14 @@ function isTinlakeLoan(loan: LoanType | TinlakeLoan): loan is TinlakeLoan { } function ActionButtons({ loan }: { loan: LoanType }) { + if (!loan) return const canBorrow = useCanBorrowAsset(loan.poolId, loan.id) const [financeShown, setFinanceShown] = React.useState(false) const [repayShown, setRepayShown] = React.useState(false) const [correctionShown, setCorrectionShown] = React.useState(false) if (!loan || !canBorrow || isTinlakeLoan(loan) || !canBorrow || loan.status === 'Closed') return null return ( - <> + setFinanceShown(false)} innerPaddingTop={2}> @@ -90,27 +91,26 @@ function ActionButtons({ loan }: { loan: LoanType }) { {!(loan.pricing.maturityDate && new Date() > new Date(loan.pricing.maturityDate)) || !loan.pricing.maturityDate ? ( - ) : null} {loan.outstandingDebt.gtn(0) && ( - )} {loan.outstandingDebt.gtn(0) && ( - )} - + ) } function Loan() { - const theme = useTheme() const { pid: poolId, aid: loanId } = useParams<{ pid: string; aid: string }>() if (!poolId || !loanId) throw new Error('Loan no found') const basePath = useBasePath() @@ -122,6 +122,7 @@ function Loan() { const { data: nftMetadata, isLoading: nftMetadataIsLoading } = useMetadata(nft?.metadataUri, nftMetadataSchema) const metadataIsLoading = poolMetadataIsLoading || nftMetadataIsLoading const borrowerAssetTransactions = useBorrowerAssetTransactions(`${poolId}`, `${loanId}`) + const isOracle = loan && 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' const currentFace = loan?.pricing && 'outstandingQuantity' in loan.pricing @@ -147,6 +148,13 @@ function Loan() { const originationDate = loan && 'originationDate' in loan ? new Date(loan?.originationDate).toISOString() : undefined + const getCurrentValue = () => { + if (loanId === '0') return pool.reserve.total + else return loan?.presentValue || 0 + } + + if (metadataIsLoading) return + return ( @@ -159,33 +167,34 @@ function Loan() { + + + + {loanId === '0' && ( - <> - + - - - - + )} {loan && pool && ( - - + + }> - {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && ( + {isOracle && ( }> @@ -198,7 +207,7 @@ function Loan() { gridAutoRows="minContent" gap={[2, 2, 2]} > - {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && ( + {isOracle && ( }> }> - + - - {section.name} - + {section.name} {borrowerAssetTransactions?.length ? ( - 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash' ? ( - - - + 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash' ? ( + ) : ( - - - - Transaction history - - - - - + + Transaction history + + ) ) : null} diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index c5f08a57d6..deacc444f1 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -48,9 +48,6 @@ export function PoolDetailAssets() { const cashLoans = (loans ?? []).filter( (loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash' ) - const nonCashLoans = (loans ?? []).filter( - (loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash' - ) if (!pool) return null @@ -100,7 +97,7 @@ export function PoolDetailAssets() { ), heading: false, }, - ...(!isTinlakePool + ...(!isTinlakePool && cashLoans.length ? [ { label: , diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 0ed530b3a9..064e0fe4cb 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -849,6 +849,7 @@ export type AssetTransaction = { interestAmount: CurrencyBalance | undefined hash: string realizedProfitFifo: CurrencyBalance | undefined + unrealizedProfitAtMarketPrice: CurrencyBalance | undefined asset: { id: string metadata: string diff --git a/fabric/src/components/Card/index.ts b/fabric/src/components/Card/index.ts index 52f34e9528..79e5894cdf 100644 --- a/fabric/src/components/Card/index.ts +++ b/fabric/src/components/Card/index.ts @@ -3,7 +3,7 @@ import styled from 'styled-components' import { Box, BoxProps } from '../Box' type Props = { - variant?: 'default' | 'interactive' | 'overlay' + variant?: 'default' | 'interactive' | 'overlay' | 'secondary' backgroundColor?: string } @@ -14,9 +14,9 @@ export const Card = styled(Box)(({ variant = 'default', backgroundColor } css({ bg: backgroundColor ?? 'white', borderRadius: 'card', - borderWidth: variant === 'default' && !backgroundColor ? 1 : 0, + borderWidth: variant === 'default' || (variant === 'secondary' && !backgroundColor) ? 1 : 0, borderStyle: 'solid', - borderColor: 'borderPrimary', + borderColor: variant === 'secondary' ? 'borderSecondary' : 'borderPrimary', boxShadow: variant === 'interactive' ? 'cardInteractive' : variant === 'overlay' ? 'cardOverlay' : undefined, transition: 'box-shadow 100ms ease', diff --git a/fabric/src/theme/tokens/theme.ts b/fabric/src/theme/tokens/theme.ts index f98a440659..68381867ce 100644 --- a/fabric/src/theme/tokens/theme.ts +++ b/fabric/src/theme/tokens/theme.ts @@ -32,7 +32,7 @@ const colors = { backgroundInverted: grayScale[800], borderPrimary: grayScale[50], - borderSecondary: 'rgba(207, 207, 207, 0.50)', + borderSecondary: grayScale[100], statusDefault, statusInfo,