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

پنلِ مدیریتِ سفارش

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

تیتر و شمارِ سفارش، جست‌وجو و سفارشِ تازه، جدولِ پرداده با کانالِ برچسب‌دار و کلیدِ فعال/غیرفعال، و صفحه‌بندیِ شماره‌دار با برو به صفحه و ردیف در صفحه.

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

این بلاک از ۱۲ آیتم دیگ استفاده می‌کند: badgebuttoncontrol-sizeiconspaginationpersiansearch-fieldselectswitchtabletext-fieldtoast

پکیج‌منیجر پیش‌فرض 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/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/switch.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/text-field.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/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/switch.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/text-field.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/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/switch.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/text-field.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/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/switch.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json

۲. کد صفحه

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

orders-admin.tsx
"use client"

import * as React from "react"

import { PlusIcon, SearchIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Pagination, PaginationContent, PaginationEllipsis, 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 { Switch } from "@/components/ui/switch"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { formatCurrency, toFaDigits, toSearchKey } from "@/lib/persian"

type Channel = "web" | "app" | "store"
const CHANNEL: Record<Channel, { label: string; color: "info" | "violet" | "orange" }> = {
  web: { label: "سایت", color: "info" },
  app: { label: "اپ", color: "violet" },
  store: { label: "حضوری", color: "orange" },
}
const ORDERS: { id: string; customer: string; phone: string; channel: Channel; active: boolean; created: string; due: string; amount: number }[] = [
  { id: "۱۰۴۲", customer: "نگار احمدی", phone: "09123456789", channel: "web", active: true, created: "۱۴۰۵/۰۷/۱۳", due: "۱۴۰۵/۰۷/۱۵", amount: 1_290_000 },
  { id: "۱۰۴۱", customer: "کافه رست", phone: "09121112233", channel: "store", active: true, created: "۱۴۰۵/۰۷/۱۳", due: "۱۴۰۵/۰۷/۱۴", amount: 4_850_000 },
  { id: "۱۰۴۰", customer: "امیر رضایی", phone: "09359876543", channel: "app", active: false, created: "۱۴۰۵/۰۷/۱۲", due: "۱۴۰۵/۰۷/۱۶", amount: 640_000 },
  { id: "۱۰۳۹", customer: "فروشگاهِ بوم", phone: "09127654321", channel: "web", active: true, created: "۱۴۰۵/۰۷/۱۲", due: "۱۴۰۵/۰۷/۱۵", amount: 2_100_000 },
  { id: "۱۰۳۸", customer: "سارا محمدی", phone: "09191234567", channel: "app", active: true, created: "۱۴۰۵/۰۷/۱۱", due: "۱۴۰۵/۰۷/۱۳", amount: 980_000 },
  { id: "۱۰۳۷", customer: "علی کاظمی", phone: "09381239876", channel: "store", active: false, created: "۱۴۰۵/۰۷/۱۱", due: "۱۴۰۵/۰۷/۱۴", amount: 350_000 },
]
const PAGES = 50

/**
 * پنلِ مدیریتِ سفارش‌ها با جدولِ پرداده: تیتر و شمارِ سفارش‌ها، جست‌وجو و «سفارشِ تازه»، جدولی با کانالِ برچسب‌دار، کلیدِ فعال/غیرفعال روی هر ردیف (با پیامِ تأیید)، و
 * صفحه‌بندیِ شماره‌دار با «برو به صفحه» و تعدادِ ردیف در هر صفحه. جدول در موبایل اسکرولِ خودش را دارد.
 */
export default function OrdersAdminBlock() {
  const [query, setQuery] = React.useState("")
  const [rows, setRows] = React.useState(ORDERS)
  const [page, setPage] = React.useState(1)
  const [jump, setJump] = React.useState("")
  const visible = rows.filter((row) => toSearchKey(row.customer + row.id).includes(toSearchKey(query)))

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-6xl flex-col gap-8 p-4 sm:p-6">
        <header className="flex items-start justify-between gap-4">
          <div className="flex flex-col gap-1">
            <h1 className="text-2xl leading-snug font-black">سفارش‌ها</h1>
            <p className="text-muted-foreground">
              {toFaDigits(rows.length)} سفارش · {toFaDigits(rows.filter((row) => row.active).length)} فعال
            </p>
          </div>
          <Button className="shrink-0">
            <PlusIcon />
            سفارشِ تازه
          </Button>
        </header>

        <section aria-label="فهرستِ سفارش‌ها" className="flex flex-col gap-6">
          <SearchField aria-label="جست‌وجو در سفارش‌ها" placeholder="نام مشتری یا شمارهٔ سفارش" value={query} onValueChange={setQuery} className="w-full sm:w-80" icon={<SearchIcon />} />

          {/* موبایل: هر سفارش یک کارتِ جمع‌وجور با همهٔ اطلاعاتِ مهم؛ جدولِ پهن فقط از md به بعد. */}
          <ul className="flex flex-col divide-y divide-border rounded-xl border border-border bg-card md:hidden">
            {visible.length === 0 ? (
              <li className="p-8 text-center text-muted-foreground">سفارشی با «{query}» پیدا نشد.</li>
            ) : (
              visible.map((row) => (
                <li key={row.id} className="flex flex-col gap-3 p-4">
                  <div className="flex items-start justify-between gap-3">
                    <span className="flex flex-col gap-0.5">
                      <span className="font-bold">{row.customer}</span>
                      <span className="text-sm text-muted-foreground tabular-nums">
                        #{row.id} · تحویل {row.due}
                      </span>
                    </span>
                    <span className="font-bold tabular-nums">{formatCurrency(row.amount)}</span>
                  </div>
                  <div className="flex items-center justify-between gap-3">
                    <Badge variant="soft" color={CHANNEL[row.channel].color} className="rounded-full">
                      {CHANNEL[row.channel].label}
                    </Badge>
                    <label className="flex items-center gap-2 text-sm text-muted-foreground">
                      {row.active ? "فعال" : "متوقف"}
                      <Switch
                        checked={row.active}
                        aria-label={`فعال بودنِ سفارشِ ${row.id}`}
                        onCheckedChange={(checked) => {
                          setRows((list) => list.map((item) => (item.id === row.id ? { ...item, active: checked } : item)))
                          toast.success(checked ? `سفارشِ ${row.id} فعال شد.` : `سفارشِ ${row.id} متوقف شد.`)
                        }}
                      />
                    </label>
                  </div>
                </li>
              ))
            )}
          </ul>

          <div className="hidden rounded-xl border border-border bg-card md:block">
            <Table className="min-w-[52rem]">
              <TableHeader>
                <TableRow className="bg-subtle hover:bg-subtle">
                  <TableHead>سفارش</TableHead>
                  <TableHead>مشتری</TableHead>
                  <TableHead>موبایل</TableHead>
                  <TableHead>کانال</TableHead>
                  <TableHead>فعال</TableHead>
                  <TableHead>ثبت</TableHead>
                  <TableHead>تحویل</TableHead>
                  <TableHead className="text-end">مبلغ</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {visible.length === 0 ? (
                  <TableRow className="hover:bg-card">
                    <TableCell colSpan={8} className="py-12 text-center text-muted-foreground">
                      سفارشی با «{query}» پیدا نشد.
                    </TableCell>
                  </TableRow>
                ) : (
                  visible.map((row) => (
                    <TableRow key={row.id}>
                      <TableCell className="font-semibold tabular-nums">#{row.id}</TableCell>
                      <TableCell className="font-medium">{row.customer}</TableCell>
                      <TableCell>
                        <bdi dir="ltr" className="text-muted-foreground tabular-nums">
                          {toFaDigits(row.phone)}
                        </bdi>
                      </TableCell>
                      <TableCell>
                        <Badge variant="soft" color={CHANNEL[row.channel].color} className="rounded-full">
                          {CHANNEL[row.channel].label}
                        </Badge>
                      </TableCell>
                      <TableCell>
                        <Switch
                          checked={row.active}
                          aria-label={`فعال بودنِ سفارشِ ${row.id}`}
                          onCheckedChange={(checked) => {
                            setRows((list) => list.map((item) => (item.id === row.id ? { ...item, active: checked } : item)))
                            toast.success(checked ? `سفارشِ ${row.id} فعال شد.` : `سفارشِ ${row.id} متوقف شد.`)
                          }}
                        />
                      </TableCell>
                      <TableCell className="text-muted-foreground tabular-nums">{row.created}</TableCell>
                      <TableCell className="text-muted-foreground tabular-nums">{row.due}</TableCell>
                      <TableCell className="text-end font-semibold tabular-nums">{formatCurrency(row.amount)}</TableCell>
                    </TableRow>
                  ))
                )}
              </TableBody>
            </Table>
          </div>

          {/* صفحه‌بندی: شماره‌ها، پرش به صفحه و تعدادِ ردیف؛ در موبایل زیرِ هم. */}
          <div className="flex flex-col items-center gap-6 lg:flex-row lg:justify-between">
            <Pagination className="mx-0 w-auto">
              <PaginationContent className="flex-wrap justify-center">
                <PaginationItem>
                  <PaginationPrevious href="#" onClick={(event) => (event.preventDefault(), setPage((value) => Math.max(1, value - 1)))} />
                </PaginationItem>
                {[1, 2, 3].map((number) => (
                  <PaginationItem key={number}>
                    <PaginationLink href="#" isActive={number === page} onClick={(event) => (event.preventDefault(), setPage(number))}>
                      {toFaDigits(number)}
                    </PaginationLink>
                  </PaginationItem>
                ))}
                <PaginationItem>
                  <PaginationEllipsis />
                </PaginationItem>
                <PaginationItem>
                  <PaginationLink href="#" isActive={page === PAGES} onClick={(event) => (event.preventDefault(), setPage(PAGES))}>
                    {toFaDigits(PAGES)}
                  </PaginationLink>
                </PaginationItem>
                <PaginationItem>
                  <PaginationNext href="#" onClick={(event) => (event.preventDefault(), setPage((value) => Math.min(PAGES, value + 1)))} />
                </PaginationItem>
              </PaginationContent>
            </Pagination>
            <div className="flex items-end justify-center gap-2">
              <form
                className="flex items-end gap-2"
                onSubmit={(event) => {
                  event.preventDefault()
                  const target = Number(jump)
                  if (target >= 1 && target <= PAGES) setPage(target)
                  else toast.error(`صفحه باید بینِ ۱ و ${toFaDigits(PAGES)} باشد.`)
                }}
              >
                <TextField aria-label="برو به صفحه" placeholder="برو به صفحه" inputMode="numeric" dir="rtl" value={jump} onValueChange={setJump} className="w-28" />
                <Button type="submit" variant="outline">
                  برو
                </Button>
              </form>
              <ControlSizeProvider size="xl" asChild>
                <Select defaultValue="10">
                  <SelectTrigger aria-label="ردیف در هر صفحه" className="w-auto gap-2">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="10">۱۰ ردیف</SelectItem>
                    <SelectItem value="25">۲۵ ردیف</SelectItem>
                    <SelectItem value="50">۵۰ ردیف</SelectItem>
                  </SelectContent>
                </Select>
              </ControlSizeProvider>
            </div>
          </div>
        </section>
      </main>
    </ControlSizeProvider>
  )
}