product page update

This commit is contained in:
Samandar Turgunboyev
2026-01-14 14:26:59 +05:00
parent 6370a118fe
commit 6bf98545db
20 changed files with 686 additions and 495 deletions

View File

@@ -21,6 +21,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",

33
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
'@radix-ui/react-popover': '@radix-ui/react-popover':
specifier: ^1.1.15 specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-scroll-area':
specifier: ^1.2.10
version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-select': '@radix-ui/react-select':
specifier: ^2.2.6 specifier: ^2.2.6
version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -806,6 +809,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-scroll-area@1.2.10':
resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-select@2.2.6': '@radix-ui/react-select@2.2.6':
resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
peerDependencies: peerDependencies:
@@ -2977,6 +2993,23 @@ snapshots:
'@types/react': 19.2.7 '@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7) '@types/react-dom': 19.2.3(@types/react@19.2.7)
'@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3)
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
optionalDependencies:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
'@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies: dependencies:
'@radix-ui/number': 1.1.1 '@radix-ui/number': 1.1.1

View File

@@ -26,4 +26,17 @@ export const categories_api = {
const res = await httpClient.delete(`${API_URLS.CategoryDelete(id)}`); const res = await httpClient.delete(`${API_URLS.CategoryDelete(id)}`);
return res; return res;
}, },
async import_category() {
const res = await httpClient.post(API_URLS.Import_Category);
return res;
},
async image_upload({ body, id }: { id: number | string; body: FormData }) {
const res = await httpClient.patch(
API_URLS.Upload_Image_Category(id),
body,
);
return res;
},
}; };

View File

@@ -13,6 +13,7 @@ import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label"; import { Label } from "@/shared/ui/label";
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 { Loader2, Upload } from "lucide-react"; import { Loader2, Upload } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react"; import { type Dispatch, type SetStateAction } from "react";
import { useForm, useWatch } from "react-hook-form"; import { useForm, useWatch } from "react-hook-form";
@@ -22,7 +23,7 @@ import z from "zod";
type FormValues = z.infer<typeof addDistrict>; type FormValues = z.infer<typeof addDistrict>;
interface Props { interface Props {
initialValues: CategoryItem | null; initialValues: CategoryItem | undefined;
setDialogOpen: Dispatch<SetStateAction<boolean>>; setDialogOpen: Dispatch<SetStateAction<boolean>>;
} }
@@ -30,48 +31,38 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({ const { mutate, isPending } = useMutation({
mutationFn: (body: FormData) => categories_api.create(body), mutationFn: ({ body, id }: { body: FormData; id: number | string }) =>
categories_api.image_upload({ id, body }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["categories"] }); queryClient.invalidateQueries({ queryKey: ["categories"] });
setDialogOpen(false); setDialogOpen(false);
}, },
onError: () => { onError: (err: AxiosError) => {
toast.error("Kategoriyani qo'shishda xatolik yuz berdi"); 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,
const { mutate: updateMutate, isPending: isUpdatePending } = useMutation({ position: "top-center",
mutationFn: ({ body, id }: { body: FormData; id: string }) => });
categories_api.update({ body, id }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["categories"] });
setDialogOpen(false);
},
onError: () => {
toast.error("Kategoriyani qo'shishda xatolik yuz berdi");
}, },
}); });
const form = useForm<FormValues>({ const form = useForm<FormValues>({
resolver: zodResolver(addDistrict), resolver: zodResolver(addDistrict),
defaultValues: { defaultValues: {
name_uz: initialValues?.name_uz ?? "", name_uz: initialValues?.name ?? "",
name_ru: initialValues?.name_ru ?? "", name_ru: initialValues?.name ?? "",
}, },
}); });
function onSubmit(values: FormValues) { function onSubmit(values: FormValues) {
const formData = new FormData(); const formData = new FormData();
formData.append("name_uz", values.name_uz);
formData.append("name_ru", values.name_ru);
if (values.image) { if (values.image) {
formData.append("image", values.image); formData.append("image", values.image);
} }
if (initialValues) { if (initialValues) {
updateMutate({ body: formData, id: initialValues.id }); mutate({ id: initialValues?.id, body: formData });
} else {
mutate(formData);
} }
} }
@@ -83,7 +74,7 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField {/* <FormField
name="name_uz" name="name_uz"
control={form.control} control={form.control}
render={({ field }) => ( render={({ field }) => (
@@ -117,7 +108,7 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> /> */}
<FormField <FormField
name="image" name="image"
control={form.control} control={form.control}
@@ -161,12 +152,10 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
<Button <Button
type="submit" type="submit"
className="w-full h-12 bg-blue-700 hover:bg-blue-800" className="w-full h-12 bg-blue-700 hover:bg-blue-800"
disabled={isPending || isUpdatePending} disabled={isPending}
> >
{isPending || isUpdatePending ? ( {isPending ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : initialValues ? (
"Tahrirlash"
) : ( ) : (
"Qo'shish" "Qo'shish"
)} )}

