Skip to content

Commit 054fee2

Browse files
aledefrawzrdx
andauthored
feat: allow to download json for invoices and burn reports (#34)
* feat: allow to download json for invoices and burn reports * fix: UI and duplicate code --------- Co-authored-by: wzrdx <[email protected]>
1 parent f01ecdc commit 054fee2

File tree

4 files changed

+118
-13
lines changed

4 files changed

+118
-13
lines changed

src/components/account/BurnReport.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Button } from '@heroui/button';
22
import { DateRangePicker } from '@heroui/date-picker';
33
import { CalendarDate, parseDate, today } from '@internationalized/date';
4-
import { downloadBurnReport } from '@lib/api/backend';
4+
import { downloadBurnReportCSV, downloadBurnReportJSON } from '@lib/api/backend';
55
import { padNumber } from '@lib/utils';
66
import { BorderedCard } from '@shared/cards/BorderedCard';
77
import { formatISO, subMonths } from 'date-fns';
88
import { useState } from 'react';
99
import toast from 'react-hot-toast';
10-
import { RiCalendarEventLine, RiFileTextLine } from 'react-icons/ri';
10+
import { LuFileJson, LuFileSpreadsheet } from 'react-icons/lu';
11+
import { RiCalendarEventLine } from 'react-icons/ri';
1112

