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

سفارش‌ها، کارِ گروهی

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

سگمنتِ وضعیت با شمار، جدولِ فشرده با انتخابِ همه و نیمه‌انتخاب، و نوارِ اقدامِ گروهی (ارسال، چاپِ برچسب) که جای ابزارها را می‌گیرد.

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

این بلاک از ۹ آیتم دیگ استفاده می‌کند: badgebuttoncheckboxcontrol-sizeiconspersiantabletoasttoggle-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/checkbox.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/table.json https://docs.digdesign.ir/r/toast.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/checkbox.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/table.json https://docs.digdesign.ir/r/toast.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/checkbox.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/table.json https://docs.digdesign.ir/r/toast.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/checkbox.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/table.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.json

۲. کد صفحه

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

orders-bulk.tsx
"use client"

import * as React from "react"

import { DownloadIcon, FilterIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { toast } from "@/components/ui/toast"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, toFaDigits } from "@/lib/persian"

type Status = "pending" | "paid" | "shipped"
const STATUS: Record<Status, { label: string; color: "warning" | "info" | "success" }> = {
  pending: { label: "در انتظارِ پرداخت", color: "warning" },
  paid: { label: "پرداخت‌شده", color: "info" },
  shipped: { label: "ارسال‌شده", color: "success" },
}
const ORDERS: { id: string; customer: string; city: string; total: number; status: Status }[] = [
  { id: "1101", customer: "آرمیتا رستگار", city: "تهران", total: 2_450_000, status: "paid" },
  { id: "1100", customer: "کیان مرادی", city: "اصفهان", total: 1_265_000, status: "paid" },
  { id: "1099", customer: "نیلوفر احمدی", city: "شیراز", total: 5_100_000, status: "pending" },
  { id: "1098", customer: "سارا محمدی", city: "تهران", total: 789_000, status: "shipped" },
  { id: "1097", customer: "امیر رضایی", city: "مشهد", total: 3_320_000, status: "paid" },
]

/**
 * سفارش‌ها برای کارِ گروهی: سگمنتِ وضعیت با شمار، جدولِ فشرده با چک‌باکس و «انتخابِ همه»
 * (حالتِ نیمه‌انتخاب هم)، و نوارِ اقدامِ گروهی که با انتخاب جای ابزارها را می‌گیرد: ارسال،
 * چاپِ برچسب، خروجی؛ پس از اقدام انتخاب پاک و نتیجه گزارش می‌شود.
 */
