From 020ecff29c0ff305563450b3f73dfb8538052d50 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Wed, 13 Dec 2023 23:29:14 +0000 Subject: [PATCH 1/4] Implemented file storage handling for Corporate Transfer and Comission Transfer --- src/components/PaymentAssetManager.tsx | 156 ++++++++++++++ src/firebase/index.ts | 2 + src/interfaces/paypal.ts | 2 + src/interfaces/storage.files.ts | 1 + src/pages/api/evaluate/interactiveSpeaking.ts | 6 +- src/pages/api/evaluate/speaking.ts | 6 +- .../api/payments/files/[type]/[paymentId].ts | 194 ++++++++++++++++++ src/pages/api/speaking.ts | 3 +- src/pages/api/users/update.ts | 3 +- src/pages/payment-record.tsx | 112 ++++++++++ 10 files changed, 473 insertions(+), 12 deletions(-) create mode 100644 src/components/PaymentAssetManager.tsx create mode 100644 src/interfaces/storage.files.ts create mode 100644 src/pages/api/payments/files/[type]/[paymentId].ts diff --git a/src/components/PaymentAssetManager.tsx b/src/components/PaymentAssetManager.tsx new file mode 100644 index 00000000..25c66727 --- /dev/null +++ b/src/components/PaymentAssetManager.tsx @@ -0,0 +1,156 @@ +import React, { ChangeEvent } from "react"; +import { BsUpload, BsDownload, BsTrash, BsFilePdf } from "react-icons/bs"; +import { FilesStorage } from "@/interfaces/storage.files"; +import axios from "axios"; + +interface Asset { + file: string | File; + complete: boolean; +} + +const PaymentAssetManager = (props: { + asset: string | undefined; + permissions: "read" | "write"; + type: FilesStorage; + paymentId: string; +}) => { + const { asset, permissions, type, paymentId } = props; + + const fileInputRef = React.useRef(null); + const fileInputReplaceRef = React.useRef(null); + + const [managingAsset, setManagingAsset] = React.useState({ + file: asset || "", + complete: asset ? true : false, + }); + + const { file, complete } = managingAsset; + + const deleteAsset = () => { + if (confirm("Are you sure you want to delete this document?")) { + axios + .delete(`/api/payments/files/${type}/${paymentId}`) + .then((response) => { + if (response.status === 200) { + console.log("File deleted successfully!"); + setManagingAsset({ + file: "", + complete: false, + }); + return; + } + + console.error("File deletion failed"); + }) + .catch((error) => { + console.error("Error occurred during file deletion:", error); + }); + } + }; + + const renderFileInput = ( + onChange: any, + ref: React.RefObject + ) => ( + + ); + + const handleFileChange = async (e: Event, method: "post" | "patch") => { + const newFile = (e.target as HTMLInputElement).files?.[0]; + if (newFile) { + setManagingAsset({ + file: newFile, + complete: false, + }); + + const formData = new FormData(); + formData.append("file", newFile); + + axios[method](`/api/payments/files/${type}/${paymentId}`, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then((response) => { + if (response.status === 200) { + console.log("File uploaded successfully!"); + console.log("Uploaded File URL:", response.data.ref); + // Further actions upon successful upload + setManagingAsset({ + file: response.data.ref, + complete: true, + }); + return; + } + + console.error("File upload failed"); + }) + .catch((error) => { + console.error("Error occurred during file upload:", error); + }); + } + }; + + const downloadAsset = () => { + axios + .get(`/api/payments/files/${type}/${paymentId}`) + .then((response) => { + if (response.status === 200) { + console.log("Uploaded File URL:", response.data.url); + const link = document.createElement("a"); + link.download = response.data.filename; + link.href = response.data.url; + link.click(); + return; + } + + console.error("Failed to download file"); + }) + .catch((error) => { + console.error("Error occurred during file upload:", error); + }); + }; + + if (permissions === "read") { + if (file) return ; + return null; + } + + if (file) { + if (complete) { + return ( + <> + + fileInputReplaceRef.current?.click()} /> + + {renderFileInput( + (e: Event) => handleFileChange(e, "patch"), + fileInputReplaceRef + )} + {renderFileInput( + (e: Event) => handleFileChange(e, "post"), + fileInputRef + )} + + ); + } + + return ; + } + + return ( + <> + fileInputRef.current?.click()} /> + {renderFileInput((e: Event) => handleFileChange(e, "post"), fileInputRef)} + + ); +}; + +export default PaymentAssetManager; diff --git a/src/firebase/index.ts b/src/firebase/index.ts index 74c44245..3b29080b 100644 --- a/src/firebase/index.ts +++ b/src/firebase/index.ts @@ -1,5 +1,6 @@ import {initializeApp} from "firebase/app"; import * as admin from "firebase-admin/app"; +import { getStorage } from "firebase/storage"; const serviceAccount = require("@/constants/serviceAccountKey.json"); @@ -19,3 +20,4 @@ export const adminApp = admin.initializeApp( }, Math.random().toString(), ); +export const storage = getStorage(app); \ No newline at end of file diff --git a/src/interfaces/paypal.ts b/src/interfaces/paypal.ts index 4305b960..56bf2688 100644 --- a/src/interfaces/paypal.ts +++ b/src/interfaces/paypal.ts @@ -32,4 +32,6 @@ export interface Payment { value: number; isPaid: boolean; date: Date; + corporateTransfer?: string; + commissionTransfer?: string; } diff --git a/src/interfaces/storage.files.ts b/src/interfaces/storage.files.ts new file mode 100644 index 00000000..d43651d5 --- /dev/null +++ b/src/interfaces/storage.files.ts @@ -0,0 +1 @@ +export type FilesStorage = "commission" | "corporate"; diff --git a/src/pages/api/evaluate/interactiveSpeaking.ts b/src/pages/api/evaluate/interactiveSpeaking.ts index e1db5851..aa761ff1 100644 --- a/src/pages/api/evaluate/interactiveSpeaking.ts +++ b/src/pages/api/evaluate/interactiveSpeaking.ts @@ -4,9 +4,9 @@ import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import axios from "axios"; import formidable from "formidable-serverless"; -import {getStorage, ref, uploadBytes} from "firebase/storage"; +import {ref, uploadBytes} from "firebase/storage"; import fs from "fs"; -import {app} from "@/firebase"; +import {storage} from "@/firebase"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -16,8 +16,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const storage = getStorage(app); - const form = formidable({keepExtensions: true}); await form.parse(req, async (err: any, fields: any, files: any) => { if (err) console.log(err); diff --git a/src/pages/api/evaluate/speaking.ts b/src/pages/api/evaluate/speaking.ts index 0d632366..f7865836 100644 --- a/src/pages/api/evaluate/speaking.ts +++ b/src/pages/api/evaluate/speaking.ts @@ -4,9 +4,9 @@ import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import axios from "axios"; import formidable from "formidable-serverless"; -import {getStorage, ref, uploadBytes} from "firebase/storage"; +import {ref, uploadBytes} from "firebase/storage"; import fs from "fs"; -import {app} from "@/firebase"; +import {storage} from "@/firebase"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -16,8 +16,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const storage = getStorage(app); - const form = formidable({keepExtensions: true}); await form.parse(req, async (err: any, fields: any, files: any) => { if (err) console.log(err); diff --git a/src/pages/api/payments/files/[type]/[paymentId].ts b/src/pages/api/payments/files/[type]/[paymentId].ts new file mode 100644 index 00000000..88c4fa24 --- /dev/null +++ b/src/pages/api/payments/files/[type]/[paymentId].ts @@ -0,0 +1,194 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app, storage } from "@/firebase"; +import { + getFirestore, + getDoc, + doc, + updateDoc, + deleteField, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { FilesStorage } from "@/interfaces/storage.files"; + +import { Payment } from "@/interfaces/paypal"; +import fs from "fs"; +import { + ref, + uploadBytes, + deleteObject, + getDownloadURL, +} from "firebase/storage"; +import formidable from "formidable-serverless"; + +const db = getFirestore(app); + +const getPaymentField = (type: FilesStorage) => { + switch (type) { + case "commission": + return "commissionTransfer"; + case "corporate": + return "corporateTransfer"; + default: + return null; + } +}; + +const handleDelete = async ( + paymentId: string, + paymentField: "commissionTransfer" | "corporateTransfer" +) => { + const paymentRef = doc(db, "payments", paymentId); + const paymentDoc = await getDoc(paymentRef); + const { [paymentField]: paymentFieldPath } = paymentDoc.data() as Payment; + // Create a reference to the file to delete + const documentRef = ref(storage, paymentFieldPath); + await deleteObject(documentRef); + await updateDoc(paymentRef, { + [paymentField]: deleteField(), + }); +}; + +const handleUpload = async ( + req: NextApiRequest, + paymentId: string, + paymentField: "commissionTransfer" | "corporateTransfer" +) => + new Promise((resolve, reject) => { + const form = formidable({ keepExtensions: true }); + form.parse(req, async (err: any, fields: any, files: any) => { + if (err) { + reject(err); + return; + } + try { + const { file } = files; + const fileName = Date.now() + "-" + file.name; + const fileRef = ref(storage, fileName); + + const binary = fs.readFileSync(file.path).buffer; + const snapshot = await uploadBytes(fileRef, binary); + fs.rmSync(file.path); + + const paymentRef = doc(db, "payments", paymentId); + + await updateDoc(paymentRef, { + [paymentField]: snapshot.ref.fullPath, + }); + resolve(snapshot.ref.fullPath); + } catch (err) { + reject(err); + } + }); + }); + +export default withIronSessionApiRoute(handler, sessionOptions); +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + if (req.method === "GET") return await get(req, res); + if (req.method === "POST") return await post(req, res); + if (req.method === "DELETE") return await del(req, res); + if (req.method === "PATCH") return await patch(req, res); + + res.status(404).json(undefined); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + const { type, paymentId } = req.query as { + type: FilesStorage; + paymentId: string; + }; + const paymentField = getPaymentField(type); + + if (paymentField === null) { + res.status(500).json({ error: "Failed to identify payment field" }); + return; + } + const paymentRef = doc(db, "payments", paymentId); + const { [paymentField]: paymentFieldPath } = ( + await getDoc(paymentRef) + ).data() as Payment; + + // Create a reference to the file to delete + const documentRef = ref(storage, paymentFieldPath); + const url = await getDownloadURL(documentRef); + res.status(200).json({ url, name: documentRef.name }); +} + +async function post(req: NextApiRequest, res: NextApiResponse) { + const { type, paymentId } = req.query as { + type: FilesStorage; + paymentId: string; + }; + const paymentField = getPaymentField(type); + + if (paymentField === null) { + res.status(500).json({ error: "Failed to identify payment field" }); + return; + } + + try { + const ref = await handleUpload(req, paymentId, paymentField); + res.status(200).json({ ref }); + } catch (error) { + res.status(500).json({ error }); + } +} + +async function del(req: NextApiRequest, res: NextApiResponse) { + const { type, paymentId } = req.query as { + type: FilesStorage; + paymentId: string; + }; + const paymentField = getPaymentField(type); + if (paymentField === null) { + res.status(500).json({ error: "Failed to identify payment field" }); + return; + } + + try { + await handleDelete(paymentId, paymentField); + res.status(200).json({ ok: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to delete file" }); + } +} + +async function patch(req: NextApiRequest, res: NextApiResponse) { + const { type, paymentId } = req.query as { + type: FilesStorage; + paymentId: string; + }; + const paymentField = getPaymentField(type); + if (paymentField === null) { + res.status(500).json({ error: "Failed to identify payment field" }); + return; + } + + try { + await handleDelete(paymentId, paymentField); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to delete file" }); + return; + } + + try { + const ref = await handleUpload(req, paymentId, paymentField); + res.status(200).json({ ref }); + } catch (err) { + res.status(500).json({ error: "Failed to upload file" }); + } +} + +export const config = { + api: { + bodyParser: false, + }, +}; diff --git a/src/pages/api/speaking.ts b/src/pages/api/speaking.ts index d33e927f..b1b1b8bd 100644 --- a/src/pages/api/speaking.ts +++ b/src/pages/api/speaking.ts @@ -3,7 +3,7 @@ import type {NextApiRequest, NextApiResponse} from "next"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {getDownloadURL, getStorage, ref} from "firebase/storage"; -import {app} from "@/firebase"; +import {app, storage} from "@/firebase"; import axios from "axios"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -14,7 +14,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const storage = getStorage(app); const {path} = req.body as {path: string}; const pathReference = ref(storage, path); diff --git a/src/pages/api/users/update.ts b/src/pages/api/users/update.ts index 65bf3aa9..a4f80fc8 100644 --- a/src/pages/api/users/update.ts +++ b/src/pages/api/users/update.ts @@ -1,6 +1,6 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type {NextApiRequest, NextApiResponse} from "next"; -import {app} from "@/firebase"; +import {app, storage} from "@/firebase"; import {getFirestore, collection, getDocs, getDoc, doc, setDoc} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; @@ -10,7 +10,6 @@ import {getAuth, signInWithEmailAndPassword, updateEmail, updatePassword} from " import {errorMessages} from "@/constants/errors"; const db = getFirestore(app); -const storage = getStorage(app); const auth = getAuth(app); export default withIronSessionApiRoute(handler, sessionOptions); diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 70f094d7..cb622333 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -24,6 +24,7 @@ import Select from "react-select"; import Input from "@/components/Low/Input"; import ReactDatePicker from "react-datepicker"; import moment from "moment"; +import PaymentAssetManager from "@/components/PaymentAssetManager"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -305,6 +306,116 @@ export default function PaymentRecord() { .finally(reload); }; + const getFileAssetsColumns = () => { + if(user) { + const containerClassName = "flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer" + switch(user.type) { + case 'corporate': return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case 'agent': return [ + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + cell: (info) => ( +
+ +
+ ), + }), + ] + case 'admin': return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + cell: (info) => ( +
+ +
+ ), + }), + ] + case 'developer': return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + cell: (info) => ( +
+ +
+ ), + }), + ] + default: return []; + } + } + + return []; + } const defaultColumns = [ columnHelper.accessor("id", { header: "ID", @@ -365,6 +476,7 @@ export default function PaymentRecord() { ), }), + ...getFileAssetsColumns(), { header: "", id: "actions", From b77e97a9d2e62ed016c4f5493da377b28c6c0335 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Wed, 13 Dec 2023 23:39:22 +0000 Subject: [PATCH 2/4] Changed icon used for replacing files --- src/components/PaymentAssetManager.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PaymentAssetManager.tsx b/src/components/PaymentAssetManager.tsx index 25c66727..65516e9e 100644 --- a/src/components/PaymentAssetManager.tsx +++ b/src/components/PaymentAssetManager.tsx @@ -1,5 +1,5 @@ import React, { ChangeEvent } from "react"; -import { BsUpload, BsDownload, BsTrash, BsFilePdf } from "react-icons/bs"; +import { BsUpload, BsDownload, BsTrash, BsArrowRepeat } from "react-icons/bs"; import { FilesStorage } from "@/interfaces/storage.files"; import axios from "axios"; @@ -128,7 +128,7 @@ const PaymentAssetManager = (props: { return ( <> - fileInputReplaceRef.current?.click()} /> + fileInputReplaceRef.current?.click()} /> {renderFileInput( (e: Event) => handleFileChange(e, "patch"), From d57223bd012fd32be745b9b527d1f5279a39fbc6 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 14 Dec 2023 09:52:47 +0000 Subject: [PATCH 3/4] Added a Fixed decimal point for the payment records --- src/pages/payment-record.tsx | 205 +++++++++++++++++------------------ 1 file changed, 98 insertions(+), 107 deletions(-) diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index cb622333..908f3ca0 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -307,115 +307,106 @@ export default function PaymentRecord() { }; const getFileAssetsColumns = () => { - if(user) { - const containerClassName = "flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer" - switch(user.type) { - case 'corporate': return [ - columnHelper.accessor("corporateTransfer", { - header: "Corporate transfer", - cell: (info) => ( -
- -
- ), - }), - ]; - case 'agent': return [ - columnHelper.accessor("commissionTransfer", { - header: "Commission transfer", - cell: (info) => ( -
- -
- ), - }), - ] - case 'admin': return [ - columnHelper.accessor("corporateTransfer", { - header: "Corporate transfer", - cell: (info) => ( -
- -
- ), - }), - columnHelper.accessor("commissionTransfer", { - header: "Commission transfer", - cell: (info) => ( -
- -
- ), - }), - ] - case 'developer': return [ - columnHelper.accessor("corporateTransfer", { - header: "Corporate transfer", - cell: (info) => ( -
- -
- ), - }), - columnHelper.accessor("commissionTransfer", { - header: "Commission transfer", - cell: (info) => ( -
- -
- ), - }), - ] - default: return []; + if (user) { + const containerClassName = "flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer"; + switch (user.type) { + case "corporate": + return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case "agent": + return [ + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case "admin": + return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case "developer": + return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + default: + return []; } } return []; - } + }; const defaultColumns = [ columnHelper.accessor("id", { header: "ID", @@ -440,7 +431,7 @@ export default function PaymentRecord() { header: "Amount", cell: (info) => ( - {info.getValue()} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label} + {info.getValue().toFixed(2)} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label} ), }), @@ -462,7 +453,7 @@ export default function PaymentRecord() { header: "Commission Value", cell: (info) => ( - {info.getValue()} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label} + {info.getValue().toFixed(2)} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label} ), }), From 45cf2dc2796336aa7bd76435cdabfa515b19169c Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Thu, 14 Dec 2023 17:20:36 +0000 Subject: [PATCH 4/4] Added a number asset to limit to a specific number of decimal cases if needed --- src/pages/api/users/update.ts | 4 ++-- src/pages/payment-record.tsx | 7 ++++--- src/utils/number.ts | 13 +++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 src/utils/number.ts diff --git a/src/pages/api/users/update.ts b/src/pages/api/users/update.ts index 315f84a1..7b6ea5f3 100644 --- a/src/pages/api/users/update.ts +++ b/src/pages/api/users/update.ts @@ -11,7 +11,7 @@ import {errorMessages} from "@/constants/errors"; import moment from "moment"; import ShortUniqueId from "short-unique-id"; import {Payment} from "@/interfaces/paypal"; - +import { toFixedNumber } from "@/utils/number"; const db = getFirestore(app); const auth = getAuth(app); @@ -32,7 +32,7 @@ const managePaymentRecords = async (user: User, userId: string | undefined): Pro corporate: userId, agent: user.corporateInformation.referralAgent, agentCommission: user.corporateInformation.payment!.commission, - agentValue: (user.corporateInformation.payment!.commission / 100) * user.corporateInformation.payment!.value, + agentValue: toFixedNumber((user.corporateInformation.payment!.commission / 100) * user.corporateInformation.payment!.value, 2), currency: user.corporateInformation.payment!.currency, value: user.corporateInformation.payment!.value, isPaid: false, diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 908f3ca0..24c99d6f 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -25,6 +25,7 @@ import Input from "@/components/Low/Input"; import ReactDatePicker from "react-datepicker"; import moment from "moment"; import PaymentAssetManager from "@/components/PaymentAssetManager"; +import { toFixedNumber } from "@/utils/number"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -89,7 +90,7 @@ const PaymentCreator = ({onClose, reload}: {onClose: () => void; reload: () => v corporate: corporate?.id, agent: referralAgent?.id, agentCommission: commission, - agentValue: (commission / 100) * price, + agentValue: toFixedNumber((commission / 100) * price, 2), currency, value: price, isPaid: false, @@ -431,7 +432,7 @@ export default function PaymentRecord() { header: "Amount", cell: (info) => ( - {info.getValue().toFixed(2)} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label} + {toFixedNumber(info.getValue(), 2)} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label} ), }), @@ -453,7 +454,7 @@ export default function PaymentRecord() { header: "Commission Value", cell: (info) => ( - {info.getValue().toFixed(2)} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label} + {toFixedNumber(info.getValue(), 2)} {CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label} ), }), diff --git a/src/utils/number.ts b/src/utils/number.ts new file mode 100644 index 00000000..47f536b7 --- /dev/null +++ b/src/utils/number.ts @@ -0,0 +1,13 @@ +export function isDecimal(num: number) { + return num % 1 !== 0; + } + +export function toFixedNumber(num: number, decimals: number = 2) { + // Rounds to 2 decimal places + if(isDecimal(num)) { + const multiplier = Math.pow(10, decimals); + return Math.round(num * multiplier) / multiplier; + } + + return num; +}