پنلِ مدیریتِ سفارش
پیشنمایش تمامصفحهتیتر و شمارِ سفارش، جستوجو و سفارشِ تازه، جدولِ پرداده با کانالِ برچسبدار و کلیدِ فعال/غیرفعال، و صفحهبندیِ شمارهدار با برو به صفحه و ردیف در صفحه.
۱. نصب کامپوننتها
این بلاک از ۱۲ آیتم دیگ استفاده میکند: 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
)
}