export default function OrdersBulkBlock() {
  const [orders, setOrders] = React.useState(ORDERS)
  const [filter, setFilter] = React.useState<Status | "all">("all")
  const [selected, setSelected] = React.useState<string[]>([])
  const visible = orders.filter((order) => filter === "all" || order.status === filter)
  const checkedCount = visible.filter((order) => selected.includes(order.id)).length
  const allState = checkedCount === 0 ? false : checkedCount === visible.length ? true : "indeterminate"
  const count = (value: Status | "all") => orders.filter((order) => value === "all" || order.status === value).length

  function ship() {
    setOrders((list) => list.map((order) => (selected.includes(order.id) ? { ...order, status: "shipped" } : order)))
    toast.success(`${toFaDigits(selected.length)} سفارش «ارسال‌شده» شد.`)
    setSelected([])
  }

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-5xl flex-col gap-8 p-4 sm:p-6">
        <header className="flex flex-col gap-1">
          <h1 className="text-2xl font-bold">سفارش‌ها</h1>
          <p className="text-muted-foreground">چند سفارش را انتخاب کن و یک‌جا جلو ببر.</p>
        </header>
        <div className="flex flex-col gap-4">
          {selected.length > 0 ? (
            <div className="flex min-h-12 flex-wrap items-center justify-between gap-3 rounded-xl bg-primary-subtle px-4 py-2" aria-live="polite">
              <span className="text-sm font-medium">{toFaDigits(selected.length)} سفارش انتخاب شده</span>
              <div className="flex flex-wrap gap-2">
                <Button onClick={ship}>علامتِ ارسال</Button>
                <Button variant="outline" className="bg-card" onClick={() => toast.success("برچسب‌ها آمادهٔ چاپ است.")}>
                  چاپِ برچسب
                </Button>
                <Button variant="ghost" onClick={() => setSelected([])}>
                  لغوِ انتخاب
                </Button>
              </div>
            </div>
          ) : (
            <div className="flex flex-wrap items-center justify-between gap-3">
              <div className="max-w-full overflow-x-auto">
                <ToggleGroup type="single" variant="segmented" value={filter} onValueChange={(value) => value && setFilter(value as Status | "all")} aria-label="وضعیت">
                  {(["all", "pending", "paid", "shipped"] as const).map((value) => (
                    <ToggleGroupItem key={value} value={value} className="gap-2 whitespace-nowrap">
                      {value === "all" ? "همه" : STATUS[value].label}
                      <span className="text-xs text-muted-foreground tabular-nums">{toFaDigits(count(value))}</span>
                    </ToggleGroupItem>
                  ))}
                </ToggleGroup>
              </div>
              <div className="flex gap-2">
                <Button variant="outline">
                  <FilterIcon />
                  فیلتر
                </Button>
                <Button variant="outline" onClick={() => toast.success("خروجی اکسل آماده شد.")}>
                  <DownloadIcon />
                  خروجی
                </Button>
              </div>
            </div>
          )}
          {/* موبایل: کارتِ هر سفارش با تیک؛ کلِ کارت ناحیهٔ لمس است. */}
          <ul className="flex flex-col divide-y divide-border rounded-xl border border-border bg-card md:hidden">
            {visible.map((order) => {
              const checked = selected.includes(order.id)
              return (
                <li key={order.id}>
                  <label className={`flex cursor-pointer items-center gap-3 p-4 ${checked ? "bg-primary-subtle" : ""}`}>
                    <Checkbox checked={checked} onCheckedChange={(value) => setSelected((list) => (value ? [...list, order.id] : list.filter((id) => id !== order.id)))} aria-label={`انتخابِ سفارشِ ${toFaDigits(order.id)}`} />
                    <span className="flex min-w-0 flex-1 flex-col gap-1">
                      <span className="flex items-center justify-between gap-2">
                        <span className="font-bold">{order.customer}</span>
                        <Badge variant="soft" color={STATUS[order.status].color}>
                          {STATUS[order.status].label}
                        </Badge>
                      </span>
                      <span className="text-sm text-muted-foreground tabular-nums">
                        #{toFaDigits(order.id)} · {order.city} · {formatCurrency(order.total)}
                      </span>
                    </span>
                  </label>
                </li>
              )
            })}
          </ul>
          <div className="hidden rounded-xl border border-border bg-card md:block">
            <Table className="min-w-[36rem]">
              <TableHeader className="bg-subtle">
                <TableRow className="hover:bg-subtle">
                  <TableHead className="w-12">
                    <Checkbox checked={allState} onCheckedChange={(checked) => setSelected(checked === true ? visible.map((order) => order.id) : [])} aria-label="انتخابِ همه" />
                  </TableHead>
                  <TableHead>سفارش</TableHead>
                  <TableHead>مشتری</TableHead>
                  <TableHead>شهر</TableHead>
                  <TableHead className="text-end">مبلغ</TableHead>
                  <TableHead>وضعیت</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {visible.map((order) => {
                  const checked = selected.includes(order.id)
                  return (
                    <TableRow key={order.id} data-state={checked ? "selected" : undefined}>
                      <TableCell>
                        <Checkbox checked={checked} onCheckedChange={(value) => setSelected((list) => (value ? [...list, order.id] : list.filter((id) => id !== order.id)))} aria-label={`انتخابِ سفارشِ ${toFaDigits(order.id)}`} />
                      </TableCell>
                      <TableCell className="font-medium">
                        <bdi dir="ltr">#{toFaDigits(order.id)}</bdi>
                      </TableCell>
                      <TableCell>{order.customer}</TableCell>
                      <TableCell className="text-muted-foreground">{order.city}</TableCell>
                      <TableCell className="text-end tabular-nums">{formatCurrency(order.total)}</TableCell>
                      <TableCell>
                        <Badge variant="soft" color={STATUS[order.status].color}>
                          {STATUS[order.status].label}
                        </Badge>
                      </TableCell>
                    </TableRow>
                  )
                })}
              </TableBody>
            </Table>
          </div>
        </div>
      </main>
    </ControlSizeProvider>
  )
}