bug fix loading

This commit is contained in:
Samandar Turgunboyev
2025-11-11 16:05:22 +05:00
parent 8a4618b454
commit 64f8467f41
11 changed files with 271 additions and 155 deletions

View File

@@ -41,7 +41,7 @@ export default function TourAgenciesPage() {
queryFn: () => getAllAgency({ page: currentPage, page_size: itemsPerPage }),
});
const { mutate } = useMutation({
const { mutate, isPending } = useMutation({
mutationFn: ({ id }: { id: number }) => {
return deleteAgency({ id });
},
@@ -293,7 +293,11 @@ export default function TourAgenciesPage() {
onClick={() => handleDelete(agency.id)}
className="bg-red-600 hover:bg-red-700 text-white"
>
{t("O'chirish")}
{isPending ? (
<Loader2 className="animate-spin" />
) : (
t("O'chirish")
)}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -8,6 +8,7 @@ import {
updateAgencyStatus,
} from "@/pages/agencies/lib/api";
import { AgencyUsersSection } from "@/pages/agencies/ui/AgencyUsersSection";
import { createTourAdmin } from "@/pages/support/lib/api";
import formatPhone from "@/shared/lib/formatPhone";
import formatPrice from "@/shared/lib/formatPrice";
import { Badge } from "@/shared/ui/badge";
@@ -50,6 +51,7 @@ export default function AgencyDetailPage() {
const router = useNavigate();
const { t } = useTranslation();
const [edit, setEdit] = useState<boolean>(false);
const [added, setAdded] = useState<boolean>(false);
const queryClient = useQueryClient();
const [openUser, setOpenUser] = useState<boolean>(false);
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({
mutationFn: (userId: number) => agencyUserDelete(userId),
onSuccess: () => {
@@ -126,6 +144,10 @@ export default function AgencyDetailPage() {
updateUserMutation.mutate(user);
};
const handleAddeUser = () => {
createAdmin.mutate(Number(params.id!));
};
const handleDeleteUser = (userId: number) => {
deleteUserMutation.mutate(userId);
};
@@ -409,8 +431,14 @@ export default function AgencyDetailPage() {
{agency?.data.data && (
<div className="mb-8">
<AgencyUsersSection
agencyId={Number(params.id)}
added={added}
setAdded={setAdded}
edit={edit}
handleAddeUser={handleAddeUser}
setEdit={setEdit}
isPending={updateUserMutation.isPending}
createAdminPending={createAdmin.isPending}
users={
Array.isArray(agency?.data.data)
? agency?.data.data
@@ -419,6 +447,7 @@ export default function AgencyDetailPage() {
onEdit={handleEditUser}
onDelete={handleDeleteUser}
isLoading={isLoadingUsers}
deletePending={deleteUserMutation.isPending}
/>
</div>
)}

View File

@@ -13,7 +13,15 @@ import {
DialogTitle,
DialogTrigger,
} 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 { useTranslation } from "react-i18next";
@@ -27,22 +35,36 @@ interface AgencyUser {
interface AgencyUsersProps {
users: AgencyUser[];
agencyId: number;
onEdit: (userId: number) => void;
handleAddeUser: () => void;
onDelete: (userId: number) => void;
isLoading?: boolean;
isPending: boolean;
createAdminPending: boolean;
edit: boolean;
added: boolean;
deletePending: boolean;
setEdit: Dispatch<SetStateAction<boolean>>;
setAdded: Dispatch<SetStateAction<boolean>>;
}
export function AgencyUsersSection({
users,
onEdit,
setAdded,
added,
edit,
setEdit,
handleAddeUser,
createAdminPending,
onDelete,
isLoading = false,
deletePending,
isPending = false,
}: AgencyUsersProps) {
const { t } = useTranslation();
const getRoleBadge = (role: string) => {
const roleColors: Record<string, string> = {
admin: "bg-purple-500/20 text-purple-300 border-purple-500/40",
@@ -75,160 +97,197 @@ export function AgencyUsersSection({
);
}
if (!users || users.length === 0) {
return (
<Card className="border border-gray-700 shadow-lg bg-gray-800">
<CardHeader>
return (
<Card className="border border-gray-700 shadow-lg bg-gray-800">
<CardHeader className="flex justify-between items-center">
<div>
<CardTitle className="text-2xl text-white flex items-center gap-2">
<User className="w-6 h-6" />
{t("Agentlik foydalanuvchilari")}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-400">
{t("Agentlik bilan bog'langan foydalanuvchilar ro'yxati")}
</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>
<CardContent>
{users.length === 0 ? (
<div className="text-center text-gray-400 py-8">
{t("Hozircha foydalanuvchilar yo'q")}
</div>
</CardContent>
</Card>
);
}
return (
<Card className="border border-gray-700 shadow-lg bg-gray-800">
<CardHeader>
<CardTitle className="text-2xl text-white flex items-center gap-2">
<User className="w-6 h-6" />
{t("Agentlik foydalanuvchilari")}
</CardTitle>
<p className="text-gray-400">
{t("Agentlik bilan bog'langan foydalanuvchilar ro'yxati")}
</p>
</CardHeader>
<CardContent>
<div className="space-y-3">
{users.map((user) => (
<div
key={user.id}
className="p-5 border border-gray-700 rounded-xl bg-gray-800 hover:bg-gray-750 transition-all"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
<User className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-white">
{user.first_name} {user.last_name}
</h3>
{getRoleBadge(user.role)}
) : (
<div className="space-y-3">
{users.map((user) => (
<div
key={user.id}
className="p-5 border border-gray-700 rounded-xl bg-gray-800 hover:bg-gray-750 transition-all"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
<User className="w-6 h-6 text-white" />
</div>
<div className="flex flex-wrap items-center gap-4 text-sm">
<div className="flex items-center gap-2 text-gray-400">
<Phone className="w-4 h-4" />
<span>{formatPhone(user.phone)}</span>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-white">
{user.first_name} {user.last_name}
</h3>
{getRoleBadge(user.role)}
</div>
<div className="flex items-center gap-2 text-gray-400">
<Shield className="w-4 h-4" />
<span>ID: {user.id}</span>
<div className="flex flex-wrap items-center gap-4 text-sm">
<div className="flex items-center gap-2 text-gray-400">
<Phone className="w-4 h-4" />
<span>{formatPhone(user.phone)}</span>
</div>
<div className="flex items-center gap-2 text-gray-400">
<Shield className="w-4 h-4" />
<span>ID: {user.id}</span>
</div>
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
{/* Edit Confirm Dialog */}
<Dialog open={edit} onOpenChange={setEdit}>
<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"
>
<Pencil
className="w-4 h-4"
onClick={() => setEdit(true)}
/>
</Button>
</DialogTrigger>
<DialogContent className="bg-gray-800 border-gray-700 text-white">
<DialogHeader>
<DialogTitle>
{t("Foydalanuvchini tahrirlash")}
</DialogTitle>
<DialogDescription className="text-gray-400">
{t("Haqiqatan ham")}{" "}
<span className="font-semibold text-white">
{user.first_name} {user.last_name}
</span>{" "}
{t("ni tahrirlamoqchimisiz?")}
</DialogDescription>
</DialogHeader>
<div className="flex items-center gap-2">
{/* ✏️ Edit */}
<Dialog open={edit} onOpenChange={setEdit}>
<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"
>
<Pencil
className="w-4 h-4"
onClick={() => setEdit(true)}
/>
</Button>
</DialogTrigger>
<DialogContent className="bg-gray-800 border-gray-700 text-white">
<DialogHeader>
<DialogTitle>
{t("Foydalanuvchini tahrirlash")}
</DialogTitle>
<DialogDescription className="text-gray-400">
{t("Haqiqatan ham")}{" "}
<span className="font-semibold text-white">
{user.first_name} {user.last_name}
</span>{" "}
{t("ni tahrirlamoqchimisiz?")}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-white"
onClick={() => setEdit(true)}
>
{t("Bekor qilish")}
</Button>
<Button
onClick={() => onEdit(user.id)}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
{t("Tahrirlash")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DialogFooter>
<Button
className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-white"
onClick={() => setEdit(false)}
>
{t("Bekor qilish")}
</Button>
<Button
onClick={() => onEdit(user.id)}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
{isPending ? (
<Loader2 className="animate-spin" />
) : (
t("Tahrirlash")
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirm Dialog */}
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="icon"
className="border-gray-600 bg-gray-700 hover:bg-red-900/20 hover:border-red-500 text-red-400 hover:text-red-300"
>
<Trash2 className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent className="bg-gray-800 border-gray-700 text-white">
<DialogHeader>
<DialogTitle>
{t("Foydalanuvchini o'chirish")}
</DialogTitle>
<DialogDescription className="text-gray-400">
{t("Haqiqatan ham")}{" "}
<span className="font-semibold text-white">
{user.first_name} {user.last_name}
</span>{" "}
{t(
"ni o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.",
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-white">
{t("Bekor qilish")}
</Button>
{/* 🗑 Delete */}
<Dialog>
<DialogTrigger asChild>
<Button
onClick={() => onDelete(user.id)}
className="bg-red-600 hover:bg-red-700 text-white"
variant="outline"
size="icon"
className="border-gray-600 bg-gray-700 hover:bg-red-900/20 hover:border-red-500 text-red-400 hover:text-red-300"
>
{t("O'chirish")}
<Trash2 className="w-4 h-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogTrigger>
<DialogContent className="bg-gray-800 border-gray-700 text-white">
<DialogHeader>
<DialogTitle>
{t("Foydalanuvchini o'chirish")}
</DialogTitle>
<DialogDescription className="text-gray-400">
{t("Haqiqatan ham")}{" "}
<span className="font-semibold text-white">
{user.first_name} {user.last_name}
</span>{" "}
{t(
"ni o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.",
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button className="border-gray-600 bg-gray-700 hover:bg-gray-600 text-white">
{t("Bekor qilish")}
</Button>
<Button
onClick={() => onDelete(user.id)}
className="bg-red-600 hover:bg-red-700 text-white"
>
{deletePending ? (
<Loader2 className="animate-spin" />
) : (
t("O'chirish")
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
)}
</CardContent>
</Card>
);

View File

@@ -40,7 +40,7 @@ const formSchema = z.object({
addres: z.string().min(1, "Manzil kiritish shart"),
email: z.string().email("Email notogri"),
phone: z.string().min(3, "Telefon raqami notogri"),
web_site: z.string().url("URL notogri"),
web_site: z.string().min(1, "URL notogri"),
});
type FormData = z.infer<typeof formSchema>;
@@ -63,12 +63,12 @@ const EditAgency = () => {
});
const queryClient = useQueryClient();
const { data, isPending } = useQuery({
const { data } = useQuery({
queryKey: ["detail_agency", params.id],
queryFn: () => getDetailAgency({ id: Number(params.id) }),
});
const { mutate } = useMutation({
const { mutate, isPending } = useMutation({
mutationFn: (body: {
status: "pending" | "approved" | "cancelled";
custom_id?: string;

View File

@@ -95,7 +95,7 @@ const NewsCategory = () => {
setIsDialogOpen(true);
};
const { mutate: added } = useMutation({
const { mutate: added, isPending } = useMutation({
mutationFn: (body: { name: string; name_ru: string }) =>
addNewsCategory(body),
onSuccess: () => {
@@ -111,7 +111,7 @@ const NewsCategory = () => {
},
});
const { mutate: edit } = useMutation({
const { mutate: edit, isPending: editPending } = useMutation({
mutationFn: ({
body,
id,
@@ -349,7 +349,11 @@ const NewsCategory = () => {
type="submit"
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>
</div>
</form>

View File

@@ -16,7 +16,7 @@ import { Label } from "@/shared/ui/label";
import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
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 { useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
@@ -134,7 +134,7 @@ const StepTwo = ({
if (file) form.setValue(`sections.${index}.image`, file);
};
const { mutate: added } = useMutation({
const { mutate: added, isPending } = useMutation({
mutationFn: (body: FormData) => addNews(body),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["all_news"] });
@@ -150,7 +150,7 @@ const StepTwo = ({
},
});
const { mutate: update } = useMutation({
const { mutate: update, isPending: updatePending } = useMutation({
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
updateNews({ id, body }),
onSuccess: () => {
@@ -404,7 +404,11 @@ const StepTwo = ({
type="submit"
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>
</div>
</form>

View File

@@ -35,7 +35,7 @@ import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
import { ChevronDownIcon, SquareCheckBig, XIcon } from "lucide-react";
import { ChevronDownIcon, Loader2, SquareCheckBig, XIcon } from "lucide-react";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
@@ -231,7 +231,7 @@ const StepOne = ({
const selectedDate = watch("departureDateTime.date");
const selectedDateTravel = watch("travelDateTime.date");
const { mutate: create } = useMutation({
const { mutate: create, isPending } = useMutation({
mutationFn: (body: FormData) => {
return createTours({ body });
},
@@ -2204,7 +2204,7 @@ const StepOne = ({
type="submit"
className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-600 cursor-pointer"
>
{t("Saqlash")}
{isPending ? <Loader2 className="animate-spin" /> : t("Saqlash")}
</Button>
</div>
</form>

View File

@@ -68,7 +68,7 @@ const Tours = ({ user }: { user: Role }) => {
getAllTours({ page: 1, page_size: 10, featured_tickets: true }),
});
const { mutate } = useMutation({
const { mutate, isPending } = useMutation({
mutationFn: (id: number) => deleteTours({ id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["all_tours"] });
@@ -270,8 +270,14 @@ const Tours = ({ user }: { user: Role }) => {
variant="destructive"
onClick={() => confirmDelete(deleteId!)}
>
<Trash2 className="w-4 h-4 mr-2" />
{t("O'chirish")}
{isPending ? (
<Loader2 className="animate-spin" />
) : (
<>
<Trash2 className="w-4 h-4 mr-2" />
{t("O'chirish")}
</>
)}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -19,7 +19,7 @@ import {
FormMessage,
} from "@/shared/ui/form";
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 { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
@@ -250,8 +250,14 @@ export default function EditUser() {
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"
>
<Save className="w-5 h-5 mr-2" />
{t("Yangilash")}
{isPending ? (
<Loader2 className="animate-spin" />
) : (
<>
<Save className="w-5 h-5 mr-2" />
{t("Yangilash")}
</>
)}
</Button>
</div>
</form>

View File

@@ -8,6 +8,8 @@
"Foydalanuvchilar": "Пользователи",
"Tur firmalar": "Турфирмы",
"Xodimlar": "Сотрудники",
"Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz": "Вы хотите добавить нового пользователя в агентство",
"Foydalanuvchi qo'shish": "Добавить пользователя",
"Byudjet": "Бюджет",
"Turlar": "Туры",
"Tur sozlamalari": "Настройки туров",

View File

@@ -10,6 +10,8 @@
"Xodimlar": "Xodimlar",
"Byudjet": "Byudjet",
"Turlar": "Turlar",
"Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz": "Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz",
"Foydalanuvchi qo'shish": "Foydalanuvchi qo'shish",
"Tur sozlamalari": "Tur sozlamalari",
"Bronlar": "Bronlar",
"Yangiliklar": "Yangiliklar",