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

مدیریتِ یک سفارش

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

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

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

این بلاک از ۹ آیتم دیگ استفاده می‌کند: avatarbadgebuttoncardcontrol-sizeiconspersiantextareatoast

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

۲. کد صفحه

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

orders-manage.tsx
"use client"

import * as React from "react"

import { ChatIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, EditIcon, EnvelopeIcon, FileTextIcon, PhoneIcon, TruckIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/toast"
import { formatCurrency, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"

const STEPS = ["بررسیِ سفارش", "آماده‌سازی", "ارسال", "تحویل"]
const ITEMS: { id: string; title: string; sku: string; variant: string; qty: number; price: number; image: StockImageName }[] = [
  { id: "1", title: "گلدانِ سرامیکی دوقلو", sku: "VAS-1000", variant: "سفید · مات", qty: 1, price: 990_000, image: "productVase" },
  { id: "2", title: "دفترِ یادداشتِ جلدسخت", sku: "NTB-5006", variant: "A5 · خط‌دار", qty: 2, price: 120_000, image: "coverNotebook" },
]
const SUBTOTAL = ITEMS.reduce((sum, item) => sum + item.qty * item.price, 0)
type Activity = { id: string; icon: React.ReactNode; title: string; time: string; note?: string; tone: "default" | "note" }
const INITIAL_ACTIVITY: Activity[] = [
  { id: "a1", icon: <CheckIcon />, title: "پرداخت تأیید شد", time: "امروز، ۱۰:۱۳", tone: "default" },
  { id: "a2", icon: <EnvelopeIcon />, title: "رسیدِ سفارش برای مشتری ایمیل شد", time: "امروز، ۱۰:۱۴", tone: "default" },
  { id: "a3", icon: <FileTextIcon />, title: "یادداشتِ آرمیتا رستگار", time: "امروز، ۱۰:۳۰", note: "مشتری خواسته بسته کادوپیچ شود؛ کارت تبریک داخلش نگذاریم.", tone: "note" },
]

/**
 * جزئیاتِ سفارش برای فروشنده (پنلِ مدیریت): شمارهٔ سفارش با وضعیتِ پرداخت و ارسال، تاریخ و منبع، رفتن
 * به سفارشِ قبلی/بعدی؛ مسیرِ چهارمرحله‌ای با «ساختِ برچسبِ ارسال» که سفارش را یک قدم جلو می‌برد و لغو
 * کنارش؛ کالاها و ریزِ پرداخت؛ گزارشِ فعالیت با افزودنِ یادداشت؛ و ستونِ کنار با یادداشتِ مشتری، مشتری
 * با پیام، نشانی با نقشه و اطلاعاتِ تماس.
 */
export default function OrdersManageBlock() {
  const [step, setStep] = React.useState(0)
  const [activity, setActivity] = React.useState(INITIAL_ACTIVITY)
  const [note, setNote] = React.useState("")
  const fulfilled = step >= 2

  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 flex-wrap items-start justify-between gap-x-4 gap-y-4">
          <div className="flex flex-col gap-2">
            <div className="flex flex-wrap items-center gap-2">
              <h1 className="text-2xl leading-snug font-black">
                سفارشِ <bdi className="tabular-nums">{toFaDigits(12567)}</bdi>
              </h1>
              <Badge variant="soft" color="success">
                پرداخت‌شده
              </Badge>
              <Badge variant="soft" color={fulfilled ? "info" : "warning"}>
                {fulfilled ? "ارسال‌شده" : "در انتظارِ ارسال"}
              </Badge>
            </div>
            <p className="text-sm text-muted-foreground">۱۹ مهر ۱۴۰۵ · از فروشگاهِ آنلاین · مشتری: آرمیتا رستگار</p>
          </div>
          <div className="flex items-center gap-2">
            <span className="text-sm text-muted-foreground tabular-nums">
              {toFaDigits(12)} از {toFaDigits(320)}
            </span>
            <Button variant="outline" size="icon" aria-label="سفارشِ قبلی">
              <ChevronRightIcon />
            </Button>
            <Button variant="outline" size="icon" aria-label="سفارشِ بعدی">
              <ChevronLeftIcon />
            </Button>
          </div>
        </header>

        <div className="grid grid-cols-1 items-start gap-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
          <div className="flex min-w-0 flex-col gap-6">
            {/* مسیرِ سفارش و اقدامِ اصلیِ مرحلهٔ فعلی. */}
            <Card className="gap-5">
              <CardContent className="flex flex-col gap-5">
                <div className="flex flex-wrap items-center justify-between gap-2 rounded-lg bg-subtle px-4 py-2.5 text-sm">
                  <span className="text-muted-foreground">
                    ارسال از <span className="font-semibold text-foreground">انبارِ تهران</span>
                  </span>
                  <span className="text-muted-foreground">
                    تحویلِ تخمینی <span className="font-semibold text-foreground">۲۲ تا ۲۴ مهر</span>
                  </span>
                </div>
                <ol className="grid grid-cols-4 gap-2" aria-label="مراحلِ سفارش">
                  {STEPS.map((title, index) => (
                    <li key={title} aria-current={index === step ? "step" : undefined} className="flex flex-col gap-2">
                      <span className={`h-1.5 rounded-full ${index < step ? "bg-success" : index === step ? "bg-primary" : "bg-muted"}`} />
                      <span className={`text-xs sm:text-sm ${index === step ? "font-bold" : index < step ? "font-medium" : "text-muted-foreground"}`}>{title}</span>
                    </li>
                  ))}
                </ol>
                <div className="flex flex-col-reverse gap-3 border-t border-border pt-5 sm:flex-row sm:items-center sm:justify-between">
                  <Button variant="ghost" className="text-destructive hover:text-destructive" disabled={step >= 3}>
                    لغوِ سفارش
                  </Button>
                  <Button
                    disabled={step >= 3}
                    onClick={() => {
                      const next = Math.min(step + 1, 3)
                      setStep(next)
                      setActivity((list) => [{ id: `s${next}`, icon: <TruckIcon />, title: `سفارش به «${STEPS[next]}» رفت`, time: "همین حالا", tone: "default" }, ...list])
                      toast.success(`سفارش به «${STEPS[next]}» رفت.`)
                    }}
                  >
                    <TruckIcon />
                    {step === 0 ? "تأیید و آماده‌سازی" : step === 1 ? "ساختِ برچسبِ ارسال" : step === 2 ? "ثبتِ تحویل" : "تحویل شد"}
                  </Button>
                </div>
              </CardContent>
            </Card>

            <Card className="gap-0 py-0">
              <CardHeader className="py-5">
                <CardTitle className="text-base">کالاها</CardTitle>
                <CardAction>
                  <span className="text-sm text-muted-foreground">{fulfilled ? "ارسال‌شده" : "ارسال‌نشده"}</span>
                </CardAction>
              </CardHeader>
              <ul className="flex flex-col divide-y divide-border border-t border-border">
                {ITEMS.map((item) => (
                  <li key={item.id} className="flex gap-4 px-6 py-4">
                    {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                    <img src={stockImages[item.image].src} alt="" className="size-16 shrink-0 rounded-lg object-cover" />
                    <span className="flex min-w-0 flex-1 flex-col gap-0.5">
                      <span className="font-semibold">{item.title}</span>
                      <span className="text-sm text-muted-foreground">
                        <bdi dir="ltr">{item.sku}</bdi> · {item.variant} · {toFaDigits(item.qty)} عدد
                      </span>
                    </span>
                    <span className="shrink-0 font-bold tabular-nums">{formatCurrency(item.qty * item.price)}</span>
                  </li>
                ))}
              </ul>
              <dl className="flex flex-col gap-2 border-t border-border bg-subtle px-6 py-5 text-sm">
                <div className="flex justify-between gap-4">
                  <dt className="text-muted-foreground">جمعِ {toFaDigits(ITEMS.length)} قلم</dt>
                  <dd className="tabular-nums">{formatCurrency(SUBTOTAL)}</dd>
                </div>
                <div className="flex justify-between gap-4">
                  <dt className="text-muted-foreground">ارسال</dt>
                  <dd>رایگان (انتخابِ مشتری)</dd>
                </div>
                <div className="flex justify-between gap-4">
                  <dt className="text-muted-foreground">روشِ پرداخت</dt>
                  <dd>درگاهِ بانکی · کارتِ ۶۰۳۷</dd>
                </div>
                <div className="flex justify-between gap-4 border-t border-border pt-3 text-base">
                  <dt className="font-bold">پرداخت‌شده</dt>
                  <dd className="font-black tabular-nums">{formatCurrency(SUBTOTAL)}</dd>
                </div>
              </dl>
            </Card>

            <section aria-labelledby="activity-title" className="flex flex-col gap-4">
              <h2 id="activity-title" className="text-lg font-bold">
                فعالیت‌ها
              </h2>
              <form
                className="flex flex-col gap-3"
                onSubmit={(event) => {
                  event.preventDefault()
                  if (!note.trim()) return
                  setActivity((list) => [{ id: `n${Date.now()}`, icon: <FileTextIcon />, title: "یادداشتِ شما", time: "همین حالا", note: note.trim(), tone: "note" }, ...list])
                  setNote("")
                }}
              >
                <Textarea aria-label="یادداشتِ داخلی" placeholder="یادداشتی برای تیم بنویس؛ مشتری آن را نمی‌بیند." value={note} onChange={(event) => setNote(event.target.value)} rows={2} />
                <Button type="submit" variant="outline" className="self-end" disabled={!note.trim()}>
                  افزودنِ یادداشت
                </Button>
              </form>
              <ol className="flex flex-col">
                {activity.map((item, index) => (
                  <li key={item.id} className="relative flex gap-3 pb-5 last:pb-0">
                    {index < activity.length - 1 && <span aria-hidden className="absolute start-5 top-11 bottom-0 w-px bg-border" />}
                    <span aria-hidden className="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted [&>svg]:size-4">
                      {item.icon}
                    </span>
                    <span className="flex min-w-0 flex-1 flex-col gap-1.5 pt-1">
                      <span className="flex flex-wrap items-baseline justify-between gap-2">
                        <span className="font-semibold">{item.title}</span>
                        <span className="text-xs text-muted-foreground">{item.time}</span>
                      </span>
                      {item.note && <span className="rounded-lg bg-[color-mix(in_oklab,var(--dig-amber-400)_16%,var(--card))] p-3 text-sm leading-6">{item.note}</span>}
                    </span>
                  </li>
                ))}
              </ol>
            </section>
          </div>

          <aside className="flex flex-col gap-4">
            <Card className="gap-3 bg-[color-mix(in_oklab,var(--dig-amber-400)_10%,var(--card))]">
              <CardHeader>
                <CardTitle className="text-base">یادداشتِ مشتری</CardTitle>
                <CardAction>
                  <Button variant="ghost" size="icon" aria-label="ویرایشِ یادداشت" className="-me-2">
                    <EditIcon />
                  </Button>
                </CardAction>
              </CardHeader>
              <CardContent className="text-sm leading-6">لطفاً کادوپیچ کنید؛ برای تولدِ مادرم است.</CardContent>
            </Card>

            <Card className="gap-4">
              <CardHeader>
                <CardTitle className="text-base">مشتری</CardTitle>
              </CardHeader>
              <CardContent className="flex flex-col gap-5">
                <div className="flex items-center gap-3">
                  <Avatar alt="آرمیتا رستگار" src={stockImages.personWoman1.src} className="size-11" />
                  <span className="flex flex-1 flex-col">
                    <span className="font-bold">آرمیتا رستگار</span>
                    <span className="text-sm text-muted-foreground">{toFaDigits(4)} سفارشِ قبلی</span>
                  </span>
                  <Button variant="outline" size="icon" aria-label="پیام به مشتری">
                    <ChatIcon />
                  </Button>
                </div>
                <div className="flex flex-col gap-3 border-t border-border pt-5">
                  <span className="flex items-center justify-between">
                    <span className="text-sm font-semibold">نشانیِ ارسال</span>
                    <Button variant="ghost" size="icon" aria-label="ویرایشِ نشانی" className="-me-2">
                      <EditIcon />
                    </Button>
                  </span>
                  {/* eslint-disable-next-line @next/next/no-img-element -- نقشهٔ تزئینی؛ در محصول نقشهٔ واقعی می‌آید */}
                  <img src="/blocks/contact-map.svg" alt="نقشهٔ نشانیِ ارسال" className="aspect-[2/1] w-full rounded-lg object-cover dark:brightness-[0.8]" />
                  <p className="text-sm leading-6">تهران، خیابانِ ولیعصر، کوچهٔ نسترن، پلاکِ ۱۲، واحدِ ۴</p>
                </div>
                <div className="flex flex-col gap-2 border-t border-border pt-5">
                  <span className="text-sm font-semibold">تماس</span>
                  <a href="mailto:armita@dig.ir" className="flex items-center gap-2 text-sm hover:underline [&>svg]:size-4 [&>svg]:text-muted-foreground">
                    <EnvelopeIcon aria-hidden />
                    <bdi dir="ltr">armita@dig.ir</bdi>
                  </a>
                  <a href="tel:09123456789" className="flex items-center gap-2 text-sm hover:underline [&>svg]:size-4 [&>svg]:text-muted-foreground">
                    <PhoneIcon aria-hidden />
                    <bdi dir="ltr">۰۹۱۲ ۳۴۵ ۶۷۸۹</bdi>
                  </a>
                </div>
              </CardContent>
            </Card>
          </aside>
        </div>
      </main>
    </ControlSizeProvider>
  )
}