feat: products page

This commit is contained in:
2025-10-08 22:32:16 +04:00
parent 820d9b4d33
commit 8af4fcce2f
25 changed files with 664 additions and 58 deletions

View File

@ -2,6 +2,7 @@ import { FC } from "react";
import { Button, Flex } from "@mantine/core";
import { modals } from "@mantine/modals";
import { useFulfillmentBaseContext } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext";
import { notifications } from "@/lib/notifications";
const ProductsActions: FC = () => {
const { deal, dealProductsList, productsCrud, dealProductsCrud } =
@ -20,6 +21,11 @@ const ProductsActions: FC = () => {
};
const onCreateDealProductClick = () => {
if (!deal.client) {
notifications.error({ message: "Выберите клиента для сделки" });
return;
}
const productIdsToExclude = dealProductsList.dealProducts.map(
product => product.product.id
);
@ -33,7 +39,7 @@ const ProductsActions: FC = () => {
dealProductsCrud.onCreate({ ...values, dealId: deal.id }),
productIdsToExclude,
isEditing: false,
clientId: 0, // TODO add clients
clientId: deal.client.id,
},
});
};

View File

@ -3,6 +3,7 @@ import { IconPlus } from "@tabler/icons-react";
import { ButtonProps, Text } from "@mantine/core";
import { modals } from "@mantine/modals";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
import { notifications } from "@/lib/notifications";
import { useFulfillmentBaseContext } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext";
type Props = ButtonProps;
@ -12,6 +13,11 @@ const AddDealProductButton: FC<Props> = props => {
useFulfillmentBaseContext();
const onCreateClick = () => {
if (!deal.client) {
notifications.error({ message: "Выберите клиента для сделки" });
return;
}
const productIdsToExclude = dealProductsList.dealProducts.map(
product => product.product.id
);
@ -25,7 +31,7 @@ const AddDealProductButton: FC<Props> = props => {
dealProductsCrud.onCreate({ ...values, dealId: deal.id }),
productIdsToExclude,
isEditing: false,
clientId: 0, // TODO add clients
clientId: deal.client.id,
},
});
};

View File

@ -11,6 +11,7 @@ import {
} from "@mantine/core";
import { modals } from "@mantine/modals";
import { DealProductSchema } from "@/lib/client";
import { notifications } from "@/lib/notifications";
import ProductFieldsList from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/ProductView/components/ProductFieldsList";
import ProductMenu from "@/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/ProductMenu/ProductMenu";
import ProductServicesTable from "@/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/ProductServicesTable/ProductServicesTable";
@ -22,9 +23,14 @@ type Props = {
};
const DealProductView: FC<Props> = ({ dealProduct }) => {
const { dealProductsCrud } = useFulfillmentBaseContext();
const { dealProductsCrud, deal } = useFulfillmentBaseContext();
const onChangeDealProductClick = () => {
if (!deal.client) {
notifications.error({ message: "Выберите клиента для сделки" });
return;
}
modals.openContextModal({
modal: "dealProductEditorModal",
title: "Добавление товара",
@ -38,7 +44,7 @@ const DealProductView: FC<Props> = ({ dealProduct }) => {
),
entity: dealProduct,
isEditing: true,
clientId: 0, // TODO add clients
clientId: deal.client.id,
},
});
};

View File

