بلاک‌ها · بانکی و پرداخت

صورت‌حساب فروش: فروشنده و خریدار، اقلام، تخفیف، مالیات بر ارزش افزوده و مبلغ نهایی.

۱. نصب کامپوننت‌ها

این بلاک از ۸ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}