diff --git a/src/app/admin/coupons/page.tsx b/src/app/admin/coupons/page.tsx new file mode 100644 index 0000000..a719390 --- /dev/null +++ b/src/app/admin/coupons/page.tsx @@ -0,0 +1,5 @@ +import AdminCoupons from "@/views/AdminCoupons"; + +export default function AdminCouponsRoute() { + return ; +} diff --git a/src/views/AdminCoupons.tsx b/src/views/AdminCoupons.tsx new file mode 100644 index 0000000..1414b51 --- /dev/null +++ b/src/views/AdminCoupons.tsx @@ -0,0 +1,241 @@ +"use client"; + +import * as React from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Edit3, Plus, Trash2 } from "lucide-react"; +import AdminDateTimeField from "@/components/AdminDateTimeField"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import type { DiscountCodeSchema, DiscountCodeWriteSchema } from "@/lib/types"; +import { api } from "@/lib/api"; +import { formatNumberPersian, resolveErrorMessage } from "@/lib/utils"; +import { useToast } from "@/hooks/use-toast"; + +const PAGE_SIZE = 20; + +const emptyForm: DiscountCodeWriteSchema = { + code: "", + type: "percent", + value: 0, + max_discount: null, + is_active: true, + starts_at: null, + ends_at: null, + usage_limit_total: null, + usage_limit_per_user: null, + min_amount: null, + applicable_event_ids: [], +}; + +export default function AdminCoupons() { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [search, setSearch] = React.useState(""); + const [debouncedSearch, setDebouncedSearch] = React.useState(""); + const [page, setPage] = React.useState(1); + const [editing, setEditing] = React.useState(null); + const [open, setOpen] = React.useState(false); + const [form, setForm] = React.useState(emptyForm); + + React.useEffect(() => { + const timer = window.setTimeout(() => setDebouncedSearch(search.trim()), 300); + return () => window.clearTimeout(timer); + }, [search]); + + const query = useQuery({ + queryKey: ["admin", "coupons", debouncedSearch, page], + queryFn: () => api.listDiscountCodes({ search: debouncedSearch || undefined, limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE }), + }); + + const saveMutation = useMutation({ + mutationFn: () => (editing ? api.updateDiscountCode(editing.id, form) : api.createDiscountCode(form)), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["admin", "coupons"] }); + setOpen(false); + toast({ title: "کد تخفیف ذخیره شد", variant: "success" }); + }, + onError: (error) => toast({ title: "خطا", description: resolveErrorMessage(error), variant: "destructive" }), + }); + + const deleteMutation = useMutation({ + mutationFn: (id: number) => api.deleteDiscountCode(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["admin", "coupons"] }); + toast({ title: "کد تخفیف حذف شد", variant: "success" }); + }, + onError: (error) => toast({ title: "خطا", description: resolveErrorMessage(error), variant: "destructive" }), + }); + + const openCreate = () => { + setEditing(null); + setForm(emptyForm); + setOpen(true); + }; + + const openEdit = (item: DiscountCodeSchema) => { + setEditing(item); + setForm({ + code: item.code, + type: item.type, + value: item.value, + max_discount: item.max_discount, + is_active: item.is_active, + starts_at: item.starts_at, + ends_at: item.ends_at, + usage_limit_total: item.usage_limit_total, + usage_limit_per_user: item.usage_limit_per_user, + min_amount: item.min_amount, + applicable_event_ids: item.applicable_event_ids, + }); + setOpen(true); + }; + + const items = query.data?.results ?? []; + const count = query.data?.count ?? 0; + const hasMore = page * PAGE_SIZE < count; + + return ( +
+
+
+

کدهای تخفیف

+

مدیریت کدهای تخفیف رویدادها

+
+ +
+ + + + فهرست کدها + جستجو و مدیریت وضعیت کدهای تخفیف + + + { + setSearch(event.target.value); + setPage(1); + }} + placeholder="جستجو بر اساس کد..." + className="max-w-md" + /> +
+ + + + + + + + + + + + + {query.isLoading ? ( + + ) : items.length === 0 ? ( + + ) : ( + items.map((item) => ( + + + + + + + + + )) + )} + +
کدنوعمقداراستفادهوضعیت
در حال بارگذاری...
کدی یافت نشد.
{item.code}{item.type === "percent" ? "درصدی" : "مبلغ ثابت"}{formatNumberPersian(item.value)}{formatNumberPersian(item.usage_count)} + {item.is_active ? "فعال" : "غیرفعال"} + +
+ + +
+
+
+
+ صفحه {formatNumberPersian(page)} از {formatNumberPersian(Math.max(1, Math.ceil(count / PAGE_SIZE)))} +
+ + +
+
+
+
+ + + + + {editing ? "ویرایش کد تخفیف" : "افزودن کد تخفیف"} + +
+
+ + setForm((current) => ({ ...current, code: event.target.value }))} /> +
+
+ + +
+
+ + setForm((current) => ({ ...current, value: Number(event.target.value) }))} /> +
+
+ + setForm((current) => ({ ...current, max_discount: event.target.value ? Number(event.target.value) : null }))} /> +
+
+ + setForm((current) => ({ ...current, min_amount: event.target.value ? Number(event.target.value) : null }))} /> +
+
+ + setForm((current) => ({ ...current, usage_limit_total: event.target.value ? Number(event.target.value) : null }))} /> +
+
+ + setForm((current) => ({ ...current, usage_limit_per_user: event.target.value ? Number(event.target.value) : null }))} /> +
+
+ + setForm((current) => ({ ...current, is_active: checked }))} /> +
+ setForm((current) => ({ ...current, starts_at: value }))} /> + setForm((current) => ({ ...current, ends_at: value }))} /> +
+ + + + +
+
+
+ ); +}