vercel deploy
This commit is contained in:
@@ -39,7 +39,6 @@ const AuthLogin = () => {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
console.log(res.data.data.access);
|
||||
|
||||
saveToken(res.data.data.access);
|
||||
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 { 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<z.infer<typeof createPlanFormData>>({
|
||||
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) => {
|
||||
<FormLabel>Mavjud miqdor</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) => 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);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -443,9 +452,15 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||
<FormLabel>Minimal miqdor</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) => 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);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -608,20 +623,19 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||
? "Yangi rasm tanlash"
|
||||
: "Rasm tanlang"}
|
||||
</p>
|
||||
|
||||
{field.value && field.value instanceof File && (
|
||||
<div className="mt-3 text-center">
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
Yangi tanlangan: {field.value.name}
|
||||
</p>
|
||||
<img
|
||||
src={URL.createObjectURL(field.value)}
|
||||
alt="preview"
|
||||
className="mt-2 mx-auto h-20 w-20 object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</FormLabel>
|
||||
{field.value && field.value instanceof File && (
|
||||
<div className="mt-3 text-center">
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
Yangi tanlangan: {field.value.name}
|
||||
</p>
|
||||
<img
|
||||
src={URL.createObjectURL(field.value)}
|
||||
alt="preview"
|
||||
className="mt-2 mx-auto h-20 w-20 object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
@@ -130,7 +162,12 @@ const ProductTable = ({
|
||||
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]">
|
||||
<SelectValue placeholder="Holati" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import OrderList from "@/features/order/ui/OrderList";
|
||||
import SidebarLayout from "@/SidebarLayout";
|
||||
|
||||
const Orders = () => {
|
||||
return <>{/* <OrdersList /> */}</>;
|
||||
return (
|
||||
<SidebarLayout>
|
||||
<OrderList />
|
||||
</SidebarLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Orders;
|
||||
|
||||
@@ -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/`,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user