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

فهرست سفارش‌ها

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

جست‌وجو و فیلتر وضعیت، جدول با برچسب وضعیت و منوی عملیات هر ردیف، حالت خالی و صفحه‌بندی.

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

این بلاک از ۱۱ آیتم دیگ استفاده می‌کند: badgebuttoncontrol-sizedropdown-menuempty-stateiconspaginationpersiansearch-fieldselecttable

پکیج‌منیجر پیش‌فرض 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/dropdown-menu.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/table.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/dropdown-menu.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/table.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/dropdown-menu.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/table.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/dropdown-menu.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/table.json

۲. کد صفحه

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

orders-list.tsx
"use client";

import * as React from "react";

import {
  CircleXIcon,
  EyeIcon,
  MoreHorizontalIcon,
  ReceiptIcon,
} from "@/components/icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ControlSizeProvider } from "@/components/ui/control-size";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  EmptyState,
  EmptyStateActions,
  EmptyStateDescription,
  EmptyStateTitle,
} from "@/components/ui/empty-state";
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination";
import { SearchField } from "@/components/ui/search-field";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { formatCurrency, formatJalali, toFaDigits } from "@/lib/persian";

type Status = "paid" | "pending" | "shipped" | "refunded";

const STATUS: Record<
  Status,
  { text: string; color: "success" | "warning" | "info" | "destructive" }
> = {
  paid: { text: "پرداخت‌شده", color: "success" },
  pending: { text: "در انتظار پرداخت", color: "warning" },
  shipped: { text: "ارسال‌شده", color: "info" },
  refunded: { text: "مرجوعی", color: "destructive" },
};

const ORDERS: {
  id: string;
  customer: string;
  date: string;
  amount: number;
  status: Status;
}[] = [
  {
    id: "1042",
    customer: "آرمیتا رستگار",
    date: "2026-09-27",
    amount: 2_450_000,
    status: "paid",
  },
  {
    id: "1041",
    customer: "کیان مرادی",
    date: "2026-09-27",
    amount: 890_000,
    status: "pending",
  },
  {
    id: "1040",
    customer: "نیلوفر احمدی",
    date: "2026-09-26",
    amount: 5_120_000,
    status: "refunded",
  },
  {
    id: "1039",
    customer: "سینا کریمی",
    date: "2026-09-25",
    amount: 1_300_000,
    status: "shipped",
  },
];

const PAGE_COUNT = 5;

/** منوی عملیاتِ هر سفارش؛ هم در جدول و هم در کارتِ موبایل. */
function OrderActions({ id }: { id: string }) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          variant="ghost"
          size="icon"
          aria-label={`عملیات سفارش ${toFaDigits(id)}`}
        >
          <MoreHorizontalIcon />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem>
          <EyeIcon />
          مشاهدهٔ جزئیات
        </DropdownMenuItem>
        <DropdownMenuItem>
          <ReceiptIcon />
          چاپ فاکتور
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive">
          <CircleXIcon />
          لغو سفارش
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

