Make public invoices RTL for Farsi businesses and fix invoice back-arrow direction.

Also add optional English name fields on the add-customer modal to match the API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-11 15:46:18 +03:30
co-authored by Cursor
parent a50685ec69
commit f21041b950
9 changed files with 231 additions and 44 deletions
@@ -222,6 +222,7 @@
font-weight: 600;
color: var(--text-primary, #0f172a);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.priceWas {
@@ -257,12 +258,32 @@
.keyPoints {
margin: 0;
padding-left: 16px;
padding-inline-start: 16px;
font-size: 12px;
line-height: 1.45;
color: var(--text-secondary, #475569);
}
.page[dir='rtl'] {
font-family: var(--font-ui, var(--font-en), var(--font-fa), sans-serif);
}
.page[dir='rtl'] .title,
.page[dir='rtl'] .itemTitle,
.page[dir='rtl'] .sectionTitle,
.page[dir='rtl'] .accountBank,
.page[dir='rtl'] .topText,
.page[dir='rtl'] .keyPoints,
.page[dir='rtl'] .meta {
font-family: var(--font-ui, var(--font-en), var(--font-fa), sans-serif);
}
.page[dir='rtl'] .itemPrice,
.page[dir='rtl'] .totals strong,
.page[dir='rtl'] .accountDetails {
font-variant-numeric: tabular-nums;
}
.accounts {
display: flex;
flex-direction: column;
+103 -27
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import { FileDown } from 'lucide-react'
import meshkeeLogo from '../assets/meshkee-logo.png'
@@ -10,10 +10,65 @@ import type { PublicInvoice } from '../types/invoice'
import { formatIrtPrice } from '../utils/irtPrice'
import styles from './PublicInvoicePage.module.css'
function formatDate(value: string) {
type InvoiceLocale = 'en' | 'fa'
const COPY: Record<
InvoiceLocale,
{
loading: string
loadError: string
issued: string
items: string
subtotal: string
total: string
keyPoints: string
accounts: string
card: string
iban: string
issuedBy: string
printHint: string
print: string
invoiceFallback: string
}
> = {
en: {
loading: 'Loading invoice…',
loadError: 'Unable to load invoice.',
issued: 'Issued',
items: 'Items',
subtotal: 'Subtotal',
total: 'Total',
keyPoints: 'Key points',
accounts: 'Account numbers',
card: 'Card',
iban: 'IBAN',
issuedBy: 'Issued by',
printHint: 'Save as PDF from the print dialog (Destination → Save as PDF).',
print: 'Print to PDF',
invoiceFallback: 'Invoice',
},
fa: {
loading: 'در حال بارگذاری فاکتور…',
loadError: 'بارگذاری فاکتور ممکن نشد.',
issued: 'صادر شده',
items: 'آیتم‌ها',
subtotal: 'جمع جزء',
total: 'مبلغ کل',
keyPoints: 'نکات کلیدی',
accounts: 'شماره حساب‌ها',
card: 'کارت',
iban: 'شبا',
issuedBy: 'صادر شده توسط',
printHint: 'از پنجره چاپ، مقصد را روی «ذخیره به‌صورت PDF» بگذارید.',
print: 'چاپ / ذخیره PDF',
invoiceFallback: 'فاکتور',
},
}
function formatDate(value: string, locale: InvoiceLocale) {
const d = new Date(value)
if (Number.isNaN(d.getTime())) return value
return d.toLocaleDateString('en-US', {
return d.toLocaleDateString(locale === 'fa' ? 'fa-IR' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
@@ -27,6 +82,21 @@ export function PublicInvoicePage() {
const [error, setError] = useState('')
const marketingUrl = getPlatformMarketingUrl()
const locale: InvoiceLocale = invoice?.locale === 'en' ? 'en' : 'fa'
const isRtl = locale === 'fa'
const t = COPY[locale]
const businessLabel = useMemo(() => {
if (!invoice?.business) return ''
return (
invoice.business.displayName?.trim() ||
(locale === 'fa'
? invoice.business.nameFa?.trim() || invoice.business.name
: invoice.business.name) ||
''
)
}, [invoice, locale])
useEffect(() => {
if (!invoiceId) return
const controller = new AbortController()
@@ -36,7 +106,7 @@ export function PublicInvoicePage() {
.then((data) => setInvoice(data))
.catch((err) => {
if (isAbortError(err)) return
setError(err instanceof ApiError ? err.message : 'Unable to load invoice.')
setError(err instanceof ApiError ? err.message : COPY.fa.loadError)
setInvoice(null)
})
.finally(() => setLoading(false))
@@ -46,29 +116,33 @@ export function PublicInvoicePage() {
useEffect(() => {
if (!invoice) return
const previous = document.title
document.title = invoice.name?.trim() || `Invoice ${invoice.publicId}`
document.title =
invoice.name?.trim() ||
`${t.invoiceFallback} ${invoice.publicId}`
return () => {
document.title = previous
}
}, [invoice])
}, [invoice, t.invoiceFallback])
function handlePrint() {
window.print()
}
return (
<div className={styles.page} lang="en">
<div className={styles.page} lang={locale} dir={isRtl ? 'rtl' : 'ltr'}>
<div className={styles.sheet}>
{loading ? <p className={styles.muted}>Loading invoice</p> : null}
{loading ? <p className={styles.muted}>{t.loading}</p> : null}
{error ? <p className={styles.error}>{error}</p> : null}
{invoice ? (
<>
<header className={styles.header}>
<h1 className={styles.title}>{invoice.name || `Invoice ${invoice.publicId}`}</h1>
<h1 className={styles.title}>
{invoice.name || `${t.invoiceFallback} ${invoice.publicId}`}
</h1>
<p className={styles.meta}>
Issued {formatDate(invoice.issuedAt)}
{invoice.business?.name ? ` · ${invoice.business.name}` : ''}
{t.issued} {formatDate(invoice.issuedAt, locale)}
{businessLabel ? ` · ${businessLabel}` : ''}
</p>
</header>
@@ -80,7 +154,7 @@ export function PublicInvoicePage() {
) : null}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Items</h2>
<h2 className={styles.sectionTitle}>{t.items}</h2>
<div className={styles.items}>
{(invoice.items ?? []).map((item) => {
const effective =
@@ -115,11 +189,11 @@ export function PublicInvoicePage() {
</div>
<div className={styles.totals}>
<div>
<span>Subtotal</span>
<span>{t.subtotal}</span>
<strong>{formatIrtPrice(invoice.subtotal ?? 0)}</strong>
</div>
<div className={styles.totalRow}>
<span>Total</span>
<span>{t.total}</span>
<strong>{formatIrtPrice(invoice.total ?? 0)}</strong>
</div>
</div>
@@ -127,7 +201,7 @@ export function PublicInvoicePage() {
{(invoice.keyPoints?.length ?? 0) > 0 ? (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Key points</h2>
<h2 className={styles.sectionTitle}>{t.keyPoints}</h2>
<ul className={styles.keyPoints}>
{invoice.keyPoints!.map((kp) => (
<li key={kp.id}>{kp.text}</li>
@@ -138,19 +212,23 @@ export function PublicInvoicePage() {
{(invoice.accounts?.length ?? 0) > 0 ? (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Account numbers</h2>
<h2 className={styles.sectionTitle}>{t.accounts}</h2>
<div className={styles.accounts}>
{invoice.accounts!.map((acc) => {
const details = [
acc.accountHolderName,
acc.cardNumber ? `Card: ${acc.cardNumber}` : null,
acc.iban ? `IBAN: ${acc.iban}` : null,
const numberBits = [
acc.cardNumber ? `${t.card}: ${acc.cardNumber}` : null,
acc.iban ? `${t.iban}: ${acc.iban}` : null,
].filter(Boolean)
return (
<div key={acc.id} className={styles.account}>
<strong className={styles.accountBank}>{acc.bankName}</strong>
{details.length > 0 ? (
<p className={styles.accountDetails}>{details.join(' · ')}</p>
{acc.accountHolderName ? (
<p className={styles.accountDetails}>{acc.accountHolderName}</p>
) : null}
{numberBits.length > 0 ? (
<p className={styles.accountDetails} dir="ltr">
{numberBits.join(' · ')}
</p>
) : null}
</div>
)
@@ -167,7 +245,7 @@ export function PublicInvoicePage() {
rel="noreferrer"
>
<img src={meshkeeLogo} alt="" className={styles.footerLogo} />
<span className={styles.footerLabel}>Issued by</span>
<span className={styles.footerLabel}>{t.issuedBy}</span>
<span className={styles.footerName}>Meshkee E-Commerce Group</span>
</a>
</footer>
@@ -177,12 +255,10 @@ export function PublicInvoicePage() {
{invoice ? (
<div className={styles.toolbar}>
<p className={styles.toolbarHint}>
Save as PDF from the print dialog (Destination Save as PDF).
</p>
<p className={styles.toolbarHint}>{t.printHint}</p>
<button type="button" className={styles.printBtn} onClick={handlePrint}>
<FileDown size={16} aria-hidden />
Print to PDF
{t.print}
</button>
</div>
) : null}
+5 -1
View File
@@ -91,7 +91,11 @@ export type PublicInvoice = {
name: string | null
topText: string | null
issuedAt: string
business?: Invoice['business']
/** Business dashboard default locale — drives public invoice RTL/LTR. */
locale?: 'en' | 'fa'
business?: Invoice['business'] & {
displayName?: string
}
user?: {
firstName: string | null
lastName: string | null