View File

@@ -55,6 +55,7 @@ const CategoriesList = () => {
<TableDistrict <TableDistrict
data={data ? data.results : []} data={data ? data.results : []}
isError={isError} isError={isError}
dialogOpen={dialogOpen}
isLoading={isLoading} isLoading={isLoading}
setDialogOpen={setDialogOpen} setDialogOpen={setDialogOpen}
setEditingDistrict={setEditingDistrict} setEditingDistrict={setEditingDistrict}

View File

@@ -1,53 +1,46 @@
import AddDistrict from "@/features/districts/ui/AddCategories"; import { categories_api } from "@/features/districts/lib/api";
import type { CategoryItem } from "@/features/plans/lib/data";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import { useMutation, useQueryClient } from "@tanstack/react-query";
Dialog, import type { AxiosError } from "axios";
DialogContent, import { toast } from "sonner";
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Plus } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react";
interface Props { // interface Props {
search: string; // search: string;
setSearch: Dispatch<SetStateAction<string>>; // setSearch: Dispatch<SetStateAction<string>>;
dialogOpen: boolean; // dialogOpen: boolean;
setDialogOpen: Dispatch<SetStateAction<boolean>>; // setDialogOpen: Dispatch<SetStateAction<boolean>>;
editing: CategoryItem | null; // editing: CategoryItem | null;
setEditing: Dispatch<SetStateAction<CategoryItem | null>>; // setEditing: Dispatch<SetStateAction<CategoryItem | null>>;
} // }
const FilterCategory = ({ const FilterCategory = () => {
dialogOpen, const queryClient = useQueryClient();
setDialogOpen, const { mutate } = useMutation({
setEditing, mutationFn: () => categories_api.import_category(),
editing, onSuccess: () => {
}: Props) => { queryClient.refetchQueries({ queryKey: ["categories"] });
toast.success("Kategoriyalar 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",
});
},
});
return ( return (
<div className="flex gap-4 justify-end"> <div className="flex gap-4 justify-end">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> <Button
<DialogTrigger asChild> className="bg-blue-500 h-12 cursor-pointer hover:bg-blue-500"
<Button onClick={() => mutate()}
onClick={() => setEditing(null)} >
className="bg-blue-500 h-12 cursor-pointer hover:bg-blue-500" Kategoriya import qilish
> </Button>
<Plus className="w-5 h-5 mr-1" /> Kategoriya qoshish
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-xl">
{editing ? "Kategoriyani tahrirlash" : "Kategoriya qoshish"}
</DialogTitle>
</DialogHeader>
<AddDistrict initialValues={editing} setDialogOpen={setDialogOpen} />
</DialogContent>
</Dialog>
</div> </div>
); );
}; };

View File