export default function OrdersListBlock() {
  const [query, setQuery] = React.useState("");
  const [status, setStatus] = React.useState("all");
  const [page, setPage] = React.useState(1);

  const rows = ORDERS.filter(
    (order) =>
      (status === "all" || order.status === status) &&
      (order.customer.includes(query) || order.id.includes(query)),
  );

  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="mb-2 flex flex-col gap-1">
          <h1 className="text-2xl font-bold">سفارش‌ها</h1>
          <p className="text-sm text-muted-foreground">
            سفارش‌های فروشگاه را بر اساس وضعیت پیگیری کنید.
          </p>
        </header>

        <ControlSizeProvider size="xl">
          {/* ابزارها ۳۲ پیکسل بالای جدول (mb-4 + gap-4)؛ جدا از جدول خوانده می‌شوند. */}
          <div className="mb-4 flex flex-wrap items-center gap-2">
            <SearchField
              aria-label="جست‌وجوی سفارش"
              placeholder="نام مشتری یا شمارهٔ سفارش"
              value={query}
              onValueChange={setQuery}
              className="min-w-0 flex-1 basis-60"
            />
            <Select
              value={status}
              onValueChange={(value) => setStatus(value as string)}
            >
              <SelectTrigger aria-label="فیلتر وضعیت" className="w-44">
                <SelectValue placeholder="وضعیت" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="all">همهٔ وضعیت‌ها</SelectItem>
                {(Object.keys(STATUS) as Status[]).map((key) => (
                  <SelectItem key={key} value={key}>
                    {STATUS[key].text}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>

          {rows.length === 0 ? (
            <EmptyState variant="bordered">
              <EmptyStateTitle>سفارشی با این فیلتر پیدا نشد</EmptyStateTitle>
              <EmptyStateDescription>
                عبارت جست‌وجو یا وضعیت را تغییر دهید.
              </EmptyStateDescription>
              <EmptyStateActions>
                <Button
                  variant="outline"
                  onClick={() => {
                    setQuery("");
                    setStatus("all");
                  }}
                >
                  پاک‌کردن فیلترها
                </Button>
              </EmptyStateActions>
            </EmptyState>
          ) : (
            <>
              {/* موبایل: کارتِ هر سفارش با وضعیت و منوی عملیات؛ جدول از md به بعد. */}
              <ul className="flex flex-col divide-y divide-border rounded-xl border border-border bg-card md:hidden">
                {rows.map((order) => (
                  <li key={order.id} className="flex items-center gap-3 p-4">
                    <span className="flex min-w-0 flex-1 flex-col gap-1">
                      <span className="flex items-center gap-2">
                        <span className="font-bold">{order.customer}</span>
                        <Badge
                          variant="soft"
                          color={STATUS[order.status].color}
                        >
                          {STATUS[order.status].text}
                        </Badge>
                      </span>
                      <span className="text-sm text-muted-foreground tabular-nums">
                        #{toFaDigits(order.id)} · {formatJalali(order.date)} ·{" "}
                        {formatCurrency(order.amount)}
                      </span>
                    </span>
                    <OrderActions id={order.id} />
                  </li>
                ))}
              </ul>
              <div className="hidden overflow-hidden rounded-xl border border-border md:block">
                <Table>
                  <TableHeader className="bg-muted">
                    <TableRow>
                      <TableHead>شماره</TableHead>
                      <TableHead>مشتری</TableHead>
                      <TableHead>تاریخ</TableHead>
                      <TableHead align="end">مبلغ</TableHead>
                      <TableHead>وضعیت</TableHead>
                      <TableHead align="end">
                        <span className="sr-only">عملیات</span>
                      </TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((order) => (
                      <TableRow key={order.id}>
                        <TableCell>{toFaDigits(order.id)}</TableCell>
                        <TableCell>{order.customer}</TableCell>
                        <TableCell>{formatJalali(order.date)}</TableCell>
                        <TableCell align="end">
                          {formatCurrency(order.amount)}
                        </TableCell>
                        <TableCell>
                          <Badge
                            variant="soft"
                            color={STATUS[order.status].color}
                          >
                            {STATUS[order.status].text}
                          </Badge>
                        </TableCell>
                        <TableCell align="end">
                          <OrderActions id={order.id} />
                        </TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </div>
            </>
          )}

          <Pagination>
            <PaginationContent>
              <PaginationItem>
                <PaginationPrevious
                  href="#"
                  aria-disabled={page === 1}
                  onClick={() => setPage((value) => Math.max(1, value - 1))}
                />
              </PaginationItem>
              {Array.from({ length: PAGE_COUNT }, (_, index) => index + 1).map(
                (number) => (
                  <PaginationItem key={number}>
                    <PaginationLink
                      href="#"
                      isActive={number === page}
                      onClick={() => setPage(number)}
                    >
                      {toFaDigits(number)}
                    </PaginationLink>
                  </PaginationItem>
                ),
              )}
              <PaginationItem>
                <PaginationNext
                  href="#"
                  aria-disabled={page === PAGE_COUNT}
                  onClick={() =>
                    setPage((value) => Math.min(PAGE_COUNT, value + 1))
                  }
                />
              </PaginationItem>
            </PaginationContent>
          </Pagination>
        </ControlSizeProvider>
      </main>
    </ControlSizeProvider>
  );
}