Skip to content

Commit 0ef0317

Browse files
kevcodezloong
andcommitted
feat: invoice status and ability to pay
Co-Authored-By: Long Hoang <1732217+loong@users.noreply.github.com>
1 parent 35b1edd commit 0ef0317

File tree

4 files changed

+148
-8
lines changed

4 files changed

+148
-8
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { FC } from 'react'
2+
import * as Tooltip from '@radix-ui/react-tooltip'
3+
import { Badge } from 'ui'
4+
import { InvoiceStatus } from './Invoices.types'
5+
6+
interface Props {
7+
status: InvoiceStatus
8+
}
9+
10+
export const invoiceStatusMapping: Record<InvoiceStatus, { label: string; badgeColor: string }> = {
11+
[InvoiceStatus.DRAFT]: {
12+
label: 'Upcoming',
13+
badgeColor: 'yellow',
14+
},
15+
[InvoiceStatus.PAID]: {
16+
label: 'Paid',
17+
badgeColor: 'green',
18+
},
19+
[InvoiceStatus.VOID]: {
20+
label: 'Forgiven',
21+
badgeColor: 'green',
22+
},
23+
24+
// We do not want to overcomplicate it for the user, so we'll treat uncollectible/open the same from a user perspective
25+
// it's an outstanding invoice
26+
[InvoiceStatus.UNCOLLECTIBLE]: {
27+
label: 'Outstanding',
28+
badgeColor: 'red',
29+
},
30+
[InvoiceStatus.OPEN]: {
31+
label: 'Outstanding',
32+
badgeColor: 'red',
33+
},
34+
}
35+
36+
const InvoiceStatusBadge: FC<Props> = ({ status }) => {
37+
const statusMapping = invoiceStatusMapping[status]
38+
39+
return (
40+
<Tooltip.Root delayDuration={0}>
41+
<Tooltip.Trigger>
42+
<Badge
43+
size="small"
44+
className="capitalize"
45+
// @ts-ignore
46+
color={statusMapping?.badgeColor || 'gray'}
47+
>
48+
{statusMapping?.label || status}
49+
</Badge>
50+
</Tooltip.Trigger>
51+
<Tooltip.Content side="bottom">
52+
<Tooltip.Arrow className="radix-tooltip-arrow" />
53+
<div
54+
className={[
55+
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
56+
'w-[300px] space-y-2 border border-scale-200',
57+
].join(' ')}
58+
>
59+
{[InvoiceStatus.OPEN, InvoiceStatus.UNCOLLECTIBLE].includes(status) && (
60+
<p className="text-xs text-scale-1200">
61+
We were not able to collect the money. Make sure you have a valid payment method and
62+
enough funds. Outstanding invoices may cause restrictions. You can manually pay the
63+
using the "Pay Now" button.
64+
</p>
65+
)}
66+
67+
{status === InvoiceStatus.DRAFT && (
68+
<p className="text-xs text-scale-1200">
69+
The invoice will soon be finalized and charged for.
70+
</p>
71+
)}
72+
73+
{status === InvoiceStatus.PAID && (
74+
<p className="text-xs text-scale-1200">
75+
The invoice has been paid successfully. No action is required on your side.
76+
</p>
77+
)}
78+
79+
{status === InvoiceStatus.VOID && (
80+
<p className="text-xs text-scale-1200">
81+
This invoice has been forgiven. No action is required on your side.
82+
</p>
83+
)}
84+
</div>
85+
</Tooltip.Content>
86+
</Tooltip.Root>
87+
)
88+
}
89+
90+
export default InvoiceStatusBadge

studio/components/interfaces/Billing/Invoices.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { FC, useState, useEffect } from 'react'
22
import { Button, Loading, IconFileText, IconDownload, IconChevronLeft, IconChevronRight } from 'ui'
3+
import Link from 'next/link'
34

45
import { useStore } from 'hooks'
56
import { API_URL } from 'lib/constants'
67
import { get, head } from 'lib/common/fetch'
78
import Table from 'components/to-be-cleaned/Table'
89

10+
import InvoiceStatusBadge from './InvoiceStatusBadge'
11+
import { Invoice, InvoiceStatus } from './Invoices.types'
12+
913
const PAGE_LIMIT = 10
1014

