mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
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:
co-authored by
Cursor
parent
a50685ec69
commit
f21041b950
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user