vercel deploy
This commit is contained in:
@@ -39,7 +39,6 @@ const AuthLogin = () => {
|
|||||||
richColors: true,
|
richColors: true,
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
console.log(res.data.data.access);
|
|
||||||
|
|
||||||
saveToken(res.data.data.access);
|
saveToken(res.data.data.access);
|
||||||
navigate("dashboard");
|
navigate("dashboard");
|
||||||
|
|||||||
30
src/features/order/lib/api.ts
Normal file
30
src/features/order/lib/api.ts
Normal file
@@ -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<AxiosResponse<OrdersResponse>> {
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
50
src/features/order/lib/type.ts
Normal file
50
src/features/order/lib/type.ts
Normal file
@@ -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[];
|
||||||
|
}
|
||||||
89
src/features/order/ui/OrderDelete.tsx
Normal file
89
src/features/order/ui/OrderDelete.tsx
Normal file
@@ -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<SetStateAction<boolean>>;
|
||||||
|
setOrderDelete: Dispatch<SetStateAction<Order | null>>;
|
||||||
|
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 (
|
||||||
|
<Dialog open={opneDelete} onOpenChange={setOpenDelete}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Buyurtmani o'chirish</DialogTitle>
|
||||||
|
<DialogDescription className="text-md font-semibold">
|
||||||
|
Siz rostan ham {orderDelete?.order_number} sonli buyurtmani
|
||||||
|
o'chirmoqchimisiz?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
className="bg-blue-600 cursor-pointer hover:bg-blue-600"
|
||||||
|
onClick={() => setOpenDelete(false)}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
Bekor qilish
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={"destructive"}
|
||||||
|
onClick={() => orderDelete && deleteUser(orderDelete.id)}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Trash />
|
||||||
|
O'chirish
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderDelete;
|
||||||
248
src/features/order/ui/OrderDetail.tsx
Normal file
248
src/features/order/ui/OrderDetail.tsx
Normal file
@@ -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<string, string> = {
|
||||||
|
YANDEX_GO: "Yandex Go",
|
||||||
|
DELIVERY_COURIES: "Kuryer orqali yetkazish",
|
||||||
|
PICKUP: "O‘zi olib ketish",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
detail: boolean;
|
||||||
|
setDetail: Dispatch<SetStateAction<boolean>>;
|
||||||
|
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<string, string> = {
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setDetail(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="fixed right-0 top-0 h-full w-full max-w-2xl bg-white shadow-xl z-50 overflow-y-auto">
|
||||||
|
<div className="sticky top-0 bg-white border-b px-6 py-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold">
|
||||||
|
Buyurtma #{order.order_number}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500">ID: {order.id}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setDetail(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<span
|
||||||
|
className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(order.status)}`}
|
||||||
|
>
|
||||||
|
{order.status.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center text-sm text-gray-500">
|
||||||
|
<Calendar className="w-4 h-4 mr-1" />
|
||||||
|
{formatDate(order.created_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-500">Jami summa</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">
|
||||||
|
{formatPrice(order.total_price)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-500">Yetkazish narxi</p>
|
||||||
|
<p className="text-xl font-semibold text-gray-700">
|
||||||
|
{formatPrice(order.delivery_price)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-lg mb-3 flex items-center">
|
||||||
|
<User className="w-5 h-5 mr-2" />
|
||||||
|
Mijoz ma'lumotlari
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">Ism:</span>
|
||||||
|
<span className="font-medium">{order.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-gray-500">Telefon:</span>
|
||||||
|
<a
|
||||||
|
href={`tel:${order.contact_number}`}
|
||||||
|
className="font-medium text-blue-600 hover:underline flex items-center"
|
||||||
|
>
|
||||||
|
<Phone className="w-4 h-4 mr-1" />
|
||||||
|
{formatPhone(order.contact_number)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-lg mb-3 flex items-center">
|
||||||
|
<Truck className="w-5 h-5 mr-2" />
|
||||||
|
Yetkazish ma'lumotlari
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">Turi:</span>
|
||||||
|
<span className="font-medium capitalize">
|
||||||
|
{deliveryTypeLabel[order.delivery_type] ??
|
||||||
|
order.delivery_type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
{order.lat && order.long && (
|
||||||
|
<div className="border rounded-lg p-4 space-y-2">
|
||||||
|
<h3 className="font-semibold text-lg mb-2 flex items-center">
|
||||||
|
<MapIcon className="w-5 h-5 mr-2" />
|
||||||
|
Manzil
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="w-full h-[300px] rounded-lg overflow-hidden">
|
||||||
|
<YMaps>
|
||||||
|
<Map
|
||||||
|
state={{
|
||||||
|
center: [order.lat, order.long],
|
||||||
|
zoom: 16,
|
||||||
|
}}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<Placemark geometry={[order.lat, order.long]} />
|
||||||
|
</Map>
|
||||||
|
</YMaps>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-lg mb-3 flex items-center">
|
||||||
|
<CreditCard className="w-5 h-5 mr-2" />
|
||||||
|
To'lov turi
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm font-medium capitalize">
|
||||||
|
{order.payment_type === "CASH" ? "Naxt" : "Karta orqali"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{order.comment && (
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-lg mb-3 flex items-center">
|
||||||
|
<MessageSquare className="w-5 h-5 mr-2" />
|
||||||
|
Izoh
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-700">{order.comment}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-lg mb-4 flex items-center">
|
||||||
|
<Package className="w-5 h-5 mr-2" />
|
||||||
|
Mahsulotlar ({order.items.length})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{order.items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between py-3 border-b last:border-b-0"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">
|
||||||
|
{item.product?.name_uz || "Noma'lum mahsulot"}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{formatPrice(item.product.price)} × {item.quantity}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-semibold">
|
||||||
|
{formatPrice(item.product.price * item.quantity)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 pt-4 border-t space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-500">Mahsulotlar:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{formatPrice(order.total_price - order.delivery_price)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-500">Yetkazish:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{formatPrice(order.delivery_price)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-lg font-bold pt-2 border-t">
|
||||||
|
<span>Jami:</span>
|
||||||
|
<span>{formatPrice(order.total_price)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderDetail;
|
||||||
75
src/features/order/ui/OrderList.tsx
Normal file
75
src/features/order/ui/OrderList.tsx
Normal file
@@ -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<Order | null>(null);
|
||||||
|
const [detail, setDetail] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [openDelete, setOpenDelete] = useState<boolean>(false);
|
||||||
|
const [planDelete, setPlanDelete] = useState<Order | null>(null);
|
||||||
|
|
||||||
|
const handleDelete = (id: Order) => {
|
||||||
|
setOpenDelete(true);
|
||||||
|
setPlanDelete(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full p-10 w-full">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
|
||||||
|
<h1 className="text-2xl font-bold">Mahsulotlar</h1>
|
||||||
|
|
||||||
|
<OrderDetail
|
||||||
|
detail={detail}
|
||||||
|
setDetail={setDetail}
|
||||||
|
order={orderDetail}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OrderTable
|
||||||
|
orders={data?.results || []}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isFetching={isFetching}
|
||||||
|
isError={isError}
|
||||||
|
setDetailOpen={setDetail}
|
||||||
|
handleDelete={handleDelete}
|
||||||
|
setOrderDetail={setOrderDetail}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
setCurrentPage={setCurrentPage}
|
||||||
|
totalPages={data?.total_pages || 1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OrderDelete
|
||||||
|
opneDelete={openDelete}
|
||||||
|
orderDelete={planDelete}
|
||||||
|
setOpenDelete={setOpenDelete}
|
||||||
|
setOrderDelete={setPlanDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderList;
|
||||||
205
src/features/order/ui/OrderTable.tsx
Normal file
205
src/features/order/ui/OrderTable.tsx
Normal file
@@ -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<SetStateAction<boolean>>;
|
||||||
|
handleDelete: (order: Order) => void;
|
||||||
|
setOrderDetail: Dispatch<SetStateAction<Order | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveryTypeLabel: Record<string, string> = {
|
||||||
|
YANDEX_GO: "Yandex Go",
|
||||||
|
DELIVERY_COURIES: "Kuryer orqali yetkazish",
|
||||||
|
PICKUP: "O‘zi olib ketish",
|
||||||
|
};
|
||||||
|
|
||||||
|
type OrderStatus =
|
||||||
|
| "NEW"
|
||||||
|
// "PROCESSING" |
|
||||||
|
| "DONE";
|
||||||
|
// | "CANCELLED";
|
||||||
|
|
||||||
|
const orderStatusLabel: Record<OrderStatus, string> = {
|
||||||
|
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 (
|
||||||
|
<div className="flex h-full items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full items-center justify-center text-red-600">
|
||||||
|
Maʼlumotlarni yuklashda xatolik yuz berdi
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>#</TableHead>
|
||||||
|
<TableHead>Order №</TableHead>
|
||||||
|
<TableHead>Foydalanuvchi</TableHead>
|
||||||
|
<TableHead>Kontakt</TableHead>
|
||||||
|
<TableHead>Toʻlov turi</TableHead>
|
||||||
|
<TableHead>Yetkazib berish</TableHead>
|
||||||
|
<TableHead>Umumiy narx</TableHead>
|
||||||
|
<TableHead>Izoh</TableHead>
|
||||||
|
<TableHead>Holat</TableHead>
|
||||||
|
<TableHead className="text-right">Harakatlar</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{orders.map((order, index) => (
|
||||||
|
<TableRow key={order.id}>
|
||||||
|
<TableCell>{index + 1}</TableCell>
|
||||||
|
<TableCell>{order.order_number}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{order.user.first_name} {order.user.last_name} (
|
||||||
|
{order.user.username})
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatPhone(order.contact_number)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{order.payment_type === "CASH" ? "Naxt" : "Karta orqali"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{deliveryTypeLabel[order.delivery_type] ?? order.delivery_type}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatPrice(order.total_price, true)}</TableCell>
|
||||||
|
<TableCell>{order.comment || "-"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Select
|
||||||
|
value={order.status}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleStatusChange(order.id, value as OrderStatus)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[150px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{(Object.keys(orderStatusLabel) as OrderStatus[]).map(
|
||||||
|
(status) => (
|
||||||
|
<SelectItem key={status} value={status}>
|
||||||
|
{orderStatusLabel[status]}
|
||||||
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="space-x-2 text-right">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setDetailOpen(true);
|
||||||
|
setOrderDetail(order);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleDelete(order)}
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{orders.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={10} className="text-center text-gray-500">
|
||||||
|
Buyurtmalar topilmadi
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderTable;
|
||||||
@@ -31,7 +31,7 @@ import { Switch } from "@/shared/ui/switch";
|
|||||||
import { Textarea } from "@/shared/ui/textarea";
|
import { Textarea } from "@/shared/ui/textarea";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Popover } from "@radix-ui/react-popover";
|
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 type { AxiosError } from "axios";
|
||||||
import { Check, ChevronsUpDown, Loader2, Upload, XIcon } from "lucide-react";
|
import { Check, ChevronsUpDown, Loader2, Upload, XIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -45,6 +45,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const form = useForm<z.infer<typeof createPlanFormData>>({
|
const form = useForm<z.infer<typeof createPlanFormData>>({
|
||||||
resolver: zodResolver(createPlanFormData),
|
resolver: zodResolver(createPlanFormData),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -79,6 +80,7 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
richColors: true,
|
richColors: true,
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
|
queryClient.refetchQueries({ queryKey: ["product_list"] });
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError) => {
|
onError: (err: AxiosError) => {
|
||||||
@@ -92,10 +94,11 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
mutationFn: ({ body, id }: { id: string; body: FormData }) =>
|
mutationFn: ({ body, id }: { id: string; body: FormData }) =>
|
||||||
plans_api.update({ body, id }),
|
plans_api.update({ body, id }),
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
toast.success("Mahsulot qo'shildi", {
|
toast.success("Mahsulot tahrirlandi", {
|
||||||
richColors: true,
|
richColors: true,
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
|
queryClient.refetchQueries({ queryKey: ["product_list"] });
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError) => {
|
onError: (err: AxiosError) => {
|
||||||
@@ -425,9 +428,15 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
<FormLabel>Mavjud miqdor</FormLabel>
|
<FormLabel>Mavjud miqdor</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
{...field}
|
placeholder="Mahsulotning qolgan miqdori"
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -443,9 +452,15 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
<FormLabel>Minimal miqdor</FormLabel>
|
<FormLabel>Minimal miqdor</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
{...field}
|
placeholder="Eng kamida nechta zakaz qilishi"
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -608,7 +623,7 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
? "Yangi rasm tanlash"
|
? "Yangi rasm tanlash"
|
||||||
: "Rasm tanlang"}
|
: "Rasm tanlang"}
|
||||||
</p>
|
</p>
|
||||||
|
</FormLabel>
|
||||||
{field.value && field.value instanceof File && (
|
{field.value && field.value instanceof File && (
|
||||||
<div className="mt-3 text-center">
|
<div className="mt-3 text-center">
|
||||||
<p className="text-sm font-medium text-gray-700">
|
<p className="text-sm font-medium text-gray-700">
|
||||||
@@ -621,7 +636,6 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/shared/ui/table";
|
} 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 clsx from "clsx";
|
||||||
import { Edit, Eye, Loader2, Trash } from "lucide-react";
|
import { Edit, Eye, Loader2, Trash } from "lucide-react";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
products: Product[] | [];
|
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) {
|
if (isLoading || isFetching) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
@@ -130,7 +162,12 @@ const ProductTable = ({
|
|||||||
product.is_active ? "text-green-600" : "text-red-600",
|
product.is_active ? "text-green-600" : "text-red-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Select value={product.is_active ? "true" : "false"}>
|
<Select
|
||||||
|
value={product.is_active ? "true" : "false"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleStatusChange(product.id, value as "true" | "false")
|
||||||
|
}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectValue placeholder="Holati" />
|
<SelectValue placeholder="Holati" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
|
import OrderList from "@/features/order/ui/OrderList";
|
||||||
|
import SidebarLayout from "@/SidebarLayout";
|
||||||
|
|
||||||
const Orders = () => {
|
const Orders = () => {
|
||||||
return <>{/* <OrdersList /> */}</>;
|
return (
|
||||||
|
<SidebarLayout>
|
||||||
|
<OrderList />
|
||||||
|
</SidebarLayout>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Orders;
|
export default Orders;
|
||||||
|
|||||||
@@ -29,6 +29,6 @@ export const API_URLS = {
|
|||||||
OrderStatus: (id: string | number) =>
|
OrderStatus: (id: string | number) =>
|
||||||
`${API_V}admin/order/${id}/status_update/`,
|
`${API_V}admin/order/${id}/status_update/`,
|
||||||
CreateProduct: `${API_V}admin/product/create/`,
|
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/`,
|
DeleteProdut: (id: string) => `${API_V}admin/product/${id}/delete/`,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user