diff --git a/src/App.tsx b/src/App.tsx index c3ee832..246eafe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,11 +30,11 @@ import CreateUser from "@/pages/users/ui/Create"; import EditUser from "@/pages/users/ui/Edit"; import UserList from "@/pages/users/ui/User"; import UserDetail from "@/pages/users/ui/UserDetail"; -import MainProvider from "@/providers/main"; +import { getMe } from "@/shared/config/api/auth/api"; import "@/shared/config/i18n"; -import useUserStore from "@/shared/hooks/user"; import { getAuthToken } from "@/shared/lib/authCookies"; import { Sidebar } from "@/widgets/sidebar/ui/Sidebar"; +import { useQuery } from "@tanstack/react-query"; import { useEffect } from "react"; import { Navigate, @@ -45,24 +45,35 @@ import { } from "react-router-dom"; const App = () => { - const { user } = useUserStore(); const token = getAuthToken(); const navigate = useNavigate(); const location = useLocation(); + const { data: user } = useQuery({ + queryKey: ["get_me"], + queryFn: () => getMe(), + select(data) { + return data.data.data; + }, + enabled: !!token, + }); const hideSidebarPaths = ["/login"]; const shouldShowSidebar = !hideSidebarPaths.includes(location.pathname); useEffect(() => { - if (token && user) { + if (token && user && user.role === "moderator") { navigate("/user"); + } else if (token && user && user.role === "tour_admin") { + navigate("/finance "); + } else if (token && user && user.role === "buxgalter") { + navigate("/finance"); } else if (!token && !user) { navigate("/login"); } }, [token, user]); return ( - + <>
{shouldShowSidebar && } @@ -78,11 +89,17 @@ const App = () => { } /> } /> } /> - } /> + } + /> } /> } /> } /> - } /> + } + /> } /> } /> } /> @@ -102,7 +119,7 @@ const App = () => { } />
-
+ ); }; diff --git a/src/main.tsx b/src/main.tsx index 61622dd..8ea83ba 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,3 +1,4 @@ +import MainProvider from "@/providers/main.tsx"; import { Toaster } from "@/shared/ui/sonner.tsx"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; @@ -7,9 +8,11 @@ import "./index.css"; createRoot(document.getElementById("root")!).render( - - - - + + + + + + , ); diff --git a/src/pages/agencies/lib/api.ts b/src/pages/agencies/lib/api.ts index 5e06cbb..fce64a9 100644 --- a/src/pages/agencies/lib/api.ts +++ b/src/pages/agencies/lib/api.ts @@ -1,9 +1,10 @@ import type { + agencyUserData, GetAllAgencyData, GetDetailAgencyData, } from "@/pages/agencies/lib/type"; import httpClient from "@/shared/config/api/httpClient"; -import { GET_ALL_AGENCY } from "@/shared/config/api/URLs"; +import { GET_ALL_AGENCY, TOUR_ADMIN } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; const getAllAgency = async ({ @@ -55,4 +56,42 @@ const updateAgencyStatus = async ({ return response; }; -export { deleteAgency, getAllAgency, getDetailAgency, updateAgencyStatus }; +const agencyUser = async ( + id: number, +): Promise> => { + const res = await httpClient.get(`${TOUR_ADMIN}${id}/list/`); + return res; +}; + +const agencyUserDelete = async (id: number) => { + const res = await httpClient.delete(`${TOUR_ADMIN}${id}/`); + return res; +}; + +const agencyUserUpdate = async ( + id: number, +): Promise< + AxiosResponse<{ + status: boolean; + data: { + id: number; + phone: string; + role: string; + travel_agency: string; + password: string; + }; + }> +> => { + const res = await httpClient.post(`${TOUR_ADMIN}${id}/update/`); + return res; +}; + +export { + agencyUser, + agencyUserDelete, + agencyUserUpdate, + deleteAgency, + getAllAgency, + getDetailAgency, + updateAgencyStatus, +}; diff --git a/src/pages/agencies/lib/type.ts b/src/pages/agencies/lib/type.ts index 57e0aea..687ec6f 100644 --- a/src/pages/agencies/lib/type.ts +++ b/src/pages/agencies/lib/type.ts @@ -56,3 +56,14 @@ export interface GetDetailAgencyData { ]; }; } + +export interface agencyUserData { + status: boolean; + data: { + first_name: string; + id: number; + last_name: string; + phone: string; + role: string; + }; +} diff --git a/src/pages/agencies/ui/AgencyDetail.tsx b/src/pages/agencies/ui/AgencyDetail.tsx index f8044e8..9f0843c 100644 --- a/src/pages/agencies/ui/AgencyDetail.tsx +++ b/src/pages/agencies/ui/AgencyDetail.tsx @@ -1,11 +1,26 @@ "use client"; -import { getDetailAgency, updateAgencyStatus } from "@/pages/agencies/lib/api"; +import { + agencyUser, + agencyUserDelete, + agencyUserUpdate, + getDetailAgency, + updateAgencyStatus, +} from "@/pages/agencies/lib/api"; +import { AgencyUsersSection } from "@/pages/agencies/ui/AgencyUsersSection"; import formatPhone from "@/shared/lib/formatPhone"; 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, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; import { Select, SelectContent, @@ -25,6 +40,7 @@ import { Percent, TrendingUp, } from "lucide-react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate, useParams } from "react-router-dom"; import { toast } from "sonner"; @@ -34,12 +50,30 @@ export default function AgencyDetailPage() { const router = useNavigate(); const { t } = useTranslation(); const queryClient = useQueryClient(); + const [openUser, setOpenUser] = useState(false); + const [user, setUser] = useState<{ + status: boolean; + data: { + id: number; + phone: string; + role: string; + travel_agency: string; + password: string; + }; + } | null>(null); const { data, isLoading, refetch, isError } = useQuery({ queryKey: ["detail_agency"], queryFn: () => getDetailAgency({ id: Number(params.id) }), }); + const { data: agency, isLoading: isLoadingUsers } = useQuery({ + queryKey: ["agency_user", params.id], + queryFn: () => { + return agencyUser(Number(params.id)); + }, + }); + const statusMutation = useMutation({ mutationFn: (newStatus: "pending" | "approved" | "cancelled") => updateAgencyStatus({ @@ -56,12 +90,44 @@ export default function AgencyDetailPage() { }, }); + const updateUserMutation = useMutation({ + mutationFn: (updatedUser: number) => agencyUserUpdate(updatedUser), + onSuccess: (res) => { + queryClient.refetchQueries({ queryKey: ["agency_user", params.id] }); + toast.success(t("Foydalanuvchi muvaffaqiyatli yangilandi")); + setOpenUser(true); + setUser(res.data); + }, + onError: () => { + toast.error(t("Foydalanuvchini yangilashda xatolik")); + }, + }); + + const deleteUserMutation = useMutation({ + mutationFn: (userId: number) => agencyUserDelete(userId), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["agency_user", params.id] }); + toast.success(t("Foydalanuvchi muvaffaqiyatli o'chirildi")); + }, + onError: () => { + toast.error(t("Foydalanuvchini o'chirishda xatolik")); + }, + }); + const handleStatusChange = ( newStatus: "pending" | "approved" | "cancelled", ) => { statusMutation.mutate(newStatus); }; + const handleEditUser = (user: number) => { + updateUserMutation.mutate(user); + }; + + const handleDeleteUser = (userId: number) => { + deleteUserMutation.mutate(userId); + }; + if (isLoading) { return (
@@ -337,6 +403,22 @@ export default function AgencyDetailPage() {
+ + {agency?.data.data && ( +
+ +
+ )} + @@ -393,6 +475,47 @@ export default function AgencyDetailPage() { + {}}> + + + + {t("Yangi foydalanuvchi ma'lumotlari")} + + + {t("Agentlik uchun tizimga kirish ma'lumotlari")} + + + + {user && ( +
+
+

+ {t("Telefon raqam (login)")} +

+

+ {formatPhone(user.data.phone)} +

+
+ +
+

{t("Parol")}

+

+ {user.data.password} +

+
+
+ )} + + + + +
+
); } diff --git a/src/pages/agencies/ui/AgencyUsersSection.tsx b/src/pages/agencies/ui/AgencyUsersSection.tsx new file mode 100644 index 0000000..8f25858 --- /dev/null +++ b/src/pages/agencies/ui/AgencyUsersSection.tsx @@ -0,0 +1,195 @@ +import formatPhone from "@/shared/lib/formatPhone"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/shared/ui/dialog"; + +import { Pencil, Phone, Shield, Trash2, User } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +interface AgencyUser { + first_name: string; + id: number; + last_name: string; + phone: string; + role: string; +} + +interface AgencyUsersProps { + users: AgencyUser[]; + onEdit: (userId: number) => void; + onDelete: (userId: number) => void; + isLoading?: boolean; +} + +export function AgencyUsersSection({ + users, + onEdit, + onDelete, + isLoading = false, +}: AgencyUsersProps) { + const { t } = useTranslation(); + + const getRoleBadge = (role: string) => { + const roleColors: Record = { + admin: "bg-purple-500/20 text-purple-300 border-purple-500/40", + manager: "bg-blue-500/20 text-blue-300 border-blue-500/40", + user: "bg-gray-500/20 text-gray-300 border-gray-500/40", + agent: "bg-green-500/20 text-green-300 border-green-500/40", + }; + + return ( + + {role.toUpperCase()} + + ); + }; + + if (isLoading) { + return ( + + + + + {t("Agentlik foydalanuvchilari")} + + + +
+ {t("Ma'lumotlar yuklanmoqda...")} +
+
+
+ ); + } + + if (!users || users.length === 0) { + return ( + + + + + {t("Agentlik foydalanuvchilari")} + + + +
+ {t("Hozircha foydalanuvchilar yo'q")} +
+
+
+ ); + } + + return ( + + + + + {t("Agentlik foydalanuvchilari")} + +

+ {t("Agentlik bilan bog'langan foydalanuvchilar ro'yxati")} +

+
+ +
+ {users.map((user) => ( +
+
+
+
+ +
+ +
+
+

+ {user.first_name} {user.last_name} +

+ {getRoleBadge(user.role)} +
+ +
+
+ + {formatPhone(user.phone)} +
+
+ + ID: {user.id} +
+
+
+
+ +
+ {/* Edit Button */} + + + {/* Delete Alert Dialog */} + + + + + + + + {t("Foydalanuvchini o'chirish")} + + + {t("Haqiqatan ham")}{" "} + + {user.first_name} {user.last_name} + {" "} + {t( + "ni o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.", + )} + + + + + + + + +
+
+
+ ))} +
+
+
+ ); +} diff --git a/src/pages/auth/ui/Login.tsx b/src/pages/auth/ui/Login.tsx index e07e060..9752ebb 100644 --- a/src/pages/auth/ui/Login.tsx +++ b/src/pages/auth/ui/Login.tsx @@ -40,10 +40,7 @@ const Login = () => { const { mutate, isPending } = useMutation({ mutationFn: ({ password, phone }: { password: string; phone: string }) => - authLogin({ - password, - phone, - }), + authLogin({ password, phone }), onSuccess: (res) => { setAuthToken(res.data.access); setAuthRefToken(res.data.refresh); @@ -83,9 +80,17 @@ const Login = () => { }); function onSubmit(values: z.infer) { + const cleanPhone = onlyNumber(values.phone); + + // allow both 998 and 888 prefixes + if (!cleanPhone.startsWith("998") && !cleanPhone.startsWith("888")) { + toast.error(t("Telefon raqami +998 yoki +888 bilan boshlanishi kerak")); + return; + } + mutate({ password: values.password, - phone: onlyNumber(values.phone), + phone: cleanPhone, }); } @@ -110,12 +115,20 @@ const Login = () => { - field.onChange(formatPhone(e.target.value)) - } + value={field.value} + onChange={(e) => { + const inputValue = e.target.value; + + if (inputValue.trim() === "") { + field.onChange(""); + return; + } + + // Formatlash + field.onChange(formatPhone(inputValue)); + }} maxLength={19} className="h-[56px] text-lg rounded-xl bg-gray-700 border-gray-600 text-white placeholder-gray-400" /> diff --git a/src/pages/finance/lib/api.ts b/src/pages/finance/lib/api.ts index 9d7d1b8..4a5ec55 100644 --- a/src/pages/finance/lib/api.ts +++ b/src/pages/finance/lib/api.ts @@ -1,9 +1,11 @@ import type { + AgencyOrderData, + UserAgencyDetailData, UserOrderData, UserOrderDetailData, } from "@/pages/finance/lib/type"; import httpClient from "@/shared/config/api/httpClient"; -import { USER_ORDERS } from "@/shared/config/api/URLs"; +import { AGENCY_ORDERS, USER_ORDERS } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; const getAllOrder = async (params: { @@ -21,6 +23,14 @@ const getAllOrder = async (params: { return res; }; +const getAllOrderAgecy = async (params: { + page: number; + page_size: number; +}): Promise> => { + const res = await httpClient.get(AGENCY_ORDERS, { params }); + return res; +}; + const getDetailOrder = async ( id: number, ): Promise> => { @@ -28,6 +38,13 @@ const getDetailOrder = async ( return res; }; +const getDetailAgencyOrder = async ( + id: number, +): Promise> => { + const res = await httpClient.get(`${AGENCY_ORDERS}${id}/`); + return res; +}; + const updateDetailOrder = async ({ id, body, @@ -46,4 +63,10 @@ const updateDetailOrder = async ({ return res; }; -export { getAllOrder, getDetailOrder, updateDetailOrder }; +export { + getAllOrder, + getAllOrderAgecy, + getDetailAgencyOrder, + getDetailOrder, + updateDetailOrder, +}; diff --git a/src/pages/finance/lib/type.ts b/src/pages/finance/lib/type.ts index 64a41ee..22fa3d7 100644 --- a/src/pages/finance/lib/type.ts +++ b/src/pages/finance/lib/type.ts @@ -110,3 +110,73 @@ export interface UserOrderDetailData { total_price: number; }; } + +export interface AgencyOrderData { + status: boolean; + data: { + links: { + previous: string; + next: string; + }; + total_items: number; + total_pages: number; + page_size: number; + current_page: number; + results: { + id: number; + name: string; + custom_id: string; + paid: number; + pending: number; + ticket_sold_count: number; + ticket_count: string; + }[]; + }; +} + +export interface UserAgencyDetailData { + status: boolean; + data: { + id: 0; + name: string; + custom_id: string; + share_percentage: number; + addres: string; + email: string; + phone: string; + web_site: string; + paid: number; + pending: number; + ticket_sold_count: number; + ticket_count: string; + total_income: number; + platform_income: number; + total_booking_count: string; + rating: string; + orders: { + id: number; + user: { + id: number; + first_name: string; + last_name: string; + contact: string; + }; + total_price: number; + departure_date: string; + arrival_time: string; + created_at: string; + }[]; + comments: { + user: { + id: number; + first_name: string; + last_name: string; + contact: string; + }; + text: string; + rating: number; + ticket: number; + created: string; + }[]; + }; +} diff --git a/src/pages/finance/ui/Finance.tsx b/src/pages/finance/ui/Finance.tsx index 2599c1b..a5dc7aa 100644 --- a/src/pages/finance/ui/Finance.tsx +++ b/src/pages/finance/ui/Finance.tsx @@ -1,6 +1,6 @@ "use client"; -import { getAllOrder } from "@/pages/finance/lib/api"; +import { getAllOrder, getAllOrderAgecy } from "@/pages/finance/lib/api"; import type { OrderStatus } from "@/pages/finance/lib/type"; import formatPhone from "@/shared/lib/formatPhone"; import formatPrice from "@/shared/lib/formatPrice"; @@ -24,100 +24,28 @@ import { } from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; +import { Link, useSearchParams } from "react-router-dom"; -type Purchase = { - id: number; - userName: string; - userPhone: string; - tourName: string; - tourId: number; - agencyName: string; - agencyId: number; - destination: string; - travelDate: string; - amount: number; - paymentStatus: "paid" | "pending" | "cancelled" | "refunded"; - purchaseDate: string; -}; +type Role = + | "superuser" + | "admin" + | "moderator" + | "tour_admin" + | "buxgalter" + | "operator" + | "user"; -const mockPurchases: Purchase[] = [ - { - id: 1, - userName: "Aziza Karimova", - userPhone: "+998 90 123 45 67", - tourName: "Dubai Luxury Tour", - tourId: 1, - agencyName: "Silk Road Travel", - agencyId: 1, - destination: "Dubai, UAE", - travelDate: "2025-11-10", - amount: 1500000, - paymentStatus: "paid", - purchaseDate: "2025-10-10", - }, - { - id: 2, - userName: "Sardor Rahimov", - userPhone: "+998 91 234 56 78", - tourName: "Bali Adventure Package", - tourId: 2, - agencyName: "Silk Road Travel", - agencyId: 1, - destination: "Bali, Indonesia", - travelDate: "2025-11-15", - amount: 1800000, - paymentStatus: "paid", - purchaseDate: "2025-10-12", - }, - { - id: 3, - userName: "Nilufar Toshmatova", - userPhone: "+998 93 345 67 89", - tourName: "Dubai Luxury Tour", - tourId: 1, - agencyName: "Silk Road Travel", - agencyId: 1, - destination: "Dubai, UAE", - travelDate: "2025-11-20", - amount: 1500000, - paymentStatus: "pending", - purchaseDate: "2025-10-14", - }, - { - id: 4, - userName: "Jamshid Alimov", - userPhone: "+998 94 456 78 90", - tourName: "Istanbul Express Tour", - tourId: 3, - agencyName: "Orient Express", - agencyId: 3, - destination: "Istanbul, Turkey", - travelDate: "2025-11-05", - amount: 1200000, - paymentStatus: "cancelled", - purchaseDate: "2025-10-08", - }, - { - id: 5, - userName: "Madina Yusupova", - userPhone: "+998 97 567 89 01", - tourName: "Paris Romantic Getaway", - tourId: 4, - agencyName: "Euro Travels", - agencyId: 2, - destination: "Paris, France", - travelDate: "2025-12-01", - amount: 2200000, - paymentStatus: "paid", - purchaseDate: "2025-10-16", - }, -]; - -export default function FinancePage() { +export default function FinancePage({ user }: { user: Role }) { const { t } = useTranslation(); - const [currentPage, setCurrentPage] = useState(1); - const [tab, setTab] = useState<"bookings" | "agencies">("bookings"); + const [searchParams, setSearchParams] = useSearchParams(); + const tabParam = searchParams.get("tab") as "bookings" | "agencies" | null; + const pageParam = Number(searchParams.get("page")) || 1; + const pageAgencyParam = Number(searchParams.get("page_agency")) || 1; + const [currentPage, setCurrentPage] = useState(pageParam); + const [currentPageAgency, setCurrentPageAgency] = useState(pageAgencyParam); + const [tab, setTab] = useState<"bookings" | "agencies">( + tabParam ?? "bookings", + ); const [filterStatus, setFilterStatus] = useState< | "" | "pending_payment" @@ -127,9 +55,25 @@ export default function FinancePage() { | "cancelled" >(""); + useEffect(() => { + setSearchParams({ + tab, + page: String(currentPage), + page_agency: String(currentPageAgency), + }); + }, [tab, currentPage, currentPageAgency, setSearchParams]); + + // ✅ Param o‘zgarsa — holatni sinxronlashtirish + useEffect(() => { + if (tabParam && tabParam !== tab) { + setTab(tabParam); + } + }, [tabParam]); + useEffect(() => { setCurrentPage(1); - }, [filterStatus]); + setCurrentPageAgency(1); + }, [filterStatus, tab]); const { data, isLoading, isError, refetch } = useQuery({ queryKey: ["list_order_user", currentPage, filterStatus], @@ -141,6 +85,20 @@ export default function FinancePage() { }), }); + const { + data: agencyData, + isLoading: agenctLoad, + isError: agencyError, + refetch: agencyRef, + } = useQuery({ + queryKey: ["agecy_order_list", currentPageAgency], + queryFn: () => + getAllOrderAgecy({ + page: currentPageAgency, + page_size: 10, + }), + }); + const stats = [ { title: t("Jami daromad"), @@ -253,52 +211,6 @@ export default function FinancePage() { } }; - if (isLoading) { - return ( -
- -

{t("Ma'lumotlar yuklanmoqda...")}

-
- ); - } - - if (isError) { - return ( -
- -

- {t("Ma'lumotlarni yuklashda xatolik yuz berdi.")} -

- -
- ); - } - - const agencies = Array.from( - new Set(mockPurchases.map((p) => p.agencyId)), - ).map((id) => { - const agencyPurchases = mockPurchases.filter((p) => p.agencyId === id); - return { - id, - name: agencyPurchases[0].agencyName, - totalPaid: agencyPurchases - .filter((p) => p.paymentStatus === "paid") - .reduce((sum, p) => sum + p.amount, 0), - pending: agencyPurchases - .filter((p) => p.paymentStatus === "pending") - .reduce((sum, p) => sum + p.amount, 0), - purchaseCount: agencyPurchases.length, - destinations: Array.from( - new Set(agencyPurchases.map((p) => p.destination)), - ), - }; - }); - return (
@@ -330,22 +242,26 @@ export default function FinancePage() { {t("Bandlovlar va to‘lovlar")} - + {user === "tour_admin" || + user === "buxgalter" || + user === "admin" || + (user === "superuser" && ( + + ))}
{tab === "bookings" && ( <> - {/* Filter */}
{[ "", @@ -389,7 +305,6 @@ export default function FinancePage() { ))}
- {/* Stats */}
{stats.map((item, index) => (
- {/* Booking Cards */}

{t("Oxirgi bandlovlar")}

-
- {data?.data.data.results.orders.map((p, index) => ( -
+ +

+ {t("Ma'lumotlar yuklanmoqda...")} +

+
+ ) : isError ? ( +
+ +

+ {t("Ma'lumotlarni yuklashda xatolik yuz berdi.")} +

+ +
+ ) : ( + data && + !isError && + !isLoading && ( +
+ {data?.data.data.results.orders.map((p, index) => ( +
+
+
+

+ {p.user.first_name} {p.user.last_name} +

+
+

+ {p.user.contact.includes("gmail.com") + ? p.user.contact + : formatPhone(p.user.contact)} +

+

+ {p.tour_name} +

+
+ +

{p.destination}

+
+
+ {/*

- {t("Sayohat sanasi")} + {t("Sayohat sanasi")}

- {p.travelDate} -

-
*/} -
-

{t("Miqdor")}

-

- {formatPrice(p.total_price, true)} + {p.travelDate}

+
*/} +
+

+ {t("Miqdor")} +

+

+ {formatPrice(p.total_price, true)} +

+
+
{getStatusBadge(p.order_status)}
+
+
+
+ + +
-
{getStatusBadge(p.order_status)}
-
-
- - - -
+ ))}
- ))} -
+ ) + )}
-
+ ) : ( + agencyData && + !agencyError && + !agenctLoad && ( + <> +

+ {t("Partner Agencies")} +

+
+ {agencyData?.data.data.results.map((a) => ( +
+

+ + {a.name} +

-
-

- Bookings:{" "} - - {a.purchaseCount} - -

-

- Destinations:{" "} - - {a.destinations.length} - -

-
+
+
+

+ {t("Paid")} +

+

+ {formatPrice(a.paid, true)} +

+
+
+

+ {t("Pending")} +

+

+ {formatPrice(a.pending, true)} +

+
+
-
- +

+ {t("Bookings")}:{" "} + + {a.ticket_sold_count} + +

+

+ {t("Destinations")}:{" "} + + {a.ticket_count} + +

+
+ +
+ + {t("Ko'rish")} + +
+
+ ))} +
+ + ) + )} +
+ + + {[...Array(agencyData?.data.data.total_pages)].map( + (_, i) => ( + + ), + )} + +
-
- ))} - - - )} + + )} + + ))} ); diff --git a/src/pages/finance/ui/FinanceDetailTour.tsx b/src/pages/finance/ui/FinanceDetailTour.tsx index ca482ac..90b3efd 100644 --- a/src/pages/finance/ui/FinanceDetailTour.tsx +++ b/src/pages/finance/ui/FinanceDetailTour.tsx @@ -1,166 +1,41 @@ "use client"; +import { getDetailAgencyOrder } from "@/pages/finance/lib/api"; +import formatDate from "@/shared/lib/formatDate"; +import formatPhone from "@/shared/lib/formatPhone"; +import formatPrice from "@/shared/lib/formatPrice"; +import { Card, CardHeader } from "@/shared/ui/card"; +import { useQuery } from "@tanstack/react-query"; import { ArrowLeft, - Calendar, DollarSign, Eye, - Hotel, + Mail, MapPin, + Phone, Plane, Star, TrendingUp, Users, } from "lucide-react"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Link, useParams } from "react-router-dom"; -type TourPurchase = { - id: number; - userName: string; - userPhone: string; - tourName: string; - tourId: number; - agencyName: string; - agencyId: number; - destination: string; - travelDate: string; - amount: number; - paymentStatus: "paid" | "pending" | "cancelled" | "refunded"; - purchaseDate: string; - rating: number; - review: string; -}; - -const mockTourData = { - id: 1, - name: "Dubai Luxury Tour", - destination: "Dubai, UAE", - duration: "7 days", - price: 1500000, - totalBookings: 45, - totalRevenue: 67500000, - averageRating: 4.8, - agency: "Silk Road Travel", - description: - "Experience the ultimate luxury in Dubai with 5-star accommodations, private tours, and exclusive experiences.", - inclusions: [ - "5-star hotel accommodation", - "Private city tours", - "Desert safari experience", - "Burj Khalifa tickets", - "Airport transfers", - ], -}; - -const mockTourPurchases: TourPurchase[] = [ - { - id: 1, - userName: "Aziza Karimova", - userPhone: "+998 90 123 45 67", - tourName: "Dubai Luxury Tour", - tourId: 1, - agencyName: "Silk Road Travel", - agencyId: 1, - destination: "Dubai, UAE", - travelDate: "2025-11-10", - amount: 1500000, - paymentStatus: "paid", - purchaseDate: "2025-10-10", - rating: 5, - review: - "Amazing experience! The hotel was luxurious and the tours were well organized.", - }, - { - id: 2, - userName: "Sardor Rahimov", - userPhone: "+998 91 234 56 78", - tourName: "Dubai Luxury Tour", - tourId: 1, - agencyName: "Silk Road Travel", - agencyId: 1, - destination: "Dubai, UAE", - travelDate: "2025-11-15", - amount: 1500000, - paymentStatus: "paid", - purchaseDate: "2025-10-12", - rating: 4, - review: - "Great tour overall. The desert safari was the highlight of our trip.", - }, - { - id: 3, - userName: "Nilufar Toshmatova", - userPhone: "+998 93 345 67 89", - tourName: "Dubai Luxury Tour", - tourId: 1, - agencyName: "Silk Road Travel", - agencyId: 1, - destination: "Dubai, UAE", - travelDate: "2025-11-20", - amount: 1500000, - paymentStatus: "pending", - purchaseDate: "2025-10-14", - rating: 0, - review: "", - }, -]; - export default function FinanceDetailTour() { + const { t } = useTranslation(); const [activeTab, setActiveTab] = useState< "overview" | "bookings" | "reviews" >("overview"); const params = useParams(); - console.log(params); - - // const {} = useQuery({ - // queryKey: ["detail_order"], - // queryFn: () => getDetailOrder() - // }) - - const getStatusBadge = (status: TourPurchase["paymentStatus"]) => { - const base = - "px-3 py-1 rounded-full text-sm font-medium inline-flex items-center gap-2"; - switch (status) { - case "paid": - return ( - -
- Paid -
- ); - case "pending": - return ( - -
- Pending -
- ); - case "cancelled": - return ( - -
- Cancelled -
- ); - case "refunded": - return ( - -
- Refunded -
- ); - } - }; + const { data } = useQuery({ + queryKey: ["detail_order", params.id], + queryFn: () => getDetailAgencyOrder(Number(params.id)), + select(data) { + return data.data.data; + }, + }); const renderStars = (rating: number) => { return ( @@ -179,14 +54,6 @@ export default function FinanceDetailTour() { ); }; - const paidBookings = mockTourPurchases.filter( - (p) => p.paymentStatus === "paid", - ); - const totalRevenue = paidBookings.reduce((sum, p) => sum + p.amount, 0); - const pendingRevenue = mockTourPurchases - .filter((p) => p.paymentStatus === "pending") - .reduce((sum, p) => sum + p.amount, 0); - return (
@@ -200,10 +67,7 @@ export default function FinanceDetailTour() {
-

Tour Financial Details

-

- Financial performance for {mockTourData.name} -

+

{data?.name}

@@ -212,48 +76,46 @@ export default function FinanceDetailTour() {
-

Total Revenue

+

+ {t("To'langan summa")} +

- ${(totalRevenue / 1000000).toFixed(1)}M -

-

- From completed bookings + {data && formatPrice(data?.paid, true)}

-

Pending Revenue

+

+ {t("Kutilayotgan summa")} +

- ${(pendingRevenue / 1000000).toFixed(1)}M + {data && formatPrice(data.pending, true)}

-

Awaiting payment

-

Total Bookings

+

{t("Total Bookings")}

- {mockTourPurchases.length} + {data?.ticket_sold_count}

-

All bookings

-

Average Rating

+

{t("Average Rating")}

- {mockTourData.averageRating}/5 + {data?.rating}/5

-

Customer satisfaction

@@ -270,7 +132,7 @@ export default function FinanceDetailTour() { onClick={() => setActiveTab("overview")} > - Tour Overview + {t("Umumiy ma'lumot")} @@ -301,14 +163,18 @@ export default function FinanceDetailTour() {
{/* Tour Information */}
-

Tour Information

+

+ {t("Umumiy ma'lumot")} +

-

Tour Name

+

+ {t("Agentlik nomi")} +

- {mockTourData.name} + {data?.name}

@@ -316,54 +182,95 @@ export default function FinanceDetailTour() {
-

Destination

+

{t("Manzili")}

+

{data?.addres}

+
+
+ +
+ +
+

+ {t("Telefon raqami")} +

- {mockTourData.destination} + {data && formatPhone(data?.phone)}

- +
-

Duration

-

{mockTourData.duration}

-
-
- -
- -
-

Agency

-

{mockTourData.agency}

+

{t("E-mail")}

+

{data?.email}

-

Description

-

- {mockTourData.description} +

+ {t("Id raqami va ulushi")}

+
+

+ {t("Id")}: {data?.custom_id} +

+

+ {t("Ulushi")}: {data?.share_percentage}% +

+
-
-

Tour Inclusions

-
-

Base Price

-

- ${(mockTourData.price / 1000000).toFixed(1)}M -

-

per person

+ {data && ( +
+ + + {t("Qo'shilgan turlar")} +

+ {data.ticket_count} +

+
+
+ + + + {t("Umumiy daromad")} +

+ {formatPrice(data.total_income, true)} +

+
+
+ + {/* Platforma daromadi */} + + + {t("Platformaga daromadi")} +

+ {formatPrice(data.platform_income, true)} +

+
+
+ + + + {t("Agentlik daromadi")} +

+ {formatPrice(data.paid + data.pending, true)} +

+
+
-
+ )}
)} {activeTab === "bookings" && (
-

Recent Bookings

- {mockTourPurchases.map((purchase) => ( +

+ {t("Recent Bookings")} +

+ {data?.orders.map((purchase) => (

- {purchase.userName} + {purchase.user.first_name} {purchase.user.last_name}

- {purchase.userPhone} + {formatPhone(purchase.user.contact)}

- {getStatusBadge(purchase.paymentStatus)} + {/* {getStatusBadge(purchase.)} */}
-

Travel Date

+

+ {t("Travel Date")} +

- {purchase.travelDate} + {purchase.departure_date}

-

Booking Date

-

{purchase.purchaseDate}

+

+ {t("Booking Date")} +

+

+ {formatDate.format(purchase.created_at, "DD-MM-YYYY")} +

-

Amount

+

{t("Amount")}

- ${(purchase.amount / 1000000).toFixed(1)}M + {formatPrice(purchase.total_price, true)}

-
-
- Agency: {purchase.agencyName} -
+
- View Details + {t("Batafsil")}
@@ -420,39 +330,29 @@ export default function FinanceDetailTour() { {activeTab === "reviews" && (

Customer Reviews

- {mockTourPurchases - .filter((purchase) => purchase.rating > 0) - .map((purchase) => ( -
-
-
-

- {purchase.userName} -

-

- {purchase.travelDate} -

-
-
- {renderStars(purchase.rating)} - - {purchase.rating}.0 - -
+ {data?.comments.map((purchase) => ( +
+
+
+

+ {purchase.user.first_name} {purchase.user.last_name} +

+

+ {formatDate.format(purchase.created, "DD-MM-YYYY")} +

- -

{purchase.review}

- -
-
- Booked on {purchase.purchaseDate} -
+
+ {renderStars(purchase.rating)} + {purchase.rating}
- ))} + +

{purchase.text}

+
+ ))}
)}
diff --git a/src/pages/support/lib/api.ts b/src/pages/support/lib/api.ts index 9287e24..541c61e 100644 --- a/src/pages/support/lib/api.ts +++ b/src/pages/support/lib/api.ts @@ -3,7 +3,12 @@ import type { GetSupportUser, } from "@/pages/support/lib/types"; import httpClient from "@/shared/config/api/httpClient"; -import { SUPPORT_AGENCY, SUPPORT_USER } from "@/shared/config/api/URLs"; +import { + GET_ALL_AGENCY, + SUPPORT_AGENCY, + SUPPORT_USER, + TOUR_ADMIN, +} from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; const getSupportUser = async (params: { @@ -55,10 +60,43 @@ const getSupportAgencyDetail = async ( return res; }; +const createTourAdmin = async ( + id: number, +): Promise< + AxiosResponse<{ + status: boolean; + data: { + id: number; + phone: string; + role: string; + travel_agency: string; + password: string; + }; + }> +> => { + const res = await httpClient.post(`${TOUR_ADMIN}${id}/create/`); + return res; +}; + +const updateTour = async ({ + id, + body, +}: { + id: number; + body: { + status: "pending" | "approved" | "cancelled"; + }; +}) => { + const res = await httpClient.patch(`${GET_ALL_AGENCY}${id}/`, body); + return res; +}; + export { + createTourAdmin, deleteSupportUser, getSupportAgency, getSupportAgencyDetail, getSupportUser, updateSupportUser, + updateTour, }; diff --git a/src/pages/support/ui/SupportAgency.tsx b/src/pages/support/ui/SupportAgency.tsx index 30b9487..49b996f 100644 --- a/src/pages/support/ui/SupportAgency.tsx +++ b/src/pages/support/ui/SupportAgency.tsx @@ -1,15 +1,40 @@ -import { getSupportAgency } from "@/pages/support/lib/api"; +import { + createTourAdmin, + getSupportAgency, + updateTour, +} from "@/pages/support/lib/api"; import type { GetSupportAgencyRes } from "@/pages/support/lib/types"; import formatPhone from "@/shared/lib/formatPhone"; import { Button } from "@/shared/ui/button"; -import { useQuery } from "@tanstack/react-query"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, Loader2, XIcon } from "lucide-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; +import { toast } from "sonner"; const SupportAgency = () => { const [query, setQuery] = useState(""); + const queryClient = useQueryClient(); + const [openUser, setOpenUser] = useState(false); + const [user, setUser] = useState<{ + status: boolean; + data: { + id: number; + phone: string; + role: string; + travel_agency: string; + password: string; + }; + } | null>(null); const { t } = useTranslation(); const [selected, setSelected] = useState(null); @@ -19,6 +44,44 @@ const SupportAgency = () => { getSupportAgency({ page: 1, page_size: 10, search: "", status: "" }), }); + const { mutate: updateTours } = useMutation({ + mutationFn: ({ + id, + body, + }: { + id: number; + body: { + status: "pending" | "approved" | "cancelled"; + }; + }) => updateTour({ body, id }), + onSuccess: (res) => { + queryClient.refetchQueries({ queryKey: ["support_agency"] }); + setOpenUser(true); + setUser(res.data); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + richColors: true, + position: "top-center", + }); + }, + }); + + const { mutate: createAdmin } = useMutation({ + mutationFn: (id: number) => createTourAdmin(id), + onSuccess: (res) => { + queryClient.refetchQueries({ queryKey: ["support_agency"] }); + setOpenUser(true); + setUser(res.data); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + richColors: true, + position: "top-center", + }); + }, + }); + if (isLoading) { return (
@@ -213,8 +276,12 @@ const SupportAgency = () => {
)} + {}}> + + + + {t("Yangi foydalanuvchi ma'lumotlari")} + + + {t("Agentlik uchun tizimga kirish ma'lumotlari")} + + + + {user && ( +
+
+

+ {t("Telefon raqam (login)")} +

+

+ {formatPhone(user.data.phone)} +

+
+ +
+

{t("Parol")}

+

+ {user.data.password} +

+
+
+ )} + + + + +
+
); }; diff --git a/src/pages/tours/ui/Tours.tsx b/src/pages/tours/ui/Tours.tsx index 9c29dca..9110971 100644 --- a/src/pages/tours/ui/Tours.tsx +++ b/src/pages/tours/ui/Tours.tsx @@ -40,7 +40,16 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; -const Tours = () => { +type Role = + | "superuser" + | "admin" + | "moderator" + | "tour_admin" + | "buxgalter" + | "operator" + | "user"; + +const Tours = ({ user }: { user: Role }) => { const { t } = useTranslation(); const [page, setPage] = useState(1); const [deleteId, setDeleteId] = useState(null); @@ -133,7 +142,11 @@ const Tours = () => {

{t("Turlar ro'yxati")}

-
@@ -149,9 +162,11 @@ const Tours = () => { {t("Mehmonxona")} {t("Narxi")} - - {t("Popular")} - + {user && user === "moderator" && ( + + {t("Popular")} + + )} {t("Операции")} @@ -170,7 +185,7 @@ const Tours = () => {
- {tour.duration_days} kun + {tour.duration_days} {t("kun")}
@@ -185,18 +200,19 @@ const Tours = () => { {formatPrice(tour.price, true)} - - - - popular({ - id: tour.id, - value: tour.featured_tickets ? 0 : 1, - }) - } - /> - + {user && user === "moderator" && ( + + + popular({ + id: tour.id, + value: tour.featured_tickets ? 0 : 1, + }) + } + /> + + )}
diff --git a/src/shared/config/api/URLs.ts b/src/shared/config/api/URLs.ts index 9373368..1f0a376 100644 --- a/src/shared/config/api/URLs.ts +++ b/src/shared/config/api/URLs.ts @@ -27,10 +27,13 @@ const SITE_SETTING = "dashboard/dashboard-site-settings/"; 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 POPULAR_TOURS = "dashboard/dashboard-ticket-featured/"; const BANNER = "dashboard/dashboard-site-banner/"; +const TOUR_ADMIN = "dashboard/dashboard-tour-admin/"; export { + AGENCY_ORDERS, AUTH_LOGIN, BANNER, BASE_URL, @@ -57,6 +60,7 @@ export { SITE_SETTING, SUPPORT_AGENCY, SUPPORT_USER, + TOUR_ADMIN, TOUR_TRANSPORT, UPDATE_USER, USER_ORDERS, diff --git a/src/shared/config/i18n/locales/ru/translation.json b/src/shared/config/i18n/locales/ru/translation.json index 99a25b2..bd6bd2f 100644 --- a/src/shared/config/i18n/locales/ru/translation.json +++ b/src/shared/config/i18n/locales/ru/translation.json @@ -466,5 +466,30 @@ "Reytingi baland turlar": "Высокорейтинговые туры", "Status muvaffaqiyatli yangilandi": "Статус успешно обновлён", "Statusni yangilashda xatolik yuz berdi": "Ошибка обновления статуса", - "Refunded": "Подтверждено" + "Refunded": "Подтверждено", + "Partner Agencies": "Партнерские агентства", + "Bookings": "Заказы", + "Destinations": "Количество видов", + "Total Revenue": "Общий доход", + "From completed bookings": "Из завершенных бронирований", + "To'langan summa": "Выплаченная сумма", + "Kutilayotgan summa": "Ожидаемая сумма", + "Average Rating": "Средняя оценка", + "Tour Overview": "Добавленные туры", + "Reviews": "Комментарии", + "Tour Information": "Данные о типе", + "Agentlik nomi": "Название агентства", + "Manzili": "Адрес", + "Id raqami va ulushi": "Номер ID и доля", + "Ulushi": "Доля", + "Tour Inclusions": "Доходы", + "Platformaga tegishli": "Принадлежащий платформе", + "Platformaga daromadi": "Доход от платформы", + "Agentlik daromadi": "Агентский доход", + "Recent Bookings": "Последние бронирования", + "Travel Date": "Дата поездки", + "Booking Date": "Дата бронирования", + "Yangi foydalanuvchi ma'lumotlari": "Данные нового пользователя", + "Agentlik uchun tizimga kirish ma'lumotlari": "Входные данные для агентства", + "Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.": "Вы уверены, что хотите удалить этого пользователя? Это действие необратимо." } diff --git a/src/shared/config/i18n/locales/uz/translation.json b/src/shared/config/i18n/locales/uz/translation.json index 16293ac..f45156a 100644 --- a/src/shared/config/i18n/locales/uz/translation.json +++ b/src/shared/config/i18n/locales/uz/translation.json @@ -467,5 +467,30 @@ "Reytingi baland turlar": "Reytingi baland turlar", "Status muvaffaqiyatli yangilandi": "Status muvaffaqiyatli yangilandi", "Statusni yangilashda xatolik yuz berdi": "Statusni yangilashda xatolik yuz berdi", - "Refunded": "Tasdiqlangan" + "Refunded": "Tasdiqlangan", + "Partner Agencies": "Hamkor agentliklar", + "Bookings": "Buyurtmalar", + "Destinations": "Turlar soni", + "Total Revenue": "Jami daromad", + "From completed bookings": "Yakunlangan bandlovlardan", + "To'langan summa": "To'langan summa", + "Kutilayotgan summa": "Kutilayotgan summa", + "Average Rating": "O‘rtacha baho", + "Tour Overview": "Qo'shilgan turlar", + "Reviews": "Sharhlar", + "Tour Information": "Tur ma’lumotlari", + "Agentlik nomi": "Agentlik nomi", + "Manzili": "Manzili", + "Id raqami va ulushi": "Id raqami va ulushi", + "Ulushi": "Ulushi", + "Tour Inclusions": "Kirimlar", + "Platformaga tegishli": "Platformaga tegishli", + "Platformaga daromadi": "Platformaga daromadi", + "Agentlik daromadi": "Agentlik daromadi", + "Recent Bookings": "Oxirgi bandlovlar", + "Travel Date": "Sayohat sanasi", + "Booking Date": "Bandlov sanasi", + "Yangi foydalanuvchi ma'lumotlari": "Yangi foydalanuvchi ma'lumotlari", + "Agentlik uchun tizimga kirish ma'lumotlari": "Agentlik uchun tizimga kirish ma'lumotlari", + "Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.": "Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi." } diff --git a/src/shared/lib/formatPhone.ts b/src/shared/lib/formatPhone.ts index 47486b6..3664c00 100644 --- a/src/shared/lib/formatPhone.ts +++ b/src/shared/lib/formatPhone.ts @@ -1,36 +1,30 @@ /** - * Format the number (+998 00 111-22-33) - * @param value Number to be formatted (XXXYYZZZAABB) - * @returns string +998 00 111-22-33 + * Format phone number: +998 00 111-22-33 yoki +888 00 111-22-33 */ const formatPhone = (value: string) => { - // Keep only numbers - const digits = value.replace(/\D/g, ''); + // faqat raqamlarni olish + const digits = value.replace(/\D/g, ""); - // Return empty string if data is not available - if (digits.length === 0) { - return ''; - } + // agar hech narsa yo'q bo'lsa — input bo'sh bo'lib tursin + if (digits.length === 0) return ""; - const prefix = digits.startsWith('998') ? '+998 ' : '+998 '; + // prefiksni aniqlash (faqat agar 998 yoki 888 bilan boshlangan bo'lsa) + let prefix = ""; + if (digits.startsWith("998")) prefix = "+998 "; + else if (digits.startsWith("888")) prefix = "+888 "; + + // agar 998 ham 888 ham emas bo‘lsa — foydalanuvchi hali prefiks kiritmagan, hech narsa qaytarmaymiz + if (!prefix) return "+" + digits; + + // prefiksni olib tashlab, asosiy raqam qismini olish + const core = digits.replace(/^998|^888/, ""); let formattedNumber = prefix; - if (digits.length > 3) { - formattedNumber += digits.slice(3, 5); - } - - if (digits.length > 5) { - formattedNumber += ' ' + digits.slice(5, 8); - } - - if (digits.length > 8) { - formattedNumber += '-' + digits.slice(8, 10); - } - - if (digits.length > 10) { - formattedNumber += '-' + digits.slice(10, 12); - } + if (core.length > 0) formattedNumber += core.slice(0, 2); + if (core.length > 2) formattedNumber += " " + core.slice(2, 5); + if (core.length > 5) formattedNumber += "-" + core.slice(5, 7); + if (core.length > 7) formattedNumber += "-" + core.slice(7, 9); return formattedNumber.trim(); }; diff --git a/src/widgets/sidebar/ui/Sidebar.tsx b/src/widgets/sidebar/ui/Sidebar.tsx index 13467be..541856e 100644 --- a/src/widgets/sidebar/ui/Sidebar.tsx +++ b/src/widgets/sidebar/ui/Sidebar.tsx @@ -1,5 +1,6 @@ "use client"; +import { removeAuthToken, removeRefAuthToken } from "@/shared/lib/authCookies"; import { cn } from "@/shared/lib/utils"; import { Button } from "@/shared/ui/button"; import { @@ -10,12 +11,14 @@ import { SheetTrigger, } from "@/shared/ui/sheet"; import LangToggle from "@/widgets/lang-toggle/ui/lang-toggle"; +import { useQueryClient } from "@tanstack/react-query"; import { Briefcase, Building2, ChevronDown, ChevronRight, HelpCircle, + LogOut, Menu, MessageSquare, Newspaper, @@ -27,6 +30,7 @@ import { import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; +import { toast } from "sonner"; type Role = | "superuser" @@ -46,13 +50,13 @@ const MENU_ITEMS = [ label: "Foydalanuvchilar", icon: Users, path: "/user", - roles: ["moderator", "admin", "superuser", "moderator"], + roles: ["moderator", "admin", "superuser", "operator"], }, { label: "Tur firmalar", icon: Building2, path: "/agencies", - roles: ["moderator", "admin", "superuser", "moderator"], + roles: ["moderator", "admin", "superuser", "operator"], }, { label: "Xodimlar", @@ -64,20 +68,13 @@ const MENU_ITEMS = [ label: "Bronlar", icon: Wallet, path: "/finance", - roles: ["moderator", "admin", "superuser", "buxgalter"], + roles: ["moderator", "admin", "superuser", "buxgalter", "tour_admin"], }, { label: "Turlar", icon: Plane, path: "/tours", - roles: [ - "moderator", - "admin", - "superuser", - "tour_admin", - "operator", - "buxgalter", - ], + roles: ["moderator", "admin", "superuser", "tour_admin"], children: [ { label: "Turlar", path: "/tours" }, { @@ -87,19 +84,6 @@ const MENU_ITEMS = [ }, ], }, - // { - // label: "Bronlar", - // icon: CalendarCheck2, - // path: "/bookings", - // roles: [ - // "moderator", - // "admin", - // "superuser", - // "tour_admin", - // "operator", - // "buxgalter", - // ], - // }, { label: "Yangiliklar", icon: Newspaper, @@ -129,7 +113,7 @@ const MENU_ITEMS = [ { label: "Agentlik arizalari", path: "/support/tours", - roles: ["moderator", "admin", "superuser", "operator"], + roles: ["moderator", "admin", "superuser"], }, { label: "Yordam arizalari", @@ -164,12 +148,21 @@ export function Sidebar({ role }: SidebarProps) { const navigate = useNavigate(); const { t } = useTranslation(); const location = useLocation(); + const queryClient = useQueryClient(); const [isSheetOpen, setIsSheetOpen] = useState(false); - const visibleMenu = useMemo( - () => MENU_ITEMS.filter((item) => item.roles.includes(role)), - [role], - ); + const visibleMenu = useMemo(() => { + return MENU_ITEMS.filter((item) => item.roles.includes(role)).map( + (item) => ({ + ...item, + children: item.children + ? item.children.filter( + (child) => !child.roles || child.roles.includes(role), + ) + : [], + }), + ); + }, [role]); const [active, setActive] = useState(location.pathname); const [openMenus, setOpenMenus] = useState([]); @@ -190,71 +183,93 @@ export function Sidebar({ role }: SidebarProps) { ); }; + const handleLogout = () => { + removeAuthToken(); + removeRefAuthToken(); + queryClient.clear(); + toast.success(t("Tizimdan chiqdingiz")); + navigate("/auth/login"); + }; + const MenuList = ( -
    - {visibleMenu.map(({ label, icon: Icon, path, children }) => { - const isActive = active.startsWith(path); - const isOpen = openMenus.includes(label); +
    +
      + {visibleMenu.map(({ label, icon: Icon, path, children }) => { + const isActive = active.startsWith(path); + const isOpen = openMenus.includes(label); + const hasChildren = children?.length > 0; - return ( -
    • -
      - children ? toggleSubMenu(label) : handleClick(path) - } - > -
      - - {t(label)} + return ( +
    • +
      + hasChildren ? toggleSubMenu(label) : handleClick(path) + } + > +
      + + {t(label)} +
      + {hasChildren && ( + + {isOpen ? ( + + ) : ( + + )} + + )}
      - {children && ( - - {isOpen ? ( - - ) : ( - - )} - + + {hasChildren && isOpen && ( +
        + {children.map((sub) => ( +
      • + +
      • + ))} +
      )} -
    + + ); + })} + +
- {children && isOpen && ( -
    - {children.map((sub) => ( -
  • - -
  • - ))} -
- )} - - ); - })} - - - +
+ +
+
); return (
+ {/* Mobil versiya */}
@@ -277,15 +292,17 @@ export function Sidebar({ role }: SidebarProps) { /> - +
+ + {/* Desktop versiya */}
);