@ -21,7 +21,7 @@ const ProductSelect: FC<Props> = (props: Props) => {
const [searchValue, setSearchValue] = useState("");
const [debounced] = useDebouncedValue(searchValue, 500);
const { products, isLoading } = useProductsList({
// clientId: props.clientId,
clientId: props.clientId,
searchInput: debounced,
page: 0,
itemsPerPage: MAX_PRODUCTS,

View File

@ -40,7 +40,9 @@ type Props = {
const useFulfillmentBaseContextState = ({
deal,
}: Props): FulfillmentBaseContextState => {
const productQueryKey = getProductsQueryKey();
const productQueryKey = getProductsQueryKey({
query: { clientId: deal.client?.id },
});
const productsCrud = useProductsCrud({ queryKey: productQueryKey });
const dealProductsList = useDealProductsList({ dealId: deal.id });

View File

@ -1,22 +1,40 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { ProductSchema } from "@/lib/client";
import { PaginationInfoSchema, ProductSchema } from "@/lib/client";
import {
getProductsOptions,
getProductsQueryKey,
} from "@/lib/client/@tanstack/react-query.gen";
type Props = {
clientId?: number;
searchInput: string;
page?: number;
itemsPerPage?: number;
};
const useProductsList = ({ searchInput, page, itemsPerPage }: Props) => {
type ReturnType = {
products: ProductSchema[];
paginationInfo?: PaginationInfoSchema;
setProducts: (products: ProductSchema[]) => void;
refetch: () => void;
queryKey: any[];
isLoading: boolean;
};
const useProductsList = ({
clientId,
searchInput,
page,
itemsPerPage,
}: Props): ReturnType => {
const queryClient = useQueryClient();
const options = {
query: { searchInput, page, itemsPerPage },
query: { clientId, searchInput, page, itemsPerPage },
};
const { data, refetch, isLoading } = useQuery(getProductsOptions(options));
const { data, refetch, isLoading } = useQuery({
...getProductsOptions(options),
enabled: !!clientId,
});
const queryKey = getProductsQueryKey(options);
@ -32,6 +50,7 @@ const useProductsList = ({ searchInput, page, itemsPerPage }: Props) => {
return {
products: data?.items ?? [],
paginationInfo: data?.paginationInfo,
setProducts,
refetch,
queryKey,

View File

@ -1,9 +1,11 @@
"use client";
import { Fieldset, Flex, TextInput } from "@mantine/core";
import { Fieldset, Flex, Stack, TagsInput, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import BarcodeTemplateSelect from "@/app/products/components/shared/BarcodeTemplateSelect/BarcodeTemplateSelect";
import {
BarcodeTemplateSchema,
CreateProductSchema,
ProductSchema,
UpdateProductSchema,
@ -18,6 +20,14 @@ type Props = CreateEditFormProps<
CreateProductSchema,
UpdateProductSchema,
ProductSchema
> & {
clientId: number;
};
type ProductForm = Partial<
ProductSchema & {
barcodeTemplate: BarcodeTemplateSchema;
}
>;
const ProductEditorModal = ({
@ -27,7 +37,7 @@ const ProductEditorModal = ({
}: ContextModalProps<Props>) => {
const isEditing = "entity" in innerProps;
const initialValues: Partial<ProductSchema> = isEditing
const initialValues: ProductForm = isEditing
? innerProps.entity!
: {
name: "",
@ -38,6 +48,9 @@ const ProductEditorModal = ({
color: "",
size: "",
additionalInfo: "",
clientId: innerProps.clientId,
barcodeTemplate: undefined,
barcodeTemplateId: undefined,
};
const form = useForm<Partial<ProductSchema>>({
@ -50,60 +63,83 @@ const ProductEditorModal = ({
},
});
const onClose = () => context.closeContextModal(id);
return (
<BaseFormModal
{...innerProps}
form={form}
closeOnSubmit
onClose={onClose}>
onClose={() => context.closeContextModal(id)}>
<Flex
gap={"xs"}
direction={"column"}>
<Fieldset legend={"Основные характеристики"}>
<TextInput
placeholder={"Введите название товара"}
label={"Название товара"}
{...form.getInputProps("name")}
/>
<TextInput
placeholder={"Введите артикул"}
label={"Артикул"}
{...form.getInputProps("article")}
/>
<TextInput
placeholder={"Введите складской артикул"}
label={"Складской артикул"}
{...form.getInputProps("factoryArticle")}
/>
<Stack gap={"xs"}>
<TextInput
placeholder={"Введите название товара"}
label={"Название товара"}
{...form.getInputProps("name")}
/>
<TextInput
placeholder={"Введите артикул"}
label={"Артикул"}
{...form.getInputProps("article")}
/>
<TextInput
placeholder={"Введите складской артикул"}
label={"Складской артикул"}
{...form.getInputProps("factoryArticle")}
/>
<BarcodeTemplateSelect
placeholder={"Выберите шаблон штрихкода"}
label={"Шаблон штрихкода"}
{...form.getInputProps("barcodeTemplate")}
onChange={template => {
form.setFieldValue("barcodeTemplate", template);
form.setFieldValue(
"barcodeTemplateId",
template?.id
);
}}
/>
<TagsInput
placeholder={
!form.values.barcodes?.length
? "Добавьте штрихкоды к товару"
: ""
}
label={"Штрихкоды"}
{...form.getInputProps("barcodes")}
/>
</Stack>
</Fieldset>
<Fieldset legend={"Дополнительные характеристики"}>
<TextInput
placeholder={"Введите бренд"}
label={ренд"}
{...form.getInputProps("brand")}
/>
<TextInput
placeholder={"Введите состав"}
label={"Состав"}
{...form.getInputProps("composition")}
/>
<TextInput
placeholder={"Введите цвет"}
label={вет"}
{...form.getInputProps("color")}
/>
<TextInput
placeholder={"Введите размер"}
label={"Размер"}
{...form.getInputProps("size")}
/>
<TextInput
placeholder={"Введите доп. информацию"}
label={оп. информация"}
{...form.getInputProps("additionalInfo")}
/>
<Stack gap={"xs"}>
<TextInput
placeholder={"Введите бренд"}
label={"Бренд"}
{...form.getInputProps("brand")}
/>
<TextInput
placeholder={"Введите состав"}
label={"Состав"}
{...form.getInputProps("composition")}
/>
<TextInput
placeholder={"Введите цвет"}
label={"Цвет"}
{...form.getInputProps("color")}
/>
<TextInput
placeholder={"Введите размер"}
label={"Размер"}
{...form.getInputProps("size")}
/>
<TextInput
placeholder={"Введите доп. информацию"}
label={"Доп. информация"}
{...form.getInputProps("additionalInfo")}
/>
</Stack>
</Fieldset>
{isEditing && (
<ProductImageDropzone