بلاک‌ها · رزرو و خدمات

استعلام خلافی با پلاک و کد ملی، جدول تخلفات و پرداخت.

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

این بلاک از ۹ آیتم دیگ استفاده می‌کند: buttoncheckboxcontrol-sizeempty-statelabellicense-platepersiantext-fieldtoast

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add 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/empty-state.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/license-plate.json https://docs.digdesign.ir/r/persian.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/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/license-plate.json https://docs.digdesign.ir/r/persian.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/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/license-plate.json https://docs.digdesign.ir/r/persian.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/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/license-plate.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json

۲. کد صفحه

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

vehicle-fines.tsx
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { EmptyState, EmptyStateDescription, EmptyStateTitle } from "@/components/ui/empty-state"
import { Label } from "@/components/ui/label"
import { LicensePlate } from "@/components/ui/license-plate"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { formatCurrency, formatJalaliDateTime, isValidNationalId, toFaDigits } from "@/lib/persian"

const FINES = [
  { id: "f1", title: "عبور از چراغ قرمز", place: "تهران، میدان ونک", date: "2026-09-12T18:20:00", amount: 1_000_000, camera: true },
  { id: "f2", title: "سرعتِ غیرمجاز", place: "بزرگراه همت، شرق به غرب", date: "2026-09-05T07:42:00", amount: 600_000, camera: true },
  { id: "f3", title: "توقف ممنوع", place: "تهران، خیابان ولیعصر", date: "2026-08-30T11:05:00", amount: 300_000, camera: false },
]

/**
 * استعلامِ خلافی: فرمِ پلاک و کد ملی در ستونِ کنار؛ نتیجه با کارتِ تیرهٔ مجموع، فهرستِ
 * تخلف‌ها با انتخابِ چندتایی، و پرداختِ فقط موارد انتخاب‌شده. اگر خلافی نبود، حالتِ خالی.
 */
export default function VehicleFinesBlock() {
  const [loading, setLoading] = React.useState(false)
  const [result, setResult] = React.useState<typeof FINES | null>(null)
  const [selected, setSelected] = React.useState<string[]>([])
  const total = result?.filter((fine) => selected.includes(fine.id)).reduce((sum, fine) => sum + fine.amount, 0) ?? 0

  async function inquire(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setLoading(true)
    await new Promise((resolve) => setTimeout(resolve, 900))
    setLoading(false)
    setResult(FINES)
    setSelected(FINES.map((fine) => fine.id))
  }

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto grid grid-cols-1 w-full max-w-5xl items-start gap-8 p-4 sm:p-6 lg:grid-cols-[22rem_minmax(0,1fr)]">
        <form onSubmit={inquire} className="flex flex-col gap-6 rounded-xl border border-border bg-card p-6">
          <div className="flex flex-col gap-1">
            <h1 className="text-2xl font-bold">استعلامِ خلافی</h1>
            <p className="text-sm text-muted-foreground">پلاک و کد ملی مالک را وارد کنید.</p>
          </div>
          <div className="flex flex-col gap-2">
            <Label id="plate-label">پلاک خودرو</Label>
            <LicensePlate aria-labelledby="plate-label" name="plate" required onValueChange={() => setResult(null)} />
          </div>
          <TextField
            label="کد ملی مالک"
            name="nationalId"
            inputMode="numeric"
            dir="ltr"
            maxLength={10}
            required
            validate={(value) => (isValidNationalId(value) ? undefined : value.length === 10 ? "کد ملی معتبر نیست." : "کد ملی باید ۱۰ رقم باشد.")}
          />
          <Button type="submit" fullWidth loading={loading} className="mt-2">
            استعلام
          </Button>
        </form>

        {!result ? (
          <EmptyState className="rounded-xl bg-subtle py-16">
            <EmptyStateTitle>هنوز استعلامی نگرفته‌اید</EmptyStateTitle>
            <EmptyStateDescription>بعد از استعلام، تخلف‌ها همین‌جا می‌آیند و می‌توانید یک‌جا پرداخت کنید.</EmptyStateDescription>
          </EmptyState>
        ) : (
          <section aria-label="نتیجهٔ استعلام" className="flex flex-col gap-6">
            <div className="flex flex-wrap items-end justify-between gap-4 rounded-xl bg-inverse p-6 text-[var(--dig-white)] [--muted-foreground:var(--dig-neutral-400)]">
              <div className="flex flex-col gap-1">
                <span className="text-sm text-muted-foreground">{toFaDigits(result.length)} تخلفِ پرداخت‌نشده</span>
                <span className="text-3xl font-black tabular-nums">{formatCurrency(result.reduce((sum, fine) => sum + fine.amount, 0))}</span>
              </div>
              <span className="text-sm text-muted-foreground">به‌روز تا امروز</span>
            </div>

            <ul className="flex flex-col gap-3">
              {result.map((fine) => (
                <li key={fine.id} className="flex items-start gap-3 rounded-xl border border-border p-4 transition-colors has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary-subtle">
                  <Checkbox
                    id={`fine-${fine.id}`}
                    checked={selected.includes(fine.id)}
                    onCheckedChange={(checked) => setSelected((list) => (checked ? [...list, fine.id] : list.filter((id) => id !== fine.id)))}
                    className="mt-1"
                  />
                  <Label htmlFor={`fine-${fine.id}`} className="flex flex-1 flex-col items-start gap-1">
                    <span className="font-semibold">{fine.title}</span>
                    <span className="text-sm font-normal text-muted-foreground">
                      {formatJalaliDateTime(fine.date)} · {fine.place}
                    </span>
                    {fine.camera && <span className="text-xs font-normal text-brand-secondary">ثبت با دوربین؛ تصویر در جزئیات</span>}
                  </Label>
                  <span className="font-bold tabular-nums">{formatCurrency(fine.amount)}</span>
                </li>
              ))}
            </ul>

            <div className="flex flex-wrap items-center justify-between gap-4 border-t border-border pt-8">
              <span className="text-sm text-muted-foreground">{toFaDigits(selected.length)} مورد انتخاب شده</span>
              <Button disabled={selected.length === 0} onClick={() => toast.success("پرداخت انجام شد.")}>
                پرداختِ {formatCurrency(total)}
              </Button>
            </div>
          </section>
        )}
      </main>
    </ControlSizeProvider>
  )
}