1213
export default function BurnReport() {
1314
const [value, setValue] = useState<{
@@ -20,14 +21,31 @@ export default function BurnReport() {
2021

2122
const [isLoading, setLoading] = useState<boolean>(false);
2223

23-
const onDownload = async () => {
24+
const onDownloadCSV = async () => {
2425
try {
2526
setLoading(true);
2627

2728
const start = `${padNumber(value.start.day, 2)}-${padNumber(value.start.month, 2)}-${value.start.year}`;
2829
const end = `${padNumber(value.end.day, 2)}-${padNumber(value.end.month, 2)}-${value.end.year}`;
2930

30-
const report = await downloadBurnReport(start, end);
31+
const report = await downloadBurnReportCSV(start, end);
32+
console.log(report);
33+
} catch (error) {
34+
console.error(error);
35+
toast.error('Failed to download burn report.');
36+
} finally {
37+
setLoading(false);
38+
}
39+
};
40+
41+
const onDownloadJSON = async () => {
42+
try {
43+
setLoading(true);
44+
45+
const start = `${padNumber(value.start.day, 2)}-${padNumber(value.start.month, 2)}-${value.start.year}`;
46+
const end = `${padNumber(value.end.day, 2)}-${padNumber(value.end.month, 2)}-${value.end.year}`;
47+
48+
const report = await downloadBurnReportJSON(start, end);
3149
console.log(report);
3250
} catch (error) {
3351
console.error(error);
@@ -69,11 +87,18 @@ export default function BurnReport() {
6987
/>
7088
</div>
7189

72-
<div className="center-all">
73-
<Button color="primary" onPress={onDownload} isLoading={isLoading}>
90+
<div className="center-all gap-1.5">
91+
<Button color="primary" onPress={onDownloadCSV} isLoading={isLoading}>
92+
<div className="row gap-1.5">
93+
<LuFileSpreadsheet className="text-lg" />
94+
<div className="text-sm">Download CSV</div>
95+
</div>
96+
</Button>
97+
98+
<Button color="primary" onPress={onDownloadJSON} isLoading={isLoading}>
7499
<div className="row gap-1.5">
75-
<RiFileTextLine className="text-lg" />
76-
<div className="text-sm">Download</div>
100+
<LuFileJson className="text-lg" />
101+
<div className="text-sm">Download JSON</div>
77102
</div>
78103
</Button>
79104
</div>

src/components/account/invoicing/DraftInvoiceCard.tsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { Button } from '@heroui/button';
2-
import { downloadCspDraft } from '@lib/api/backend';
2+
import { downloadCspDraft, downloadCspDraftJSON } from '@lib/api/backend';
33
import { getShortAddressOrHash } from '@lib/utils';
44
import { BorderedCard } from '@shared/cards/BorderedCard';
55
import { CopyableValue } from '@shared/CopyableValue';
66
import ItemWithLabel from '@shared/ItemWithLabel';
77
import { InvoiceDraft } from '@typedefs/general';
88
import { useState } from 'react';
99
import toast from 'react-hot-toast';
10+
import { RiDownloadLine } from 'react-icons/ri';
1011

1112
export default function DraftInvoiceCard({
1213
draft,
@@ -32,6 +33,19 @@ export default function DraftInvoiceCard({
3233
}
3334
};
3435

36+
const downloadDraftJson = async (draftId: string) => {
37+
try {
38+
setLoading(true);
39+
const draft = await downloadCspDraftJSON(draftId);
40+
console.log('Draft JSON', draft);
41+
} catch (error) {
42+
console.error(error);
43+
toast.error('Failed to download draft.');
44+
} finally {
45+
setLoading(false);
46+
}
47+
};
48+
3549
return (
3650
<BorderedCard isHoverable onClick={toggle}>
3751
<div className="col gap-4">
@@ -55,7 +69,7 @@ export default function DraftInvoiceCard({
5569

5670
<div className="min-w-[118px] font-medium">${draft.totalUsdcAmount.toFixed(2)}</div>
5771

58-
<div className="flex min-w-[124px] justify-end">
72+
<div className="flex min-w-[214px] justify-end gap-2">
5973
<Button
6074
className="border-2 border-slate-200 bg-white data-[hover=true]:!opacity-65"
6175
isLoading={isLoading}
@@ -69,7 +83,29 @@ export default function DraftInvoiceCard({
6983
}
7084
}}
7185
>
72-
<div className="text-sm">Download</div>
86+
<div className="row gap-1">
87+
<RiDownloadLine className="text-base" />
88+
<div className="text-sm">Document</div>
89+
</div>
90+
</Button>
91+
92+
<Button
93+
className="border-2 border-slate-200 bg-white data-[hover=true]:!opacity-65"
94+
isLoading={isLoading}
95+
size="sm"
96+
color="primary"
97+
variant="flat"
98+
onPress={() => {
99+
if (!isLoading) {
100+
console.log('Download JSON', draft.draftId);
101+
downloadDraftJson(draft.draftId);
102+
}
103+
}}
104+
>
105+
<div className="row gap-1">
106+
<RiDownloadLine className="text-base" />
107+
<div className="text-sm">JSON</div>
108+
</div>
73109
</Button>
74110
</div>
75111
</div>

src/components/account/invoicing/Invoicing.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function Invoicing() {
9797
<div className="min-w-[122px]">Date</div>
9898
<div className="min-w-[170px]">Node Operator</div>
9999
<div className="min-w-[118px]">Amount ($USDC)</div>
100-
<div className="min-w-[124px]"></div>
100+
<div className="min-w-[214px]"></div>
101101
</ListHeader>
102102

103103
{isLoading ? (

src/lib/api/backend.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ export const downloadCspDraft = async (draftId: string) => {
4545
setTimeout(() => URL.revokeObjectURL(urlObj), 0);
4646
};
4747

48-
export const downloadBurnReport = async (start: string, end: string) => {
48+
export const downloadCspDraftJSON = async (draftId: string) =>
49+
downloadJsonFile(`/invoice-draft/download-csp-draft-json?draftId=${draftId}`, 'csp_draft.json');
50+
51+
export const downloadBurnReportCSV = async (start: string, end: string) => {
4952
const res = await axiosDapp.get(`/burn-report/download-burn-report?startTime=${start}&endTime=${end}`, {
5053
responseType: 'blob',
5154
});
@@ -84,6 +87,12 @@ export const downloadBurnReport = async (start: string, end: string) => {
8487
setTimeout(() => URL.revokeObjectURL(urlObj), 0);
8588
};
8689

90+
export const downloadBurnReportJSON = async (start: string, end: string) =>
91+
downloadJsonFile(
92+
`/burn-report/download-burn-report-json?startTime=${start}&endTime=${end}`,
93+
'burn_report.json',
94+
);
95+
8796
export const getBrandingPlatforms = async () => _doGet<string[]>('/branding/get-platforms');
8897

8998
export const getProfilePicture = async (address: types.EthAddress) =>
@@ -142,6 +151,41 @@ async function _doPost<T>(endpoint: string, body: any, headers?: Record<string,
142151
return data.data;
143152
}
144153

154+
async function downloadJsonFile<T = unknown>(endpoint: string, defaultFilename: string): Promise<T> {
155+
const res = await axiosDapp.get(endpoint);
156+
157+
if (res.status !== 200) {
158+
throw new Error(`Download failed with status ${res.status}.`);
159+
}
160+
161+
if (res.data?.error) {
162+
throw new Error(res.data.error);
163+
}
164+
165+
const payload = (res.data?.data ?? res.data) as T;
166+
167+
let filename = defaultFilename;
168+
const contentDisposition = res.headers['content-disposition'];
169+
if (contentDisposition) {
170+
const filenameMatch = contentDisposition.match(/filename=([^;]+)/);
171+
if (filenameMatch) {
172+
filename = filenameMatch[1].replace(/['"]/g, '');
173+
}
174+
}
175+
176+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
177+
const urlObj = URL.createObjectURL(blob);
178+
const a = document.createElement('a');
179+
a.href = urlObj;
180+
a.download = filename;
181+
document.body.appendChild(a);
182+
a.click();
183+
a.remove();
184+
setTimeout(() => URL.revokeObjectURL(urlObj), 0);
185+
186+
return payload;
187+
}
188+
145189
const axiosDapp = axios.create({
146190
baseURL: backendUrl,
147191
headers: {

0 commit comments

Comments
 (0)