product delete and user search

This commit is contained in:
Samandar Turgunboyev
2026-01-28 13:26:15 +05:00
parent 10dbf1593b
commit a48a9318c8
11 changed files with 477 additions and 326 deletions

View File

@@ -27,8 +27,10 @@ export const plans_api = {
return res;
},
async delete(id: string) {
const res = await httpClient.delete(`${API_URLS.DeleteProdut(id)}`);
async delete(body: { ids: number[] }) {
const res = await httpClient.delete(`${API_URLS.DeleteProdut}`, {
data: body,
});
return res;
},

View File

@@ -1,3 +1,4 @@
import { plans_api } from "@/features/plans/lib/api";
import type { Product } from "@/features/plans/lib/data";
import { Button } from "@/shared/ui/button";
import {
@@ -8,34 +9,107 @@ import {
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { X } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import type { Dispatch, SetStateAction } from "react";
import { toast } from "sonner";
interface Props {
opneDelete: boolean;
setOpenDelete: Dispatch<SetStateAction<boolean>>;
setPlanDelete: Dispatch<SetStateAction<Product | null>>;
planDelete: Product | null;
setPlanDelete: Dispatch<SetStateAction<Product | null>>;
selectedProducts?: number[];
setSelectedProducts?: Dispatch<SetStateAction<number[]>>;
}
const DeletePlan = ({ opneDelete, setOpenDelete }: Props) => {
const DeletePlan = ({
opneDelete,
setOpenDelete,
planDelete,
setPlanDelete,
selectedProducts = [],
setSelectedProducts,
}: Props) => {
const queryClient = useQueryClient();
// Ko'plab mahsulotlarni o'chirish
const { mutate: deleteBulk, isPending: isPendingBulk } = useMutation({
mutationFn: async (ids: number[]) => {
return await plans_api.delete({ ids });
},
onSuccess: () => {
toast.success(`${selectedProducts.length} ta mahsulot o'chirildi`, {
richColors: true,
position: "top-center",
});
queryClient.invalidateQueries({ queryKey: ["product_list"] });
setOpenDelete(false);
setPlanDelete(null);
if (setSelectedProducts) {
setSelectedProducts([]);
}
},
onError: (err: AxiosError) => {
const errorData = (err.response?.data as { data: string }).data;
const errorMessage = (err.response?.data as { message: string }).message;
toast.error(errorMessage || errorData || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const handleConfirmDelete = () => {
deleteBulk(selectedProducts);
};
const isDeleting = isPendingBulk;
return (
<Dialog open={opneDelete} onOpenChange={setOpenDelete}>
<DialogContent>
<DialogHeader>
<DialogTitle>Mahsulotni o'chirish</DialogTitle>
<DialogDescription className="text-md font-semibold">
Siz rostan ham mahsulotni o'chirmoqchimisiz?
<DialogDescription>
{selectedProducts.length > 0 ? (
<>
Siz{" "}
<span className="font-bold text-red-600">
{selectedProducts.length}
</span>{" "}
ta mahsulotni o'chirmoqchisiz. Bu amalni qaytarib bo'lmaydi.
Davom etishni xohlaysizmi?
</>
) : (
<>
Siz{" "}
<span className="font-bold text-red-600">
{planDelete?.name}
</span>{" "}
nomli mahsulotni o'chirmoqchisiz. Bu amalni qaytarib bo'lmaydi.
Davom etishni xohlaysizmi?
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-blue-600 cursor-pointer hover:bg-blue-600"
onClick={() => setOpenDelete(false)}
onClick={() => {
setOpenDelete(false);
setPlanDelete(null);
}}
disabled={isDeleting}
>
<X />
Bekor qilish
</Button>
<Button
onClick={handleConfirmDelete}
className="bg-red-600 hover:bg-red-700 cursor-pointer"
disabled={isDeleting}
>
{isDeleting ? "O'chirilmoqda..." : "O'chirish"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -1,8 +1,7 @@
"use client";
import type { Product } from "@/features/plans/lib/data";
import { API_URLS } from "@/shared/config/api/URLs";
import { Button } from "@/shared/ui/button";
import { Checkbox } from "@/shared/ui/checkbox";
import {
Table,
TableBody,
@@ -11,18 +10,24 @@ import {
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { Eye, Loader2 } from "lucide-react";
import { Eye, Loader2, Trash2 } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
products: Product[] | [];
isLoading: boolean;
isFetching: boolean;
setIsAllPagesSelected: Dispatch<SetStateAction<boolean>>;
isError: boolean;
setEditingProduct: Dispatch<SetStateAction<Product | null>>;
setDetailOpen: Dispatch<SetStateAction<boolean>>;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
handleDelete: (product: Product) => void;
selectedProducts: number[];
setSelectedProducts: Dispatch<SetStateAction<number[]>>;
handleBulkDelete: () => void;
totalCount?: number;
handleSelectAllPages: () => void;
isAllPagesSelected: boolean;
}
const ProductTable = ({
@@ -31,7 +36,14 @@ const ProductTable = ({
isFetching,
isError,
setEditingProduct,
setIsAllPagesSelected,
setDetailOpen,
selectedProducts,
setSelectedProducts,
handleBulkDelete,
totalCount = 0,
handleSelectAllPages,
isAllPagesSelected,
}: Props) => {
if (isLoading || isFetching) {
return (
@@ -49,11 +61,83 @@ const ProductTable = ({
);
}
const currentPageIds = products.map((p) => p.id);
const isAllSelected =
isAllPagesSelected ||
(products.length > 0 &&
currentPageIds.every((id) => selectedProducts.includes(id)));
const isSomeSelected =
currentPageIds.some((id) => selectedProducts.includes(id)) &&
!currentPageIds.every((id) => selectedProducts.includes(id));
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedProducts(
Array.from(new Set([...selectedProducts, ...currentPageIds])),
);
} else {
setSelectedProducts(
selectedProducts.filter((id) => !currentPageIds.includes(id)),
);
if (isAllPagesSelected) setIsAllPagesSelected(false);
}
};
const handleSelectProduct = (productId: number, checked: boolean) => {
if (checked) setSelectedProducts([...selectedProducts, productId]);
else setSelectedProducts(selectedProducts.filter((id) => id !== productId));
};
return (
<div className="flex-1 overflow-auto">
{selectedProducts.length > 0 && (
<div className="mb-4 space-y-2">
<div className="flex items-center justify-between bg-blue-50 p-4 rounded-lg border border-blue-200">
<span className="text-sm font-medium text-blue-900">
{isAllPagesSelected
? `Barcha ${totalCount} ta mahsulot tanlandi`
: `${selectedProducts.length} ta mahsulot tanlandi`}
</span>
<div className="flex gap-2">
{!isAllPagesSelected && totalCount > products.length && (
<Button
size="sm"
variant="outline"
onClick={handleSelectAllPages}
className="cursor-pointer border-blue-500 text-blue-700 hover:bg-blue-100"
>
Barcha {totalCount} ta mahsulotni tanlash
</Button>
)}
<Button
size="sm"
variant="destructive"
onClick={handleBulkDelete}
className="cursor-pointer"
>
<Trash2 className="h-4 w-4 mr-2" />
Tanlanganlarni o'chirish
</Button>
</div>
</div>
</div>
)}
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px]">
<Checkbox
checked={isAllSelected}
onCheckedChange={handleSelectAll}
aria-label="Barchasini tanlash"
className={
isSomeSelected ? "data-[state=checked]:bg-blue-500" : ""
}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>Rasmi</TableHead>
<TableHead>Nomi</TableHead>
@@ -61,54 +145,37 @@ const ProductTable = ({
<TableHead className="text-end">Harakatlar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products.map((product, index) => {
const isSelected = selectedProducts.includes(product.id);
return (
<TableRow key={product.id}>
<TableCell>{index + 1}</TableCell>
{product.images.length > 0 ? (
<TableCell>
<img
src={API_URLS.BASE_URL + product.images[0].image}
alt={product.name}
className="w-16 h-16 object-cover rounded"
/>
</TableCell>
) : (
<TableCell>
<img
src={"/logo.png"}
alt={product.name}
className="w-10 h-10 object-cover rounded"
/>
</TableCell>
)}
<TableCell>{product.name}</TableCell>
<TableRow
key={product.id}
className={isSelected ? "bg-blue-50" : ""}
>
<TableCell>
{product.short_name && product.short_name.slice(0, 15)}...
</TableCell>
{/* <TableCell
className={clsx(
product.is_active ? "text-green-600" : "text-red-600",
)}
>
<Select
value={product.is_active ? "true" : "false"}
onValueChange={(value) =>
handleStatusChange(product.id, value as "true" | "false")
<Checkbox
checked={isSelected}
onCheckedChange={(checked) =>
handleSelectProduct(product.id, checked as boolean)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Holati" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">Faol</SelectItem>
<SelectItem value="false">Nofaol</SelectItem>
</SelectContent>
</Select>
</TableCell> */}
aria-label={`${product.name} tanlash`}
/>
</TableCell>
<TableCell>{index + 1}</TableCell>
<TableCell>
<img
src={
product.images.length > 0
? API_URLS.BASE_URL + product.images[0].image
: "/logo.png"
}
alt={product.name}
className="w-16 h-16 object-cover rounded"
/>
</TableCell>
<TableCell>{product.name}</TableCell>
<TableCell>{product.short_name?.slice(0, 15)}...</TableCell>
<TableCell className="space-x-2 text-right">
<Button
size="sm"
@@ -120,37 +187,10 @@ const ProductTable = ({
>
<Eye className="h-4 w-4" />
</Button>
{/*<Button
size="sm"
className="bg-blue-500 text-white hover:bg-blue-600"
onClick={() => {
setEditingProduct(product);
setDialogOpen(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(product)}
>
<Trash className="h-4 w-4" />
</Button>*/}
</TableCell>
</TableRow>
);
})}
{products.length === 0 && (
<TableRow>
<TableCell colSpan={8} className="text-center text-gray-500">
Mahsulotlar topilmadi
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>

View File

@@ -2,7 +2,7 @@ import { plans_api } from "@/features/plans/lib/api";
import type { Product } from "@/features/plans/lib/data";
import DeletePlan from "@/features/plans/ui/DeletePlan";
import FilterPlans from "@/features/plans/ui/FilterPlans";
import PalanTable from "@/features/plans/ui/PalanTable";
import ProductTable from "@/features/plans/ui/PalanTable";
import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
@@ -11,7 +11,10 @@ import { PlanDetail } from "./PlanDetail";
const ProductList = () => {
const [currentPage, setCurrentPage] = useState(1);
const [searchUser, setSearchUser] = useState<string>("");
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
const [isAllPagesSelected, setIsAllPagesSelected] = useState(false);
const limit = 20;
const { data, isLoading, isError, isFetching } = useQuery({
queryKey: ["product_list", searchUser, currentPage],
queryFn: () => {
@@ -33,9 +36,46 @@ const ProductList = () => {
const [openDelete, setOpenDelete] = useState<boolean>(false);
const [planDelete, setPlanDelete] = useState<Product | null>(null);
const handleDelete = (id: Product) => {
const handleDelete = (product: Product) => {
setOpenDelete(true);
setPlanDelete(id);
setPlanDelete(product);
};
console.log(selectedProducts);
const handleSelectAllPages = async () => {
try {
if (!data?.total) return;
const allIds: number[] = [];
const pageSize = 100; // backend limit
const totalPages = Math.ceil(data.total / pageSize);
for (let page = 1; page <= totalPages; page++) {
const response = await plans_api.list({
page,
page_size: pageSize,
search: searchUser,
});
const ids = response.data.results.map((product: Product) => product.id);
allIds.push(...ids);
}
setSelectedProducts(allIds);
setIsAllPagesSelected(true);
} catch (error) {
console.error("Barcha mahsulotlarni tanlashda xatolik:", error);
}
};
const handleBulkDelete = () => {
if (selectedProducts.length > 0) {
setOpenDelete(true);
}
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
return (
@@ -58,20 +98,27 @@ const ProductList = () => {
/>
</div>
<PalanTable
<ProductTable
products={data?.results || []}
isLoading={isLoading}
isFetching={isFetching}
isError={isError}
setDetailOpen={setDetail}
setIsAllPagesSelected={setIsAllPagesSelected}
setEditingProduct={setEditingPlan}
setDialogOpen={setDialogOpen}
handleDelete={handleDelete}
selectedProducts={selectedProducts}
setSelectedProducts={setSelectedProducts}
handleBulkDelete={handleBulkDelete}
totalCount={data?.total || 0}
handleSelectAllPages={handleSelectAllPages}
isAllPagesSelected={isAllPagesSelected}
/>
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
handlePageChange={handlePageChange}
totalPages={data?.total_pages || 1}
/>
@@ -80,6 +127,8 @@ const ProductList = () => {
planDelete={planDelete}
setOpenDelete={setOpenDelete}
setPlanDelete={setPlanDelete}
selectedProducts={selectedProducts}
setSelectedProducts={setSelectedProducts}
/>
</div>
);

View File

@@ -6,6 +6,7 @@ import { type AxiosResponse } from "axios";
export const user_api = {
async list(params: {
page?: number;
search?: string;
page_size?: number;
}): Promise<AxiosResponse<UserListRes>> {
const res = await httpClient.get(`${API_URLS.UsesList}`, { params });

View File

@@ -1,56 +1,5 @@
import type { UserData, UserListRes } 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<UserListRes | 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>
);
const Filter = () => {
return <div className="flex flex-wrap gap-2 items-center"></div>;
};
export default Filter;

View File

@@ -2,10 +2,8 @@
import { user_api } from "@/features/users/lib/api";
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 {
Dialog,
DialogContent,
@@ -21,11 +19,19 @@ import {
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Calendar, Edit, MapPin } from "lucide-react";
import { useState, type Dispatch, type SetStateAction } from "react";
import { Edit } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";
@@ -35,19 +41,16 @@ const passwordSet = z.object({
.string()
.min(8, { message: "Eng kamida 8ta belgi bolishi kerak" }),
});
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>>;
}) {
interface UserListProps {
users: UserData[];
}
export function UserCard({ users }: UserListProps) {
const [edit, setEdit] = useState<number | string | null>(null);
const [open, setOpen] = useState<boolean>(false);
const queryClient = useQueryClient();
const getGenderColor = (gender: "M" | "F" | null) => {
switch (gender) {
case "M":
@@ -70,14 +73,7 @@ export function UserCard({
}
};
const getInitials = () => {
if (user.first_name && user.last_name) {
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase();
}
return user.username.substring(0, 2).toUpperCase();
};
const getFullName = () => {
const getFullName = (user: UserData) => {
const parts = [user.first_name, user.middle_name, user.last_name].filter(
Boolean,
);
@@ -91,11 +87,6 @@ export function UserCard({
},
});
const [edit, setEdit] = useState<number | string | null>(null);
const [open, setOpen] = useState<boolean>(false);
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: ({
id,
@@ -107,6 +98,7 @@ export function UserCard({
onSuccess: () => {
setOpen(false);
setEdit(null);
form.reset();
queryClient.refetchQueries({ queryKey: ["user_list"] });
toast.success("Parol qo'yildi", {
richColors: true,
@@ -131,113 +123,138 @@ export function UserCard({
}
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-gradient-to-br from-purple-100 to-blue-100 text-purple-700">
{getInitials()}
</AvatarFallback>
</Avatar>
<div>
<h3 className="font-semibold">{getFullName()}</h3>
<p className="text-sm text-muted-foreground">@{user.username}</p>
</div>
</div>
<Badge className={getGenderColor(user.gender)}>
{getGenderLabel(user.gender)}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Calendar className="h-4 w-4" />
<span>
Ro'yxatdan o'tgan:{" "}
{new Date(user.created_at).toLocaleDateString("uz-UZ", {
year: "numeric",
month: "long",
day: "numeric",
})}
</span>
</div>
{user.region && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<MapPin className="h-4 w-4" />
<span>Hudud: {user.region}</span>
</div>
)}
{user.address && (
<div className="flex items-start gap-2 text-sm text-muted-foreground">
<MapPin className="h-4 w-4 mt-0.5" />
<span>Manzil: {user.address}</span>
</div>
)}
{/* ID ma'lumoti */}
<div className="pt-2 border-t text-xs text-muted-foreground">
ID: {user.id}
</div>
<div className="flex items-center gap-2 pt-2">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="flex-1 cursor-pointer"
onClick={() => {
setOpen(true);
setEdit(user.id);
}}
>
<Edit className="h-4 w-4 mr-2" />
{user.password_set ? "Parolni o'zgartirish" : "Parol qo'yish"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="text-xl font-semibold">
Parolni qo'yish
</DialogHeader>
<div>
<Form {...form}>
<form
className="space-y-2"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<Label>Parolni yozing</Label>
<FormControl>
<Input
placeholder="12345678"
className="h-12 focus-visible:ring-0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button className="bg-blue-500 hover:bg-blue-600 cursor-pointer">
Tasdiqlash
</Button>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px]">ID</TableHead>
<TableHead>To'liq ism</TableHead>
<TableHead>Username</TableHead>
<TableHead>Jinsi</TableHead>
<TableHead>Hudud</TableHead>
<TableHead>Manzil</TableHead>
<TableHead>Ro'yxatdan o'tgan</TableHead>
<TableHead className="text-right">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id} className="hover:bg-muted/50">
<TableCell className="font-medium">{user.id}</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="font-medium">{getFullName(user)}</span>
</div>
</TableCell>
<TableCell>
<span className="text-muted-foreground">@{user.username}</span>
</TableCell>
<TableCell>
<Badge className={getGenderColor(user.gender)}>
{getGenderLabel(user.gender)}
</Badge>
</TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">
{user.region || "-"}
</span>
</TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">
{user.address || "-"}
</span>
</TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">
{new Date(user.created_at).toLocaleDateString("uz-UZ", {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
</TableCell>
<TableCell className="text-right">
<Dialog
open={open && edit === user.id}
onOpenChange={(isOpen) => {
setOpen(isOpen);
if (!isOpen) {
setEdit(null);
form.reset();
}
}}
>
<DialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="cursor-pointer"
onClick={() => {
setOpen(true);
setEdit(user.id);
}}
>
<Edit className="h-4 w-4 mr-2" />
{user.password_set ? "O'zgartirish" : "Qo'yish"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="text-xl font-semibold">
Parolni {user.password_set ? "o'zgartirish" : "qo'yish"}
</DialogHeader>
<div>
<Form {...form}>
<form
className="space-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<Label>Yangi parol</Label>
<FormControl>
<Input
type="password"
placeholder="12345678"
className="h-12 focus-visible:ring-0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => {
setOpen(false);
setEdit(null);
form.reset();
}}
>
Bekor qilish
</Button>
<Button
type="submit"
className="bg-blue-500 hover:bg-blue-600 cursor-pointer"
>
Tasdiqlash
</Button>
</div>
</form>
</Form>
</div>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
</div>
</CardContent>
</Card>
</DialogContent>
</Dialog>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}

View File

@@ -13,14 +13,7 @@ interface Props {
setUserDelete?: Dispatch<SetStateAction<UserData | null>>;
}
const UserTable = ({
data,
isLoading,
isError,
setEditingUser,
setOpenDelete,
setUserDelete,
}: Props) => {
const UserTable = ({ data, isLoading, isError }: Props) => {
return (
<div className="flex-1 overflow-auto">
{isLoading && (
@@ -36,23 +29,7 @@ const UserTable = ({
</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 }}
/>
))}
</div>
</>
)}
{!isLoading && !isError && <>{data && <UserCard users={data} />}</>}
</div>
);
};

View File

@@ -3,10 +3,11 @@ import type { UserData } from "@/features/users/lib/data";
import DeleteUser from "@/features/users/ui/DeleteUser";
import UserTable from "@/features/users/ui/UserTable";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import Pagination from "@/shared/ui/pagination";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
import { Loader2, Search } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
@@ -15,17 +16,19 @@ const UsersList = () => {
const queryClient = useQueryClient();
const [opneDelete, setOpenDelete] = useState(false);
const [userDelete, setUserDelete] = useState<UserData | null>(null);
const [search, setSearch] = useState<string>("");
// const [regionValue, setRegionValue] = useState<UserListRes | null>(null);
const limit = 20;
const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading, isError } = useQuery({
queryKey: ["user_list", currentPage],
queryKey: ["user_list", currentPage, search],
queryFn: () => {
return user_api.list({
page: currentPage,
page_size: limit,
search: search,
});
},
select(data) {
@@ -66,14 +69,14 @@ const UsersList = () => {
"Foydalanuvchini import qilish"
)}
</Button>
{/* <Filter
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
editingUser={editingUser}
setEditingUser={setEditingUser}
setRegionValue={setRegionValue}
/> */}
</div>
<div className="relative h-12 mb-4">
<Search className="text-gray-400 absolute top-1/2 -translate-y-1/2 left-4" />
<Input
className="w-96 h-12 pl-12 !text-lg placeholder:text-lg focus-visible:ring-0"
placeholder="Qidirish..."
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<UserTable

View File

@@ -30,7 +30,7 @@ export const API_URLS = {
`${API_V}admin/order/${id}/status_update/`,
CreateProduct: `${API_V}admin/product/create/`,
UpdateProduct: (id: string) => `${API_V}admin/product/${id}/update/`,
DeleteProdut: (id: string) => `${API_V}admin/product/${id}/delete/`,
DeleteProdut: `${API_V}admin/product/bulk-delete/`,
QuestionnaireList: `${API_V}admin/questionnaire/list/`,
Import_User: `${API_V}admin/user/import_users/`,
Refresh_Token: `${API_V}accounts/refresh/token/`,

View File

@@ -7,41 +7,80 @@ interface Props {
currentPage: number;
setCurrentPage: Dispatch<SetStateAction<number>>;
totalPages: number;
maxVisible?: number; // nechta sahifa korsatiladi
handlePageChange?: (page: number) => void;
}
const Pagination = ({ currentPage, setCurrentPage, totalPages }: Props) => {
const Pagination = ({
currentPage,
setCurrentPage,
totalPages,
handlePageChange,
maxVisible = 5,
}: Props) => {
if (totalPages <= 1) return null;
const getPageNumbers = () => {
const pages = [];
const half = Math.floor(maxVisible / 2);
let start = Math.max(currentPage - half, 1);
const end = Math.min(start + maxVisible - 1, totalPages);
if (end - start + 1 < maxVisible) {
start = Math.max(end - maxVisible + 1, 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
};
return (
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
<Button
variant="outline"
size="icon"
disabled={currentPage === 1}
onClick={() => {
setCurrentPage((prev) => Math.max(prev - 1, 1));
if (handlePageChange) {
handlePageChange(Math.max(currentPage - 1, 1));
}
}}
className="cursor-pointer"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
>
<ChevronLeft />
</Button>
{Array.from({ length: totalPages }, (_, i) => (
{getPageNumbers().map((page) => (
<Button
key={i}
variant={currentPage === i + 1 ? "default" : "outline"}
key={page}
variant={currentPage === page ? "default" : "outline"}
size="icon"
className={clsx(
currentPage === i + 1
? "bg-blue-500 hover:bg-blue-500"
: " bg-none hover:bg-blue-200",
currentPage === page
? "bg-blue-500 hover:bg-blue-500 text-white"
: "hover:bg-blue-200",
"cursor-pointer",
)}
onClick={() => setCurrentPage(i + 1)}
onClick={() => setCurrentPage(page)}
>
{i + 1}
{page}
</Button>
))}
<Button
variant="outline"
size="icon"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
onClick={() => {
setCurrentPage((prev) => Math.min(prev + 1, totalPages));
if (handlePageChange) {
handlePageChange(Math.max(currentPage + 1, 1));
}
}}
className="cursor-pointer"
>
<ChevronRight />