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; return res;
}, },
async delete(id: string) { async delete(body: { ids: number[] }) {
const res = await httpClient.delete(`${API_URLS.DeleteProdut(id)}`); const res = await httpClient.delete(`${API_URLS.DeleteProdut}`, {
data: body,
});
return res; 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 type { Product } from "@/features/plans/lib/data";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
@@ -8,34 +9,107 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/shared/ui/dialog"; } 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 type { Dispatch, SetStateAction } from "react";
import { toast } from "sonner";
interface Props { interface Props {
opneDelete: boolean; opneDelete: boolean;
setOpenDelete: Dispatch<SetStateAction<boolean>>; setOpenDelete: Dispatch<SetStateAction<boolean>>;
setPlanDelete: Dispatch<SetStateAction<Product | null>>;
planDelete: 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 ( return (
<Dialog open={opneDelete} onOpenChange={setOpenDelete}> <Dialog open={opneDelete} onOpenChange={setOpenDelete}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Mahsulotni o'chirish</DialogTitle> <DialogTitle>Mahsulotni o'chirish</DialogTitle>
<DialogDescription className="text-md font-semibold"> <DialogDescription>
Siz rostan ham mahsulotni o'chirmoqchimisiz? {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> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<Button <Button
className="bg-blue-600 cursor-pointer hover:bg-blue-600" onClick={() => {
onClick={() => setOpenDelete(false)} setOpenDelete(false);
setPlanDelete(null);
}}
disabled={isDeleting}
> >
<X />
Bekor qilish Bekor qilish
</Button> </Button>
<Button
onClick={handleConfirmDelete}
className="bg-red-600 hover:bg-red-700 cursor-pointer"
disabled={isDeleting}
>
{isDeleting ? "O'chirilmoqda..." : "O'chirish"}
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -1,8 +1,7 @@
"use client";
import type { Product } from "@/features/plans/lib/data"; import type { Product } from "@/features/plans/lib/data";
import { API_URLS } from "@/shared/config/api/URLs"; import { API_URLS } from "@/shared/config/api/URLs";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { Checkbox } from "@/shared/ui/checkbox";
import { import {
Table, Table,
TableBody, TableBody,
@@ -11,18 +10,24 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/shared/ui/table"; } from "@/shared/ui/table";
import { Eye, Loader2 } from "lucide-react"; import { Eye, Loader2, Trash2 } from "lucide-react";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
interface Props { interface Props {
products: Product[] | []; products: Product[] | [];
isLoading: boolean; isLoading: boolean;
isFetching: boolean; isFetching: boolean;
setIsAllPagesSelected: Dispatch<SetStateAction<boolean>>;
isError: boolean; isError: boolean;
setEditingProduct: Dispatch<SetStateAction<Product | null>>; setEditingProduct: Dispatch<SetStateAction<Product | null>>;
setDetailOpen: Dispatch<SetStateAction<boolean>>; setDetailOpen: Dispatch<SetStateAction<boolean>>;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
handleDelete: (product: Product) => void; handleDelete: (product: Product) => void;
selectedProducts: number[];
setSelectedProducts: Dispatch<SetStateAction<number[]>>;
handleBulkDelete: () => void;
totalCount?: number;
handleSelectAllPages: () => void;
isAllPagesSelected: boolean;
} }
const ProductTable = ({ const ProductTable = ({
@@ -31,7 +36,14 @@ const ProductTable = ({
isFetching, isFetching,
isError, isError,
setEditingProduct, setEditingProduct,
setIsAllPagesSelected,
setDetailOpen, setDetailOpen,
selectedProducts,
setSelectedProducts,
handleBulkDelete,
totalCount = 0,
handleSelectAllPages,
isAllPagesSelected,
}: Props) => { }: Props) => {
if (isLoading || isFetching) { if (isLoading || isFetching) {
return ( 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 ( return (
<div className="flex-1 overflow-auto"> <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> <Table>
<TableHeader> <TableHeader>
<TableRow> <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>ID</TableHead>
<TableHead>Rasmi</TableHead> <TableHead>Rasmi</TableHead>
<TableHead>Nomi</TableHead> <TableHead>Nomi</TableHead>
@@ -61,54 +145,37 @@ const ProductTable = ({
<TableHead className="text-end">Harakatlar</TableHead> <TableHead className="text-end">Harakatlar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{products.map((product, index) => { {products.map((product, index) => {
const isSelected = selectedProducts.includes(product.id);
return ( return (
<TableRow key={product.id}> <TableRow
key={product.id}
className={isSelected ? "bg-blue-50" : ""}
>
<TableCell>
<Checkbox
checked={isSelected}
onCheckedChange={(checked) =>
handleSelectProduct(product.id, checked as boolean)
}
aria-label={`${product.name} tanlash`}
/>
</TableCell>
<TableCell>{index + 1}</TableCell> <TableCell>{index + 1}</TableCell>
{product.images.length > 0 ? (
<TableCell> <TableCell>
<img <img
src={API_URLS.BASE_URL + product.images[0].image} src={
product.images.length > 0
? API_URLS.BASE_URL + product.images[0].image
: "/logo.png"
}
alt={product.name} alt={product.name}
className="w-16 h-16 object-cover rounded" className="w-16 h-16 object-cover rounded"
/> />
</TableCell> </TableCell>
) : (
<TableCell>
<img
src={"/logo.png"}
alt={product.name}
className="w-10 h-10 object-cover rounded"
/>
</TableCell>
)}
<TableCell>{product.name}</TableCell> <TableCell>{product.name}</TableCell>
<TableCell> <TableCell>{product.short_name?.slice(0, 15)}...</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")
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Holati" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">Faol</SelectItem>
<SelectItem value="false">Nofaol</SelectItem>
</SelectContent>
</Select>
</TableCell> */}
<TableCell className="space-x-2 text-right"> <TableCell className="space-x-2 text-right">
<Button <Button
size="sm" size="sm"
@@ -120,37 +187,10 @@ const ProductTable = ({
> >
<Eye className="h-4 w-4" /> <Eye className="h-4 w-4" />
</Button> </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> </TableCell>
</TableRow> </TableRow>
); );
})} })}
{products.length === 0 && (
<TableRow>
<TableCell colSpan={8} className="text-center text-gray-500">
Mahsulotlar topilmadi
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>

View File

@@ -2,7 +2,7 @@ import { plans_api } from "@/features/plans/lib/api";
import type { Product } from "@/features/plans/lib/data"; import type { Product } from "@/features/plans/lib/data";
import DeletePlan from "@/features/plans/ui/DeletePlan"; import DeletePlan from "@/features/plans/ui/DeletePlan";
import FilterPlans from "@/features/plans/ui/FilterPlans"; 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 Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
@@ -11,7 +11,10 @@ import { PlanDetail } from "./PlanDetail";
const ProductList = () => { const ProductList = () => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [searchUser, setSearchUser] = useState<string>(""); const [searchUser, setSearchUser] = useState<string>("");
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
const [isAllPagesSelected, setIsAllPagesSelected] = useState(false);
const limit = 20; const limit = 20;
const { data, isLoading, isError, isFetching } = useQuery({ const { data, isLoading, isError, isFetching } = useQuery({
queryKey: ["product_list", searchUser, currentPage], queryKey: ["product_list", searchUser, currentPage],
queryFn: () => { queryFn: () => {
@@ -33,9 +36,46 @@ const ProductList = () => {
const [openDelete, setOpenDelete] = useState<boolean>(false); const [openDelete, setOpenDelete] = useState<boolean>(false);
const [planDelete, setPlanDelete] = useState<Product | null>(null); const [planDelete, setPlanDelete] = useState<Product | null>(null);
const handleDelete = (id: Product) => { const handleDelete = (product: Product) => {
setOpenDelete(true); 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 ( return (
@@ -58,20 +98,27 @@ const ProductList = () => {
/> />
</div> </div>
<PalanTable <ProductTable
products={data?.results || []} products={data?.results || []}
isLoading={isLoading} isLoading={isLoading}
isFetching={isFetching} isFetching={isFetching}
isError={isError} isError={isError}
setDetailOpen={setDetail} setDetailOpen={setDetail}
setIsAllPagesSelected={setIsAllPagesSelected}
setEditingProduct={setEditingPlan} setEditingProduct={setEditingPlan}
setDialogOpen={setDialogOpen}
handleDelete={handleDelete} handleDelete={handleDelete}
selectedProducts={selectedProducts}
setSelectedProducts={setSelectedProducts}
handleBulkDelete={handleBulkDelete}
totalCount={data?.total || 0}
handleSelectAllPages={handleSelectAllPages}
isAllPagesSelected={isAllPagesSelected}
/> />
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
setCurrentPage={setCurrentPage} setCurrentPage={setCurrentPage}
handlePageChange={handlePageChange}
totalPages={data?.total_pages || 1} totalPages={data?.total_pages || 1}
/> />
@@ -80,6 +127,8 @@ const ProductList = () => {
planDelete={planDelete} planDelete={planDelete}
setOpenDelete={setOpenDelete} setOpenDelete={setOpenDelete}
setPlanDelete={setPlanDelete} setPlanDelete={setPlanDelete}
selectedProducts={selectedProducts}
setSelectedProducts={setSelectedProducts}
/> />
</div> </div>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,41 +7,80 @@ interface Props {
currentPage: number; currentPage: number;
setCurrentPage: Dispatch<SetStateAction<number>>; setCurrentPage: Dispatch<SetStateAction<number>>;
totalPages: 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 ( return (
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t"> <div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
disabled={currentPage === 1} disabled={currentPage === 1}
onClick={() => {
setCurrentPage((prev) => Math.max(prev - 1, 1));
if (handlePageChange) {
handlePageChange(Math.max(currentPage - 1, 1));
}
}}
className="cursor-pointer" className="cursor-pointer"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
> >
<ChevronLeft /> <ChevronLeft />
</Button> </Button>
{Array.from({ length: totalPages }, (_, i) => (
{getPageNumbers().map((page) => (
<Button <Button
key={i} key={page}
variant={currentPage === i + 1 ? "default" : "outline"} variant={currentPage === page ? "default" : "outline"}
size="icon" size="icon"
className={clsx( className={clsx(
currentPage === i + 1 currentPage === page
? "bg-blue-500 hover:bg-blue-500" ? "bg-blue-500 hover:bg-blue-500 text-white"
: " bg-none hover:bg-blue-200", : "hover:bg-blue-200",
"cursor-pointer", "cursor-pointer",
)} )}
onClick={() => setCurrentPage(i + 1)} onClick={() => setCurrentPage(page)}
> >
{i + 1} {page}
</Button> </Button>
))} ))}
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
disabled={currentPage === totalPages} 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" className="cursor-pointer"
> >
<ChevronRight /> <ChevronRight />