barcha apilar ulandi
This commit is contained in:
33
src/App.tsx
33
src/App.tsx
@@ -30,11 +30,11 @@ import CreateUser from "@/pages/users/ui/Create";
|
|||||||
import EditUser from "@/pages/users/ui/Edit";
|
import EditUser from "@/pages/users/ui/Edit";
|
||||||
import UserList from "@/pages/users/ui/User";
|
import UserList from "@/pages/users/ui/User";
|
||||||
import UserDetail from "@/pages/users/ui/UserDetail";
|
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 "@/shared/config/i18n";
|
||||||
import useUserStore from "@/shared/hooks/user";
|
|
||||||
import { getAuthToken } from "@/shared/lib/authCookies";
|
import { getAuthToken } from "@/shared/lib/authCookies";
|
||||||
import { Sidebar } from "@/widgets/sidebar/ui/Sidebar";
|
import { Sidebar } from "@/widgets/sidebar/ui/Sidebar";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Navigate,
|
Navigate,
|
||||||
@@ -45,24 +45,35 @@ import {
|
|||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { user } = useUserStore();
|
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { data: user } = useQuery({
|
||||||
|
queryKey: ["get_me"],
|
||||||
|
queryFn: () => getMe(),
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
enabled: !!token,
|
||||||
|
});
|
||||||
|
|
||||||
const hideSidebarPaths = ["/login"];
|
const hideSidebarPaths = ["/login"];
|
||||||
const shouldShowSidebar = !hideSidebarPaths.includes(location.pathname);
|
const shouldShowSidebar = !hideSidebarPaths.includes(location.pathname);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token && user) {
|
if (token && user && user.role === "moderator") {
|
||||||
navigate("/user");
|
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) {
|
} else if (!token && !user) {
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
}
|
}
|
||||||
}, [token, user]);
|
}, [token, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainProvider>
|
<>
|
||||||
<div className="flex max-lg:flex-col bg-gray-900">
|
<div className="flex max-lg:flex-col bg-gray-900">
|
||||||
{shouldShowSidebar && <Sidebar role={user ? user.role : "admin"} />}
|
{shouldShowSidebar && <Sidebar role={user ? user.role : "admin"} />}
|
||||||
|
|
||||||
@@ -78,11 +89,17 @@ const App = () => {
|
|||||||
<Route path="/agency/:id/edit" element={<EditAgecy />} />
|
<Route path="/agency/:id/edit" element={<EditAgecy />} />
|
||||||
<Route path="/tours/:id" element={<TourDetail />} />
|
<Route path="/tours/:id" element={<TourDetail />} />
|
||||||
<Route path="/employees" element={<Employees />} />
|
<Route path="/employees" element={<Employees />} />
|
||||||
<Route path="/finance" element={<FinancePage />} />
|
<Route
|
||||||
|
path="/finance"
|
||||||
|
element={<FinancePage user={user ? user.role : "moderator"} />}
|
||||||
|
/>
|
||||||
<Route path="/purchases/:id/" element={<PurchaseDetailPage />} />
|
<Route path="/purchases/:id/" element={<PurchaseDetailPage />} />
|
||||||
<Route path="/travel/booking/:id/" element={<FinanceDetailTour />} />
|
<Route path="/travel/booking/:id/" element={<FinanceDetailTour />} />
|
||||||
<Route path="/bookings/:id/" element={<FinanceDetailUsers />} />
|
<Route path="/bookings/:id/" element={<FinanceDetailUsers />} />
|
||||||
<Route path="/tours" element={<Tours />} />
|
<Route
|
||||||
|
path="/tours"
|
||||||
|
element={<Tours user={user ? user.role : "moderator"} />}
|
||||||
|
/>
|
||||||
<Route path="/tours/setting" element={<ToursSetting />} />
|
<Route path="/tours/setting" element={<ToursSetting />} />
|
||||||
<Route path="/tours/:id/edit" element={<CreateEditTour />} />
|
<Route path="/tours/:id/edit" element={<CreateEditTour />} />
|
||||||
<Route path="/tours/create" element={<CreateEditTour />} />
|
<Route path="/tours/create" element={<CreateEditTour />} />
|
||||||
@@ -102,7 +119,7 @@ const App = () => {
|
|||||||
<Route path="/site-banner/" element={<SiteBannerAdmin />} />
|
<Route path="/site-banner/" element={<SiteBannerAdmin />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</MainProvider>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import MainProvider from "@/providers/main.tsx";
|
||||||
import { Toaster } from "@/shared/ui/sonner.tsx";
|
import { Toaster } from "@/shared/ui/sonner.tsx";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
@@ -7,9 +8,11 @@ import "./index.css";
|
|||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<MainProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</MainProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type {
|
import type {
|
||||||
|
agencyUserData,
|
||||||
GetAllAgencyData,
|
GetAllAgencyData,
|
||||||
GetDetailAgencyData,
|
GetDetailAgencyData,
|
||||||
} from "@/pages/agencies/lib/type";
|
} from "@/pages/agencies/lib/type";
|
||||||
import httpClient from "@/shared/config/api/httpClient";
|
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";
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
const getAllAgency = async ({
|
const getAllAgency = async ({
|
||||||
@@ -55,4 +56,42 @@ const updateAgencyStatus = async ({
|
|||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { deleteAgency, getAllAgency, getDetailAgency, updateAgencyStatus };
|
const agencyUser = async (
|
||||||
|
id: number,
|
||||||
|
): Promise<AxiosResponse<agencyUserData>> => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
"use client";
|
"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 formatPhone from "@/shared/lib/formatPhone";
|
||||||
import formatPrice from "@/shared/lib/formatPrice";
|
import formatPrice from "@/shared/lib/formatPrice";
|
||||||
import { Badge } from "@/shared/ui/badge";
|
import { Badge } from "@/shared/ui/badge";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -25,6 +40,7 @@ import {
|
|||||||
Percent,
|
Percent,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -34,12 +50,30 @@ export default function AgencyDetailPage() {
|
|||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [openUser, setOpenUser] = useState<boolean>(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({
|
const { data, isLoading, refetch, isError } = useQuery({
|
||||||
queryKey: ["detail_agency"],
|
queryKey: ["detail_agency"],
|
||||||
queryFn: () => getDetailAgency({ id: Number(params.id) }),
|
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({
|
const statusMutation = useMutation({
|
||||||
mutationFn: (newStatus: "pending" | "approved" | "cancelled") =>
|
mutationFn: (newStatus: "pending" | "approved" | "cancelled") =>
|
||||||
updateAgencyStatus({
|
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 = (
|
const handleStatusChange = (
|
||||||
newStatus: "pending" | "approved" | "cancelled",
|
newStatus: "pending" | "approved" | "cancelled",
|
||||||
) => {
|
) => {
|
||||||
statusMutation.mutate(newStatus);
|
statusMutation.mutate(newStatus);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditUser = (user: number) => {
|
||||||
|
updateUserMutation.mutate(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteUser = (userId: number) => {
|
||||||
|
deleteUserMutation.mutate(userId);
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
||||||
@@ -337,6 +403,22 @@ export default function AgencyDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{agency?.data.data && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<AgencyUsersSection
|
||||||
|
users={
|
||||||
|
Array.isArray(agency?.data.data)
|
||||||
|
? agency?.data.data
|
||||||
|
: [agency?.data.data]
|
||||||
|
}
|
||||||
|
onEdit={handleEditUser}
|
||||||
|
onDelete={handleDeleteUser}
|
||||||
|
isLoading={isLoadingUsers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card className="border border-gray-700 shadow-lg bg-gray-800">
|
<Card className="border border-gray-700 shadow-lg bg-gray-800">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl text-white">
|
<CardTitle className="text-2xl text-white">
|
||||||
@@ -393,6 +475,47 @@ export default function AgencyDetailPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
<Dialog open={openUser} onOpenChange={() => {}}>
|
||||||
|
<DialogContent className="bg-gray-900 text-white sm:max-w-sm rounded-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-lg font-semibold">
|
||||||
|
{t("Yangi foydalanuvchi ma'lumotlari")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-400 text-sm">
|
||||||
|
{t("Agentlik uchun tizimga kirish ma'lumotlari")}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 text-sm mb-1">
|
||||||
|
{t("Telefon raqam (login)")}
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-medium">
|
||||||
|
{formatPhone(user.data.phone)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 text-sm mb-1">{t("Parol")}</p>
|
||||||
|
<p className="text-lg font-semibold text-green-400">
|
||||||
|
{user.data.password}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter className="mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={() => setOpenUser(false)}
|
||||||
|
className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white px-5 py-2 rounded-md hover:opacity-90"
|
||||||
|
>
|
||||||
|
{t("Yopish")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
195
src/pages/agencies/ui/AgencyUsersSection.tsx
Normal file
195
src/pages/agencies/ui/AgencyUsersSection.tsx
Normal file
@@ -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<string, string> = {
|
||||||
|
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 (
|
||||||
|
<Badge className={roleColors[role] || roleColors.user}>
|
||||||
|
{role.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card className="border border-gray-700 shadow-lg bg-gray-800">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl text-white flex items-center gap-2">
|
||||||
|
<User className="w-6 h-6" />
|
||||||
|
{t("Agentlik foydalanuvchilari")}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-center text-gray-400 py-8">
|
||||||
|
{t("Ma'lumotlar yuklanmoqda...")}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!users || users.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="border border-gray-700 shadow-lg bg-gray-800">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl text-white flex items-center gap-2">
|
||||||
|
<User className="w-6 h-6" />
|
||||||
|
{t("Agentlik foydalanuvchilari")}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-center text-gray-400 py-8">
|
||||||
|
{t("Hozircha foydalanuvchilar yo'q")}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="border border-gray-700 shadow-lg bg-gray-800">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl text-white flex items-center gap-2">
|
||||||
|
<User className="w-6 h-6" />
|
||||||
|
{t("Agentlik foydalanuvchilari")}
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-gray-400">
|
||||||
|
{t("Agentlik bilan bog'langan foydalanuvchilar ro'yxati")}
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{users.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.id}
|
||||||
|
className="p-5 border border-gray-700 rounded-xl bg-gray-800 hover:bg-gray-750 transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4 flex-1">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
|
||||||
|
<User className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<h3 className="text-lg font-semibold text-white">
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</h3>
|
||||||
|
{getRoleBadge(user.role)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-4 text-sm">
|
||||||
|
<div className="flex items-center gap-2 text-gray-400">
|
||||||
|
<Phone className="w-4 h-4" />
|
||||||
|
<span>{formatPhone(user.phone)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-gray-400">
|
||||||
|
<Shield className="w-4 h-4" />
|
||||||
|
<span>ID: {user.id}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Edit Button */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onEdit(user.id)}
|
||||||
|
className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-blue-400 hover:text-blue-300"
|
||||||
|
>
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Delete Alert Dialog */}
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="border-gray-600 bg-gray-700 hover:bg-red-900/20 hover:border-red-500 text-red-400 hover:text-red-300"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="bg-gray-800 border-gray-700 text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t("Foydalanuvchini o'chirish")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-400">
|
||||||
|
{t("Haqiqatan ham")}{" "}
|
||||||
|
<span className="font-semibold text-white">
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</span>{" "}
|
||||||
|
{t(
|
||||||
|
"ni o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.",
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-white">
|
||||||
|
{t("Bekor qilish")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => onDelete(user.id)}
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
>
|
||||||
|
{t("O'chirish")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -40,10 +40,7 @@ const Login = () => {
|
|||||||
|
|
||||||
const { mutate, isPending } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: ({ password, phone }: { password: string; phone: string }) =>
|
mutationFn: ({ password, phone }: { password: string; phone: string }) =>
|
||||||
authLogin({
|
authLogin({ password, phone }),
|
||||||
password,
|
|
||||||
phone,
|
|
||||||
}),
|
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
setAuthToken(res.data.access);
|
setAuthToken(res.data.access);
|
||||||
setAuthRefToken(res.data.refresh);
|
setAuthRefToken(res.data.refresh);
|
||||||
@@ -83,9 +80,17 @@ const Login = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
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({
|
mutate({
|
||||||
password: values.password,
|
password: values.password,
|
||||||
phone: onlyNumber(values.phone),
|
phone: cleanPhone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +115,20 @@ const Login = () => {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="+998 __ ___-__-__"
|
placeholder="+998 yoki +888 bilan boshlang"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value || "+998"}
|
value={field.value}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
field.onChange(formatPhone(e.target.value))
|
const inputValue = e.target.value;
|
||||||
|
|
||||||
|
if (inputValue.trim() === "") {
|
||||||
|
field.onChange("");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Formatlash
|
||||||
|
field.onChange(formatPhone(inputValue));
|
||||||
|
}}
|
||||||
maxLength={19}
|
maxLength={19}
|
||||||
className="h-[56px] text-lg rounded-xl bg-gray-700 border-gray-600 text-white placeholder-gray-400"
|
className="h-[56px] text-lg rounded-xl bg-gray-700 border-gray-600 text-white placeholder-gray-400"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AgencyOrderData,
|
||||||
|
UserAgencyDetailData,
|
||||||
UserOrderData,
|
UserOrderData,
|
||||||
UserOrderDetailData,
|
UserOrderDetailData,
|
||||||
} from "@/pages/finance/lib/type";
|
} from "@/pages/finance/lib/type";
|
||||||
import httpClient from "@/shared/config/api/httpClient";
|
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";
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
const getAllOrder = async (params: {
|
const getAllOrder = async (params: {
|
||||||
@@ -21,6 +23,14 @@ const getAllOrder = async (params: {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllOrderAgecy = async (params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
}): Promise<AxiosResponse<AgencyOrderData>> => {
|
||||||
|
const res = await httpClient.get(AGENCY_ORDERS, { params });
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
const getDetailOrder = async (
|
const getDetailOrder = async (
|
||||||
id: number,
|
id: number,
|
||||||
): Promise<AxiosResponse<UserOrderDetailData>> => {
|
): Promise<AxiosResponse<UserOrderDetailData>> => {
|
||||||
@@ -28,6 +38,13 @@ const getDetailOrder = async (
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDetailAgencyOrder = async (
|
||||||
|
id: number,
|
||||||
|
): Promise<AxiosResponse<UserAgencyDetailData>> => {
|
||||||
|
const res = await httpClient.get(`${AGENCY_ORDERS}${id}/`);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
const updateDetailOrder = async ({
|
const updateDetailOrder = async ({
|
||||||
id,
|
id,
|
||||||
body,
|
body,
|
||||||
@@ -46,4 +63,10 @@ const updateDetailOrder = async ({
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getAllOrder, getDetailOrder, updateDetailOrder };
|
export {
|
||||||
|
getAllOrder,
|
||||||
|
getAllOrderAgecy,
|
||||||
|
getDetailAgencyOrder,
|
||||||
|
getDetailOrder,
|
||||||
|
updateDetailOrder,
|
||||||
|
};
|
||||||
|
|||||||
@@ -110,3 +110,73 @@ export interface UserOrderDetailData {
|
|||||||
total_price: number;
|
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;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"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 type { OrderStatus } from "@/pages/finance/lib/type";
|
||||||
import formatPhone from "@/shared/lib/formatPhone";
|
import formatPhone from "@/shared/lib/formatPhone";
|
||||||
import formatPrice from "@/shared/lib/formatPrice";
|
import formatPrice from "@/shared/lib/formatPrice";
|
||||||
@@ -24,100 +24,28 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
type Purchase = {
|
type Role =
|
||||||
id: number;
|
| "superuser"
|
||||||
userName: string;
|
| "admin"
|
||||||
userPhone: string;
|
| "moderator"
|
||||||
tourName: string;
|
| "tour_admin"
|
||||||
tourId: number;
|
| "buxgalter"
|
||||||
agencyName: string;
|
| "operator"
|
||||||
agencyId: number;
|
| "user";
|
||||||
destination: string;
|
|
||||||
travelDate: string;
|
|
||||||
amount: number;
|
|
||||||
paymentStatus: "paid" | "pending" | "cancelled" | "refunded";
|
|
||||||
purchaseDate: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockPurchases: Purchase[] = [
|
export default function FinancePage({ user }: { user: Role }) {
|
||||||
{
|
|
||||||
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() {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [tab, setTab] = useState<"bookings" | "agencies">("bookings");
|
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<
|
const [filterStatus, setFilterStatus] = useState<
|
||||||
| ""
|
| ""
|
||||||
| "pending_payment"
|
| "pending_payment"
|
||||||
@@ -127,9 +55,25 @@ export default function FinancePage() {
|
|||||||
| "cancelled"
|
| "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(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, [filterStatus]);
|
setCurrentPageAgency(1);
|
||||||
|
}, [filterStatus, tab]);
|
||||||
|
|
||||||
const { data, isLoading, isError, refetch } = useQuery({
|
const { data, isLoading, isError, refetch } = useQuery({
|
||||||
queryKey: ["list_order_user", currentPage, filterStatus],
|
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 = [
|
const stats = [
|
||||||
{
|
{
|
||||||
title: t("Jami daromad"),
|
title: t("Jami daromad"),
|
||||||
@@ -253,52 +211,6 @@ export default function FinancePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
|
||||||
<Loader2 className="w-10 h-10 animate-spin text-cyan-400" />
|
|
||||||
<p className="text-slate-400">{t("Ma'lumotlar yuklanmoqda...")}</p>
|
|
||||||
</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 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 (
|
return (
|
||||||
<div className="min-h-screen w-full bg-gray-900 text-gray-100">
|
<div className="min-h-screen w-full bg-gray-900 text-gray-100">
|
||||||
<div className="w-[90%] mx-auto py-6">
|
<div className="w-[90%] mx-auto py-6">
|
||||||
@@ -330,6 +242,10 @@ export default function FinancePage() {
|
|||||||
<CreditCard size={18} />
|
<CreditCard size={18} />
|
||||||
{t("Bandlovlar va to‘lovlar")}
|
{t("Bandlovlar va to‘lovlar")}
|
||||||
</button>
|
</button>
|
||||||
|
{user === "tour_admin" ||
|
||||||
|
user === "buxgalter" ||
|
||||||
|
user === "admin" ||
|
||||||
|
(user === "superuser" && (
|
||||||
<button
|
<button
|
||||||
className={`px-6 py-3 rounded-lg flex items-center gap-2 transition-all ${
|
className={`px-6 py-3 rounded-lg flex items-center gap-2 transition-all ${
|
||||||
tab === "agencies"
|
tab === "agencies"
|
||||||
@@ -341,11 +257,11 @@ export default function FinancePage() {
|
|||||||
<Users size={18} />
|
<Users size={18} />
|
||||||
{t("Agentlik hisobotlari")}
|
{t("Agentlik hisobotlari")}
|
||||||
</button>
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tab === "bookings" && (
|
{tab === "bookings" && (
|
||||||
<>
|
<>
|
||||||
{/* Filter */}
|
|
||||||
<div className="flex gap-2 mb-6 flex-wrap">
|
<div className="flex gap-2 mb-6 flex-wrap">
|
||||||
{[
|
{[
|
||||||
"",
|
"",
|
||||||
@@ -389,7 +305,6 @@ export default function FinancePage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
|
||||||
{stats.map((item, index) => (
|
{stats.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -410,8 +325,31 @@ export default function FinancePage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Booking Cards */}
|
|
||||||
<h2 className="text-xl font-bold mb-4">{t("Oxirgi bandlovlar")}</h2>
|
<h2 className="text-xl font-bold mb-4">{t("Oxirgi bandlovlar")}</h2>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
||||||
|
<Loader2 className="w-10 h-10 animate-spin text-cyan-400" />
|
||||||
|
<p className="text-slate-400">
|
||||||
|
{t("Ma'lumotlar yuklanmoqda...")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<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>
|
||||||
|
) : (
|
||||||
|
data &&
|
||||||
|
!isError &&
|
||||||
|
!isLoading && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{data?.data.data.results.orders.map((p, index) => (
|
{data?.data.data.results.orders.map((p, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -446,7 +384,9 @@ export default function FinancePage() {
|
|||||||
</p>
|
</p>
|
||||||
</div> */}
|
</div> */}
|
||||||
<div className="text-start">
|
<div className="text-start">
|
||||||
<p className="text-gray-500 text-sm">{t("Miqdor")}</p>
|
<p className="text-gray-500 text-sm">
|
||||||
|
{t("Miqdor")}
|
||||||
|
</p>
|
||||||
<p className="text-green-400 font-bold">
|
<p className="text-green-400 font-bold">
|
||||||
{formatPrice(p.total_price, true)}
|
{formatPrice(p.total_price, true)}
|
||||||
</p>
|
</p>
|
||||||
@@ -464,6 +404,8 @@ export default function FinancePage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
<div className="flex justify-end gap-2 mt-5">
|
<div className="flex justify-end gap-2 mt-5">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
@@ -501,12 +443,43 @@ export default function FinancePage() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{user === "tour_admin" ||
|
||||||
|
user === "buxgalter" ||
|
||||||
|
user === "admin" ||
|
||||||
|
(user === "superuser" && (
|
||||||
|
<>
|
||||||
{tab === "agencies" && (
|
{tab === "agencies" && (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-xl font-bold mb-6">Partner Agencies</h2>
|
{agenctLoad ? (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
||||||
|
<Loader2 className="w-10 h-10 animate-spin text-cyan-400" />
|
||||||
|
<p className="text-slate-400">
|
||||||
|
{t("Ma'lumotlar yuklanmoqda...")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : agencyError ? (
|
||||||
|
<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={() => agencyRef()}
|
||||||
|
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>
|
||||||
|
) : (
|
||||||
|
agencyData &&
|
||||||
|
!agencyError &&
|
||||||
|
!agenctLoad && (
|
||||||
|
<>
|
||||||
|
<h2 className="text-xl font-bold mb-6">
|
||||||
|
{t("Partner Agencies")}
|
||||||
|
</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{agencies.map((a) => (
|
{agencyData?.data.data.results.map((a) => (
|
||||||
<div
|
<div
|
||||||
key={a.id}
|
key={a.id}
|
||||||
className="bg-gray-800 p-6 rounded-xl shadow hover:shadow-md transition-all"
|
className="bg-gray-800 p-6 rounded-xl shadow hover:shadow-md transition-all"
|
||||||
@@ -518,30 +491,34 @@ export default function FinancePage() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
<div className="bg-green-900 p-3 rounded-lg">
|
<div className="bg-green-900 p-3 rounded-lg">
|
||||||
<p className="text-gray-400 text-sm">Paid</p>
|
<p className="text-gray-400 text-sm">
|
||||||
|
{t("Paid")}
|
||||||
|
</p>
|
||||||
<p className="text-green-400 font-bold text-lg">
|
<p className="text-green-400 font-bold text-lg">
|
||||||
${(a.totalPaid / 1000000).toFixed(1)}M
|
{formatPrice(a.paid, true)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-yellow-900 p-3 rounded-lg">
|
<div className="bg-yellow-900 p-3 rounded-lg">
|
||||||
<p className="text-gray-400 text-sm">Pending</p>
|
<p className="text-gray-400 text-sm">
|
||||||
|
{t("Pending")}
|
||||||
|
</p>
|
||||||
<p className="text-yellow-400 font-bold text-lg">
|
<p className="text-yellow-400 font-bold text-lg">
|
||||||
${(a.pending / 1000000).toFixed(1)}M
|
{formatPrice(a.pending, true)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4 text-gray-400">
|
<div className="mb-4 text-gray-400">
|
||||||
<p className="text-sm mb-1">
|
<p className="text-sm mb-1">
|
||||||
Bookings:{" "}
|
{t("Bookings")}:{" "}
|
||||||
<span className="font-medium text-gray-100">
|
<span className="font-medium text-gray-100">
|
||||||
{a.purchaseCount}
|
{a.ticket_sold_count}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
Destinations:{" "}
|
{t("Destinations")}:{" "}
|
||||||
<span className="font-medium text-gray-100">
|
<span className="font-medium text-gray-100">
|
||||||
{a.destinations.length}
|
{a.ticket_count}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -551,14 +528,63 @@ export default function FinancePage() {
|
|||||||
to={`/travel/booking/${a.id}`}
|
to={`/travel/booking/${a.id}`}
|
||||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-1 transition-colors flex-1 justify-center"
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-1 transition-colors flex-1 justify-center"
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" /> View Details
|
<Eye className="w-4 h-4" /> {t("Ko'rish")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
<div className="flex justify-end gap-2 mt-5">
|
||||||
|
<button
|
||||||
|
disabled={currentPageAgency === 1}
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPageAgency((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(agencyData?.data.data.total_pages)].map(
|
||||||
|
(_, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => setCurrentPageAgency(i + 1)}
|
||||||
|
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
||||||
|
currentPageAgency === 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={
|
||||||
|
currentPageAgency === agencyData?.data.data.total_pages
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPageAgency((p) =>
|
||||||
|
Math.min(
|
||||||
|
p + 1,
|
||||||
|
agencyData ? agencyData?.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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,166 +1,41 @@
|
|||||||
"use client";
|
"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 {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Calendar,
|
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Eye,
|
Eye,
|
||||||
Hotel,
|
Mail,
|
||||||
MapPin,
|
MapPin,
|
||||||
|
Phone,
|
||||||
Plane,
|
Plane,
|
||||||
Star,
|
Star,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useParams } from "react-router-dom";
|
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() {
|
export default function FinanceDetailTour() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [activeTab, setActiveTab] = useState<
|
const [activeTab, setActiveTab] = useState<
|
||||||
"overview" | "bookings" | "reviews"
|
"overview" | "bookings" | "reviews"
|
||||||
>("overview");
|
>("overview");
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
console.log(params);
|
const { data } = useQuery({
|
||||||
|
queryKey: ["detail_order", params.id],
|
||||||
// const {} = useQuery({
|
queryFn: () => getDetailAgencyOrder(Number(params.id)),
|
||||||
// queryKey: ["detail_order"],
|
select(data) {
|
||||||
// queryFn: () => getDetailOrder()
|
return data.data.data;
|
||||||
// })
|
},
|
||||||
|
});
|
||||||
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 (
|
|
||||||
<span
|
|
||||||
className={`${base} bg-green-900 text-green-400 border border-green-700`}
|
|
||||||
>
|
|
||||||
<div className="w-2 h-2 rounded-full bg-green-400"></div>
|
|
||||||
Paid
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case "pending":
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`${base} bg-yellow-900 text-yellow-400 border border-yellow-700`}
|
|
||||||
>
|
|
||||||
<div className="w-2 h-2 rounded-full bg-yellow-400"></div>
|
|
||||||
Pending
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case "cancelled":
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`${base} bg-red-900 text-red-400 border border-red-700`}
|
|
||||||
>
|
|
||||||
<div className="w-2 h-2 rounded-full bg-red-400"></div>
|
|
||||||
Cancelled
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case "refunded":
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`${base} bg-blue-900 text-blue-400 border border-blue-700`}
|
|
||||||
>
|
|
||||||
<div className="w-2 h-2 rounded-full bg-blue-400"></div>
|
|
||||||
Refunded
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStars = (rating: number) => {
|
const renderStars = (rating: number) => {
|
||||||
return (
|
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 (
|
return (
|
||||||
<div className="min-h-screen w-full bg-gray-900 text-gray-100">
|
<div className="min-h-screen w-full bg-gray-900 text-gray-100">
|
||||||
<div className="w-[90%] mx-auto py-6">
|
<div className="w-[90%] mx-auto py-6">
|
||||||
@@ -200,10 +67,7 @@ export default function FinanceDetailTour() {
|
|||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Tour Financial Details</h1>
|
<h1 className="text-3xl font-bold">{data?.name}</h1>
|
||||||
<p className="text-gray-400 mt-1">
|
|
||||||
Financial performance for {mockTourData.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,48 +76,46 @@ export default function FinanceDetailTour() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
<div className="bg-gray-800 p-6 rounded-xl shadow">
|
<div className="bg-gray-800 p-6 rounded-xl shadow">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-gray-400 font-medium">Total Revenue</p>
|
<p className="text-gray-400 font-medium">
|
||||||
|
{t("To'langan summa")}
|
||||||
|
</p>
|
||||||
<DollarSign className="text-green-400 w-6 h-6" />
|
<DollarSign className="text-green-400 w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-green-400 mt-3">
|
<p className="text-2xl font-bold text-green-400 mt-3">
|
||||||
${(totalRevenue / 1000000).toFixed(1)}M
|
{data && formatPrice(data?.paid, true)}
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
|
||||||
From completed bookings
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-800 p-6 rounded-xl shadow">
|
<div className="bg-gray-800 p-6 rounded-xl shadow">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-gray-400 font-medium">Pending Revenue</p>
|
<p className="text-gray-400 font-medium">
|
||||||
|
{t("Kutilayotgan summa")}
|
||||||
|
</p>
|
||||||
<TrendingUp className="text-yellow-400 w-6 h-6" />
|
<TrendingUp className="text-yellow-400 w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-yellow-400 mt-3">
|
<p className="text-2xl font-bold text-yellow-400 mt-3">
|
||||||
${(pendingRevenue / 1000000).toFixed(1)}M
|
{data && formatPrice(data.pending, true)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mt-1">Awaiting payment</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-800 p-6 rounded-xl shadow">
|
<div className="bg-gray-800 p-6 rounded-xl shadow">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-gray-400 font-medium">Total Bookings</p>
|
<p className="text-gray-400 font-medium">{t("Total Bookings")}</p>
|
||||||
<Users className="text-blue-400 w-6 h-6" />
|
<Users className="text-blue-400 w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-blue-400 mt-3">
|
<p className="text-2xl font-bold text-blue-400 mt-3">
|
||||||
{mockTourPurchases.length}
|
{data?.ticket_sold_count}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mt-1">All bookings</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-800 p-6 rounded-xl shadow">
|
<div className="bg-gray-800 p-6 rounded-xl shadow">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-gray-400 font-medium">Average Rating</p>
|
<p className="text-gray-400 font-medium">{t("Average Rating")}</p>
|
||||||
<Star className="text-yellow-400 w-6 h-6" />
|
<Star className="text-yellow-400 w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-yellow-400 mt-3">
|
<p className="text-2xl font-bold text-yellow-400 mt-3">
|
||||||
{mockTourData.averageRating}/5
|
{data?.rating}/5
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mt-1">Customer satisfaction</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -270,7 +132,7 @@ export default function FinanceDetailTour() {
|
|||||||
onClick={() => setActiveTab("overview")}
|
onClick={() => setActiveTab("overview")}
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
Tour Overview
|
{t("Umumiy ma'lumot")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
|
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
|
||||||
@@ -281,7 +143,7 @@ export default function FinanceDetailTour() {
|
|||||||
onClick={() => setActiveTab("bookings")}
|
onClick={() => setActiveTab("bookings")}
|
||||||
>
|
>
|
||||||
<Users className="w-4 h-4" />
|
<Users className="w-4 h-4" />
|
||||||
Bookings ({mockTourPurchases.length})
|
{t("Bookings")} ({data?.total_booking_count})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
|
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
|
||||||
@@ -292,7 +154,7 @@ export default function FinanceDetailTour() {
|
|||||||
onClick={() => setActiveTab("reviews")}
|
onClick={() => setActiveTab("reviews")}
|
||||||
>
|
>
|
||||||
<Star className="w-4 h-4" />
|
<Star className="w-4 h-4" />
|
||||||
Reviews
|
{t("Reviews")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -301,14 +163,18 @@ export default function FinanceDetailTour() {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
{/* Tour Information */}
|
{/* Tour Information */}
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h3 className="text-lg font-bold mb-4">Tour Information</h3>
|
<h3 className="text-lg font-bold mb-4">
|
||||||
|
{t("Umumiy ma'lumot")}
|
||||||
|
</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
|
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
|
||||||
<Plane className="w-5 h-5 text-blue-400" />
|
<Plane className="w-5 h-5 text-blue-400" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Tour Name</p>
|
<p className="text-sm text-gray-400">
|
||||||
|
{t("Agentlik nomi")}
|
||||||
|
</p>
|
||||||
<p className="text-gray-100 font-medium">
|
<p className="text-gray-100 font-medium">
|
||||||
{mockTourData.name}
|
{data?.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -316,54 +182,95 @@ export default function FinanceDetailTour() {
|
|||||||
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
|
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
|
||||||
<MapPin className="w-5 h-5 text-green-400" />
|
<MapPin className="w-5 h-5 text-green-400" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Destination</p>
|
<p className="text-sm text-gray-400">{t("Manzili")}</p>
|
||||||
|
<p className="text-gray-100">{data?.addres}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
|
||||||
|
<Phone className="w-5 h-5 text-purple-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
{t("Telefon raqami")}
|
||||||
|
</p>
|
||||||
<p className="text-gray-100">
|
<p className="text-gray-100">
|
||||||
{mockTourData.destination}
|
{data && formatPhone(data?.phone)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
|
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
|
||||||
<Calendar className="w-5 h-5 text-purple-400" />
|
<Mail className="w-5 h-5 text-yellow-400" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Duration</p>
|
<p className="text-sm text-gray-400">{t("E-mail")}</p>
|
||||||
<p className="text-gray-100">{mockTourData.duration}</p>
|
<p className="text-gray-100">{data?.email}</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
|
|
||||||
<Hotel className="w-5 h-5 text-yellow-400" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Agency</p>
|
|
||||||
<p className="text-gray-100">{mockTourData.agency}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3 bg-gray-700 rounded-lg">
|
<div className="p-3 bg-gray-700 rounded-lg">
|
||||||
<p className="text-sm text-gray-400 mb-2">Description</p>
|
<p className="text-sm text-gray-400 mb-2">
|
||||||
|
{t("Id raqami va ulushi")}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-10">
|
||||||
<p className="text-gray-100">
|
<p className="text-gray-100">
|
||||||
{mockTourData.description}
|
{t("Id")}: {data?.custom_id}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-100">
|
||||||
|
{t("Ulushi")}: {data?.share_percentage}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<h3 className="text-lg font-bold mb-4">Tour Inclusions</h3>
|
{data && (
|
||||||
<div className="mt-6 p-4 bg-gray-700 rounded-lg">
|
<div className="grid gap-4">
|
||||||
<p className="text-sm text-gray-400 mb-2">Base Price</p>
|
<Card className="bg-gray-800 text-white border-gray-700">
|
||||||
<p className="text-2xl font-bold text-green-400">
|
<CardHeader>
|
||||||
${(mockTourData.price / 1000000).toFixed(1)}M
|
{t("Qo'shilgan turlar")}
|
||||||
|
<p className="text-2xl font-bold text-purple-400">
|
||||||
|
{data.ticket_count}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-400 mt-1">per person</p>
|
</CardHeader>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-gray-800 text-white border-gray-700">
|
||||||
|
<CardHeader>
|
||||||
|
{t("Umumiy daromad")}
|
||||||
|
<p className="text-2xl font-bold text-blue-400">
|
||||||
|
{formatPrice(data.total_income, true)}
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Platforma daromadi */}
|
||||||
|
<Card className="bg-gray-800 text-white border-gray-700">
|
||||||
|
<CardHeader>
|
||||||
|
{t("Platformaga daromadi")}
|
||||||
|
<p className="text-2xl font-bold text-purple-400">
|
||||||
|
{formatPrice(data.platform_income, true)}
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-gray-800 text-white border-gray-700">
|
||||||
|
<CardHeader>
|
||||||
|
{t("Agentlik daromadi")}
|
||||||
|
<p className="text-2xl font-bold text-cyan-400">
|
||||||
|
{formatPrice(data.paid + data.pending, true)}
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "bookings" && (
|
{activeTab === "bookings" && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-xl font-bold mb-4">Recent Bookings</h2>
|
<h2 className="text-xl font-bold mb-4">
|
||||||
{mockTourPurchases.map((purchase) => (
|
{t("Recent Bookings")}
|
||||||
|
</h2>
|
||||||
|
{data?.orders.map((purchase) => (
|
||||||
<div
|
<div
|
||||||
key={purchase.id}
|
key={purchase.id}
|
||||||
className="bg-gray-700 p-6 rounded-lg hover:bg-gray-600 transition-colors"
|
className="bg-gray-700 p-6 rounded-lg hover:bg-gray-600 transition-colors"
|
||||||
@@ -371,45 +278,48 @@ export default function FinanceDetailTour() {
|
|||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-gray-100">
|
<h3 className="text-lg font-bold text-gray-100">
|
||||||
{purchase.userName}
|
{purchase.user.first_name} {purchase.user.last_name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400 text-sm">
|
<p className="text-gray-400 text-sm">
|
||||||
{purchase.userPhone}
|
{formatPhone(purchase.user.contact)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{getStatusBadge(purchase.paymentStatus)}
|
{/* {getStatusBadge(purchase.)} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Travel Date</p>
|
<p className="text-sm text-gray-400">
|
||||||
|
{t("Travel Date")}
|
||||||
|
</p>
|
||||||
<p className="text-gray-100 font-medium">
|
<p className="text-gray-100 font-medium">
|
||||||
{purchase.travelDate}
|
{purchase.departure_date}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Booking Date</p>
|
<p className="text-sm text-gray-400">
|
||||||
<p className="text-gray-100">{purchase.purchaseDate}</p>
|
{t("Booking Date")}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-100">
|
||||||
|
{formatDate.format(purchase.created_at, "DD-MM-YYYY")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Amount</p>
|
<p className="text-sm text-gray-400">{t("Amount")}</p>
|
||||||
<p className="text-green-400 font-bold">
|
<p className="text-green-400 font-bold">
|
||||||
${(purchase.amount / 1000000).toFixed(1)}M
|
{formatPrice(purchase.total_price, true)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-4 border-t border-gray-600">
|
<div className="flex justify-end items-center pt-4 border-t border-gray-600">
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
Agency: {purchase.agencyName}
|
|
||||||
</div>
|
|
||||||
<Link
|
<Link
|
||||||
to={`/bookings/${purchase.id}`}
|
to={`/bookings/${purchase.id}`}
|
||||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
View Details
|
{t("Batafsil")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -420,37 +330,27 @@ export default function FinanceDetailTour() {
|
|||||||
{activeTab === "reviews" && (
|
{activeTab === "reviews" && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-xl font-bold mb-4">Customer Reviews</h2>
|
<h2 className="text-xl font-bold mb-4">Customer Reviews</h2>
|
||||||
{mockTourPurchases
|
{data?.comments.map((purchase) => (
|
||||||
.filter((purchase) => purchase.rating > 0)
|
|
||||||
.map((purchase) => (
|
|
||||||
<div
|
<div
|
||||||
key={purchase.id}
|
key={purchase.created}
|
||||||
className="bg-gray-700 p-6 rounded-lg hover:bg-gray-600 transition-colors"
|
className="bg-gray-700 p-6 rounded-lg hover:bg-gray-600 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-gray-100">
|
<h3 className="text-lg font-bold text-gray-100">
|
||||||
{purchase.userName}
|
{purchase.user.first_name} {purchase.user.last_name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400 text-sm">
|
<p className="text-gray-400 text-sm">
|
||||||
{purchase.travelDate}
|
{formatDate.format(purchase.created, "DD-MM-YYYY")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{renderStars(purchase.rating)}
|
{renderStars(purchase.rating)}
|
||||||
<span className="text-gray-300">
|
<span className="text-gray-300">{purchase.rating}</span>
|
||||||
{purchase.rating}.0
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-100 mb-4">{purchase.review}</p>
|
<p className="text-gray-100 mb-4">{purchase.text}</p>
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-4 border-t border-gray-600">
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
Booked on {purchase.purchaseDate}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import type {
|
|||||||
GetSupportUser,
|
GetSupportUser,
|
||||||
} from "@/pages/support/lib/types";
|
} from "@/pages/support/lib/types";
|
||||||
import httpClient from "@/shared/config/api/httpClient";
|
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";
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
const getSupportUser = async (params: {
|
const getSupportUser = async (params: {
|
||||||
@@ -55,10 +60,43 @@ const getSupportAgencyDetail = async (
|
|||||||
return res;
|
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 {
|
export {
|
||||||
|
createTourAdmin,
|
||||||
deleteSupportUser,
|
deleteSupportUser,
|
||||||
getSupportAgency,
|
getSupportAgency,
|
||||||
getSupportAgencyDetail,
|
getSupportAgencyDetail,
|
||||||
getSupportUser,
|
getSupportUser,
|
||||||
updateSupportUser,
|
updateSupportUser,
|
||||||
|
updateTour,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 type { GetSupportAgencyRes } from "@/pages/support/lib/types";
|
||||||
import formatPhone from "@/shared/lib/formatPhone";
|
import formatPhone from "@/shared/lib/formatPhone";
|
||||||
import { Button } from "@/shared/ui/button";
|
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 { AlertTriangle, Loader2, XIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const SupportAgency = () => {
|
const SupportAgency = () => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [openUser, setOpenUser] = useState<boolean>(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 { t } = useTranslation();
|
||||||
const [selected, setSelected] = useState<GetSupportAgencyRes | null>(null);
|
const [selected, setSelected] = useState<GetSupportAgencyRes | null>(null);
|
||||||
|
|
||||||
@@ -19,6 +44,44 @@ const SupportAgency = () => {
|
|||||||
getSupportAgency({ page: 1, page_size: 10, search: "", status: "" }),
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
||||||
@@ -213,8 +276,12 @@ const SupportAgency = () => {
|
|||||||
<div className="mt-6 flex justify-end gap-3">
|
<div className="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
alert("Qabul qilindi (ishlab chiqishingiz kerak)");
|
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
|
createAdmin(selected.id);
|
||||||
|
updateTours({
|
||||||
|
body: { status: "approved" },
|
||||||
|
id: selected.id,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition"
|
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition"
|
||||||
>
|
>
|
||||||
@@ -222,8 +289,11 @@ const SupportAgency = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
alert("Rad etildi (ishlab chiqishingiz kerak)");
|
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
|
updateTours({
|
||||||
|
body: { status: "cancelled" },
|
||||||
|
id: selected.id,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition"
|
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition"
|
||||||
>
|
>
|
||||||
@@ -233,6 +303,47 @@ const SupportAgency = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Dialog open={openUser} onOpenChange={() => {}}>
|
||||||
|
<DialogContent className="bg-gray-900 text-white sm:max-w-sm rounded-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-lg font-semibold">
|
||||||
|
{t("Yangi foydalanuvchi ma'lumotlari")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-400 text-sm">
|
||||||
|
{t("Agentlik uchun tizimga kirish ma'lumotlari")}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 text-sm mb-1">
|
||||||
|
{t("Telefon raqam (login)")}
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-medium">
|
||||||
|
{formatPhone(user.data.phone)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 text-sm mb-1">{t("Parol")}</p>
|
||||||
|
<p className="text-lg font-semibold text-green-400">
|
||||||
|
{user.data.password}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter className="mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={() => setOpenUser(false)}
|
||||||
|
className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white px-5 py-2 rounded-md hover:opacity-90"
|
||||||
|
>
|
||||||
|
{t("Yopish")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,16 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
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 { t } = useTranslation();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||||
@@ -133,7 +142,11 @@ const Tours = () => {
|
|||||||
<div className="min-h-screen bg-gray-900 text-foreground py-10 px-5 w-full">
|
<div className="min-h-screen bg-gray-900 text-foreground py-10 px-5 w-full">
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
<h1 className="text-3xl font-semibold">{t("Turlar ro'yxati")}</h1>
|
<h1 className="text-3xl font-semibold">{t("Turlar ro'yxati")}</h1>
|
||||||
<Button onClick={() => navigate("/tours/create")} variant="default">
|
<Button
|
||||||
|
onClick={() => navigate("/tours/create")}
|
||||||
|
variant="default"
|
||||||
|
disabled={user !== "tour_admin"}
|
||||||
|
>
|
||||||
<PlusCircle className="w-5 h-5 mr-2" /> {t("Yangi tur qo'shish")}
|
<PlusCircle className="w-5 h-5 mr-2" /> {t("Yangi tur qo'shish")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,9 +162,11 @@ const Tours = () => {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="min-w-[180px]">{t("Mehmonxona")}</TableHead>
|
<TableHead className="min-w-[180px]">{t("Mehmonxona")}</TableHead>
|
||||||
<TableHead className="min-w-[200px]">{t("Narxi")}</TableHead>
|
<TableHead className="min-w-[200px]">{t("Narxi")}</TableHead>
|
||||||
|
{user && user === "moderator" && (
|
||||||
<TableHead className="min-w-[120px] text-center">
|
<TableHead className="min-w-[120px] text-center">
|
||||||
{t("Popular")}
|
{t("Popular")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
)}
|
||||||
<TableHead className="min-w-[150px] text-center">
|
<TableHead className="min-w-[150px] text-center">
|
||||||
{t("Операции")}
|
{t("Операции")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -170,7 +185,7 @@ const Tours = () => {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-primary font-medium">
|
<TableCell className="text-sm text-primary font-medium">
|
||||||
{tour.duration_days} kun
|
{tour.duration_days} {t("kun")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -185,7 +200,7 @@ const Tours = () => {
|
|||||||
{formatPrice(tour.price, true)}
|
{formatPrice(tour.price, true)}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
{user && user === "moderator" && (
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Switch
|
<Switch
|
||||||
checked={tour.featured_tickets}
|
checked={tour.featured_tickets}
|
||||||
@@ -197,6 +212,7 @@ const Tours = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
)}
|
||||||
|
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<div className="flex gap-2 justify-center">
|
<div className="flex gap-2 justify-center">
|
||||||
|
|||||||
@@ -27,10 +27,13 @@ const SITE_SETTING = "dashboard/dashboard-site-settings/";
|
|||||||
const SUPPORT_USER = "dashboard/dashboard-support/";
|
const SUPPORT_USER = "dashboard/dashboard-support/";
|
||||||
const SUPPORT_AGENCY = "dashboard/dashboard-travel-agency-request/";
|
const SUPPORT_AGENCY = "dashboard/dashboard-travel-agency-request/";
|
||||||
const USER_ORDERS = "dashboard/dashboard-ticket-order/";
|
const USER_ORDERS = "dashboard/dashboard-ticket-order/";
|
||||||
|
const AGENCY_ORDERS = "dashboard/dashboard-site-travel-agency-report/";
|
||||||
const POPULAR_TOURS = "dashboard/dashboard-ticket-featured/";
|
const POPULAR_TOURS = "dashboard/dashboard-ticket-featured/";
|
||||||
const BANNER = "dashboard/dashboard-site-banner/";
|
const BANNER = "dashboard/dashboard-site-banner/";
|
||||||
|
const TOUR_ADMIN = "dashboard/dashboard-tour-admin/";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
AGENCY_ORDERS,
|
||||||
AUTH_LOGIN,
|
AUTH_LOGIN,
|
||||||
BANNER,
|
BANNER,
|
||||||
BASE_URL,
|
BASE_URL,
|
||||||
@@ -57,6 +60,7 @@ export {
|
|||||||
SITE_SETTING,
|
SITE_SETTING,
|
||||||
SUPPORT_AGENCY,
|
SUPPORT_AGENCY,
|
||||||
SUPPORT_USER,
|
SUPPORT_USER,
|
||||||
|
TOUR_ADMIN,
|
||||||
TOUR_TRANSPORT,
|
TOUR_TRANSPORT,
|
||||||
UPDATE_USER,
|
UPDATE_USER,
|
||||||
USER_ORDERS,
|
USER_ORDERS,
|
||||||
|
|||||||
@@ -466,5 +466,30 @@
|
|||||||
"Reytingi baland turlar": "Высокорейтинговые туры",
|
"Reytingi baland turlar": "Высокорейтинговые туры",
|
||||||
"Status muvaffaqiyatli yangilandi": "Статус успешно обновлён",
|
"Status muvaffaqiyatli yangilandi": "Статус успешно обновлён",
|
||||||
"Statusni yangilashda xatolik yuz berdi": "Ошибка обновления статуса",
|
"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.": "Вы уверены, что хотите удалить этого пользователя? Это действие необратимо."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -467,5 +467,30 @@
|
|||||||
"Reytingi baland turlar": "Reytingi baland turlar",
|
"Reytingi baland turlar": "Reytingi baland turlar",
|
||||||
"Status muvaffaqiyatli yangilandi": "Status muvaffaqiyatli yangilandi",
|
"Status muvaffaqiyatli yangilandi": "Status muvaffaqiyatli yangilandi",
|
||||||
"Statusni yangilashda xatolik yuz berdi": "Statusni yangilashda xatolik yuz berdi",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,30 @@
|
|||||||
/**
|
/**
|
||||||
* Format the number (+998 00 111-22-33)
|
* Format phone number: +998 00 111-22-33 yoki +888 00 111-22-33
|
||||||
* @param value Number to be formatted (XXXYYZZZAABB)
|
|
||||||
* @returns string +998 00 111-22-33
|
|
||||||
*/
|
*/
|
||||||
const formatPhone = (value: string) => {
|
const formatPhone = (value: string) => {
|
||||||
// Keep only numbers
|
// faqat raqamlarni olish
|
||||||
const digits = value.replace(/\D/g, '');
|
const digits = value.replace(/\D/g, "");
|
||||||
|
|
||||||
// Return empty string if data is not available
|
// agar hech narsa yo'q bo'lsa — input bo'sh bo'lib tursin
|
||||||
if (digits.length === 0) {
|
if (digits.length === 0) return "";
|
||||||
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;
|
let formattedNumber = prefix;
|
||||||
|
|
||||||
if (digits.length > 3) {
|
if (core.length > 0) formattedNumber += core.slice(0, 2);
|
||||||
formattedNumber += digits.slice(3, 5);
|
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);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedNumber.trim();
|
return formattedNumber.trim();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { removeAuthToken, removeRefAuthToken } from "@/shared/lib/authCookies";
|
||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -10,12 +11,14 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/shared/ui/sheet";
|
} from "@/shared/ui/sheet";
|
||||||
import LangToggle from "@/widgets/lang-toggle/ui/lang-toggle";
|
import LangToggle from "@/widgets/lang-toggle/ui/lang-toggle";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Briefcase,
|
Briefcase,
|
||||||
Building2,
|
Building2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
|
LogOut,
|
||||||
Menu,
|
Menu,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Newspaper,
|
Newspaper,
|
||||||
@@ -27,6 +30,7 @@ import {
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
type Role =
|
type Role =
|
||||||
| "superuser"
|
| "superuser"
|
||||||
@@ -46,13 +50,13 @@ const MENU_ITEMS = [
|
|||||||
label: "Foydalanuvchilar",
|
label: "Foydalanuvchilar",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
path: "/user",
|
path: "/user",
|
||||||
roles: ["moderator", "admin", "superuser", "moderator"],
|
roles: ["moderator", "admin", "superuser", "operator"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Tur firmalar",
|
label: "Tur firmalar",
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
path: "/agencies",
|
path: "/agencies",
|
||||||
roles: ["moderator", "admin", "superuser", "moderator"],
|
roles: ["moderator", "admin", "superuser", "operator"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Xodimlar",
|
label: "Xodimlar",
|
||||||
@@ -64,20 +68,13 @@ const MENU_ITEMS = [
|
|||||||
label: "Bronlar",
|
label: "Bronlar",
|
||||||
icon: Wallet,
|
icon: Wallet,
|
||||||
path: "/finance",
|
path: "/finance",
|
||||||
roles: ["moderator", "admin", "superuser", "buxgalter"],
|
roles: ["moderator", "admin", "superuser", "buxgalter", "tour_admin"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Turlar",
|
label: "Turlar",
|
||||||
icon: Plane,
|
icon: Plane,
|
||||||
path: "/tours",
|
path: "/tours",
|
||||||
roles: [
|
roles: ["moderator", "admin", "superuser", "tour_admin"],
|
||||||
"moderator",
|
|
||||||
"admin",
|
|
||||||
"superuser",
|
|
||||||
"tour_admin",
|
|
||||||
"operator",
|
|
||||||
"buxgalter",
|
|
||||||
],
|
|
||||||
children: [
|
children: [
|
||||||
{ label: "Turlar", path: "/tours" },
|
{ 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",
|
label: "Yangiliklar",
|
||||||
icon: Newspaper,
|
icon: Newspaper,
|
||||||
@@ -129,7 +113,7 @@ const MENU_ITEMS = [
|
|||||||
{
|
{
|
||||||
label: "Agentlik arizalari",
|
label: "Agentlik arizalari",
|
||||||
path: "/support/tours",
|
path: "/support/tours",
|
||||||
roles: ["moderator", "admin", "superuser", "operator"],
|
roles: ["moderator", "admin", "superuser"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Yordam arizalari",
|
label: "Yordam arizalari",
|
||||||
@@ -164,12 +148,21 @@ export function Sidebar({ role }: SidebarProps) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||||
|
|
||||||
const visibleMenu = useMemo(
|
const visibleMenu = useMemo(() => {
|
||||||
() => MENU_ITEMS.filter((item) => item.roles.includes(role)),
|
return MENU_ITEMS.filter((item) => item.roles.includes(role)).map(
|
||||||
[role],
|
(item) => ({
|
||||||
|
...item,
|
||||||
|
children: item.children
|
||||||
|
? item.children.filter(
|
||||||
|
(child) => !child.roles || child.roles.includes(role),
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
}, [role]);
|
||||||
|
|
||||||
const [active, setActive] = useState<string>(location.pathname);
|
const [active, setActive] = useState<string>(location.pathname);
|
||||||
const [openMenus, setOpenMenus] = useState<string[]>([]);
|
const [openMenus, setOpenMenus] = useState<string[]>([]);
|
||||||
@@ -190,11 +183,21 @@ export function Sidebar({ role }: SidebarProps) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
removeAuthToken();
|
||||||
|
removeRefAuthToken();
|
||||||
|
queryClient.clear();
|
||||||
|
toast.success(t("Tizimdan chiqdingiz"));
|
||||||
|
navigate("/auth/login");
|
||||||
|
};
|
||||||
|
|
||||||
const MenuList = (
|
const MenuList = (
|
||||||
<ul className="p-2 space-y-1">
|
<div className="flex flex-col h-full justify-between">
|
||||||
|
<ul className="p-2 space-y-1 flex-1 overflow-y-auto">
|
||||||
{visibleMenu.map(({ label, icon: Icon, path, children }) => {
|
{visibleMenu.map(({ label, icon: Icon, path, children }) => {
|
||||||
const isActive = active.startsWith(path);
|
const isActive = active.startsWith(path);
|
||||||
const isOpen = openMenus.includes(label);
|
const isOpen = openMenus.includes(label);
|
||||||
|
const hasChildren = children?.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={path}>
|
<li key={path}>
|
||||||
@@ -206,14 +209,14 @@ export function Sidebar({ role }: SidebarProps) {
|
|||||||
: "text-gray-400 hover:bg-gray-700 hover:text-white",
|
: "text-gray-400 hover:bg-gray-700 hover:text-white",
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
children ? toggleSubMenu(label) : handleClick(path)
|
hasChildren ? toggleSubMenu(label) : handleClick(path)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Icon className="w-5 h-5" />
|
<Icon className="w-5 h-5" />
|
||||||
{t(label)}
|
{t(label)}
|
||||||
</div>
|
</div>
|
||||||
{children && (
|
{hasChildren && (
|
||||||
<span className="transition-transform">
|
<span className="transition-transform">
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<ChevronDown className="w-4 h-4" />
|
<ChevronDown className="w-4 h-4" />
|
||||||
@@ -224,7 +227,7 @@ export function Sidebar({ role }: SidebarProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children && isOpen && (
|
{hasChildren && isOpen && (
|
||||||
<ul className="ml-6 mt-1 space-y-1 border-l border-gray-700 pl-3">
|
<ul className="ml-6 mt-1 space-y-1 border-l border-gray-700 pl-3">
|
||||||
{children.map((sub) => (
|
{children.map((sub) => (
|
||||||
<li key={sub.path}>
|
<li key={sub.path}>
|
||||||
@@ -248,13 +251,25 @@ export function Sidebar({ role }: SidebarProps) {
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<LangToggle />
|
<LangToggle />
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-700 mt-2 pt-3 px-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleLogout}
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full flex items-center justify-start gap-2 text-red-400 hover:bg-red-900/20 hover:text-red-300 mt-2"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
<span>{t("Chiqish")}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lg:border">
|
<div className="lg:border">
|
||||||
|
{/* Mobil versiya */}
|
||||||
<div className="lg:hidden flex items-center justify-end bg-gray-900 p-4 sticky top-0 z-50">
|
<div className="lg:hidden flex items-center justify-end bg-gray-900 p-4 sticky top-0 z-50">
|
||||||
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
@@ -277,15 +292,17 @@ export function Sidebar({ role }: SidebarProps) {
|
|||||||
/>
|
/>
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<nav className="overflow-y-auto">{MenuList}</nav>
|
<nav className="h-full">{MenuList}</nav>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop versiya */}
|
||||||
<aside className="hidden bg-gray-900 lg:flex w-64 flex-col h-screen">
|
<aside className="hidden bg-gray-900 lg:flex w-64 flex-col h-screen">
|
||||||
<div className="flex items-center gap-2 p-4 border-b">
|
<div className="flex items-center gap-2 p-4 border-b">
|
||||||
<img src="/Logo_white.png" width={120} height={120} alt="logo" />
|
<img src="/Logo_white.png" width={120} height={120} alt="logo" />
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex-1 overflow-y-auto">{MenuList}</nav>
|
<nav className="flex-1">{MenuList}</nav>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user