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

تحلیلِ هزینه‌ها

پیش‌نمایش تمام‌صفحه

هزینهٔ ماه با مقایسه، نوارِ سهمِ دسته‌ها و فهرستِ دسته‌ها با بودجه؛ عبور از بودجه با متنِ صریح و رنگ، نه فقط رنگ.

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

این بلاک از ۶ آیتم دیگ استفاده می‌کند: buttoncontrol-sizeiconspersianprogresstoggle-group

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add 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/progress.json https://docs.digdesign.ir/r/toggle-group.json
pnpm dlx digdesign@latest add 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/progress.json https://docs.digdesign.ir/r/toggle-group.json
yarn dlx digdesign@latest add 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/progress.json https://docs.digdesign.ir/r/toggle-group.json
bunx --bun digdesign@latest add 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/progress.json https://docs.digdesign.ir/r/toggle-group.json

۲. کد صفحه

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

transactions-insights.tsx
"use client"

import * as React from "react"

import { ReceiptIcon, ShopIcon, TruckIcon, WalletIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Progress } from "@/components/ui/progress"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, toFaDigits } from "@/lib/persian"

const CATEGORIES = [
  { key: "food", label: "خوراک و کافه", icon: <ShopIcon />, spent: 4_850_000, budget: 5_000_000, count: 23 },
  { key: "transport", label: "رفت‌وآمد", icon: <TruckIcon />, spent: 1_920_000, budget: 1_500_000, count: 31 },
  { key: "bills", label: "قبض و اشتراک", icon: <ReceiptIcon />, spent: 1_240_000, budget: 2_000_000, count: 6 },
  { key: "other", label: "سایر", icon: <WalletIcon />, spent: 2_310_000, budget: 3_000_000, count: 12 },
]

/**
 * تحلیلِ هزینه‌ها: جمعِ ماه با مقایسه با ماهِ قبل، نوارِ سهمِ هر دسته در یک خط، و فهرستِ
 * دسته‌ها با بودجه؛ دسته‌ای که از بودجه رد شده با رنگ و متنِ صریح («۴۲۰ هزار بیشتر») نشان
 * داده می‌شود، نه فقط رنگ. لمسِ هر دسته فهرستِ تراکنش‌هایش را باز می‌کند.
 */
export default function TransactionsInsightsBlock() {
  const [month, setMonth] = React.useState("this")
  const total = CATEGORIES.reduce((sum, item) => sum + item.spent, 0)
  const colors = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)"]

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-3xl flex-col gap-8 p-4 sm:p-6">
        <header className="flex flex-wrap items-end justify-between gap-x-4 gap-y-6">
          <div className="flex flex-col gap-1">
            <h1 className="text-2xl font-bold">هزینه‌های من</h1>
            <p className="text-muted-foreground">کجا خرج کردی و چقدر تا سقفِ هر دسته مانده.</p>
          </div>
          <ToggleGroup type="single" variant="segmented" value={month} onValueChange={(value) => value && setMonth(value as string)} aria-label="ماه">
            <ToggleGroupItem value="last">شهریور</ToggleGroupItem>
            <ToggleGroupItem value="this">مهر</ToggleGroupItem>
          </ToggleGroup>
        </header>

        <section aria-label="جمعِ هزینه" className="flex flex-col gap-5 rounded-xl bg-subtle p-6">
          <div className="flex flex-wrap items-end justify-between gap-3">
            <div className="flex flex-col gap-1">
              <span className="text-sm text-muted-foreground">جمعِ هزینهٔ {month === "this" ? "مهر" : "شهریور"}</span>
              <span className="text-3xl font-black tabular-nums">{formatCurrency(total)}</span>
            </div>
            <span className="text-sm text-muted-foreground">٪{toFaDigits(8)} کمتر از ماهِ قبل</span>
          </div>
          <div className="flex h-3 gap-0.5 overflow-hidden rounded-full" role="img" aria-label={CATEGORIES.map((item) => `${item.label} ٪${toFaDigits(Math.round((item.spent / total) * 100))}`).join("، ")}>
            {CATEGORIES.map((item, index) => (
              <span key={item.key} style={{ width: `${(item.spent / total) * 100}%`, background: colors[index] }} />
            ))}
          </div>
        </section>

        <ul className="flex flex-col gap-3">
          {CATEGORIES.map((item, index) => {
            const over = item.spent - item.budget
            // نزدیکِ سقف (۹۰٪ به بالا) پیش از عبور هشدار می‌گیرد تا کاربر فرصتِ ترمز داشته باشد.
            const near = over <= 0 && item.spent / item.budget >= 0.9
            return (
              <li key={item.key}>
                <Button variant="outline" className="h-auto w-full flex-col items-stretch gap-3 p-4 text-start font-normal">
                  <span className="flex items-center gap-3">
                    <span aria-hidden className="flex size-10 items-center justify-center rounded-lg [&>svg]:size-5" style={{ background: `color-mix(in oklab, ${colors[index]} 15%, var(--card))`, color: colors[index] }}>
                      {item.icon}
                    </span>
                    <span className="flex flex-1 flex-col">
                      <span className="font-semibold">{item.label}</span>
                      <span className="text-xs text-muted-foreground">{toFaDigits(item.count)} تراکنش</span>
                    </span>
                    <span className="flex flex-col items-end">
                      <span className="font-bold tabular-nums">{formatCurrency(item.spent)}</span>
                      <span className={`text-xs ${over > 0 ? "font-medium text-destructive" : near ? "font-medium text-warning" : "text-muted-foreground"}`}>
                        {over > 0 ? `${formatCurrency(over)} بیشتر از بودجه` : near ? `نزدیکِ سقف؛ ${formatCurrency(-over)} مانده` : `${formatCurrency(-over)} مانده`}
                      </span>
                    </span>
                  </span>
                  <Progress value={Math.min(100, (item.spent / item.budget) * 100)} aria-label={`مصرفِ بودجهٔ ${item.label}`} color={over > 0 ? "destructive" : near ? "warning" : undefined}
                    // رنگِ عادیِ هر دسته همان رنگِ بخشش در نوارِ بالاست تا راهنما و ردیف یکی خوانده شوند.
                    style={over > 0 || near ? undefined : ({ "--progress": colors[index] } as React.CSSProperties)}
                  />
                </Button>
              </li>
            )
          })}
        </ul>
      </main>
    </ControlSizeProvider>
  )
}