Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,087 changes: 1,038 additions & 49 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions packages/invoice-dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
"@requestnetwork/payment-detection": "0.43.1-next.2043",
"@requestnetwork/payment-processor": "0.45.1-next.2043",
"@requestnetwork/request-client.js": "0.47.1-next.2043",
"viem": "^2.9.15",
"ethers": "^5.7.2"
"ethers": "^5.7.2",
"svelte-sonner": "^0.3.27",
"viem": "^2.9.15"
},
"devDependencies": {
"svelte": "^4.0.5",
"@sveltejs/package": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^2.5.2",
"@tailwindcss/typography": "^0.5.13",
"svelte": "^4.0.5",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
Expand Down
11 changes: 0 additions & 11 deletions packages/invoice-dashboard/src/app.css

This file was deleted.

29 changes: 28 additions & 1 deletion packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
hasErc20Approval,
} from "@requestnetwork/payment-processor";
import { getPaymentNetworkExtension } from "@requestnetwork/payment-detection";
import { toast } from "svelte-sonner";

// Components
import Button from "@requestnetwork/shared-components/button.svelte";
import Accordion from "@requestnetwork/shared-components/accordion.svelte";
import Tooltip from "@requestnetwork/shared-components/tooltip.svelte";

// Icons
import Check from "@requestnetwork/shared-icons/check.svelte";
import Download from "@requestnetwork/shared-icons/download.svelte";

// Utils
import { formatDate } from "@requestnetwork/shared-utils/formatDate";
Expand All @@ -24,7 +27,7 @@
// Types
import type { WalletState } from "@requestnetwork/shared-types/web3Onboard";

import { walletClientToSigner } from "../../utils";
import { walletClientToSigner, exportToPDF } from "../../utils";
import { formatUnits } from "viem";
import { onMount } from "svelte";

Expand Down Expand Up @@ -224,6 +227,24 @@
<p class={`invoice-status ${isPaid ? "bg-green" : "bg-zinc"}`}>
{isPaid ? "Paid" : "Created"}
</p>
<Tooltip text="Download PDF">
<Download
onClick={async () => {
try {
await exportToPDF(request, currency, config.logo);
} catch (error) {
toast.error(`Failed to export PDF`, {
description: `${error}`,
action: {
label: "X",
onClick: () => console.info("Close"),
},
});
console.error("Failed to export PDF:", error);
}
}}
/>
</Tooltip>
</h2>
<div class="invoice-address">
<h2>From:</h2>
Expand Down Expand Up @@ -451,9 +472,15 @@
line-height: 1.75rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 12px;
}

.invoice-number svg {
width: 13px;
height: 13px;
}

.invoice-status {
padding-left: 0.5rem;
padding-right: 0.5rem;
Expand Down
34 changes: 33 additions & 1 deletion packages/invoice-dashboard/src/lib/view-requests.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
import Dropdown from "@requestnetwork/shared-components/dropdown.svelte";
import Skeleton from "@requestnetwork/shared-components/skeleton.svelte";
import PoweredBy from "@requestnetwork/shared-components/powered-by.svelte";
import Tooltip from "@requestnetwork/shared-components/tooltip.svelte";
import Toaster from "@requestnetwork/shared-components/sonner.svelte";
import { toast } from "svelte-sonner";

// Icons
import ChevronUp from "@requestnetwork/shared-icons/chevron-up.svelte";
import ChevronDown from "@requestnetwork/shared-icons/chevron-down.svelte";
import ChevronLeft from "@requestnetwork/shared-icons/chevron-left.svelte";
import ChevronRight from "@requestnetwork/shared-icons/chevron-right.svelte";
import Search from "@requestnetwork/shared-icons/search.svelte";
import Download from "@requestnetwork/shared-icons/download.svelte";

// Types
import type { IConfig } from "@requestnetwork/shared-types";
Expand All @@ -30,7 +34,7 @@
import { Drawer, InvoiceView } from "./dashboard";
import { Types } from "@requestnetwork/request-client.js";
import type { RequestNetwork } from "@requestnetwork/request-client.js";
import { debounce, formatAddress } from "../utils";
import { debounce, exportToPDF, formatAddress } from "../utils";
import { CurrencyManager } from "@requestnetwork/currency";

export let config: IConfig;
Expand Down Expand Up @@ -412,6 +416,7 @@
</i>
</div>
</th>
<th></th>
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -484,6 +489,32 @@
?.symbol}
</td>
<td> {checkStatus(request)}</td>
<td
><Tooltip text="Download PDF">
<Download
onClick={async () => {
try {
await exportToPDF(
request,
currencyManager.fromAddress(
request?.currencyInfo?.value
),
config.logo
);
} catch (error) {
toast.error(`Failed to export PDF`, {
description: `${error}`,
action: {
label: "X",
onClick: () => console.info("Close"),
},
});
console.error("Failed to export PDF:", error);
}
}}
/>
</Tooltip></td
>
</tr>
{/each}
{/if}
Expand Down Expand Up @@ -551,6 +582,7 @@
</div>
{/if}
<PoweredBy />
<Toaster />
</div>

