Compare commits

..

10 Commits

Author SHA1 Message Date
Samandar Turgunboyev
94ba3f3db9 banner order update 2026-02-10 18:15:28 +05:00
Samandar Turgunboyev
287fe8a842 banner order update 2026-02-10 18:14:52 +05:00
Samandar Turgunboyev
bd1cf26c46 complated 2026-02-06 20:01:44 +05:00
Samandar Turgunboyev
d5106d85e9 bug fixed 2026-02-03 18:56:12 +05:00
Samandar Turgunboyev
0fd6bb9906 text update 2026-01-28 15:44:59 +05:00
Samandar Turgunboyev
711bbe83ca bug fix 2026-01-28 13:46:30 +05:00
Samandar Turgunboyev
a48a9318c8 product delete and user search 2026-01-28 13:26:15 +05:00
Samandar Turgunboyev
10dbf1593b bug fix 2026-01-24 14:51:52 +05:00
Samandar Turgunboyev
ce125ef982 bug fix 2026-01-23 21:41:24 +05:00
Samandar Turgunboyev
b198e3458b update 2026-01-23 19:29:56 +05:00
30 changed files with 949 additions and 375 deletions

View File

@@ -4,12 +4,14 @@ import FilterCategory from "@/features/districts/ui/Filter";
import TableDistrict from "@/features/districts/ui/TableCategories"; import TableDistrict from "@/features/districts/ui/TableCategories";
import type { CategoryItem } from "@/features/plans/lib/data"; import type { CategoryItem } from "@/features/plans/lib/data";
import Pagination from "@/shared/ui/pagination"; import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
const CategoriesList = () => { const CategoriesList = () => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const queryClient = useQueryClient();
const { data, isLoading, isError } = useQuery({ const { data, isLoading, isError } = useQuery({
queryKey: ["categories", currentPage], queryKey: ["categories", currentPage],
@@ -33,6 +35,31 @@ const CategoriesList = () => {
setOpenDelete(true); setOpenDelete(true);
}; };
const { mutate } = useMutation({
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
categories_api.image_upload({ body, id }),
onSuccess() {
toast.success("Banner tartibi o'zgartilidi", {
richColors: true,
position: "top-center",
});
queryClient.refetchQueries({ queryKey: ["categories"] });
},
onError() {
toast.error("Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const handleOrderChange = ({ body, id }: { id: number; body: FormData }) => {
mutate({
body,
id: id,
});
};
return ( return (
<div className="flex flex-col h-full p-10 w-full"> <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"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
@@ -42,6 +69,7 @@ const CategoriesList = () => {
</div> </div>
<TableDistrict <TableDistrict
handleOrderChange={handleOrderChange}
data={data ? data.results : []} data={data ? data.results : []}
isError={isError} isError={isError}
dialogOpen={dialogOpen} dialogOpen={dialogOpen}

View File

@@ -16,8 +16,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/shared/ui/table"; } from "@/shared/ui/table";
import { Image, Loader2 } from "lucide-react"; import { Check, Image, Loader2 } from "lucide-react";
import { useState, type Dispatch, type SetStateAction } from "react"; import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
interface Props { interface Props {
data: CategoryItem[] | []; data: CategoryItem[] | [];
@@ -27,6 +27,7 @@ interface Props {
setDialogOpen: Dispatch<SetStateAction<boolean>>; setDialogOpen: Dispatch<SetStateAction<boolean>>;
dialogOpen: boolean; dialogOpen: boolean;
currentPage: number; currentPage: number;
handleOrderChange: ({ body, id }: { id: number; body: FormData }) => void;
} }
const TableDistrict = ({ const TableDistrict = ({
@@ -35,8 +36,35 @@ const TableDistrict = ({
isLoading, isLoading,
dialogOpen, dialogOpen,
setDialogOpen, setDialogOpen,
handleOrderChange,
}: Props) => { }: Props) => {
const [initialValues, setEditing] = useState<CategoryItem>(); const [initialValues, setEditing] = useState<CategoryItem>();
const [localOrders, setLocalOrders] = useState<{ [key: number]: number }>({});
useEffect(() => {
const orders = data.reduce(
(acc, item) => {
acc[item.id] = item.order;
return acc;
},
{} as { [key: number]: number },
);
setLocalOrders(orders);
}, [data]);
const onOrderChange = (id: number, value: string) => {
if (value === "") {
// bo'sh inputni ham qabul qilamiz
setLocalOrders((prev) => ({ ...prev, [id]: 0 })); // yoki null ham bo'lishi mumkin
return;
}
const num = parseInt(value, 10);
if (!isNaN(num)) {
setLocalOrders((prev) => ({ ...prev, [id]: num }));
}
};
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{isLoading && ( {isLoading && (
@@ -61,7 +89,8 @@ const TableDistrict = ({
<TableRow> <TableRow>
<TableHead>ID</TableHead> <TableHead>ID</TableHead>
<TableHead>Rasmi</TableHead> <TableHead>Rasmi</TableHead>
<TableHead>Nomi (uz)</TableHead> <TableHead>Nomi (uz)</TableHead>{" "}
<TableHead>Tartib raqami</TableHead>
<TableHead>Kategoriya idsi</TableHead> <TableHead>Kategoriya idsi</TableHead>
<TableHead>Turi</TableHead> <TableHead>Turi</TableHead>
<TableHead className="text-right">Harakatlar</TableHead> <TableHead className="text-right">Harakatlar</TableHead>
@@ -79,10 +108,41 @@ const TableDistrict = ({
className="w-10 h-10 object-cover rounded-md" className="w-10 h-10 object-cover rounded-md"
/> />
</TableCell> </TableCell>
<TableCell>{d.name}</TableCell> <TableCell>{d.name}</TableCell>{" "}
<TableCell className="font-medium flex items-center gap-2">
<input
type="number"
value={localOrders[d.id] ?? ""}
onChange={(e) => onOrderChange(d.id, e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
const formData = new FormData();
formData.append("order", localOrders[d.id].toString());
handleOrderChange({
id: d.id,
body: formData,
});
}
}}
className="w-24 h-10 border rounded px-2"
/>
<Button
size="icon"
variant="outline"
onClick={() => {
const formData = new FormData();
formData.append("order", localOrders[d.id].toString());
handleOrderChange({
id: d.id,
body: formData,
});
}}
>
<Check size={18} />
</Button>
</TableCell>
<TableCell>{d.category_id ? d.category_id : "----"}</TableCell> <TableCell>{d.category_id ? d.category_id : "----"}</TableCell>
<TableCell>{d.type ? d.type : "----"}</TableCell> <TableCell>{d.type ? d.type : "----"}</TableCell>
<TableCell className="flex gap-2 justify-end"> <TableCell className="flex gap-2 justify-end">
<Button <Button
variant="outline" variant="outline"

View File

@@ -17,10 +17,10 @@ export const banner_api = {
return res; return res;
}, },
// async update({ body, id }: { id: number; body: ObjectUpdate }) { async update({ body, id }: { id: number; body: FormData }) {
// const res = await httpClient.patch(`${API_URLS.OBJECT}${id}/update/`, body); const res = await httpClient.patch(`${API_URLS.BannerUpdate(id)}`, body);
// return res; return res;
// }, },
async delete(id: string) { async delete(id: string) {
const res = await httpClient.delete(`${API_URLS.BannerDelete(id)}`); const res = await httpClient.delete(`${API_URLS.BannerDelete(id)}`);

View File

@@ -9,6 +9,7 @@ export interface BannersListData {
} }
export interface BannerListItem { export interface BannerListItem {
id: string; id: number;
banner: string; banner: string;
order: number;
} }

View File

@@ -4,14 +4,16 @@ import ObjectFilter from "@/features/objects/ui/BannersFilter";
import ObjectTable from "@/features/objects/ui/BannersTable"; import ObjectTable from "@/features/objects/ui/BannersTable";
import DeleteObject from "@/features/objects/ui/DeleteBanners"; import DeleteObject from "@/features/objects/ui/DeleteBanners";
import Pagination from "@/shared/ui/pagination"; import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
export default function BannersList() { export default function BannersList() {
const [editingPlan, setEditingPlan] = useState<BannerListItem | null>(null); const [editingPlan, setEditingPlan] = useState<BannerListItem | null>(null);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const limit = 20; const limit = 20;
const queryClient = useQueryClient();
const [disricDelete, setDiscritDelete] = useState<BannerListItem | null>( const [disricDelete, setDiscritDelete] = useState<BannerListItem | null>(
null, null,
@@ -39,6 +41,31 @@ export default function BannersList() {
}, },
}); });
const { mutate } = useMutation({
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
banner_api.update({ body, id }),
onSuccess() {
toast.success("Banner tartibi o'zgartilidi", {
richColors: true,
position: "top-center",
});
queryClient.refetchQueries({ queryKey: ["banner_list"] });
},
onError() {
toast.error("Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const handleOrderChange = ({ body, id }: { id: number; body: FormData }) => {
mutate({
body,
id: id,
});
};
return ( return (
<div className="flex flex-col h-full p-10 w-full"> <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"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
@@ -52,6 +79,7 @@ export default function BannersList() {
</div> </div>
<ObjectTable <ObjectTable
handleOrderChange={handleOrderChange}
filteredData={object ? object.results : []} filteredData={object ? object.results : []}
handleDelete={handleDelete} handleDelete={handleDelete}
isError={isError} isError={isError}

View File

@@ -9,8 +9,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/shared/ui/table"; } from "@/shared/ui/table";
import { Loader2, Trash2 } from "lucide-react"; import { Check, Loader2, Trash2 } from "lucide-react";
import type { Dispatch, SetStateAction } from "react"; import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
interface Props { interface Props {
filteredData: BannerListItem[] | []; filteredData: BannerListItem[] | [];
@@ -19,6 +19,7 @@ interface Props {
handleDelete: (object: BannerListItem) => void; handleDelete: (object: BannerListItem) => void;
isLoading: boolean; isLoading: boolean;
isError: boolean; isError: boolean;
handleOrderChange: ({ body, id }: { id: number; body: FormData }) => void; // tartib raqamini update qilish uchun
} }
const ObjectTable = ({ const ObjectTable = ({
@@ -26,7 +27,35 @@ const ObjectTable = ({
handleDelete, handleDelete,
isError, isError,
isLoading, isLoading,
handleOrderChange,
}: Props) => { }: Props) => {
// Mahsulotlar tartibini local state orqali boshqaramiz
const [localOrders, setLocalOrders] = useState<{ [key: number]: number }>({});
useEffect(() => {
const orders = filteredData.reduce(
(acc, item) => {
acc[item.id] = item.order;
return acc;
},
{} as { [key: number]: number },
);
setLocalOrders(orders);
}, [filteredData]);
const onOrderChange = (id: number, value: string) => {
if (value === "") {
// bo'sh inputni ham qabul qilamiz
setLocalOrders((prev) => ({ ...prev, [id]: 0 })); // yoki null ham bo'lishi mumkin
return;
}
const num = parseInt(value, 10);
if (!isNaN(num)) {
setLocalOrders((prev) => ({ ...prev, [id]: num }));
}
};
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{isLoading && ( {isLoading && (
@@ -44,12 +73,14 @@ const ObjectTable = ({
</span> </span>
</div> </div>
)} )}
{!isError && !isLoading && ( {!isError && !isLoading && (
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>#</TableHead> <TableHead>#</TableHead>
<TableHead>Banner</TableHead> <TableHead>Banner</TableHead>
<TableHead>Tartib raqami</TableHead>
<TableHead className="text-right">Amallar</TableHead> <TableHead className="text-right">Amallar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -61,11 +92,48 @@ const ObjectTable = ({
<TableCell className="font-medium"> <TableCell className="font-medium">
<img <img
src={API_URLS.BASE_URL + item.banner} src={API_URLS.BASE_URL + item.banner}
alt={item.id} alt={item.id.toString()}
className="w-16 h-16" className="w-16 h-16"
/> />
</TableCell> </TableCell>
<TableCell className="font-medium flex items-center gap-2">
<input
type="number"
value={localOrders[item.id] ?? ""}
onChange={(e) => onOrderChange(item.id, e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
const formData = new FormData();
formData.append(
"order",
localOrders[item.id].toString(),
);
handleOrderChange({
id: item.id,
body: formData,
});
}
}}
className="w-24 h-10 border rounded px-2"
/>
<Button
size="icon"
variant="outline"
onClick={() => {
const formData = new FormData();
formData.append(
"order",
localOrders[item.id].toString(),
);
handleOrderChange({
id: item.id,
body: formData,
});
}}
>
<Check size={18} />
</Button>
</TableCell>
<TableCell className="text-right space-x-2 gap-2"> <TableCell className="text-right space-x-2 gap-2">
<Button <Button
variant="destructive" variant="destructive"

View File

@@ -60,7 +60,7 @@ const DeleteObject = ({
</Button> </Button>
<Button <Button
variant={"destructive"} variant={"destructive"}
onClick={() => discrit && mutate(discrit.id)} onClick={() => discrit && mutate(discrit.id.toString())}
> >
{isPending ? ( {isPending ? (
<Loader2 className="animate-spin" /> <Loader2 className="animate-spin" />

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;
}, },
@@ -59,4 +61,20 @@ export const plans_api = {
const res = await httpClient.post(API_URLS.Import_Product); const res = await httpClient.post(API_URLS.Import_Product);
return res; return res;
}, },
async update_payment_type({
id,
body,
}: {
id: number;
body: { payment_type: "cash" | "card" };
}) {
const res = await httpClient.put(API_URLS.Update_Pyment_Type(id), body);
return res;
},
async import_balance() {
const res = await httpClient.post(API_URLS.Import_Balance);
return res;
},
}; };

View File

@@ -37,6 +37,7 @@ export interface Product {
marketing_group_code: null | string; marketing_group_code: null | string;
inventory_kinds: { id: number; name: string }[]; inventory_kinds: { id: number; name: string }[];
sector_codes: { id: number; code: string }[]; sector_codes: { id: number; code: string }[];
payment_type: "cash" | "card" | null;
} }
export interface Category { export interface Category {

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

@@ -5,6 +5,7 @@ import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input"; import { Input } from "@/shared/ui/input";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { Loader } from "lucide-react";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -46,6 +47,22 @@ const FilterPlans = ({ searchUser, setSearchUser }: Props) => {
}, },
}); });
const { mutate: balanceMutate, isPending } = useMutation({
mutationFn: () => plans_api.import_balance(),
onSuccess: () => {
toast.success("Mahsulotlar soni import qilindi", {
richColors: true,
position: "top-center",
});
},
onError: () => {
toast.error("Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
return ( return (
<div className="flex gap-2 mb-4"> <div className="flex gap-2 mb-4">
<Input <Input
@@ -101,6 +118,16 @@ const FilterPlans = ({ searchUser, setSearchUser }: Props) => {
/> />
</DialogContent> </DialogContent>
</Dialog>*/} </Dialog>*/}
<Button
className="h-12 bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
variant={"secondary"}
onClick={() => balanceMutate()}
disabled={isPending}
>
Mahsulotlar sonini olish
{isPending && <Loader className="animate-spin" />}
</Button>
</div> </div>
); );
}; };

View File

@@ -1,8 +1,16 @@
"use client"; 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 { 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 {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { import {
Table, Table,
TableBody, TableBody,
@@ -11,18 +19,27 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/shared/ui/table"; } from "@/shared/ui/table";
import { Eye, Loader2 } from "lucide-react"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Eye, Loader2, Trash2 } from "lucide-react";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { toast } from "sonner";
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;
count: number;
handleSelectAllPages: () => void;
isAllPagesSelected: boolean;
} }
const ProductTable = ({ const ProductTable = ({
@@ -31,8 +48,38 @@ const ProductTable = ({
isFetching, isFetching,
isError, isError,
setEditingProduct, setEditingProduct,
setIsAllPagesSelected,
setDetailOpen, setDetailOpen,
selectedProducts,
setSelectedProducts,
handleBulkDelete,
totalCount,
handleSelectAllPages,
isAllPagesSelected,
}: Props) => { }: Props) => {
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: ({
id,
body,
}: {
id: number;
body: { payment_type: "cash" | "card" };
}) => plans_api.update_payment_type({ id, body }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["product_list"] });
toast.success("Tolov turi yangilandi", {
richColors: true,
position: "top-center",
});
},
onError: () => {
toast.error("Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
if (isLoading || isFetching) { if (isLoading || isFetching) {
return ( return (
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
@@ -49,66 +96,144 @@ 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">
{`${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>
<TableHead>Tavsif</TableHead> <TableHead>Tavsif</TableHead>
<TableHead>Narx turi</TableHead>
<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
<TableCell>{index + 1}</TableCell> key={product.id}
{product.images.length > 0 ? ( className={isSelected ? "bg-blue-50" : ""}
<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>
<TableCell> <TableCell>
{product.short_name && product.short_name.slice(0, 15)}... <Checkbox
</TableCell> checked={isSelected}
{/* <TableCell onCheckedChange={(checked) =>
className={clsx( handleSelectProduct(product.id, checked as boolean)
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")
} }
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>
<Select
value={product.payment_type ?? ""}
disabled={isPending}
onValueChange={(value) => {
mutate({
id: product.id,
body: {
payment_type: value as "cash" | "card",
},
});
}}
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-full max-w-48">
<SelectValue placeholder="Holati" /> <SelectValue placeholder="To'lov turi" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="true">Faol</SelectItem> <SelectGroup>
<SelectItem value="false">Nofaol</SelectItem> <SelectItem value="cash">Naqd</SelectItem>
<SelectItem value="card">Karta</SelectItem>
</SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
</TableCell> */} </TableCell>
<TableCell className="space-x-2 text-right"> <TableCell className="space-x-2 text-right">
<Button <Button
size="sm" size="sm"
@@ -120,37 +245,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,44 @@ 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);
};
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 +96,28 @@ const ProductList = () => {
/> />
</div> </div>
<PalanTable <ProductTable
products={data?.results || []} products={data?.results || []}
count={data?.total || 0}
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 +126,8 @@ const ProductList = () => {
planDelete={planDelete} planDelete={planDelete}
setOpenDelete={setOpenDelete} setOpenDelete={setOpenDelete}
setPlanDelete={setPlanDelete} setPlanDelete={setPlanDelete}
selectedProducts={selectedProducts}
setSelectedProducts={setSelectedProducts}
/> />
</div> </div>
); );

View File

@@ -0,0 +1,18 @@
import httpClient from "@/shared/config/api/httpClient";
import { API_URLS } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
import type { PriceTypeResponse } from "./type";
const price_type_api = {
async list(): Promise<AxiosResponse<PriceTypeResponse[]>> {
const res = await httpClient.get(API_URLS.PriceTypeList);
return res;
},
async import() {
const res = await httpClient.post(API_URLS.PriceTypeImport);
return res;
},
};
export default price_type_api;

View File

@@ -0,0 +1,10 @@
export interface PriceTypeResponse {
id: number;
created_at: string;
name: string;
code: string;
with_card: string;
state: string;
price_type_kind: string;
currency_code: string;
}

View File

@@ -0,0 +1,72 @@
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { Loader2 } from "lucide-react";
import type { PriceTypeResponse } from "../lib/type";
interface Props {
data: PriceTypeResponse[] | [];
isLoading: boolean;
isError: boolean;
}
const TablePriceType = ({ data, isError, isLoading }: Props) => {
return (
<div className="flex-1 overflow-auto">
{isLoading && (
<div className="h-full flex items-center justify-center bg-white/70 z-10">
<span className="text-lg font-medium">
<Loader2 className="animate-spin" />
</span>
</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>
)}
{!isError && !isLoading && (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Nomi</TableHead>
<TableHead>Kodi</TableHead>
<TableHead>Valyuta kodi</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((d, index) => (
<TableRow key={d.id}>
<TableCell>{index + 1}</TableCell>
<TableCell>{d.name}</TableCell>
<TableCell>{d.code}</TableCell>
<TableCell>{d.currency_code}</TableCell>
</TableRow>
))}
{data.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center py-6">
Hech qanday tuman topilmadi
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
);
};
export default TablePriceType;

View File

@@ -0,0 +1,61 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import price_type_api from "../lib/api";
import TablePriceType from "./TablePriceType";
import { Button } from "@/shared/ui/button";
import { toast } from "sonner";
import type { AxiosError } from "axios";
const PriceTypeList = () => {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: () => price_type_api.import(),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["price_type"] });
toast.success("Narx turlari import qilindi", {
richColors: true,
position: "top-center",
});
},
onError: (err: AxiosError) => {
const errData = (err.response?.data as { data: string }).data;
const errMessage = (err.response?.data as { message: string }).message;
toast.error(errData || errMessage || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const { data, isLoading, isError } = useQuery({
queryKey: ["price_type"],
queryFn: () => price_type_api.list(),
select(data) {
return data.data;
},
});
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">Narx turlari</h1>
<div className="flex gap-4 justify-end">
<Button
className="bg-blue-500 h-12 cursor-pointer hover:bg-blue-500"
onClick={() => mutate()}
>
Narx turlarini import qilish
</Button>
</div>
</div>
<TablePriceType
data={data ? data : []}
isError={isError}
isLoading={isLoading}
/>
</div>
);
};
export default PriceTypeList;

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

@@ -2,11 +2,8 @@ export interface UserData {
id: number; id: number;
username: string; username: string;
first_name: string; first_name: string;
last_name: string; name: string;
middle_name: null | string; short_name: string;
gender: "M" | "F" | null;
region: null | string;
address: null | string;
created_at: string; created_at: string;
password_set: boolean; password_set: boolean;
} }

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 formatDate from "@/shared/lib/formatDate";
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,54 +41,15 @@ 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,
// setEditingUser,
// setDialogOpen,
// setOpenDelete,
// setUserDelete,
}: {
user: UserData;
setEditingUser?: (user: UserData) => void;
setDialogOpen?: (open: boolean) => void;
setOpenDelete?: Dispatch<SetStateAction<boolean>>;
setUserDelete?: Dispatch<SetStateAction<UserData | null>>;
}) {
const getGenderColor = (gender: "M" | "F" | null) => {
switch (gender) {
case "M":
return "bg-blue-100 text-blue-800";
case "F":
return "bg-pink-100 text-pink-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getGenderLabel = (gender: "M" | "F" | null) => { interface UserListProps {
switch (gender) { users: UserData[];
case "M": }
return "Erkak";
case "F":
return "Ayol";
default:
return "Ko'rsatilmagan";
}
};
const getInitials = () => { export function UserCard({ users }: UserListProps) {
if (user.first_name && user.last_name) { const [edit, setEdit] = useState<number | string | null>(null);
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase(); const [open, setOpen] = useState<boolean>(false);
} const queryClient = useQueryClient();
return user.username.substring(0, 2).toUpperCase();
};
const getFullName = () => {
const parts = [user.first_name, user.middle_name, user.last_name].filter(
Boolean,
);
return parts.length > 0 ? parts.join(" ") : user.username;
};
const form = useForm<z.infer<typeof passwordSet>>({ const form = useForm<z.infer<typeof passwordSet>>({
resolver: zodResolver(passwordSet), resolver: zodResolver(passwordSet),
@@ -91,11 +58,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 +69,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,113 +94,117 @@ 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>Ro'yxatdan o'tgan</TableHead>
</Avatar> <TableHead className="text-right">Amallar</TableHead>
<div> </TableRow>
<h3 className="font-semibold">{getFullName()}</h3> </TableHeader>
<p className="text-sm text-muted-foreground">@{user.username}</p> <TableBody>
</div> {users.map((user) => (
</div> <TableRow key={user.id} className="hover:bg-muted/50">
<Badge className={getGenderColor(user.gender)}> <TableCell className="font-medium">{user.id}</TableCell>
{getGenderLabel(user.gender)} <TableCell>
</Badge> <div className="flex flex-col">
</div> <span className="font-medium">{user.name}</span>
</CardHeader> </div>
</TableCell>
<TableCell>
<span className="text-muted-foreground">@{user.username}</span>
</TableCell>
<CardContent className="space-y-3"> <TableCell>
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
<Calendar className="h-4 w-4" /> {formatDate.format(user.created_at, "DD-MM-YYYY")}
<span> </span>
Ro'yxatdan o'tgan:{" "} </TableCell>
{new Date(user.created_at).toLocaleDateString("uz-UZ", { <TableCell className="text-right">
year: "numeric", <Dialog
month: "long", open={open && edit === user.id}
day: "numeric", onOpenChange={(isOpen) => {
})} setOpen(isOpen);
</span> if (!isOpen) {
</div> setEdit(null);
form.reset();
{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> <DialogTrigger asChild>
</div> <Button
)} size="sm"
variant="outline"
{user.address && ( className="cursor-pointer"
<div className="flex items-start gap-2 text-sm text-muted-foreground"> onClick={() => {
<MapPin className="h-4 w-4 mt-0.5" /> setOpen(true);
<span>Manzil: {user.address}</span> setEdit(user.id);
</div> }}
)} >
<Edit className="h-4 w-4 mr-2" />
{/* ID ma'lumoti */} Parol {user.password_set ? "O'zgartirish" : "Qo'yish"}
<div className="pt-2 border-t text-xs text-muted-foreground"> </Button>
ID: {user.id} </DialogTrigger>
</div> <DialogContent>
<DialogHeader className="text-xl font-semibold">
<div className="flex items-center gap-2 pt-2"> Parolni {user.password_set ? "o'zgartirish" : "qo'yish"}
<Dialog open={open} onOpenChange={setOpen}> </DialogHeader>
<DialogTrigger asChild> <div>
<Button <Form {...form}>
size="sm" <form
variant="outline" className="space-y-4"
className="flex-1 cursor-pointer" onSubmit={form.handleSubmit(onSubmit)}
onClick={() => { >
setOpen(true); <FormField
setEdit(user.id); name="password"
}} control={form.control}
> render={({ field }) => (
<Edit className="h-4 w-4 mr-2" /> <FormItem>
{user.password_set ? "Parolni o'zgartirish" : "Parol qo'yish"} <Label>Yangi parol</Label>
</Button> <FormControl>
</DialogTrigger> <Input
<DialogContent> type="password"
<DialogHeader className="text-xl font-semibold"> placeholder="12345678"
Parolni qo'yish className="h-12 focus-visible:ring-0"
</DialogHeader> {...field}
<div> />
<Form {...form}> </FormControl>
<form <FormMessage />
className="space-y-2" </FormItem>
onSubmit={form.handleSubmit(onSubmit)} )}
> />
<FormField <div className="flex justify-end gap-2">
name="password" <Button
control={form.control} type="button"
render={({ field }) => ( variant="outline"
<FormItem> onClick={() => {
<Label>Parolni yozing</Label> setOpen(false);
<FormControl> setEdit(null);
<Input form.reset();
placeholder="12345678" }}
className="h-12 focus-visible:ring-0" >
{...field} Bekor qilish
/> </Button>
</FormControl> <Button
<FormMessage /> type="submit"
</FormItem> className="bg-blue-500 hover:bg-blue-600 cursor-pointer"
)} >
/> Tasdiqlash
<div className="flex justify-end"> </Button>
<Button className="bg-blue-500 hover:bg-blue-600 cursor-pointer"> </div>
Tasdiqlash </form>
</Button> </Form>
</div> </div>
</form> </DialogContent>
</Form> </Dialog>
</div> </TableCell>
</DialogContent> </TableRow>
</Dialog> ))}
</div> </TableBody>
</CardContent> </Table>
</Card> </div>
); );
} }

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

12
src/pages/PriceType.tsx Normal file
View File

@@ -0,0 +1,12 @@
import PriceTypeList from "@/features/price-type/ui/priceTypeList";
import SidebarLayout from "@/SidebarLayout";
const PriceType = () => {
return (
<SidebarLayout>
<PriceTypeList />
</SidebarLayout>
);
};
export default PriceType;

View File

@@ -4,6 +4,7 @@ import Categories from "@/pages/Categories";
import Faq from "@/pages/Faq"; import Faq from "@/pages/Faq";
import HomePage from "@/pages/Home"; import HomePage from "@/pages/Home";
import Orders from "@/pages/Orders"; import Orders from "@/pages/Orders";
import PriceType from "@/pages/PriceType";
import Product from "@/pages/Product"; import Product from "@/pages/Product";
import Questionnaire from "@/pages/Questionnaire"; import Questionnaire from "@/pages/Questionnaire";
import Units from "@/pages/Units"; import Units from "@/pages/Units";
@@ -58,6 +59,10 @@ const AppRouter = () => {
path: "/dashboard/questionnaire", path: "/dashboard/questionnaire",
element: <Questionnaire />, element: <Questionnaire />,
}, },
{
path: "/dashboard/price-type",
element: <PriceType />,
},
]); ]);
return routes; return routes;

View File

@@ -15,6 +15,7 @@ export const API_URLS = {
UnitsDelete: (id: string) => `${API_V}admin/unity/${id}/delete/`, UnitsDelete: (id: string) => `${API_V}admin/unity/${id}/delete/`,
BannersList: `${API_V}admin/banner/list/`, BannersList: `${API_V}admin/banner/list/`,
BannerCreate: `${API_V}admin/banner/create/`, BannerCreate: `${API_V}admin/banner/create/`,
BannerUpdate: (id: number) => `${API_V}admin/banner/${id}/update/`,
BannerDelete: (id: string) => `${API_V}admin/banner/${id}/delete/`, BannerDelete: (id: string) => `${API_V}admin/banner/${id}/delete/`,
OrdersList: `${API_V}admin/order/list/`, OrdersList: `${API_V}admin/order/list/`,
OrdersDelete: (id: string | number) => `${API_V}admin/order/${id}/delete/`, OrdersDelete: (id: string | number) => `${API_V}admin/order/${id}/delete/`,
@@ -30,7 +31,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/`,
@@ -41,4 +42,9 @@ export const API_URLS = {
`${API_V}admin/category/${id}/upload_image/`, `${API_V}admin/category/${id}/upload_image/`,
PasswordSet: (id: number | string) => PasswordSet: (id: number | string) =>
`${API_V}admin/user/${id}/set_password/`, `${API_V}admin/user/${id}/set_password/`,
PriceTypeList: `${API_V}shared/price_type/`,
PriceTypeImport: `${API_V}shared/price_type/`,
Update_Pyment_Type: (id: number) =>
`${API_V}admin/product/${id}/update_payment_type/`,
Import_Balance: `${API_V}admin/product/import/balance/`,
}; };

View File

@@ -12,6 +12,7 @@
"users": "Foydalanuvchilar", "users": "Foydalanuvchilar",
"Hech kim": "Hech kim", "Hech kim": "Hech kim",
"Cancele": "Bekor qilish", "Cancele": "Bekor qilish",
"test": "test",
"Rasm": "Rasm", "Rasm": "Rasm",
"comment": "Izoh", "comment": "Izoh",
"new": "Yangi", "new": "Yangi",

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

View File

@@ -1,4 +1,5 @@
import { import {
BanknoteIcon,
CircleQuestionMark, CircleQuestionMark,
FileText, FileText,
FolderOpen, FolderOpen,
@@ -7,7 +8,6 @@ import {
LogOut, LogOut,
Package, Package,
Ruler, Ruler,
ShoppingCart,
Users, Users,
} from "lucide-react"; } from "lucide-react";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
@@ -42,6 +42,11 @@ const items = [
url: "/dashboard/categories", url: "/dashboard/categories",
icon: FolderOpen, icon: FolderOpen,
}, },
{
title: "Narx turlari",
url: "/dashboard/price-type",
icon: BanknoteIcon,
},
{ {
title: "Birliklar", title: "Birliklar",
url: "/dashboard/units", url: "/dashboard/units",
@@ -52,11 +57,11 @@ const items = [
url: "/dashboard/banners", url: "/dashboard/banners",
icon: Image, icon: Image,
}, },
{ // {
title: "Zakazlar", // title: "Zakazlar",
url: "/dashboard/orders", // url: "/dashboard/orders",
icon: ShoppingCart, // icon: ShoppingCart,
}, // },
{ {
title: "Foydalanuvchilar", title: "Foydalanuvchilar",
url: "/dashboard/users", url: "/dashboard/users",

View File

@@ -11,7 +11,7 @@ export default defineConfig({
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }], alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
}, },
server: { server: {
port: 3002, port: 8081,
open: true, open: true,
}, },
}); });