product page update
This commit is contained in:
@@ -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
33
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 qo‘shish
|
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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/`,
|
||||||
};
|
};
|
||||||
|
|||||||
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") }],
|
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3002,
|
||||||
open: true,
|
open: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user