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

فهرست تراکنش با فیلتر بازهٔ تاریخ و نوع، مبلغ علامت‌دار و خروجی.

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

این بلاک از ۱۱ آیتم دیگ استفاده می‌کند: badgebuttoncontrol-sizedate-range-pickerempty-stateiconspersiansearch-fieldstat-tiletabletoggle-group

پکیج‌منیجر پیش‌فرض 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/date-range-picker.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.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/date-range-picker.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.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/date-range-picker.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.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/date-range-picker.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

transactions.tsx
"use client"

import * as React from "react"
import type { DateRange } from "react-day-picker"

import { DownloadIcon, TrendingDownIcon, TrendingUpIcon, WalletIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { DateRangePicker } from "@/components/ui/date-range-picker"
import {
  EmptyState,
  EmptyStateActions,
  EmptyStateDescription,
  EmptyStateTitle,
} from "@/components/ui/empty-state"
import { SearchField } from "@/components/ui/search-field"
import {
  StatTile,
  StatTileContent,
  StatTileHeader,
  StatTileIcon,
  StatTileLabel,
  StatTileSegments,
  StatTileSparkline,
  StatTileTrend,
  StatTileUnit,
  StatTileValue,
} from "@/components/ui/stat-tile"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatNumber, formatJalaliDateTime } from "@/lib/persian"

type Kind = "deposit" | "withdraw"

const ROWS: { id: string; title: string; date: string; amount: number; kind: Kind; ok: boolean }[] = [
  { id: "t1", title: "واریز حقوق", date: "2026-09-29T08:00:00", amount: 38_000_000, kind: "deposit", ok: true },
  { id: "t2", title: "کارت به کارت به کیان مرادی", date: "2026-09-28T19:30:00", amount: 2_500_000, kind: "withdraw", ok: true },
  { id: "t3", title: "پرداخت قبض برق", date: "2026-09-27T11:10:00", amount: 386_000, kind: "withdraw", ok: true },
  { id: "t4", title: "خرید اینترنتی", date: "2026-09-26T22:45:00", amount: 1_240_000, kind: "withdraw", ok: false },
  { id: "t5", title: "واریز از حساب پس‌انداز", date: "2026-09-25T09:00:00", amount: 5_000_000, kind: "deposit", ok: true },
]

