feat: products page
This commit is contained in:
@ -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: "Шаблоны штрихкодов",
|
||||
|
||||
@ -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";
|
||||
@ -1,3 +1,3 @@
|
||||
import DealsTable from "@/app/deals/components/desktop/DealsTable/DealsTable";
|
||||
import DealsTable from "../DealsTable/DealsTable";
|
||||
|
||||
export const TableView = () => <DealsTable />;
|
||||
|
||||
@ -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 (
|
||||
<Group justify={"space-between"}>
|
||||
<Flex gap={"xs"}>
|
||||
<ClientSelect
|
||||
{...productsFiltersForm.getInputProps("client")}
|
||||
/>
|
||||
<InlineButton
|
||||
onClick={onCreateClick}
|
||||
disabled={!productsFiltersForm.values.client}>
|
||||
Создать
|
||||
</InlineButton>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<TextInput
|
||||
placeholder={"Артикул, название, шк"}
|
||||
{...productsFiltersForm.getInputProps("searchInput")}
|
||||
/>
|
||||
</Flex>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductsDesktopHeader;
|
||||
@ -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 (
|
||||
<Stack
|
||||
gap={"xs"}
|
||||
justify={"space-between"}
|
||||
mt={"xs"}
|
||||
mx={"xs"}>
|
||||
<Flex
|
||||
gap={"xs"}
|
||||
flex={2}>
|
||||
<ClientSelect
|
||||
{...productsFiltersForm.getInputProps("client")}
|
||||
flex={1}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Артикул, название, шк"}
|
||||
{...productsFiltersForm.getInputProps("searchInput")}
|
||||
flex={1}
|
||||
/>
|
||||
</Flex>
|
||||
<InlineButton
|
||||
onClick={onCreateClick}
|
||||
disabled={!productsFiltersForm.values.client}>
|
||||
Создать
|
||||
</InlineButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductsMobileHeader;
|
||||
@ -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<ObjectSelectProps<BarcodeTemplateSchema>, "data">;
|
||||
|
||||
const BarcodeTemplateSelect: FC<Props> = props => {
|
||||
const { barcodeTemplates } = useBarcodeTemplatesList();
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={barcodeTemplates}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarcodeTemplateSelect;
|
||||
36
src/app/products/components/shared/PageBody/PageBody.tsx
Normal file
36
src/app/products/components/shared/PageBody/PageBody.tsx
Normal file
@ -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 (
|
||||
<Stack h={"100%"}>
|
||||
{!isMobile && (
|
||||
<PageBlock>
|
||||
<ProductsDesktopHeader />
|
||||
</PageBlock>
|
||||
)}
|
||||
<PageBlock
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
fullScreenMobile>
|
||||
<Stack
|
||||
gap={"xs"}
|
||||
h={"100%"}>
|
||||
{isMobile && <ProductsMobileHeader />}
|
||||
<div style={{ flex: 1, overflow: "auto" }}>
|
||||
<ProductsTable />
|
||||
</div>
|
||||
</Stack>
|
||||
</PageBlock>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageBody;
|
||||
@ -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 (
|
||||
<Stack
|
||||
h={"100%"}
|
||||
pb={"xs"}
|
||||
px={isMobile ? "xs" : ""}>
|
||||
<BaseTable
|
||||
withTableBorder
|
||||
records={products}
|
||||
columns={columns}
|
||||
groups={undefined}
|
||||
style={{
|
||||
height: "100%",
|
||||
}}
|
||||
emptyState={
|
||||
productsFiltersForm.values.client ? (
|
||||
<Group
|
||||
align={"center"}
|
||||
gap={"xs"}>
|
||||
<Text>Нет товаров</Text>
|
||||
<IconMoodSad />
|
||||
</Group>
|
||||
) : (
|
||||
<Text>Выберите клиента для вывода товаров</Text>
|
||||
)
|
||||
}
|
||||
verticalSpacing={"md"}
|
||||
/>
|
||||
{paginationInfo && paginationInfo.totalPages > 1 && (
|
||||
<Group justify={"end"}>
|
||||
<Pagination
|
||||
withEdges
|
||||
total={paginationInfo?.totalPages}
|
||||
{...productsFiltersForm.getInputProps("page")}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductsTable;
|
||||
106
src/app/products/components/shared/ProductsTable/columns.tsx
Normal file
106
src/app/products/components/shared/ProductsTable/columns.tsx
Normal file
@ -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: <Center>Действия</Center>,
|
||||
width: "0%",
|
||||
render: product => (
|
||||
<Flex gap={isMobile ? "sm" : "md"}>
|
||||
{/*<ActionIconWithTip*/}
|
||||
{/* tipLabel={"Печать штрихкода"}*/}
|
||||
{/* onClick={() => onPrintBarcodeClick(product)}>*/}
|
||||
{/* <IconBarcode />*/}
|
||||
{/*</ActionIconWithTip>*/}
|
||||
<ActionIconWithTip
|
||||
tipLabel={"Редактировать"}
|
||||
onClick={() => onChange(product)}>
|
||||
<IconEdit />
|
||||
</ActionIconWithTip>
|
||||
<ActionIconWithTip
|
||||
tipLabel={"Удалить"}
|
||||
onClick={() => onDelete(product)}>
|
||||
<IconTrash />
|
||||
</ActionIconWithTip>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "article",
|
||||
title: "Артикул",
|
||||
},
|
||||
{
|
||||
accessor: "factoryArticle",
|
||||
title: "Складской артикул",
|
||||
},
|
||||
{
|
||||
accessor: "name",
|
||||
title: "Название",
|
||||
},
|
||||
{
|
||||
accessor: "barcodes",
|
||||
title: "Штрихкоды",
|
||||
render: product => {
|
||||
return (
|
||||
<List size={"sm"}>
|
||||
<Spoiler
|
||||
maxHeight={
|
||||
parseFloat(theme.lineHeights.sm) * 30
|
||||
}
|
||||
showLabel={"Показать все"}
|
||||
hideLabel={"Скрыть"}>
|
||||
{product.barcodes.map(barcode => (
|
||||
<List.Item key={barcode}>
|
||||
{barcode}
|
||||
</List.Item>
|
||||
))}
|
||||
</Spoiler>
|
||||
</List>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: "barcodeTemplate.name",
|
||||
title: "Шаблон ШК",
|
||||
},
|
||||
{
|
||||
accessor: "brand",
|
||||
title: "Бренд",
|
||||
},
|
||||
{
|
||||
accessor: "composition",
|
||||
title: "Состав",
|
||||
},
|
||||
{
|
||||
accessor: "color",
|
||||
title: "Цвет",
|
||||
},
|
||||
{
|
||||
accessor: "size",
|
||||
title: "Размер",
|
||||
},
|
||||
{
|
||||
accessor: "additionalInfo",
|
||||
title: "Доп. информация",
|
||||
},
|
||||
] as DataTableColumn<ProductSchema>[],
|
||||
[]
|
||||
);
|
||||
};
|
||||
57
src/app/products/contexts/ProductsContext.tsx
Normal file
57
src/app/products/contexts/ProductsContext.tsx
Normal file
@ -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<ProductsFiltersForm>;
|
||||
products: ProductSchema[];
|
||||
productsCrud: ProductsCrud;
|
||||
paginationInfo?: PaginationInfoSchema;
|
||||
};
|
||||
|
||||
const useProductsContextState = (): ProductsContextState => {
|
||||
const productsFiltersForm = useForm<ProductsFiltersForm>({
|
||||
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<ProductsContextState>(useProductsContextState, "Products");
|
||||
59
src/app/products/hooks/useProductsActions.ts
Normal file
59
src/app/products/hooks/useProductsActions.ts
Normal file
@ -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;
|
||||
20
src/app/products/hooks/useProductsList.tsx
Normal file
20
src/app/products/hooks/useProductsList.tsx
Normal file
@ -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;
|
||||
22
src/app/products/page.tsx
Normal file
22
src/app/products/page.tsx
Normal file
@ -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 (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Center h="50vh">
|
||||
<Loader size="lg" />
|
||||
</Center>
|
||||
}>
|
||||
<PageContainer>
|
||||
<ProductsContextProvider>
|
||||
<PageBody />
|
||||
</ProductsContextProvider>
|
||||
</PageContainer>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user