history order status
This commit is contained in:
70
src/App.tsx
70
src/App.tsx
@@ -15,6 +15,7 @@ import {
|
||||
import AddNews from "@/pages/news/ui/AddNews";
|
||||
import News from "@/pages/news/ui/News";
|
||||
import NewsCategory from "@/pages/news/ui/NewsCategory";
|
||||
import WithdrawRequestsList from "@/pages/payout-request/ui/PayoutRequest";
|
||||
import Page from "@/pages/profile/ui/Profile";
|
||||
import Seo from "@/pages/seo/ui/Seo";
|
||||
import SiteBannerAdmin from "@/pages/site-banner/ui/Banner";
|
||||
@@ -39,14 +40,9 @@ import { Sidebar } from "@/widgets/sidebar/ui/Sidebar";
|
||||
import { useWelcomeStore } from "@/widgets/welcome/lib/hook";
|
||||
import Welcome from "@/widgets/welcome/ui/welcome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
Navigate,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
} from "react-router-dom";
|
||||
import { Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
const App = () => {
|
||||
const token = getAuthToken();
|
||||
@@ -54,9 +50,9 @@ const App = () => {
|
||||
const location = useLocation();
|
||||
const { setOpenModal } = useWelcomeStore();
|
||||
|
||||
const { data: user } = useQuery({
|
||||
const { data: user, isLoading } = useQuery({
|
||||
queryKey: ["get_me"],
|
||||
queryFn: () => getMe(),
|
||||
queryFn: getMe,
|
||||
select: (data) => data.data.data,
|
||||
enabled: !!token,
|
||||
});
|
||||
@@ -64,19 +60,7 @@ const App = () => {
|
||||
const hideSidebarPaths = ["/login"];
|
||||
const shouldShowSidebar = !hideSidebarPaths.includes(location.pathname);
|
||||
|
||||
useEffect(() => {
|
||||
if (token && user) {
|
||||
if (location.pathname === "/") {
|
||||
if (user.role === "moderator") navigate("/user");
|
||||
else if (user.role === "tour_admin") navigate("/profile");
|
||||
else if (user.role === "buxgalter") navigate("/finance");
|
||||
}
|
||||
} else if (!token) {
|
||||
navigate("/login");
|
||||
}
|
||||
}, [token, user]);
|
||||
|
||||
// 🔹 Faqat userda ism yoki familiya yo‘q bo‘lsa Welcome modalni ochamiz
|
||||
// ✅ Modalni boshqarish
|
||||
useEffect(() => {
|
||||
if (
|
||||
user &&
|
||||
@@ -88,7 +72,39 @@ const App = () => {
|
||||
} else {
|
||||
setOpenModal(false);
|
||||
}
|
||||
}, [user, location.pathname]);
|
||||
}, [user, location.pathname, setOpenModal]);
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
navigate("/login", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) return;
|
||||
|
||||
if (location.pathname === "/") {
|
||||
if (user.role === "tour_admin") {
|
||||
navigate("/profile", { replace: true });
|
||||
} else if (user.role === "moderator") {
|
||||
navigate("/user", { replace: true });
|
||||
} else if (user.role === "buxgalter") {
|
||||
navigate("/finance", { replace: true });
|
||||
} else {
|
||||
navigate("/login", { replace: true });
|
||||
}
|
||||
}
|
||||
}, [token, user, location.pathname, navigate]);
|
||||
|
||||
if (!token) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-900 text-white">
|
||||
<Loader2 className="animate-spin w-10 h-10 text-cyan-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex max-lg:flex-col bg-gray-900">
|
||||
@@ -100,11 +116,9 @@ const App = () => {
|
||||
shouldShowSidebar ? "lg:ml-64" : "ml-0",
|
||||
)}
|
||||
>
|
||||
{/* ✅ Welcome faqat login sahifasida ko‘rinmaydi */}
|
||||
{location.pathname !== "/login" && <Welcome />}
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to={"/user"} />} />
|
||||
<Route path="/user" element={<UserList />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/profile" element={<Page />} />
|
||||
@@ -115,6 +129,12 @@ const App = () => {
|
||||
<Route path="/agencies/:id" element={<AgencyDetail />} />
|
||||
<Route path="/agency/:id/edit" element={<EditAgecy />} />
|
||||
<Route path="/tours/:id" element={<TourDetail />} />
|
||||
<Route
|
||||
path="/withdraw/list"
|
||||
element={
|
||||
<WithdrawRequestsList user={user ? user.role : "moderator"} />
|
||||
}
|
||||
/>
|
||||
<Route path="/employees" element={<Employees />} />
|
||||
<Route
|
||||
path="/finance"
|
||||
|
||||
@@ -94,6 +94,28 @@ const Login = () => {
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const userData = data.data.data;
|
||||
setUser(userData);
|
||||
|
||||
if (userData.role === "user") {
|
||||
toast.error(t("Sizga tizimga kirishga ruxsat berilmagan"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
|
||||
setAuthToken("");
|
||||
setAuthRefToken("");
|
||||
queryClient.clear();
|
||||
|
||||
navigate("/login", { replace: true });
|
||||
} else {
|
||||
navigate("/", { replace: true });
|
||||
}
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-900 px-4 w-full">
|
||||
<div className="absolute top-4 right-4 flex gap-2">
|
||||
|
||||
@@ -110,7 +110,7 @@ const EditEmployees = ({
|
||||
...(modalMode === "add" && { password: "" }),
|
||||
});
|
||||
}
|
||||
}, [data, editId, showModal, modalMode]);
|
||||
}, [data, editId, showModal, modalMode, form]);
|
||||
|
||||
const { mutate: create, isPending } = useMutation({
|
||||
mutationFn: ({
|
||||
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
PlusCircle,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
@@ -100,8 +100,9 @@ const Faq = () => {
|
||||
setCurrentPage(1);
|
||||
}, [activeTab]);
|
||||
|
||||
const category =
|
||||
categoryData?.pages.flatMap((page) => page.data.data.results) ?? [];
|
||||
const category = useMemo(() => {
|
||||
return categoryData?.pages.flatMap((page) => page.data.data.results) ?? [];
|
||||
}, [categoryData]);
|
||||
|
||||
const { data: faq } = useQuery({
|
||||
queryKey: ["all_faq", activeTab, currentPage],
|
||||
@@ -239,7 +240,7 @@ const Faq = () => {
|
||||
form.setValue("answer_ru", detailFaq.text_ru);
|
||||
form.setValue("categories", activeTab);
|
||||
}
|
||||
}, [detailFaq, form]);
|
||||
}, [detailFaq, form, activeTab]);
|
||||
|
||||
function onSubmit(value: z.infer<typeof faqForm>) {
|
||||
if (editFaq === null) {
|
||||
|
||||
@@ -146,7 +146,7 @@ const FaqCategory = () => {
|
||||
form.setValue("name", oneCategory.name_uz);
|
||||
form.setValue("name_ru", oneCategory.name_ru);
|
||||
}
|
||||
}, [oneCategory, categories]);
|
||||
}, [oneCategory, categories, form]);
|
||||
|
||||
const onSubmit = (values: z.infer<typeof categoryFormSchema>) => {
|
||||
if (categories !== null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AgencyOrderData,
|
||||
History,
|
||||
OrderHistoryData,
|
||||
UserAgencyDetailData,
|
||||
UserOrderData,
|
||||
UserOrderDetailData,
|
||||
@@ -8,6 +9,7 @@ import type {
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import {
|
||||
AGENCY_ORDERS,
|
||||
AGENCY_ORDERS_HISTORY,
|
||||
PAYMENT_AGENCY,
|
||||
USER_ORDERS,
|
||||
} from "@/shared/config/api/URLs";
|
||||
@@ -74,7 +76,7 @@ const payAgency = async ({
|
||||
body: {
|
||||
travel_agency: number;
|
||||
amount: number;
|
||||
note: string;
|
||||
note?: string;
|
||||
};
|
||||
}) => {
|
||||
const res = await httpClient.post(PAYMENT_AGENCY, body);
|
||||
@@ -89,12 +91,41 @@ const getPaymentHistory = async (params: {
|
||||
return res;
|
||||
};
|
||||
|
||||
const getOrderHistory = async (params: {
|
||||
page: number;
|
||||
page_size: number;
|
||||
order: number;
|
||||
}): Promise<AxiosResponse<OrderHistoryData>> => {
|
||||
const res = await httpClient.get(AGENCY_ORDERS_HISTORY, { params });
|
||||
return res;
|
||||
};
|
||||
|
||||
const savedOrderHistory = async (body: {
|
||||
order: number;
|
||||
old_status:
|
||||
| "pending_payment"
|
||||
| "pending_confirmation"
|
||||
| "cancelled"
|
||||
| "confirmed"
|
||||
| "completed";
|
||||
new_status:
|
||||
| "pending_payment"
|
||||
| "pending_confirmation"
|
||||
| "cancelled"
|
||||
| "confirmed"
|
||||
| "completed";
|
||||
}) => {
|
||||
const res = await httpClient.post(AGENCY_ORDERS_HISTORY, body);
|
||||
return res;
|
||||
};
|
||||
export {
|
||||
getAllOrder,
|
||||
getAllOrderAgecy,
|
||||
getDetailAgencyOrder,
|
||||
getDetailOrder,
|
||||
getOrderHistory,
|
||||
getPaymentHistory,
|
||||
payAgency,
|
||||
savedOrderHistory,
|
||||
updateDetailOrder,
|
||||
};
|
||||
|
||||
@@ -211,3 +211,54 @@ export interface History {
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface OrderHistoryData {
|
||||
status: boolean;
|
||||
data: {
|
||||
links: {
|
||||
previous: string;
|
||||
next: string;
|
||||
};
|
||||
total_items: number;
|
||||
total_pages: number;
|
||||
page_size: number;
|
||||
current_page: number;
|
||||
results: [
|
||||
{
|
||||
id: number;
|
||||
user: {
|
||||
id: number;
|
||||
last_login: string;
|
||||
is_superuser: boolean;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
is_staff: boolean;
|
||||
is_active: boolean;
|
||||
date_joined: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
validated_at: string;
|
||||
role: string;
|
||||
total_spent: number;
|
||||
travel_agency: number;
|
||||
};
|
||||
created_at: string;
|
||||
order: number;
|
||||
old_status:
|
||||
| "pending_payment"
|
||||
| "pending_confirmation"
|
||||
| "cancelled"
|
||||
| "confirmed"
|
||||
| "completed";
|
||||
new_status:
|
||||
| "pending_payment"
|
||||
| "pending_confirmation"
|
||||
| "cancelled"
|
||||
| "confirmed"
|
||||
| "completed";
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ export default function FinancePage({ user }: { user: Role }) {
|
||||
});
|
||||
}, [tab, currentPage, currentPageAgency, setSearchParams]);
|
||||
|
||||
// ✅ Param o‘zgarsa — holatni sinxronlashtirish
|
||||
useEffect(() => {
|
||||
if (tabParam && tabParam !== tab) {
|
||||
setTab(tabParam);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { getDetailOrder, updateDetailOrder } from "@/pages/finance/lib/api";
|
||||
import {
|
||||
getDetailOrder,
|
||||
getOrderHistory,
|
||||
savedOrderHistory,
|
||||
updateDetailOrder,
|
||||
} from "@/pages/finance/lib/api";
|
||||
import type { OrderStatus } from "@/pages/finance/lib/type";
|
||||
import { OrderHistoryTable } from "@/pages/finance/ui/OrderHistoryTable";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import {
|
||||
@@ -31,10 +37,10 @@ import { toast } from "sonner";
|
||||
export default function FinanceDetailUser() {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const [activeTab, setActiveTab] = useState<"bookings" | "details">(
|
||||
"bookings",
|
||||
);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"bookings" | "details" | "history"
|
||||
>("bookings");
|
||||
const [historyPage, setHistoryPage] = useState(1);
|
||||
const params = useParams();
|
||||
|
||||
const { data } = useQuery({
|
||||
@@ -76,6 +82,49 @@ export default function FinanceDetailUser() {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: savedHistory } = useMutation({
|
||||
mutationFn: (body: {
|
||||
order: number;
|
||||
old_status:
|
||||
| "pending_payment"
|
||||
| "pending_confirmation"
|
||||
| "cancelled"
|
||||
| "confirmed"
|
||||
| "completed";
|
||||
new_status:
|
||||
| "pending_payment"
|
||||
| "pending_confirmation"
|
||||
| "cancelled"
|
||||
| "confirmed"
|
||||
| "completed";
|
||||
}) => savedOrderHistory(body),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_order"] });
|
||||
queryClient.refetchQueries({ queryKey: ["list_order_user"] });
|
||||
queryClient.refetchQueries({ queryKey: ["order_history"] });
|
||||
toast.success(t("Status muvaffaqiyatli yangilandi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Statusni yangilashda xatolik yuz berdi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { data: history } = useQuery({
|
||||
queryKey: ["order_history", historyPage, params.id],
|
||||
queryFn: () =>
|
||||
getOrderHistory({
|
||||
page: historyPage,
|
||||
page_size: 10,
|
||||
order: Number(params.id),
|
||||
}),
|
||||
});
|
||||
|
||||
const getStatusBadge = (status: OrderStatus["order_status"]) => {
|
||||
const base =
|
||||
"px-3 py-1 rounded-full text-sm font-medium inline-flex items-center gap-2";
|
||||
@@ -168,6 +217,17 @@ export default function FinanceDetailUser() {
|
||||
<User className="w-4 h-4" />
|
||||
{t("User Details")}
|
||||
</button>
|
||||
<button
|
||||
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
|
||||
activeTab === "history"
|
||||
? "text-blue-400 border-b-2 border-blue-400"
|
||||
: "text-gray-400 hover:text-gray-300"
|
||||
}`}
|
||||
onClick={() => setActiveTab("history")}
|
||||
>
|
||||
<Calendar className="w-4 h-4" />
|
||||
{t("Order History")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
@@ -194,12 +254,17 @@ export default function FinanceDetailUser() {
|
||||
| "cancelled"
|
||||
| "confirmed"
|
||||
| "completed",
|
||||
) =>
|
||||
) => {
|
||||
mutate({
|
||||
id: data.id,
|
||||
body: { order_status: value },
|
||||
})
|
||||
}
|
||||
});
|
||||
savedHistory({
|
||||
new_status: value,
|
||||
old_status: data.order_status,
|
||||
order: data.id,
|
||||
});
|
||||
}}
|
||||
value={data.order_status}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] bg-gray-800 border-gray-700 text-gray-200">
|
||||
@@ -423,6 +488,16 @@ export default function FinanceDetailUser() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "history" && history?.data && (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-bold mb-4">{t("Order History")}</h2>
|
||||
<OrderHistoryTable
|
||||
t={t}
|
||||
data={history.data.data}
|
||||
onPageChange={setHistoryPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
174
src/pages/finance/ui/OrderHistoryTable.tsx
Normal file
174
src/pages/finance/ui/OrderHistoryTable.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
|
||||
import type { OrderHistoryData } from "@/pages/finance/lib/type";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/shared/ui/pagination";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface OrderHistoryTableProps {
|
||||
data: OrderHistoryData["data"];
|
||||
onPageChange: (page: number) => void;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export function OrderHistoryTable({
|
||||
data,
|
||||
onPageChange,
|
||||
t,
|
||||
}: OrderHistoryTableProps) {
|
||||
const [currentPage, setCurrentPage] = useState(data.current_page);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
onPageChange(page);
|
||||
};
|
||||
|
||||
const getStatusColor = (
|
||||
status: string,
|
||||
): "default" | "secondary" | "destructive" | "outline" => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "pending_payment":
|
||||
return "secondary";
|
||||
case "pending_confirmation":
|
||||
return "default";
|
||||
case "confirmed":
|
||||
return "default";
|
||||
case "completed":
|
||||
return "default";
|
||||
case "cancelled":
|
||||
return "destructive";
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
};
|
||||
|
||||
const formatStatus = (status: string) => {
|
||||
return status
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
<div className="rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("Kim tomonidan")}</TableHead>
|
||||
<TableHead>{t("Lavozimi")}</TableHead>
|
||||
<TableHead>{t("O'zgartirilgan status")}</TableHead>
|
||||
<TableHead className="text-right">
|
||||
{t("O'zgartirgan sanasi")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.results.length > 0 ? (
|
||||
data.results.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div>
|
||||
{item.user.first_name.length > 0 ||
|
||||
item.user.last_name.length > 0 ? (
|
||||
<p className="font-semibold">
|
||||
{item.user.first_name} {item.user.last_name}
|
||||
</p>
|
||||
) : (
|
||||
<p>{t("No'malum")}</p>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{item.user.role}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getStatusColor(item.old_status)}>
|
||||
{formatStatus(t(item.old_status))}
|
||||
</Badge>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||
<Badge variant={getStatusColor(item.new_status)}>
|
||||
{formatStatus(t(item.new_status))}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-sm text-muted-foreground">
|
||||
{item.created_at}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
className="text-center text-muted-foreground py-8"
|
||||
>
|
||||
{t("Hozircha tarix mavjud emas")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{data.total_pages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
{data.links.previous && (
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
/>
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
{Array.from({ length: data.total_pages }, (_, i) => i + 1).map(
|
||||
(page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink
|
||||
isActive={page === currentPage}
|
||||
onClick={() => handlePageChange(page)}
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
),
|
||||
)}
|
||||
|
||||
{data.links.next && (
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
/>
|
||||
</PaginationItem>
|
||||
)}
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
src/pages/payout-request/lib/api.ts
Normal file
31
src/pages/payout-request/lib/api.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { PayoutListData } from "@/pages/payout-request/lib/types";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { PAYOT_REQUEST } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
const sendpayout = async ({ body }: { body: { amount: number } }) => {
|
||||
const res = await httpClient.post(PAYOT_REQUEST, body);
|
||||
return res;
|
||||
};
|
||||
|
||||
const upatePayout = async ({
|
||||
body,
|
||||
id,
|
||||
}: {
|
||||
id: number;
|
||||
body: { status: string };
|
||||
}) => {
|
||||
const res = await httpClient.patch(`${PAYOT_REQUEST}${id}/`, body);
|
||||
return res;
|
||||
};
|
||||
|
||||
const getPayoutList = async (params: {
|
||||
page_size: number;
|
||||
page: number;
|
||||
status: "pending" | "approved" | "cancelled" | "";
|
||||
}): Promise<AxiosResponse<PayoutListData>> => {
|
||||
const res = await httpClient.get(PAYOT_REQUEST, { params });
|
||||
return res;
|
||||
};
|
||||
|
||||
export { getPayoutList, sendpayout, upatePayout };
|
||||
23
src/pages/payout-request/lib/types.ts
Normal file
23
src/pages/payout-request/lib/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface PayoutListData {
|
||||
status: boolean;
|
||||
data: {
|
||||
links: {
|
||||
previous: string;
|
||||
next: string;
|
||||
};
|
||||
total_items: number;
|
||||
total_pages: number;
|
||||
page_size: number;
|
||||
current_page: number;
|
||||
results: {
|
||||
id: number;
|
||||
travel_agency: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
amount: number;
|
||||
status: "pending" | "approved" | "cancelled";
|
||||
created_at: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
400
src/pages/payout-request/ui/PayoutRequest.tsx
Normal file
400
src/pages/payout-request/ui/PayoutRequest.tsx
Normal file
@@ -0,0 +1,400 @@
|
||||
"use client";
|
||||
|
||||
import { payAgency } from "@/pages/finance/lib/api";
|
||||
import {
|
||||
getPayoutList,
|
||||
sendpayout,
|
||||
upatePayout,
|
||||
} from "@/pages/payout-request/lib/api";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Banknote,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function WithdrawRequests({
|
||||
user,
|
||||
}: {
|
||||
user:
|
||||
| "superuser"
|
||||
| "admin"
|
||||
| "moderator"
|
||||
| "tour_admin"
|
||||
| "buxgalter"
|
||||
| "operator"
|
||||
| "user";
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [statusFilter, setStatusFilter] = useState<
|
||||
"pending" | "approved" | "cancelled" | ""
|
||||
>("");
|
||||
const { t } = useTranslation();
|
||||
const tabs = [
|
||||
{ label: t("Barchasi"), value: "" as const },
|
||||
{ label: t("Kutilmoqda"), value: "pending" as const },
|
||||
{ label: t("Confirmed"), value: "approved" as const },
|
||||
{ label: t("cancelled"), value: "cancelled" as const },
|
||||
];
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [amount, setAmount] = useState("");
|
||||
const [loadingId, setLoadingId] = useState<number | null>(null);
|
||||
const [closeId, setCloseId] = useState<number | null>(null);
|
||||
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ["withdraw-requests", currentPage, statusFilter],
|
||||
queryFn: () =>
|
||||
getPayoutList({
|
||||
page: currentPage,
|
||||
page_size: 10,
|
||||
status: statusFilter,
|
||||
}),
|
||||
});
|
||||
|
||||
const { mutate: updatePayed } = useMutation({
|
||||
mutationFn: ({ body, id }: { id: number; body: { status: string } }) =>
|
||||
upatePayout({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["withdraw-requests"] });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: ({
|
||||
body,
|
||||
}: {
|
||||
body: {
|
||||
travel_agency: number;
|
||||
amount: number;
|
||||
note?: string;
|
||||
};
|
||||
orderId: number;
|
||||
}) => payAgency({ body }),
|
||||
onSuccess: (__, variables) => {
|
||||
toast.success(t("To‘lov muvaffaqiyatli amalga oshirildi!"), {
|
||||
position: "top-center",
|
||||
});
|
||||
updatePayed({
|
||||
body: {
|
||||
status: "approved",
|
||||
},
|
||||
id: variables.orderId,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ body }: { body: { amount: number } }) =>
|
||||
sendpayout({ body }),
|
||||
onSuccess: () => {
|
||||
toast.success("So‘rov muvaffaqiyatli yuborildi!");
|
||||
setAmount("");
|
||||
setDialogOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["withdraw-requests"] });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("So‘rov yuborishda xatolik yuz berdi!");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const numericAmount = Number(amount);
|
||||
if (!numericAmount || numericAmount <= 0) {
|
||||
toast.error("Iltimos, miqdorni to‘g‘ri kiriting!");
|
||||
return;
|
||||
}
|
||||
mutation.mutate({ body: { amount: numericAmount } });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white gap-4">
|
||||
<Loader2 className="animate-spin w-10 h-10 text-cyan-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 w-full text-center text-white gap-4">
|
||||
<AlertTriangle className="w-10 h-10 text-red-500" />
|
||||
<p className="text-lg">
|
||||
{t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg px-5 py-2 hover:opacity-90"
|
||||
>
|
||||
{t("Qayta urinish")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const results = data?.data?.data.results ?? [];
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
{/* 🔹 FILTER TAB */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-6">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{tabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.value}
|
||||
onClick={() => {
|
||||
setStatusFilter(tab.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className={`rounded-xl px-4 py-2 font-medium transition-all ${
|
||||
statusFilter === tab.value
|
||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white"
|
||||
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{user === "tour_admin" && (
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
{t("Pul olish so'rovi")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("Pul olish so‘rovlari")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">
|
||||
{t("Miqdorni kiriting")}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formatPrice(amount)}
|
||||
onChange={(e) => {
|
||||
const onlyNumbers = e.target.value.replace(/\D/g, "");
|
||||
setAmount(onlyNumbers);
|
||||
}}
|
||||
placeholder={t("Miqdorni kiriting")}
|
||||
className="bg-gray-800 border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
disabled={!amount || mutation.isPending}
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
t("Yuborish")
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 🔹 TABLE */}
|
||||
<Card className="bg-gray-900 border-gray-800 text-white">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("Pul olish so‘rovlari")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>{t("Agentlik nomi")}</TableHead>
|
||||
<TableHead>{t("Miqdor")}</TableHead>
|
||||
<TableHead>{t("Status")}</TableHead>
|
||||
<TableHead>{t("Yaratilgan sana")}</TableHead>
|
||||
{(user === "admin" ||
|
||||
user === "buxgalter" ||
|
||||
user === "moderator" ||
|
||||
user === "superuser") && (
|
||||
<TableHead>{t("Harakatlar")}</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{results.length > 0 ? (
|
||||
results.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.id}</TableCell>
|
||||
<TableCell>{item.travel_agency.name}</TableCell>
|
||||
<TableCell>{formatPrice(item.amount, true)}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
className={
|
||||
item.status === "pending"
|
||||
? "bg-yellow-600"
|
||||
: item.status === "approved"
|
||||
? "bg-green-600"
|
||||
: "bg-red-600"
|
||||
}
|
||||
>
|
||||
{t(item.status)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{format(new Date(item.created_at), "yyyy-MM-dd HH:mm")}
|
||||
</TableCell>
|
||||
{(user === "admin" ||
|
||||
user === "buxgalter" ||
|
||||
user === "moderator" ||
|
||||
user === "superuser") && (
|
||||
<TableCell className="flex gap-4">
|
||||
<Button
|
||||
disabled={item.status !== "pending"}
|
||||
onClick={() => {
|
||||
setLoadingId(item.id);
|
||||
mutate(
|
||||
{
|
||||
body: {
|
||||
amount: item.amount,
|
||||
travel_agency: item.travel_agency.id,
|
||||
},
|
||||
orderId: item.id,
|
||||
},
|
||||
{
|
||||
onSettled: () => setLoadingId(null),
|
||||
},
|
||||
);
|
||||
}}
|
||||
className="bg-green-600 hover:bg-green-700 text-white flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
{loadingId === item.id ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Banknote />
|
||||
{t("Pulni o'tkazish")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCloseId(item.id);
|
||||
updatePayed(
|
||||
{
|
||||
body: {
|
||||
status: "cancelled",
|
||||
},
|
||||
id: item.id,
|
||||
},
|
||||
{
|
||||
onSettled: () => setCloseId(null),
|
||||
},
|
||||
);
|
||||
}}
|
||||
disabled={item.status !== "pending"}
|
||||
className="bg-red-600 hover:bg-red-700 text-white flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
{closeId === item.id ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<XIcon />
|
||||
{t("Bekor qilish")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={5}
|
||||
className="text-center text-gray-400 py-4"
|
||||
>
|
||||
{t("Hozircha hech qanday pul olish so‘rovi mavjud emas.")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 🔹 PAGINATION */}
|
||||
<div className="flex justify-end gap-2 mt-5">
|
||||
<button
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
||||
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{[...Array(data?.data.data.total_pages)].map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
||||
currentPage === i + 1
|
||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
disabled={currentPage === data?.data.data.total_pages}
|
||||
onClick={() =>
|
||||
setCurrentPage((p) =>
|
||||
Math.min(p + 1, data ? data?.data.data.total_pages : 0),
|
||||
)
|
||||
}
|
||||
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
src/pages/payout-request/ui/WithdrawRequestForm.tsx
Normal file
73
src/pages/payout-request/ui/WithdrawRequestForm.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { sendpayout } from "@/pages/payout-request/lib/api";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function WithdrawRequestForm() {
|
||||
const [amount, setAmount] = useState("");
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({ body }: { body: { amount: number } }) => {
|
||||
return sendpayout({ body });
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("So‘rov muvaffaqiyatli yuborildi!");
|
||||
setAmount("");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("So‘rov yuborishda xatolik yuz berdi!");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Amount bo'sh bo'lmasligi va raqam ekanligini tekshirish
|
||||
const numericAmount = Number(amount);
|
||||
if (!numericAmount || numericAmount <= 0) {
|
||||
toast.error("Iltimos, miqdorni to‘g‘ri kiriting!");
|
||||
return;
|
||||
}
|
||||
|
||||
mutation.mutate({ body: { amount: numericAmount } });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-md mx-auto mt-10 bg-gray-900 border-gray-800 text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-semibold">
|
||||
Pul olish so‘rovi
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">
|
||||
Miqdorni kiriting (so‘m)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="Masalan: 100000"
|
||||
className="bg-gray-800 border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
disabled={!amount || mutation.isPending}
|
||||
>
|
||||
{mutation.isPending ? "Yuborilmoqda..." : "So‘rov yuborish"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
const BASE_URL =
|
||||
import.meta.env.VITE_API_URL || "https://simple-travel.felixits.uz/api/v1/";
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
const AUTH_LOGIN = "auth/token/phone/";
|
||||
const GET_ME = "auth/me/";
|
||||
@@ -31,13 +30,16 @@ const SUPPORT_USER = "dashboard/dashboard-support/";
|
||||
const SUPPORT_AGENCY = "dashboard/dashboard-travel-agency-request/";
|
||||
const USER_ORDERS = "dashboard/dashboard-ticket-order/";
|
||||
const AGENCY_ORDERS = "dashboard/dashboard-site-travel-agency-report/";
|
||||
const AGENCY_ORDERS_HISTORY = "dashboard/dashboard-order-status-history/";
|
||||
const POPULAR_TOURS = "dashboard/dashboard-ticket-featured/";
|
||||
const BANNER = "dashboard/dashboard-site-banner/";
|
||||
const TOUR_ADMIN = "dashboard/dashboard-tour-admin/";
|
||||
const PAYMENT_AGENCY = "dashboard/dashboard-site-agency-payments/";
|
||||
const PAYOT_REQUEST = "dashboard/dashboard-agency-payout-request/";
|
||||
|
||||
export {
|
||||
AGENCY_ORDERS,
|
||||
AGENCY_ORDERS_HISTORY,
|
||||
AMENITIES,
|
||||
APPROVED_AGENCY,
|
||||
AUTH_LOGIN,
|
||||
@@ -62,6 +64,7 @@ export {
|
||||
NEWS_CATEGORY,
|
||||
OFFERTA,
|
||||
PAYMENT_AGENCY,
|
||||
PAYOT_REQUEST,
|
||||
POPULAR_TOURS,
|
||||
SITE_SEO,
|
||||
SITE_SETTING,
|
||||
|
||||
@@ -510,5 +510,25 @@
|
||||
"Foydalanuvchini tahrirlash": "Редактировать пользователя",
|
||||
"Haqiqatan ham": "Вы действительно хотите",
|
||||
"ni tahrirlamoqchimisiz?": "отредактировать?",
|
||||
"Agentlik ID": "ID агентства"
|
||||
"Agentlik ID": "ID агентства",
|
||||
"Order History": "История статуса заказа",
|
||||
"Kim tomonidan": "Кем изменено",
|
||||
"Lavozimi": "Должность",
|
||||
"No'malum": "Неизвестно",
|
||||
"O'zgartirilgan status": "Изменённый статус",
|
||||
"O'zgartirgan sanasi": "Дата изменения",
|
||||
"Hozircha tarix mavjud emas": "История пока отсутствует",
|
||||
"pending_payment": "Ожидается оплата",
|
||||
"pending_confirmation": "Ожидается подтверждение",
|
||||
"cancelled": "Отменён",
|
||||
"confirmed": "Подтверждён",
|
||||
"completed": "Завершён",
|
||||
"Pul olish so‘rovlari": "Запросы на получение средств",
|
||||
"Pul olish so'rovi": "Запрос на снятие средств",
|
||||
"Miqdorni kiriting": "Введите сумму",
|
||||
"Hozircha hech qanday pul olish so‘rovi mavjud emas.": "Пока нет запросов на получение денег.",
|
||||
"Yuborish": "Отправить",
|
||||
"Pulni o'tkazish": "Перевод средств",
|
||||
"To‘lov muvaffaqiyatli amalga oshirildi!": "Оплата успешно выполнена!",
|
||||
"Sizga tizimga kirishga ruxsat berilmagan": "Вам не разрешен доступ к системе."
|
||||
}
|
||||
|
||||
@@ -511,5 +511,25 @@
|
||||
"Foydalanuvchini tahrirlash": "Foydalanuvchini tahrirlash",
|
||||
"Haqiqatan ham": "Haqiqatan ham",
|
||||
"ni tahrirlamoqchimisiz?": "ni tahrirlamoqchimisiz?",
|
||||
"Agentlik ID": "Agentlik ID"
|
||||
"Agentlik ID": "Agentlik ID",
|
||||
"Order History": "Buyurtma holati tarixi",
|
||||
"Kim tomonidan": "Kim tomonidan",
|
||||
"Lavozimi": "Lavozimi",
|
||||
"No'malum": "Noma’lum",
|
||||
"O'zgartirilgan status": "O‘zgartirilgan holat",
|
||||
"O'zgartirgan sanasi": "O‘zgartirilgan sana",
|
||||
"Hozircha tarix mavjud emas": "Hozircha tarix mavjud emas",
|
||||
"pending_payment": "To‘lov kutilmoqda",
|
||||
"pending_confirmation": "Tasdiqlanish kutilmoqda",
|
||||
"cancelled": "Bekor qilingan",
|
||||
"confirmed": "Tasdiqlangan",
|
||||
"completed": "Yakunlangan",
|
||||
"Pul olish so‘rovlari": "Pul olish so‘rovlari",
|
||||
"Pul olish so'rovi": "Pul olish so'rovi",
|
||||
"Miqdorni kiriting": "Miqdorni kiriting",
|
||||
"Hozircha hech qanday pul olish so‘rovi mavjud emas.": "Hozircha hech qanday pul olish so‘rovi mavjud emas.",
|
||||
"Yuborish": "Yuborish",
|
||||
"Pulni o'tkazish": "Pulni o'tkazish",
|
||||
"To‘lov muvaffaqiyatli amalga oshirildi!": "To‘lov muvaffaqiyatli amalga oshirildi!",
|
||||
"Sizga tizimga kirishga ruxsat berilmagan": "Sizga tizimga kirishga ruxsat berilmagan"
|
||||
}
|
||||
|
||||
127
src/shared/ui/pagination.tsx
Normal file
127
src/shared/ui/pagination.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
import { Button, buttonVariants } from "@/shared/ui/button"
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
@@ -148,6 +148,12 @@ const MENU_ITEMS = [
|
||||
{ label: "Sayt uchun Banner", path: "/site-banner/" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Pul olish so‘rovlari",
|
||||
icon: Wallet,
|
||||
path: "/withdraw/list",
|
||||
roles: ["buxgalter", "moderator", "tour_admin"],
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar({ role }: SidebarProps) {
|
||||
@@ -275,7 +281,7 @@ export function Sidebar({ role }: SidebarProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="lg:border fixed">
|
||||
<div className="lg:border fixed max-lg:sticky">
|
||||
{/* Mobil versiya */}
|
||||
<div className="lg:hidden flex items-center justify-end bg-gray-900 p-4 sticky top-0 z-50">
|
||||
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
||||
|
||||
Reference in New Issue
Block a user