feat: modules creation

This commit is contained in:
2025-10-25 18:05:49 +04:00
parent 2bdbebc453
commit 5b754865cf
12 changed files with 287 additions and 15 deletions

View File

@ -2,10 +2,14 @@
import { FC } from "react"; import { FC } from "react";
import { Group } from "@mantine/core"; import { Group } from "@mantine/core";
import { useModulesContext } from "@/app/modules/contexts/ModulesContext";
import InlineButton from "@/components/ui/InlineButton/InlineButton"; import InlineButton from "@/components/ui/InlineButton/InlineButton";
import useIsMobile from "@/hooks/utils/useIsMobile"; import useIsMobile from "@/hooks/utils/useIsMobile";
const ModulesHeader: FC = () => { const ModulesHeader: FC = () => {
const {
modulesActions: { onCreate },
} = useModulesContext();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
return ( return (
@ -14,7 +18,9 @@ const ModulesHeader: FC = () => {
justify={"space-between"} justify={"space-between"}
mt={isMobile ? "xs" : ""} mt={isMobile ? "xs" : ""}
mx={isMobile ? "xs" : ""}> mx={isMobile ? "xs" : ""}>
<InlineButton w={isMobile ? "100%" : ""}> <InlineButton
onClick={onCreate}
w={isMobile ? "100%" : ""}>
Создать модуль Создать модуль
</InlineButton> </InlineButton>
</Group> </Group>

View File

@ -3,18 +3,22 @@
import useModulesWithAttrsList from "@/app/modules/hooks/useModulesWithAttrsList"; import useModulesWithAttrsList from "@/app/modules/hooks/useModulesWithAttrsList";
import { ModuleWithAttributesSchema } from "@/lib/client"; import { ModuleWithAttributesSchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory"; import makeContext from "@/lib/contextFactory/contextFactory";
import useModulesActions, { ModulesActions } from "@/app/modules/hooks/useModulesActions";
type ModulesContextState = { type ModulesContextState = {
modules: ModuleWithAttributesSchema[]; modules: ModuleWithAttributesSchema[];
refetchModules: () => void; refetchModules: () => void;
modulesActions: ModulesActions;
}; };
const useModulesContextState = (): ModulesContextState => { const useModulesContextState = (): ModulesContextState => {
const { modules, refetch } = useModulesWithAttrsList(); const { modules, refetch } = useModulesWithAttrsList();
const modulesActions = useModulesActions({ refetchModules: refetch });
return { return {
modules, modules,
refetchModules: refetch, refetchModules: refetch,
modulesActions,
}; };
}; };

View File

@ -5,10 +5,14 @@ import { AxiosError } from "axios";
import { Text } from "@mantine/core"; import { Text } from "@mantine/core";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { HttpValidationError, ModuleSchemaOutput } from "@/lib/client"; import { HttpValidationError, ModuleSchemaOutput } from "@/lib/client";
import { deleteModuleMutation } from "@/lib/client/@tanstack/react-query.gen"; import {
createModuleMutation,
deleteModuleMutation,
} from "@/lib/client/@tanstack/react-query.gen";
import { notifications } from "@/lib/notifications"; import { notifications } from "@/lib/notifications";
export type ModulesActions = { export type ModulesActions = {
onCreate: () => void;
onUpdate: (module: ModuleSchemaOutput) => void; onUpdate: (module: ModuleSchemaOutput) => void;
onDelete: (module: ModuleSchemaOutput) => void; onDelete: (module: ModuleSchemaOutput) => void;
}; };
@ -18,10 +22,6 @@ type Props = {
}; };
const useModulesActions = ({ refetchModules }: Props): ModulesActions => { const useModulesActions = ({ refetchModules }: Props): ModulesActions => {
const onUpdate = (module: ModuleSchemaOutput) => {
redirect(`/module-editor/${module.id}`);
};
const onError = (error: AxiosError<HttpValidationError>, _: any) => { const onError = (error: AxiosError<HttpValidationError>, _: any) => {
console.error(error); console.error(error);
notifications.error({ notifications.error({
@ -29,6 +29,34 @@ const useModulesActions = ({ refetchModules }: Props): ModulesActions => {
}); });
}; };
const createMutation = useMutation({
...createModuleMutation(),
onError,
});
const onCreate = () => {
modals.openContextModal({
modal: "moduleCreatorModal",
title: "Создание модуля",
innerProps: {
onCreate: (data, onSuccess) =>
createMutation.mutate(
{ body: { entity: data } },
{
onSuccess: () => {
refetchModules();
onSuccess && onSuccess();
},
}
),
},
});
};
const onUpdate = (module: ModuleSchemaOutput) => {
redirect(`/module-editor/${module.id}`);
};
const deleteMutation = useMutation({ const deleteMutation = useMutation({
...deleteModuleMutation(), ...deleteModuleMutation(),
onError, onError,
@ -37,7 +65,7 @@ const useModulesActions = ({ refetchModules }: Props): ModulesActions => {
const onDelete = (module: ModuleSchemaOutput) => { const onDelete = (module: ModuleSchemaOutput) => {
modals.openConfirmModal({ modals.openConfirmModal({
title: "Удаление услуги из сделки", title: "Удаление модуля",
children: ( children: (
<Text> <Text>
Вы уверены, что хотите удалить модуль "{module.label}"? Вы уверены, что хотите удалить модуль "{module.label}"?
@ -49,6 +77,7 @@ const useModulesActions = ({ refetchModules }: Props): ModulesActions => {
}; };
return { return {
onCreate,
onUpdate, onUpdate,
onDelete, onDelete,
}; };

View File

@ -8,7 +8,6 @@ import {
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable"; import { DataTableColumn } from "mantine-datatable";
import { Box, Group, Text, Tooltip } from "@mantine/core"; import { Box, Group, Text, Tooltip } from "@mantine/core";
import useModulesActions from "@/app/modules/hooks/useModulesTableActions";
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions"; import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
import useIsMobile from "@/hooks/utils/useIsMobile"; import useIsMobile from "@/hooks/utils/useIsMobile";
import { ModuleWithAttributesSchema } from "@/lib/client"; import { ModuleWithAttributesSchema } from "@/lib/client";
@ -24,8 +23,10 @@ const useModulesTableColumns = ({
setExpandedModuleIds, setExpandedModuleIds,
}: Props) => { }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { modules, refetchModules } = useModulesContext(); const {
const { onUpdate, onDelete } = useModulesActions({ refetchModules }); modules,
modulesActions: { onUpdate, onDelete },
} = useModulesContext();
const onExpandAllClick = () => { const onExpandAllClick = () => {
if (expandedModuleIds.length !== modules.length) { if (expandedModuleIds.length !== modules.length) {

View File

@ -0,0 +1,54 @@
"use client";
import { Button, Stack, Textarea, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import { CreateModuleSchema } from "@/lib/client";
type Props = {
onCreate: (data: CreateModuleSchema, onSuccess?: () => void) => void;
};
const ModuleCreatorModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const form = useForm<CreateModuleSchema>({
initialValues: {
label: "",
description: "",
},
validate: {
label: label => !label?.trim() && "Название не заполнено",
},
});
const close = () => context.closeContextModal(id);
return (
<form
onSubmit={form.onSubmit(values =>
innerProps.onCreate(values, close)
)}>
<Stack gap={"md"}>
<TextInput
withAsterisk
label={"Название"}
{...form.getInputProps("label")}
/>
<Textarea
label={"Описание"}
{...form.getInputProps("description")}
/>
<Button
type={"submit"}
variant={"default"}>
Сохранить
</Button>
</Stack>
</form>
);
};
export default ModuleCreatorModal;

View File

@ -71,7 +71,7 @@ export const useProductsTableColumns = ({
} }
showLabel={"Показать все"} showLabel={"Показать все"}
hideLabel={"Скрыть"}> hideLabel={"Скрыть"}>
{product.barcodes.map(barcode => ( {product.barcodes?.map(barcode => (
<List.Item key={barcode}> <List.Item key={barcode}>
{barcode} {barcode}
</List.Item> </List.Item>

View File

@ -24,6 +24,7 @@ import {
createDealService, createDealService,
createDealTag, createDealTag,
createMarketplace, createMarketplace,
createModule,
createProduct, createProduct,
createProject, createProject,
createService, createService,
@ -144,6 +145,9 @@ import type {
CreateMarketplaceData, CreateMarketplaceData,
CreateMarketplaceError, CreateMarketplaceError,
CreateMarketplaceResponse2, CreateMarketplaceResponse2,
CreateModuleData,
CreateModuleError,
CreateModuleResponse2,
CreateProductData, CreateProductData,
CreateProductError, CreateProductError,
CreateProductResponse2, CreateProductResponse2,
@ -1295,6 +1299,54 @@ export const getModulesOptions = (options?: Options<GetModulesData>) => {
}); });
}; };
export const createModuleQueryKey = (options: Options<CreateModuleData>) =>
createQueryKey("createModule", options);
/**
* Create Module
*/
export const createModuleOptions = (options: Options<CreateModuleData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await createModule({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: createModuleQueryKey(options),
});
};
/**
* Create Module
*/
export const createModuleMutation = (
options?: Partial<Options<CreateModuleData>>
): UseMutationOptions<
CreateModuleResponse2,
AxiosError<CreateModuleError>,
Options<CreateModuleData>
> => {
const mutationOptions: UseMutationOptions<
CreateModuleResponse2,
AxiosError<CreateModuleError>,
Options<CreateModuleData>
> = {
mutationFn: async localOptions => {
const { data } = await createModule({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
export const getModulesWithAttributesQueryKey = ( export const getModulesWithAttributesQueryKey = (
options?: Options<GetModulesWithAttributesData> options?: Options<GetModulesWithAttributesData>
) => createQueryKey("getModulesWithAttributes", options); ) => createQueryKey("getModulesWithAttributes", options);

View File

@ -50,6 +50,9 @@ import type {
CreateMarketplaceData, CreateMarketplaceData,
CreateMarketplaceErrors, CreateMarketplaceErrors,
CreateMarketplaceResponses, CreateMarketplaceResponses,
CreateModuleData,
CreateModuleErrors,
CreateModuleResponses,
CreateProductData, CreateProductData,
CreateProductErrors, CreateProductErrors,
CreateProductResponses, CreateProductResponses,
@ -292,6 +295,8 @@ import {
zCreateDealTagResponse2, zCreateDealTagResponse2,
zCreateMarketplaceData, zCreateMarketplaceData,
zCreateMarketplaceResponse2, zCreateMarketplaceResponse2,
zCreateModuleData,
zCreateModuleResponse2,
zCreateProductData, zCreateProductData,
zCreateProductResponse2, zCreateProductResponse2,
zCreateProjectData, zCreateProjectData,
@ -1088,6 +1093,33 @@ export const getModules = <ThrowOnError extends boolean = false>(
}); });
}; };
/**
* Create Module
*/
export const createModule = <ThrowOnError extends boolean = false>(
options: Options<CreateModuleData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
CreateModuleResponses,
CreateModuleErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zCreateModuleData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zCreateModuleResponse2.parseAsync(data);
},
url: "/crm/v1/module/",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/** /**
* Get Modules With Attributes * Get Modules With Attributes
*/ */

View File

@ -660,6 +660,37 @@ export type CreateMarketplaceSchema = {
}; };
}; };
/**
* CreateModuleRequest
*/
export type CreateModuleRequest = {
entity: CreateModuleSchema;
};
/**
* CreateModuleResponse
*/
export type CreateModuleResponse = {
/**
* Message
*/
message: string;
};
/**
* CreateModuleSchema
*/
export type CreateModuleSchema = {
/**
* Label
*/
label: string;
/**
* Description
*/
description: string | null;
};
/** /**
* CreateProductRequest * CreateProductRequest
*/ */
@ -1823,7 +1854,7 @@ export type ModuleTabSchema = {
/** /**
* Iconname * Iconname
*/ */
iconName: string; iconName: string | null;
/** /**
* Device * Device
*/ */
@ -3715,6 +3746,32 @@ export type GetModulesResponses = {
export type GetModulesResponse = GetModulesResponses[keyof GetModulesResponses]; export type GetModulesResponse = GetModulesResponses[keyof GetModulesResponses];
export type CreateModuleData = {
body: CreateModuleRequest;
path?: never;
query?: never;
url: "/crm/v1/module/";
};
export type CreateModuleErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CreateModuleError = CreateModuleErrors[keyof CreateModuleErrors];
export type CreateModuleResponses = {
/**
* Successful Response
*/
200: CreateModuleResponse;
};
export type CreateModuleResponse2 =
CreateModuleResponses[keyof CreateModuleResponses];
export type GetModulesWithAttributesData = { export type GetModulesWithAttributesData = {
body?: never; body?: never;
path?: never; path?: never;

View File

@ -559,6 +559,28 @@ export const zCreateMarketplaceResponse = z.object({
entity: zMarketplaceSchema, entity: zMarketplaceSchema,
}); });
/**
* CreateModuleSchema
*/
export const zCreateModuleSchema = z.object({
label: z.string(),
description: z.union([z.string(), z.null()]),
});
/**
* CreateModuleRequest
*/
export const zCreateModuleRequest = z.object({
entity: zCreateModuleSchema,
});
/**
* CreateModuleResponse
*/
export const zCreateModuleResponse = z.object({
message: z.string(),
});
/** /**
* CreateProductSchema * CreateProductSchema
*/ */
@ -637,7 +659,7 @@ export const zModuleTabSchema = z.object({
id: z.int(), id: z.int(),
key: z.string(), key: z.string(),
label: z.string(), label: z.string(),
iconName: z.string(), iconName: z.union([z.string(), z.null()]),
device: z.string(), device: z.string(),
}); });
@ -2058,6 +2080,17 @@ export const zGetModulesData = z.object({
*/ */
export const zGetModulesResponse = zGetAllModulesResponse; export const zGetModulesResponse = zGetAllModulesResponse;
export const zCreateModuleData = z.object({
body: zCreateModuleRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zCreateModuleResponse2 = zCreateModuleResponse;
export const zGetModulesWithAttributesData = z.object({ export const zGetModulesWithAttributesData = z.object({
body: z.optional(z.never()), body: z.optional(z.never()),
path: z.optional(z.never()), path: z.optional(z.never()),

View File

@ -6,6 +6,7 @@ import DealsBoardFiltersModal from "@/app/deals/modals/DealsBoardFiltersModal/De
import DealsScheduleFiltersModal from "@/app/deals/modals/DealsScheduleFiltersModal/DealsScheduleFiltersModal"; import DealsScheduleFiltersModal from "@/app/deals/modals/DealsScheduleFiltersModal/DealsScheduleFiltersModal";
import DealsTableFiltersModal from "@/app/deals/modals/DealsTableFiltersModal/DealsTableFiltersModal"; import DealsTableFiltersModal from "@/app/deals/modals/DealsTableFiltersModal/DealsTableFiltersModal";
import AttributeEditorModal from "@/app/module-editor/[moduleId]/modals/AttributeEditorModal"; import AttributeEditorModal from "@/app/module-editor/[moduleId]/modals/AttributeEditorModal";
import ModuleCreatorModal from "@/app/modules/modals/ModuleCreatorModal";
import { import {
ServiceCategoryEditorModal, ServiceCategoryEditorModal,
ServiceEditorModal, ServiceEditorModal,
@ -44,4 +45,5 @@ export const modals = {
marketplaceEditorModal: MarketplaceEditorModal, marketplaceEditorModal: MarketplaceEditorModal,
dealTagModal: DealTagModal, dealTagModal: DealTagModal,
attributeEditorModal: AttributeEditorModal, attributeEditorModal: AttributeEditorModal,
moduleCreatorModal: ModuleCreatorModal,
}; };

View File

@ -8,7 +8,7 @@ type ModuleTab = {
id: number; id: number;
key: string; key: string;
label: string; label: string;
iconName: string; iconName?: string;
moduleId: number; moduleId: number;
}; };
@ -16,6 +16,7 @@ type Module = {
id: number; id: number;
label: string; label: string;
description: string; description: string;
isBuiltIn: boolean;
tabs: ModuleTab[]; tabs: ModuleTab[];
}; };
@ -77,6 +78,7 @@ const generateRows = (modules: Module[]) => {
const iconsToImport = new Set<string>(); const iconsToImport = new Set<string>();
for (const module of modules) { for (const module of modules) {
for (const tab of module.tabs) { for (const tab of module.tabs) {
if (!tab.iconName) continue;
iconsToImport.add(tab.iconName); iconsToImport.add(tab.iconName);
} }
} }
@ -98,7 +100,7 @@ const modulesFileGen = () => {
axios axios
.get(ENDPOINT) .get(ENDPOINT)
.then((response: AxiosResponse<ModulesResponse>) => { .then((response: AxiosResponse<ModulesResponse>) => {
generateRows(response.data.items); generateRows(response.data.items.filter(item => item.isBuiltIn));
}) })
.catch(err => console.log(err)); .catch(err => console.log(err));
}; };