Compare commits
10 Commits
8a4618b454
...
5918da89be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5918da89be | ||
|
|
da46b6cac1 | ||
|
|
394d158947 | ||
|
|
3eddadaad0 | ||
|
|
11f88d1edf | ||
|
|
0f650ad15a | ||
|
|
41fe8725c7 | ||
|
|
fa19172634 | ||
|
|
e09ec2b7d5 | ||
|
|
64f8467f41 |
6
.npmrc
Normal file
6
.npmrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# pnpm configuration
|
||||||
|
|
||||||
|
audit=true
|
||||||
|
ignore-scripts=false
|
||||||
|
strict-ssl=true
|
||||||
|
minimum-release-age=262974
|
||||||
6918
package-lock.json
generated
6918
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
|||||||
"prettier": "prettier src --write",
|
"prettier": "prettier src --write",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
|
"packageManager": "pnpm@9.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.25",
|
||||||
"i18next": "^25.5.2",
|
"i18next": "^25.5.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
|||||||
5127
pnpm-lock.yaml
generated
Normal file
5127
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -26,22 +26,29 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export default function TourAgenciesPage() {
|
export default function TourAgenciesPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const itemsPerPage = 4;
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [currentPage, setCurrentPage] = useState(initialPage);
|
||||||
|
const itemsPerPage = 6;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setCurrentPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
const { data, refetch, isLoading, isError } = useQuery({
|
const { data, refetch, isLoading, isError } = useQuery({
|
||||||
queryKey: ["all_agency", currentPage],
|
queryKey: ["all_agency", currentPage],
|
||||||
queryFn: () => getAllAgency({ page: currentPage, page_size: itemsPerPage }),
|
queryFn: () => getAllAgency({ page: currentPage, page_size: itemsPerPage }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: ({ id }: { id: number }) => {
|
mutationFn: ({ id }: { id: number }) => {
|
||||||
return deleteAgency({ id });
|
return deleteAgency({ id });
|
||||||
},
|
},
|
||||||
@@ -293,7 +300,11 @@ export default function TourAgenciesPage() {
|
|||||||
onClick={() => handleDelete(agency.id)}
|
onClick={() => handleDelete(agency.id)}
|
||||||
className="bg-red-600 hover:bg-red-700 text-white"
|
className="bg-red-600 hover:bg-red-700 text-white"
|
||||||
>
|
>
|
||||||
{t("O'chirish")}
|
{isPending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("O'chirish")
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -305,37 +316,40 @@ export default function TourAgenciesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => updatePage(Math.max(currentPage - 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{[...Array(data?.data.data.total_pages)].map((_, i) => (
|
{[...Array(data?.data.data.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setCurrentPage(i + 1)}
|
onClick={() => updatePage(pageNum)}
|
||||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
currentPage === i + 1
|
currentPage === pageNum
|
||||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{pageNum}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === data?.data.data.total_pages}
|
disabled={currentPage === data?.data.data.total_pages}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentPage((p) =>
|
updatePage(
|
||||||
Math.min(p + 1, data ? data?.data.data.total_pages : 0),
|
Math.min(currentPage + 1, data?.data.data.total_pages ?? 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
updateAgencyStatus,
|
updateAgencyStatus,
|
||||||
} from "@/pages/agencies/lib/api";
|
} from "@/pages/agencies/lib/api";
|
||||||
import { AgencyUsersSection } from "@/pages/agencies/ui/AgencyUsersSection";
|
import { AgencyUsersSection } from "@/pages/agencies/ui/AgencyUsersSection";
|
||||||
|
import { createTourAdmin } from "@/pages/support/lib/api";
|
||||||
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";
|
||||||
@@ -50,6 +51,7 @@ export default function AgencyDetailPage() {
|
|||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [edit, setEdit] = useState<boolean>(false);
|
const [edit, setEdit] = useState<boolean>(false);
|
||||||
|
const [added, setAdded] = 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<{
|
||||||
@@ -105,6 +107,22 @@ export default function AgencyDetailPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createAdmin = useMutation({
|
||||||
|
mutationFn: (id: number) => createTourAdmin(id),
|
||||||
|
onSuccess: (res) => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["agency_user"] });
|
||||||
|
setOpenUser(true);
|
||||||
|
setUser(res.data);
|
||||||
|
setAdded(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
richColors: true,
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const deleteUserMutation = useMutation({
|
const deleteUserMutation = useMutation({
|
||||||
mutationFn: (userId: number) => agencyUserDelete(userId),
|
mutationFn: (userId: number) => agencyUserDelete(userId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -126,6 +144,10 @@ export default function AgencyDetailPage() {
|
|||||||
updateUserMutation.mutate(user);
|
updateUserMutation.mutate(user);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddeUser = () => {
|
||||||
|
createAdmin.mutate(Number(params.id!));
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteUser = (userId: number) => {
|
const handleDeleteUser = (userId: number) => {
|
||||||
deleteUserMutation.mutate(userId);
|
deleteUserMutation.mutate(userId);
|
||||||
};
|
};
|
||||||
@@ -409,8 +431,14 @@ export default function AgencyDetailPage() {
|
|||||||
{agency?.data.data && (
|
{agency?.data.data && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<AgencyUsersSection
|
<AgencyUsersSection
|
||||||
|
agencyId={Number(params.id)}
|
||||||
|
added={added}
|
||||||
|
setAdded={setAdded}
|
||||||
edit={edit}
|
edit={edit}
|
||||||
|
handleAddeUser={handleAddeUser}
|
||||||
setEdit={setEdit}
|
setEdit={setEdit}
|
||||||
|
isPending={updateUserMutation.isPending}
|
||||||
|
createAdminPending={createAdmin.isPending}
|
||||||
users={
|
users={
|
||||||
Array.isArray(agency?.data.data)
|
Array.isArray(agency?.data.data)
|
||||||
? agency?.data.data
|
? agency?.data.data
|
||||||
@@ -419,6 +447,7 @@ export default function AgencyDetailPage() {
|
|||||||
onEdit={handleEditUser}
|
onEdit={handleEditUser}
|
||||||
onDelete={handleDeleteUser}
|
onDelete={handleDeleteUser}
|
||||||
isLoading={isLoadingUsers}
|
isLoading={isLoadingUsers}
|
||||||
|
deletePending={deleteUserMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,7 +13,15 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/shared/ui/dialog";
|
} from "@/shared/ui/dialog";
|
||||||
import { Pencil, Phone, Shield, Trash2, User } from "lucide-react";
|
import {
|
||||||
|
Loader2,
|
||||||
|
Pencil,
|
||||||
|
Phone,
|
||||||
|
Shield,
|
||||||
|
Trash2,
|
||||||
|
User,
|
||||||
|
UserPlus,
|
||||||
|
} from "lucide-react";
|
||||||
import { type Dispatch, type SetStateAction } from "react";
|
import { type Dispatch, type SetStateAction } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -27,22 +35,36 @@ interface AgencyUser {
|
|||||||
|
|
||||||
interface AgencyUsersProps {
|
interface AgencyUsersProps {
|
||||||
users: AgencyUser[];
|
users: AgencyUser[];
|
||||||
|
agencyId: number;
|
||||||
onEdit: (userId: number) => void;
|
onEdit: (userId: number) => void;
|
||||||
|
handleAddeUser: () => void;
|
||||||
onDelete: (userId: number) => void;
|
onDelete: (userId: number) => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
isPending: boolean;
|
||||||
|
createAdminPending: boolean;
|
||||||
edit: boolean;
|
edit: boolean;
|
||||||
|
added: boolean;
|
||||||
|
deletePending: boolean;
|
||||||
setEdit: Dispatch<SetStateAction<boolean>>;
|
setEdit: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setAdded: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgencyUsersSection({
|
export function AgencyUsersSection({
|
||||||
users,
|
users,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
setAdded,
|
||||||
|
added,
|
||||||
edit,
|
edit,
|
||||||
setEdit,
|
setEdit,
|
||||||
|
handleAddeUser,
|
||||||
|
createAdminPending,
|
||||||
onDelete,
|
onDelete,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
deletePending,
|
||||||
|
isPending = 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",
|
||||||
@@ -75,27 +97,10 @@ export function AgencyUsersSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!users || users.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<Card className="border border-gray-700 shadow-lg bg-gray-800">
|
<Card className="border border-gray-700 shadow-lg bg-gray-800">
|
||||||
<CardHeader>
|
<CardHeader className="flex justify-between items-center">
|
||||||
<CardTitle className="text-2xl text-white flex items-center gap-2">
|
<div>
|
||||||
<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">
|
<CardTitle className="text-2xl text-white flex items-center gap-2">
|
||||||
<User className="w-6 h-6" />
|
<User className="w-6 h-6" />
|
||||||
{t("Agentlik foydalanuvchilari")}
|
{t("Agentlik foydalanuvchilari")}
|
||||||
@@ -103,8 +108,54 @@ export function AgencyUsersSection({
|
|||||||
<p className="text-gray-400">
|
<p className="text-gray-400">
|
||||||
{t("Agentlik bilan bog'langan foydalanuvchilar ro'yxati")}
|
{t("Agentlik bilan bog'langan foydalanuvchilar ro'yxati")}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={added} onOpenChange={setAdded}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-blue-400 hover:text-blue-300"
|
||||||
|
>
|
||||||
|
<UserPlus className="w-4 h-4" onClick={() => setAdded(true)} />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="bg-gray-800 border-gray-700 text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("Foydalanuvchi qo'shish")}</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-400">
|
||||||
|
{t("Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz")}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-white"
|
||||||
|
onClick={() => setAdded(false)}
|
||||||
|
>
|
||||||
|
{t("Bekor qilish")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleAddeUser()}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
{createAdminPending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("Qo'shish")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{users.length === 0 ? (
|
||||||
|
<div className="text-center text-gray-400 py-8">
|
||||||
|
{t("Hozircha foydalanuvchilar yo'q")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
<div
|
<div
|
||||||
@@ -138,9 +189,8 @@ export function AgencyUsersSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Edit Confirm Dialog */}
|
{/* ✏️ Edit */}
|
||||||
<Dialog open={edit} onOpenChange={setEdit}>
|
<Dialog open={edit} onOpenChange={setEdit}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -171,7 +221,7 @@ export function AgencyUsersSection({
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-white"
|
className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-white"
|
||||||
onClick={() => setEdit(true)}
|
onClick={() => setEdit(false)}
|
||||||
>
|
>
|
||||||
{t("Bekor qilish")}
|
{t("Bekor qilish")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -179,13 +229,17 @@ export function AgencyUsersSection({
|
|||||||
onClick={() => onEdit(user.id)}
|
onClick={() => onEdit(user.id)}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
>
|
>
|
||||||
{t("Tahrirlash")}
|
{isPending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("Tahrirlash")
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Delete Confirm Dialog */}
|
{/* 🗑 Delete */}
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -219,7 +273,11 @@ export function AgencyUsersSection({
|
|||||||
onClick={() => onDelete(user.id)}
|
onClick={() => onDelete(user.id)}
|
||||||
className="bg-red-600 hover:bg-red-700 text-white"
|
className="bg-red-600 hover:bg-red-700 text-white"
|
||||||
>
|
>
|
||||||
{t("O'chirish")}
|
{deletePending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("O'chirish")
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -229,6 +287,7 @@ export function AgencyUsersSection({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { getDetailAgency, updateAgencyStatus } from "@/pages/agencies/lib/api";
|
import { getDetailAgency, updateAgencyStatus } from "@/pages/agencies/lib/api";
|
||||||
import formatPhone from "@/shared/lib/formatPhone";
|
import formatPhone from "@/shared/lib/formatPhone";
|
||||||
|
import onlyNumber from "@/shared/lib/onlyNumber";
|
||||||
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 {
|
import {
|
||||||
@@ -40,7 +41,7 @@ const formSchema = z.object({
|
|||||||
addres: z.string().min(1, "Manzil kiritish shart"),
|
addres: z.string().min(1, "Manzil kiritish shart"),
|
||||||
email: z.string().email("Email noto‘g‘ri"),
|
email: z.string().email("Email noto‘g‘ri"),
|
||||||
phone: z.string().min(3, "Telefon raqami noto‘g‘ri"),
|
phone: z.string().min(3, "Telefon raqami noto‘g‘ri"),
|
||||||
web_site: z.string().url("URL noto‘g‘ri"),
|
web_site: z.string().min(1, "URL noto‘g‘ri"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
@@ -63,12 +64,12 @@ const EditAgency = () => {
|
|||||||
});
|
});
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data, isPending } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ["detail_agency", params.id],
|
queryKey: ["detail_agency", params.id],
|
||||||
queryFn: () => getDetailAgency({ id: Number(params.id) }),
|
queryFn: () => getDetailAgency({ id: Number(params.id) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: (body: {
|
mutationFn: (body: {
|
||||||
status: "pending" | "approved" | "cancelled";
|
status: "pending" | "approved" | "cancelled";
|
||||||
custom_id?: string;
|
custom_id?: string;
|
||||||
@@ -106,7 +107,7 @@ const EditAgency = () => {
|
|||||||
form.setValue("name", agency.name);
|
form.setValue("name", agency.name);
|
||||||
form.setValue("addres", agency.addres);
|
form.setValue("addres", agency.addres);
|
||||||
form.setValue("email", agency.email);
|
form.setValue("email", agency.email);
|
||||||
form.setValue("phone", agency.phone);
|
form.setValue("phone", onlyNumber(agency.phone));
|
||||||
form.setValue("web_site", agency.web_site);
|
form.setValue("web_site", agency.web_site);
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
@@ -118,7 +119,7 @@ const EditAgency = () => {
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
addres: values.addres,
|
addres: values.addres,
|
||||||
email: values.email,
|
email: values.email,
|
||||||
phone: values.phone,
|
phone: onlyNumber(values.phone),
|
||||||
web_site: values.web_site,
|
web_site: values.web_site,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,17 +25,26 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const EmployeesManagement = () => {
|
const EmployeesManagement = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ["employees", currentPage],
|
queryKey: ["employees", page],
|
||||||
queryFn: () => getAllEmployees({ page: currentPage, page_size: 10 }),
|
queryFn: () => getAllEmployees({ page: page, page_size: 12 }),
|
||||||
select: (data) => data.data.data,
|
select: (data) => data.data.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,37 +202,38 @@ const EmployeesManagement = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={page === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => updatePage(Math.max(page - 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{[...Array(data?.total_pages || 0)].map((_, i) => (
|
{[...Array(data?.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setCurrentPage(i + 1)}
|
onClick={() => updatePage(pageNum)}
|
||||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
currentPage === i + 1
|
page === pageNum
|
||||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{pageNum}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === data?.total_pages}
|
disabled={page === data?.total_pages}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentPage((p) =>
|
updatePage(Math.min(page + 1, data?.total_pages ?? 1))
|
||||||
Math.min(p + 1, 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } 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 { useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
@@ -67,7 +68,10 @@ const faqForm = z.object({
|
|||||||
|
|
||||||
const Faq = () => {
|
const Faq = () => {
|
||||||
const [activeTab, setActiveTab] = useState<string>("");
|
const [activeTab, setActiveTab] = useState<string>("");
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [openModal, setOpenModal] = useState(false);
|
const [openModal, setOpenModal] = useState(false);
|
||||||
const [editFaq, setEditFaq] = useState<number | null>(null);
|
const [editFaq, setEditFaq] = useState<number | null>(null);
|
||||||
@@ -75,6 +79,11 @@ const Faq = () => {
|
|||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const loaderRef = useRef<HTMLDivElement>(null);
|
const loaderRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
// Infinite scroll uchun useInfiniteQuery
|
// Infinite scroll uchun useInfiniteQuery
|
||||||
const {
|
const {
|
||||||
data: categoryData,
|
data: categoryData,
|
||||||
@@ -96,20 +105,16 @@ const Faq = () => {
|
|||||||
initialPageParam: 1,
|
initialPageParam: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentPage(1);
|
|
||||||
}, [activeTab]);
|
|
||||||
|
|
||||||
const category = useMemo(() => {
|
const category = useMemo(() => {
|
||||||
return categoryData?.pages.flatMap((page) => page.data.data.results) ?? [];
|
return categoryData?.pages.flatMap((page) => page.data.data.results) ?? [];
|
||||||
}, [categoryData]);
|
}, [categoryData]);
|
||||||
|
|
||||||
const { data: faq } = useQuery({
|
const { data: faq } = useQuery({
|
||||||
queryKey: ["all_faq", activeTab, currentPage],
|
queryKey: ["all_faq", activeTab, page],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
return getAllFaq({
|
return getAllFaq({
|
||||||
page: currentPage,
|
page,
|
||||||
page_size: 10,
|
page_size: 1,
|
||||||
category: Number(activeTab),
|
category: Number(activeTab),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -301,7 +306,14 @@ const Faq = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setActiveTab(value);
|
||||||
|
updatePage(1);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<TabsList
|
<TabsList
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
@@ -390,35 +402,36 @@ const Faq = () => {
|
|||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={page === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => updatePage(Math.max(page - 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{[...Array(faq?.total_pages)].map((_, i) => (
|
{[...Array(faq?.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setCurrentPage(i + 1)}
|
onClick={() => updatePage(pageNum)}
|
||||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
currentPage === i + 1
|
page === pageNum
|
||||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{pageNum}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === faq?.total_pages}
|
disabled={page === faq?.total_pages}
|
||||||
onClick={() =>
|
onClick={() => updatePage(Math.min(page + 1, faq?.total_pages ?? 1))}
|
||||||
setCurrentPage((p) => Math.min(p + 1, faq ? faq.total_pages : 0))
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
}
|
|
||||||
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" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } 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 { useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
@@ -55,13 +56,22 @@ const categoryFormSchema = z.object({
|
|||||||
|
|
||||||
const FaqCategory = () => {
|
const FaqCategory = () => {
|
||||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
const [categories, setCategories] = useState<number | null>(null);
|
const [categories, setCategories] = useState<number | null>(null);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
const { data: category } = useQuery({
|
const { data: category } = useQuery({
|
||||||
queryKey: ["all_faqcategory", currentPage],
|
queryKey: ["all_faqcategory", page],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
return getAllFaqCategory({ page: currentPage, page_size: 10 });
|
return getAllFaqCategory({ page: page, page_size: 20 });
|
||||||
},
|
},
|
||||||
select(data) {
|
select(data) {
|
||||||
return data.data.data;
|
return data.data.data;
|
||||||
@@ -255,37 +265,38 @@ const FaqCategory = () => {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={page === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => updatePage(Math.max(page - 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{[...Array(category?.total_pages)].map((_, i) => (
|
{[...Array(category?.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setCurrentPage(i + 1)}
|
onClick={() => updatePage(pageNum)}
|
||||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
currentPage === i + 1
|
page === pageNum
|
||||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{pageNum}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === category?.total_pages}
|
disabled={page === category?.total_pages}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentPage((p) =>
|
updatePage(Math.min(page + 1, category?.total_pages ?? 1))
|
||||||
Math.min(p + 1, category ? category.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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -40,28 +40,23 @@ export default function FinancePage({ user }: { user: Role }) {
|
|||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const tabParam = searchParams.get("tab") as "bookings" | "agencies" | null;
|
const tabParam = searchParams.get("tab") as "bookings" | "agencies" | null;
|
||||||
const pageParam = Number(searchParams.get("page")) || 1;
|
const pageParam = Number(searchParams.get("page")) || 1;
|
||||||
|
const filterParam = searchParams.get("filter") ?? "";
|
||||||
const pageAgencyParam = Number(searchParams.get("page_agency")) || 1;
|
const pageAgencyParam = Number(searchParams.get("page_agency")) || 1;
|
||||||
const [currentPage, setCurrentPage] = useState(pageParam);
|
const [currentPage, setCurrentPage] = useState(pageParam);
|
||||||
const [currentPageAgency, setCurrentPageAgency] = useState(pageAgencyParam);
|
const [currentPageAgency, setCurrentPageAgency] = useState(pageAgencyParam);
|
||||||
const [tab, setTab] = useState<"bookings" | "agencies">(
|
const [tab, setTab] = useState<"bookings" | "agencies">(
|
||||||
tabParam ?? "bookings",
|
tabParam ?? "bookings",
|
||||||
);
|
);
|
||||||
const [filterStatus, setFilterStatus] = useState<
|
const [filterStatus, setFilterStatus] = useState(filterParam);
|
||||||
| ""
|
|
||||||
| "pending_payment"
|
|
||||||
| "pending_confirmation"
|
|
||||||
| "confirmed"
|
|
||||||
| "completed"
|
|
||||||
| "cancelled"
|
|
||||||
>("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearchParams({
|
setSearchParams({
|
||||||
tab,
|
tab,
|
||||||
page: String(currentPage),
|
page: String(currentPage),
|
||||||
page_agency: String(currentPageAgency),
|
page_agency: String(currentPageAgency),
|
||||||
|
filter: String(filterStatus),
|
||||||
});
|
});
|
||||||
}, [tab, currentPage, currentPageAgency, setSearchParams]);
|
}, [tab, currentPage, currentPageAgency, setSearchParams, filterStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tabParam && tabParam !== tab) {
|
if (tabParam && tabParam !== tab) {
|
||||||
@@ -70,17 +65,24 @@ export default function FinancePage({ user }: { user: Role }) {
|
|||||||
}, [tabParam]);
|
}, [tabParam]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(1);
|
if (filterParam === "null") {
|
||||||
setCurrentPageAgency(1);
|
setFilterStatus("");
|
||||||
}, [filterStatus, tab]);
|
}
|
||||||
|
}, [filterParam]);
|
||||||
|
|
||||||
const { data, isLoading, isError, refetch } = useQuery({
|
const { data, isLoading, isError, refetch } = useQuery({
|
||||||
queryKey: ["list_order_user", currentPage, filterStatus],
|
queryKey: ["list_order_user", currentPage, filterStatus],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
getAllOrder({
|
getAllOrder({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: 10,
|
page_size: 12,
|
||||||
order_status: filterStatus,
|
order_status: filterStatus as
|
||||||
|
| ""
|
||||||
|
| "pending_payment"
|
||||||
|
| "pending_confirmation"
|
||||||
|
| "confirmed"
|
||||||
|
| "completed"
|
||||||
|
| "cancelled",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,7 +96,7 @@ export default function FinancePage({ user }: { user: Role }) {
|
|||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
getAllOrderAgecy({
|
getAllOrderAgecy({
|
||||||
page: currentPageAgency,
|
page: currentPageAgency,
|
||||||
page_size: 10,
|
page_size: 12,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -277,7 +279,7 @@ export default function FinancePage({ user }: { user: Role }) {
|
|||||||
? "bg-blue-600 text-white shadow-md"
|
? "bg-blue-600 text-white shadow-md"
|
||||||
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
setFilterStatus(
|
setFilterStatus(
|
||||||
s as
|
s as
|
||||||
| ""
|
| ""
|
||||||
@@ -286,8 +288,9 @@ export default function FinancePage({ user }: { user: Role }) {
|
|||||||
| "confirmed"
|
| "confirmed"
|
||||||
| "completed"
|
| "completed"
|
||||||
| "cancelled",
|
| "cancelled",
|
||||||
)
|
);
|
||||||
}
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{s === ""
|
{s === ""
|
||||||
? t("Barcha bandlovlar")
|
? t("Barcha bandlovlar")
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const newsPostForm = z.object({
|
|||||||
sections: z
|
sections: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
id: z.number().optional(),
|
||||||
image: z.union([z.instanceof(File), z.string()]).optional(),
|
image: z.union([z.instanceof(File), z.string()]).optional(),
|
||||||
text: z.string().optional(),
|
text: z.string().optional(),
|
||||||
text_ru: z.string().optional(),
|
text_ru: z.string().optional(),
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export interface NewsAll {
|
|||||||
name_ru: string;
|
name_ru: string;
|
||||||
}[];
|
}[];
|
||||||
post_images: {
|
post_images: {
|
||||||
|
id: number;
|
||||||
image: string;
|
image: string;
|
||||||
text: string;
|
text: string;
|
||||||
text_ru: string;
|
text_ru: string;
|
||||||
@@ -109,6 +110,7 @@ export interface NewsDetail {
|
|||||||
];
|
];
|
||||||
post_images: [
|
post_images: [
|
||||||
{
|
{
|
||||||
|
id: number;
|
||||||
image: string;
|
image: string;
|
||||||
text: string;
|
text: string;
|
||||||
text_ru: string;
|
text_ru: string;
|
||||||
|
|||||||
@@ -26,22 +26,31 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const News = () => {
|
const News = () => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: allNews,
|
data: allNews,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["all_news", currentPage],
|
queryKey: ["all_news", page],
|
||||||
queryFn: () => getAllNews({ page: currentPage, page_size: 10 }),
|
queryFn: () => getAllNews({ page, page_size: 12 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: deleteMutate, isPending } = useMutation({
|
const { mutate: deleteMutate, isPending } = useMutation({
|
||||||
@@ -303,37 +312,38 @@ const News = () => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex justify-end gap-2 w-[90%] mx-auto mt-8">
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={page === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => updatePage(Math.max(page - 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{[...Array(allNews?.data.data.total_pages)].map((_, i) => (
|
{[...Array(allNews?.data.data.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setCurrentPage(i + 1)}
|
onClick={() => updatePage(pageNum)}
|
||||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
currentPage === i + 1
|
page === pageNum
|
||||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{pageNum}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === allNews?.data.data.total_pages}
|
disabled={page === allNews?.data.data.total_pages}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentPage((p) =>
|
updatePage(Math.min(page + 1, allNews?.data.data.total_pages ?? 1))
|
||||||
Math.min(p + 1, allNews ? allNews?.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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const NewsCategory = () => {
|
|||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutate: added } = useMutation({
|
const { mutate: added, isPending } = useMutation({
|
||||||
mutationFn: (body: { name: string; name_ru: string }) =>
|
mutationFn: (body: { name: string; name_ru: string }) =>
|
||||||
addNewsCategory(body),
|
addNewsCategory(body),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -111,7 +111,7 @@ const NewsCategory = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: edit } = useMutation({
|
const { mutate: edit, isPending: editPending } = useMutation({
|
||||||
mutationFn: ({
|
mutationFn: ({
|
||||||
body,
|
body,
|
||||||
id,
|
id,
|
||||||
@@ -349,7 +349,11 @@ const NewsCategory = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="bg-blue-600 px-5 py-5 hover:bg-blue-700 text-white mt-4 cursor-pointer"
|
className="bg-blue-600 px-5 py-5 hover:bg-blue-700 text-white mt-4 cursor-pointer"
|
||||||
>
|
>
|
||||||
{t("Saqlash")}
|
{isPending || editPending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("Saqlash")
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { Label } from "@/shared/ui/label";
|
|||||||
import { Textarea } from "@/shared/ui/textarea";
|
import { Textarea } from "@/shared/ui/textarea";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { ImagePlus, PlusCircle, Trash2 } from "lucide-react";
|
import { ImagePlus, Loader2, PlusCircle, Trash2 } from "lucide-react";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -45,6 +45,7 @@ interface Data {
|
|||||||
name_uz: string;
|
name_uz: string;
|
||||||
}>;
|
}>;
|
||||||
post_images: Array<{
|
post_images: Array<{
|
||||||
|
id: number;
|
||||||
image: string;
|
image: string;
|
||||||
text: string;
|
text: string;
|
||||||
text_ru: string;
|
text_ru: string;
|
||||||
@@ -65,6 +66,7 @@ const StepTwo = ({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { stepOneData, resetStepOneData } = useNewsStore();
|
const { stepOneData, resetStepOneData } = useNewsStore();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const deletedSections = useRef<number[]>([]);
|
||||||
|
|
||||||
const form = useForm<NewsPostFormType>({
|
const form = useForm<NewsPostFormType>({
|
||||||
resolver: zodResolver(newsPostForm),
|
resolver: zodResolver(newsPostForm),
|
||||||
@@ -79,13 +81,6 @@ const StepTwo = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (detail && !hasReset.current) {
|
if (detail && !hasReset.current) {
|
||||||
const mappedSections =
|
|
||||||
detail.post_images?.map((img) => ({
|
|
||||||
image: img.image,
|
|
||||||
text: img.text_uz,
|
|
||||||
text_ru: img.text_ru,
|
|
||||||
})) ?? [];
|
|
||||||
|
|
||||||
const mappedTags =
|
const mappedTags =
|
||||||
detail.post_tags?.map((t) => ({
|
detail.post_tags?.map((t) => ({
|
||||||
name: t.name_uz,
|
name: t.name_uz,
|
||||||
@@ -99,15 +94,29 @@ const StepTwo = ({
|
|||||||
post_tags:
|
post_tags:
|
||||||
mappedTags.length > 0 ? mappedTags : [{ name: "", name_ru: "" }],
|
mappedTags.length > 0 ? mappedTags : [{ name: "", name_ru: "" }],
|
||||||
sections:
|
sections:
|
||||||
mappedSections.length > 0
|
detail.post_images?.map((img) => ({
|
||||||
? mappedSections
|
id: img.id,
|
||||||
: [{ image: "", text: "", text_ru: "" }],
|
image: img.image,
|
||||||
|
text: img.text_uz,
|
||||||
|
text_ru: img.text_ru,
|
||||||
|
})) ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
hasReset.current = true;
|
hasReset.current = true;
|
||||||
}
|
}
|
||||||
}, [detail, form]);
|
}, [detail, form]);
|
||||||
|
|
||||||
|
const handleRemoveSection = (index: number) => {
|
||||||
|
const section = form.getValues(`sections.${index}`);
|
||||||
|
|
||||||
|
if (section?.id) {
|
||||||
|
deletedSections.current.push(section.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formdan o'chiramiz
|
||||||
|
removeSection(index);
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fields: sectionFields,
|
fields: sectionFields,
|
||||||
append: appendSection,
|
append: appendSection,
|
||||||
@@ -134,7 +143,7 @@ const StepTwo = ({
|
|||||||
if (file) form.setValue(`sections.${index}.image`, file);
|
if (file) form.setValue(`sections.${index}.image`, file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutate: added } = useMutation({
|
const { mutate: added, isPending } = useMutation({
|
||||||
mutationFn: (body: FormData) => addNews(body),
|
mutationFn: (body: FormData) => addNews(body),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.refetchQueries({ queryKey: ["all_news"] });
|
queryClient.refetchQueries({ queryKey: ["all_news"] });
|
||||||
@@ -150,7 +159,7 @@ const StepTwo = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: update } = useMutation({
|
const { mutate: update, isPending: updatePending } = useMutation({
|
||||||
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
||||||
updateNews({ id, body }),
|
updateNews({ id, body }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -172,8 +181,8 @@ const StepTwo = ({
|
|||||||
|
|
||||||
formData.append("title", stepOneData.title);
|
formData.append("title", stepOneData.title);
|
||||||
formData.append("title_ru", stepOneData.title_ru);
|
formData.append("title_ru", stepOneData.title_ru);
|
||||||
formData.append("text", stepOneData.desc);
|
formData.append("text", values.desc);
|
||||||
formData.append("text_ru", stepOneData.desc_ru);
|
formData.append("text_ru", values.desc_ru);
|
||||||
formData.append("is_public", values.is_public === "no" ? "false" : "true");
|
formData.append("is_public", values.is_public === "no" ? "false" : "true");
|
||||||
formData.append("category", String(stepOneData.category));
|
formData.append("category", String(stepOneData.category));
|
||||||
|
|
||||||
@@ -181,27 +190,44 @@ const StepTwo = ({
|
|||||||
formData.append("image", stepOneData.banner);
|
formData.append("image", stepOneData.banner);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sections
|
|
||||||
values.sections?.forEach((section, i) => {
|
values.sections?.forEach((section, i) => {
|
||||||
if (section.image instanceof File)
|
if (section.id) {
|
||||||
formData.append(`post_images[${i}]`, section.image);
|
formData.append(`updates[${i}]id`, String(section.id));
|
||||||
if (section.text) formData.append(`post_text[${i}]`, section.text);
|
|
||||||
|
if (section.text) formData.append(`updates[${i}]text`, section.text);
|
||||||
|
|
||||||
if (section.text_ru)
|
if (section.text_ru)
|
||||||
formData.append(`post_text_ru[${i}]`, section.text_ru);
|
formData.append(`updates[${i}]text_ru`, section.text_ru);
|
||||||
|
|
||||||
|
if (section.image instanceof File) {
|
||||||
|
formData.append(`updates[${i}]image`, section.image);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (section.image instanceof File)
|
||||||
|
formData.append(`post_images`, section.image);
|
||||||
|
|
||||||
|
if (section.text) formData.append(`post_text`, section.text);
|
||||||
|
|
||||||
|
if (section.text_ru) formData.append(`post_text_ru`, section.text_ru);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deletedSections.current.forEach((id) => {
|
||||||
|
formData.append(`delete_list`, String(id));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Post Tags
|
|
||||||
values.post_tags.forEach((tag, i) => {
|
values.post_tags.forEach((tag, i) => {
|
||||||
formData.append(`post_tags[${i}]name`, tag.name);
|
formData.append(`post_tags[${i}]name`, tag.name);
|
||||||
formData.append(`post_tags[${i}]name_ru`, tag.name_ru);
|
formData.append(`post_tags[${i}]name_ru`, tag.name_ru);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (id) update({ body: formData, id: Number(id) });
|
if (id) {
|
||||||
else added(formData);
|
update({ body: formData, id: Number(id) });
|
||||||
|
} else {
|
||||||
|
added(formData);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(form.formState.errors);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -276,7 +302,7 @@ const StepTwo = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeTag(i)}
|
onClick={() => removeTag(i)}
|
||||||
className="text-red-400 hover:text-red-500 mt-1"
|
className="text-red-400 hover:text-red-500"
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -305,7 +331,7 @@ const StepTwo = ({
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeSection(index)}
|
onClick={() => handleRemoveSection(index)}
|
||||||
className="text-red-400 hover:text-red-500"
|
className="text-red-400 hover:text-red-500"
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
@@ -404,7 +430,11 @@ const StepTwo = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 cursor-pointer"
|
className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 cursor-pointer"
|
||||||
>
|
>
|
||||||
{t("Saqlash")}
|
{isPending || updatePending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("Saqlash")
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export default function WithdrawRequests({
|
export default function WithdrawRequests({
|
||||||
@@ -53,7 +54,16 @@ export default function WithdrawRequests({
|
|||||||
| "user";
|
| "user";
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
const [statusFilter, setStatusFilter] = useState<
|
const [statusFilter, setStatusFilter] = useState<
|
||||||
"pending" | "approved" | "cancelled" | ""
|
"pending" | "approved" | "cancelled" | ""
|
||||||
>("");
|
>("");
|
||||||
@@ -70,11 +80,11 @@ export default function WithdrawRequests({
|
|||||||
const [closeId, setCloseId] = useState<number | null>(null);
|
const [closeId, setCloseId] = useState<number | null>(null);
|
||||||
|
|
||||||
const { data, isLoading, isError, refetch } = useQuery({
|
const { data, isLoading, isError, refetch } = useQuery({
|
||||||
queryKey: ["withdraw-requests", currentPage, statusFilter],
|
queryKey: ["withdraw-requests", page, statusFilter],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
getPayoutList({
|
getPayoutList({
|
||||||
page: currentPage,
|
page,
|
||||||
page_size: 10,
|
page_size: 20,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -178,7 +188,7 @@ export default function WithdrawRequests({
|
|||||||
key={tab.value}
|
key={tab.value}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStatusFilter(tab.value);
|
setStatusFilter(tab.value);
|
||||||
setCurrentPage(1);
|
updatePage(1);
|
||||||
}}
|
}}
|
||||||
className={`rounded-xl px-4 py-2 font-medium transition-all ${
|
className={`rounded-xl px-4 py-2 font-medium transition-all ${
|
||||||
statusFilter === tab.value
|
statusFilter === tab.value
|
||||||
@@ -360,37 +370,38 @@ export default function WithdrawRequests({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 🔹 PAGINATION */}
|
{/* 🔹 PAGINATION */}
|
||||||
<div className="flex justify-end gap-2 mt-5">
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={page === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => updatePage(Math.max(page - 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{[...Array(data?.data.data.total_pages)].map((_, i) => (
|
{[...Array(data?.data.data.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setCurrentPage(i + 1)}
|
onClick={() => updatePage(pageNum)}
|
||||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
currentPage === i + 1
|
page === pageNum
|
||||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{pageNum}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === data?.data.data.total_pages}
|
disabled={page === data?.data.data.total_pages}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentPage((p) =>
|
updatePage(Math.min(page + 1, data?.data.data.total_pages ?? 1))
|
||||||
Math.min(p + 1, data ? data?.data.data.total_pages : 0),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } 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 { useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -92,15 +93,24 @@ const positions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const SiteBannerAdmin = () => {
|
const SiteBannerAdmin = () => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: banner,
|
data: banner,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["all_banner", currentPage],
|
queryKey: ["all_banner", page],
|
||||||
queryFn: () => getBanner({ page: currentPage, page_size: 10 }),
|
queryFn: () => getBanner({ page, page_size: 20 }),
|
||||||
select(data) {
|
select(data) {
|
||||||
return data.data.data;
|
return data.data.data;
|
||||||
},
|
},
|
||||||
@@ -385,37 +395,38 @@ const SiteBannerAdmin = () => {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-5">
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={page === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => updatePage(Math.max(page - 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{[...Array(banner?.total_pages)].map((_, i) => (
|
{[...Array(banner?.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setCurrentPage(i + 1)}
|
onClick={() => updatePage(pageNum)}
|
||||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
currentPage === i + 1
|
page === pageNum
|
||||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{pageNum}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === banner?.total_pages}
|
disabled={page === banner?.total_pages}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentPage((p) =>
|
updatePage(Math.min(page + 1, banner?.total_pages ?? 1))
|
||||||
Math.min(p + 1, banner ? banner?.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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -15,15 +15,25 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/shared/ui/dialog";
|
} from "@/shared/ui/dialog";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AlertTriangle, Loader2, XIcon } from "lucide-react";
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
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, useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const SupportAgency = () => {
|
const SupportAgency = () => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
const [openUser, setOpenUser] = useState<boolean>(false);
|
const [openUser, setOpenUser] = useState<boolean>(false);
|
||||||
const [user, setUser] = useState<{
|
const [user, setUser] = useState<{
|
||||||
status: boolean;
|
status: boolean;
|
||||||
@@ -38,10 +48,15 @@ const SupportAgency = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selected, setSelected] = useState<GetSupportAgencyRes | null>(null);
|
const [selected, setSelected] = useState<GetSupportAgencyRes | null>(null);
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
const { data, isLoading, isError, refetch } = useQuery({
|
const { data, isLoading, isError, refetch } = useQuery({
|
||||||
queryKey: ["support_agency"],
|
queryKey: ["support_agency", page],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
getSupportAgency({ page: 1, page_size: 10, search: "", status: "" }),
|
getSupportAgency({ page: page, page_size: 12, search: "", status: "" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: updateTours } = useMutation({
|
const { mutate: updateTours } = useMutation({
|
||||||
@@ -117,7 +132,10 @@ const SupportAgency = () => {
|
|||||||
<div className="flex gap-3 mb-6">
|
<div className="flex gap-3 mb-6">
|
||||||
<input
|
<input
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setQuery(e.target.value);
|
||||||
|
updatePage(1);
|
||||||
|
}}
|
||||||
placeholder={t("Qidiruv (ism, email yoki telefon)...")}
|
placeholder={t("Qidiruv (ism, email yoki telefon)...")}
|
||||||
className="flex-1 p-2 border rounded-md focus:outline-none focus:ring"
|
className="flex-1 p-2 border rounded-md focus:outline-none focus:ring"
|
||||||
/>
|
/>
|
||||||
@@ -188,6 +206,43 @@ const SupportAgency = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
|
<button
|
||||||
|
disabled={page === 1}
|
||||||
|
onClick={() => updatePage(Math.max(page - 1, 1))}
|
||||||
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{[...Array(data?.data.data.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => updatePage(pageNum)}
|
||||||
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
|
page === pageNum
|
||||||
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={page === data?.data.data.total_pages}
|
||||||
|
onClick={() =>
|
||||||
|
updatePage(Math.min(page + 1, data?.data.data.total_pages ?? 1))
|
||||||
|
}
|
||||||
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{selected && (
|
{selected && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4"
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
AllAmenitiesData,
|
AllAmenitiesData,
|
||||||
|
CountryDeatil,
|
||||||
|
CountryList,
|
||||||
CreateTourRes,
|
CreateTourRes,
|
||||||
DetailAmenitiesData,
|
DetailAmenitiesData,
|
||||||
GetAllTours,
|
GetAllTours,
|
||||||
@@ -18,10 +20,13 @@ import type {
|
|||||||
HotelAllFeaturesType,
|
HotelAllFeaturesType,
|
||||||
HotelFeaturesDetail,
|
HotelFeaturesDetail,
|
||||||
HotelFeaturesTypeDetail,
|
HotelFeaturesTypeDetail,
|
||||||
|
HotelMealdetail,
|
||||||
|
HotelMealList,
|
||||||
} from "@/pages/tours/lib/type";
|
} from "@/pages/tours/lib/type";
|
||||||
import httpClient from "@/shared/config/api/httpClient";
|
import httpClient from "@/shared/config/api/httpClient";
|
||||||
import {
|
import {
|
||||||
AMENITIES,
|
AMENITIES,
|
||||||
|
COUNTRY,
|
||||||
GET_TICKET,
|
GET_TICKET,
|
||||||
HOTEL,
|
HOTEL,
|
||||||
HOTEL_BADGE,
|
HOTEL_BADGE,
|
||||||
@@ -29,7 +34,9 @@ import {
|
|||||||
HOTEL_FEATURES_TYPE,
|
HOTEL_FEATURES_TYPE,
|
||||||
HOTEL_TARIF,
|
HOTEL_TARIF,
|
||||||
HPTEL_TYPES,
|
HPTEL_TYPES,
|
||||||
|
MEAL_PLAN,
|
||||||
POPULAR_TOURS,
|
POPULAR_TOURS,
|
||||||
|
REGION,
|
||||||
TOUR_TRANSPORT,
|
TOUR_TRANSPORT,
|
||||||
} from "@/shared/config/api/URLs";
|
} from "@/shared/config/api/URLs";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
@@ -67,7 +74,7 @@ const getDetailToursId = async ({
|
|||||||
}: {
|
}: {
|
||||||
id: number;
|
id: number;
|
||||||
}): Promise<AxiosResponse<GetDetailTours>> => {
|
}): Promise<AxiosResponse<GetDetailTours>> => {
|
||||||
const response = await httpClient.get(`tickets/${id}/`);
|
const response = await httpClient.get(`${GET_TICKET}${id}/`);
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -500,10 +507,124 @@ const amenitiesUpdate = async ({
|
|||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const countryList = async (params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
name?: string;
|
||||||
|
}): Promise<AxiosResponse<CountryList>> => {
|
||||||
|
const res = await httpClient.get(COUNTRY, { params });
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const countryDeatil = async (
|
||||||
|
id: number,
|
||||||
|
): Promise<AxiosResponse<CountryDeatil>> => {
|
||||||
|
const res = await httpClient.get(`${COUNTRY}${id}/`);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const countryCreate = async (body: { name: string; name_ru: string }) => {
|
||||||
|
const res = await httpClient.post(`${COUNTRY}`, body);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
const countryUpdate = async ({
|
||||||
|
body,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: { name: string; name_ru: string };
|
||||||
|
}) => {
|
||||||
|
const res = await httpClient.patch(`${COUNTRY}${id}/`, body);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const countryDelete = async (id: number) => {
|
||||||
|
const res = await httpClient.delete(`${COUNTRY}${id}/`);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const regionList = async (params: {
|
||||||
|
page: number;
|
||||||
|
name?: string;
|
||||||
|
country: number;
|
||||||
|
page_size: number;
|
||||||
|
}): Promise<AxiosResponse<CountryList>> => {
|
||||||
|
const res = await httpClient.get(REGION, { params });
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const regionDeatil = async (
|
||||||
|
id: number,
|
||||||
|
): Promise<AxiosResponse<CountryDeatil>> => {
|
||||||
|
const res = await httpClient.get(`${REGION}${id}/`);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const regionCreate = async (body: {
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
country: number;
|
||||||
|
}) => {
|
||||||
|
const res = await httpClient.post(`${REGION}`, body);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
const regionUpdate = async ({
|
||||||
|
body,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: { name: string; name_ru: string; country: number };
|
||||||
|
}) => {
|
||||||
|
const res = await httpClient.patch(`${REGION}${id}/`, body);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hotelMealList = async (params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
name?: string;
|
||||||
|
}): Promise<AxiosResponse<HotelMealList>> => {
|
||||||
|
const res = await httpClient.get(`${MEAL_PLAN}`, { params });
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hotelMealDetail = async (
|
||||||
|
id: number,
|
||||||
|
): Promise<AxiosResponse<HotelMealdetail>> => {
|
||||||
|
const res = await httpClient.get(`${MEAL_PLAN}${id}/`);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hotelMealDelete = async (id: number) => {
|
||||||
|
const res = await httpClient.delete(`${MEAL_PLAN}${id}/`);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hotelMealCreate = async (body: { name: string; name_ru: string }) => {
|
||||||
|
const res = await httpClient.post(`${MEAL_PLAN}`, body);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hotelMealUpdate = async ({
|
||||||
|
body,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: { name: string; name_ru: string };
|
||||||
|
}) => {
|
||||||
|
const res = await httpClient.patch(`${MEAL_PLAN}${id}/`, body);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
addedPopularTours,
|
addedPopularTours,
|
||||||
amenitiesCreate,
|
amenitiesCreate,
|
||||||
amenitiesUpdate,
|
amenitiesUpdate,
|
||||||
|
countryCreate,
|
||||||
|
countryDeatil,
|
||||||
|
countryDelete,
|
||||||
|
countryList,
|
||||||
|
countryUpdate,
|
||||||
createHotel,
|
createHotel,
|
||||||
createTours,
|
createTours,
|
||||||
deleteAmenities,
|
deleteAmenities,
|
||||||
@@ -530,6 +651,11 @@ export {
|
|||||||
hotelFeatureTypeDetail,
|
hotelFeatureTypeDetail,
|
||||||
hotelFeatureTypeUpdate,
|
hotelFeatureTypeUpdate,
|
||||||
hotelFeatureUpdate,
|
hotelFeatureUpdate,
|
||||||
|
hotelMealCreate,
|
||||||
|
hotelMealDelete,
|
||||||
|
hotelMealDetail,
|
||||||
|
hotelMealList,
|
||||||
|
hotelMealUpdate,
|
||||||
hotelTarfiDetail,
|
hotelTarfiDetail,
|
||||||
hotelTarif,
|
hotelTarif,
|
||||||
hotelTarifCreate,
|
hotelTarifCreate,
|
||||||
@@ -545,5 +671,9 @@ export {
|
|||||||
hotelTypeDelete,
|
hotelTypeDelete,
|
||||||
hotelTypeDetail,
|
hotelTypeDetail,
|
||||||
hotelTypeUpdate,
|
hotelTypeUpdate,
|
||||||
|
regionCreate,
|
||||||
|
regionDeatil,
|
||||||
|
regionList,
|
||||||
|
regionUpdate,
|
||||||
updateTours,
|
updateTours,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
import type {
|
import type {
|
||||||
AllAmenitiesDataRes,
|
AllAmenitiesDataRes,
|
||||||
Badge,
|
Badge,
|
||||||
|
CountryLisResult,
|
||||||
HotelFeatures,
|
HotelFeatures,
|
||||||
HotelFeaturesType,
|
HotelFeaturesType,
|
||||||
|
HotelMealListData,
|
||||||
Tarif,
|
Tarif,
|
||||||
Transport,
|
Transport,
|
||||||
Type,
|
Type,
|
||||||
@@ -364,3 +366,111 @@ export const AmenitiesColumns = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const CountryColumns = (
|
||||||
|
onEdit: (id: number) => void,
|
||||||
|
onDelete: (id: number) => void,
|
||||||
|
t: (key: string) => string,
|
||||||
|
setActiveTab?: Dispatch<SetStateAction<string>>,
|
||||||
|
setFeatureId?: Dispatch<SetStateAction<number | null>>,
|
||||||
|
): ColumnDef<CountryLisResult>[] => [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "ID",
|
||||||
|
cell: ({ row }) => <span>{row.original.id}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: t("Nomi"),
|
||||||
|
cell: ({ row }) => <span>{row.original.name_uz}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: () => <div>{t("Nomi")} (ru)</div>,
|
||||||
|
cell: ({ row }) => <span>{row.original.name_ru}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: () => <div className="text-right">{t("Harakatlar")}</div>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
{setActiveTab && setFeatureId && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab("region");
|
||||||
|
setFeatureId(row.original.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("Ko'rish")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onEdit(row.original.id)}
|
||||||
|
>
|
||||||
|
{t("Tahrirlash")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => onDelete(row.original.id)}
|
||||||
|
>
|
||||||
|
{t("O'chirish")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MealColumns = (
|
||||||
|
onEdit: (id: number) => void,
|
||||||
|
onDelete: (id: number) => void,
|
||||||
|
t: (key: string) => string,
|
||||||
|
): ColumnDef<HotelMealListData>[] => [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "ID",
|
||||||
|
cell: ({ row }) => <span>{row.original.id}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: t("Nomi"),
|
||||||
|
cell: ({ row }) => <span>{row.original.name_uz}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: () => <div>{t("Nomi")} (ru)</div>,
|
||||||
|
cell: ({ row }) => <span>{row.original.name_ru}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: () => <div className="text-right">{t("Harakatlar")}</div>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onEdit(row.original.id)}
|
||||||
|
>
|
||||||
|
{t("Tahrirlash")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => onDelete(row.original.id)}
|
||||||
|
>
|
||||||
|
{t("O'chirish")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -23,18 +23,18 @@ export const TourformSchema = z.object({
|
|||||||
max_person: z.string().min(1, {
|
max_person: z.string().min(1, {
|
||||||
message: "Kamida 1 yo'lovchi bo'lishi kerak.",
|
message: "Kamida 1 yo'lovchi bo'lishi kerak.",
|
||||||
}),
|
}),
|
||||||
departure: z.string().min(2, {
|
departure: z.string().min(1, {
|
||||||
message: "Ketish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.",
|
message: "Ketish joyi eng kamida 1 ta belgidan iborat bo'lishi kerak.",
|
||||||
}),
|
}),
|
||||||
departure_ru: z.string().min(2, {
|
// departure_ru: z.string().min(2, {
|
||||||
message: "Ketish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.",
|
// message: "Ketish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.",
|
||||||
}),
|
// }),
|
||||||
destination: z.string().min(2, {
|
destination: z.string().min(1, {
|
||||||
message: "Borish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.",
|
message: "Borish joyi eng kamida 1 ta belgidan iborat bo'lishi kerak.",
|
||||||
}),
|
|
||||||
destination_ru: z.string().min(2, {
|
|
||||||
message: "Borish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.",
|
|
||||||
}),
|
}),
|
||||||
|
// destination_ru: z.string().min(2, {
|
||||||
|
// message: "Borish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.",
|
||||||
|
// }),
|
||||||
location_name: z.string().min(2, {
|
location_name: z.string().min(2, {
|
||||||
message: "Eng kamida 2 ta belgidan iborat bo'lishi kerak.",
|
message: "Eng kamida 2 ta belgidan iborat bo'lishi kerak.",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -11,7 +11,18 @@ export interface GetAllTours {
|
|||||||
current_page: number;
|
current_page: number;
|
||||||
results: {
|
results: {
|
||||||
id: number;
|
id: number;
|
||||||
destination: string;
|
destination:
|
||||||
|
| {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
country: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| string;
|
||||||
featured_tickets: boolean;
|
featured_tickets: boolean;
|
||||||
duration_days: number;
|
duration_days: number;
|
||||||
hotel_name: string;
|
hotel_name: string;
|
||||||
@@ -35,12 +46,26 @@ export interface GetOneTours {
|
|||||||
price: number;
|
price: number;
|
||||||
min_person: number;
|
min_person: number;
|
||||||
max_person: number;
|
max_person: number;
|
||||||
departure: string;
|
departure: {
|
||||||
departure_ru: string;
|
id: number;
|
||||||
departure_uz: string;
|
name: string;
|
||||||
destination: string;
|
name_ru: string;
|
||||||
destination_ru: string;
|
country: {
|
||||||
destination_uz: string;
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
destination: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
country: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
departure_time: string;
|
departure_time: string;
|
||||||
travel_time: string;
|
travel_time: string;
|
||||||
location_name: string;
|
location_name: string;
|
||||||
@@ -151,10 +176,10 @@ export interface CreateTourRes {
|
|||||||
price: number;
|
price: number;
|
||||||
min_person: number;
|
min_person: number;
|
||||||
max_person: number;
|
max_person: number;
|
||||||
departure: string;
|
departure: number;
|
||||||
departure_ru: string;
|
// departure_ru: string;
|
||||||
destination: string;
|
destination: number;
|
||||||
destination_ru: string;
|
// destination_ru: string;
|
||||||
departure_time: string;
|
departure_time: string;
|
||||||
travel_time: string;
|
travel_time: string;
|
||||||
location_name: string;
|
location_name: string;
|
||||||
@@ -349,6 +374,33 @@ export interface HotelFeaturesTypeDetail {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HotelMealList {
|
||||||
|
status: boolean;
|
||||||
|
data: {
|
||||||
|
links: {
|
||||||
|
previous: null | string;
|
||||||
|
next: null | string;
|
||||||
|
};
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
results: HotelMealListData[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HotelMealdetail {
|
||||||
|
status: boolean;
|
||||||
|
data: HotelMealListData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HotelMealListData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
name_uz: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetDetailTours {
|
export interface GetDetailTours {
|
||||||
status: boolean;
|
status: boolean;
|
||||||
data: {
|
data: {
|
||||||
@@ -363,12 +415,26 @@ export interface GetDetailTours {
|
|||||||
price: number;
|
price: number;
|
||||||
min_person: number;
|
min_person: number;
|
||||||
max_person: number;
|
max_person: number;
|
||||||
departure: string;
|
departure: {
|
||||||
departure_ru: string;
|
id: number;
|
||||||
departure_uz: string;
|
name: string;
|
||||||
destination: string;
|
name_ru: string;
|
||||||
destination_ru: string;
|
country: {
|
||||||
destination_uz: string;
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
destination: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
country: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
departure_time: string;
|
departure_time: string;
|
||||||
travel_time: string;
|
travel_time: string;
|
||||||
location_name: string;
|
location_name: string;
|
||||||
@@ -488,12 +554,11 @@ export interface GetHotelRes {
|
|||||||
total_pages: number;
|
total_pages: number;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
current_page: number;
|
current_page: number;
|
||||||
results: [
|
results: {
|
||||||
{
|
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
meal_plan: string;
|
meal_plan: number;
|
||||||
ticket: number;
|
ticket: number;
|
||||||
hotel_type: [
|
hotel_type: [
|
||||||
{
|
{
|
||||||
@@ -524,8 +589,7 @@ export interface GetHotelRes {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
}[];
|
||||||
];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,3 +626,35 @@ export interface DetailAmenitiesData {
|
|||||||
icon_name: string;
|
icon_name: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CountryList {
|
||||||
|
status: boolean;
|
||||||
|
data: {
|
||||||
|
links: {
|
||||||
|
previous: null | string;
|
||||||
|
next: null | string;
|
||||||
|
};
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
results: CountryLisResult[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CountryLisResult {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
name_uz: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CountryDeatil {
|
||||||
|
status: boolean;
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
name_uz: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
340
src/pages/tours/ui/CountryTable.tsx
Normal file
340
src/pages/tours/ui/CountryTable.tsx
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
import {
|
||||||
|
countryCreate,
|
||||||
|
countryDeatil,
|
||||||
|
countryDelete,
|
||||||
|
countryUpdate,
|
||||||
|
} from "@/pages/tours/lib/api";
|
||||||
|
import { CountryColumns } from "@/pages/tours/lib/column";
|
||||||
|
import type { CountryLisResult } from "@/pages/tours/lib/type";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { Dialog, DialogContent } from "@/shared/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/shared/ui/form";
|
||||||
|
import { Input } from "@/shared/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/shared/ui/table";
|
||||||
|
import RealPagination from "@/widgets/real-pagination/ui/RealPagination";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { Loader, PlusIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const CountryTable = ({
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
setActiveTab,
|
||||||
|
setFeatureId,
|
||||||
|
}: {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
setActiveTab: Dispatch<SetStateAction<string>>;
|
||||||
|
setFeatureId: Dispatch<SetStateAction<number | null>>;
|
||||||
|
data:
|
||||||
|
| {
|
||||||
|
links: {
|
||||||
|
previous: string | null;
|
||||||
|
next: string | null;
|
||||||
|
};
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
results: CountryLisResult[];
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [editId, setEditId] = useState<number | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [types, setTypes] = useState<"edit" | "create">("create");
|
||||||
|
|
||||||
|
const handleEdit = (id: number) => {
|
||||||
|
setTypes("edit");
|
||||||
|
setOpen(true);
|
||||||
|
setEditId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: badgeDetail } = useQuery({
|
||||||
|
queryKey: ["detail_country", editId],
|
||||||
|
queryFn: () => countryDeatil(editId!),
|
||||||
|
enabled: !!editId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: deleteMutate } = useMutation({
|
||||||
|
mutationFn: (id: number) => countryDelete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_country"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["detail_country"] });
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
position: "top-center",
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = (id: number) => {
|
||||||
|
deleteMutate(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = CountryColumns(
|
||||||
|
handleEdit,
|
||||||
|
handleDelete,
|
||||||
|
t,
|
||||||
|
setActiveTab,
|
||||||
|
setFeatureId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutate: create, isPending } = useMutation({
|
||||||
|
mutationFn: (body: { name: string; name_ru: string }) =>
|
||||||
|
countryCreate(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_country"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["detail_country"] });
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
position: "top-center",
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: update, isPending: updatePending } = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
body,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: {
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
};
|
||||||
|
}) => countryUpdate({ body, id }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_country"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["detail_country"] });
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
position: "top-center",
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
name_ru: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (badgeDetail) {
|
||||||
|
form.setValue("name", badgeDetail.data.data.name_uz);
|
||||||
|
form.setValue("name_ru", badgeDetail.data.data.name_ru);
|
||||||
|
}
|
||||||
|
}, [editId, badgeDetail]);
|
||||||
|
|
||||||
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
if (types === "create") {
|
||||||
|
create({
|
||||||
|
name: values.name,
|
||||||
|
name_ru: values.name_ru,
|
||||||
|
});
|
||||||
|
} else if (types === "edit" && editId) {
|
||||||
|
update({
|
||||||
|
id: editId,
|
||||||
|
body: {
|
||||||
|
name: values.name,
|
||||||
|
name_ru: values.name_ru,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: data?.results ?? [],
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
pageCount: data?.total_pages ?? 0,
|
||||||
|
state: {
|
||||||
|
pagination: {
|
||||||
|
pageIndex: page - 1,
|
||||||
|
pageSize: pageSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
setTypes("create");
|
||||||
|
form.reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="mr-2" />
|
||||||
|
{t("Qo‘shish")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
|
||||||
|
<Table key={data?.current_page}>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center text-gray-400"
|
||||||
|
>
|
||||||
|
{t("Ma'lumot topilmadi")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RealPagination
|
||||||
|
table={table}
|
||||||
|
totalPages={data?.total_pages}
|
||||||
|
namePage="pageCountry"
|
||||||
|
namePageSize="pageSizeCountry"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<p className="text-xl">
|
||||||
|
{types === "create" ? t("Saqlash") : t("Tahrirlash")}
|
||||||
|
</p>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-6 p-2"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("Nomi")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={t("Nomi")} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name_ru"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("Nomi (ru)")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={t("Nomi (ru)")} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="border-slate-600 text-white hover:bg-gray-600 bg-gray-600"
|
||||||
|
>
|
||||||
|
{t("Bekor qilish")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{isPending || updatePending ? (
|
||||||
|
<Loader className="animate-spin" />
|
||||||
|
) : types === "create" ? (
|
||||||
|
t("Saqlash")
|
||||||
|
) : (
|
||||||
|
t("Tahrirlash")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CountryTable;
|
||||||
@@ -22,6 +22,7 @@ const CreateEditTour = () => {
|
|||||||
select(data) {
|
select(data) {
|
||||||
return data.data;
|
return data.data;
|
||||||
},
|
},
|
||||||
|
enabled: !!id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
324
src/pages/tours/ui/HotelType.tsx
Normal file
324
src/pages/tours/ui/HotelType.tsx
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import {
|
||||||
|
hotelTypeCreate,
|
||||||
|
hotelTypeDelete,
|
||||||
|
hotelTypeDetail,
|
||||||
|
hotelTypeUpdate,
|
||||||
|
} from "@/pages/tours/lib/api";
|
||||||
|
import { TypeColumns } from "@/pages/tours/lib/column";
|
||||||
|
import type { Type } from "@/pages/tours/lib/type";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { Dialog, DialogContent } from "@/shared/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/shared/ui/form";
|
||||||
|
import { Input } from "@/shared/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/shared/ui/table";
|
||||||
|
import RealPagination from "@/widgets/real-pagination/ui/RealPagination";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { Loader, PlusIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const HotelType = ({
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
}: {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
data:
|
||||||
|
| {
|
||||||
|
links: {
|
||||||
|
previous: string;
|
||||||
|
next: string;
|
||||||
|
};
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
results: Type[];
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [editId, setEditId] = useState<number | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [types, setTypes] = useState<"edit" | "create">("create");
|
||||||
|
|
||||||
|
const handleEdit = (id: number) => {
|
||||||
|
setTypes("edit");
|
||||||
|
setOpen(true);
|
||||||
|
setEditId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: typeDetail } = useQuery({
|
||||||
|
queryKey: ["detail_type", editId],
|
||||||
|
queryFn: () => hotelTypeDetail({ id: editId! }),
|
||||||
|
enabled: !!editId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: deleteMutate } = useMutation({
|
||||||
|
mutationFn: ({ id }: { id: number }) => hotelTypeDelete({ id }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["detail_type"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_type"] });
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
position: "top-center",
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = (id: number) => {
|
||||||
|
deleteMutate({ id });
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = TypeColumns(handleEdit, handleDelete, t);
|
||||||
|
|
||||||
|
const { mutate: create, isPending } = useMutation({
|
||||||
|
mutationFn: ({ body }: { body: { name: string; name_ru: string } }) =>
|
||||||
|
hotelTypeCreate({ body }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["detail_type"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_type"] });
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
position: "top-center",
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: update, isPending: updatePending } = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
body,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: { name: string; name_ru: string };
|
||||||
|
}) => hotelTypeUpdate({ body, id }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["detail_type"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_type"] });
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
position: "top-center",
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
name_ru: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeDetail) {
|
||||||
|
form.setValue("name", typeDetail.data.data.name_uz);
|
||||||
|
form.setValue("name_ru", typeDetail.data.data.name_ru);
|
||||||
|
}
|
||||||
|
}, [editId, typeDetail]);
|
||||||
|
|
||||||
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
if (types === "create") {
|
||||||
|
create({
|
||||||
|
body: {
|
||||||
|
name: values.name,
|
||||||
|
name_ru: values.name_ru,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (types === "edit" && editId) {
|
||||||
|
update({
|
||||||
|
id: editId,
|
||||||
|
body: {
|
||||||
|
name: values.name,
|
||||||
|
name_ru: values.name_ru,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: data?.results ?? [],
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
pageCount: data?.total_pages ?? 0,
|
||||||
|
state: {
|
||||||
|
pagination: {
|
||||||
|
pageIndex: page - 1,
|
||||||
|
pageSize: pageSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
setTypes("create");
|
||||||
|
form.reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="mr-2" />
|
||||||
|
{t("Qo‘shish")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
|
||||||
|
<Table key={data?.current_page}>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center text-gray-400"
|
||||||
|
>
|
||||||
|
{t("Ma'lumot topilmadi")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RealPagination table={table} totalPages={data?.total_pages} />
|
||||||
|
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<p className="text-xl">
|
||||||
|
{types === "create" ? t("Saqlash") : t("Tahrirlash")}
|
||||||
|
</p>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-6 p-2"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("Nomi")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={t("Nomi")} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name_ru"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("Nomi (ru)")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={t("Nomi (ru)")} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="border-slate-600 text-white hover:bg-gray-600 bg-gray-600"
|
||||||
|
>
|
||||||
|
{t("Bekor qilish")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{isPending || updatePending ? (
|
||||||
|
<Loader className="animate-spin" />
|
||||||
|
) : types === "create" ? (
|
||||||
|
t("Saqlash")
|
||||||
|
) : (
|
||||||
|
t("Tahrirlash")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HotelType;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
hotelTypeCreate,
|
hotelMealCreate,
|
||||||
hotelTypeDelete,
|
hotelMealDelete,
|
||||||
hotelTypeDetail,
|
hotelMealDetail,
|
||||||
hotelTypeUpdate,
|
hotelMealUpdate,
|
||||||
} from "@/pages/tours/lib/api";
|
} from "@/pages/tours/lib/api";
|
||||||
import { TypeColumns } from "@/pages/tours/lib/column";
|
import { MealColumns } from "@/pages/tours/lib/column";
|
||||||
import type { Type } from "@/pages/tours/lib/type";
|
import type { HotelMealListData } from "@/pages/tours/lib/type";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { Dialog, DialogContent } from "@/shared/ui/dialog";
|
import { Dialog, DialogContent } from "@/shared/ui/dialog";
|
||||||
import {
|
import {
|
||||||
@@ -45,7 +45,7 @@ const formSchema = z.object({
|
|||||||
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
|
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const MealTable = ({
|
const MealPlan = ({
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -55,14 +55,14 @@ const MealTable = ({
|
|||||||
data:
|
data:
|
||||||
| {
|
| {
|
||||||
links: {
|
links: {
|
||||||
previous: string;
|
previous: string | null;
|
||||||
next: string;
|
next: string | null;
|
||||||
};
|
};
|
||||||
total_items: number;
|
total_items: number;
|
||||||
total_pages: number;
|
total_pages: number;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
current_page: number;
|
current_page: number;
|
||||||
results: Type[];
|
results: HotelMealListData[];
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -78,17 +78,17 @@ const MealTable = ({
|
|||||||
setEditId(id);
|
setEditId(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: typeDetail } = useQuery({
|
const { data: badgeDetail } = useQuery({
|
||||||
queryKey: ["detail_type", editId],
|
queryKey: ["detail_meal", editId],
|
||||||
queryFn: () => hotelTypeDetail({ id: editId! }),
|
queryFn: () => hotelMealDetail(editId!),
|
||||||
enabled: !!editId,
|
enabled: !!editId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: deleteMutate } = useMutation({
|
const { mutate: deleteMutate } = useMutation({
|
||||||
mutationFn: ({ id }: { id: number }) => hotelTypeDelete({ id }),
|
mutationFn: ({ id }: { id: number }) => hotelMealDelete(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.refetchQueries({ queryKey: ["detail_type"] });
|
queryClient.refetchQueries({ queryKey: ["detail_meal"] });
|
||||||
queryClient.refetchQueries({ queryKey: ["all_type"] });
|
queryClient.refetchQueries({ queryKey: ["all_meal"] });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
},
|
},
|
||||||
@@ -104,14 +104,20 @@ const MealTable = ({
|
|||||||
deleteMutate({ id });
|
deleteMutate({ id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = TypeColumns(handleEdit, handleDelete, t);
|
const columns = MealColumns(handleEdit, handleDelete, t);
|
||||||
|
|
||||||
const { mutate: create, isPending } = useMutation({
|
const { mutate: create, isPending } = useMutation({
|
||||||
mutationFn: ({ body }: { body: { name: string; name_ru: string } }) =>
|
mutationFn: ({
|
||||||
hotelTypeCreate({ body }),
|
body,
|
||||||
|
}: {
|
||||||
|
body: {
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
};
|
||||||
|
}) => hotelMealCreate(body),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.refetchQueries({ queryKey: ["detail_type"] });
|
queryClient.refetchQueries({ queryKey: ["detail_meal"] });
|
||||||
queryClient.refetchQueries({ queryKey: ["all_type"] });
|
queryClient.refetchQueries({ queryKey: ["all_meal"] });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
},
|
},
|
||||||
@@ -129,11 +135,14 @@ const MealTable = ({
|
|||||||
id,
|
id,
|
||||||
}: {
|
}: {
|
||||||
id: number;
|
id: number;
|
||||||
body: { name: string; name_ru: string };
|
body: {
|
||||||
}) => hotelTypeUpdate({ body, id }),
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
};
|
||||||
|
}) => hotelMealUpdate({ body, id }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.refetchQueries({ queryKey: ["detail_type"] });
|
queryClient.refetchQueries({ queryKey: ["detail_meal"] });
|
||||||
queryClient.refetchQueries({ queryKey: ["all_type"] });
|
queryClient.refetchQueries({ queryKey: ["all_meal"] });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
},
|
},
|
||||||
@@ -154,11 +163,11 @@ const MealTable = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeDetail) {
|
if (badgeDetail) {
|
||||||
form.setValue("name", typeDetail.data.data.name_uz);
|
form.setValue("name", badgeDetail.data.data.name_uz);
|
||||||
form.setValue("name_ru", typeDetail.data.data.name_ru);
|
form.setValue("name_ru", badgeDetail.data.data.name_ru);
|
||||||
}
|
}
|
||||||
}, [editId, typeDetail]);
|
}, [editId, badgeDetail]);
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
if (types === "create") {
|
if (types === "create") {
|
||||||
@@ -192,7 +201,6 @@ const MealTable = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
@@ -256,7 +264,12 @@ const MealTable = ({
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RealPagination table={table} totalPages={data?.total_pages} />
|
<RealPagination
|
||||||
|
table={table}
|
||||||
|
totalPages={data?.total_pages}
|
||||||
|
namePage="pageMeal"
|
||||||
|
namePageSize="pageSizeMeal"
|
||||||
|
/>
|
||||||
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -321,4 +334,4 @@ const MealTable = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MealTable;
|
export default MealPlan;
|
||||||
|
|||||||
335
src/pages/tours/ui/RegionTable.tsx
Normal file
335
src/pages/tours/ui/RegionTable.tsx
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import {
|
||||||
|
hotelFeatureTypeDelete,
|
||||||
|
regionCreate,
|
||||||
|
regionDeatil,
|
||||||
|
regionUpdate,
|
||||||
|
} from "@/pages/tours/lib/api";
|
||||||
|
import { CountryColumns } from "@/pages/tours/lib/column";
|
||||||
|
import type { CountryLisResult } from "@/pages/tours/lib/type";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { Dialog, DialogContent } from "@/shared/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/shared/ui/form";
|
||||||
|
import { Input } from "@/shared/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/shared/ui/table";
|
||||||
|
import RealPagination from "@/widgets/real-pagination/ui/RealPagination";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { Loader, PlusIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const RegionTable = ({
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
featureId,
|
||||||
|
}: {
|
||||||
|
page: number;
|
||||||
|
featureId: number | null;
|
||||||
|
pageSize: number;
|
||||||
|
data:
|
||||||
|
| {
|
||||||
|
links: {
|
||||||
|
previous: string | null;
|
||||||
|
next: string | null;
|
||||||
|
};
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
results: CountryLisResult[];
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [editId, setEditId] = useState<number | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [types, setTypes] = useState<"edit" | "create">("create");
|
||||||
|
|
||||||
|
const handleEdit = (id: number) => {
|
||||||
|
setTypes("edit");
|
||||||
|
setOpen(true);
|
||||||
|
setEditId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: badgeDetail } = useQuery({
|
||||||
|
queryKey: ["region_detail", editId],
|
||||||
|
queryFn: () => regionDeatil(editId!),
|
||||||
|
enabled: !!editId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: deleteMutate } = useMutation({
|
||||||
|
mutationFn: ({ id }: { id: number }) => hotelFeatureTypeDelete({ id }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["region_detail"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_region"] });
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
position: "top-center",
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = (id: number) => {
|
||||||
|
deleteMutate({ id });
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = CountryColumns(handleEdit, handleDelete, t);
|
||||||
|
|
||||||
|
const { mutate: create, isPending } = useMutation({
|
||||||
|
mutationFn: (body: { name: string; name_ru: string; country: number }) =>
|
||||||
|
regionCreate(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["region_detail"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_region"] });
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
position: "top-center",
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: update, isPending: updatePending } = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
body,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
body: {
|
||||||
|
name: string;
|
||||||
|
name_ru: string;
|
||||||
|
country: number;
|
||||||
|
};
|
||||||
|
}) => regionUpdate({ body, id }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["region_detail"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_region"] });
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
|
position: "top-center",
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
name_ru: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (badgeDetail) {
|
||||||
|
form.setValue("name", badgeDetail.data.data.name_uz);
|
||||||
|
form.setValue("name_ru", badgeDetail.data.data.name_ru);
|
||||||
|
}
|
||||||
|
}, [editId, badgeDetail]);
|
||||||
|
|
||||||
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
if (types === "create") {
|
||||||
|
create({
|
||||||
|
country: Number(featureId),
|
||||||
|
name: values.name,
|
||||||
|
name_ru: values.name_ru,
|
||||||
|
});
|
||||||
|
} else if (types === "edit" && editId) {
|
||||||
|
update({
|
||||||
|
id: editId,
|
||||||
|
body: {
|
||||||
|
country: Number(featureId),
|
||||||
|
name: values.name,
|
||||||
|
name_ru: values.name_ru,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: data?.results ?? [],
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
pageCount: data?.total_pages ?? 0,
|
||||||
|
state: {
|
||||||
|
pagination: {
|
||||||
|
pageIndex: page - 1,
|
||||||
|
pageSize: pageSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
setTypes("create");
|
||||||
|
form.reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="mr-2" />
|
||||||
|
{t("Qo‘shish")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
|
||||||
|
<Table key={data?.current_page}>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center text-gray-400"
|
||||||
|
>
|
||||||
|
{t("Ma'lumot topilmadi")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RealPagination
|
||||||
|
table={table}
|
||||||
|
totalPages={data?.total_pages}
|
||||||
|
namePage="pageRegion"
|
||||||
|
namePageSize="pageSizeRegion"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<p className="text-xl">
|
||||||
|
{types === "create" ? t("Saqlash") : t("Tahrirlash")}
|
||||||
|
</p>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-6 p-2"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("Nomi")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={t("Nomi")} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name_ru"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("Nomi (ru)")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={t("Nomi (ru)")} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="border-slate-600 text-white hover:bg-gray-600 bg-gray-600"
|
||||||
|
>
|
||||||
|
{t("Bekor qilish")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{isPending || updatePending ? (
|
||||||
|
<Loader className="animate-spin" />
|
||||||
|
) : types === "create" ? (
|
||||||
|
t("Saqlash")
|
||||||
|
) : (
|
||||||
|
t("Tahrirlash")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegionTable;
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
countryList,
|
||||||
createTours,
|
createTours,
|
||||||
getAllAmenities,
|
getAllAmenities,
|
||||||
hotelBadge,
|
hotelBadge,
|
||||||
hotelTransport,
|
hotelTransport,
|
||||||
|
regionList,
|
||||||
updateTours,
|
updateTours,
|
||||||
} from "@/pages/tours/lib/api";
|
} from "@/pages/tours/lib/api";
|
||||||
import { TourformSchema } from "@/pages/tours/lib/form";
|
import { TourformSchema } from "@/pages/tours/lib/form";
|
||||||
@@ -12,12 +14,14 @@ import { useTicketStore } from "@/pages/tours/lib/store";
|
|||||||
import type { GetOneTours } from "@/pages/tours/lib/type";
|
import type { GetOneTours } from "@/pages/tours/lib/type";
|
||||||
import TicketsImagesModel from "@/pages/tours/ui/TicketsImagesModel";
|
import TicketsImagesModel from "@/pages/tours/ui/TicketsImagesModel";
|
||||||
import formatPrice from "@/shared/lib/formatPrice";
|
import formatPrice from "@/shared/lib/formatPrice";
|
||||||
|
import { cn } from "@/shared/lib/utils";
|
||||||
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 { Calendar } from "@/shared/ui/calendar";
|
import { Calendar } from "@/shared/ui/calendar";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/shared/ui/command";
|
} from "@/shared/ui/command";
|
||||||
@@ -34,8 +38,17 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
|||||||
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
|
||||||
import { Textarea } from "@/shared/ui/textarea";
|
import { Textarea } from "@/shared/ui/textarea";
|
||||||
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 { ChevronDownIcon, SquareCheckBig, XIcon } from "lucide-react";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import {
|
||||||
|
Check,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronsUpDown,
|
||||||
|
Loader2,
|
||||||
|
MoveLeft,
|
||||||
|
SquareCheckBig,
|
||||||
|
XIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import { useEffect, useState, type Dispatch, type SetStateAction } 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";
|
||||||
@@ -54,6 +67,7 @@ const StepOne = ({
|
|||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [displayPrice, setDisplayPrice] = useState("");
|
const [displayPrice, setDisplayPrice] = useState("");
|
||||||
|
const queryClient = useQueryClient();
|
||||||
// const [tarifdisplayPrice, setTarifDisplayPrice] = useState<string[]>([]);
|
// const [tarifdisplayPrice, setTarifDisplayPrice] = useState<string[]>([]);
|
||||||
const [transportPrices, setTransportPrices] = useState<string[]>([]);
|
const [transportPrices, setTransportPrices] = useState<string[]>([]);
|
||||||
|
|
||||||
@@ -71,9 +85,9 @@ const StepOne = ({
|
|||||||
departure: "",
|
departure: "",
|
||||||
// tarif: [],
|
// tarif: [],
|
||||||
transport: [],
|
transport: [],
|
||||||
departure_ru: "",
|
// departure_ru: "",
|
||||||
destination: "",
|
destination: "",
|
||||||
destination_ru: "",
|
// destination_ru: "",
|
||||||
location_name: "",
|
location_name: "",
|
||||||
location_name_ru: "",
|
location_name_ru: "",
|
||||||
departureDateTime: {
|
departureDateTime: {
|
||||||
@@ -126,12 +140,13 @@ const StepOne = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Transport
|
// 🔹 TransportS
|
||||||
const transports =
|
const transports =
|
||||||
tour.transports?.map((t, i) => ({
|
tour.transports?.map((t) => ({
|
||||||
transport: i + 1,
|
transport: t.transport.id,
|
||||||
price: t.price ?? 0,
|
price: t.price ?? 0,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
setTransportPrices(transports.map((t) => formatPrice(t.price ?? 0)));
|
setTransportPrices(transports.map((t) => formatPrice(t.price ?? 0)));
|
||||||
|
|
||||||
// 🔹 Tarif
|
// 🔹 Tarif
|
||||||
@@ -149,10 +164,10 @@ const StepOne = ({
|
|||||||
passenger_count: String(tour.passenger_count) ?? "1",
|
passenger_count: String(tour.passenger_count) ?? "1",
|
||||||
min_person: String(tour.min_person) ?? "1",
|
min_person: String(tour.min_person) ?? "1",
|
||||||
max_person: String(tour.max_person) ?? "1",
|
max_person: String(tour.max_person) ?? "1",
|
||||||
departure: tour.departure_uz ?? "",
|
departure: tour.departure?.id.toString() ?? "",
|
||||||
departure_ru: tour.departure_ru ?? "",
|
// departure_ru: tour.departure_ru ?? "",
|
||||||
destination: tour.destination_uz ?? "",
|
destination: tour.destination?.id.toString() ?? "",
|
||||||
destination_ru: tour.destination_ru ?? "",
|
// destination_ru: tour.destination_ru ?? "",
|
||||||
location_name: tour.location_name_uz ?? "",
|
location_name: tour.location_name_uz ?? "",
|
||||||
location_name_ru: tour.location_name_ru ?? "",
|
location_name_ru: tour.location_name_ru ?? "",
|
||||||
hotel_info: tour.hotel_info_uz ?? "",
|
hotel_info: tour.hotel_info_uz ?? "",
|
||||||
@@ -225,17 +240,25 @@ const StepOne = ({
|
|||||||
|
|
||||||
// TicketStore uchun id
|
// TicketStore uchun id
|
||||||
setId(tour.id);
|
setId(tour.id);
|
||||||
|
setSelectedCountry(data.data.departure?.country?.id);
|
||||||
|
setSearchCity(data.data.departure?.name);
|
||||||
|
setSelectedCountryDes(data.data.destination?.country.id);
|
||||||
|
setSearchCityDes(data.data.destination?.name);
|
||||||
}, [isEditMode, data, form, setId]);
|
}, [isEditMode, data, form, setId]);
|
||||||
|
|
||||||
const { watch, setValue } = form;
|
const { watch, setValue } = form;
|
||||||
const selectedDate = watch("departureDateTime.date");
|
const selectedDate = watch("departureDateTime.date");
|
||||||
const selectedDateTravel = watch("travelDateTime.date");
|
const selectedDateTravel = watch("travelDateTime.date");
|
||||||
|
|
||||||
const { mutate: create } = useMutation({
|
const { mutate: create, isPending } = useMutation({
|
||||||
mutationFn: (body: FormData) => {
|
mutationFn: (body: FormData) => {
|
||||||
return createTours({ body });
|
return createTours({ body });
|
||||||
},
|
},
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["popular_tours"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["tours_detail"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
||||||
setId(res.data.data.id);
|
setId(res.data.data.id);
|
||||||
setStep(2);
|
setStep(2);
|
||||||
},
|
},
|
||||||
@@ -247,11 +270,15 @@ const StepOne = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: update } = useMutation({
|
const { mutate: update, isPending: updatePending } = useMutation({
|
||||||
mutationFn: ({ body, id }: { id: number; body: FormData }) => {
|
mutationFn: ({ body, id }: { id: number; body: FormData }) => {
|
||||||
return updateTours({ body, id });
|
return updateTours({ body, id });
|
||||||
},
|
},
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["popular_tours"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["tours_detail"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
||||||
setId(res.data.data.id);
|
setId(res.data.data.id);
|
||||||
setStep(2);
|
setStep(2);
|
||||||
},
|
},
|
||||||
@@ -265,7 +292,7 @@ const StepOne = ({
|
|||||||
|
|
||||||
function onSubmit(value: z.infer<typeof TourformSchema>) {
|
function onSubmit(value: z.infer<typeof TourformSchema>) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
const tour = data ? data.data : null;
|
||||||
// Asosiy ma'lumotlar
|
// Asosiy ma'lumotlar
|
||||||
formData.append("title", value.title);
|
formData.append("title", value.title);
|
||||||
formData.append("location_name", value.location_name);
|
formData.append("location_name", value.location_name);
|
||||||
@@ -279,9 +306,9 @@ const StepOne = ({
|
|||||||
formData.append("min_person", String(value.min_person));
|
formData.append("min_person", String(value.min_person));
|
||||||
formData.append("max_person", String(value.max_person));
|
formData.append("max_person", String(value.max_person));
|
||||||
formData.append("departure", String(value.departure));
|
formData.append("departure", String(value.departure));
|
||||||
formData.append("departure_ru", String(value.departure_ru));
|
// formData.append("departure_ru", String(value.departure_ru));
|
||||||
formData.append("destination", String(value.destination));
|
formData.append("destination", String(value.destination));
|
||||||
formData.append("destination_ru", String(value.destination_ru));
|
// formData.append("destination_ru", String(value.destination_ru));
|
||||||
formData.append(
|
formData.append(
|
||||||
"departure_time",
|
"departure_time",
|
||||||
value.departureDateTime.date?.toISOString(),
|
value.departureDateTime.date?.toISOString(),
|
||||||
@@ -308,12 +335,11 @@ const StepOne = ({
|
|||||||
formData.append("hotel_meals_ru", value.hotel_meals_info_ru);
|
formData.append("hotel_meals_ru", value.hotel_meals_info_ru);
|
||||||
}
|
}
|
||||||
formData.append("duration_days", String(value.duration));
|
formData.append("duration_days", String(value.duration));
|
||||||
formData.append("rating", String("0.0"));
|
formData.append("rating", tour ? String(tour.rating) : "0.0");
|
||||||
|
|
||||||
if (value.banner instanceof File) {
|
if (value.banner instanceof File) {
|
||||||
formData.append("image_banner", value.banner);
|
formData.append("image_banner", value.banner);
|
||||||
}
|
}
|
||||||
console.log(value.banner, "value.banner");
|
|
||||||
|
|
||||||
// Tarif va transport
|
// Tarif va transport
|
||||||
// value.tarif?.forEach((e, i) => {
|
// value.tarif?.forEach((e, i) => {
|
||||||
@@ -479,6 +505,70 @@ const StepOne = ({
|
|||||||
// queryFn: () => hotelTarif({ page: 1, page_size: 10 }),
|
// queryFn: () => hotelTarif({ page: 1, page_size: 10 }),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
const [searchCountry, setSearchCountry] = useState<string>("");
|
||||||
|
const [searchCity, setSearchCity] = useState<string>("");
|
||||||
|
const [openCountry, setOpenCountry] = useState<boolean>(false);
|
||||||
|
const [openCity, setOpenCity] = useState<boolean>(false);
|
||||||
|
const [selectedCountry, setSelectedCountry] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const { data: countryData, isLoading: countryLoad } = useQuery({
|
||||||
|
queryKey: ["all_country", searchCountry],
|
||||||
|
queryFn: () => countryList({ page: 1, page_size: 10, name: searchCountry }),
|
||||||
|
select: (res) => res.data.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: regionData, isLoading: regionLoad } = useQuery({
|
||||||
|
queryKey: ["all_region", selectedCountry, searchCity],
|
||||||
|
queryFn: () =>
|
||||||
|
regionList({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
country: selectedCountry!,
|
||||||
|
name: searchCity,
|
||||||
|
}),
|
||||||
|
select: (res) => res.data.data,
|
||||||
|
enabled: !!selectedCountry,
|
||||||
|
});
|
||||||
|
//destions select country
|
||||||
|
const [searchCountryDes, setSearchCountryDes] = useState<string>("");
|
||||||
|
const [searchCityDes, setSearchCityDes] = useState<string>("");
|
||||||
|
const [openCountryDes, setOpenCountryDes] = useState<boolean>(false);
|
||||||
|
const [openCityDes, setOpenCityDes] = useState<boolean>(false);
|
||||||
|
const [selectedCountryDes, setSelectedCountryDes] = useState<number | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const { data: countryDataDes, isLoading: countryLoadDes } = useQuery({
|
||||||
|
queryKey: ["all_country_des", searchCountryDes],
|
||||||
|
queryFn: () =>
|
||||||
|
countryList({ page: 1, page_size: 10, name: searchCountryDes }),
|
||||||
|
select: (res) => res.data.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: regionDataDes, isLoading: regionLoadDes } = useQuery({
|
||||||
|
queryKey: ["all_region_des", selectedCountryDes, searchCityDes],
|
||||||
|
queryFn: () =>
|
||||||
|
regionList({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
country: selectedCountryDes!,
|
||||||
|
name: searchCityDes,
|
||||||
|
}),
|
||||||
|
select: (res) => res.data.data,
|
||||||
|
enabled: !!selectedCountryDes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCountrySelect = (id: number | null) => {
|
||||||
|
setSelectedCountry(id);
|
||||||
|
form.setValue("departure", "");
|
||||||
|
setOpenCountry(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCountrySelectDes = (id: number | null) => {
|
||||||
|
setSelectedCountryDes(id);
|
||||||
|
form.setValue("destination", "");
|
||||||
|
setOpenCountryDes(false);
|
||||||
|
};
|
||||||
|
|
||||||
const { data: transport } = useQuery({
|
const { data: transport } = useQuery({
|
||||||
queryKey: ["all_transport"],
|
queryKey: ["all_transport"],
|
||||||
queryFn: () => hotelTransport({ page: 1, page_size: 10 }),
|
queryFn: () => hotelTransport({ page: 1, page_size: 10 }),
|
||||||
@@ -613,24 +703,224 @@ const StepOne = ({
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-start">
|
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-start">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
|
||||||
name="departure"
|
name="departure"
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormItem>
|
render={({ field }) => {
|
||||||
<Label className="text-md">{t("Ketish joyi")}</Label>
|
const selectedCity = regionData?.results.find(
|
||||||
|
(u) => String(u.id) === field.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem className="flex flex-col gap-2">
|
||||||
|
<Label className="text-md">Ketish joyi</Label>
|
||||||
|
{!selectedCountry && (
|
||||||
|
<Popover open={openCountry} onOpenChange={setOpenCountry}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Button
|
||||||
placeholder="Toshkent"
|
variant="outline"
|
||||||
{...field}
|
className={cn(
|
||||||
className="h-12 !text-md"
|
"w-full h-12 justify-between transition-all duration-200",
|
||||||
/>
|
!selectedCountry && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedCountry
|
||||||
|
? countryData?.results.find(
|
||||||
|
(c) => c.id === selectedCountry,
|
||||||
|
)?.name
|
||||||
|
: "Davlatni tanlang"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
</PopoverTrigger>
|
||||||
</FormItem>
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<PopoverContent className="!w-96 p-0">
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Qidirish..."
|
||||||
|
value={searchCountry}
|
||||||
|
onValueChange={setSearchCountry}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
{countryLoad ? (
|
||||||
|
<div className="py-6 text-center">
|
||||||
|
<Loader2 className="animate-spin mx-auto" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CommandGroup>
|
||||||
|
{countryData?.results.map((c, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={c.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
delay: index * 0.03,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
handleCountrySelect(c.id);
|
||||||
|
setOpenCountry(false);
|
||||||
|
setTimeout(
|
||||||
|
() => setOpenCity(true),
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 transition-opacity duration-200",
|
||||||
|
selectedCountry === c.id
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{c.name}
|
||||||
|
</CommandItem>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
{/* CITY DROPDOWN */}
|
||||||
|
{selectedCountry && (
|
||||||
|
<Popover open={openCity} onOpenChange={setOpenCity}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full h-12 justify-between transition-all duration-200",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedCity
|
||||||
|
? selectedCity.name
|
||||||
|
: "Shaharni tanlang"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0, y: -10 }}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
height: "auto",
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
height: { duration: 0.3 },
|
||||||
|
opacity: { duration: 0.3, delay: 0.1 },
|
||||||
|
y: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
height: 0,
|
||||||
|
y: -10,
|
||||||
|
transition: {
|
||||||
|
height: { duration: 0.25 },
|
||||||
|
opacity: { duration: 0.15 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
style={{ overflow: "hidden" }}
|
||||||
|
>
|
||||||
|
<PopoverContent className="!w-96 p-0">
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Qidirish..."
|
||||||
|
value={searchCity}
|
||||||
|
onValueChange={setSearchCity}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="w-fit ml-2 p-1 cursor-pointer hover:bg-accent transition-colors"
|
||||||
|
size={"icon"}
|
||||||
|
variant={"ghost"}
|
||||||
|
onClick={() => {
|
||||||
|
handleCountrySelect(null);
|
||||||
|
setOpenCity(false);
|
||||||
|
setTimeout(() => setOpenCountry(true), 200);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoveLeft className="mr-1" />
|
||||||
|
<p>Orqaga</p>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
<CommandList>
|
||||||
|
{regionLoad ? (
|
||||||
|
<div className="py-6 text-center">
|
||||||
|
<Loader2 className="animate-spin mx-auto" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CommandGroup>
|
||||||
|
{regionData?.results.map((r, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={r.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
delay: index * 0.03,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
field.onChange(String(r.id));
|
||||||
|
setOpenCity(false);
|
||||||
|
}}
|
||||||
|
className="cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 transition-opacity duration-200",
|
||||||
|
field.value === String(r.id)
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{r.name}
|
||||||
|
</CommandItem>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="departure_ru"
|
name="departure_ru"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -646,27 +936,234 @@ const StepOne = ({
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
|
||||||
name="destination"
|
name="destination"
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormItem>
|
render={({ field }) => {
|
||||||
<Label className="text-md">{t("Borish joyi")}</Label>
|
const selectedCity = regionDataDes?.results.find(
|
||||||
|
(u) => String(u.id) === field.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem className="flex flex-col gap-2">
|
||||||
|
<Label className="text-md">Borish joyi</Label>
|
||||||
|
|
||||||
|
{/* COUNTRY DROPDOWN */}
|
||||||
|
{!selectedCountryDes && (
|
||||||
|
<Popover
|
||||||
|
open={openCountryDes}
|
||||||
|
onOpenChange={setOpenCountryDes}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Button
|
||||||
placeholder="Dubai"
|
variant="outline"
|
||||||
{...field}
|
className={cn(
|
||||||
className="h-12 !text-md"
|
"w-full h-12 justify-between transition-all duration-200",
|
||||||
/>
|
!selectedCountryDes && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedCountryDes
|
||||||
|
? countryDataDes?.results.find(
|
||||||
|
(c) => c.id === selectedCountryDes,
|
||||||
|
)?.name
|
||||||
|
: "Davlatni tanlang"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
</PopoverTrigger>
|
||||||
</FormItem>
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<PopoverContent className="!w-96 p-0">
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Qidirish..."
|
||||||
|
value={searchCountryDes}
|
||||||
|
onValueChange={setSearchCountryDes}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
{countryLoadDes ? (
|
||||||
|
<div className="py-6 text-center">
|
||||||
|
<Loader2 className="animate-spin mx-auto" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CommandGroup>
|
||||||
|
{countryDataDes?.results.map((c, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={c.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
delay: index * 0.03,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
handleCountrySelectDes(c.id);
|
||||||
|
setOpenCountryDes(false);
|
||||||
|
setTimeout(
|
||||||
|
() => setOpenCityDes(true),
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 transition-opacity duration-200",
|
||||||
|
selectedCountryDes === c.id
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{c.name}
|
||||||
|
</CommandItem>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
{selectedCountryDes && (
|
||||||
|
<Popover open={openCityDes} onOpenChange={setOpenCityDes}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full h-12 justify-between transition-all duration-200",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedCity
|
||||||
|
? selectedCity.name
|
||||||
|
: "Shaharni tanlang"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0, y: -10 }}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
height: "auto",
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
height: { duration: 0.3 },
|
||||||
|
opacity: { duration: 0.3, delay: 0.1 },
|
||||||
|
y: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
height: 0,
|
||||||
|
y: -10,
|
||||||
|
transition: {
|
||||||
|
height: { duration: 0.25 },
|
||||||
|
opacity: { duration: 0.15 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
style={{ overflow: "hidden" }}
|
||||||
|
>
|
||||||
|
<PopoverContent className="!w-96 p-0">
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Qidirish..."
|
||||||
|
value={searchCityDes}
|
||||||
|
onValueChange={setSearchCityDes}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="w-fit ml-2 p-1 cursor-pointer hover:bg-accent transition-colors"
|
||||||
|
size={"icon"}
|
||||||
|
variant={"ghost"}
|
||||||
|
onClick={() => {
|
||||||
|
handleCountrySelectDes(null);
|
||||||
|
setOpenCityDes(false);
|
||||||
|
setTimeout(
|
||||||
|
() => setOpenCountryDes(true),
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoveLeft className="mr-1" />
|
||||||
|
<p>Orqaga</p>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
<CommandList>
|
||||||
|
{regionLoadDes ? (
|
||||||
|
<div className="py-6 text-center">
|
||||||
|
<Loader2 className="animate-spin mx-auto" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CommandGroup>
|
||||||
|
{regionDataDes?.results.map((r, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={r.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
delay: index * 0.03,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
field.onChange(String(r.id));
|
||||||
|
setOpenCityDes(false);
|
||||||
|
}}
|
||||||
|
className="cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 transition-opacity duration-200",
|
||||||
|
field.value === String(r.id)
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{r.name}
|
||||||
|
</CommandItem>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="destination_ru"
|
name="destination_ru"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -682,7 +1179,7 @@ const StepOne = ({
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -2204,7 +2701,11 @@ const StepOne = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-600 cursor-pointer"
|
className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-600 cursor-pointer"
|
||||||
>
|
>
|
||||||
{t("Saqlash")}
|
{isPending || updatePending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("Saqlash")
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getHotel,
|
getHotel,
|
||||||
hotelFeature,
|
hotelFeature,
|
||||||
hotelFeatureType,
|
hotelFeatureType,
|
||||||
|
hotelMealList,
|
||||||
hotelType,
|
hotelType,
|
||||||
} from "@/pages/tours/lib/api";
|
} from "@/pages/tours/lib/api";
|
||||||
import { useTicketStore } from "@/pages/tours/lib/store";
|
import { useTicketStore } from "@/pages/tours/lib/store";
|
||||||
@@ -15,6 +16,16 @@ import type {
|
|||||||
HotelFeaturesType,
|
HotelFeaturesType,
|
||||||
Type,
|
Type,
|
||||||
} from "@/pages/tours/lib/type";
|
} from "@/pages/tours/lib/type";
|
||||||
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/shared/ui/command";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -24,6 +35,7 @@ import {
|
|||||||
} from "@/shared/ui/form";
|
} from "@/shared/ui/form";
|
||||||
import { Input } from "@/shared/ui/input";
|
import { Input } from "@/shared/ui/input";
|
||||||
import { Label } from "@/shared/ui/label";
|
import { Label } from "@/shared/ui/label";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -33,7 +45,7 @@ import {
|
|||||||
} from "@/shared/ui/select";
|
} from "@/shared/ui/select";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { X } from "lucide-react";
|
import { Check, ChevronsUpDown, Loader2, X } from "lucide-react";
|
||||||
import { useEffect, useState, type Dispatch, type SetStateAction } 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";
|
||||||
@@ -100,19 +112,7 @@ const StepTwo = ({
|
|||||||
form.setValue("title", hotel.name);
|
form.setValue("title", hotel.name);
|
||||||
form.setValue("rating", String(hotel.rating));
|
form.setValue("rating", String(hotel.rating));
|
||||||
|
|
||||||
const mealPlan =
|
form.setValue("mealPlan", hotel.meal_plan.toString());
|
||||||
hotel.meal_plan === "breakfast"
|
|
||||||
? "Breakfast Only"
|
|
||||||
: hotel.meal_plan === "all_inclusive"
|
|
||||||
? "All Inclusive"
|
|
||||||
: hotel.meal_plan === "half_board"
|
|
||||||
? "Half Board"
|
|
||||||
: hotel.meal_plan === "full_board"
|
|
||||||
? "Full Board"
|
|
||||||
: "all_inclusive";
|
|
||||||
|
|
||||||
// ✅ SetValue faqat backenddan qiymat kelganda chaqiriladi
|
|
||||||
form.setValue("mealPlan", mealPlan);
|
|
||||||
|
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"hotelType",
|
"hotelType",
|
||||||
@@ -126,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]);
|
}, [isEditMode, hotelDetail, form, data]);
|
||||||
|
|
||||||
// 🧩 Select ma'lumotlari
|
// 🧩 Select ma'lumotlari
|
||||||
const [allHotelTypes, setAllHotelTypes] = useState<Type[]>([]);
|
const [allHotelTypes, setAllHotelTypes] = useState<Type[]>([]);
|
||||||
@@ -231,6 +231,9 @@ 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: ["popular_tours"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["tours_detail"] });
|
||||||
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
||||||
toast.success(t("Muvaffaqiyatli saqlandi"));
|
toast.success(t("Muvaffaqiyatli saqlandi"));
|
||||||
navigate("/tours");
|
navigate("/tours");
|
||||||
@@ -247,6 +250,9 @@ 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: ["popular_tours"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ["tours_detail"] });
|
||||||
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
||||||
toast.success(t("Muvaffaqiyatli saqlandi"));
|
toast.success(t("Muvaffaqiyatli saqlandi"));
|
||||||
navigate("/tours");
|
navigate("/tours");
|
||||||
@@ -280,6 +286,15 @@ const StepTwo = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [openMeal, setOpenMeal] = useState<boolean>(false);
|
||||||
|
const [searchMeal, setSearchMeal] = useState<string>("");
|
||||||
|
|
||||||
|
const { data: mealData, isLoading: mealLoad } = useQuery({
|
||||||
|
queryKey: ["all_meal", searchMeal],
|
||||||
|
queryFn: () => hotelMealList({ page: 1, page_size: 99, name: searchMeal }),
|
||||||
|
select: (res) => res.data.data,
|
||||||
|
});
|
||||||
|
|
||||||
const removeFeatureType = (id: string) =>
|
const removeFeatureType = (id: string) =>
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"hotelFeaturesType",
|
"hotelFeaturesType",
|
||||||
@@ -297,38 +312,27 @@ const StepTwo = ({
|
|||||||
formData.append(`hotel_amenities[${i}]`, String(e));
|
formData.append(`hotel_amenities[${i}]`, String(e));
|
||||||
});
|
});
|
||||||
|
|
||||||
const mealPlan =
|
formData.append("meal_plan", data.mealPlan);
|
||||||
data.mealPlan === "Breakfast Only"
|
|
||||||
? "breakfast"
|
|
||||||
: data.mealPlan === "Half Board"
|
|
||||||
? "half_board"
|
|
||||||
: data.mealPlan === "Full Board"
|
|
||||||
? "full_board"
|
|
||||||
: "all_inclusive";
|
|
||||||
formData.append("meal_plan", mealPlan);
|
|
||||||
|
|
||||||
data.hotelType &&
|
data.hotelType &&
|
||||||
data.hotelType.forEach((id) => formData.append("hotel_type", id));
|
data.hotelType.forEach((id) => formData.append("hotel_type", id));
|
||||||
data.hotelFeatures &&
|
data.hotelFeaturesType &&
|
||||||
data.hotelFeatures.forEach((id) => formData.append("hotel_features", id));
|
data.hotelFeaturesType.forEach((id) =>
|
||||||
|
formData.append("hotel_features", id),
|
||||||
|
);
|
||||||
|
|
||||||
if (isEditMode && hotelDetail) {
|
if (isEditMode && hotelDetail && hotelDetail?.length > 0) {
|
||||||
edit({
|
edit({
|
||||||
body: formData,
|
body: formData,
|
||||||
id: Number(hotelDetail[0].id),
|
id: Number(hotelDetail[0].id),
|
||||||
});
|
});
|
||||||
|
} else if (isEditMode && hotelDetail && hotelDetail?.length === 0) {
|
||||||
|
mutate(formData);
|
||||||
} else {
|
} else {
|
||||||
mutate(formData);
|
mutate(formData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mealPlans = [
|
|
||||||
"Breakfast Only",
|
|
||||||
"Half Board",
|
|
||||||
"Full Board",
|
|
||||||
"All Inclusive",
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
@@ -391,28 +395,89 @@ const StepTwo = ({
|
|||||||
|
|
||||||
{/* Meal Plan */}
|
{/* Meal Plan */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
|
||||||
name="mealPlan"
|
name="mealPlan"
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormItem>
|
render={({ field }) => {
|
||||||
<Label>{t("Taom rejasi")}</Label>
|
const selectedUser = mealData?.results.find(
|
||||||
|
(u) => String(u.id) === field.value,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<Label className="text-md">Taom rejasi</Label>
|
||||||
|
|
||||||
|
<Popover open={openMeal} onOpenChange={setOpenMeal}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select value={field.value} onValueChange={field.onChange}>
|
<Button
|
||||||
<SelectTrigger className="!h-12 w-full">
|
type="button"
|
||||||
<SelectValue placeholder={t("Tanlang")} />
|
variant="outline"
|
||||||
</SelectTrigger>
|
role="combobox"
|
||||||
<SelectContent>
|
aria-expanded={openMeal}
|
||||||
{mealPlans.map((plan) => (
|
className={cn(
|
||||||
<SelectItem key={plan} value={plan}>
|
"w-full h-12 justify-between",
|
||||||
{t(plan)}
|
!field.value && "text-muted-foreground",
|
||||||
</SelectItem>
|
)}
|
||||||
))}
|
>
|
||||||
</SelectContent>
|
{selectedUser
|
||||||
</Select>
|
? `${selectedUser.name}`
|
||||||
|
: "Taom rejasi tanlang"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
className="w-[--radix-popover-trigger-width] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Qidirish..."
|
||||||
|
className="h-9"
|
||||||
|
value={searchMeal}
|
||||||
|
onValueChange={setSearchMeal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CommandList>
|
||||||
|
{mealLoad ? (
|
||||||
|
<div className="py-6 text-center text-sm">
|
||||||
|
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : mealData && mealData.results.length > 0 ? (
|
||||||
|
<CommandGroup>
|
||||||
|
{mealData.results.map((u) => (
|
||||||
|
<CommandItem
|
||||||
|
key={u.id}
|
||||||
|
value={`${u.id}`}
|
||||||
|
onSelect={() => {
|
||||||
|
field.onChange(String(u.id));
|
||||||
|
setOpenMeal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
field.value === String(u.id)
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{u.name}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
) : (
|
||||||
|
<CommandEmpty>Taom rejasi topilmadi</CommandEmpty>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Hotel Type */}
|
{/* Hotel Type */}
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export default function TourDetailPage() {
|
|||||||
{t("Jo'nash joyi")}
|
{t("Jo'nash joyi")}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-semibold text-white">
|
<p className="font-semibold text-white">
|
||||||
{tour.departure}
|
{tour.departure?.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,7 +233,7 @@ export default function TourDetailPage() {
|
|||||||
{t("Yo'nalish")}
|
{t("Yo'nalish")}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-semibold text-white">
|
<p className="font-semibold text-white">
|
||||||
{tour.destination}
|
{tour.destination?.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
type Role =
|
type Role =
|
||||||
@@ -51,15 +51,17 @@ type Role =
|
|||||||
|
|
||||||
const Tours = ({ user }: { user: Role }) => {
|
const Tours = ({ user }: { user: Role }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||||
const [showPopularDialog, setShowPopularDialog] = useState(false);
|
const [showPopularDialog, setShowPopularDialog] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
|
|
||||||
const { data, isLoading, isError, refetch } = useQuery({
|
const { data, isLoading, isError, refetch } = useQuery({
|
||||||
queryKey: ["all_tours", page],
|
queryKey: ["all_tours", page],
|
||||||
queryFn: () => getAllTours({ page: page, page_size: 10 }),
|
queryFn: () => getAllTours({ page, page_size: 10 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: popularTour } = useQuery({
|
const { data: popularTour } = useQuery({
|
||||||
@@ -68,7 +70,7 @@ const Tours = ({ user }: { user: Role }) => {
|
|||||||
getAllTours({ page: 1, page_size: 10, featured_tickets: true }),
|
getAllTours({ page: 1, page_size: 10, featured_tickets: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: (id: number) => deleteTours({ id }),
|
mutationFn: (id: number) => deleteTours({ id }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||||||
@@ -110,6 +112,11 @@ const Tours = ({ user }: { user: Role }) => {
|
|||||||
setShowPopularDialog(false);
|
setShowPopularDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
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">
|
||||||
@@ -176,12 +183,14 @@ const Tours = ({ user }: { user: Role }) => {
|
|||||||
{data?.data.data.results.map((tour, idx) => (
|
{data?.data.data.results.map((tour, idx) => (
|
||||||
<TableRow key={tour.id}>
|
<TableRow key={tour.id}>
|
||||||
<TableCell className="font-medium text-center">
|
<TableCell className="font-medium text-center">
|
||||||
{(page - 1) * 6 + idx + 1}
|
{(page - 1) * 10 + idx + 1}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2 font-semibold">
|
<div className="flex items-center gap-2 font-semibold">
|
||||||
<Plane className="w-4 h-4 text-primary" />
|
<Plane className="w-4 h-4 text-primary" />
|
||||||
{tour.destination}
|
{typeof tour.destination === "object"
|
||||||
|
? tour.destination?.name
|
||||||
|
: tour.destination}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-primary font-medium">
|
<TableCell className="text-sm text-primary font-medium">
|
||||||
@@ -235,7 +244,12 @@ const Tours = ({ user }: { user: Role }) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigate(`/tours/${tour.id}`)}
|
onClick={() => {
|
||||||
|
navigate(`/tours/${tour.id}`);
|
||||||
|
queryClient.refetchQueries({
|
||||||
|
queryKey: ["tours_detail"],
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("Batafsil")}
|
{t("Batafsil")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -270,8 +284,14 @@ const Tours = ({ user }: { user: Role }) => {
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => confirmDelete(deleteId!)}
|
onClick={() => confirmDelete(deleteId!)}
|
||||||
>
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
{t("O'chirish")}
|
{t("O'chirish")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -301,7 +321,9 @@ const Tours = ({ user }: { user: Role }) => {
|
|||||||
<Plane className="w-4 h-4 text-primary flex-shrink-0" />
|
<Plane className="w-4 h-4 text-primary flex-shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="font-semibold truncate">
|
<p className="font-semibold truncate">
|
||||||
{tour.destination}
|
{typeof tour.destination === "object"
|
||||||
|
? tour.destination?.name
|
||||||
|
: tour.destination}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{tour.duration_days} kun • {tour.hotel_name}
|
{tour.duration_days} kun • {tour.hotel_name}
|
||||||
@@ -337,32 +359,35 @@ const Tours = ({ user }: { user: Role }) => {
|
|||||||
<div className="flex justify-end mt-10 gap-3">
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
<button
|
<button
|
||||||
disabled={page === 1}
|
disabled={page === 1}
|
||||||
onClick={() => setPage((p) => Math.max(p - 1, 1))}
|
onClick={() => updatePage(Math.max(page - 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
{[...Array(data?.data.data.total_pages)].map((_, i) => (
|
|
||||||
|
{[...Array(data?.data.data.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setPage(i + 1)}
|
onClick={() => updatePage(pageNum)}
|
||||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
page === i + 1
|
page === pageNum
|
||||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{pageNum}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={page === data?.data.data.total_pages}
|
disabled={page === data?.data.data.total_pages}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setPage((p) =>
|
updatePage(Math.min(page + 1, data?.data.data.total_pages ?? 1))
|
||||||
Math.min(p + 1, data ? data.data.data.total_pages : 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
countryList,
|
||||||
getAllAmenities,
|
getAllAmenities,
|
||||||
hotelBadge,
|
hotelBadge,
|
||||||
hotelFeature,
|
hotelFeature,
|
||||||
hotelFeatureType,
|
hotelFeatureType,
|
||||||
|
hotelMealList,
|
||||||
hotelTarif,
|
hotelTarif,
|
||||||
hotelTransport,
|
hotelTransport,
|
||||||
hotelType,
|
hotelType,
|
||||||
|
regionList,
|
||||||
} from "@/pages/tours/lib/api";
|
} from "@/pages/tours/lib/api";
|
||||||
import Amenities from "@/pages/tours/ui/Amenities";
|
import Amenities from "@/pages/tours/ui/Amenities";
|
||||||
import BadgeTable from "@/pages/tours/ui/BadgeTable";
|
import BadgeTable from "@/pages/tours/ui/BadgeTable";
|
||||||
|
import CountryTable from "@/pages/tours/ui/CountryTable";
|
||||||
import FeaturesTable from "@/pages/tours/ui/FeaturesTable";
|
import FeaturesTable from "@/pages/tours/ui/FeaturesTable";
|
||||||
import FeaturesTableType from "@/pages/tours/ui/FeaturesTableType";
|
import FeaturesTableType from "@/pages/tours/ui/FeaturesTableType";
|
||||||
|
import HotelType from "@/pages/tours/ui/HotelType";
|
||||||
import MealTable from "@/pages/tours/ui/MealTable";
|
import MealTable from "@/pages/tours/ui/MealTable";
|
||||||
|
import RegionTable from "@/pages/tours/ui/RegionTable";
|
||||||
import TarifTable from "@/pages/tours/ui/TarifTable";
|
import TarifTable from "@/pages/tours/ui/TarifTable";
|
||||||
import TransportTable from "@/pages/tours/ui/TransportTable";
|
import TransportTable from "@/pages/tours/ui/TransportTable";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
@@ -25,11 +31,45 @@ import React, { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
// Error Component
|
||||||
|
const ErrorDisplay: React.FC<{ message: string; onRetry: () => void }> = ({
|
||||||
|
message,
|
||||||
|
onRetry,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[300px] bg-slate-800/50 rounded-lg text-center text-white gap-4 p-6">
|
||||||
|
<AlertTriangle className="w-10 h-10 text-red-500" />
|
||||||
|
<p className="text-lg">{message}</p>
|
||||||
|
<Button
|
||||||
|
onClick={onRetry}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loading Component
|
||||||
|
const LoadingDisplay: React.FC<{ message?: string }> = ({ message }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[300px] bg-slate-800/50 rounded-lg text-white gap-4">
|
||||||
|
<Loader2 className="w-10 h-10 animate-spin text-cyan-400" />
|
||||||
|
<p className="text-slate-400">
|
||||||
|
{message || t("Ma'lumotlar yuklanmoqda...")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ToursSetting: React.FC = () => {
|
const ToursSetting: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [activeTab, setActiveTab] = useState("badge");
|
const [activeTab, setActiveTab] = useState("badge");
|
||||||
const [featureId, setFeatureId] = useState<number | null>(null);
|
const [featureId, setFeatureId] = useState<number | null>(null);
|
||||||
|
const [countryId, setCountryId] = useState<number | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const page = parseInt(searchParams.get("page") || "1", 10);
|
const page = parseInt(searchParams.get("page") || "1", 10);
|
||||||
@@ -87,6 +127,47 @@ const ToursSetting: React.FC = () => {
|
|||||||
select: (res) => res.data.data,
|
select: (res) => res.data.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pageCountry = parseInt(searchParams.get("pageCountry") || "1", 10);
|
||||||
|
const pageSizeCountry = parseInt(
|
||||||
|
searchParams.get("pageSizeCountry") || "10",
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: countryData,
|
||||||
|
isLoading: countryLoad,
|
||||||
|
isError: countryError,
|
||||||
|
refetch: countryRef,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["all_country", pageCountry, pageSizeCountry],
|
||||||
|
queryFn: () =>
|
||||||
|
countryList({ page: pageCountry, page_size: pageSizeCountry }),
|
||||||
|
select: (res) => res.data.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageRegion = parseInt(searchParams.get("pageRegion") || "1", 10);
|
||||||
|
const pageSizeRegion = parseInt(
|
||||||
|
searchParams.get("pageSizeRegion") || "10",
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: regionData,
|
||||||
|
isLoading: regionLoad,
|
||||||
|
isError: regionError,
|
||||||
|
refetch: regionRef,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["all_region", pageRegion, pageSizeRegion, countryId],
|
||||||
|
queryFn: () =>
|
||||||
|
regionList({
|
||||||
|
page: pageRegion,
|
||||||
|
page_size: pageSizeRegion,
|
||||||
|
country: countryId!,
|
||||||
|
}),
|
||||||
|
select: (res) => res.data.data,
|
||||||
|
enabled: !!countryId,
|
||||||
|
});
|
||||||
|
|
||||||
const pageFeature = parseInt(searchParams.get("pageFeature") || "1", 10);
|
const pageFeature = parseInt(searchParams.get("pageFeature") || "1", 10);
|
||||||
const pageSizeFeature = parseInt(
|
const pageSizeFeature = parseInt(
|
||||||
searchParams.get("pageSizeFeature") || "10",
|
searchParams.get("pageSizeFeature") || "10",
|
||||||
@@ -134,60 +215,25 @@ const ToursSetting: React.FC = () => {
|
|||||||
isError: amenitiesError,
|
isError: amenitiesError,
|
||||||
refetch: amenitiesRef,
|
refetch: amenitiesRef,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["all_amenities", page, pageSize],
|
queryKey: ["all_amenities", pageAmenities, pageSizeAmenities],
|
||||||
queryFn: () => getAllAmenities({ page, page_size: pageSize }),
|
queryFn: () =>
|
||||||
|
getAllAmenities({ page: pageAmenities, page_size: pageSizeAmenities }),
|
||||||
select: (res) => res.data.data,
|
select: (res) => res.data.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
const pageMeal = parseInt(searchParams.get("pageMeal") || "1", 10);
|
||||||
isLoading ||
|
const pageSizeMeal = parseInt(searchParams.get("pageSizeMeal") || "10", 10);
|
||||||
tarifLoad ||
|
|
||||||
transportLoad ||
|
|
||||||
typeLoad ||
|
|
||||||
featureLoad ||
|
|
||||||
featureTypeLoad ||
|
|
||||||
amenitiesLoad
|
|
||||||
) {
|
|
||||||
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 (
|
const {
|
||||||
isError ||
|
data: mealData,
|
||||||
tarifError ||
|
isLoading: mealLoad,
|
||||||
transportError ||
|
isError: mealError,
|
||||||
typeError ||
|
refetch: mealRef,
|
||||||
featureError ||
|
} = useQuery({
|
||||||
featureTypeError ||
|
queryKey: ["all_meal", pageMeal, pageSizeMeal],
|
||||||
amenitiesError
|
queryFn: () => hotelMealList({ page: pageMeal, page_size: pageSizeMeal }),
|
||||||
) {
|
select: (res) => res.data.data,
|
||||||
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();
|
|
||||||
tarifRef();
|
|
||||||
transportRef();
|
|
||||||
typeRef();
|
|
||||||
featureRef();
|
|
||||||
featureTypeRef();
|
|
||||||
amenitiesRef();
|
|
||||||
}}
|
|
||||||
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 handleTabChange = (value: string) => {
|
const handleTabChange = (value: string) => {
|
||||||
setActiveTab(value);
|
setActiveTab(value);
|
||||||
@@ -207,6 +253,9 @@ const ToursSetting: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<TabsList className="w-full">
|
<TabsList className="w-full">
|
||||||
<TabsTrigger value="badge">{t("Belgilar (Badge)")}</TabsTrigger>
|
<TabsTrigger value="badge">{t("Belgilar (Badge)")}</TabsTrigger>
|
||||||
|
<TabsTrigger value="meal_plan">
|
||||||
|
{t("Ovqatlanish turlari")}
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="tarif">{t("Tariflar")}</TabsTrigger>
|
<TabsTrigger value="tarif">{t("Tariflar")}</TabsTrigger>
|
||||||
<TabsTrigger value="transport">{t("Transport")}</TabsTrigger>
|
<TabsTrigger value="transport">{t("Transport")}</TabsTrigger>
|
||||||
<TabsTrigger value="meal">{t("Qulayliklar")}</TabsTrigger>
|
<TabsTrigger value="meal">{t("Qulayliklar")}</TabsTrigger>
|
||||||
@@ -214,40 +263,116 @@ const ToursSetting: React.FC = () => {
|
|||||||
<TabsTrigger value="hotel_features">
|
<TabsTrigger value="hotel_features">
|
||||||
{t("Otel sharoitlari")}
|
{t("Otel sharoitlari")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="country">{t("Davlatlar")}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="badge" className="space-y-4">
|
<TabsContent value="badge" className="space-y-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<LoadingDisplay />
|
||||||
|
) : isError ? (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t("Belgilarni yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={refetch}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<BadgeTable data={data} page={page} pageSize={pageSize} />
|
<BadgeTable data={data} page={page} pageSize={pageSize} />
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="tarif" className="space-y-4">
|
<TabsContent value="tarif" className="space-y-4">
|
||||||
|
{tarifLoad ? (
|
||||||
|
<LoadingDisplay />
|
||||||
|
) : tarifError ? (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t("Tariflarni yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={tarifRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<TarifTable
|
<TarifTable
|
||||||
data={tarifData}
|
data={tarifData}
|
||||||
page={pageTarif}
|
page={pageTarif}
|
||||||
pageSize={pageSizeTarif}
|
pageSize={pageSizeTarif}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="transport" className="space-y-4">
|
<TabsContent value="transport" className="space-y-4">
|
||||||
|
{transportLoad ? (
|
||||||
|
<LoadingDisplay />
|
||||||
|
) : transportError ? (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t("Transportlarni yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={transportRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<TransportTable
|
<TransportTable
|
||||||
data={transportData}
|
data={transportData}
|
||||||
page={pageTransport}
|
page={pageTransport}
|
||||||
pageSize={pageSizeTransport}
|
pageSize={pageSizeTransport}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="meal" className="space-y-4">
|
<TabsContent value="meal" className="space-y-4">
|
||||||
|
{amenitiesLoad ? (
|
||||||
|
<LoadingDisplay />
|
||||||
|
) : amenitiesError ? (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t("Qulayliklarni yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={amenitiesRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Amenities
|
<Amenities
|
||||||
data={amenitiesData}
|
data={amenitiesData}
|
||||||
page={pageAmenities}
|
page={pageAmenities}
|
||||||
pageSize={pageSizeAmenities}
|
pageSize={pageSizeAmenities}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="hotel_type" className="space-y-4">
|
<TabsContent value="hotel_type" className="space-y-4">
|
||||||
<MealTable
|
{typeLoad ? (
|
||||||
data={typeData}
|
<LoadingDisplay />
|
||||||
page={pageTransport}
|
) : typeError ? (
|
||||||
pageSize={pageSizeTransport}
|
<ErrorDisplay
|
||||||
|
message={t("Otel turlarini yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={typeRef}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<HotelType
|
||||||
|
data={typeData}
|
||||||
|
page={pageType}
|
||||||
|
pageSize={pageSizeType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="meal_plan" className="space-y-4">
|
||||||
|
{mealLoad ? (
|
||||||
|
<LoadingDisplay />
|
||||||
|
) : mealError ? (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t("Otel turlarini yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={mealRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MealTable
|
||||||
|
data={mealData}
|
||||||
|
page={pageMeal}
|
||||||
|
pageSize={pageSizeMeal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="hotel_features" className="space-y-4">
|
<TabsContent value="hotel_features" className="space-y-4">
|
||||||
|
{featureLoad ? (
|
||||||
|
<LoadingDisplay />
|
||||||
|
) : featureError ? (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t("Otel sharoitlarini yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={featureRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<FeaturesTable
|
<FeaturesTable
|
||||||
data={featureData}
|
data={featureData}
|
||||||
page={pageFeature}
|
page={pageFeature}
|
||||||
@@ -255,14 +380,62 @@ const ToursSetting: React.FC = () => {
|
|||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
setFeatureId={setFeatureId}
|
setFeatureId={setFeatureId}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="feature_type" className="space-y-4">
|
<TabsContent value="feature_type" className="space-y-4">
|
||||||
|
{featureTypeLoad ? (
|
||||||
|
<LoadingDisplay />
|
||||||
|
) : featureTypeError ? (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t("Sharoit turlarini yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={featureTypeRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<FeaturesTableType
|
<FeaturesTableType
|
||||||
data={featureTypeData}
|
data={featureTypeData}
|
||||||
page={pageFeature}
|
page={pageFeature}
|
||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
pageSize={pageSizeFeature}
|
pageSize={pageSizeFeature}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="country" className="space-y-4">
|
||||||
|
{countryLoad ? (
|
||||||
|
<LoadingDisplay />
|
||||||
|
) : countryError ? (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t("Sharoit turlarini yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={countryRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CountryTable
|
||||||
|
data={countryData}
|
||||||
|
page={pageCountry}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
setFeatureId={setCountryId}
|
||||||
|
pageSize={pageSizeCountry}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="region" className="space-y-4">
|
||||||
|
{regionLoad ? (
|
||||||
|
<LoadingDisplay />
|
||||||
|
) : regionError ? (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t("Sharoit turlarini yuklashda xatolik yuz berdi.")}
|
||||||
|
onRetry={regionRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RegionTable
|
||||||
|
data={regionData}
|
||||||
|
page={pageRegion}
|
||||||
|
featureId={countryId}
|
||||||
|
pageSize={pageSizeRegion}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "@/shared/ui/form";
|
} from "@/shared/ui/form";
|
||||||
import { Input } from "@/shared/ui/input";
|
import { Input } from "@/shared/ui/input";
|
||||||
import IconSelect from "@/shared/ui/iocnSelect";
|
import IconSelect from "@/shared/ui/iocnSelect";
|
||||||
|
import { Label } from "@/shared/ui/label";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -312,15 +313,17 @@ const TransportTable = ({
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="icon_name"
|
name="icon_name"
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("Belgi (Icon)")}</FormLabel>
|
<Label className="text-md">{t("Belgi (Icon)")}</Label>
|
||||||
<FormControl className="w-full">
|
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<IconSelect
|
<IconSelect
|
||||||
setSelectedIcon={setSelectedIcon}
|
selectedIcon={field.value ?? ""}
|
||||||
selectedIcon={selectedIcon}
|
setSelectedIcon={(val: string) => field.onChange(val)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</div>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/shared/ui/form";
|
} from "@/shared/ui/form";
|
||||||
import { Input } from "@/shared/ui/input";
|
import { Input } from "@/shared/ui/input";
|
||||||
import { ArrowLeft, Mail, Phone, Save, User } from "lucide-react";
|
import { ArrowLeft, Loader2, Mail, Phone, Save, User } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
@@ -250,8 +250,14 @@ export default function EditUser() {
|
|||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="flex-1 h-11 rounded-lg bg-gradient-to-r from-emerald-600 to-teal-700 hover:from-emerald-700 hover:to-teal-800 text-white font-medium"
|
className="flex-1 h-11 rounded-lg bg-gradient-to-r from-emerald-600 to-teal-700 hover:from-emerald-700 hover:to-teal-800 text-white font-medium"
|
||||||
>
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Save className="w-5 h-5 mr-2" />
|
<Save className="w-5 h-5 mr-2" />
|
||||||
{t("Yangilash")}
|
{t("Yangilash")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,20 +18,28 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
export default function UserList() {
|
export default function UserList() {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const initialPage = Number(searchParams.get("page")) || 1;
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
const usersPerPage = 6;
|
const usersPerPage = 6;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const updatePage = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
setSearchParams({ page: newPage.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
const { data, isLoading, isError, refetch } = useQuery({
|
const { data, isLoading, isError, refetch } = useQuery({
|
||||||
queryKey: ["user_all", currentPage, searchQuery],
|
queryKey: ["user_all", page, searchQuery],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
getAllUsers({
|
getAllUsers({
|
||||||
page: currentPage,
|
page,
|
||||||
page_size: usersPerPage,
|
page_size: usersPerPage,
|
||||||
search: searchQuery,
|
search: searchQuery,
|
||||||
}),
|
}),
|
||||||
@@ -119,7 +127,7 @@ export default function UserList() {
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchQuery(e.target.value);
|
setSearchQuery(e.target.value);
|
||||||
setCurrentPage(1);
|
updatePage(1);
|
||||||
}}
|
}}
|
||||||
className="w-full pl-14 pr-4 py-3 bg-slate-700/30 border border-slate-600/50 text-white placeholder-slate-400 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
className="w-full pl-14 pr-4 py-3 bg-slate-700/30 border border-slate-600/50 text-white placeholder-slate-400 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||||
/>
|
/>
|
||||||
@@ -214,41 +222,40 @@ export default function UserList() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end mt-10 gap-3">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={page === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => updatePage(Math.max(page - 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{[...Array(data?.data.data.total_pages)].map((_, i) => (
|
{[...Array(data?.data.data.total_pages)].map((_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setCurrentPage(i + 1)}
|
onClick={() => updatePage(pageNum)}
|
||||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
|
||||||
currentPage === i + 1
|
page === pageNum
|
||||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
|
||||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i + 1}
|
{pageNum}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={
|
disabled={page === data?.data.data.total_pages}
|
||||||
data
|
|
||||||
? currentPage === data.data.data.total_pages
|
|
||||||
: currentPage === 1
|
|
||||||
}
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentPage((p) =>
|
updatePage(
|
||||||
Math.min(p + 1, data ? data.data.data.total_pages : 1),
|
Math.min(page + 1, data?.data.data.total_pages ?? 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"
|
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ 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/";
|
const PAYMENT_AGENCY = "dashboard/dashboard-site-agency-payments/";
|
||||||
const PAYOT_REQUEST = "dashboard/dashboard-agency-payout-request/";
|
const PAYOT_REQUEST = "dashboard/dashboard-agency-payout-request/";
|
||||||
|
const COUNTRY = "dashboard/dashboard-ticket-settings-country/";
|
||||||
|
const REGION = "dashboard/dashboard-ticket-settings-region/";
|
||||||
|
const MEAL_PLAN = "dashboard/dashboard-hotel-meal-plan/";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AGENCY_ORDERS,
|
AGENCY_ORDERS,
|
||||||
@@ -45,6 +48,7 @@ export {
|
|||||||
AUTH_LOGIN,
|
AUTH_LOGIN,
|
||||||
BANNER,
|
BANNER,
|
||||||
BASE_URL,
|
BASE_URL,
|
||||||
|
COUNTRY,
|
||||||
DOWNLOAD_PDF,
|
DOWNLOAD_PDF,
|
||||||
FAQ,
|
FAQ,
|
||||||
FAQ_CATEGORIES,
|
FAQ_CATEGORIES,
|
||||||
@@ -60,12 +64,14 @@ export {
|
|||||||
HOTEL_FEATURES_TYPE,
|
HOTEL_FEATURES_TYPE,
|
||||||
HOTEL_TARIF,
|
HOTEL_TARIF,
|
||||||
HPTEL_TYPES,
|
HPTEL_TYPES,
|
||||||
|
MEAL_PLAN,
|
||||||
NEWS,
|
NEWS,
|
||||||
NEWS_CATEGORY,
|
NEWS_CATEGORY,
|
||||||
OFFERTA,
|
OFFERTA,
|
||||||
PAYMENT_AGENCY,
|
PAYMENT_AGENCY,
|
||||||
PAYOT_REQUEST,
|
PAYOT_REQUEST,
|
||||||
POPULAR_TOURS,
|
POPULAR_TOURS,
|
||||||
|
REGION,
|
||||||
SITE_SEO,
|
SITE_SEO,
|
||||||
SITE_SETTING,
|
SITE_SETTING,
|
||||||
SUPPORT_AGENCY,
|
SUPPORT_AGENCY,
|
||||||
|
|||||||
@@ -30,10 +30,14 @@ const httpClient = axios.create({
|
|||||||
|
|
||||||
httpClient.interceptors.request.use(
|
httpClient.interceptors.request.use(
|
||||||
async (config) => {
|
async (config) => {
|
||||||
// Faqat GET so'rovlarida Accept-Language headerini qo'shish
|
const method: string = (config.method || "").toLowerCase();
|
||||||
if (config.method?.toLowerCase() === "get") {
|
|
||||||
const language = i18n.language;
|
if (method === "get") {
|
||||||
config.headers["Accept-Language"] = language;
|
// GET so'rovlarda hozirgi i18n tilini yuboramiz
|
||||||
|
config.headers["Accept-Language"] = i18n.language;
|
||||||
|
} else if (["put", "patch", "post", "delete"].includes(method)) {
|
||||||
|
// PUT, PATCH, POST, DELETE da faqat "uz"
|
||||||
|
config.headers["Accept-Language"] = "uz";
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = getAuthToken();
|
const accessToken = getAuthToken();
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"Foydalanuvchilar": "Пользователи",
|
"Foydalanuvchilar": "Пользователи",
|
||||||
"Tur firmalar": "Турфирмы",
|
"Tur firmalar": "Турфирмы",
|
||||||
"Xodimlar": "Сотрудники",
|
"Xodimlar": "Сотрудники",
|
||||||
|
"Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz": "Вы хотите добавить нового пользователя в агентство",
|
||||||
|
"Foydalanuvchi qo'shish": "Добавить пользователя",
|
||||||
|
"Ovqatlanish turlari": "Виды питания",
|
||||||
|
"Davlatlar": "Государство",
|
||||||
"Byudjet": "Бюджет",
|
"Byudjet": "Бюджет",
|
||||||
"Turlar": "Туры",
|
"Turlar": "Туры",
|
||||||
"Tur sozlamalari": "Настройки туров",
|
"Tur sozlamalari": "Настройки туров",
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
"Xodimlar": "Xodimlar",
|
"Xodimlar": "Xodimlar",
|
||||||
"Byudjet": "Byudjet",
|
"Byudjet": "Byudjet",
|
||||||
"Turlar": "Turlar",
|
"Turlar": "Turlar",
|
||||||
|
"Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz": "Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz",
|
||||||
|
"Foydalanuvchi qo'shish": "Foydalanuvchi qo'shish",
|
||||||
|
"Ovqalanish turlari": "Ovqalanish turlari",
|
||||||
|
"Davlatlar": "Davlatlar",
|
||||||
"Tur sozlamalari": "Tur sozlamalari",
|
"Tur sozlamalari": "Tur sozlamalari",
|
||||||
"Bronlar": "Bronlar",
|
"Bronlar": "Bronlar",
|
||||||
"Yangiliklar": "Yangiliklar",
|
"Yangiliklar": "Yangiliklar",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export const hotelIcons = [
|
export const hotelIcons = [
|
||||||
{ name: "Wifi", uz: "Wi-Fi", ru: "Wi-Fi" },
|
{ name: "Wifi", uz: "Wi-Fi", ru: "Wi-Fi" },
|
||||||
{ name: "Bus", uz: "Avtobus", ru: "Автобус" },
|
{ name: "Bus", uz: "Avtobus", ru: "Автобус" },
|
||||||
{ name: "CarFront", uz: "Auto", ru: "Авто" },
|
|
||||||
{ name: "PlaneTakeoff", uz: "Avia", ru: "Авиа" },
|
{ name: "PlaneTakeoff", uz: "Avia", ru: "Авиа" },
|
||||||
{ name: "WifiOff", uz: "Wi-Fi mavjud emas", ru: "Нет Wi-Fi" },
|
{ name: "WifiOff", uz: "Wi-Fi mavjud emas", ru: "Нет Wi-Fi" },
|
||||||
{ name: "Tv", uz: "Televizor", ru: "Телевизор" },
|
{ name: "Tv", uz: "Televizor", ru: "Телевизор" },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
|
||||||
@@ -43,4 +43,4 @@ function PopoverAnchor({
|
|||||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
|
||||||
|
|||||||
Reference in New Issue
Block a user