api ulangan

This commit is contained in:
Samandar Turgunboyev
2025-12-22 19:03:57 +05:00
parent fd397b670b
commit 7f2fe3868b
121 changed files with 12636 additions and 5528 deletions

View File

@@ -0,0 +1,180 @@
import { user_api } from "@/features/users/lib/api";
import type { UserCreateReq, UserData } from "@/features/users/lib/data";
import { AddedUser } from "@/features/users/lib/form";
import { Button } from "@/shared/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Loader2 } from "lucide-react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type z from "zod";
interface UserFormProps {
initialData: UserData | null;
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => {
const form = useForm<z.infer<typeof AddedUser>>({
resolver: zodResolver(AddedUser),
defaultValues: {
is_superuser: initialData?.is_superuser ? "true" : "false",
password: "",
username: initialData?.username || "",
},
});
const queryClient = useQueryClient();
const { mutate: update } = useMutation({
mutationFn: ({ body, id }: { id: string; body: UserCreateReq }) =>
user_api.update({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["user_list"] });
toast.success(`Foydalanuvchi tahrirlandi`);
setDialogOpen(false);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const { mutate: create, isPending: createPending } = useMutation({
mutationFn: (body: UserCreateReq) => user_api.create(body),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["user_list"] });
toast.success(`Foydalanuvchi qo'shildi`);
setDialogOpen(false);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
function onSubmit(values: z.infer<typeof AddedUser>) {
if (initialData) {
update({
body: {
is_superuser: values.is_superuser === "true" ? true : false,
username: values.username,
password: values.password,
},
id: initialData.id,
});
} else if (initialData === null) {
create({
is_superuser: values.is_superuser === "true" ? true : false,
username: values.username,
password: values.password,
});
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<Label className="text-md">Username</Label>
<FormControl>
<Input
placeholder="Username"
{...field}
className="!h-12 !text-md"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<Label className="text-md">Parol</Label>
<FormControl>
<Input
placeholder="Parol"
{...field}
className="!h-12 !text-md"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="is_superuser"
render={({ field }) => (
<FormItem>
<Label className="text-md mr-4">Foydalanuvchi roli</Label>
<FormControl>
<Select
value={String(field.value)}
onValueChange={field.onChange}
>
<SelectTrigger className="w-full !h-12">
<SelectValue placeholder="Foydalanuvchi roli" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">Admin</SelectItem>
<SelectItem value="false">Foydalanuvchi</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
>
{createPending ? (
<Loader2 className="animate-spin" />
) : initialData ? (
"Saqlash"
) : (
"Qo'shish"
)}
</Button>
</form>
</Form>
);
};
export default AddUsers;

View File

@@ -0,0 +1,89 @@
import { user_api } from "@/features/users/lib/api";
import type { UserData } from "@/features/users/lib/data";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Loader2, Trash, X } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
import { toast } from "sonner";
interface Props {
opneDelete: boolean;
setOpenDelete: Dispatch<SetStateAction<boolean>>;
setUserDelete: Dispatch<SetStateAction<UserData | null>>;
userDelete: UserData | null;
}
const DeleteUser = ({
opneDelete,
setOpenDelete,
userDelete,
setUserDelete,
}: Props) => {
const queryClient = useQueryClient();
const { mutate: deleteUser, isPending } = useMutation({
mutationFn: ({ id }: { id: string }) => user_api.delete({ id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["user_list"] });
toast.success(`Foydalanuvchi o'chirildi`);
setOpenDelete(false);
setUserDelete(null);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
return (
<Dialog open={opneDelete} onOpenChange={setOpenDelete}>
<DialogContent>
<DialogHeader>
<DialogTitle>Foydalanuvchini o'chrish</DialogTitle>
<DialogDescription className="text-md font-semibold">
Siz rostan ham {userDelete?.username}
nomli foydalanuvchini o'chimoqchimiszi
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-blue-600 cursor-pointer hover:bg-blue-600"
onClick={() => setOpenDelete(false)}
>
<X />
Bekor qilish
</Button>
<Button
variant={"destructive"}
onClick={() => userDelete && deleteUser({ id: userDelete.id })}
>
{isPending ? (
<Loader2 className="animate-spin" />
) : (
<>
<Trash />
O'chirish
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default DeleteUser;

View File

@@ -0,0 +1,57 @@
import type { RegionListResData } from "@/features/region/lib/data";
import type { UserData } from "@/features/users/lib/data";
import AddUsers from "@/features/users/ui/AddUsers";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Plus } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react";
interface Props {
setRegionValue: Dispatch<SetStateAction<RegionListResData | null>>;
dialogOpen: boolean;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
editingUser: UserData | null;
setEditingUser: Dispatch<SetStateAction<UserData | null>>;
}
const Filter = ({
dialogOpen,
setDialogOpen,
editingUser,
setEditingUser,
}: Props) => {
return (
<div className="flex flex-wrap gap-2 items-center">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
onClick={() => setEditingUser(null)}
>
<Plus className="!h-5 !w-5" /> Qo'shish
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{editingUser
? "Foydalanuvchini tahrirlash"
: "Foydalanuvchi qo'shish"}
</DialogTitle>
</DialogHeader>
<AddUsers initialData={editingUser} setDialogOpen={setDialogOpen} />
</DialogContent>
</Dialog>
</div>
);
};
export default Filter;

View File

@@ -0,0 +1,112 @@
"use client";
import type { UserData } from "@/features/users/lib/data";
import { Avatar, AvatarFallback } from "@/shared/ui/avatar";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader } from "@/shared/ui/card";
import { Clock, Edit, Trash2 } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react";
export function UserCard({
user,
setEditingUser,
setDialogOpen,
setOpenDelete,
setUserDelete,
}: {
user: UserData;
setEditingUser: (user: UserData) => void;
setDialogOpen: (open: boolean) => void;
setOpenDelete: Dispatch<SetStateAction<boolean>>;
setUserDelete: Dispatch<SetStateAction<UserData | null>>;
}) {
const getRoleColor = (role: UserData["role"]) => {
switch (role) {
case "admin":
return "bg-red-100 text-red-800";
case "moderator":
return "bg-blue-100 text-blue-800";
case "user":
return "bg-green-100 text-green-800";
default:
return "bg-gray-100 text-gray-800";
}
};
return (
<>
<Card className="group hover:shadow-md transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<Avatar>
<AvatarFallback className="bg-green-100 text-green-700">
{user.username
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<h3 className="font-semibold">{user.username}</h3>
</div>
</div>
<Badge className={getRoleColor(user.role)}>
{user.role === "admin"
? "Admin"
: user.role === "user"
? "Foydalanuvchi"
: user.role}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
<span>
{"Kirgan vaqt"} {new Date(user.date_joined).toLocaleDateString()}
</span>
</div>
{user.last_login && (
<div className="text-sm text-muted-foreground">
{"Ohirgi marta kirgan"}:{" "}
{new Date(user.last_login).toLocaleDateString()}
</div>
)}
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
setEditingUser(user);
setDialogOpen(true);
}}
className="flex-1"
>
<Edit className="h-4 w-4 mr-2" />
Tahrirlash
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
setOpenDelete(true);
if (setUserDelete) {
setUserDelete(user);
}
}}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</>
);
}

View File

@@ -0,0 +1,61 @@
import type { UserData } from "@/features/users/lib/data";
import { UserCard } from "@/features/users/ui/UserCard";
import { Loader2 } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react";
interface Props {
data: UserData[] | undefined;
isLoading: boolean;
isError: boolean;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
setEditingUser: Dispatch<SetStateAction<UserData | null>>;
setOpenDelete: Dispatch<SetStateAction<boolean>>;
setUserDelete: Dispatch<SetStateAction<UserData | null>>;
}
const UserTable = ({
data,
isLoading,
isError,
setDialogOpen,
setEditingUser,
setOpenDelete,
setUserDelete,
}: Props) => {
return (
<div className="flex-1 overflow-auto">
{isLoading && (
<div className="h-full flex items-center justify-center bg-white/70 z-10">
<Loader2 className="animate-spin" />
</div>
)}
{isError && (
<div className="h-full flex items-center justify-center z-10">
<span className="text-lg font-medium text-red-600">
Ma'lumotlarni olishda xatolik yuz berdi.
</span>
</div>
)}
{!isLoading && !isError && (
<>
{/* Users Grid */}
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{data?.map((user) => (
<UserCard
setOpenDelete={setOpenDelete}
setUserDelete={setUserDelete}
key={user.id}
setDialogOpen={setDialogOpen}
setEditingUser={setEditingUser}
user={{ ...user, role: user.is_superuser ? "admin" : "user" }}
/>
))}
</div>
</>
)}
</div>
);
};
export default UserTable;

View File

@@ -0,0 +1,77 @@
import type { RegionListResData } from "@/features/region/lib/data";
import { user_api } from "@/features/users/lib/api";
import type { UserData } from "@/features/users/lib/data";
import DeleteUser from "@/features/users/ui/DeleteUser";
import Filter from "@/features/users/ui/Filter";
import UserTable from "@/features/users/ui/UserTable";
import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
const UsersList = () => {
const [editingUser, setEditingUser] = useState<UserData | null>(null);
const [opneDelete, setOpenDelete] = useState(false);
const [userDelete, setUserDelete] = useState<UserData | null>(null);
const [regionValue, setRegionValue] = useState<RegionListResData | null>(
null,
);
const limit = 20;
const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading, isError } = useQuery({
queryKey: ["user_list", currentPage, regionValue],
queryFn: () => {
return user_api.list({
page: currentPage,
page_size: limit,
});
},
select(data) {
return data.data;
},
});
const [dialogOpen, setDialogOpen] = useState(false);
return (
<div className="flex flex-col h-full p-10 w-full">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<h1 className="text-2xl font-bold">Foydalanuvchilar ro'yxati</h1>
<Filter
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
editingUser={editingUser}
setEditingUser={setEditingUser}
setRegionValue={setRegionValue}
/>
</div>
<UserTable
data={data?.results}
isLoading={isLoading}
setEditingUser={setEditingUser}
isError={isError}
setDialogOpen={setDialogOpen}
setOpenDelete={setOpenDelete}
setUserDelete={setUserDelete}
/>
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={data?.total_pages || 1}
/>
<DeleteUser
opneDelete={opneDelete}
setOpenDelete={setOpenDelete}
userDelete={userDelete}
setUserDelete={setUserDelete}
/>
</div>
);
};
export default UsersList;