product page update
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@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-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
|
||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@@ -32,6 +32,9 @@ importers:
|
||||
'@radix-ui/react-popover':
|
||||
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)
|
||||
'@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':
|
||||
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)
|
||||
@@ -806,6 +809,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
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':
|
||||
resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
|
||||
peerDependencies:
|
||||
@@ -2977,6 +2993,23 @@ snapshots:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.1
|
||||
|
||||
@@ -26,4 +26,17 @@ export const categories_api = {
|
||||
const res = await httpClient.delete(`${API_URLS.CategoryDelete(id)}`);
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ 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 { Loader2, Upload } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
@@ -22,7 +23,7 @@ import z from "zod";
|
||||
type FormValues = z.infer<typeof addDistrict>;
|
||||
|
||||
interface Props {
|
||||
initialValues: CategoryItem | null;
|
||||
initialValues: CategoryItem | undefined;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
@@ -30,48 +31,38 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
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: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["categories"] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Kategoriyani qo'shishda xatolik yuz berdi");
|
||||
},
|
||||
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 { mutate: updateMutate, isPending: isUpdatePending } = useMutation({
|
||||
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>({
|
||||
resolver: zodResolver(addDistrict),
|
||||
defaultValues: {
|
||||
name_uz: initialValues?.name_uz ?? "",
|
||||
name_ru: initialValues?.name_ru ?? "",
|
||||
name_uz: initialValues?.name ?? "",
|
||||
name_ru: initialValues?.name ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: FormValues) {
|
||||
const formData = new FormData();
|
||||
formData.append("name_uz", values.name_uz);
|
||||
formData.append("name_ru", values.name_ru);
|
||||
if (values.image) {
|
||||
formData.append("image", values.image);
|
||||
}
|
||||
|
||||
if (initialValues) {
|
||||
updateMutate({ body: formData, id: initialValues.id });
|
||||
} else {
|
||||
mutate(formData);
|
||||
mutate({ id: initialValues?.id, body: formData });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +74,7 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
{/* <FormField
|
||||
name="name_uz"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
@@ -117,7 +108,7 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
<FormField
|
||||
name="image"
|
||||
control={form.control}
|
||||
@@ -161,12 +152,10 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
|
||||
<Button
|
||||
type="submit"
|
||||
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" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Qo'shish"
|
||||
)}
|
||||
|
||||
@@ -55,6 +55,7 @@ const CategoriesList = () => {
|
||||
<TableDistrict
|
||||
data={data ? data.results : []}
|
||||
isError={isError}
|
||||
dialogOpen={dialogOpen}
|
||||
isLoading={isLoading}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingDistrict={setEditingDistrict}
|
||||
|
||||
@@ -1,53 +1,46 @@
|
||||
import AddDistrict from "@/features/districts/ui/AddCategories";
|
||||
import type { CategoryItem } from "@/features/plans/lib/data";
|
||||
import { categories_api } from "@/features/districts/lib/api";
|
||||
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";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
search: string;
|
||||
setSearch: Dispatch<SetStateAction<string>>;
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
editing: CategoryItem | null;
|
||||
setEditing: Dispatch<SetStateAction<CategoryItem | null>>;
|
||||
}
|
||||
// interface Props {
|
||||
// search: string;
|
||||
// setSearch: Dispatch<SetStateAction<string>>;
|
||||
// dialogOpen: boolean;
|
||||
// setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
// editing: CategoryItem | null;
|
||||
// setEditing: Dispatch<SetStateAction<CategoryItem | null>>;
|
||||
// }
|
||||
|
||||
const FilterCategory = ({
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
setEditing,
|
||||
editing,
|
||||
}: Props) => {
|
||||
const FilterCategory = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: () => categories_api.import_category(),
|
||||
onSuccess: () => {
|
||||
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 (
|
||||
<div className="flex gap-4 justify-end">
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => setEditing(null)}
|
||||
className="bg-blue-500 h-12 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => mutate()}
|
||||
>
|
||||
<Plus className="w-5 h-5 mr-1" /> Kategoriya qo‘shish
|
||||
Kategoriya import qilish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editing ? "Kategoriyani tahrirlash" : "Kategoriya qo‘shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddDistrict initialValues={editing} setDialogOpen={setDialogOpen} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import AddDistrict from "@/features/districts/ui/AddCategories";
|
||||
import type { CategoryItem } from "@/features/plans/lib/data";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -9,8 +16,8 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { Edit, Loader2, Trash } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { Image, Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
data: CategoryItem[] | [];
|
||||
@@ -19,6 +26,7 @@ interface Props {
|
||||
handleDelete: (user: CategoryItem) => void;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setEditingDistrict: Dispatch<SetStateAction<CategoryItem | null>>;
|
||||
dialogOpen: boolean;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
@@ -26,10 +34,10 @@ const TableDistrict = ({
|
||||
data,
|
||||
isError,
|
||||
isLoading,
|
||||
handleDelete,
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
setEditingDistrict,
|
||||
}: Props) => {
|
||||
const [initialValues, setEditing] = useState<CategoryItem>();
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading && (
|
||||
@@ -55,7 +63,8 @@ const TableDistrict = ({
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Rasmi</TableHead>
|
||||
<TableHead>Nomi (uz)</TableHead>
|
||||
<TableHead>Nomi (ru)</TableHead>
|
||||
<TableHead>Kategoriya idsi</TableHead>
|
||||
<TableHead>Turi</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -66,34 +75,35 @@ const TableDistrict = ({
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<img
|
||||
src={API_URLS.BASE_URL + d.image}
|
||||
alt={d.name_uz}
|
||||
src={d.image ? API_URLS.BASE_URL + d.image : "/logo.png"}
|
||||
alt={d.name}
|
||||
className="w-10 h-10 object-cover rounded-md"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{d.name_uz}</TableCell>
|
||||
<TableCell>{d.name_ru}</TableCell>
|
||||
<TableCell>{d.name}</TableCell>
|
||||
<TableCell>{d.category_id ? d.category_id : "----"}</TableCell>
|
||||
<TableCell>{d.type ? d.type : "----"}</TableCell>
|
||||
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditingDistrict(d);
|
||||
setEditing(d);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
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
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(d)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
</Button> */}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -108,6 +118,18 @@ const TableDistrict = ({
|
||||
</TableBody>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,4 +47,16 @@ export const plans_api = {
|
||||
const res = await httpClient.get(`${API_URLS.UnityList}`, { params });
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,30 +9,34 @@ export interface ProductsList {
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
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;
|
||||
id: number;
|
||||
images: {
|
||||
id: string;
|
||||
id: number;
|
||||
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 {
|
||||
@@ -46,11 +50,12 @@ export interface Category {
|
||||
}
|
||||
|
||||
export interface CategoryItem {
|
||||
id: string;
|
||||
name_uz: string;
|
||||
name_ru: string;
|
||||
image: string;
|
||||
id: number;
|
||||
name: string;
|
||||
image: string | null;
|
||||
order: number;
|
||||
category_id: null | number;
|
||||
type: null | string;
|
||||
}
|
||||
|
||||
export interface UnityList {
|
||||
|
||||
@@ -1,51 +1,40 @@
|
||||
"use client";
|
||||
|
||||
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() {
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Excel format tekshiruvi
|
||||
const allowedTypes = [
|
||||
"application/vnd.ms-excel",
|
||||
"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)
|
||||
};
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: () => plans_api.importProduct(),
|
||||
onSuccess: (res) => {
|
||||
toast.success(res.data.message, {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = (err.response?.data as { message: string }).message;
|
||||
toast.error(errMessage || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
onClick={() => mutate()}
|
||||
className="h-12 bg-blue-500 text-white hover:bg-blue-600"
|
||||
variant="secondary"
|
||||
>
|
||||
Excel yuklash
|
||||
Import qilish
|
||||
{isPending && <Loader2 className="animate-spin" />}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".xls,.xlsx"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { plans_api } from "@/features/plans/lib/api";
|
||||
import type { Product } from "@/features/plans/lib/data";
|
||||
import AddedPlan from "@/features/plans/ui/AddedPlan";
|
||||
import ExcelUpload from "@/features/plans/ui/ExcelUpload";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
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 { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
searchUser: string;
|
||||
@@ -22,33 +17,54 @@ interface Props {
|
||||
setEditingPlan: Dispatch<SetStateAction<Product | null>>;
|
||||
}
|
||||
|
||||
const FilterPlans = ({
|
||||
searchUser,
|
||||
setSearchUser,
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
setEditingPlan,
|
||||
editingPlan,
|
||||
}: Props) => {
|
||||
const FilterPlans = ({ searchUser, setSearchUser }: Props) => {
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: () => plans_api.exportProduct(),
|
||||
onSuccess: (response) => {
|
||||
const blob = new Blob([response.data], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
|
||||
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 (
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Foydalanuvchi ismi"
|
||||
placeholder="Mahsulot nomi"
|
||||
className="h-12"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
|
||||
<Button
|
||||
className="h-12 bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
|
||||
variant={"secondary"}
|
||||
onClick={() => mutate()}
|
||||
>
|
||||
Excel uchun shablon
|
||||
Excel yuklab olish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
|
||||
{/* <DialogContent>
|
||||
<DialogHeader className="text-xl font-medium">
|
||||
Excel uchun kerakli shablon
|
||||
</DialogHeader>
|
||||
@@ -60,10 +76,9 @@ const FilterPlans = ({
|
||||
xatolik yuz berishi mumkin.
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DialogContent> */}
|
||||
<ExcelUpload />
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
{/*<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -85,7 +100,7 @@ const FilterPlans = ({
|
||||
setDialogOpen={setDialogOpen}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Dialog>*/}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { plans_api } from "@/features/plans/lib/api";
|
||||
import type { Product } from "@/features/plans/lib/data";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -20,12 +11,8 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import clsx from "clsx";
|
||||
import { Edit, Eye, Loader2, Trash } from "lucide-react";
|
||||
import { Eye, Loader2 } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
products: Product[] | [];
|
||||
@@ -45,59 +32,7 @@ const ProductTable = ({
|
||||
isError,
|
||||
setEditingProduct,
|
||||
setDetailOpen,
|
||||
setDialogOpen,
|
||||
handleDelete,
|
||||
}: 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) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
@@ -121,43 +56,39 @@ const ProductTable = ({
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Rasmi</TableHead>
|
||||
<TableHead>Nomi (UZ)</TableHead>
|
||||
<TableHead>Nomi (RU)</TableHead>
|
||||
<TableHead>Tavsif (UZ)</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>
|
||||
<TableHead>Nomi</TableHead>
|
||||
<TableHead>Tavsif</TableHead>
|
||||
<TableHead className="text-end">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{products.map((product, index) => {
|
||||
const cat = data?.find((c) => c.id === product.category);
|
||||
const unity = unityData?.find((u) => u.id === product.unity);
|
||||
|
||||
return (
|
||||
<TableRow key={product.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
{product.images.length > 0 ? (
|
||||
<TableCell>
|
||||
<img
|
||||
src={API_URLS.BASE_URL + product.image}
|
||||
alt={product.name_uz}
|
||||
src={API_URLS.BASE_URL + product.images[0].image}
|
||||
alt={product.name}
|
||||
className="w-16 h-16 object-cover rounded"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{product.name_uz}</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
|
||||
) : (
|
||||
<TableCell>
|
||||
<img
|
||||
src={"/logo.png"}
|
||||
alt={product.name}
|
||||
className="w-10 h-10 object-cover rounded"
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>{product.name}</TableCell>
|
||||
<TableCell>
|
||||
{product.short_name && product.short_name.slice(0, 15)}...
|
||||
</TableCell>
|
||||
{/* <TableCell
|
||||
className={clsx(
|
||||
product.is_active ? "text-green-600" : "text-red-600",
|
||||
)}
|
||||
@@ -176,7 +107,7 @@ const ProductTable = ({
|
||||
<SelectItem value="false">Nofaol</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableCell> */}
|
||||
|
||||
<TableCell className="space-x-2 text-right">
|
||||
<Button
|
||||
@@ -190,7 +121,7 @@ const ProductTable = ({
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
{/*<Button
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-600"
|
||||
onClick={() => {
|
||||
@@ -207,7 +138,7 @@ const ProductTable = ({
|
||||
onClick={() => handleDelete(product)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</Button>*/}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -1,218 +1,225 @@
|
||||
import { categories_api } from "@/features/districts/lib/api";
|
||||
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";
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
import { ScrollArea } from "@/shared/ui/scroll-area";
|
||||
import type { Product } from "../lib/data";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
|
||||
interface Props {
|
||||
setDetail: Dispatch<SetStateAction<boolean>>;
|
||||
detail: boolean;
|
||||
plan: Product | null;
|
||||
interface ProductDetailModalProps {
|
||||
product: Product | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PlanDetail = ({ detail, setDetail, plan }: Props) => {
|
||||
const { data } = useQuery({
|
||||
queryKey: ["categories"],
|
||||
queryFn: () =>
|
||||
categories_api.list({
|
||||
page: 1,
|
||||
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;
|
||||
export function PlanDetail({
|
||||
product,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: ProductDetailModalProps) {
|
||||
if (!product) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={detail} onOpenChange={setDetail}>
|
||||
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
{plan.name_uz}
|
||||
</DialogTitle>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] p-0 overflow-hidden">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-6">
|
||||
<DialogHeader className="mb-6">
|
||||
<DialogTitle className="text-2xl">{product.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 mt-4">
|
||||
{/* Product Images */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
{plan.image && (
|
||||
<img
|
||||
src={API_URLS.BASE_URL + plan.image}
|
||||
alt={plan.name_uz}
|
||||
className="w-full h-48 object-cover rounded-lg border"
|
||||
/>
|
||||
)}
|
||||
{plan.images.map((img) => (
|
||||
<img
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Images Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-lg">Rasmlar</h3>
|
||||
{product.images && product.images.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{product.images.map((img) => (
|
||||
<div
|
||||
key={img.id}
|
||||
src={API_URLS.BASE_URL + img.image}
|
||||
alt={plan.name_uz}
|
||||
className="w-full h-48 object-cover rounded-lg border"
|
||||
className="relative w-full aspect-square bg-muted rounded-lg overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={
|
||||
API_URLS.BASE_URL + img.image || "/placeholder.svg"
|
||||
}
|
||||
alt={product.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Product Status */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge
|
||||
className={
|
||||
plan.is_active
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-red-100 text-red-700"
|
||||
}
|
||||
>
|
||||
{plan.is_active ? "Faol" : "Nofaol"}
|
||||
</Badge>
|
||||
{plan.quantity_left <= plan.min_quantity && (
|
||||
<Badge className="bg-orange-100 text-orange-700">Kam qoldi</Badge>
|
||||
) : (
|
||||
<div className="w-full aspect-square bg-muted rounded-lg flex items-center justify-center text-muted-foreground">
|
||||
Rasm yo'q
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Basic Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Nomi (O'zbekcha):</p>
|
||||
<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 className="space-y-3">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">
|
||||
Tavsifi (O'zbekcha):
|
||||
</p>
|
||||
<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 className="grid grid-cols-1 md:grid-cols-3 gap-4 bg-gray-50 p-4 rounded-lg">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Narxi:</p>
|
||||
<p className="text-xl font-bold text-blue-600">
|
||||
{plan.price.toLocaleString()} so'm
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Kategoriya:</p>
|
||||
<p className="text-gray-700">
|
||||
{data?.results.find((e) => e.id === plan.category)?.name_uz}
|
||||
</p>
|
||||
</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 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Kod:</p>
|
||||
<p className="text-gray-700">{plan.code}</p>
|
||||
<label className="text-sm text-muted-foreground">
|
||||
Mahsulot ID
|
||||
</label>
|
||||
<p className="font-mono text-sm">{product.product_id}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Artikul:</p>
|
||||
<p className="text-gray-700">{plan.article}</p>
|
||||
<label className="text-sm text-muted-foreground">Kod</label>
|
||||
<p className="font-mono text-sm">{product.code}</p>
|
||||
</div>
|
||||
{plan.brand && (
|
||||
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Brand:</p>
|
||||
<p className="text-gray-700">{plan.brand}</p>
|
||||
<label className="text-sm text-muted-foreground">
|
||||
Qisqa nom
|
||||
</label>
|
||||
<p className="text-sm">{product.short_name || "—"}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">
|
||||
Holati
|
||||
</label>
|
||||
<Badge
|
||||
variant={
|
||||
product.state === "active" ? "default" : "secondary"
|
||||
}
|
||||
className="mt-1"
|
||||
>
|
||||
{product.state}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{product.article_code && (
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">
|
||||
Artikul kodi
|
||||
</label>
|
||||
<p className="font-mono text-sm">{product.article_code}</p>
|
||||
</div>
|
||||
)}
|
||||
{plan.manufacturer && (
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">
|
||||
Ishlab chiqaruvchi:
|
||||
</p>
|
||||
<p className="text-gray-700">{plan.manufacturer}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Hajm:</p>
|
||||
<p className="text-gray-700">{plan.volume}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quantity Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-blue-50 p-4 rounded-lg">
|
||||
{/* Weight and Size Info */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6 pt-6 border-t">
|
||||
{product.weight_netto && (
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Qolgan miqdor:</p>
|
||||
<p className="text-lg font-semibold text-gray-700">
|
||||
{plan.quantity_left}
|
||||
</p>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
Xalol Vazni
|
||||
</label>
|
||||
<p className="font-semibold">{product.weight_netto} kg</p>
|
||||
</div>
|
||||
)}
|
||||
{product.weight_brutto && (
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Minimal miqdor:</p>
|
||||
<p className="text-lg font-semibold text-gray-700">
|
||||
{plan.min_quantity}
|
||||
</p>
|
||||
<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>
|
||||
|
||||
{/* Dates */}
|
||||
{(plan.return_date || plan.expires_date) && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{plan.return_date && (
|
||||
{/* Box Info */}
|
||||
{(product.box_type_code || product.box_quant) && (
|
||||
<div className="grid grid-cols-2 gap-4 mt-4 p-4 bg-muted rounded-lg">
|
||||
{product.box_type_code && (
|
||||
<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>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
Qutining turi
|
||||
</label>
|
||||
<p className="font-semibold">{product.box_type_code}</p>
|
||||
</div>
|
||||
)}
|
||||
{plan.expires_date && (
|
||||
{product.box_quant && (
|
||||
<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>
|
||||
<label className="text-xs text-muted-foreground">
|
||||
Qutida miqdori
|
||||
</label>
|
||||
<p className="font-semibold">{product.box_quant}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Groups */}
|
||||
{product.groups && product.groups.length > 0 && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanDetail;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { Product } from "@/features/plans/lib/data";
|
||||
import DeletePlan from "@/features/plans/ui/DeletePlan";
|
||||
import FilterPlans from "@/features/plans/ui/FilterPlans";
|
||||
import PalanTable from "@/features/plans/ui/PalanTable";
|
||||
import PlanDetail from "@/features/plans/ui/PlanDetail";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { PlanDetail } from "./PlanDetail";
|
||||
|
||||
const ProductList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -51,7 +51,11 @@ const ProductList = () => {
|
||||
setSearchUser={setSearchUser}
|
||||
/>
|
||||
|
||||
<PlanDetail detail={detail} setDetail={setDetail} plan={editingPlan} />
|
||||
<PlanDetail
|
||||
product={editingPlan}
|
||||
isOpen={detail}
|
||||
onClose={() => setDetail(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PalanTable
|
||||
|
||||
@@ -31,4 +31,15 @@ export const user_api = {
|
||||
const res = await httpClient.post(API_URLS.Import_User);
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface UserData {
|
||||
region: null | string;
|
||||
address: null | string;
|
||||
created_at: string;
|
||||
password_set: boolean;
|
||||
}
|
||||
|
||||
export interface UserListRes {
|
||||
|
||||
@@ -1,12 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import type { UserData } from "@/features/users/lib/data";
|
||||
import { Avatar, AvatarFallback } from "@/shared/ui/avatar";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/shared/ui/card";
|
||||
import { Calendar, MapPin } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
import {
|
||||
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({
|
||||
user,
|
||||
// setEditingUser,
|
||||
@@ -56,6 +84,52 @@ export function UserCard({
|
||||
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 (
|
||||
<Card className="group hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
@@ -109,33 +183,60 @@ export function UserCard({
|
||||
ID: {user.id}
|
||||
</div>
|
||||
|
||||
{/* Uncommnet qilish uchun tugmalar
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex-1 cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingUser(user);
|
||||
setDialogOpen?.(true);
|
||||
setOpen(true);
|
||||
setEdit(user.id);
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Tahrirlash
|
||||
{user.password_set ? "Parolni o'zgartirish" : "Parol qo'yish"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
setUserDelete(user);
|
||||
}}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader className="text-xl font-semibold">
|
||||
Parolni qo'yish
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-2"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -34,4 +34,11 @@ export const API_URLS = {
|
||||
QuestionnaireList: `${API_V}admin/questionnaire/list/`,
|
||||
Import_User: `${API_V}admin/user/import_users/`,
|
||||
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/`,
|
||||
};
|
||||
|
||||
56
src/shared/ui/scroll-area.tsx
Normal file
56
src/shared/ui/scroll-area.tsx
Normal 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 };
|
||||
@@ -11,7 +11,7 @@ export default defineConfig({
|
||||
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
port: 3002,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user