export default function TransactionsBlock() {
  const [query, setQuery] = React.useState("")
  const [kind, setKind] = React.useState("all")
  const [range, setRange] = React.useState<DateRange>()

  const rows = ROWS.filter((row) => {
    const date = new Date(row.date)
    return (
      (kind === "all" || row.kind === kind) &&
      row.title.includes(query) &&
      (!range?.from || date >= range.from) &&
      (!range?.to || date <= new Date(range.to.getTime() + 86_400_000))
    )
  })
  const sum = (k: Kind) => rows.filter((row) => row.kind === k && row.ok).reduce((total, row) => total + row.amount, 0)

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full min-w-0 max-w-5xl flex-col gap-6 p-4 sm:p-6">
        <header className="flex items-start justify-between gap-4 sm:items-end">
          <div className="flex min-w-0 flex-col gap-1">
            <h1 className="text-2xl font-bold">گردش حساب</h1>
            <p className="text-sm text-muted-foreground">واریز و برداشت‌های حساب جاری را جست‌وجو و فیلتر کنید.</p>
          </div>
          <Button variant="outline" aria-label="خروجی اکسل" className="shrink-0">
            <DownloadIcon />
            <span className="hidden sm:inline">خروجی اکسل</span>
          </Button>
        </header>

        {/* سه کارتِ آمار: واریز و برداشت با روند، مانده با نوارِ سهمِ هر کدام. */}
        <div className="grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-3">
          <StatTile className="gap-3 p-4 sm:p-5">
            <StatTileHeader>
              <StatTileIcon tone="success">
                <TrendingUpIcon />
              </StatTileIcon>
              <StatTileLabel>مجموع واریز</StatTileLabel>
            </StatTileHeader>
            <StatTileContent>
              <StatTileValue className="whitespace-nowrap text-xl sm:text-2xl">
                {formatNumber(sum("deposit"))}
                <StatTileUnit>تومان</StatTileUnit>
              </StatTileValue>
              <StatTileTrend value={8.4} formatValue={() => "+۸٫۴٪"} className="max-sm:hidden" />
            </StatTileContent>
            <StatTileSparkline values={[22, 25, 24, 30, 28, 35, 43]} className="h-10" />
          </StatTile>
          <StatTile className="gap-3 p-4 sm:p-5">
            <StatTileHeader>
              <StatTileIcon tone="destructive">
                <TrendingDownIcon />
              </StatTileIcon>
              <StatTileLabel>مجموع برداشت</StatTileLabel>
            </StatTileHeader>
            <StatTileContent>
              <StatTileValue className="whitespace-nowrap text-xl sm:text-2xl">
                {formatNumber(sum("withdraw"))}
                <StatTileUnit>تومان</StatTileUnit>
              </StatTileValue>
              {/* کاهشِ برداشت خبرِ خوبی است؛ رنگ برعکس. */}
              <StatTileTrend value={-3.2} invertColor formatValue={() => "−۳٫۲٪"} className="max-sm:hidden" />
            </StatTileContent>
            <StatTileSparkline values={[4.1, 3.6, 3.9, 3.2, 3.4, 2.9, 2.9]} color="var(--destructive)" className="h-10" />
          </StatTile>
          <StatTile className="col-span-2 gap-3 p-4 sm:p-5 lg:col-span-1">
            <StatTileHeader>
              <StatTileIcon tone="accent">
                <WalletIcon />
              </StatTileIcon>
              <StatTileLabel>مانده در این بازه</StatTileLabel>
            </StatTileHeader>
            <StatTileValue className="whitespace-nowrap text-xl sm:text-2xl">
              {formatNumber(sum("deposit") - sum("withdraw"))}
              <StatTileUnit>تومان</StatTileUnit>
            </StatTileValue>
            <StatTileSegments
              segments={[
                { value: sum("deposit"), color: "var(--success)" },
                { value: Math.max(1, sum("withdraw")), color: "var(--destructive)" },
              ]}
            />
            <div className="flex justify-between text-xs text-muted-foreground">
              <span className="flex items-center gap-1.5">
                <span aria-hidden className="size-2 rounded-full bg-success" />
                واریز
              </span>
              <span className="flex items-center gap-1.5">
                <span aria-hidden className="size-2 rounded-full bg-destructive" />
                برداشت
              </span>
            </div>
          </StatTile>
        </div>

        {/* جدول و فیلترهایش یک سطح‌اند: نوارِ ابزار بالای همان کارت، نه ردیفِ جدا. */}
        {/* ابزارها بیرونِ قاب، ۳۲ پیکسل بالای جدول؛ فقط فهرست/جدول قاب دارد. */}
        <section aria-label="فهرست تراکنش‌ها" className="mt-6 flex flex-col gap-8">
          <ControlSizeProvider size="xl">
            {/* سه فیلترِ هم‌خانواده با فاصلهٔ برابر (۱۲). */}
            <div className="flex flex-col gap-9 md:flex-row md:items-center md:gap-3">
              {/* فیلترِ تک‌انتخابی با سگمنت؛ در موبایل تمام‌عرض با سه بخشِ مساوی. */}
              <ToggleGroup type="single" variant="segmented" value={kind} onValueChange={(value) => value && setKind(value as string)} aria-label="نوع تراکنش" className="w-full md:w-auto">
                <ToggleGroupItem value="all" className="flex-1 px-4">همه</ToggleGroupItem>
                <ToggleGroupItem value="deposit" className="flex-1 px-4">واریز</ToggleGroupItem>
                <ToggleGroupItem value="withdraw" className="flex-1 px-4">برداشت</ToggleGroupItem>
              </ToggleGroup>
              <div className="flex gap-3 md:ms-auto">
                <SearchField
                  aria-label="جست‌وجوی تراکنش"
                  placeholder="جست‌وجو در شرح"
                  value={query}
                  onValueChange={setQuery}
                  className="min-w-0 flex-1 md:w-60 md:flex-none"
                />
                <DateRangePicker
                  calendar="jalali"
                  value={range}
                  onChange={setRange}
                  aria-label="بازهٔ تاریخ"
                  className="w-36 shrink-0 sm:w-56"
                />
              </div>
            </div>
          </ControlSizeProvider>

          {rows.length === 0 ? (
            <EmptyState variant="bordered" className="py-12">
              <EmptyStateTitle>تراکنشی در این بازه نیست</EmptyStateTitle>
              <EmptyStateDescription>بازهٔ تاریخ یا فیلترها را تغییر دهید.</EmptyStateDescription>
              <EmptyStateActions>
                <Button
                  variant="outline"
                  onClick={() => {
                    setQuery("")
                    setKind("all")
                    setRange(undefined)
                  }}
                >
                  پاک‌کردن فیلترها
                </Button>
              </EmptyStateActions>
            </EmptyState>
          ) : (
            <>
            {/* موبایل: فهرستِ دوخطی به‌جای جدولِ اسکرول‌خور؛ مبلغ همیشه دیده می‌شود. */}
            <ul className="divide-y divide-border overflow-hidden rounded-xl border border-border bg-card md:hidden">
              {rows.map((row) => (
                <li key={row.id} className="flex items-center justify-between gap-3 px-4 py-3.5">
                  <div className="flex min-w-0 flex-col gap-3">
                    <span className="truncate font-medium">{row.title}</span>
                    <span className="text-xs text-muted-foreground">{formatJalaliDateTime(row.date)}</span>
                  </div>
                  <div className="flex shrink-0 flex-col items-end gap-1">
                    <span className={row.kind === "deposit" ? "font-semibold text-success-subtle-foreground" : "font-semibold"}>
                      <bdi dir="ltr">
                        {row.kind === "deposit" ? "+" : "−"}
                        {formatNumber(row.amount)}
                      </bdi>
                    </span>
                    {!row.ok && (
                      <Badge variant="soft" color="destructive" size="sm">
                        ناموفق
                      </Badge>
                    )}
                  </div>
                </li>
              ))}
            </ul>
            <div className="hidden rounded-xl border border-border bg-card md:block">
              <Table>
                <TableHeader className="bg-muted">
                  <TableRow>
                    <TableHead>شرح</TableHead>
                    <TableHead>تاریخ و ساعت</TableHead>
                    <TableHead>وضعیت</TableHead>
                    <TableHead align="end">مبلغ</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {rows.map((row) => (
                    <TableRow key={row.id}>
                      <TableCell className="font-medium">{row.title}</TableCell>
                      <TableCell className="whitespace-nowrap text-muted-foreground">{formatJalaliDateTime(row.date)}</TableCell>
                      <TableCell>
                        <Badge variant="soft" color={row.ok ? "success" : "destructive"}>
                          {row.ok ? "موفق" : "ناموفق"}
                        </Badge>
                      </TableCell>
                      <TableCell
                        align="end"
                        className={row.kind === "deposit" ? "whitespace-nowrap font-medium text-success-subtle-foreground" : "whitespace-nowrap font-medium"}
                      >
                        <bdi dir="ltr">
                          {row.kind === "deposit" ? "+" : "−"}
                          {formatNumber(row.amount)}
                        </bdi>{" "}
                        تومان
                      </TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </div>
            </>
          )}
        </section>
      </main>
    </ControlSizeProvider>
  )
}