@@ -1,6 +1,13 @@
import AddDistrict from "@/features/districts/ui/AddCategories";
import type { CategoryItem } from "@/features/plans/lib/data"; import type { CategoryItem } 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 {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { import {
Table, Table,
TableBody, TableBody,
@@ -9,8 +16,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/shared/ui/table"; } from "@/shared/ui/table";
import { Edit, Loader2, Trash } from "lucide-react"; import { Image, Loader2 } from "lucide-react";
import type { Dispatch, SetStateAction } from "react"; import { useState, type Dispatch, type SetStateAction } from "react";
interface Props { interface Props {
data: CategoryItem[] | []; data: CategoryItem[] | [];
@@ -19,6 +26,7 @@ interface Props {
handleDelete: (user: CategoryItem) => void; handleDelete: (user: CategoryItem) => void;
setDialogOpen: Dispatch<SetStateAction<boolean>>; setDialogOpen: Dispatch<SetStateAction<boolean>>;
setEditingDistrict: Dispatch<SetStateAction<CategoryItem | null>>; setEditingDistrict: Dispatch<SetStateAction<CategoryItem | null>>;
dialogOpen: boolean;
currentPage: number; currentPage: number;
} }
@@ -26,10 +34,10 @@ const TableDistrict = ({
data, data,
isError, isError,
isLoading, isLoading,
handleDelete, dialogOpen,
setDialogOpen, setDialogOpen,
setEditingDistrict,
}: Props) => { }: Props) => {
const [initialValues, setEditing] = useState<CategoryItem>();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{isLoading && ( {isLoading && (
@@ -55,7 +63,8 @@ const TableDistrict = ({
<TableHead>ID</TableHead> <TableHead>ID</TableHead>
<TableHead>Rasmi</TableHead> <TableHead>Rasmi</TableHead>
<TableHead>Nomi (uz)</TableHead> <TableHead>Nomi (uz)</TableHead>
<TableHead>Nomi (ru)</TableHead> <TableHead>Kategoriya idsi</TableHead>
<TableHead>Turi</TableHead>
<TableHead className="text-right">Harakatlar</TableHead> <TableHead className="text-right">Harakatlar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -66,34 +75,35 @@ const TableDistrict = ({
<TableCell>{index + 1}</TableCell> <TableCell>{index + 1}</TableCell>
<TableCell> <TableCell>
<img <img
src={API_URLS.BASE_URL + d.image} src={d.image ? API_URLS.BASE_URL + d.image : "/logo.png"}
alt={d.name_uz} alt={d.name}
className="w-10 h-10 object-cover rounded-md" className="w-10 h-10 object-cover rounded-md"
/> />
</TableCell> </TableCell>
<TableCell>{d.name_uz}</TableCell> <TableCell>{d.name}</TableCell>
<TableCell>{d.name_ru}</TableCell> <TableCell>{d.category_id ? d.category_id : "----"}</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"
size="sm" size="sm"
onClick={() => { onClick={() => {
setEditingDistrict(d); setEditing(d);
setDialogOpen(true); setDialogOpen(true);
}} }}
className="bg-blue-500 text-white hover:text-white hover:bg-blue-500 cursor-pointer" className="bg-blue-500 text-white hover:text-white hover:bg-blue-500 cursor-pointer"
> >
<Edit className="w-4 h-4" /> <Image className="w-4 h-4" />
</Button> </Button>
<Button {/* <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
onClick={() => handleDelete(d)} onClick={() => handleDelete(d)}
className="cursor-pointer" className="cursor-pointer"
> >
<Trash className="w-4 h-4" /> <Trash className="w-4 h-4" />
</Button> </Button> */}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@@ -108,6 +118,18 @@ const TableDistrict = ({
</TableBody> </TableBody>
</Table> </Table>
)} )}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-xl">Rasmdi qo'shish</DialogTitle>
</DialogHeader>
<AddDistrict
setDialogOpen={setDialogOpen}
initialValues={initialValues}
/>
</DialogContent>
</Dialog>
</div> </div>
); );
}; };

View File

@@ -47,4 +47,16 @@ export const plans_api = {
const res = await httpClient.get(`${API_URLS.UnityList}`, { params }); const res = await httpClient.get(`${API_URLS.UnityList}`, { params });
return res; return res;
}, },
async exportProduct() {
const res = await httpClient.get(API_URLS.Export_Product, {
responseType: "blob",
});
return res;
},
async importProduct() {
const res = await httpClient.post(API_URLS.Import_Product);
return res;
},
}; };

View File

@@ -9,30 +9,34 @@ export interface ProductsList {
} }
export interface Product { export interface Product {
id: string; id: number;
name_uz: string;
name_ru: string;
is_active: boolean;
image: string;
category: string;
price: number;
description_uz: string;
description_ru: string;
unity: string;
tg_id: string;
code: string;
article: string;
quantity_left: number;
min_quantity: number;
brand: null | string;
return_date: null | string;
expires_date: null | string;
manufacturer: null | string;
volume: string;
images: { images: {
id: string; id: number;
image: string; image: string;
}[]; }[];
meansurement: null | number;
inventory_id: null | string;
product_id: string;
code: string;
name: string;
short_name: string;
weight_netto: null | string;
weight_brutto: null | string;
litr: null | string;
box_type_code: null | string;
box_quant: null | string;
groups: {
id: number;
name: string;
image: string;
type: string;
}[];
state: string;
barcodes: null | string;
article_code: null | string;
marketing_group_code: null | string;
inventory_kinds: { id: number; name: string }[];
sector_codes: { id: number; code: string }[];
} }
export interface Category { export interface Category {
@@ -46,11 +50,12 @@ export interface Category {
} }
export interface CategoryItem { export interface CategoryItem {
id: string; id: number;
name_uz: string; name: string;
name_ru: string; image: string | null;
image: string;
order: number; order: number;
category_id: null | number;
type: null | string;
} }
export interface UnityList { export interface UnityList {

View File

@@ -1,51 +1,40 @@
"use client"; "use client";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { useRef } from "react"; import { useMutation } from "@tanstack/react-query";
import { plans_api } from "../lib/api";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import type { AxiosError } from "axios";
export default function ExcelUpload() { export default function ExcelUpload() {
const fileInputRef = useRef<HTMLInputElement | null>(null); const { mutate, isPending } = useMutation({
mutationFn: () => plans_api.importProduct(),
const handleButtonClick = () => { onSuccess: (res) => {
fileInputRef.current?.click(); toast.success(res.data.message, {
}; richColors: true,
position: "top-center",
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { });
const file = e.target.files?.[0]; },
if (!file) return; onError: (err: AxiosError) => {
const errMessage = (err.response?.data as { message: string }).message;
// Excel format tekshiruvi toast.error(errMessage || "Xatolik yuz berdi", {
const allowedTypes = [ richColors: true,
"application/vnd.ms-excel", position: "top-center",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", });
]; },
});
if (!allowedTypes.includes(file.type)) {
alert("Iltimos, faqat Excel (.xls, .xlsx) fayl yuklang");
return;
}
// 👉 shu yerda backendga yuborasiz
// uploadExcel(file)
};
return ( return (
<> <>
<Button <Button
onClick={handleButtonClick} onClick={() => mutate()}
className="h-12 bg-blue-500 text-white hover:bg-blue-600" className="h-12 bg-blue-500 text-white hover:bg-blue-600"
variant="secondary" variant="secondary"
> >
Excel yuklash Import qilish
{isPending && <Loader2 className="animate-spin" />}
</Button> </Button>
<input
ref={fileInputRef}
type="file"
accept=".xls,.xlsx"
className="hidden"
onChange={handleFileChange}
/>
</> </>
); );
} }

View File

@@ -1,17 +1,12 @@
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 AddedPlan from "@/features/plans/ui/AddedPlan";
import ExcelUpload from "@/features/plans/ui/ExcelUpload"; import ExcelUpload from "@/features/plans/ui/ExcelUpload";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input"; import { Input } from "@/shared/ui/input";
import { AlertCircle, Plus } from "lucide-react"; import { useMutation } 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 {
searchUser: string; searchUser: string;
@@ -22,33 +17,54 @@ interface Props {
setEditingPlan: Dispatch<SetStateAction<Product | null>>; setEditingPlan: Dispatch<SetStateAction<Product | null>>;
} }
const FilterPlans = ({ const FilterPlans = ({ searchUser, setSearchUser }: Props) => {
searchUser, const { mutate } = useMutation({
setSearchUser, mutationFn: () => plans_api.exportProduct(),
dialogOpen, onSuccess: (response) => {
setDialogOpen, const blob = new Blob([response.data], {
setEditingPlan, type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
editingPlan, });
}: Props) => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "products.xlsx";
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
},
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",
});
},
});
return ( return (
<div className="flex gap-2 mb-4"> <div className="flex gap-2 mb-4">
<Input <Input
type="text" type="text"
placeholder="Foydalanuvchi ismi" placeholder="Mahsulot nomi"
className="h-12" className="h-12"
value={searchUser} value={searchUser}
onChange={(e) => setSearchUser(e.target.value)} onChange={(e) => setSearchUser(e.target.value)}
/> />
<Dialog>
<DialogTrigger asChild> <Button
<Button className="h-12 bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
className="h-12 bg-blue-500 text-white hover:bg-blue-600 cursor-pointer" variant={"secondary"}
variant={"secondary"} onClick={() => mutate()}
> >
Excel uchun shablon Excel yuklab olish
</Button> </Button>
</DialogTrigger>
<DialogContent> {/* <DialogContent>
<DialogHeader className="text-xl font-medium"> <DialogHeader className="text-xl font-medium">
Excel uchun kerakli shablon Excel uchun kerakli shablon
</DialogHeader> </DialogHeader>
@@ -60,10 +76,9 @@ const FilterPlans = ({
xatolik yuz berishi mumkin. xatolik yuz berishi mumkin.
</p> </p>
</div> </div>
</DialogContent> </DialogContent> */}
</Dialog>
<ExcelUpload /> <ExcelUpload />
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> {/*<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
variant="default" variant="default"
@@ -85,7 +100,7 @@ const FilterPlans = ({
setDialogOpen={setDialogOpen} setDialogOpen={setDialogOpen}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>*/}
</div> </div>
); );
}; };

View File

@@ -1,17 +1,8 @@
"use client"; "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 formatPrice from "@/shared/lib/formatPrice";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { import {
Table, Table,
TableBody, TableBody,
@@ -20,12 +11,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/shared/ui/table"; } from "@/shared/ui/table";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Eye, Loader2 } from "lucide-react";
import type { AxiosError } from "axios";
import clsx from "clsx";
import { Edit, Eye, Loader2, Trash } 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[] | [];
@@ -45,59 +32,7 @@ const ProductTable = ({
isError, isError,
setEditingProduct, setEditingProduct,
setDetailOpen, setDetailOpen,
setDialogOpen,
handleDelete,
}: Props) => { }: Props) => {
const { data } = useQuery({
queryKey: ["categories"],
queryFn: () => {
return plans_api.categories({ page: 1, page_size: 1000 });
},
select(data) {
return data.data.results;
},
});
const { data: unityData } = useQuery({
queryKey: ["unity"],
queryFn: () => {
return plans_api.unity({ page: 1, page_size: 1000 });
},
select(data) {
return data.data.results;
},
});
const queryClient = useQueryClient();
const { mutate: updated } = useMutation({
mutationFn: ({ body, id }: { id: string; body: FormData }) =>
plans_api.update({ body, id }),
onSuccess() {
toast.success("Mahsulot statusi o'zgardi", {
richColors: true,
position: "top-center",
});
queryClient.refetchQueries({ queryKey: ["product_list"] });
setDialogOpen(false);
},
onError: (err: AxiosError) => {
toast.error((err.response?.data as string) || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const handleStatusChange = async (
product: string,
status: "true" | "false",
) => {
const formData = new FormData();
formData.append("is_active", status);
updated({ body: formData, id: product });
};
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">
@@ -121,43 +56,39 @@ const ProductTable = ({
<TableRow> <TableRow>
<TableHead>ID</TableHead> <TableHead>ID</TableHead>
<TableHead>Rasmi</TableHead> <TableHead>Rasmi</TableHead>
<TableHead>Nomi (UZ)</TableHead> <TableHead>Nomi</TableHead>
<TableHead>Nomi (RU)</TableHead> <TableHead>Tavsif</TableHead>
<TableHead>Tavsif (UZ)</TableHead> <TableHead className="text-end">Harakatlar</TableHead>
<TableHead>Tavsif (RU)</TableHead>
<TableHead>Kategoriya</TableHead>
<TableHead>Birligi</TableHead>
<TableHead>Brendi</TableHead>
<TableHead>Narx</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Harakatlar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{products.map((product, index) => { {products.map((product, index) => {
const cat = data?.find((c) => c.id === product.category);
const unity = unityData?.find((u) => u.id === product.unity);
return ( return (
<TableRow key={product.id}> <TableRow key={product.id}>
<TableCell>{index + 1}</TableCell> <TableCell>{index + 1}</TableCell>
{product.images.length > 0 ? (
<TableCell>
<img
src={API_URLS.BASE_URL + product.images[0].image}
alt={product.name}
className="w-16 h-16 object-cover rounded"
/>
</TableCell>
) : (
<TableCell>
<img
src={"/logo.png"}
alt={product.name}
className="w-10 h-10 object-cover rounded"
/>
</TableCell>
)}
<TableCell>{product.name}</TableCell>
<TableCell> <TableCell>
<img {product.short_name && product.short_name.slice(0, 15)}...
src={API_URLS.BASE_URL + product.image}
alt={product.name_uz}
className="w-16 h-16 object-cover rounded"
/>
</TableCell> </TableCell>
<TableCell>{product.name_uz}</TableCell> {/* <TableCell
<TableCell>{product.name_ru}</TableCell>
<TableCell>{product.description_uz.slice(0, 15)}...</TableCell>
<TableCell>{product.description_ru.slice(0, 15)}...</TableCell>
<TableCell>{cat?.name_uz}</TableCell>
<TableCell>{unity?.name_uz}</TableCell>
<TableCell>{product.brand}</TableCell>
<TableCell>{formatPrice(product.price, true)}</TableCell>
<TableCell
className={clsx( className={clsx(
product.is_active ? "text-green-600" : "text-red-600", product.is_active ? "text-green-600" : "text-red-600",
)} )}
@@ -176,7 +107,7 @@ const ProductTable = ({
<SelectItem value="false">Nofaol</SelectItem> <SelectItem value="false">Nofaol</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</TableCell> </TableCell> */}
<TableCell className="space-x-2 text-right"> <TableCell className="space-x-2 text-right">
<Button <Button
@@ -190,7 +121,7 @@ const ProductTable = ({
<Eye className="h-4 w-4" /> <Eye className="h-4 w-4" />
</Button> </Button>
<Button {/*<Button
size="sm" size="sm"
className="bg-blue-500 text-white hover:bg-blue-600" className="bg-blue-500 text-white hover:bg-blue-600"
onClick={() => { onClick={() => {
@@ -207,7 +138,7 @@ const ProductTable = ({
onClick={() => handleDelete(product)} onClick={() => handleDelete(product)}
> >
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />
</Button> </Button>*/}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );

View File

@@ -1,218 +1,225 @@
import { categories_api } from "@/features/districts/lib/api"; "use client";
import type { Product } from "@/features/plans/lib/data";
import { unity_api } from "@/features/units/lib/api";
import { API_URLS } from "@/shared/config/api/URLs";
import { Badge } from "@/shared/ui/badge";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/shared/ui/dialog"; } from "@/shared/ui/dialog";
import { useQuery } from "@tanstack/react-query"; import { ScrollArea } from "@/shared/ui/scroll-area";
import { type Dispatch, type SetStateAction } from "react"; import type { Product } from "../lib/data";
import { Badge } from "@/shared/ui/badge";
import { API_URLS } from "@/shared/config/api/URLs";
interface Props { interface ProductDetailModalProps {
setDetail: Dispatch<SetStateAction<boolean>>; product: Product | null;
detail: boolean; isOpen: boolean;
plan: Product | null; onClose: () => void;
} }
const PlanDetail = ({ detail, setDetail, plan }: Props) => { export function PlanDetail({
const { data } = useQuery({ product,
queryKey: ["categories"], isOpen,
queryFn: () => onClose,
categories_api.list({ }: ProductDetailModalProps) {
page: 1, if (!product) return null;
page_size: 999,
}),
select(data) {
return data.data;
},
});
const { data: unity } = useQuery({
queryKey: ["unity_list"],
queryFn: () =>
unity_api.list({
page: 1,
page_size: 999,
}),
select(data) {
return data.data;
},
});
if (!plan) return null;
return ( return (
<Dialog open={detail} onOpenChange={setDetail}> <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto"> <DialogContent className="max-w-3xl max-h-[90vh] p-0 overflow-hidden">
<DialogHeader> <ScrollArea className="h-full">
<DialogTitle className="text-2xl font-bold"> <div className="p-6">
{plan.name_uz} <DialogHeader className="mb-6">
</DialogTitle> <DialogTitle className="text-2xl">{product.name}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-6 mt-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Product Images */} {/* Images Section */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3"> <div className="space-y-4">
{plan.image && ( <h3 className="font-semibold text-lg">Rasmlar</h3>
<img {product.images && product.images.length > 0 ? (
src={API_URLS.BASE_URL + plan.image} <div className="grid grid-cols-2 gap-4">
alt={plan.name_uz} {product.images.map((img) => (
className="w-full h-48 object-cover rounded-lg border" <div
/> key={img.id}
)} className="relative w-full aspect-square bg-muted rounded-lg overflow-hidden"
{plan.images.map((img) => ( >
<img <img
key={img.id} src={
src={API_URLS.BASE_URL + img.image} API_URLS.BASE_URL + img.image || "/placeholder.svg"
alt={plan.name_uz} }
className="w-full h-48 object-cover rounded-lg border" alt={product.name}
/> className="object-cover"
))} />
</div> </div>
))}
</div>
) : (
<div className="w-full aspect-square bg-muted rounded-lg flex items-center justify-center text-muted-foreground">
Rasm yo'q
</div>
)}
</div>
{/* Product Status */} {/* Product Details */}
<div className="flex items-center gap-3"> <div className="space-y-4">
<Badge <div>
className={ <label className="text-sm text-muted-foreground">
plan.is_active Mahsulot ID
? "bg-green-100 text-green-700" </label>
: "bg-red-100 text-red-700" <p className="font-mono text-sm">{product.product_id}</p>
} </div>
>
{plan.is_active ? "Faol" : "Nofaol"}
</Badge>
{plan.quantity_left <= plan.min_quantity && (
<Badge className="bg-orange-100 text-orange-700">Kam qoldi</Badge>
)}
</div>
{/* Basic Information */} <div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <label className="text-sm text-muted-foreground">Kod</label>
<div> <p className="font-mono text-sm">{product.code}</p>
<p className="font-semibold text-gray-900">Nomi (O'zbekcha):</p> </div>
<p className="text-gray-700">{plan.name_uz}</p>
</div>
<div>
<p className="font-semibold text-gray-900">Nomi (Ruscha):</p>
<p className="text-gray-700">{plan.name_ru}</p>
</div>
</div>
{/* Descriptions */} <div>
<div className="space-y-3"> <label className="text-sm text-muted-foreground">
<div> Qisqa nom
<p className="font-semibold text-gray-900"> </label>
Tavsifi (O'zbekcha): <p className="text-sm">{product.short_name || "—"}</p>
</p> </div>
<p className="text-gray-700">{plan.description_uz}</p>
</div>
<div>
<p className="font-semibold text-gray-900">Tavsifi (Ruscha):</p>
<p className="text-gray-700">{plan.description_ru}</p>
</div>
</div>
{/* Price and Category */} <div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 bg-gray-50 p-4 rounded-lg"> <label className="text-sm text-muted-foreground">
<div> Holati
<p className="font-semibold text-gray-900">Narxi:</p> </label>
<p className="text-xl font-bold text-blue-600"> <Badge
{plan.price.toLocaleString()} so'm variant={
</p> product.state === "active" ? "default" : "secondary"
</div> }
<div> className="mt-1"
<p className="font-semibold text-gray-900">Kategoriya:</p> >
<p className="text-gray-700"> {product.state}
{data?.results.find((e) => e.id === plan.category)?.name_uz} </Badge>
</p> </div>
</div>
<div>
<p className="font-semibold text-gray-900">O'lchov birligi:</p>
<p className="text-gray-700">
{unity?.results.find((e) => e.id === plan.unity)?.name_uz}
</p>
</div>
</div>
{/* Product Details */} {product.article_code && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div>
<div> <label className="text-sm text-muted-foreground">
<p className="font-semibold text-gray-900">Kod:</p> Artikul kodi
<p className="text-gray-700">{plan.code}</p> </label>
<p className="font-mono text-sm">{product.article_code}</p>
</div>
)}
</div>
</div> </div>
<div>
<p className="font-semibold text-gray-900">Artikul:</p> {/* Weight and Size Info */}
<p className="text-gray-700">{plan.article}</p> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6 pt-6 border-t">
{product.weight_netto && (
<div>
<label className="text-xs text-muted-foreground">
Xalol Vazni
</label>
<p className="font-semibold">{product.weight_netto} kg</p>
</div>
)}
{product.weight_brutto && (
<div>
<label className="text-xs text-muted-foreground">
Jami Vazni
</label>
<p className="font-semibold">{product.weight_brutto} kg</p>
</div>
)}
{product.litr && (
<div>
<label className="text-xs text-muted-foreground">Hajmi</label>
<p className="font-semibold">{product.litr} L</p>
</div>
)}
{product.meansurement && (
<div>
<label className="text-xs text-muted-foreground">
O'lchami
</label>
<p className="font-semibold">{product.meansurement}</p>
</div>
)}
</div> </div>
{plan.brand && (
<div> {/* Box Info */}
<p className="font-semibold text-gray-900">Brand:</p> {(product.box_type_code || product.box_quant) && (
<p className="text-gray-700">{plan.brand}</p> <div className="grid grid-cols-2 gap-4 mt-4 p-4 bg-muted rounded-lg">
{product.box_type_code && (
<div>
<label className="text-xs text-muted-foreground">
Qutining turi
</label>
<p className="font-semibold">{product.box_type_code}</p>
</div>
)}
{product.box_quant && (
<div>
<label className="text-xs text-muted-foreground">
Qutida miqdori
</label>
<p className="font-semibold">{product.box_quant}</p>
</div>
)}
</div> </div>
)} )}
{plan.manufacturer && (
<div> {/* Groups */}
<p className="font-semibold text-gray-900"> {product.groups && product.groups.length > 0 && (
Ishlab chiqaruvchi: <div className="mt-6 pt-6 border-t">
<h3 className="font-semibold mb-3">Guruhlari</h3>
<div className="flex flex-wrap gap-2">
{product.groups.map((group) => (
<Badge key={group.id} variant="outline">
{group.name}
</Badge>
))}
</div>
</div>
)}
{/* Inventory Kinds */}
{product.inventory_kinds && product.inventory_kinds.length > 0 && (
<div className="mt-4">
<h3 className="font-semibold mb-3">Omborxona turlari</h3>
<div className="flex flex-wrap gap-2">
{product.inventory_kinds.map((kind) => (
<Badge key={kind.id} variant="secondary">
{kind.name}
</Badge>
))}
</div>
</div>
)}
{/* Sector Codes */}
{product.sector_codes && product.sector_codes.length > 0 && (
<div className="mt-4">
<h3 className="font-semibold mb-3">Sektor kodlari</h3>
<div className="space-y-2">
{product.sector_codes.map((sector) => (
<div
key={sector.id}
className="p-2 bg-muted rounded-md font-mono text-sm"
>
{sector.code}
</div>
))}
</div>
</div>
)}
{/* Barcodes */}
{product.barcodes && (
<div className="mt-6 pt-6 border-t">
<label className="text-sm font-semibold">Barcodlar</label>
<p className="font-mono text-sm mt-2 p-3 bg-muted rounded-lg break-all">
{product.barcodes}
</p> </p>
<p className="text-gray-700">{plan.manufacturer}</p>
</div> </div>
)} )}
<div>
<p className="font-semibold text-gray-900">Hajm:</p>
<p className="text-gray-700">{plan.volume}</p>
</div>
</div> </div>
</ScrollArea>
{/* Quantity Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-blue-50 p-4 rounded-lg">
<div>
<p className="font-semibold text-gray-900">Qolgan miqdor:</p>
<p className="text-lg font-semibold text-gray-700">
{plan.quantity_left}
</p>
</div>
<div>
<p className="font-semibold text-gray-900">Minimal miqdor:</p>
<p className="text-lg font-semibold text-gray-700">
{plan.min_quantity}
</p>
</div>
</div>
{/* Dates */}
{(plan.return_date || plan.expires_date) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{plan.return_date && (
<div>
<p className="font-semibold text-gray-900">
Qaytarish sanasi:
</p>
<p className="text-gray-700">
{new Date(plan.return_date).toLocaleDateString("uz-UZ")}
</p>
</div>
)}
{plan.expires_date && (
<div>
<p className="font-semibold text-gray-900">
Amal qilish muddati:
</p>
<p className="text-gray-700">
{new Date(plan.expires_date).toLocaleDateString("uz-UZ")}
</p>
</div>
)}
</div>
)}
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; }
export default PlanDetail;

View File

@@ -3,10 +3,10 @@ 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 PalanTable from "@/features/plans/ui/PalanTable";
import PlanDetail from "@/features/plans/ui/PlanDetail";
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";
import { PlanDetail } from "./PlanDetail";
const ProductList = () => { const ProductList = () => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@@ -51,7 +51,11 @@ const ProductList = () => {
setSearchUser={setSearchUser} setSearchUser={setSearchUser}
/> />
<PlanDetail detail={detail} setDetail={setDetail} plan={editingPlan} /> <PlanDetail
product={editingPlan}
isOpen={detail}
onClose={() => setDetail(false)}
/>
</div> </div>
<PalanTable <PalanTable

View File

@@ -31,4 +31,15 @@ export const user_api = {
const res = await httpClient.post(API_URLS.Import_User); const res = await httpClient.post(API_URLS.Import_User);
return res; return res;
}, },
async password_set({
body,
id,
}: {
id: string | number;
body: { password: string };
}) {
const res = await httpClient.post(API_URLS.PasswordSet(id), body);
return res;
},
}; };

View File

@@ -8,6 +8,7 @@ export interface UserData {
region: null | string; region: null | string;
address: null | string; address: null | string;
created_at: string; created_at: string;
password_set: boolean;
} }
export interface UserListRes { export interface UserListRes {

View File

@@ -1,12 +1,40 @@
"use client"; "use client";
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 { 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 { Card, CardContent, CardHeader } from "@/shared/ui/card"; import { Card, CardContent, CardHeader } from "@/shared/ui/card";
import { Calendar, MapPin } from "lucide-react"; import {
import { type Dispatch, type SetStateAction } from "react"; Dialog,
DialogContent,
DialogHeader,
DialogTrigger,
} from "@/shared/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Calendar, Edit, MapPin } from "lucide-react";
import { useState, type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";
const passwordSet = z.object({
password: z
.string()
.min(8, { message: "Eng kamida 8ta belgi bolishi kerak" }),
});
export function UserCard({ export function UserCard({
user, user,
// setEditingUser, // setEditingUser,
@@ -56,6 +84,52 @@ export function UserCard({
return parts.length > 0 ? parts.join(" ") : user.username; return parts.length > 0 ? parts.join(" ") : user.username;
}; };
const form = useForm<z.infer<typeof passwordSet>>({
resolver: zodResolver(passwordSet),
defaultValues: {
password: "",
},
});
const [edit, setEdit] = useState<number | string | null>(null);
const [open, setOpen] = useState<boolean>(false);
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: ({
id,
body,
}: {
id: number | string;
body: { password: string };
}) => user_api.password_set({ body, id }),
onSuccess: () => {
setOpen(false);
setEdit(null);
queryClient.refetchQueries({ queryKey: ["user_list"] });
toast.success("Parol qo'yildi", {
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",
});
},
});
function onSubmit(value: z.infer<typeof passwordSet>) {
if (edit) {
mutate({ body: { password: value.password }, id: edit });
}
}
return ( return (
<Card className="group hover:shadow-md transition-shadow"> <Card className="group hover:shadow-md transition-shadow">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
@@ -109,33 +183,60 @@ export function UserCard({
ID: {user.id} ID: {user.id}
</div> </div>
{/* Uncommnet qilish uchun tugmalar
<div className="flex items-center gap-2 pt-2"> <div className="flex items-center gap-2 pt-2">
<Button <Dialog open={open} onOpenChange={setOpen}>
size="sm" <DialogTrigger asChild>
variant="outline" <Button
onClick={() => { size="sm"
setEditingUser(user); variant="outline"
setDialogOpen?.(true); className="flex-1 cursor-pointer"
}} onClick={() => {
className="flex-1" setOpen(true);
> setEdit(user.id);
<Edit className="h-4 w-4 mr-2" /> }}
Tahrirlash >
</Button> <Edit className="h-4 w-4 mr-2" />
<Button {user.password_set ? "Parolni o'zgartirish" : "Parol qo'yish"}
size="sm" </Button>
variant="outline" </DialogTrigger>
onClick={() => { <DialogContent>
setOpenDelete(true); <DialogHeader className="text-xl font-semibold">
setUserDelete(user); Parolni qo'yish
}} </DialogHeader>
className="text-red-600 hover:text-red-700" <div>
> <Form {...form}>
<Trash2 className="h-4 w-4" /> <form
</Button> className="space-y-2"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<Label>Parolni yozing</Label>
<FormControl>
<Input
placeholder="12345678"
className="h-12 focus-visible:ring-0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button className="bg-blue-500 hover:bg-blue-600 cursor-pointer">
Tasdiqlash
</Button>
</div>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
</div> </div>
*/}
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -34,4 +34,11 @@ export const API_URLS = {
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/`,
Export_Product: `${API_V}admin/product/export/`,
Import_Product: `${API_V}admin/product/import/`,
Import_Category: `${API_V}admin/category/import/`,
Upload_Image_Category: (id: number | string) =>
`${API_V}admin/category/${id}/upload_image/`,
PasswordSet: (id: number | string) =>
`${API_V}admin/user/${id}/set_password/`,
}; };

View File

@@ -0,0 +1,56 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/shared/lib/utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

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: 3000, port: 3002,
open: true, open: true,
}, },
}); });