diff --git a/frontend/src/components/InvoicePreview.jsx b/frontend/src/components/InvoicePreview.jsx index 3e7e895d..62b1a9f6 100644 --- a/frontend/src/components/InvoicePreview.jsx +++ b/frontend/src/components/InvoicePreview.jsx @@ -38,6 +38,7 @@ import PaidIcon from "@mui/icons-material/CheckCircle"; import UnpaidIcon from "@mui/icons-material/Pending"; import CancelIcon from "@mui/icons-material/Cancel"; import CurrencyExchangeIcon from "@mui/icons-material/CurrencyExchange"; +import { formatInvoiceDate } from "../utils/formatDate"; const InvoicePreview = ({ invoice, @@ -293,11 +294,7 @@ const InvoicePreview = ({ Issued: - {new Date(invoice.issueDate).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - })} + {formatInvoiceDate(invoice.issueDate)}
@@ -305,11 +302,7 @@ const InvoicePreview = ({ Due: - {new Date(invoice.dueDate).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - })} + {formatInvoiceDate(invoice.dueDate)}
diff --git a/frontend/src/page/BatchPayment.jsx b/frontend/src/page/BatchPayment.jsx index 6982df09..8326dc19 100644 --- a/frontend/src/page/BatchPayment.jsx +++ b/frontend/src/page/BatchPayment.jsx @@ -8,6 +8,7 @@ import html2canvas from "html2canvas"; import { LitNodeClient } from "@lit-protocol/lit-node-client"; import { decryptToString } from "@lit-protocol/encryption/src/lib/encryption.js"; import { LIT_ABILITY, LIT_NETWORK } from "@lit-protocol/constants"; +import { formatInvoiceDate, formatDateTime } from "../utils/formatDate"; import { createSiweMessageWithRecaps, generateAuthSig, @@ -822,11 +823,6 @@ function BatchPayment() { )}`; }; - const formatDate = (issueDate) => { - const date = new Date(issueDate); - return date.toLocaleString(); - }; - const unpaidInvoices = receivedInvoices.filter( (inv) => !inv.isPaid && !inv.isCancelled ); @@ -1186,7 +1182,7 @@ function BatchPayment() { )} - {formatDate(invoice.issueDate)} + {formatInvoiceDate(invoice.issueDate)}
@@ -1482,16 +1478,10 @@ function BatchPayment() {
- Issued:{" "} - {new Date( - drawerState.selectedInvoice.issueDate - ).toLocaleDateString()} + Issued: {formatInvoiceDate(drawerState.selectedInvoice.issueDate)} - Due:{" "} - {new Date( - drawerState.selectedInvoice.dueDate - ).toLocaleDateString()} + Due: {formatInvoiceDate(drawerState.selectedInvoice.dueDate)}
diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 1bac242f..f7b9a720 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -57,6 +57,7 @@ import CloseIcon from "@mui/icons-material/Close"; import ErrorIcon from "@mui/icons-material/Error"; import { useTokenList } from "@/hooks/useTokenList"; import WalletConnectionAlert from "@/components/WalletConnectionAlert"; +import { formatInvoiceDate, formatDateTime } from "@/utils/formatDate"; const columns = [ { id: "select", label: "", minWidth: 50 }, @@ -883,11 +884,6 @@ function ReceivedInvoice() { )}`; }; - const formatDate = (issueDate) => { - const date = new Date(issueDate); - return date.toLocaleString(); - }; - const unpaidInvoices = receivedInvoices.filter( (inv) => !inv.isPaid && !inv.isCancelled ); @@ -1404,7 +1400,7 @@ function ReceivedInvoice() { - {formatDate(invoice.issueDate)} + {formatDateTime(invoice.issueDate)} @@ -1780,16 +1776,10 @@ function ReceivedInvoice() {
- Issued:{" "} - {new Date( - drawerState.selectedInvoice.issueDate - ).toLocaleDateString()} + Issued: {formatInvoiceDate(drawerState.selectedInvoice.issueDate)} - Due:{" "} - {new Date( - drawerState.selectedInvoice.dueDate - ).toLocaleDateString()} + Due: {formatInvoiceDate(drawerState.selectedInvoice.dueDate)}
diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index b953713f..a104d485 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -47,6 +47,7 @@ import CancelIcon from "@mui/icons-material/Cancel"; import CurrencyExchangeIcon from "@mui/icons-material/CurrencyExchange"; import { useTokenList } from "@/hooks/useTokenList"; import WalletConnectionAlert from "@/components/WalletConnectionAlert"; +import { formatInvoiceDate, formatDateTime } from "@/utils/formatDate"; const columns = [ { id: "fname", label: "Client", minWidth: 120 }, @@ -417,11 +418,6 @@ function SentInvoice() { )}`; }; - const formatDate = (issueDate) => { - const date = new Date(issueDate); - return date.toLocaleString(); - }; - return ( <>
@@ -613,7 +609,7 @@ function SentInvoice() { {/* Date Column */} - {formatDate(invoice.issueDate)} + {formatInvoiceDate(invoice.issueDate)} @@ -922,16 +918,10 @@ function SentInvoice() {
- Issued:{" "} - {new Date( - drawerState.selectedInvoice.issueDate - ).toLocaleDateString()} + Issued: {formatInvoiceDate(drawerState.selectedInvoice.issueDate)} - Due:{" "} - {new Date( - drawerState.selectedInvoice.dueDate - ).toLocaleDateString()} + Due: {formatInvoiceDate(drawerState.selectedInvoice.dueDate)}
diff --git a/frontend/src/utils/formatDate.js b/frontend/src/utils/formatDate.js new file mode 100644 index 00000000..b4ce1cc2 --- /dev/null +++ b/frontend/src/utils/formatDate.js @@ -0,0 +1,68 @@ +/** + * Returns the user's local UTC offset string, e.g. "UTC+5:30" or "UTC-8:00" + */ +function getUTCOffset() { + const offset = -new Date().getTimezoneOffset(); // minutes, sign flipped + const sign = offset >= 0 ? "+" : "-"; + const abs = Math.abs(offset); + const hours = Math.floor(abs / 60); + const minutes = abs % 60; + return `UTC${sign}${hours}:${minutes.toString().padStart(2, "0")}`; +} + +/** + * Formats invoice issue/due dates (date only, no time). + * Output: "Feb 20, 2026 (UTC+5:30)" + * + * Why: new Date(isoString).toLocaleDateString() silently shifts the date + * across midnight for users in negative UTC offsets. Rendering the calendar + * date only keeps financial document dates stable across viewers. + * + * @param {string|Date} dateStr + * @returns {string} ++ */ +export function formatInvoiceDate(dateStr) { + if (!dateStr) return "N/A"; + // Strip time component to avoid UTC→local midnight shift + // "2026-02-20T..." or "2026-02-20" → ["2026","02","20"] + const parts = String(dateStr).split("T")[0].split("-"); + if (parts.length === 3) { + const [year, month, day] = parts.map(Number); + const date = new Date(year, month - 1, day); // local date, no TZ conversion + if (isNaN(date.getTime())) return "Invalid date"; + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + // Fallback for non-ISO formats + const date = new Date(dateStr); + if (isNaN(date.getTime())) return "Invalid date"; + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + + /** + * Formats a full timestamp (date + time) for table rows. + * * Output: "Feb 20, 2026" + * + * @param {string|Date} dateStr + * @returns {string} + */ + export function formatDateTime(dateStr) { + if (!dateStr) return "N/A"; + const date = new Date(dateStr); + if (isNaN(date.getTime())) return "Invalid date"; + const formatted = date.toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + return `${formatted} (${getUTCOffset()})`; + } +} diff --git a/frontend/src/utils/generateInvoicePDF.js b/frontend/src/utils/generateInvoicePDF.js index 52a0a14d..c32b78a0 100644 --- a/frontend/src/utils/generateInvoicePDF.js +++ b/frontend/src/utils/generateInvoicePDF.js @@ -1,6 +1,8 @@ import jsPDF from "jspdf"; import { ethers } from "ethers"; import { getWagmiChainName, getWagmiChainInfo } from "./wagmiChainHelpers"; +import { formatInvoiceDate } from "./formatDate"; + /** * Load logo image with multiple fallback methods @@ -369,16 +371,8 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { pdf.setTextColor(...darkGray); pdf.setFontSize(9); pdf.setFont("helvetica", "normal"); - const issueDate = new Date(invoice.issueDate).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - const dueDate = new Date(invoice.dueDate).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); + const issueDate = formatInvoiceDate(invoice.issueDate); + const dueDate = formatInvoiceDate(invoice.dueDate); pdf.text(`Issued: ${issueDate}`, 25, yPos + 5.5); pdf.text(`Due: ${dueDate}`, 160, yPos + 5.5);