bug fix
This commit is contained in:
150
src/App.tsx
150
src/App.tsx
@@ -15,6 +15,7 @@ import {
|
|||||||
import AddNews from "@/pages/news/ui/AddNews";
|
import AddNews from "@/pages/news/ui/AddNews";
|
||||||
import News from "@/pages/news/ui/News";
|
import News from "@/pages/news/ui/News";
|
||||||
import NewsCategory from "@/pages/news/ui/NewsCategory";
|
import NewsCategory from "@/pages/news/ui/NewsCategory";
|
||||||
|
import Page from "@/pages/profile/ui/Profile";
|
||||||
import Seo from "@/pages/seo/ui/Seo";
|
import Seo from "@/pages/seo/ui/Seo";
|
||||||
import SiteBannerAdmin from "@/pages/site-banner/ui/Banner";
|
import SiteBannerAdmin from "@/pages/site-banner/ui/Banner";
|
||||||
import PolicyCrud from "@/pages/site-page/ui/PolicyCrud";
|
import PolicyCrud from "@/pages/site-page/ui/PolicyCrud";
|
||||||
@@ -35,6 +36,8 @@ import "@/shared/config/i18n";
|
|||||||
import { getAuthToken } from "@/shared/lib/authCookies";
|
import { getAuthToken } from "@/shared/lib/authCookies";
|
||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
import { Sidebar } from "@/widgets/sidebar/ui/Sidebar";
|
import { Sidebar } from "@/widgets/sidebar/ui/Sidebar";
|
||||||
|
import { useWelcomeStore } from "@/widgets/welcome/lib/hook";
|
||||||
|
import Welcome from "@/widgets/welcome/ui/welcome";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -49,85 +52,104 @@ const App = () => {
|
|||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { setOpenModal } = useWelcomeStore();
|
||||||
|
|
||||||
const { data: user } = useQuery({
|
const { data: user } = useQuery({
|
||||||
queryKey: ["get_me"],
|
queryKey: ["get_me"],
|
||||||
queryFn: () => getMe(),
|
queryFn: () => getMe(),
|
||||||
select(data) {
|
select: (data) => data.data.data,
|
||||||
return data.data.data;
|
|
||||||
},
|
|
||||||
enabled: !!token,
|
enabled: !!token,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hideSidebarPaths = ["/login"];
|
const hideSidebarPaths = ["/login"];
|
||||||
const shouldShowSidebar = !hideSidebarPaths.includes(location.pathname);
|
const shouldShowSidebar = !hideSidebarPaths.includes(location.pathname);
|
||||||
|
|
||||||
|
// 🔹 Avtorizatsiya yo‘nalishlari
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token && user && user.role === "moderator") {
|
if (token && user) {
|
||||||
navigate("/user");
|
if (user.role === "moderator") {
|
||||||
} else if (token && user && user.role === "tour_admin") {
|
navigate("/user");
|
||||||
navigate("/finance ");
|
} else if (user.role === "tour_admin") {
|
||||||
} else if (token && user && user.role === "buxgalter") {
|
navigate("/profile");
|
||||||
navigate("/finance");
|
} else if (user.role === "buxgalter") {
|
||||||
} else if (!token && !user) {
|
navigate("/finance");
|
||||||
|
}
|
||||||
|
} else if (!token) {
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
}
|
}
|
||||||
}, [token, user]);
|
}, [token, user]);
|
||||||
|
|
||||||
return (
|
// 🔹 Faqat userda ism yoki familiya yo‘q bo‘lsa Welcome modalni ochamiz
|
||||||
<>
|
useEffect(() => {
|
||||||
<div className="flex max-lg:flex-col bg-gray-900">
|
if (
|
||||||
{shouldShowSidebar && <Sidebar role={user ? user.role : "admin"} />}
|
user &&
|
||||||
|
(!user.first_name || !user.last_name) &&
|
||||||
|
location.pathname !== "/login" &&
|
||||||
|
user.role !== "moderator"
|
||||||
|
) {
|
||||||
|
setOpenModal(true);
|
||||||
|
} else {
|
||||||
|
setOpenModal(false);
|
||||||
|
}
|
||||||
|
}, [user, location.pathname]);
|
||||||
|
|
||||||
<main
|
return (
|
||||||
className={cn(
|
<div className="flex max-lg:flex-col bg-gray-900">
|
||||||
"flex-1 min-h-screen bg-gray-900 transition-all",
|
{shouldShowSidebar && <Sidebar role={user ? user.role : "admin"} />}
|
||||||
shouldShowSidebar ? "lg:ml-64" : "ml-0"
|
|
||||||
)}
|
<main
|
||||||
>
|
className={cn(
|
||||||
<Routes>
|
"flex-1 min-h-screen bg-gray-900 transition-all",
|
||||||
<Route path="/" element={<Navigate to={"/user"} />} />
|
shouldShowSidebar ? "lg:ml-64" : "ml-0",
|
||||||
<Route path="/user" element={<UserList />} />
|
)}
|
||||||
<Route path="/login" element={<Login />} />
|
>
|
||||||
<Route path="/users/create" element={<CreateUser />} />
|
{/* ✅ Welcome faqat login sahifasida ko‘rinmaydi */}
|
||||||
<Route path="/users/:id/edit" element={<EditUser />} />
|
{location.pathname !== "/login" && <Welcome />}
|
||||||
<Route path="/users/:id/" element={<UserDetail />} />
|
|
||||||
<Route path="/agencies" element={<Agencies />} />
|
<Routes>
|
||||||
<Route path="/agencies/:id" element={<AgencyDetail />} />
|
<Route path="/" element={<Navigate to={"/user"} />} />
|
||||||
<Route path="/agency/:id/edit" element={<EditAgecy />} />
|
<Route path="/user" element={<UserList />} />
|
||||||
<Route path="/tours/:id" element={<TourDetail />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/employees" element={<Employees />} />
|
<Route path="/profile" element={<Page />} />
|
||||||
<Route
|
<Route path="/users/create" element={<CreateUser />} />
|
||||||
path="/finance"
|
<Route path="/users/:id/edit" element={<EditUser />} />
|
||||||
element={<FinancePage user={user ? user.role : "moderator"} />}
|
<Route path="/users/:id/" element={<UserDetail />} />
|
||||||
/>
|
<Route path="/agencies" element={<Agencies />} />
|
||||||
<Route path="/purchases/:id/" element={<PurchaseDetailPage />} />
|
<Route path="/agencies/:id" element={<AgencyDetail />} />
|
||||||
<Route path="/travel/booking/:id/" element={<FinanceDetailTour />} />
|
<Route path="/agency/:id/edit" element={<EditAgecy />} />
|
||||||
<Route path="/bookings/:id/" element={<FinanceDetailUsers />} />
|
<Route path="/tours/:id" element={<TourDetail />} />
|
||||||
<Route
|
<Route path="/employees" element={<Employees />} />
|
||||||
path="/tours"
|
<Route
|
||||||
element={<Tours user={user ? user.role : "moderator"} />}
|
path="/finance"
|
||||||
/>
|
element={<FinancePage user={user ? user.role : "moderator"} />}
|
||||||
<Route path="/tours/setting" element={<ToursSetting />} />
|
/>
|
||||||
<Route path="/tours/:id/edit" element={<CreateEditTour />} />
|
<Route path="/purchases/:id/" element={<PurchaseDetailPage />} />
|
||||||
<Route path="/tours/create" element={<CreateEditTour />} />
|
<Route path="/travel/booking/:id/" element={<FinanceDetailTour />} />
|
||||||
<Route path="/bookings" element={<Bookings />} />
|
<Route path="/bookings/:id/" element={<FinanceDetailUsers />} />
|
||||||
<Route path="/news" element={<News />} />
|
<Route
|
||||||
<Route path="/news/add" element={<AddNews />} />
|
path="/tours"
|
||||||
<Route path="/news/edit/:id" element={<AddNews />} />
|
element={<Tours user={user ? user.role : "moderator"} />}
|
||||||
<Route path="/news/categories" element={<NewsCategory />} />
|
/>
|
||||||
<Route path="/faq" element={<Faq />} />
|
<Route path="/tours/setting" element={<ToursSetting />} />
|
||||||
<Route path="/faq/categories" element={<FaqCategory />} />
|
<Route path="/tours/:id/edit" element={<CreateEditTour />} />
|
||||||
<Route path="/support/tours" element={<SupportAgency />} />
|
<Route path="/tours/create" element={<CreateEditTour />} />
|
||||||
<Route path="/support/user" element={<SupportTours />} />
|
<Route path="/bookings" element={<Bookings />} />
|
||||||
<Route path="/site-seo" element={<Seo />} />
|
<Route path="/news" element={<News />} />
|
||||||
<Route path="/site-pages/" element={<SitePage />} />
|
<Route path="/news/add" element={<AddNews />} />
|
||||||
<Route path="/site-help/" element={<PolicyCrud />} />
|
<Route path="/news/edit/:id" element={<AddNews />} />
|
||||||
<Route path="/site-settings/" element={<TourSettings />} />
|
<Route path="/news/categories" element={<NewsCategory />} />
|
||||||
<Route path="/site-banner/" element={<SiteBannerAdmin />} />
|
<Route path="/faq" element={<Faq />} />
|
||||||
</Routes>
|
<Route path="/faq/categories" element={<FaqCategory />} />
|
||||||
</main>
|
<Route path="/support/tours" element={<SupportAgency />} />
|
||||||
</div>
|
<Route path="/support/user" element={<SupportTours />} />
|
||||||
</>
|
<Route path="/site-seo" element={<Seo />} />
|
||||||
|
<Route path="/site-pages/" element={<SitePage />} />
|
||||||
|
<Route path="/site-help/" element={<PolicyCrud />} />
|
||||||
|
<Route path="/site-settings/" element={<TourSettings />} />
|
||||||
|
<Route path="/site-banner/" element={<SiteBannerAdmin />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default function AgencyDetailPage() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [edit, setEdit] = useState<boolean>(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [openUser, setOpenUser] = useState<boolean>(false);
|
const [openUser, setOpenUser] = useState<boolean>(false);
|
||||||
const [user, setUser] = useState<{
|
const [user, setUser] = useState<{
|
||||||
@@ -97,6 +98,7 @@ export default function AgencyDetailPage() {
|
|||||||
toast.success(t("Foydalanuvchi muvaffaqiyatli yangilandi"));
|
toast.success(t("Foydalanuvchi muvaffaqiyatli yangilandi"));
|
||||||
setOpenUser(true);
|
setOpenUser(true);
|
||||||
setUser(res.data);
|
setUser(res.data);
|
||||||
|
setEdit(false);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error(t("Foydalanuvchini yangilashda xatolik"));
|
toast.error(t("Foydalanuvchini yangilashda xatolik"));
|
||||||
@@ -407,6 +409,8 @@ export default function AgencyDetailPage() {
|
|||||||
{agency?.data.data && (
|
{agency?.data.data && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<AgencyUsersSection
|
<AgencyUsersSection
|
||||||
|
edit={edit}
|
||||||
|
setEdit={setEdit}
|
||||||
users={
|
users={
|
||||||
Array.isArray(agency?.data.data)
|
Array.isArray(agency?.data.data)
|
||||||
? agency?.data.data
|
? agency?.data.data
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import formatPhone from "@/shared/lib/formatPhone";
|
import formatPhone from "@/shared/lib/formatPhone";
|
||||||
import { Badge } from "@/shared/ui/badge";
|
import { Badge } from "@/shared/ui/badge";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
@@ -11,8 +13,8 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/shared/ui/dialog";
|
} from "@/shared/ui/dialog";
|
||||||
|
|
||||||
import { Pencil, Phone, Shield, Trash2, User } from "lucide-react";
|
import { Pencil, Phone, Shield, Trash2, User } from "lucide-react";
|
||||||
|
import { type Dispatch, type SetStateAction } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface AgencyUser {
|
interface AgencyUser {
|
||||||
@@ -28,16 +30,19 @@ interface AgencyUsersProps {
|
|||||||
onEdit: (userId: number) => void;
|
onEdit: (userId: number) => void;
|
||||||
onDelete: (userId: number) => void;
|
onDelete: (userId: number) => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
edit: boolean;
|
||||||
|
setEdit: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgencyUsersSection({
|
export function AgencyUsersSection({
|
||||||
users,
|
users,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
edit,
|
||||||
|
setEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: AgencyUsersProps) {
|
}: AgencyUsersProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const getRoleBadge = (role: string) => {
|
const getRoleBadge = (role: string) => {
|
||||||
const roleColors: Record<string, string> = {
|
const roleColors: Record<string, string> = {
|
||||||
admin: "bg-purple-500/20 text-purple-300 border-purple-500/40",
|
admin: "bg-purple-500/20 text-purple-300 border-purple-500/40",
|
||||||
@@ -45,7 +50,6 @@ export function AgencyUsersSection({
|
|||||||
user: "bg-gray-500/20 text-gray-300 border-gray-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",
|
agent: "bg-green-500/20 text-green-300 border-green-500/40",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge className={roleColors[role] || roleColors.user}>
|
<Badge className={roleColors[role] || roleColors.user}>
|
||||||
{role.toUpperCase()}
|
{role.toUpperCase()}
|
||||||
@@ -134,18 +138,54 @@ export function AgencyUsersSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Edit Button */}
|
{/* Edit Confirm Dialog */}
|
||||||
<Button
|
<Dialog open={edit} onOpenChange={setEdit}>
|
||||||
variant="outline"
|
<DialogTrigger asChild>
|
||||||
size="icon"
|
<Button
|
||||||
onClick={() => onEdit(user.id)}
|
variant="outline"
|
||||||
className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-blue-400 hover:text-blue-300"
|
size="icon"
|
||||||
>
|
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>
|
<Pencil
|
||||||
|
className="w-4 h-4"
|
||||||
|
onClick={() => setEdit(true)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="bg-gray-800 border-gray-700 text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t("Foydalanuvchini tahrirlash")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-400">
|
||||||
|
{t("Haqiqatan ham")}{" "}
|
||||||
|
<span className="font-semibold text-white">
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</span>{" "}
|
||||||
|
{t("ni tahrirlamoqchimisiz?")}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
{/* Delete Alert Dialog */}
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-white"
|
||||||
|
onClick={() => setEdit(true)}
|
||||||
|
>
|
||||||
|
{t("Bekor qilish")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => onEdit(user.id)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
{t("Tahrirlash")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirm Dialog */}
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import type {
|
import type {
|
||||||
AgencyOrderData,
|
AgencyOrderData,
|
||||||
|
History,
|
||||||
UserAgencyDetailData,
|
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 { AGENCY_ORDERS, USER_ORDERS } from "@/shared/config/api/URLs";
|
import {
|
||||||
|
AGENCY_ORDERS,
|
||||||
|
PAYMENT_AGENCY,
|
||||||
|
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: {
|
||||||
@@ -63,10 +68,33 @@ const updateDetailOrder = async ({
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const payAgency = async ({
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
body: {
|
||||||
|
travel_agency: number;
|
||||||
|
amount: number;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const res = await httpClient.post(PAYMENT_AGENCY, body);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaymentHistory = async (params: {
|
||||||
|
page_size: number;
|
||||||
|
page: number;
|
||||||
|
}): Promise<AxiosResponse<History>> => {
|
||||||
|
const res = await httpClient.get(PAYMENT_AGENCY, { params });
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getAllOrder,
|
getAllOrder,
|
||||||
getAllOrderAgecy,
|
getAllOrderAgecy,
|
||||||
getDetailAgencyOrder,
|
getDetailAgencyOrder,
|
||||||
getDetailOrder,
|
getDetailOrder,
|
||||||
|
getPaymentHistory,
|
||||||
|
payAgency,
|
||||||
updateDetailOrder,
|
updateDetailOrder,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -180,3 +180,36 @@ export interface UserAgencyDetailData {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface History {
|
||||||
|
status: boolean;
|
||||||
|
data: {
|
||||||
|
links: {
|
||||||
|
previous: string;
|
||||||
|
next: string;
|
||||||
|
};
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: number;
|
||||||
|
travel_agency: {
|
||||||
|
custom_id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
amount: number;
|
||||||
|
note: string;
|
||||||
|
accountant: {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { getDetailAgencyOrder } from "@/pages/finance/lib/api";
|
import { getDetailAgencyOrder } from "@/pages/finance/lib/api";
|
||||||
|
import { PayDialog } from "@/pages/finance/ui/PayDialog";
|
||||||
|
import { PaymentHistory } from "@/pages/finance/ui/PaymentHistory";
|
||||||
import formatDate from "@/shared/lib/formatDate";
|
import formatDate from "@/shared/lib/formatDate";
|
||||||
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 { Button } from "@/shared/ui/button";
|
||||||
import { Card, CardHeader } from "@/shared/ui/card";
|
import { Card, CardHeader } from "@/shared/ui/card";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
Banknote,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Eye,
|
Eye,
|
||||||
Mail,
|
Mail,
|
||||||
@@ -23,9 +27,10 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
|
|
||||||
export default function FinanceDetailTour() {
|
export default function FinanceDetailTour() {
|
||||||
|
const [openPayDialog, setOpenPayDialog] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [activeTab, setActiveTab] = useState<
|
const [activeTab, setActiveTab] = useState<
|
||||||
"overview" | "bookings" | "reviews"
|
"overview" | "bookings" | "reviews" | "payment"
|
||||||
>("overview");
|
>("overview");
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
@@ -70,6 +75,21 @@ export default function FinanceDetailTour() {
|
|||||||
<h1 className="text-3xl font-bold">{data?.name}</h1>
|
<h1 className="text-3xl font-bold">{data?.name}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end mt-8">
|
||||||
|
<Button
|
||||||
|
onClick={() => setOpenPayDialog(true)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg"
|
||||||
|
>
|
||||||
|
{t("Qolgan summani to‘lash")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{data && (
|
||||||
|
<PayDialog
|
||||||
|
open={openPayDialog}
|
||||||
|
onClose={() => setOpenPayDialog(false)}
|
||||||
|
agencyId={data?.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tour Summary Cards */}
|
{/* Tour Summary Cards */}
|
||||||
@@ -156,8 +176,18 @@ export default function FinanceDetailTour() {
|
|||||||
<Star className="w-4 h-4" />
|
<Star className="w-4 h-4" />
|
||||||
{t("Reviews")}
|
{t("Reviews")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
|
||||||
|
activeTab === "payment"
|
||||||
|
? "text-blue-400 border-b-2 border-blue-400"
|
||||||
|
: "text-gray-400 hover:text-gray-300"
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTab("payment")}
|
||||||
|
>
|
||||||
|
<Banknote className="w-4 h-4" />
|
||||||
|
{t("To'lovlar tarixi")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{activeTab === "overview" && (
|
{activeTab === "overview" && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
@@ -355,6 +385,8 @@ export default function FinanceDetailTour() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === "payment" && <PaymentHistory />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
140
src/pages/finance/ui/PayDialog.tsx
Normal file
140
src/pages/finance/ui/PayDialog.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { payAgency } from "@/pages/finance/lib/api";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
|
import { Input } from "@/shared/ui/input";
|
||||||
|
import { Label } from "@/shared/ui/label";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface PayDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
agencyId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PayFormValues {
|
||||||
|
amount: string; // formatted value (e.g. "1 200 000")
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narxni formatlovchi funksiya
|
||||||
|
function formatPrice(value: number | string): string {
|
||||||
|
const num = Number(value.toString().replace(/\D/g, ""));
|
||||||
|
if (isNaN(num)) return "";
|
||||||
|
return num.toLocaleString("ru-RU");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayDialog({ open, onClose, agencyId }: PayDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const form = useForm<PayFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
amount: "",
|
||||||
|
note: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate, isPending } = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
body: {
|
||||||
|
travel_agency: number;
|
||||||
|
amount: number;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
}) => payAgency({ body }),
|
||||||
|
onSuccess: () => {
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
richColors: true,
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const raw = e.target.value.replace(/\D/g, "");
|
||||||
|
const formatted = formatPrice(raw);
|
||||||
|
form.setValue("amount", formatted);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (values: PayFormValues) => {
|
||||||
|
const cleanAmount = Number(
|
||||||
|
values.amount.replace(/\s/g, "").replace(/,/g, ""),
|
||||||
|
);
|
||||||
|
mutate({
|
||||||
|
body: {
|
||||||
|
amount: cleanAmount,
|
||||||
|
note: values.note,
|
||||||
|
travel_agency: agencyId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="sm:max-w-md text-white bg-gray-900">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("Qolgan summani to‘lash")}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
|
className="space-y-4 py-2"
|
||||||
|
>
|
||||||
|
{/* Summani kiritish */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="amount">{t("Summani kiriting")}</Label>
|
||||||
|
<Input
|
||||||
|
id="amount"
|
||||||
|
inputMode="numeric"
|
||||||
|
placeholder="1 200 000"
|
||||||
|
value={form.watch("amount")}
|
||||||
|
onChange={handleAmountChange}
|
||||||
|
/>
|
||||||
|
{form.formState.errors.amount && (
|
||||||
|
<p className="text-red-400 text-sm">
|
||||||
|
{t("Summani to‘g‘ri kiriting")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Izoh */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="note">{t("Izoh")}</Label>
|
||||||
|
<Input
|
||||||
|
id="note"
|
||||||
|
type="text"
|
||||||
|
placeholder={t("Izoh kiriting") || ""}
|
||||||
|
{...form.register("note")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tugmalar */}
|
||||||
|
<DialogFooter className="flex justify-end gap-3 pt-4">
|
||||||
|
<Button type="button" variant="outline" onClick={onClose}>
|
||||||
|
{t("Bekor qilish")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isPending}>
|
||||||
|
{isPending ? <Loader2 className="animate-spin" /> : t("To‘lash")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/pages/finance/ui/PaymentHistory.tsx
Normal file
110
src/pages/finance/ui/PaymentHistory.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { getPaymentHistory } from "@/pages/finance/lib/api";
|
||||||
|
import formatPrice from "@/shared/lib/formatPrice";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/shared/ui/table";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { ChevronLeft, ChevronRightIcon, Loader2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export function PaymentHistory() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ["payment-history", page],
|
||||||
|
queryFn: () => getPaymentHistory({ page, page_size: 10 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<Loader2 className="animate-spin text-gray-500" size={40} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isError || !data)
|
||||||
|
return (
|
||||||
|
<Card className="p-6 text-center">
|
||||||
|
<p className="text-red-500">Xatolik yuz berdi. Qayta urinib ko‘ring.</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const history = data.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900 text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t("To'lovlar tarixi")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>{t("Agentlik")}</TableHead>
|
||||||
|
<TableHead>{t("Telefon")}</TableHead>
|
||||||
|
<TableHead>{t("Summasi")}</TableHead>
|
||||||
|
<TableHead>{t("Izoh")}</TableHead>
|
||||||
|
<TableHead>{t("Buxgalter")}</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{history.data.results.map((item) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>{item.travel_agency.custom_id}</TableCell>
|
||||||
|
<TableCell>{item.travel_agency.name}</TableCell>
|
||||||
|
<TableCell>{item.travel_agency.phone}</TableCell>
|
||||||
|
<TableCell>{formatPrice(item.amount, true)}</TableCell>
|
||||||
|
<TableCell>{item.note || "-"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.accountant.first_name} {item.accountant.last_name}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className="flex justify-between items-center mt-6">
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
{t("Sahifa")} {history.data.current_page} /{" "}
|
||||||
|
{history.data.total_pages}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!history.data.links.previous}
|
||||||
|
onClick={() => setPage((p) => Math.max(p - 1, 1))}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-6" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!history.data.links.next}
|
||||||
|
onClick={() =>
|
||||||
|
setPage((p) =>
|
||||||
|
Math.min(p + 1, history.data.total_pages || p + 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className="size-6" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/pages/profile/lib/api.ts
Normal file
10
src/pages/profile/lib/api.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import httpClient from "@/shared/config/api/httpClient";
|
||||||
|
import { GET_ME } from "@/shared/config/api/URLs";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
const getMeAgency = async (): Promise<AxiosResponse<any>> => {
|
||||||
|
const res = await httpClient.get(GET_ME);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getMeAgency };
|
||||||
145
src/pages/profile/ui/AgencyInfo.tsx
Normal file
145
src/pages/profile/ui/AgencyInfo.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import formatPhone from "@/shared/lib/formatPhone";
|
||||||
|
import formatPrice from "@/shared/lib/formatPrice";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
||||||
|
import { DollarSign, Mail, Phone, Star, Ticket } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface UserAgencyDetailData {
|
||||||
|
id: number;
|
||||||
|
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;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgencyCard({ agency }: { agency: UserAgencyDetailData }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Card className="border border-gray-700 bg-gray-900 text-white shadow-lg">
|
||||||
|
{/* HEADER */}
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl font-semibold">{agency.name}</CardTitle>
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
{t("Agentlik ID")}: {agency.custom_id}
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
{/* CONTENT */}
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Contact info */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400">Email</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Mail className="w-4 h-4 text-blue-400" />
|
||||||
|
<p>{agency.email || "—"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400">{t("Telefon")}</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Phone className="w-4 h-4 text-green-400" />
|
||||||
|
<p>{formatPhone(agency.phone) || "—"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400">{t("Veb-sayt")}</p>
|
||||||
|
<p>{agency.web_site || "—"}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-400">{t("Manzil")}</p>
|
||||||
|
<p>{agency.addres || "—"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Financial data */}
|
||||||
|
<div className="border-t border-gray-700 pt-3 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DollarSign className="w-5 h-5 text-yellow-400" />
|
||||||
|
<p>
|
||||||
|
<strong>{t("Jami daromad")}:</strong>{" "}
|
||||||
|
{formatPrice(agency.total_income, true)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{t("To'langan")}:</strong> {formatPrice(agency.paid, true)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{t("Kutilayotgan to‘lovlar")}:</strong>{" "}
|
||||||
|
{formatPrice(agency.pending)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{t("Platformaga tegishli")}:</strong>{" "}
|
||||||
|
{formatPrice(agency.platform_income, true)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="border-t border-gray-700 pt-3 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Ticket className="w-5 h-5 text-blue-400" />
|
||||||
|
<p>
|
||||||
|
<strong>{t("Sotilgan turlar soni")}:</strong>{" "}
|
||||||
|
{agency.ticket_sold_count}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Star className="w-5 h-5 text-yellow-500" />
|
||||||
|
<p>
|
||||||
|
<strong>{t("Reyting")}:</strong> {agency.rating || "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{t("Bookings")}:</strong> {agency.total_booking_count}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
119
src/pages/profile/ui/EditDialog.tsx
Normal file
119
src/pages/profile/ui/EditDialog.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
|
import { Input } from "@/shared/ui/input";
|
||||||
|
import { Auth_Api } from "@/widgets/welcome/lib/data";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface EditDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditDialog({ open, onClose, user }: EditDialogProps) {
|
||||||
|
const [firstName, setFirstName] = useState(user.first_name);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [lastName, setLastName] = useState(user.last_name);
|
||||||
|
|
||||||
|
const { mutate, isPending } = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
}: {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
}) => {
|
||||||
|
return Auth_Api.updateUser({ first_name, last_name });
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.resetQueries({ queryKey: ["get_me"] });
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError(error: AxiosError<{ non_field_errors: [string] }>) {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
icon: null,
|
||||||
|
description: error.name,
|
||||||
|
position: "bottom-right",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
mutate({
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="sm:max-w-[425px] rounded-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("Tahrirlash")}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t(
|
||||||
|
"Update your personal information below and click save when done.",
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label className="text-sm font-medium text-gray-200">
|
||||||
|
{t("First Name")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
placeholder={t("First Name")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label className="text-sm font-medium text-gray-200">
|
||||||
|
{t("Last Name")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
placeholder={t("Last Name")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
{t("Bekor qilish")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>
|
||||||
|
{isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t("Saqlash")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
src/pages/profile/ui/Profile.tsx
Normal file
64
src/pages/profile/ui/Profile.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { getDetailAgencyOrder } from "@/pages/finance/lib/api";
|
||||||
|
import { AgencyCard } from "@/pages/profile/ui/AgencyInfo";
|
||||||
|
import { EditDialog } from "@/pages/profile/ui/EditDialog";
|
||||||
|
import { ProfileCard, type ProfileData } from "@/pages/profile/ui/ProfileCard";
|
||||||
|
import { getMe } from "@/shared/config/api/auth/api";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
|
const [editingUser, setEditingUser] = useState<any>(null);
|
||||||
|
|
||||||
|
const { data: get_me, isLoading: userLoading } = useQuery({
|
||||||
|
queryKey: ["get_me"],
|
||||||
|
queryFn: () => getMe(),
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: agencyData, isLoading: agencyLoading } = useQuery({
|
||||||
|
queryKey: ["agency", get_me?.travel_agency],
|
||||||
|
queryFn: () => getDetailAgencyOrder(Number(get_me?.travel_agency)),
|
||||||
|
enabled: !!get_me?.travel_agency,
|
||||||
|
select: (data) => data.data?.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenEdit = (user: ProfileData) => {
|
||||||
|
setEditingUser(user);
|
||||||
|
setEditDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-gray-900 p-4 md:p-8">
|
||||||
|
<div className="mx-auto max-w-[100%] space-y-8">
|
||||||
|
<h1 className="text-3xl font-bold">Profile</h1>
|
||||||
|
|
||||||
|
{get_me && (
|
||||||
|
<ProfileCard
|
||||||
|
user={get_me}
|
||||||
|
onEditClick={() => handleOpenEdit(get_me)}
|
||||||
|
isLoading={userLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{agencyData && <AgencyCard agency={agencyData} />}
|
||||||
|
|
||||||
|
{(userLoading || agencyLoading) && (
|
||||||
|
<p className="text-center text-gray-500">Loading...</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editDialogOpen && editingUser && (
|
||||||
|
<EditDialog
|
||||||
|
open={editDialogOpen}
|
||||||
|
onClose={() => setEditDialogOpen(false)}
|
||||||
|
user={editingUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/pages/profile/ui/ProfileCard.tsx
Normal file
67
src/pages/profile/ui/ProfileCard.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import formatPhone from "@/shared/lib/formatPhone";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
||||||
|
import { BadgeCheck, Phone } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface ProfileData {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
avatar?: string;
|
||||||
|
username: string;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileCardProps {
|
||||||
|
user: ProfileData;
|
||||||
|
onEditClick: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileCard({
|
||||||
|
user,
|
||||||
|
onEditClick,
|
||||||
|
isLoading,
|
||||||
|
}: ProfileCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-2xl font-semibold">
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-sm text-gray-500">@{user.username}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onEditClick}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{t("Tahrirlash")}
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Phone className="h-4 w-4 text-gray-400" />
|
||||||
|
<span>{formatPhone(user.phone)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BadgeCheck
|
||||||
|
className={`h-4 w-4 ${user.is_active ? "text-green-500" : "text-gray-400"}`}
|
||||||
|
/>
|
||||||
|
<span>{user.is_active ? "Active" : "Inactive"}</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
} 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 {
|
import {
|
||||||
GET_ALL_AGENCY,
|
APPROVED_AGENCY,
|
||||||
SUPPORT_AGENCY,
|
SUPPORT_AGENCY,
|
||||||
SUPPORT_USER,
|
SUPPORT_USER,
|
||||||
TOUR_ADMIN,
|
TOUR_ADMIN,
|
||||||
@@ -87,7 +87,7 @@ const updateTour = async ({
|
|||||||
status: "pending" | "approved" | "cancelled";
|
status: "pending" | "approved" | "cancelled";
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const res = await httpClient.patch(`${GET_ALL_AGENCY}${id}/`, body);
|
const res = await httpClient.patch(`${APPROVED_AGENCY}${id}/`, body);
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ const SupportAgency = () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-md text-white">{t("Telefon raqam")}</div>
|
<div className="text-md text-white">{t("Telefon raqam")}</div>
|
||||||
<div>{formatPhone(selected.phone)}</div>
|
<div>{formatPhone(selected.phone ?? "")}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -321,7 +321,7 @@ const SupportAgency = () => {
|
|||||||
{t("Telefon raqam (login)")}
|
{t("Telefon raqam (login)")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg font-medium">
|
<p className="text-lg font-medium">
|
||||||
{formatPhone(user.data.phone)}
|
{formatPhone(user.data.phone ?? "")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,14 @@ const CreateEditTour = () => {
|
|||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === 2 && <StepTwo data={data} isEditMode={isEditMode} />}
|
{step === 2 && (
|
||||||
|
<StepTwo
|
||||||
|
data={data}
|
||||||
|
isEditMode={isEditMode}
|
||||||
|
step={step}
|
||||||
|
setStep={setStep}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ const StepOne = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
value.ticket_itinerary?.forEach((itinerary, i) => {
|
value.ticket_itinerary?.forEach((itinerary, i) => {
|
||||||
|
// Har bir itinerary uchun asosiy maydonlar
|
||||||
formData.append(`ticket_itinerary[${i}]title`, itinerary.title);
|
formData.append(`ticket_itinerary[${i}]title`, itinerary.title);
|
||||||
formData.append(`ticket_itinerary[${i}]title_ru`, itinerary.title_ru);
|
formData.append(`ticket_itinerary[${i}]title_ru`, itinerary.title_ru);
|
||||||
formData.append(
|
formData.append(
|
||||||
@@ -361,10 +362,17 @@ const StepOne = ({
|
|||||||
String(itinerary.duration),
|
String(itinerary.duration),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rasmlar
|
// 🖼 Rasmlar (faqat yangi yuklangan File-larni yuborish)
|
||||||
if (Array.isArray(itinerary.ticket_itinerary_image)) {
|
if (Array.isArray(itinerary.ticket_itinerary_image)) {
|
||||||
itinerary.ticket_itinerary_image.forEach((img, j) => {
|
itinerary.ticket_itinerary_image.forEach((img, j) => {
|
||||||
const file = img instanceof File ? img : img.image;
|
// img -> File yoki { image: File | string } shaklida bo‘lishi mumkin
|
||||||
|
const file =
|
||||||
|
img instanceof File
|
||||||
|
? img
|
||||||
|
: img?.image instanceof File
|
||||||
|
? img.image
|
||||||
|
: null;
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
formData.append(
|
formData.append(
|
||||||
`ticket_itinerary[${i}]ticket_itinerary_image[${j}]image`,
|
`ticket_itinerary[${i}]ticket_itinerary_image[${j}]image`,
|
||||||
@@ -374,7 +382,7 @@ const StepOne = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destinations
|
// 📍 Destinations (yo‘nalishlar)
|
||||||
if (Array.isArray(itinerary.ticket_itinerary_destinations)) {
|
if (Array.isArray(itinerary.ticket_itinerary_destinations)) {
|
||||||
itinerary.ticket_itinerary_destinations.forEach((dest, k) => {
|
itinerary.ticket_itinerary_destinations.forEach((dest, k) => {
|
||||||
formData.append(
|
formData.append(
|
||||||
@@ -388,6 +396,7 @@ const StepOne = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
value.hotel_meals.forEach((e, i) => {
|
value.hotel_meals.forEach((e, i) => {
|
||||||
if (e.image instanceof File) {
|
if (e.image instanceof File) {
|
||||||
formData.append(`ticket_hotel_meals[${i}]image`, e.image);
|
formData.append(`ticket_hotel_meals[${i}]image`, e.image);
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/shared/ui/select";
|
} from "@/shared/ui/select";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -55,13 +55,17 @@ const formSchema = z.object({
|
|||||||
const StepTwo = ({
|
const StepTwo = ({
|
||||||
data,
|
data,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
|
setStep,
|
||||||
}: {
|
}: {
|
||||||
data: GetOneTours | undefined;
|
data: GetOneTours | undefined;
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
|
step: number;
|
||||||
|
setStep: Dispatch<SetStateAction<number>>;
|
||||||
}) => {
|
}) => {
|
||||||
const { amenities, id: ticketId } = useTicketStore();
|
const { amenities, id: ticketId } = useTicketStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// 🧩 Query - Hotel detail
|
// 🧩 Query - Hotel detail
|
||||||
const { data: hotelDetail } = useQuery({
|
const { data: hotelDetail } = useQuery({
|
||||||
@@ -77,16 +81,20 @@ const StepTwo = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
title: "",
|
||||||
rating: "3.0",
|
rating: "3.0",
|
||||||
mealPlan: "",
|
mealPlan: "all_inclusive",
|
||||||
hotelType: [],
|
hotelType: [],
|
||||||
hotelFeatures: [],
|
hotelFeatures: [],
|
||||||
hotelFeaturesType: [],
|
hotelFeaturesType: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🧩 Edit holati uchun formni to‘ldirish
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditMode && hotelDetail?.[0]) {
|
if (
|
||||||
|
isEditMode &&
|
||||||
|
hotelDetail &&
|
||||||
|
hotelDetail.length > 0 &&
|
||||||
|
hotelDetail[0].meal_plan
|
||||||
|
) {
|
||||||
const hotel = hotelDetail[0];
|
const hotel = hotelDetail[0];
|
||||||
|
|
||||||
form.setValue("title", hotel.name);
|
form.setValue("title", hotel.name);
|
||||||
@@ -101,8 +109,9 @@ const StepTwo = ({
|
|||||||
? "Half Board"
|
? "Half Board"
|
||||||
: hotel.meal_plan === "full_board"
|
: hotel.meal_plan === "full_board"
|
||||||
? "Full Board"
|
? "Full Board"
|
||||||
: "All Inclusive";
|
: "all_inclusive";
|
||||||
|
|
||||||
|
// ✅ SetValue faqat backenddan qiymat kelganda chaqiriladi
|
||||||
form.setValue("mealPlan", mealPlan);
|
form.setValue("mealPlan", mealPlan);
|
||||||
|
|
||||||
form.setValue(
|
form.setValue(
|
||||||
@@ -117,7 +126,7 @@ const StepTwo = ({
|
|||||||
...new Set(hotel.hotel_features?.map((f) => String(f.id)) ?? []),
|
...new Set(hotel.hotel_features?.map((f) => String(f.id)) ?? []),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}, [isEditMode, hotelDetail, form, data]);
|
}, [isEditMode, hotelDetail, form]);
|
||||||
|
|
||||||
// 🧩 Select ma'lumotlari
|
// 🧩 Select ma'lumotlari
|
||||||
const [allHotelTypes, setAllHotelTypes] = useState<Type[]>([]);
|
const [allHotelTypes, setAllHotelTypes] = useState<Type[]>([]);
|
||||||
@@ -222,8 +231,10 @@ const StepTwo = ({
|
|||||||
const { mutate, isPending } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: (body: FormData) => createHotel({ body }),
|
mutationFn: (body: FormData) => createHotel({ body }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
||||||
toast.success(t("Muvaffaqiyatli saqlandi"));
|
toast.success(t("Muvaffaqiyatli saqlandi"));
|
||||||
navigate("/tours");
|
navigate("/tours");
|
||||||
|
setStep(1);
|
||||||
},
|
},
|
||||||
onError: () =>
|
onError: () =>
|
||||||
toast.error(t("Xatolik yuz berdi"), {
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
@@ -236,8 +247,10 @@ const StepTwo = ({
|
|||||||
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
||||||
editHotel({ body, id }),
|
editHotel({ body, id }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
||||||
toast.success(t("Muvaffaqiyatli saqlandi"));
|
toast.success(t("Muvaffaqiyatli saqlandi"));
|
||||||
navigate("/tours");
|
navigate("/tours");
|
||||||
|
setStep(1);
|
||||||
},
|
},
|
||||||
onError: () =>
|
onError: () =>
|
||||||
toast.error(t("Xatolik yuz berdi"), {
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ const AUTH_LOGIN = "auth/token/phone/";
|
|||||||
const GET_ME = "auth/me/";
|
const GET_ME = "auth/me/";
|
||||||
const GET_ALL_USERS = "dashboard/users/";
|
const GET_ALL_USERS = "dashboard/users/";
|
||||||
const DOWNLOAD_PDF = "get-order-pdf/";
|
const DOWNLOAD_PDF = "get-order-pdf/";
|
||||||
const UPDATE_USER = "/dashboard/users/";
|
const UPDATE_USERS = "auth/user-update/";
|
||||||
|
const UPDATE_USER = "dashboard/users/";
|
||||||
const GET_ALL_AGENCY = "dashboard/tour-agency/";
|
const GET_ALL_AGENCY = "dashboard/tour-agency/";
|
||||||
|
const APPROVED_AGENCY = "dashboard/dashboard-travel-agency-request/";
|
||||||
const GET_ALL_EMPLOYEES = "dashboard/employees/";
|
const GET_ALL_EMPLOYEES = "dashboard/employees/";
|
||||||
const GET_TICKET = "dashboard/dashboard-tickets/";
|
const GET_TICKET = "dashboard/dashboard-tickets/";
|
||||||
const HOTEL_BADGE = "dashboard/dashboard-tickets-settings-badge/";
|
const HOTEL_BADGE = "dashboard/dashboard-tickets-settings-badge/";
|
||||||
@@ -32,10 +34,12 @@ 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/";
|
const TOUR_ADMIN = "dashboard/dashboard-tour-admin/";
|
||||||
|
const PAYMENT_AGENCY = "dashboard/dashboard-site-agency-payments/";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AGENCY_ORDERS,
|
AGENCY_ORDERS,
|
||||||
AMENITIES,
|
AMENITIES,
|
||||||
|
APPROVED_AGENCY,
|
||||||
AUTH_LOGIN,
|
AUTH_LOGIN,
|
||||||
BANNER,
|
BANNER,
|
||||||
BASE_URL,
|
BASE_URL,
|
||||||
@@ -57,6 +61,7 @@ export {
|
|||||||
NEWS,
|
NEWS,
|
||||||
NEWS_CATEGORY,
|
NEWS_CATEGORY,
|
||||||
OFFERTA,
|
OFFERTA,
|
||||||
|
PAYMENT_AGENCY,
|
||||||
POPULAR_TOURS,
|
POPULAR_TOURS,
|
||||||
SITE_SEO,
|
SITE_SEO,
|
||||||
SITE_SETTING,
|
SITE_SETTING,
|
||||||
@@ -65,5 +70,6 @@ export {
|
|||||||
TOUR_ADMIN,
|
TOUR_ADMIN,
|
||||||
TOUR_TRANSPORT,
|
TOUR_TRANSPORT,
|
||||||
UPDATE_USER,
|
UPDATE_USER,
|
||||||
|
UPDATE_USERS,
|
||||||
USER_ORDERS,
|
USER_ORDERS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -492,5 +492,23 @@
|
|||||||
"Yangi foydalanuvchi ma'lumotlari": "Данные нового пользователя",
|
"Yangi foydalanuvchi ma'lumotlari": "Данные нового пользователя",
|
||||||
"Agentlik uchun tizimga kirish ma'lumotlari": "Входные данные для агентства",
|
"Agentlik uchun tizimga kirish ma'lumotlari": "Входные данные для агентства",
|
||||||
"Ikona tanlang": "Выберите иконку0",
|
"Ikona tanlang": "Выберите иконку0",
|
||||||
"Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.": "Вы уверены, что хотите удалить этого пользователя? Это действие необратимо."
|
"Last Name": "Фамилия",
|
||||||
|
"First Name": "Имя",
|
||||||
|
"Summani kiriting": "Введите сумму",
|
||||||
|
"Izoh": "Комментарий",
|
||||||
|
"To'lovlar tarixi": "История платежей",
|
||||||
|
"Qolgan summani to‘lash": "Оплатить оставшуюся сумму",
|
||||||
|
"Update your personal information below and click save when done.": "Обновите следующую личную информацию и нажмите Сохранить когда это будет сделано.",
|
||||||
|
"Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.": "Вы уверены, что хотите удалить этого пользователя? Это действие необратимо.",
|
||||||
|
"Agentlik": "Агентство",
|
||||||
|
"Telefon": "Телефон",
|
||||||
|
"Summasi": "Сумма",
|
||||||
|
"Buxgalter": "Бухгалтер",
|
||||||
|
"Sahifa": "Страница",
|
||||||
|
"Keyingi": "Следующий",
|
||||||
|
"Oldingi": "Дальше",
|
||||||
|
"Foydalanuvchini tahrirlash": "Редактировать пользователя",
|
||||||
|
"Haqiqatan ham": "Вы действительно хотите",
|
||||||
|
"ni tahrirlamoqchimisiz?": "отредактировать?",
|
||||||
|
"Agentlik ID": "ID агентства"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,5 +493,23 @@
|
|||||||
"Yangi foydalanuvchi ma'lumotlari": "Yangi foydalanuvchi ma'lumotlari",
|
"Yangi foydalanuvchi ma'lumotlari": "Yangi foydalanuvchi ma'lumotlari",
|
||||||
"Ikona tanlang": "Ikona tanlang",
|
"Ikona tanlang": "Ikona tanlang",
|
||||||
"Agentlik uchun tizimga kirish ma'lumotlari": "Agentlik uchun tizimga kirish 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."
|
"Update your personal information below and click save when done.": "Quyidagi shaxsiy axborotlaringizni yangilang va bajarilganda saqlash tugmasini bosing.",
|
||||||
|
"Last Name": "Familiya",
|
||||||
|
"First Name": "Ismi",
|
||||||
|
"Qolgan summani to‘lash": "Qolgan summani to‘lash",
|
||||||
|
"Summani kiriting": "Summani kiriting",
|
||||||
|
"Izoh": "Izoh",
|
||||||
|
"To'lovlar tarixi": "To'lovlar tarixi",
|
||||||
|
"Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.": "Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.",
|
||||||
|
"Agentlik": "Agentlik",
|
||||||
|
"Telefon": "Telefon",
|
||||||
|
"Summasi": "Summasi",
|
||||||
|
"Buxgalter": "Buxgalter",
|
||||||
|
"Sahifa": "Sahifa",
|
||||||
|
"Keyingi": "Keyingi",
|
||||||
|
"Oldingi": "Oldinga",
|
||||||
|
"Foydalanuvchini tahrirlash": "Foydalanuvchini tahrirlash",
|
||||||
|
"Haqiqatan ham": "Haqiqatan ham",
|
||||||
|
"ni tahrirlamoqchimisiz?": "ni tahrirlamoqchimisiz?",
|
||||||
|
"Agentlik ID": "Agentlik ID"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ const MENU_ITEMS = [
|
|||||||
path: "/user",
|
path: "/user",
|
||||||
roles: ["moderator", "admin", "superuser", "operator"],
|
roles: ["moderator", "admin", "superuser", "operator"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Profile",
|
||||||
|
icon: Users,
|
||||||
|
path: "/profile",
|
||||||
|
roles: ["tour_admin"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Tur firmalar",
|
label: "Tur firmalar",
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
@@ -251,9 +257,10 @@ export function Sidebar({ role }: SidebarProps) {
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<LangToggle />
|
|
||||||
</ul>
|
</ul>
|
||||||
|
<div className="border-t border-gray-700 mt-2 pt-3 px-2">
|
||||||
|
<LangToggle />
|
||||||
|
</div>
|
||||||
<div className="border-t border-gray-700 mt-2 pt-3 px-2">
|
<div className="border-t border-gray-700 mt-2 pt-3 px-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
|
|||||||
50
src/widgets/welcome/lib/api.ts
Normal file
50
src/widgets/welcome/lib/api.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import httpClient from "@/shared/config/api/httpClient";
|
||||||
|
import { GET_ME } from "@/shared/config/api/URLs";
|
||||||
|
|
||||||
|
interface GetMe {
|
||||||
|
status: boolean;
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
last_login: string;
|
||||||
|
is_superuser: boolean;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
is_staff: boolean;
|
||||||
|
is_active: boolean;
|
||||||
|
date_joined: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string;
|
||||||
|
validated_at: string;
|
||||||
|
role: string;
|
||||||
|
travel_agency: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetAllParticipantData {
|
||||||
|
status: boolean;
|
||||||
|
data: {
|
||||||
|
links: {
|
||||||
|
previous: string;
|
||||||
|
next: string;
|
||||||
|
};
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
results: {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
gender: "male" | "female";
|
||||||
|
last_name: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const User_Api = {
|
||||||
|
async getMe() {
|
||||||
|
const res = await httpClient.get<GetMe>(GET_ME);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
28
src/widgets/welcome/lib/data.ts
Normal file
28
src/widgets/welcome/lib/data.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import httpClient from "@/shared/config/api/httpClient";
|
||||||
|
import { UPDATE_USERS } from "@/shared/config/api/URLs";
|
||||||
|
|
||||||
|
export const Auth_Api = {
|
||||||
|
async updateUser({
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
avatar,
|
||||||
|
}: {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
avatar?: File;
|
||||||
|
}) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("first_name", first_name);
|
||||||
|
formData.append("last_name", last_name);
|
||||||
|
if (avatar) {
|
||||||
|
formData.append("avatar", avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await httpClient.patch(UPDATE_USERS, formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
11
src/widgets/welcome/lib/form.ts
Normal file
11
src/widgets/welcome/lib/form.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
export const welcomeForm = z.object({
|
||||||
|
firstName: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
lastName: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const editUserName = z.object({
|
||||||
|
firstName: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
lastName: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
});
|
||||||
15
src/widgets/welcome/lib/hook.ts
Normal file
15
src/widgets/welcome/lib/hook.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface WelcomeState {
|
||||||
|
openModal: boolean;
|
||||||
|
setOpenModal: (openModal: boolean) => void;
|
||||||
|
openModalMobile: boolean;
|
||||||
|
setOpenModalMobile: (openModal: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWelcomeStore = create<WelcomeState>((set) => ({
|
||||||
|
openModal: false,
|
||||||
|
setOpenModal: (openModal) => set({ openModal }),
|
||||||
|
openModalMobile: false,
|
||||||
|
setOpenModalMobile: (openModalMobile) => set({ openModalMobile }),
|
||||||
|
}));
|
||||||
@@ -1,26 +1,210 @@
|
|||||||
import LangToggle from "@/widgets/lang-toggle/ui/lang-toggle";
|
"use client";
|
||||||
import ModeToggle from "@/widgets/theme-toggle/ui/theme-toggle";
|
|
||||||
import GitHubButton from "react-github-btn";
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/shared/ui/form";
|
||||||
|
import { Input } from "@/shared/ui/input";
|
||||||
|
import { Label } from "@/shared/ui/label";
|
||||||
|
import { Auth_Api } from "@/widgets/welcome/lib/data";
|
||||||
|
import { welcomeForm } from "@/widgets/welcome/lib/form";
|
||||||
|
import { useWelcomeStore } from "@/widgets/welcome/lib/hook";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Drawer from "@mui/material/Drawer";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { LoaderCircle, XIcon } from "lucide-react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
const Welcome = () => {
|
const Welcome = () => {
|
||||||
return (
|
const { openModal: open, setOpenModal: setOpen } = useWelcomeStore();
|
||||||
<div className="custom-container h-screen rounded-2xl flex items-center justify-center">
|
const queryClient = useQueryClient();
|
||||||
<div className="flex flex-col gap-2 items-center">
|
const { t } = useTranslation();
|
||||||
<GitHubButton
|
const form = useForm<z.infer<typeof welcomeForm>>({
|
||||||
href="https://github.com/fiasuz/fias-ui"
|
resolver: zodResolver(welcomeForm),
|
||||||
data-color-scheme="no-preference: light; light: light; dark: dark;"
|
defaultValues: {
|
||||||
data-size="large"
|
firstName: "",
|
||||||
data-show-count="true"
|
lastName: "",
|
||||||
aria-label="Star fiasuz/fias-ui on GitHub"
|
},
|
||||||
|
});
|
||||||
|
const isMobile = useMediaQuery("(max-width:1024px)");
|
||||||
|
|
||||||
|
const { mutate, isPending } = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
}: {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
}) => {
|
||||||
|
return Auth_Api.updateUser({ first_name, last_name });
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
onError(error: AxiosError<{ non_field_errors: [string] }>) {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
icon: null,
|
||||||
|
description: error.name,
|
||||||
|
position: "bottom-right",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSubmit(values: z.infer<typeof welcomeForm>) {
|
||||||
|
mutate({
|
||||||
|
first_name: values.firstName,
|
||||||
|
last_name: values.lastName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const formContent = (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 w-full">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="firstName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<Label className="text-xl font-semibold text-[#212122]">
|
||||||
|
{t("Имя")}
|
||||||
|
</Label>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={t("Введите имя")}
|
||||||
|
className="h-[60px] px-4 font-medium !text-lg rounded-xl text-black"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="lastName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<Label className="text-xl font-semibold text-[#212122]">
|
||||||
|
{t("Фамилия")}
|
||||||
|
</Label>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={t("Введите фамилию")}
|
||||||
|
className="h-[60px] px-4 font-medium !text-lg rounded-xl text-black max-lg:w-full"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="px-14 py-8 rounded-4xl text-lg font-medium cursor-pointer bg-[#1764FC] hover:bg-[#1764FC] max-lg:w-full max-lg:mt-10"
|
||||||
>
|
>
|
||||||
Star
|
{isPending ? (
|
||||||
</GitHubButton>
|
<LoaderCircle className="animate-spin" />
|
||||||
<div className="flex flex-row gap-2">
|
) : (
|
||||||
<ModeToggle />
|
t("Сохранить")
|
||||||
<LangToggle />
|
)}
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</Form>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isMobile ? (
|
||||||
|
<Dialog open={open}>
|
||||||
|
<DialogContent
|
||||||
|
className="rounded-4xl !max-w-3xl"
|
||||||
|
showCloseButton={false}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle
|
||||||
|
className={clsx("flex justify-between w-full items-center")}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<p className="text-2xl">{t("Давайте познакомимся!")}</p>
|
||||||
|
<p className="w-[80%] text-[#646465] font-medium">
|
||||||
|
{t(
|
||||||
|
"Чтобы завершить регистрацию, пожалуйста, укажите ваше имя",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
disabled
|
||||||
|
className="rounded-full p-6 h-12 w-12 cursor-pointer"
|
||||||
|
>
|
||||||
|
<XIcon className="w-26 h-26" />
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="flex flex-col justify-center items-center gap-8 mt-5">
|
||||||
|
{formContent}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
) : (
|
||||||
|
<Drawer
|
||||||
|
anchor="bottom"
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderTopRightRadius: 16,
|
||||||
|
width: "100vw",
|
||||||
|
height: "auto",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
p: 2,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="text-3xl font-semibold">
|
||||||
|
{t("Давайте познакомимся!")}
|
||||||
|
</p>
|
||||||
|
<p className="w-[80%] text-[#646465] font-medium">
|
||||||
|
{t("Чтобы завершить регистрацию, пожалуйста, укажите ваше имя")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
disabled
|
||||||
|
className="rounded-full p-6 h-12 w-12 cursor-pointer"
|
||||||
|
>
|
||||||
|
<XIcon className="w-26 h-26" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">{formContent}</div>
|
||||||
|
</Drawer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user