فاکتور
پیشنمایش تمامصفحهصورتحساب فروش: فروشنده و خریدار، اقلام، تخفیف، مالیات بر ارزش افزوده و مبلغ نهایی.
۱. نصب کامپوننتها
این بلاک از ۸ آیتم دیگ استفاده میکند: badgebuttoncontrol-sizeiconspersianseparatortabletoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/separator.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/separator.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/separator.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/separator.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
invoice.tsx
"use client"
import { CopyIcon, DownloadIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Separator } from "@/components/ui/separator"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { toast } from "@/components/ui/toast"
import { formatCurrency, formatJalali, formatNumber, toFaDigits } from "@/lib/persian"
const ITEMS = [
{ id: 1, title: "اشتراک سالانهٔ پلن حرفهای", note: "۱۴۰۵/۰۷/۰۶ تا ۱۴۰۶/۰۷/۰۵", qty: 1, price: 4_900_000 },
{ id: 2, title: "عضو اضافه", note: "برای یک سال", qty: 3, price: 350_000 },
{ id: 3, title: "راهاندازی و آموزش", note: "دو جلسهٔ آنلاین", qty: 1, price: 1_200_000 },
]
const VAT_RATE = 0.1
const INVOICE_NO = "1405-0231"
/**
* صورتحساب: نوارِ تیرهٔ بالا با شماره، وضعیت و مبلغِ نهایی بهصورتِ برجسته؛ فروشنده
* و خریدار کنارِ هم، جدولِ اقلام با توضیحِ هر ردیف، جمعبندی روی سطحِ subtle و پانویسِ
* پرداخت. دکمهها بیرون از سند تا در چاپ نیایند.
*/
export default function InvoiceBlock() {
const subtotal = ITEMS.reduce((sum, item) => sum + item.qty * item.price, 0)
const discount = 500_000
const vat = Math.round((subtotal - discount) * VAT_RATE)
const total = subtotal - discount + vat
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-3xl flex-col gap-6 p-4 sm:p-6">
<div className="flex flex-wrap items-center justify-between gap-3 print:hidden">
<h1 className="text-2xl font-bold">صورتحساب</h1>
<Button variant="outline" onClick={() => toast.success("فایلِ PDF آماده شد.")}>
<DownloadIcon />
دریافت PDF
</Button>
</div>
<article className="overflow-hidden rounded-xl border border-border bg-card">
{/* سربرگِ سند روشن و برنددار، نه سیاه: زمینهٔ بنفشِ کمرنگ با پترنِ نقطهای و نوارِ برند بالای کارت. */}
<header className="relative isolate flex flex-wrap items-end justify-between gap-x-6 gap-y-4 border-t-4 border-[var(--dig-violet-600)] bg-[color-mix(in_oklab,var(--dig-violet-500)_8%,var(--card))] p-5 sm:p-8">
<span aria-hidden className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(color-mix(in_oklab,var(--dig-violet-500)_22%,transparent)_1.2px,transparent_1.3px)] [background-size:16px_16px] [mask-image:linear-gradient(to_left,black,transparent_55%)]" />
<div className="flex flex-col gap-3">
<span className="flex items-center gap-2 text-lg font-black">
<span aria-hidden className="size-3 rounded-sm bg-[var(--dig-violet-500)]" />
نرمافزارِ آوند
</span>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
شمارهٔ <bdi dir="ltr">{toFaDigits(INVOICE_NO)}</bdi>
<Button
variant="ghost"
size="icon"
aria-label="کپی شمارهٔ صورتحساب"
onClick={() => {
navigator.clipboard?.writeText(INVOICE_NO)
toast.success("شماره کپی شد.")
}}
className="print:hidden"
>
<CopyIcon />
</Button>
</div>
</div>
<div className="flex flex-col items-start gap-2 sm:items-end">
<Badge variant="soft" color="success">
پرداختشده
</Badge>
<span className="text-3xl font-black tabular-nums">{formatCurrency(total)}</span>
<span className="text-sm text-muted-foreground">{formatJalali(new Date("2026-09-28"), "long")}</span>
</div>
</header>
<div className="flex flex-col gap-8 p-6 sm:p-8">
<div className="grid grid-cols-1 gap-6 text-sm sm:grid-cols-2">
<Party id="seller" title="از" name="شرکتِ نرمافزاری آوند" idLabel="شناسهٔ ملی" idValue="10320894512" address="تهران، خیابان ولیعصر، پلاک ۱۲" />
<Party id="buyer" title="برای" name="آرمیتا رستگار" idLabel="کد ملی" idValue="0012345678" address="اصفهان، خیابان چهارباغ، کوچهٔ نهم" />
</div>
<Table>
<TableHeader>
<TableRow className="hover:bg-card">
<TableHead>شرح</TableHead>
<TableHead className="hidden text-center sm:table-cell">تعداد</TableHead>
<TableHead className="hidden text-end sm:table-cell">فی</TableHead>
<TableHead className="text-end">جمع</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ITEMS.map((item) => (
<TableRow key={item.id}>
<TableCell>
<div className="flex flex-col gap-0.5">
<span className="font-medium">{item.title}</span>
<span className="text-xs text-muted-foreground">{item.note}</span>
{/* در موبایل ستونهای تعداد و فی جا نمیشوند؛ زیرِ شرح میآیند. */}
<span className="text-xs text-muted-foreground tabular-nums sm:hidden">
{formatNumber(item.qty)} × {formatCurrency(item.price)}
</span>
</div>
</TableCell>
<TableCell className="hidden text-center text-muted-foreground sm:table-cell">{formatNumber(item.qty)}</TableCell>
<TableCell className="hidden text-end text-muted-foreground tabular-nums sm:table-cell">{formatCurrency(item.price)}</TableCell>
<TableCell className="text-end align-top font-medium whitespace-nowrap tabular-nums">{formatCurrency(item.qty * item.price)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<dl className="ms-auto flex w-full max-w-sm flex-col gap-3 rounded-lg bg-subtle p-5 text-sm">
<div className="flex justify-between gap-4">
<dt className="text-muted-foreground">جمع اقلام</dt>
<dd className="tabular-nums">{formatCurrency(subtotal)}</dd>
</div>
<div className="flex justify-between gap-4">
<dt className="text-muted-foreground">تخفیف</dt>
<dd className="text-success tabular-nums">
{/* علامت و عدد یک تکهٔ چپبهراستاند تا منفی همیشه پشتِ عدد بماند. */}
<bdi dir="ltr">−{formatNumber(discount)}</bdi> تومان
</dd>
</div>
<div className="flex justify-between gap-4">
<dt className="text-muted-foreground">مالیات بر ارزش افزوده (٪{toFaDigits(VAT_RATE * 100)})</dt>
<dd className="tabular-nums">{formatCurrency(vat)}</dd>
</div>
<Separator className="my-1" />
<div className="flex items-baseline justify-between gap-4">
<dt className="font-semibold">مبلغ نهایی</dt>
<dd className="text-xl font-bold tabular-nums">{formatCurrency(total)}</dd>
</div>
</dl>
</div>
<footer className="flex flex-wrap justify-between gap-3 border-t border-border px-6 py-5 text-sm text-muted-foreground sm:px-8">
<span>پرداخت با کارتِ ملت · کدِ پیگیری <bdi dir="ltr">{toFaDigits("448120")}</bdi></span>
<span>
پرسشی دارید؟ <bdi dir="ltr">billing@avand.example</bdi>
</span>
</footer>
</article>
</main>
</ControlSizeProvider>
)
}
function Party({ id, title, name, idLabel, idValue, address }: { id: string; title: string; name: string; idLabel: string; idValue: string; address: string }) {
return (
<section className="flex flex-col gap-1.5" aria-labelledby={id}>
<h2 id={id} className="text-xs font-medium text-muted-foreground">
{title}
</h2>
<p className="font-semibold">{name}</p>
<p className="text-muted-foreground">
{idLabel} <bdi dir="ltr">{toFaDigits(idValue)}</bdi>
</p>
<p className="text-muted-foreground">{address}</p>
</section>
)
}