diff --git a/src/app/actions/data/mobileButtonsData.tsx b/src/app/actions/data/mobileButtonsData.tsx index b01b2bc..a09b95e 100644 --- a/src/app/actions/data/mobileButtonsData.tsx +++ b/src/app/actions/data/mobileButtonsData.tsx @@ -1,4 +1,9 @@ -import { IconColumns, IconFileBarcode, IconUsers } from "@tabler/icons-react"; +import { + IconBox, + IconColumns, + IconFileBarcode, + IconUsers, +} from "@tabler/icons-react"; import { ModuleNames } from "@/modules/modules"; import LinkData from "@/types/LinkData"; @@ -15,6 +20,12 @@ const mobileButtonsData: LinkData[] = [ href: "/services", moduleName: ModuleNames.FULFILLMENT_BASE, }, + { + icon: IconBox, + label: "Товары", + href: "/products", + moduleName: ModuleNames.FULFILLMENT_BASE, + }, { icon: IconFileBarcode, label: "Шаблоны штрихкодов", diff --git a/src/app/deals/components/desktop/DealsTable/DealsTable.tsx b/src/app/deals/components/shared/DealsTable/DealsTable.tsx similarity index 96% rename from src/app/deals/components/desktop/DealsTable/DealsTable.tsx rename to src/app/deals/components/shared/DealsTable/DealsTable.tsx index dcc91e3..70720e0 100644 --- a/src/app/deals/components/desktop/DealsTable/DealsTable.tsx +++ b/src/app/deals/components/shared/DealsTable/DealsTable.tsx @@ -1,7 +1,7 @@ import { FC, useCallback } from "react"; import { IconMoodSad } from "@tabler/icons-react"; import { Group, Pagination, Stack, Text } from "@mantine/core"; -import useDealsTableColumns from "@/app/deals/components/desktop/DealsTable/useDealsTableColumns"; +import useDealsTableColumns from "@/app/deals/components/shared/DealsTable/useDealsTableColumns"; import { useDealsContext } from "@/app/deals/contexts/DealsContext"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import BaseTable from "@/components/ui/BaseTable/BaseTable"; diff --git a/src/app/deals/components/desktop/DealsTable/useDealsTableColumns.tsx b/src/app/deals/components/shared/DealsTable/useDealsTableColumns.tsx similarity index 100% rename from src/app/deals/components/desktop/DealsTable/useDealsTableColumns.tsx rename to src/app/deals/components/shared/DealsTable/useDealsTableColumns.tsx diff --git a/src/app/deals/components/shared/views/TableView.tsx b/src/app/deals/components/shared/views/TableView.tsx index 51279d8..8ae908e 100644 --- a/src/app/deals/components/shared/views/TableView.tsx +++ b/src/app/deals/components/shared/views/TableView.tsx @@ -1,3 +1,3 @@ -import DealsTable from "@/app/deals/components/desktop/DealsTable/DealsTable"; +import DealsTable from "../DealsTable/DealsTable"; export const TableView = () => ; diff --git a/src/app/products/components/desktop/ProductsDesktopHeader/ProductsDesktopHeader.tsx b/src/app/products/components/desktop/ProductsDesktopHeader/ProductsDesktopHeader.tsx new file mode 100644 index 0000000..5b4291c --- /dev/null +++ b/src/app/products/components/desktop/ProductsDesktopHeader/ProductsDesktopHeader.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { Flex, Group, TextInput } from "@mantine/core"; +import { useProductsContext } from "@/app/products/contexts/ProductsContext"; +import useProductsActions from "@/app/products/hooks/useProductsActions"; +import InlineButton from "@/components/ui/InlineButton/InlineButton"; +import ClientSelect from "@/modules/dealModularEditorTabs/Clients/components/ClientSelect"; + +const ProductsDesktopHeader = () => { + const { productsFiltersForm } = useProductsContext(); + const { onCreateClick } = useProductsActions(); + + return ( + + + + + Создать + + + + + + + ); +}; + +export default ProductsDesktopHeader; diff --git a/src/app/products/components/mobile/ProductsMobileHeader/ProductsMobileHeader.tsx b/src/app/products/components/mobile/ProductsMobileHeader/ProductsMobileHeader.tsx new file mode 100644 index 0000000..1299b18 --- /dev/null +++ b/src/app/products/components/mobile/ProductsMobileHeader/ProductsMobileHeader.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Flex, Stack, TextInput } from "@mantine/core"; +import { useProductsContext } from "@/app/products/contexts/ProductsContext"; +import useProductsActions from "@/app/products/hooks/useProductsActions"; +import InlineButton from "@/components/ui/InlineButton/InlineButton"; +import ClientSelect from "@/modules/dealModularEditorTabs/Clients/components/ClientSelect"; + +const ProductsMobileHeader = () => { + const { productsFiltersForm } = useProductsContext(); + const { onCreateClick } = useProductsActions(); + + return ( + + + + + + + Создать + + + ); +}; + +export default ProductsMobileHeader; diff --git a/src/app/products/components/shared/BarcodeTemplateSelect/BarcodeTemplateSelect.tsx b/src/app/products/components/shared/BarcodeTemplateSelect/BarcodeTemplateSelect.tsx new file mode 100644 index 0000000..9b0092e --- /dev/null +++ b/src/app/products/components/shared/BarcodeTemplateSelect/BarcodeTemplateSelect.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { FC } from "react"; +import useBarcodeTemplatesList from "@/app/barcode-templates/hooks/useBarcodeTemplatesList"; +import ObjectSelect, { + ObjectSelectProps, +} from "@/components/selects/ObjectSelect/ObjectSelect"; +import { BarcodeTemplateSchema } from "@/lib/client"; + +type Props = Omit, "data">; + +const BarcodeTemplateSelect: FC = props => { + const { barcodeTemplates } = useBarcodeTemplatesList(); + + return ( + + ); +}; + +export default BarcodeTemplateSelect; diff --git a/src/app/products/components/shared/PageBody/PageBody.tsx b/src/app/products/components/shared/PageBody/PageBody.tsx new file mode 100644 index 0000000..e888b5e --- /dev/null +++ b/src/app/products/components/shared/PageBody/PageBody.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Stack } from "@mantine/core"; +import ProductsDesktopHeader from "@/app/products/components/desktop/ProductsDesktopHeader/ProductsDesktopHeader"; +import ProductsMobileHeader from "@/app/products/components/mobile/ProductsMobileHeader/ProductsMobileHeader"; +import PageBlock from "@/components/layout/PageBlock/PageBlock"; +import useIsMobile from "@/hooks/utils/useIsMobile"; +import ProductsTable from "@/app/products/components/shared/ProductsTable/ProductsTable"; + +const PageBody = () => { + const isMobile = useIsMobile(); + + return ( + + {!isMobile && ( + + + + )} + + + {isMobile && } +
+ +
+
+
+
+ ); +}; + +export default PageBody; diff --git a/src/app/products/components/shared/ProductsTable/ProductsTable.tsx b/src/app/products/components/shared/ProductsTable/ProductsTable.tsx new file mode 100644 index 0000000..a22c90a --- /dev/null +++ b/src/app/products/components/shared/ProductsTable/ProductsTable.tsx @@ -0,0 +1,60 @@ +import { IconMoodSad } from "@tabler/icons-react"; +import { Group, Pagination, Stack, Text } from "@mantine/core"; +import { useProductsTableColumns } from "@/app/products/components/shared/ProductsTable/columns"; +import { useProductsContext } from "@/app/products/contexts/ProductsContext"; +import useProductsActions from "@/app/products/hooks/useProductsActions"; +import BaseTable from "@/components/ui/BaseTable/BaseTable"; +import useIsMobile from "@/hooks/utils/useIsMobile"; + +const ProductsTable = () => { + const isMobile = useIsMobile(); + const { productsCrud, products, productsFiltersForm, paginationInfo } = + useProductsContext(); + const { onChangeClick } = useProductsActions(); + + const columns = useProductsTableColumns({ + onChange: onChangeClick, + onDelete: productsCrud.onDelete, + }); + + return ( + + + Нет товаров + + + ) : ( + Выберите клиента для вывода товаров + ) + } + verticalSpacing={"md"} + /> + {paginationInfo && paginationInfo.totalPages > 1 && ( + + + + )} + + ); +}; + +export default ProductsTable; diff --git a/src/app/products/components/shared/ProductsTable/columns.tsx b/src/app/products/components/shared/ProductsTable/columns.tsx new file mode 100644 index 0000000..b8d2290 --- /dev/null +++ b/src/app/products/components/shared/ProductsTable/columns.tsx @@ -0,0 +1,106 @@ +import { useMemo } from "react"; +import { IconEdit, IconTrash } from "@tabler/icons-react"; +import { DataTableColumn } from "mantine-datatable"; +import { Center, Flex, List, Spoiler, useMantineTheme } from "@mantine/core"; +import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip"; +import useIsMobile from "@/hooks/utils/useIsMobile"; +import { ProductSchema } from "@/lib/client"; + +type Props = { + onChange: (product: ProductSchema) => void; + onDelete: (product: ProductSchema) => void; +}; + +export const useProductsTableColumns = ({ onChange, onDelete }: Props) => { + const theme = useMantineTheme(); + const isMobile = useIsMobile(); + + return useMemo( + () => + [ + { + accessor: "actions", + title:
Действия
, + width: "0%", + render: product => ( + + {/* onPrintBarcodeClick(product)}>*/} + {/* */} + {/**/} + onChange(product)}> + + + onDelete(product)}> + + + + ), + }, + { + accessor: "article", + title: "Артикул", + }, + { + accessor: "factoryArticle", + title: "Складской артикул", + }, + { + accessor: "name", + title: "Название", + }, + { + accessor: "barcodes", + title: "Штрихкоды", + render: product => { + return ( + + + {product.barcodes.map(barcode => ( + + {barcode} + + ))} + + + ); + }, + }, + { + accessor: "barcodeTemplate.name", + title: "Шаблон ШК", + }, + { + accessor: "brand", + title: "Бренд", + }, + { + accessor: "composition", + title: "Состав", + }, + { + accessor: "color", + title: "Цвет", + }, + { + accessor: "size", + title: "Размер", + }, + { + accessor: "additionalInfo", + title: "Доп. информация", + }, + ] as DataTableColumn[], + [] + ); +}; diff --git a/src/app/products/contexts/ProductsContext.tsx b/src/app/products/contexts/ProductsContext.tsx new file mode 100644 index 0000000..815b888 --- /dev/null +++ b/src/app/products/contexts/ProductsContext.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useForm, UseFormReturnType } from "@mantine/form"; +import { useDebouncedValue } from "@mantine/hooks"; +import { ClientSchema, PaginationInfoSchema, ProductSchema } from "@/lib/client"; +import makeContext from "@/lib/contextFactory/contextFactory"; +import { + ProductsCrud, + useProductsCrud, +} from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/cruds/useProductsCrud"; +import useProductsList from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useProductsList"; + +export type ProductsFiltersForm = { + searchInput: string; + client: ClientSchema | null; + page: number; +}; + +type ProductsContextState = { + productsFiltersForm: UseFormReturnType; + products: ProductSchema[]; + productsCrud: ProductsCrud; + paginationInfo?: PaginationInfoSchema; +}; + +const useProductsContextState = (): ProductsContextState => { + const productsFiltersForm = useForm({ + initialValues: { + searchInput: "", + client: null, + page: 1, + }, + }); + + const [debouncedSearchInput] = useDebouncedValue( + productsFiltersForm.values.searchInput, + 500 + ); + + const { products, paginationInfo, queryKey } = useProductsList({ + clientId: productsFiltersForm.values.client?.id, + searchInput: debouncedSearchInput, + page: productsFiltersForm.values.page, + itemsPerPage: 10, + }); + const productsCrud = useProductsCrud({ queryKey }); + + return { + productsFiltersForm, + products, + productsCrud, + paginationInfo, + }; +}; + +export const [ProductsContextProvider, useProductsContext] = + makeContext(useProductsContextState, "Products"); diff --git a/src/app/products/hooks/useProductsActions.ts b/src/app/products/hooks/useProductsActions.ts new file mode 100644 index 0000000..4a7b0d6 --- /dev/null +++ b/src/app/products/hooks/useProductsActions.ts @@ -0,0 +1,59 @@ +import { modals } from "@mantine/modals"; +import { useProductsContext } from "@/app/products/contexts/ProductsContext"; +import { ProductSchema } from "@/lib/client"; +import { notifications } from "@/lib/notifications"; + +const useProductsActions = () => { + const { productsCrud, productsFiltersForm } = useProductsContext(); + + const onCreateClick = () => { + if (!productsFiltersForm.values.client) { + notifications.error({ message: "Выберите клиента" }); + return; + } + + modals.openContextModal({ + modal: "productEditorModal", + title: "Создание товара", + withCloseButton: false, + innerProps: { + onCreate: productsCrud.onCreate, + clientId: productsFiltersForm.values.client.id, + isEditing: false, + }, + }); + }; + + const onChangeClick = (product: ProductSchema) => { + modals.openContextModal({ + modal: "productEditorModal", + title: "Редактирование товара", + withCloseButton: false, + innerProps: { + onChange: updated => productsCrud.onUpdate(product.id, updated), + clientId: product.clientId, + entity: product, + isEditing: true, + }, + }); + }; + + const onPrintBarcodeClick = (product: ProductSchema) => { + modals.openContextModal({ + modal: "printBarcode", + title: "Печать штрихкода", // TODO + withCloseButton: true, + innerProps: { + productId: product.id, + }, + }); + }; + + return { + onCreateClick, + onChangeClick, + onPrintBarcodeClick, + }; +}; + +export default useProductsActions; diff --git a/src/app/products/hooks/useProductsList.tsx b/src/app/products/hooks/useProductsList.tsx new file mode 100644 index 0000000..7ce21a1 --- /dev/null +++ b/src/app/products/hooks/useProductsList.tsx @@ -0,0 +1,20 @@ +import { useQuery } from "@tanstack/react-query"; +import { ProductService } from "../../../client"; + +type Props = { + clientId: number; + page?: number; + itemsPerPage?: number; + searchInput: string; +}; +const useProductsList = (props: Props) => { + const { clientId, page, itemsPerPage, searchInput } = props; + const { data, refetch, isLoading } = useQuery({ + queryKey: ["getAllServices", clientId, page, itemsPerPage, searchInput], + queryFn: () => ProductService.getProductsByClientId(props), + }); + const products = !data ? [] : data.products; + const paginationInfo = data?.paginationInfo; + return { products, paginationInfo, refetch, isLoading }; +}; +export default useProductsList; diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx new file mode 100644 index 0000000..0b38ed9 --- /dev/null +++ b/src/app/products/page.tsx @@ -0,0 +1,22 @@ +import { Suspense } from "react"; +import { Center, Loader } from "@mantine/core"; +import PageBody from "@/app/products/components/shared/PageBody/PageBody"; +import { ProductsContextProvider } from "@/app/products/contexts/ProductsContext"; +import PageContainer from "@/components/layout/PageContainer/PageContainer"; + +export default async function ProductsPage() { + return ( + + + + }> + + + + + + + ); +} diff --git a/src/components/layout/Navbar/data/linksData.ts b/src/components/layout/Navbar/data/linksData.ts index fa03ef2..a51c1f8 100644 --- a/src/components/layout/Navbar/data/linksData.ts +++ b/src/components/layout/Navbar/data/linksData.ts @@ -1,4 +1,5 @@ import { + IconBox, IconColumns, IconFileBarcode, IconLayoutKanban, @@ -26,6 +27,12 @@ const linksData: LinkData[] = [ href: "/services", moduleName: ModuleNames.FULFILLMENT_BASE, }, + { + icon: IconBox, + label: "Товары", + href: "/products", + moduleName: ModuleNames.FULFILLMENT_BASE, + }, { icon: IconFileBarcode, label: "Шаблоны штрихкодов", diff --git a/src/lib/client/types.gen.ts b/src/lib/client/types.gen.ts index c0ee4c3..b083c7e 100644 --- a/src/lib/client/types.gen.ts +++ b/src/lib/client/types.gen.ts @@ -487,6 +487,14 @@ export type CreateProductSchema = { * Factoryarticle */ factoryArticle: string; + /** + * Clientid + */ + clientId: number; + /** + * Barcodetemplateid + */ + barcodeTemplateId: number; /** * Brand */ @@ -507,6 +515,10 @@ export type CreateProductSchema = { * Additionalinfo */ additionalInfo: string | null; + /** + * Barcodes + */ + barcodes: Array; }; /** @@ -1098,6 +1110,7 @@ export type GetProductsResponse = { * Items */ items: Array; + paginationInfo: PaginationInfoSchema; }; /** @@ -1218,6 +1231,14 @@ export type ProductSchema = { * Factoryarticle */ factoryArticle: string; + /** + * Clientid + */ + clientId: number; + /** + * Barcodetemplateid + */ + barcodeTemplateId: number; /** * Brand */ @@ -1238,10 +1259,15 @@ export type ProductSchema = { * Additionalinfo */ additionalInfo: string | null; + /** + * Barcodes + */ + barcodes: Array; /** * Id */ id: number; + barcodeTemplate: BarcodeTemplateSchema; }; /** @@ -1704,6 +1730,10 @@ export type UpdateProductSchema = { * Factoryarticle */ factoryArticle?: string | null; + /** + * Barcodetemplateid + */ + barcodeTemplateId?: number | null; /** * Brand */ @@ -1724,6 +1754,10 @@ export type UpdateProductSchema = { * Additionalinfo */ additionalInfo?: string | null; + /** + * Barcodes + */ + barcodes?: Array | null; /** * Images */ @@ -3218,6 +3252,10 @@ export type GetProductsData = { body?: never; path?: never; query?: { + /** + * Clientid + */ + clientId?: number | null; /** * Searchinput */ diff --git a/src/lib/client/zod.gen.ts b/src/lib/client/zod.gen.ts index 01adaa2..e7dfe7b 100644 --- a/src/lib/client/zod.gen.ts +++ b/src/lib/client/zod.gen.ts @@ -208,12 +208,16 @@ export const zProductSchema = z.object({ name: z.string(), article: z.string(), factoryArticle: z.string(), + clientId: z.int(), + barcodeTemplateId: z.int(), brand: z.union([z.string(), z.null()]), color: z.union([z.string(), z.null()]), composition: z.union([z.string(), z.null()]), size: z.union([z.string(), z.null()]), additionalInfo: z.union([z.string(), z.null()]), + barcodes: z.array(z.string()), id: z.int(), + barcodeTemplate: zBarcodeTemplateSchema, }); /** @@ -377,11 +381,14 @@ export const zCreateProductSchema = z.object({ name: z.string(), article: z.string(), factoryArticle: z.string(), + clientId: z.int(), + barcodeTemplateId: z.int(), brand: z.union([z.string(), z.null()]), color: z.union([z.string(), z.null()]), composition: z.union([z.string(), z.null()]), size: z.union([z.string(), z.null()]), additionalInfo: z.union([z.string(), z.null()]), + barcodes: z.array(z.string()), }); /** @@ -765,6 +772,7 @@ export const zGetDealsResponse = z.object({ */ export const zGetProductsResponse = z.object({ items: z.array(zProductSchema), + paginationInfo: zPaginationInfoSchema, }); /** @@ -1014,11 +1022,13 @@ export const zUpdateProductSchema = z.object({ name: z.optional(z.union([z.string(), z.null()])), article: z.optional(z.union([z.string(), z.null()])), factoryArticle: z.optional(z.union([z.string(), z.null()])), + barcodeTemplateId: z.optional(z.union([z.int(), z.null()])), brand: z.optional(z.union([z.string(), z.null()])), color: z.optional(z.union([z.string(), z.null()])), composition: z.optional(z.union([z.string(), z.null()])), size: z.optional(z.union([z.string(), z.null()])), additionalInfo: z.optional(z.union([z.string(), z.null()])), + barcodes: z.optional(z.union([z.array(z.string()), z.null()])), images: z.optional(z.union([z.array(zProductImageSchema), z.null()])), }); @@ -1718,6 +1728,7 @@ export const zGetProductsData = z.object({ path: z.optional(z.never()), query: z.optional( z.object({ + clientId: z.optional(z.union([z.int(), z.null()])), searchInput: z.optional(z.union([z.string(), z.null()])), page: z.optional(z.union([z.int(), z.null()])), itemsPerPage: z.optional(z.union([z.int(), z.null()])), diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealInfoView/components/ProductsActions/ProductsActions.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealInfoView/components/ProductsActions/ProductsActions.tsx index 3d0b753..9dd25eb 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealInfoView/components/ProductsActions/ProductsActions.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealInfoView/components/ProductsActions/ProductsActions.tsx @@ -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, }, }); }; diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/AddDealProductButton/AddDealProductButton.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/AddDealProductButton/AddDealProductButton.tsx index ebe4f23..d2b9892 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/AddDealProductButton/AddDealProductButton.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/AddDealProductButton/AddDealProductButton.tsx @@ -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 => { 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 => { dealProductsCrud.onCreate({ ...values, dealId: deal.id }), productIdsToExclude, isEditing: false, - clientId: 0, // TODO add clients + clientId: deal.client.id, }, }); }; diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/DealProductView/DealProductView.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/DealProductView/DealProductView.tsx index 6f2eb05..c740962 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/DealProductView/DealProductView.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/mobile/components/DealProductView/DealProductView.tsx @@ -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 = ({ 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 = ({ dealProduct }) => { ), entity: dealProduct, isEditing: true, - clientId: 0, // TODO add clients + clientId: deal.client.id, }, }); }; diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/components/ProductSelect/ProductSelect.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/components/ProductSelect/ProductSelect.tsx index f951d37..d367505 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/components/ProductSelect/ProductSelect.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/components/ProductSelect/ProductSelect.tsx @@ -21,7 +21,7 @@ const ProductSelect: FC = (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, diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext.tsx index 47cc095..6b2a193 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext.tsx @@ -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 }); diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useProductsList.ts b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useProductsList.ts index 2a5a7c4..6fb2d1a 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useProductsList.ts +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/hooks/lists/useProductsList.ts @@ -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, diff --git a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/ProductEditorModal.tsx b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/ProductEditorModal.tsx index 07736a4..0cdf4d9 100644 --- a/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/ProductEditorModal.tsx +++ b/src/modules/dealModularEditorTabs/FulfillmentBase/shared/modals/ProductEditorModal/ProductEditorModal.tsx @@ -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) => { const isEditing = "entity" in innerProps; - const initialValues: Partial = 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>({ @@ -50,60 +63,83 @@ const ProductEditorModal = ({ }, }); - const onClose = () => context.closeContextModal(id); - return ( + onClose={() => context.closeContextModal(id)}>
- - - + + + + + { + form.setFieldValue("barcodeTemplate", template); + form.setFieldValue( + "barcodeTemplateId", + template?.id + ); + }} + /> + +
- - - - - + + + + + + +
{isEditing && (