|
1 | 1 | <script lang="ts"> |
2 | 2 | import { page } from '$app/state'; |
| 3 | + import { onMount } from 'svelte'; |
3 | 4 | import { CardGrid, PaginationInline } from '$lib/components'; |
4 | 5 | import { Button } from '$lib/elements/forms'; |
5 | 6 | import { toLocaleDate } from '$lib/helpers/date'; |
|
28 | 29 | IconRefresh |
29 | 30 | } from '@appwrite.io/pink-icons-svelte'; |
30 | 31 |
|
| 32 | + let limit = $state(5); |
31 | 33 | let offset = $state(0); |
32 | 34 | let isLoadingInvoices = $state(false); |
33 | 35 | let invoiceList: InvoiceList = $state({ |
34 | 36 | invoices: [], |
35 | 37 | total: 0 |
36 | 38 | }); |
37 | 39 |
|
38 | | - const limit = 5; |
39 | 40 | const endpoint = getApiEndpoint(); |
40 | 41 | const hasPaymentError = $derived(invoiceList?.invoices.some((invoice) => invoice?.lastError)); |
41 | 42 |
|
42 | | - async function request() { |
| 43 | + /** |
| 44 | + * Special case handling for the first page! |
| 45 | + * |
| 46 | + * As per Damodar - `there is some logic to **hide current cycle invoice** in the endpoint`. |
| 47 | + * |
| 48 | + * Due to this, the first page always loads `limit - 1` invoices which is inconsistent! |
| 49 | + * Therefore, we load `limit + 1` to counter that so the returned invoices are consistent. |
| 50 | + */ |
| 51 | + onMount(() => request(true)); |
| 52 | +
|
| 53 | + async function request(patchQuery: boolean = false) { |
43 | 54 | isLoadingInvoices = true; |
44 | 55 | invoiceList = await sdk.forConsole.billing.listInvoices(page.params.organization, [ |
45 | | - Query.limit(limit), |
46 | | - Query.offset(offset), |
47 | | - Query.orderDesc('$createdAt') |
| 56 | + Query.orderDesc('$createdAt'), |
| 57 | +
|
| 58 | + // first page extra must have an extra limit! |
| 59 | + Query.limit(patchQuery ? limit + 1 : limit), |
| 60 | +
|
| 61 | + // so an invoice isn't repeated on 2nd page! |
| 62 | + Query.offset(patchQuery ? offset : offset + 1) |
48 | 63 | ]); |
49 | 64 |
|
50 | 65 | isLoadingInvoices = false; |
|
62 | 77 | } |
63 | 78 | }); |
64 | 79 |
|
65 | | - $effect(() => { |
66 | | - if (offset !== null) { |
67 | | - request(); |
68 | | - } |
69 | | - }); |
70 | | -
|
71 | 80 | const columns = $derived([ |
72 | 81 | { id: 'dueDate', width: { min: 120 } }, |
73 | 82 | { id: 'status', width: { min: hasPaymentError ? 200 : 100 } }, |
74 | 83 | { id: 'amount', width: { min: 120 } }, |
75 | | - { id: 'action', width: 40 } |
| 84 | + { id: 'actions', width: 40 } |
76 | 85 | ]); |
77 | 86 | </script> |
78 | 87 |
|
|
86 | 95 | <Table.Header.Cell column="dueDate" {root}>Due date</Table.Header.Cell> |
87 | 96 | <Table.Header.Cell column="status" {root}>Status</Table.Header.Cell> |
88 | 97 | <Table.Header.Cell column="amount" {root}>Amount due</Table.Header.Cell> |
89 | | - <Table.Header.Cell column="action" {root} /> |
| 98 | + <Table.Header.Cell column="actions" {root} /> |
90 | 99 | </svelte:fragment> |
91 | 100 |
|
92 | 101 | {#if isLoadingInvoices} |
93 | | - {#each Array.from({ length: 2 }).keys() as index (index)} |
| 102 | + {#each Array.from({ length: 5 }).keys() as index (index)} |
94 | 103 | <Table.Row.Base {root}> |
95 | 104 | {#each columns as column} |
96 | 105 | <Table.Cell column={column.id} {root}> |
|
99 | 108 | {/each} |
100 | 109 | </Table.Row.Base> |
101 | 110 | {/each} |
102 | | - {/if} |
103 | | - |
104 | | - {#each invoiceList?.invoices as invoice} |
105 | | - {@const status = invoice.status} |
106 | | - <Table.Row.Base {root}> |
107 | | - <Table.Cell column="dueDate" {root}> |
108 | | - {toLocaleDate(invoice.dueAt)} |
109 | | - </Table.Cell> |
110 | | - <Table.Cell column="status" {root}> |
111 | | - {@const isDanger = |
112 | | - status === 'overdue' || |
113 | | - status === 'failed' || |
114 | | - status === 'requires_authentication'} |
115 | | - {@const isSuccess = status === 'paid' || status === 'succeeded'} |
116 | | - {@const isWarning = status === 'pending'} |
117 | | - <Layout.Stack direction="row" gap="s"> |
118 | | - <Badge |
119 | | - variant="secondary" |
120 | | - content={status === 'requires_authentication' |
121 | | - ? 'failed' |
122 | | - : status} |
123 | | - type={isDanger |
124 | | - ? 'error' |
125 | | - : isWarning |
126 | | - ? 'warning' |
127 | | - : isSuccess |
128 | | - ? 'success' |
129 | | - : undefined} /> |
130 | | - {#if invoice?.lastError} |
131 | | - <Popover let:toggle> |
132 | | - <Link.Button on:click={toggle}>Details</Link.Button> |
133 | | - <svelte:fragment slot="tooltip"> |
134 | | - The scheduled payment has failed. |
135 | | - <Link.Button on:click={() => retryPayment(invoice)} |
136 | | - >Try again |
137 | | - </Link.Button> |
138 | | - </svelte:fragment> |
139 | | - </Popover> |
140 | | - {/if} |
141 | | - </Layout.Stack> |
142 | | - </Table.Cell> |
143 | | - <Table.Cell column="amount" {root}> |
144 | | - {formatCurrency(invoice.grossAmount)} |
145 | | - </Table.Cell> |
146 | | - <Table.Cell column="status" {root}> |
147 | | - <Popover let:toggle placement="bottom-start" padding="none"> |
148 | | - <Button text icon ariaLabel="more options" on:click={toggle}> |
149 | | - <Icon icon={IconDotsHorizontal} size="s" /> |
150 | | - </Button> |
151 | | - <ActionMenu.Root slot="tooltip"> |
152 | | - <!-- todo: add missing event --> |
153 | | - <ActionMenu.Item.Anchor |
154 | | - leadingIcon={IconExternalLink} |
155 | | - external |
156 | | - href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}> |
157 | | - View invoice |
158 | | - </ActionMenu.Item.Anchor> |
159 | | - <ActionMenu.Item.Anchor |
160 | | - leadingIcon={IconDownload} |
161 | | - href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}> |
162 | | - Download PDF |
163 | | - </ActionMenu.Item.Anchor> |
164 | | - {#if status === 'overdue' || status === 'failed' || status === 'abandoned'} |
165 | | - <ActionMenu.Item.Button |
166 | | - leadingIcon={IconRefresh} |
167 | | - on:click={() => { |
168 | | - retryPayment(invoice); |
169 | | - trackEvent(`click_retry_payment`, { |
170 | | - from: 'button', |
171 | | - source: 'billing_invoice_menu' |
172 | | - }); |
173 | | - }}> |
174 | | - Retry payment |
175 | | - </ActionMenu.Item.Button> |
| 111 | + {:else} |
| 112 | + {#each invoiceList?.invoices as invoice (invoice.$id)} |
| 113 | + {@const status = invoice.status} |
| 114 | + <Table.Row.Base {root}> |
| 115 | + <Table.Cell column="dueDate" {root} |
| 116 | + >{toLocaleDate(invoice.dueAt)}</Table.Cell> |
| 117 | + <Table.Cell column="status" {root}> |
| 118 | + {@const isDanger = |
| 119 | + status === 'overdue' || |
| 120 | + status === 'failed' || |
| 121 | + status === 'requires_authentication'} |
| 122 | + {@const isSuccess = status === 'paid' || status === 'succeeded'} |
| 123 | + {@const isWarning = status === 'pending'} |
| 124 | + <Layout.Stack direction="row" gap="s"> |
| 125 | + <Badge |
| 126 | + variant="secondary" |
| 127 | + content={status === 'requires_authentication' |
| 128 | + ? 'failed' |
| 129 | + : status} |
| 130 | + type={isDanger |
| 131 | + ? 'error' |
| 132 | + : isWarning |
| 133 | + ? 'warning' |
| 134 | + : isSuccess |
| 135 | + ? 'success' |
| 136 | + : undefined} /> |
| 137 | + {#if invoice?.lastError} |
| 138 | + <Popover let:toggle> |
| 139 | + <Link.Button on:click={toggle}>Details</Link.Button> |
| 140 | + <svelte:fragment slot="tooltip"> |
| 141 | + The scheduled payment has failed. |
| 142 | + <Link.Button on:click={() => retryPayment(invoice)} |
| 143 | + >Try again |
| 144 | + </Link.Button> |
| 145 | + </svelte:fragment> |
| 146 | + </Popover> |
176 | 147 | {/if} |
177 | | - </ActionMenu.Root> |
178 | | - </Popover> |
179 | | - </Table.Cell> |
180 | | - </Table.Row.Base> |
181 | | - {/each} |
| 148 | + </Layout.Stack> |
| 149 | + </Table.Cell> |
| 150 | + <Table.Cell column="amount" {root}> |
| 151 | + {formatCurrency(invoice.grossAmount)} |
| 152 | + </Table.Cell> |
| 153 | + <Table.Cell column="actions" {root}> |
| 154 | + <Popover let:toggle placement="bottom-start" padding="none"> |
| 155 | + <Button text icon ariaLabel="more options" on:click={toggle}> |
| 156 | + <Icon icon={IconDotsHorizontal} size="s" /> |
| 157 | + </Button> |
| 158 | + <ActionMenu.Root slot="tooltip"> |
| 159 | + <!-- todo: add missing event --> |
| 160 | + <ActionMenu.Item.Anchor |
| 161 | + leadingIcon={IconExternalLink} |
| 162 | + external |
| 163 | + href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}> |
| 164 | + View invoice |
| 165 | + </ActionMenu.Item.Anchor> |
| 166 | + <ActionMenu.Item.Anchor |
| 167 | + leadingIcon={IconDownload} |
| 168 | + href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}> |
| 169 | + Download PDF |
| 170 | + </ActionMenu.Item.Anchor> |
| 171 | + {#if status === 'overdue' || status === 'failed' || status === 'abandoned'} |
| 172 | + <ActionMenu.Item.Button |
| 173 | + leadingIcon={IconRefresh} |
| 174 | + on:click={() => { |
| 175 | + retryPayment(invoice); |
| 176 | + trackEvent(`click_retry_payment`, { |
| 177 | + from: 'button', |
| 178 | + source: 'billing_invoice_menu' |
| 179 | + }); |
| 180 | + }}> |
| 181 | + Retry payment |
| 182 | + </ActionMenu.Item.Button> |
| 183 | + {/if} |
| 184 | + </ActionMenu.Root> |
| 185 | + </Popover> |
| 186 | + </Table.Cell> |
| 187 | + </Table.Row.Base> |
| 188 | + {/each} |
| 189 | + {/if} |
182 | 190 | </Table.Root> |
183 | | - {#if invoiceList.total > limit} |
| 191 | + {#if invoiceList.total >= limit} |
184 | 192 | <Layout.Stack direction="row" justifyContent="space-between" alignItems="center"> |
185 | 193 | <p class="text">Total results: {invoiceList.total}</p> |
186 | | - <PaginationInline {limit} bind:offset total={invoiceList.total} hidePages /> |
| 194 | + <PaginationInline |
| 195 | + {limit} |
| 196 | + hidePages |
| 197 | + bind:offset |
| 198 | + total={invoiceList.total} |
| 199 | + on:change={() => request()} /> |
187 | 200 | </Layout.Stack> |
188 | 201 | {/if} |
189 | 202 | {:else} |
|
0 commit comments