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
@@ -1,3 +1,7 @@
.wideModal {
max-width: min(800px, calc(100vw - 32px));
}
.formGrid { .formGrid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@@ -8,15 +12,29 @@
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.hint { .keyPoints {
margin-bottom: 12px; margin: 0 0 14px;
font-size: 12px; padding-inline-start: 1.15em;
line-height: 1.45; list-style: disc;
font-size: 12.5px;
line-height: 1.5;
color: var(--text-secondary); color: var(--text-secondary);
} }
@media (max-width: 520px) { .keyPoints li {
white-space: nowrap;
}
.keyPoints li + li {
margin-top: 4px;
}
@media (max-width: 640px) {
.formGrid { .formGrid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.keyPoints li {
white-space: normal;
}
} }
@@ -28,6 +28,8 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP
const [closing, setClosing] = useState(false) const [closing, setClosing] = useState(false)
const [firstName, setFirstName] = useState('') const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('') const [lastName, setLastName] = useState('')
const [firstNameEn, setFirstNameEn] = useState('')
const [lastNameEn, setLastNameEn] = useState('')
const [cellNumber, setCellNumber] = useState('') const [cellNumber, setCellNumber] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [passwordConfirm, setPasswordConfirm] = useState('') const [passwordConfirm, setPasswordConfirm] = useState('')
@@ -40,6 +42,8 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP
setClosing(false) setClosing(false)
setFirstName('') setFirstName('')
setLastName('') setLastName('')
setFirstNameEn('')
setLastNameEn('')
setCellNumber('') setCellNumber('')
setPassword('') setPassword('')
setPasswordConfirm('') setPasswordConfirm('')
@@ -86,6 +90,16 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP
} }
} }
const trimmedFirstEn = firstNameEn.trim()
const trimmedLastEn = lastNameEn.trim()
if (
(trimmedFirstEn && trimmedFirstEn.length < 2) ||
(trimmedLastEn && trimmedLastEn.length < 2)
) {
setError(t('customers.addModal.errorNameEnLength'))
return
}
setIsSubmitting(true) setIsSubmitting(true)
setError('') setError('')
try { try {
@@ -93,6 +107,8 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP
cellNumber: normalizedCell, cellNumber: normalizedCell,
firstName: firstName.trim(), firstName: firstName.trim(),
lastName: lastName.trim(), lastName: lastName.trim(),
...(trimmedFirstEn ? { firstNameEn: trimmedFirstEn } : {}),
...(trimmedLastEn ? { lastNameEn: trimmedLastEn } : {}),
...(trimmedPassword ? { password: trimmedPassword } : {}), ...(trimmedPassword ? { password: trimmedPassword } : {}),
}) })
onCreated({ onCreated({
@@ -119,7 +135,7 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP
onClick={onClose} onClick={onClose}
> >
<div <div
className={`${modalStyles.modal} ${closing ? modalStyles.modalOut : modalStyles.modalIn}`} className={`${modalStyles.modal} ${formStyles.wideModal} ${closing ? modalStyles.modalOut : modalStyles.modalIn}`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
@@ -132,7 +148,6 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP
<h3 id="add-customer-title" className={modalStyles.title}> <h3 id="add-customer-title" className={modalStyles.title}>
{t('customers.addModal.title')} {t('customers.addModal.title')}
</h3> </h3>
<p className={modalStyles.subtitle}>{t('customers.addModal.subtitle')}</p>
</div> </div>
<button <button
type="button" type="button"
@@ -145,29 +160,58 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP
</div> </div>
<div className={modalStyles.body}> <div className={modalStyles.body}>
<p className={formStyles.hint}>{t('customers.addModal.hint')}</p> <ul className={formStyles.keyPoints}>
<li>{t('customers.addModal.pointCreate')}</li>
<li>{t('customers.addModal.pointPassword')}</li>
</ul>
{error && <p className={modalStyles.errorText}>{error}</p>} {error && <p className={modalStyles.errorText}>{error}</p>}
<div className={formStyles.formGrid}> <div className={formStyles.formGrid}>
<div className={modalStyles.field}> <div className={modalStyles.field}>
<label htmlFor="add-customer-first-name">{t('customers.addModal.firstName')}</label> <label htmlFor="add-customer-first-name">{t('customers.addModal.firstNameFa')}</label>
<input <input
id="add-customer-first-name" id="add-customer-first-name"
value={firstName} value={firstName}
onChange={(e) => setFirstName(e.target.value)} onChange={(e) => setFirstName(e.target.value)}
disabled={isSubmitting} disabled={isSubmitting}
dir={isFa ? 'rtl' : 'ltr'} dir="rtl"
lang="fa"
className="faText"
/> />
</div> </div>
<div className={modalStyles.field}> <div className={modalStyles.field}>
<label htmlFor="add-customer-last-name">{t('customers.addModal.lastName')}</label> <label htmlFor="add-customer-last-name">{t('customers.addModal.lastNameFa')}</label>
<input <input
id="add-customer-last-name" id="add-customer-last-name"
value={lastName} value={lastName}
onChange={(e) => setLastName(e.target.value)} onChange={(e) => setLastName(e.target.value)}
disabled={isSubmitting} disabled={isSubmitting}
dir={isFa ? 'rtl' : 'ltr'} dir="rtl"
lang="fa"
className="faText"
/>
</div>
<div className={modalStyles.field}>
<label htmlFor="add-customer-first-name-en">{t('customers.addModal.firstNameEn')}</label>
<input
id="add-customer-first-name-en"
value={firstNameEn}
onChange={(e) => setFirstNameEn(e.target.value)}
disabled={isSubmitting}
dir="ltr"
lang="en"
/>
</div>
<div className={modalStyles.field}>
<label htmlFor="add-customer-last-name-en">{t('customers.addModal.lastNameEn')}</label>
<input
id="add-customer-last-name-en"
value={lastNameEn}
onChange={(e) => setLastNameEn(e.target.value)}
disabled={isSubmitting}
dir="ltr"
lang="en"
/> />
</div> </div>
<div className={`${modalStyles.field} ${formStyles.formFull}`}> <div className={`${modalStyles.field} ${formStyles.formFull}`}>
@@ -233,8 +277,7 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP
</div> </div>
</div> </div>
</div> </div>
</div> </div>,
, document.body,
document.body,
) )
} }
@@ -192,6 +192,10 @@
text-decoration: underline; text-decoration: underline;
} }
:global([dir='rtl']) .backLink svg {
transform: scaleX(-1);
}
.section { .section {
padding: 16px; padding: 16px;
background: var(--glass-bg); background: var(--glass-bg);
+18
View File
@@ -833,8 +833,16 @@ const en = {
'Creates a verified customer account or links an existing user to your business.', 'Creates a verified customer account or links an existing user to your business.',
'customers.addModal.hint': 'customers.addModal.hint':
'Password is required only for new accounts. Existing users are added as verified customers.', 'Password is required only for new accounts. Existing users are added as verified customers.',
'customers.addModal.pointCreate':
'Creates a verified customer account or links an existing user to your business.',
'customers.addModal.pointPassword':
'Password is required only for new accounts. Existing users are added as verified customers.',
'customers.addModal.firstName': 'First name', 'customers.addModal.firstName': 'First name',
'customers.addModal.lastName': 'Last name', 'customers.addModal.lastName': 'Last name',
'customers.addModal.firstNameFa': 'First name (FA)',
'customers.addModal.lastNameFa': 'Last name (FA)',
'customers.addModal.firstNameEn': 'First name (EN)',
'customers.addModal.lastNameEn': 'Last name (EN)',
'customers.addModal.cell': 'Cell number', 'customers.addModal.cell': 'Cell number',
'customers.addModal.cellPlaceholder': '0912...', 'customers.addModal.cellPlaceholder': '0912...',
'customers.addModal.password': 'Password (new users)', 'customers.addModal.password': 'Password (new users)',
@@ -846,6 +854,7 @@ const en = {
'customers.addModal.error': 'Unable to add customer.', 'customers.addModal.error': 'Unable to add customer.',
'customers.addModal.errorPasswordMatch': 'Passwords do not match.', 'customers.addModal.errorPasswordMatch': 'Passwords do not match.',
'customers.addModal.errorPasswordLength': 'Password must be at least 8 characters.', 'customers.addModal.errorPasswordLength': 'Password must be at least 8 characters.',
'customers.addModal.errorNameEnLength': 'English names must be at least 2 characters.',
'customers.access.title': 'Change access', 'customers.access.title': 'Change access',
'customers.access.subtitle': 'Choose customer or manager access for {name}.', 'customers.access.subtitle': 'Choose customer or manager access for {name}.',
'customers.access.customer': 'Customer', 'customers.access.customer': 'Customer',
@@ -2434,8 +2443,16 @@ const fa: Record<MessageKey, string> = {
'یک حساب تأییدشده مشتری می‌سازد یا کاربر موجود را به کسب‌وکار شما وصل می‌کند.', 'یک حساب تأییدشده مشتری می‌سازد یا کاربر موجود را به کسب‌وکار شما وصل می‌کند.',
'customers.addModal.hint': 'customers.addModal.hint':
'رمز عبور فقط برای حساب‌های جدید لازم است. کاربران موجود به‌عنوان مشتری تأییدشده اضافه می‌شوند.', 'رمز عبور فقط برای حساب‌های جدید لازم است. کاربران موجود به‌عنوان مشتری تأییدشده اضافه می‌شوند.',
'customers.addModal.pointCreate':
'یک حساب تأییدشده مشتری می‌سازد یا کاربر موجود را به کسب‌وکار شما وصل می‌کند.',
'customers.addModal.pointPassword':
'رمز عبور فقط برای حساب‌های جدید لازم است. کاربران موجود به‌عنوان مشتری تأییدشده اضافه می‌شوند.',
'customers.addModal.firstName': 'نام', 'customers.addModal.firstName': 'نام',
'customers.addModal.lastName': 'نام خانوادگی', 'customers.addModal.lastName': 'نام خانوادگی',
'customers.addModal.firstNameFa': 'نام (فارسی)',
'customers.addModal.lastNameFa': 'نام خانوادگی (فارسی)',
'customers.addModal.firstNameEn': 'نام (انگلیسی)',
'customers.addModal.lastNameEn': 'نام خانوادگی (انگلیسی)',
'customers.addModal.cell': 'شماره موبایل', 'customers.addModal.cell': 'شماره موبایل',
'customers.addModal.cellPlaceholder': '۰۹۱۲...', 'customers.addModal.cellPlaceholder': '۰۹۱۲...',
'customers.addModal.password': 'رمز عبور (کاربران جدید)', 'customers.addModal.password': 'رمز عبور (کاربران جدید)',
@@ -2447,6 +2464,7 @@ const fa: Record<MessageKey, string> = {
'customers.addModal.error': 'افزودن مشتری ممکن نشد.', 'customers.addModal.error': 'افزودن مشتری ممکن نشد.',
'customers.addModal.errorPasswordMatch': 'رمزهای عبور یکسان نیستند.', 'customers.addModal.errorPasswordMatch': 'رمزهای عبور یکسان نیستند.',
'customers.addModal.errorPasswordLength': 'رمز عبور باید حداقل ۸ کاراکتر باشد.', 'customers.addModal.errorPasswordLength': 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
'customers.addModal.errorNameEnLength': 'نام انگلیسی باید حداقل ۲ کاراکتر باشد.',
'customers.access.title': 'تغییر دسترسی', 'customers.access.title': 'تغییر دسترسی',
'customers.access.subtitle': 'دسترسی مشتری یا مدیر را برای {name} انتخاب کنید.', 'customers.access.subtitle': 'دسترسی مشتری یا مدیر را برای {name} انتخاب کنید.',
'customers.access.customer': 'مشتری', 'customers.access.customer': 'مشتری',
@@ -76,6 +76,8 @@ export interface CreateCustomerPayload {
cellNumber: string cellNumber: string
firstName: string firstName: string
lastName: string lastName: string
firstNameEn?: string
lastNameEn?: string
password?: string password?: string
email?: string email?: string
} }
@@ -222,6 +222,7 @@
font-weight: 600; font-weight: 600;
color: var(--text-primary, #0f172a); color: var(--text-primary, #0f172a);
white-space: nowrap; white-space: nowrap;
font-variant-numeric: tabular-nums;
} }
.priceWas { .priceWas {
@@ -257,12 +258,32 @@
.keyPoints { .keyPoints {
margin: 0; margin: 0;
padding-left: 16px; padding-inline-start: 16px;
font-size: 12px; font-size: 12px;
line-height: 1.45; line-height: 1.45;
color: var(--text-secondary, #475569); 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 { .accounts {
display: flex; display: flex;
flex-direction: column; 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 { useParams } from 'react-router-dom'
import { FileDown } from 'lucide-react' import { FileDown } from 'lucide-react'
import meshkeeLogo from '../assets/meshkee-logo.png' import meshkeeLogo from '../assets/meshkee-logo.png'
@@ -10,10 +10,65 @@ import type { PublicInvoice } from '../types/invoice'
import { formatIrtPrice } from '../utils/irtPrice' import { formatIrtPrice } from '../utils/irtPrice'
import styles from './PublicInvoicePage.module.css' 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) const d = new Date(value)
if (Number.isNaN(d.getTime())) return value if (Number.isNaN(d.getTime())) return value
return d.toLocaleDateString('en-US', { return d.toLocaleDateString(locale === 'fa' ? 'fa-IR' : 'en-US', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
@@ -27,6 +82,21 @@ export function PublicInvoicePage() {
const [error, setError] = useState('') const [error, setError] = useState('')
const marketingUrl = getPlatformMarketingUrl() 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(() => { useEffect(() => {
if (!invoiceId) return if (!invoiceId) return
const controller = new AbortController() const controller = new AbortController()
@@ -36,7 +106,7 @@ export function PublicInvoicePage() {
.then((data) => setInvoice(data)) .then((data) => setInvoice(data))
.catch((err) => { .catch((err) => {
if (isAbortError(err)) return 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) setInvoice(null)
}) })
.finally(() => setLoading(false)) .finally(() => setLoading(false))
@@ -46,29 +116,33 @@ export function PublicInvoicePage() {
useEffect(() => { useEffect(() => {
if (!invoice) return if (!invoice) return
const previous = document.title const previous = document.title
document.title = invoice.name?.trim() || `Invoice ${invoice.publicId}` document.title =
invoice.name?.trim() ||
`${t.invoiceFallback} ${invoice.publicId}`
return () => { return () => {
document.title = previous document.title = previous
} }
}, [invoice]) }, [invoice, t.invoiceFallback])
function handlePrint() { function handlePrint() {
window.print() window.print()
} }
return ( return (
<div className={styles.page} lang="en"> <div className={styles.page} lang={locale} dir={isRtl ? 'rtl' : 'ltr'}>
<div className={styles.sheet}> <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} {error ? <p className={styles.error}>{error}</p> : null}
{invoice ? ( {invoice ? (
<> <>
<header className={styles.header}> <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}> <p className={styles.meta}>
Issued {formatDate(invoice.issuedAt)} {t.issued} {formatDate(invoice.issuedAt, locale)}
{invoice.business?.name ? ` · ${invoice.business.name}` : ''} {businessLabel ? ` · ${businessLabel}` : ''}
</p> </p>
</header> </header>
@@ -80,7 +154,7 @@ export function PublicInvoicePage() {
) : null} ) : null}
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.sectionTitle}>Items</h2> <h2 className={styles.sectionTitle}>{t.items}</h2>
<div className={styles.items}> <div className={styles.items}>
{(invoice.items ?? []).map((item) => { {(invoice.items ?? []).map((item) => {
const effective = const effective =
@@ -115,11 +189,11 @@ export function PublicInvoicePage() {
</div> </div>
<div className={styles.totals}> <div className={styles.totals}>
<div> <div>
<span>Subtotal</span> <span>{t.subtotal}</span>
<strong>{formatIrtPrice(invoice.subtotal ?? 0)}</strong> <strong>{formatIrtPrice(invoice.subtotal ?? 0)}</strong>
</div> </div>
<div className={styles.totalRow}> <div className={styles.totalRow}>
<span>Total</span> <span>{t.total}</span>
<strong>{formatIrtPrice(invoice.total ?? 0)}</strong> <strong>{formatIrtPrice(invoice.total ?? 0)}</strong>
</div> </div>
</div> </div>
@@ -127,7 +201,7 @@ export function PublicInvoicePage() {
{(invoice.keyPoints?.length ?? 0) > 0 ? ( {(invoice.keyPoints?.length ?? 0) > 0 ? (
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.sectionTitle}>Key points</h2> <h2 className={styles.sectionTitle}>{t.keyPoints}</h2>
<ul className={styles.keyPoints}> <ul className={styles.keyPoints}>
{invoice.keyPoints!.map((kp) => ( {invoice.keyPoints!.map((kp) => (
<li key={kp.id}>{kp.text}</li> <li key={kp.id}>{kp.text}</li>
@@ -138,19 +212,23 @@ export function PublicInvoicePage() {
{(invoice.accounts?.length ?? 0) > 0 ? ( {(invoice.accounts?.length ?? 0) > 0 ? (
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.sectionTitle}>Account numbers</h2> <h2 className={styles.sectionTitle}>{t.accounts}</h2>
<div className={styles.accounts}> <div className={styles.accounts}>
{invoice.accounts!.map((acc) => { {invoice.accounts!.map((acc) => {
const details = [ const numberBits = [
acc.accountHolderName, acc.cardNumber ? `${t.card}: ${acc.cardNumber}` : null,
acc.cardNumber ? `Card: ${acc.cardNumber}` : null, acc.iban ? `${t.iban}: ${acc.iban}` : null,
acc.iban ? `IBAN: ${acc.iban}` : null,
].filter(Boolean) ].filter(Boolean)
return ( return (
<div key={acc.id} className={styles.account}> <div key={acc.id} className={styles.account}>
<strong className={styles.accountBank}>{acc.bankName}</strong> <strong className={styles.accountBank}>{acc.bankName}</strong>
{details.length > 0 ? ( {acc.accountHolderName ? (
<p className={styles.accountDetails}>{details.join(' · ')}</p> <p className={styles.accountDetails}>{acc.accountHolderName}</p>
) : null}
{numberBits.length > 0 ? (
<p className={styles.accountDetails} dir="ltr">
{numberBits.join(' · ')}
</p>
) : null} ) : null}
</div> </div>
) )
@@ -167,7 +245,7 @@ export function PublicInvoicePage() {
rel="noreferrer" rel="noreferrer"
> >
<img src={meshkeeLogo} alt="" className={styles.footerLogo} /> <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> <span className={styles.footerName}>Meshkee E-Commerce Group</span>
</a> </a>
</footer> </footer>
@@ -177,12 +255,10 @@ export function PublicInvoicePage() {
{invoice ? ( {invoice ? (
<div className={styles.toolbar}> <div className={styles.toolbar}>
<p className={styles.toolbarHint}> <p className={styles.toolbarHint}>{t.printHint}</p>
Save as PDF from the print dialog (Destination Save as PDF).
</p>
<button type="button" className={styles.printBtn} onClick={handlePrint}> <button type="button" className={styles.printBtn} onClick={handlePrint}>
<FileDown size={16} aria-hidden /> <FileDown size={16} aria-hidden />
Print to PDF {t.print}
</button> </button>
</div> </div>
) : null} ) : null}
+5 -1
View File
@@ -91,7 +91,11 @@ export type PublicInvoice = {
name: string | null name: string | null
topText: string | null topText: string | null
issuedAt: string issuedAt: string
business?: Invoice['business'] /** Business dashboard default locale — drives public invoice RTL/LTR. */
locale?: 'en' | 'fa'
business?: Invoice['business'] & {
displayName?: string
}
user?: { user?: {
firstName: string | null firstName: string | null
lastName: string | null lastName: string | null
+1
View File
@@ -395,6 +395,7 @@ Auth: platform template routes → super-admin. Business routes → `BusinessPer
**Public invoice viewer:** **Public invoice viewer:**
- Super-admin SPA `/invoices/:publicId` (`PublicInvoicePage`) — print-to-PDF; opaque 12-digit `publicId` - Super-admin SPA `/invoices/:publicId` (`PublicInvoicePage`) — print-to-PDF; opaque 12-digit `publicId`
- Payload includes `locale` (`fa` | `en` from business `settings.branding.defaultLocale`) — page is RTL + Farsi chrome when `fa`
- Platform invoices: `https://{INVOICE_PUBLIC_DOMAIN}/invoices/{publicId}` (default `meshkee.com`) - Platform invoices: `https://{INVOICE_PUBLIC_DOMAIN}/invoices/{publicId}` (default `meshkee.com`)
- Business invoices: `https://{primaryBusinessDomain}/invoices/{publicId}` (e.g. `sanihome.ir`) - Business invoices: `https://{primaryBusinessDomain}/invoices/{publicId}` (e.g. `sanihome.ir`)