From a48a9318c8c40ae8b9683c55fa5bab1ab3083516 Mon Sep 17 00:00:00 2001 From: Samandar Turgunboyev Date: Wed, 28 Jan 2026 13:26:15 +0500 Subject: [PATCH] product delete and user search --- src/features/plans/lib/api.ts | 6 +- src/features/plans/ui/DeletePlan.tsx | 90 +++++++- src/features/plans/ui/PalanTable.tsx | 188 ++++++++++------- src/features/plans/ui/ProductList.tsx | 59 +++++- src/features/users/lib/api.ts | 1 + src/features/users/ui/Filter.tsx | 55 +---- src/features/users/ui/UserCard.tsx | 291 ++++++++++++++------------ src/features/users/ui/UserTable.tsx | 27 +-- src/features/users/ui/UsersList.tsx | 23 +- src/shared/config/api/URLs.ts | 2 +- src/shared/ui/pagination.tsx | 61 +++++- 11 files changed, 477 insertions(+), 326 deletions(-) diff --git a/src/features/plans/lib/api.ts b/src/features/plans/lib/api.ts index 21caa14..0fe4c56 100644 --- a/src/features/plans/lib/api.ts +++ b/src/features/plans/lib/api.ts @@ -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; }, diff --git a/src/features/plans/ui/DeletePlan.tsx b/src/features/plans/ui/DeletePlan.tsx index d828c75..62f9e2c 100644 --- a/src/features/plans/ui/DeletePlan.tsx +++ b/src/features/plans/ui/DeletePlan.tsx @@ -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>; - setPlanDelete: Dispatch>; planDelete: Product | null; + setPlanDelete: Dispatch>; + selectedProducts?: number[]; + setSelectedProducts?: Dispatch>; } -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 ( Mahsulotni o'chirish - - Siz rostan ham mahsulotni o'chirmoqchimisiz? + + {selectedProducts.length > 0 ? ( + <> + Siz{" "} + + {selectedProducts.length} + {" "} + ta mahsulotni o'chirmoqchisiz. Bu amalni qaytarib bo'lmaydi. + Davom etishni xohlaysizmi? + + ) : ( + <> + Siz{" "} + + {planDelete?.name} + {" "} + nomli mahsulotni o'chirmoqchisiz. Bu amalni qaytarib bo'lmaydi. + Davom etishni xohlaysizmi? + + )} + diff --git a/src/features/plans/ui/PalanTable.tsx b/src/features/plans/ui/PalanTable.tsx index 3294514..fec3443 100644 --- a/src/features/plans/ui/PalanTable.tsx +++ b/src/features/plans/ui/PalanTable.tsx @@ -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>; isError: boolean; setEditingProduct: Dispatch>; setDetailOpen: Dispatch>; - setDialogOpen: Dispatch>; handleDelete: (product: Product) => void; + selectedProducts: number[]; + setSelectedProducts: Dispatch>; + 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 (
+ {selectedProducts.length > 0 && ( +
+
+ + {isAllPagesSelected + ? `Barcha ${totalCount} ta mahsulot tanlandi` + : `${selectedProducts.length} ta mahsulot tanlandi`} + +
+ {!isAllPagesSelected && totalCount > products.length && ( + + )} + +
+
+
+ )} + + + + ID Rasmi Nomi @@ -61,54 +145,37 @@ const ProductTable = ({ Harakatlar - {products.map((product, index) => { + const isSelected = selectedProducts.includes(product.id); return ( - - {index + 1} - {product.images.length > 0 ? ( - - {product.name} - - ) : ( - - {product.name} - - )} - {product.name} + - {product.short_name && product.short_name.slice(0, 15)}... - - {/* - - */} - + aria-label={`${product.name} tanlash`} + /> + + {index + 1} + + 0 + ? API_URLS.BASE_URL + product.images[0].image + : "/logo.png" + } + alt={product.name} + className="w-16 h-16 object-cover rounded" + /> + + {product.name} + {product.short_name?.slice(0, 15)}... - - {/* - - */} ); })} - - {products.length === 0 && ( - - - Mahsulotlar topilmadi - - - )}
diff --git a/src/features/plans/ui/ProductList.tsx b/src/features/plans/ui/ProductList.tsx index e7d8ea3..0a33573 100644 --- a/src/features/plans/ui/ProductList.tsx +++ b/src/features/plans/ui/ProductList.tsx @@ -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(""); + const [selectedProducts, setSelectedProducts] = useState([]); + 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(false); const [planDelete, setPlanDelete] = useState(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 = () => { /> - @@ -80,6 +127,8 @@ const ProductList = () => { planDelete={planDelete} setOpenDelete={setOpenDelete} setPlanDelete={setPlanDelete} + selectedProducts={selectedProducts} + setSelectedProducts={setSelectedProducts} /> ); diff --git a/src/features/users/lib/api.ts b/src/features/users/lib/api.ts index 368467c..9e0c1ee 100644 --- a/src/features/users/lib/api.ts +++ b/src/features/users/lib/api.ts @@ -6,6 +6,7 @@ import { type AxiosResponse } from "axios"; export const user_api = { async list(params: { page?: number; + search?: string; page_size?: number; }): Promise> { const res = await httpClient.get(`${API_URLS.UsesList}`, { params }); diff --git a/src/features/users/ui/Filter.tsx b/src/features/users/ui/Filter.tsx index 38efa1a..52d688d 100644 --- a/src/features/users/ui/Filter.tsx +++ b/src/features/users/ui/Filter.tsx @@ -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>; - dialogOpen: boolean; - setDialogOpen: Dispatch>; - editingUser: UserData | null; - setEditingUser: Dispatch>; -} - -const Filter = ({ - dialogOpen, - setDialogOpen, - editingUser, - setEditingUser, -}: Props) => { - return ( -
- - - - - - - - {editingUser - ? "Foydalanuvchini tahrirlash" - : "Foydalanuvchi qo'shish"} - - - - - - -
- ); +const Filter = () => { + return
; }; export default Filter; diff --git a/src/features/users/ui/UserCard.tsx b/src/features/users/ui/UserCard.tsx index 0253e5c..9e11990 100644 --- a/src/features/users/ui/UserCard.tsx +++ b/src/features/users/ui/UserCard.tsx @@ -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>; - setUserDelete?: Dispatch>; -}) { + +interface UserListProps { + users: UserData[]; +} + +export function UserCard({ users }: UserListProps) { + const [edit, setEdit] = useState(null); + const [open, setOpen] = useState(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(null); - const [open, setOpen] = useState(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 ( - - -
-
- - - {getInitials()} - - -
-

{getFullName()}

-

@{user.username}

-
-
- - {getGenderLabel(user.gender)} - -
-
- - -
- - - Ro'yxatdan o'tgan:{" "} - {new Date(user.created_at).toLocaleDateString("uz-UZ", { - year: "numeric", - month: "long", - day: "numeric", - })} - -
- - {user.region && ( -
- - Hudud: {user.region} -
- )} - - {user.address && ( -
- - Manzil: {user.address} -
- )} - - {/* ID ma'lumoti */} -
- ID: {user.id} -
- -
- - - - - - - Parolni qo'yish - -
-
- - ( - - - - - - - - )} - /> -
- +
+ + + + ID + To'liq ism + Username + Jinsi + Hudud + Manzil + Ro'yxatdan o'tgan + Amallar + + + + {users.map((user) => ( + + {user.id} + +
+ {getFullName(user)} +
+
+ + @{user.username} + + + + {getGenderLabel(user.gender)} + + + + + {user.region || "-"} + + + + + {user.address || "-"} + + + + + {new Date(user.created_at).toLocaleDateString("uz-UZ", { + year: "numeric", + month: "short", + day: "numeric", + })} + + + + { + setOpen(isOpen); + if (!isOpen) { + setEdit(null); + form.reset(); + } + }} + > + + + + + + Parolni {user.password_set ? "o'zgartirish" : "qo'yish"} + +
+ + + ( + + + + + + + + )} + /> +
+ + +
+ +
- - - -
-
- - - + + +
+
+ ))} +
+
+
); } diff --git a/src/features/users/ui/UserTable.tsx b/src/features/users/ui/UserTable.tsx index 525e119..7974984 100644 --- a/src/features/users/ui/UserTable.tsx +++ b/src/features/users/ui/UserTable.tsx @@ -13,14 +13,7 @@ interface Props { setUserDelete?: Dispatch>; } -const UserTable = ({ - data, - isLoading, - isError, - setEditingUser, - setOpenDelete, - setUserDelete, -}: Props) => { +const UserTable = ({ data, isLoading, isError }: Props) => { return (
{isLoading && ( @@ -36,23 +29,7 @@ const UserTable = ({
)} - {!isLoading && !isError && ( - <> - {/* Users Grid */} -
- {data?.map((user) => ( - - ))} -
- - )} + {!isLoading && !isError && <>{data && }}
); }; diff --git a/src/features/users/ui/UsersList.tsx b/src/features/users/ui/UsersList.tsx index 99e7df7..688db94 100644 --- a/src/features/users/ui/UsersList.tsx +++ b/src/features/users/ui/UsersList.tsx @@ -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(null); + const [search, setSearch] = useState(""); // const [regionValue, setRegionValue] = useState(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" )} - - {/* */} +
+
+ + setSearch(e.target.value)} + />
`${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/`, diff --git a/src/shared/ui/pagination.tsx b/src/shared/ui/pagination.tsx index b2d780c..e0498f3 100644 --- a/src/shared/ui/pagination.tsx +++ b/src/shared/ui/pagination.tsx @@ -7,41 +7,80 @@ interface Props { currentPage: number; setCurrentPage: Dispatch>; totalPages: number; + maxVisible?: number; // nechta sahifa ko‘rsatiladi + 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 (
- {Array.from({ length: totalPages }, (_, i) => ( + + {getPageNumbers().map((page) => ( ))} +