1115
interface Props {
@@ -18,7 +22,7 @@ const Invoices: FC<Props> = ({ projectRef }) => {
1822

1923
const [page, setPage] = useState(1)
2024
const [count, setCount] = useState(0)
21-
const [invoices, setInvoices] = useState<any>([])
25+
const [invoices, setInvoices] = useState<Invoice[]>([])
2226

2327
const offset = (page - 1) * PAGE_LIMIT
2428

@@ -85,6 +89,9 @@ const Invoices: FC<Props> = ({ projectRef }) => {
8589
<Table.th key="header-date">Date</Table.th>,
8690
<Table.th key="header-amount">Amount due</Table.th>,
8791
<Table.th key="header-invoice">Invoice number</Table.th>,
92+
<Table.th key="header-invoice" className="flex items-center">
93+
Status
94+
</Table.th>,
8895
<Table.th key="header-download" className="text-right"></Table.th>,
8996
]}
9097
body={
@@ -98,7 +105,7 @@ const Invoices: FC<Props> = ({ projectRef }) => {
98105
</Table.tr>
99106
) : (
100107
<>
101-
{invoices.map((x: any) => {
108+
{invoices.map((x) => {
102109
return (
103110
<Table.tr key={x.id}>
104111
<Table.td>
@@ -117,11 +124,22 @@ const Invoices: FC<Props> = ({ projectRef }) => {
117124
<Table.td>
118125
<p>{x.number}</p>
119126
</Table.td>
127+
<Table.td>
128+
<InvoiceStatusBadge status={x.status} />
129+
</Table.td>
120130
<Table.td className="align-right">
121131
<div className="flex items-center justify-end space-x-2">
132+
{[InvoiceStatus.UNCOLLECTIBLE, InvoiceStatus.OPEN].includes(x.status) && (
133+
<Link href={`https://redirect.revops.supabase.com/pay-invoice/${x.id}`}>
134+
<a target="_blank">
135+
<Button>Pay Now</Button>
136+
</a>
137+
</Link>
138+
)}
139+
122140
<Button
123141
type="outline"
124-
icon={<IconDownload />}
142+
icon={<IconDownload size={16} strokeWidth={1.5} />}
125143
onClick={() => fetchInvoice(x.id)}
126144
/>
127145
</div>
@@ -130,7 +148,7 @@ const Invoices: FC<Props> = ({ projectRef }) => {
130148
)
131149
})}
132150
<Table.tr key="navigation">
133-
<Table.td colSpan={5}>
151+
<Table.td colSpan={6}>
134152
<div className="flex items-center justify-between">
135153
<p className="text-sm opacity-50">
136154
Showing {offset + 1} to {offset + invoices.length} out of {count} invoices
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export enum InvoiceStatus {
2+
DRAFT = 'draft',
3+
PAID = 'paid',
4+
VOID = 'void',
5+
UNCOLLECTIBLE = 'uncollectible',
6+
OPEN = 'open',
7+
}
8+
9+
export type Invoice = {
10+
id: string
11+
number: string
12+
period_end: number
13+
subtotal: number
14+
status: InvoiceStatus
15+
}

studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { useState, useEffect } from 'react'
22
import { Button, Loading, IconFileText, IconDownload, IconChevronLeft, IconChevronRight } from 'ui'
33
import { PermissionAction } from '@supabase/shared-types/out/constants'
4+
import Link from 'next/link'
45

56
import { checkPermissions, useStore } from 'hooks'
67
import { API_URL } from 'lib/constants'
78
import { get, head } from 'lib/common/fetch'
89
import Table from 'components/to-be-cleaned/Table'
910
import NoPermission from 'components/ui/NoPermission'
11+
import InvoiceStatusBadge from 'components/interfaces/Billing/InvoiceStatusBadge'
12+
import { Invoice, InvoiceStatus } from 'components/interfaces/Billing/Invoices.types'
1013

1114
const PAGE_LIMIT = 10
1215

@@ -18,7 +21,7 @@ const InvoicesSettings = () => {
1821

1922
const [page, setPage] = useState(1)
2023
const [count, setCount] = useState(0)
21-
const [invoices, setInvoices] = useState<any>([])
24+
const [invoices, setInvoices] = useState<Invoice[]>([])
2225

2326
const { stripe_customer_id } = ui.selectedOrganization ?? {}
2427
const offset = (page - 1) * PAGE_LIMIT
@@ -96,6 +99,9 @@ const InvoicesSettings = () => {
9699
<Table.th key="header-date">Date</Table.th>,
97100
<Table.th key="header-amount">Amount due</Table.th>,
98101
<Table.th key="header-invoice">Invoice number</Table.th>,
102+
<Table.th key="header-invoice" className="flex items-center">
103+
Status
104+
</Table.th>,
99105
<Table.th key="header-download" className="text-right"></Table.th>,
100106
]}
101107
body={
@@ -109,7 +115,7 @@ const InvoicesSettings = () => {
109115
</Table.tr>
110116
) : (
111117
<>
112-
{invoices.map((x: any) => {
118+
{invoices.map((x) => {
113119
return (
114120
<Table.tr key={x.id}>
115121
<Table.td>
@@ -124,11 +130,22 @@ const InvoicesSettings = () => {
124130
<Table.td>
125131
<p>{x.number}</p>
126132
</Table.td>
133+
<Table.td>
134+
<InvoiceStatusBadge status={x.status} />
135+
</Table.td>
127136
<Table.td className="align-right">
128137
<div className="flex items-center justify-end space-x-2">
138+
{[InvoiceStatus.UNCOLLECTIBLE, InvoiceStatus.OPEN].includes(x.status) && (
139+
<Link href={`https://redirect.revops.supabase.com/pay-invoice/${x.id}`}>
140+
<a target="_blank">
141+
<Button>Pay Now</Button>
142+
</a>
143+
</Link>
144+
)}
145+
129146
<Button
130147
type="outline"
131-
icon={<IconDownload />}
148+
icon={<IconDownload size={16} strokeWidth={1.5} />}
132149
onClick={() => fetchInvoice(x.id)}
133150
/>
134151
</div>
@@ -137,7 +154,7 @@ const InvoicesSettings = () => {
137154
)
138155
})}
139156
<Table.tr key="navigation">
140-
<Table.td colSpan={5}>
157+
<Table.td colSpan={6}>
141158
<div className="flex items-center justify-between">
142159
<p className="text-sm opacity-50">
143160
Showing {offset + 1} to {offset + invoices.length} out of {count} invoices

0 commit comments

Comments
 (0)