diff --git a/src/features/auth/ui/AuthLogin.tsx b/src/features/auth/ui/AuthLogin.tsx index 515d215..9be1988 100644 --- a/src/features/auth/ui/AuthLogin.tsx +++ b/src/features/auth/ui/AuthLogin.tsx @@ -39,7 +39,6 @@ const AuthLogin = () => { richColors: true, position: "top-center", }); - console.log(res.data.data.access); saveToken(res.data.data.access); navigate("dashboard"); diff --git a/src/features/order/lib/api.ts b/src/features/order/lib/api.ts new file mode 100644 index 0000000..dad682c --- /dev/null +++ b/src/features/order/lib/api.ts @@ -0,0 +1,30 @@ +import type { OrdersResponse } from "@/features/order/lib/type"; +import httpClient from "@/shared/config/api/httpClient"; +import { API_URLS } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const order_api = { + async list(params: { + page: number; + page_size: number; + }): Promise> { + const res = await httpClient.get(`${API_URLS.OrdersList}`, { params }); + return res; + }, + + async status_update({ + body, + id, + }: { + id: string | number; + body: { status: string }; + }) { + const res = await httpClient.post(API_URLS.OrderStatus(id), body); + return res; + }, + + async delete_order(id: number | string) { + const res = await httpClient.delete(API_URLS.OrdersDelete(id)); + return res; + }, +}; diff --git a/src/features/order/lib/type.ts b/src/features/order/lib/type.ts new file mode 100644 index 0000000..5fae92c --- /dev/null +++ b/src/features/order/lib/type.ts @@ -0,0 +1,50 @@ +export interface User { + id: string; + first_name: string; + last_name: string; + username: string; +} + +export interface Product { + id: string; + name_uz: string; + name_ru: string; + price: number; + code: string | null; + unity: string; +} + +export interface Item { + id: string; + quantity: number; + price: number; + product: Product; +} + +export interface Order { + id: string; + order_number: number; + status: string; + total_price: number; + user: User; + payment_type: string; + delivery_type: string; + delivery_price: number; + contact_number: string; + comment: string; + name: string; + items: Item[]; + created_at: string; + long: number | null; + lat: number | null; +} + +export interface OrdersResponse { + total: number; + page: number; + page_size: number; + total_pages: number; + has_next: boolean; + has_previous: boolean; + results: Order[]; +} diff --git a/src/features/order/ui/OrderDelete.tsx b/src/features/order/ui/OrderDelete.tsx new file mode 100644 index 0000000..1470e04 --- /dev/null +++ b/src/features/order/ui/OrderDelete.tsx @@ -0,0 +1,89 @@ +import { order_api } from "@/features/order/lib/api"; +import type { Order } from "@/features/order/lib/type"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Loader2, Trash, X } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + opneDelete: boolean; + setOpenDelete: Dispatch>; + setOrderDelete: Dispatch>; + orderDelete: Order | null; +} + +const OrderDelete = ({ + opneDelete, + orderDelete, + setOpenDelete, + setOrderDelete, +}: Props) => { + const queryClient = useQueryClient(); + + const { mutate: deleteUser, isPending } = useMutation({ + mutationFn: (id: string | number) => order_api.delete_order(id), + + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["order_list"] }); + toast.success(`Mahsulot o'chirildi`); + setOpenDelete(false); + setOrderDelete(null); + }, + onError: (err: AxiosError) => { + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + + return ( + + + + Buyurtmani o'chirish + + Siz rostan ham {orderDelete?.order_number} sonli buyurtmani + o'chirmoqchimisiz? + + + + + + + + + ); +}; + +export default OrderDelete; diff --git a/src/features/order/ui/OrderDetail.tsx b/src/features/order/ui/OrderDetail.tsx new file mode 100644 index 0000000..42faaa6 --- /dev/null +++ b/src/features/order/ui/OrderDetail.tsx @@ -0,0 +1,248 @@ +import type { Order } from "@/features/order/lib/type"; +import formatPhone from "@/shared/lib/formatPhone"; +import { Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; +import { + Calendar, + CreditCard, + MapIcon, + MessageSquare, + Package, + Phone, + Truck, + User, + X, +} from "lucide-react"; +import { type Dispatch, type SetStateAction } from "react"; + +const deliveryTypeLabel: Record = { + YANDEX_GO: "Yandex Go", + DELIVERY_COURIES: "Kuryer orqali yetkazish", + PICKUP: "O‘zi olib ketish", +}; + +interface Props { + detail: boolean; + setDetail: Dispatch>; + order: Order | null; +} + +const OrderDetail = ({ detail, setDetail, order }: Props) => { + if (!detail || !order) return null; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString("uz-UZ", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; + + const formatPrice = (price: number) => { + return new Intl.NumberFormat("uz-UZ").format(price) + " so'm"; + }; + + const getStatusColor = (status: string) => { + const colors: Record = { + pending: "bg-yellow-100 text-yellow-800", + confirmed: "bg-blue-100 text-blue-800", + preparing: "bg-purple-100 text-purple-800", + delivering: "bg-indigo-100 text-indigo-800", + completed: "bg-green-100 text-green-800", + cancelled: "bg-red-100 text-red-800", + }; + return colors[status] || "bg-gray-100 text-gray-800"; + }; + + return ( + <> +
setDetail(false)} + /> + +
+
+
+

+ Buyurtma #{order.order_number} +

+

ID: {order.id}

+
+ +
+ +
+
+
+ + {order.status.toUpperCase()} + +
+ + {formatDate(order.created_at)} +
+
+ +
+
+

Jami summa

+

+ {formatPrice(order.total_price)} +

+
+
+

Yetkazish narxi

+

+ {formatPrice(order.delivery_price)} +

+
+
+
+ +
+

+ + Mijoz ma'lumotlari +

+
+
+ Ism: + {order.name} +
+ +
+
+ +
+

+ + Yetkazish ma'lumotlari +

+
+
+ Turi: + + {deliveryTypeLabel[order.delivery_type] ?? + order.delivery_type} + +
+
+
+
+ {order.lat && order.long && ( +
+

+ + Manzil +

+ +
+ + + + + +
+
+ )} +
+ +
+

+ + To'lov turi +

+

+ {order.payment_type === "CASH" ? "Naxt" : "Karta orqali"} +

+
+ + {order.comment && ( +
+

+ + Izoh +

+

{order.comment}

+
+ )} + +
+

+ + Mahsulotlar ({order.items.length}) +

+
+ {order.items.map((item, index) => ( +
+
+

+ {item.product?.name_uz || "Noma'lum mahsulot"} +

+

+ {formatPrice(item.product.price)} × {item.quantity} +

+
+
+

+ {formatPrice(item.product.price * item.quantity)} +

+
+
+ ))} +
+ +
+
+ Mahsulotlar: + + {formatPrice(order.total_price - order.delivery_price)} + +
+
+ Yetkazish: + + {formatPrice(order.delivery_price)} + +
+
+ Jami: + {formatPrice(order.total_price)} +
+
+
+
+
+ + ); +}; + +export default OrderDetail; diff --git a/src/features/order/ui/OrderList.tsx b/src/features/order/ui/OrderList.tsx new file mode 100644 index 0000000..0f61a20 --- /dev/null +++ b/src/features/order/ui/OrderList.tsx @@ -0,0 +1,75 @@ +import { order_api } from "@/features/order/lib/api"; +import type { Order } from "@/features/order/lib/type"; +import OrderDelete from "@/features/order/ui/OrderDelete"; +import OrderDetail from "@/features/order/ui/OrderDetail"; +import OrderTable from "@/features/order/ui/OrderTable"; +import Pagination from "@/shared/ui/pagination"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + +const OrderList = () => { + const [currentPage, setCurrentPage] = useState(1); + const limit = 20; + const { data, isLoading, isError, isFetching } = useQuery({ + queryKey: ["order_list", currentPage], + queryFn: () => { + return order_api.list({ + page: currentPage, + page_size: limit, + }); + }, + select(data) { + return data.data; + }, + }); + + const [orderDetail, setOrderDetail] = useState(null); + const [detail, setDetail] = useState(false); + + const [openDelete, setOpenDelete] = useState(false); + const [planDelete, setPlanDelete] = useState(null); + + const handleDelete = (id: Order) => { + setOpenDelete(true); + setPlanDelete(id); + }; + + return ( +
+
+

Mahsulotlar

+ + +
+ + + + + + +
+ ); +}; + +export default OrderList; diff --git a/src/features/order/ui/OrderTable.tsx b/src/features/order/ui/OrderTable.tsx new file mode 100644 index 0000000..8fd2dc7 --- /dev/null +++ b/src/features/order/ui/OrderTable.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { order_api } from "@/features/order/lib/api"; +import type { Order } from "@/features/order/lib/type"; +import formatPhone from "@/shared/lib/formatPhone"; +import formatPrice from "@/shared/lib/formatPrice"; +import { Button } from "@/shared/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui/table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Eye, Loader2, Trash } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + orders: Order[] | []; + isLoading: boolean; + isFetching: boolean; + isError: boolean; + setDetailOpen: Dispatch>; + handleDelete: (order: Order) => void; + setOrderDetail: Dispatch>; +} + +const deliveryTypeLabel: Record = { + YANDEX_GO: "Yandex Go", + DELIVERY_COURIES: "Kuryer orqali yetkazish", + PICKUP: "O‘zi olib ketish", +}; + +type OrderStatus = + | "NEW" + // "PROCESSING" | + | "DONE"; +// | "CANCELLED"; + +const orderStatusLabel: Record = { + NEW: "Yangi", + // PROCESSING: "Jarayonda", + DONE: "Yakunlangan", + // CANCELLED: "Bekor qilingan", +}; + +const OrderTable = ({ + orders, + isLoading, + isFetching, + isError, + setDetailOpen, + handleDelete, + setOrderDetail, +}: Props) => { + const queryClient = useQueryClient(); + + const { mutate } = useMutation({ + mutationFn: ({ + body, + id, + }: { + body: { status: string }; + id: number | string; + }) => order_api.status_update({ body, id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["order_list"] }); + }, + onError: () => { + toast.error("Xatolik yuz berdi status o'zgarmadi", { + richColors: true, + position: "top-center", + }); + }, + }); + + const handleStatusChange = async ( + orderId: number | string, + status: OrderStatus, + ) => { + mutate({ id: orderId, body: { status: status } }); + }; + + if (isLoading || isFetching) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+ Maʼlumotlarni yuklashda xatolik yuz berdi +
+ ); + } + + return ( +
+ + + + # + Order № + Foydalanuvchi + Kontakt + Toʻlov turi + Yetkazib berish + Umumiy narx + Izoh + Holat + Harakatlar + + + + + {orders.map((order, index) => ( + + {index + 1} + {order.order_number} + + {order.user.first_name} {order.user.last_name} ( + {order.user.username}) + + {formatPhone(order.contact_number)} + + {order.payment_type === "CASH" ? "Naxt" : "Karta orqali"} + + + {deliveryTypeLabel[order.delivery_type] ?? order.delivery_type} + + {formatPrice(order.total_price, true)} + {order.comment || "-"} + + + + + + + + + + + ))} + + {orders.length === 0 && ( + + + Buyurtmalar topilmadi + + + )} + +
+
+ ); +}; + +export default OrderTable; diff --git a/src/features/plans/ui/AddedPlan.tsx b/src/features/plans/ui/AddedPlan.tsx index fb3a943..0ae007e 100644 --- a/src/features/plans/ui/AddedPlan.tsx +++ b/src/features/plans/ui/AddedPlan.tsx @@ -31,7 +31,7 @@ import { Switch } from "@/shared/ui/switch"; import { Textarea } from "@/shared/ui/textarea"; import { zodResolver } from "@hookform/resolvers/zod"; import { Popover } from "@radix-ui/react-popover"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { Check, ChevronsUpDown, Loader2, Upload, XIcon } from "lucide-react"; import { useState } from "react"; @@ -45,6 +45,7 @@ interface Props { } const AddedPlan = ({ initialValues, setDialogOpen }: Props) => { + const queryClient = useQueryClient(); const form = useForm>({ resolver: zodResolver(createPlanFormData), defaultValues: { @@ -79,6 +80,7 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => { richColors: true, position: "top-center", }); + queryClient.refetchQueries({ queryKey: ["product_list"] }); setDialogOpen(false); }, onError: (err: AxiosError) => { @@ -92,10 +94,11 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => { mutationFn: ({ body, id }: { id: string; body: FormData }) => plans_api.update({ body, id }), onSuccess() { - toast.success("Mahsulot qo'shildi", { + toast.success("Mahsulot tahrirlandi", { richColors: true, position: "top-center", }); + queryClient.refetchQueries({ queryKey: ["product_list"] }); setDialogOpen(false); }, onError: (err: AxiosError) => { @@ -425,9 +428,15 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => { Mavjud miqdor field.onChange(Number(e.target.value))} + type="text" + placeholder="Mahsulotning qolgan miqdori" + className="h-12" + value={field.value ? formatPrice(field.value) : ""} + onChange={(e) => { + // faqat raqamlarni qoldiramiz + const rawValue = e.target.value.replace(/\D/g, ""); + field.onChange(rawValue ? Number(rawValue) : 0); + }} /> @@ -443,9 +452,15 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => { Minimal miqdor field.onChange(Number(e.target.value))} + type="text" + placeholder="Eng kamida nechta zakaz qilishi" + className="h-12" + value={field.value ? formatPrice(field.value) : ""} + onChange={(e) => { + // faqat raqamlarni qoldiramiz + const rawValue = e.target.value.replace(/\D/g, ""); + field.onChange(rawValue ? Number(rawValue) : 0); + }} /> @@ -608,20 +623,19 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => { ? "Yangi rasm tanlash" : "Rasm tanlang"}

- - {field.value && field.value instanceof File && ( -
-

- Yangi tanlangan: {field.value.name} -

- preview -
- )} + {field.value && field.value instanceof File && ( +
+

+ Yangi tanlangan: {field.value.name} +

+ preview +
+ )}
diff --git a/src/features/plans/ui/PalanTable.tsx b/src/features/plans/ui/PalanTable.tsx index 4ebd94c..6a3ff4a 100644 --- a/src/features/plans/ui/PalanTable.tsx +++ b/src/features/plans/ui/PalanTable.tsx @@ -20,10 +20,12 @@ import { TableHeader, TableRow, } from "@/shared/ui/table"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; import clsx from "clsx"; import { Edit, Eye, Loader2, Trash } from "lucide-react"; import type { Dispatch, SetStateAction } from "react"; +import { toast } from "sonner"; interface Props { products: Product[] | []; @@ -66,6 +68,36 @@ const ProductTable = ({ }, }); + const queryClient = useQueryClient(); + + const { mutate: updated } = useMutation({ + mutationFn: ({ body, id }: { id: string; body: FormData }) => + plans_api.update({ body, id }), + onSuccess() { + toast.success("Mahsulot statusi o'zgardi", { + richColors: true, + position: "top-center", + }); + queryClient.refetchQueries({ queryKey: ["product_list"] }); + setDialogOpen(false); + }, + onError: (err: AxiosError) => { + toast.error((err.response?.data as string) || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + + const handleStatusChange = async ( + product: string, + status: "true" | "false", + ) => { + const formData = new FormData(); + formData.append("is_active", status); + updated({ body: formData, id: product }); + }; + if (isLoading || isFetching) { return (
@@ -130,7 +162,12 @@ const ProductTable = ({ product.is_active ? "text-green-600" : "text-red-600", )} > - + handleStatusChange(product.id, value as "true" | "false") + } + > diff --git a/src/pages/Orders.tsx b/src/pages/Orders.tsx index 9a6a524..8ef145b 100644 --- a/src/pages/Orders.tsx +++ b/src/pages/Orders.tsx @@ -1,5 +1,12 @@ +import OrderList from "@/features/order/ui/OrderList"; +import SidebarLayout from "@/SidebarLayout"; + const Orders = () => { - return <>{/* */}; + return ( + + + + ); }; export default Orders; diff --git a/src/shared/config/api/URLs.ts b/src/shared/config/api/URLs.ts index 81acdf4..5a16314 100644 --- a/src/shared/config/api/URLs.ts +++ b/src/shared/config/api/URLs.ts @@ -29,6 +29,6 @@ export const API_URLS = { OrderStatus: (id: string | number) => `${API_V}admin/order/${id}/status_update/`, CreateProduct: `${API_V}admin/product/create/`, - UpdateProduct: (id: string) => `${API_V}admin/user/${id}/update/`, + UpdateProduct: (id: string) => `${API_V}admin/product/${id}/update/`, DeleteProdut: (id: string) => `${API_V}admin/product/${id}/delete/`, };