<style>
Expand Down
170 changes: 170 additions & 0 deletions packages/invoice-dashboard/src/utils/generateInvoice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { formatUnits } from "viem";
import { loadScript } from "./loadScript";
import { calculateItemTotal } from "@requestnetwork/shared-utils/invoiceTotals";

declare global {
interface Window {
html2pdf: any;
}
}

async function ensureHtml2PdfLoaded() {
if (typeof window.html2pdf === "undefined") {
await loadScript(
"https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"
);
}
}

export const exportToPDF = async (
invoice: any,
currency: any,
logo: string
) => {
await ensureHtml2PdfLoaded();

const content = `
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
</head>
<body>
<div id="invoice" style="font-family: Urbanist, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px;">
<div style="display: flex; justify-content: space-between; align-items: start;">
<img src="${logo}" alt="Logo" style="width: 50px; height: 50px;">
<div style="text-align: right;">
<p>Issued on ${new Date(
invoice.contentData.creationDate
).toLocaleDateString()}</p>
<p>Payment due by ${new Date(
invoice.contentData.paymentTerms.dueDate
).toLocaleDateString()}</p>
</div>
</div>

<h1 style="text-align: center; color: #333; font-size: 28px; font-style: bold; margin-bottom: 14px;">INVOICE #${
invoice.contentData.invoiceNumber
}</h1>

<div style="display: flex; justify-content: space-between; margin-bottom: 20px; background-color: #FBFBFB; padding: 10px;">
<div>
<strong>From:</strong><br>
<p style="font-size: 14px">${invoice.payee.value}</p>
${invoice.contentData.sellerInfo.firstName ?? ""} ${
invoice.contentData.sellerInfo.lastName ?? ""
}<br>
${invoice.contentData.sellerInfo.address["street-address"] ?? ""}<br>
${invoice.contentData.sellerInfo.address.locality ?? ""}${
invoice.contentData.sellerInfo.address.locality ? "," : ""
} ${invoice.contentData.sellerInfo.address["postal-code"] ?? ""}<br>
${invoice.contentData.sellerInfo.address["country-name"] ?? ""}<br>
${
invoice.contentData.sellerInfo.taxRegistration
? `VAT: ${invoice.contentData.sellerInfo.taxRegistration}`
: ""
}<br>
</div>

<div>
<strong>To:</strong><br>
<p style="font-size: 14px">${invoice.payer.value}</p>
${invoice.contentData.buyerInfo.firstName ?? ""} ${
invoice.contentData.buyerInfo.lastName ?? ""
}<br>
${invoice.contentData.buyerInfo.address["street-address"] ?? ""}<br>
${invoice.contentData.buyerInfo.address.locality ?? ""}${
invoice.contentData.sellerInfo.address.locality ? "," : ""
} ${invoice.contentData.buyerInfo.address["postal-code"] ?? ""}<br>
${invoice.contentData.buyerInfo.address["country-name"] ?? ""}<br>
${
invoice.contentData.buyerInfo.taxRegistration
? `VAT: ${invoice.contentData.buyerInfo.taxRegistration}`
: ""
}<br>
</div>
</div>

<div style="margin-bottom: 20px;">
<strong>Payment Chain:</strong> ${invoice.currencyInfo.network}<br>
<strong>Invoice Currency:</strong> ${invoice.currency}<br>
<strong>Invoice Type:</strong> Regular Invoice
</div>

<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background-color: #f2f2f2;">
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Description</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: right;">Quantity</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: right;">Unit Price</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: right;">Discount</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: right;">Tax</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: right;">Amount</th>
</tr>
</thead>
<tbody>
${invoice.contentData.invoiceItems
.map(
(item: any) => `
<tr>
<td style="border: 1px solid #ddd; padding: 8px;">${
item.name
}</td>
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${
item.quantity
}</td>
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${formatUnits(
item.unitPrice,
currency.decimals
)}</td>
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${formatUnits(
item.discount,
currency.decimals
)}</td>
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${
item.tax.amount
}%</td>
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${formatUnits(
calculateItemTotal(item),
currency?.decimals
)}</td>
</tr>
`
)
.join("")}
</tbody>
<tfoot>
<tr>
<td colspan="5" style="border: 1px solid #ddd; padding: 8px; text-align: right;"><strong>Due:</strong></td>
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;"><strong>${formatUnits(
invoice.expectedAmount,
currency.decimals
)} ${invoice.currency}</strong></td>
</tr>
</tfoot>
</table>

${
invoice.contentData.note
? `<div style="margin-top: 20px;">
<h3>Memo:</h3>
<p>${invoice.contentData.note}</p>
</div>`
: ""
}
</div>
</body>
</html>
`;

const opt = {
margin: 10,
filename: `invoice-${invoice.contentData.invoiceNumber}.PDF`,
image: { type: "jpeg", quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
};

window.html2pdf().from(content).set(opt).save();
};
1 change: 1 addition & 0 deletions packages/invoice-dashboard/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { debounce } from "./debounce";
export { formatAddress } from "./formatAddress";
export { exportToPDF } from "./generateInvoice";
export { publicClientToProvider, walletClientToSigner } from "./wallet-utils";
9 changes: 9 additions & 0 deletions packages/invoice-dashboard/src/utils/loadScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const loadScript = (src: string): Promise<void> => {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
document.head.appendChild(script);
});
};
Loading