Skip to content

Commit

Permalink
Merge pull request #299 from AmbireTech/admin-invoices
Browse files Browse the repository at this point in the history
Admin invoices
  • Loading branch information
ivopaunov authored Sep 30, 2024
2 parents 8edccb5 + 741bb77 commit d9d0f7b
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 124 deletions.
6 changes: 6 additions & 0 deletions src/components/AdminPanel/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useNavigate, useParams, Outlet } from 'react-router-dom'
import { AdminBadge } from 'components/common/AdminBadge'
import Dashboard from 'components/Dashboard'
import { StickyPanel } from 'components/TopBar/TopBarStickyPanel'
import Invoices from 'components/Billing/Invoices'
import AdminAnalytics from './AdminAnalytics'
import Accounts from './Accounts'
import { SspStats } from './SspStats'
Expand All @@ -26,6 +27,7 @@ const AdminPanel = () => {
>
<Tabs.List>
<Tabs.Tab value="campaigns">All Campaigns</Tabs.Tab>
<Tabs.Tab value="invoices">Invoices</Tabs.Tab>
<Tabs.Tab value="validatorAnalytics">Validator Analytics</Tabs.Tab>
<Tabs.Tab value="sspStats">SSP stats</Tabs.Tab>
<Tabs.Tab value="accounts">Accounts</Tabs.Tab>
Expand All @@ -39,6 +41,10 @@ const AdminPanel = () => {
<Dashboard isAdminPanel accountId={accountId} />
</Tabs.Panel>

<Tabs.Panel value="invoices" pt="xs">
<Invoices forAdmin />
</Tabs.Panel>

<Tabs.Panel value="validatorAnalytics" pt="xs">
<AdminAnalytics />
</Tabs.Panel>
Expand Down
8 changes: 7 additions & 1 deletion src/components/Billing/AccountStatements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,13 @@ const Statements = () => {

return (
<>
<BillingDetailsModal title="Statement" loading={!statement} opened={opened} close={close}>
<BillingDetailsModal
title="Statement"
documentTitle={`adex-statement-${statement.periodIndex}-${statement.token}`}
loading={!statement}
opened={opened}
close={close}
>
{statement && (
<StatementsPDF
statement={statement}
Expand Down
58 changes: 49 additions & 9 deletions src/components/Billing/BillingDetailsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PropsWithChildren } from 'react'
import { PropsWithChildren, useEffect, useCallback, useMemo } from 'react'
import {
Button,
Center,
Expand All @@ -16,9 +16,11 @@ import { useColorScheme } from '@mantine/hooks'

type DetailsProps = PropsWithChildren & {
title: string
documentTitle: string
loading: boolean
opened: boolean
close: () => void
closeAfterPrint?: boolean
}

const useStyles = createStyles((theme: MantineTheme) => {
Expand Down Expand Up @@ -58,21 +60,59 @@ const useStyles = createStyles((theme: MantineTheme) => {
}
})

export const BillingDetailsModal = ({ children, loading, title, opened, close }: DetailsProps) => {
export const BillingDetailsModal = ({
children,
loading,
title,
documentTitle,
opened,
close,
closeAfterPrint
}: DetailsProps) => {
const { classes } = useStyles()
const originalTitle = useMemo(() => {
return window.document.title
}, [])

useEffect(() => {
window.addEventListener('beforeprint', () => {
window.document.title = documentTitle
})

window.addEventListener('afterprint', () => {
window.document.title = originalTitle

if (closeAfterPrint) {
close()
}
})

return () => {
window.removeEventListener('beforeprint', () => {
window.document.title = documentTitle
})

window.removeEventListener('afterprint', () => {
window.document.title = originalTitle

if (closeAfterPrint) {
close()
}
})
}
}, [close, closeAfterPrint, documentTitle, originalTitle])

const print = useCallback(() => {
window.print()
}, [])

return (
<>
<Modal
className={classes.printableModal}
title={
<Group>
<Button
mt="md"
mb="md"
onClick={async () => {
window.print()
}}
>
<Button mt="md" mb="md" onClick={print}>
Print
</Button>
{title}
Expand Down
192 changes: 170 additions & 22 deletions src/components/Billing/Invoices.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,122 @@
import CustomTable from 'components/common/CustomTable'
import CustomTable, { DataElement } from 'components/common/CustomTable'
import { useDisclosure } from '@mantine/hooks'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { CampaignStatus } from 'adex-common'
import { Campaign, CampaignStatus } from 'adex-common'
import { useCampaignsData } from 'hooks/useCampaignsData'
import useAccount from 'hooks/useAccount'
import { formatDateShort } from 'helpers'
import { formatDateShort, parseBigNumTokenAmountToDecimal, timeout } from 'helpers'
import VisibilityIcon from 'resources/icons/Visibility'
import useAdmin from 'hooks/useAdmin'
import { Account } from 'types'
import { MonthPickerInput } from '@mantine/dates'
import dayjs from 'dayjs'
import { Stack, Text, Group, ThemeIcon } from '@mantine/core'
import DownloadIcon from 'resources/icons/Download'
import CalendarIcon from 'resources/icons/Calendar'
import { InvoicesModal } from './InvoicesModal'

const columnTitles = ['Company Name', 'Campaign', 'Campaign Period']
const columnTitles = ['Company Name', 'Campaign', 'Date', 'Campaign Period']
const minDate = new Date('2024-04-01')

const Invoices = () => {
const getInvoiceDate = (account?: Account, campaign?: Campaign) => {
const to = Number(campaign?.activeTo || 0)
const campaignCloseData = account?.refundsFromCampaigns.perCampaign.find(
(item) => item.id === campaign?.id
)

const end = new Date(campaignCloseData?.closeDate || to).getTime()

const invoiceDate = Math.min(end, to)

return invoiceDate
}

const Invoices = ({ forAdmin }: { forAdmin?: boolean }) => {
const [opened, { open, close }] = useDisclosure(false)
const { campaignsData, updateAllCampaignsData, initialDataLoading } = useCampaignsData()
const { campaignsData, updateAllCampaignsData } = useCampaignsData()
const campaigns = useMemo(() => Array.from(campaignsData.values()), [campaignsData])
const {
adexAccount: {
billingDetails: { companyName }
}
} = useAccount()
const { accounts, initialDataLoading, getAllAccounts } = useAdmin()
const { adexAccount } = useAccount()
const [months, setMonths] = useState<Date[]>([])
const [closeAfterPrint, setCloseAfterPrint] = useState<boolean>(false)
const now = new Date()

const [selectedCampaignId, setSelectedCampaignId] = useState('')

const invoiceElements = useMemo(
const campaignData = useMemo(
() => campaignsData.get(selectedCampaignId),

[selectedCampaignId, campaignsData]
)

const selectedCampaign = useMemo(() => campaignData?.campaign, [campaignData?.campaign])

const account = useMemo(
() => (forAdmin ? accounts.get(selectedCampaign?.owner || '') || adexAccount : adexAccount),
[accounts, adexAccount, selectedCampaign?.owner, forAdmin]
)

const invoiceData = useMemo(() => {
const campaignOpenData = account.fundsOnCampaigns.perCampaign.find(
(item) => item.id === selectedCampaign?.id
)
const campaignCloseData = account.refundsFromCampaigns.perCampaign.find(
(item) => item.id === selectedCampaign?.id
)
const currencyName = campaignOpenData?.token.name || ''
const decimals = campaignOpenData?.token.decimals || 6

// NOTE: the actual payment is when the campaign is started (openings -> start dates, activeFrom is fallback)
// The question is: Is that ok from accounting stand point
// TODO: Should we have invoices for the full amount and credit notes for the refunds (on stop or expire with no full budget used)
const start = new Date(
campaignOpenData?.startDate || Number(selectedCampaign?.activeFrom) || 0
).getTime()

// TODO: discuss the payment and invoice date

const amount = parseBigNumTokenAmountToDecimal(
BigInt(campaignOpenData?.amount || selectedCampaign?.campaignBudget || 0) -
BigInt(campaignCloseData?.amount || 0),
decimals
)
return {
invoiceDate: getInvoiceDate(account, selectedCampaign),
paymentDate: start,
amount,
currencyName
}
}, [account, selectedCampaign])

const invoiceElements: DataElement[] = useMemo(
() =>
campaigns
.filter(
(c) =>
.filter((c) => {
return (
[
CampaignStatus.expired,
CampaignStatus.closedByUser,
CampaignStatus.exhausted
].includes(c.campaign.status) && c.paid > 0
)
)
})
.sort((a, b) => Number(b.campaign.activeFrom) - Number(a.campaign.activeFrom))
.map((campaign) => {
const accountData = forAdmin ? accounts.get(campaign.campaign.owner) : adexAccount
const invoiceDate = getInvoiceDate(accountData, campaign.campaign)
const verifiedAccount = accountData?.billingDetails?.verified
return {
id: campaign.campaignId,
rowColor: verifiedAccount ? undefined : 'error',
columns: [
{ value: companyName },
{
value: `${!verifiedAccount ? '* ' : ''}${accountData?.billingDetails?.companyName}`
},
{ value: campaign.campaign.title },
{
value: invoiceDate,
element: formatDateShort(new Date(invoiceDate))
},
{
value: campaign.campaign.activeFrom,
element: (
Expand All @@ -52,35 +129,99 @@ const Invoices = () => {
}
]
}
})
.filter((c) => {
if (!months.length) {
return true
}
const monthRanges = months.map((x) => {
const start = dayjs(x).unix() * 1000
const end = dayjs(x).add(1, 'month').unix() * 1000

return { start, end }
})

return monthRanges.some(
(x: { start: number; end: number }) =>
x.start <= Number(c.columns[2].value) && x.end >= Number(c.columns[2].value)
)
}),
[campaigns, companyName]
[accounts, adexAccount, campaigns, forAdmin, months]
)

useEffect(() => {
updateAllCampaignsData(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

useEffect(() => {
getAllAccounts()
}, [getAllAccounts])

const handlePreview = useCallback(
(item: { id: string }) => {
setSelectedCampaignId(item.id)
setCloseAfterPrint(false)
open()
},
[open]
)

const handlePreviewAndPrint = useCallback(
async (item: { id: string }) => {
setSelectedCampaignId(item.id)
setCloseAfterPrint(true)
open()
await timeout(69)
window.print()
},
[open]
)

const actions = useMemo(() => {
return [
const tableActions = [
{
action: handlePreview,
label: 'Show campaign details',
icon: <VisibilityIcon />
}
]
}, [handlePreview])

if (forAdmin) {
tableActions.push({
action: handlePreviewAndPrint,
label: 'Print and download',
icon: <DownloadIcon />
})
}

return tableActions
}, [forAdmin, handlePreview, handlePreviewAndPrint])

return (
<>
<Stack>
{forAdmin && (
<Group gap="xs">
<MonthPickerInput
leftSection={
<ThemeIcon size="sm" variant="transparent">
<CalendarIcon />
</ThemeIcon>
}
clearable
placeholder="Pick month/s"
type="multiple"
value={months}
onChange={setMonths}
maxDate={now}
minDate={minDate}
/>
<Text size="sm" c="error" inline>
* rows in red can not be downloaded by users (Do not send to accounting until not red) -
their data is not confirmed
</Text>
</Group>
)}
<CustomTable
headings={columnTitles}
data={invoiceElements}
Expand All @@ -89,8 +230,15 @@ const Invoices = () => {
shadow="xs"
loading={initialDataLoading}
/>
<InvoicesModal campaignId={selectedCampaignId} opened={opened} close={close} />
</>
<InvoicesModal
campaignData={campaignData}
opened={opened}
invoiceData={invoiceData}
account={account}
close={close}
closeAfterPrint={closeAfterPrint}
/>
</Stack>
)
}

Expand Down
Loading

0 comments on commit d9d0f7b

Please sign in to comment.