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 }), 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 +293,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>

View File

@@ -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>
)} )}

View File

@@ -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>
); );

View File

@@ -40,7 +40,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 notogri"), email: z.string().email("Email notogri"),
phone: z.string().min(3, "Telefon raqami 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>; type FormData = z.infer<typeof formSchema>;
@@ -63,12 +63,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;

View File

@@ -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>

View File

@@ -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";
@@ -134,7 +134,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 +150,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: () => {
@@ -404,7 +404,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>

View File

@@ -35,7 +35,7 @@ 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 } 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 { 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";
@@ -231,7 +231,7 @@ const StepOne = ({
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 });
}, },
@@ -2204,7 +2204,7 @@ 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 ? <Loader2 className="animate-spin" /> : t("Saqlash")}
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -68,7 +68,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"] });
@@ -270,8 +270,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>

View File

@@ -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>

View File

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

View File

@@ -10,6 +10,8 @@
"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",
"Tur sozlamalari": "Tur sozlamalari", "Tur sozlamalari": "Tur sozlamalari",
"Bronlar": "Bronlar", "Bronlar": "Bronlar",
"Yangiliklar": "Yangiliklar", "Yangiliklar": "Yangiliklar",