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

View File

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

View File

@ -5,10 +5,14 @@ import { AxiosError } from "axios";
import { Text } from "@mantine/core";
import { modals } from "@mantine/modals";
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";
export type ModulesActions = {
onCreate: () => void;
onUpdate: (module: ModuleSchemaOutput) => void;
onDelete: (module: ModuleSchemaOutput) => void;
};
@ -18,10 +22,6 @@ type Props = {
};
const useModulesActions = ({ refetchModules }: Props): ModulesActions => {
const onUpdate = (module: ModuleSchemaOutput) => {
redirect(`/module-editor/${module.id}`);
};
const onError = (error: AxiosError<HttpValidationError>, _: any) => {
console.error(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({
...deleteModuleMutation(),
onError,
@ -37,7 +65,7 @@ const useModulesActions = ({ refetchModules }: Props): ModulesActions => {
const onDelete = (module: ModuleSchemaOutput) => {
modals.openConfirmModal({
title: "Удаление услуги из сделки",
title: "Удаление модуля",
children: (
<Text>
Вы уверены, что хотите удалить модуль "{module.label}"?
@ -49,6 +77,7 @@ const useModulesActions = ({ refetchModules }: Props): ModulesActions => {
};
return {
onCreate,
onUpdate,
onDelete,
};

View File

@ -8,7 +8,6 @@ import {
} from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable";
import { Box, Group, Text, Tooltip } from "@mantine/core";
import useModulesActions from "@/app/modules/hooks/useModulesTableActions";
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ModuleWithAttributesSchema } from "@/lib/client";
@ -24,8 +23,10 @@ const useModulesTableColumns = ({
setExpandedModuleIds,
}: Props) => {
const isMobile = useIsMobile();
const { modules, refetchModules } = useModulesContext();
const { onUpdate, onDelete } = useModulesActions({ refetchModules });
const {
modules,
modulesActions: { onUpdate, onDelete },
} = useModulesContext();
const onExpandAllClick = () => {
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={"Показать все"}
hideLabel={"Скрыть"}>
{product.barcodes.map(barcode => (
{product.barcodes?.map(barcode => (
<List.Item key={barcode}>
{barcode}
</List.Item>

View File

@ -24,6 +24,7 @@ import {
createDealService,
createDealTag,
createMarketplace,
createModule,
createProduct,
createProject,
createService,
@ -144,6 +145,9 @@ import type {
CreateMarketplaceData,
CreateMarketplaceError,
CreateMarketplaceResponse2,
CreateModuleData,
CreateModuleError,
CreateModuleResponse2,
CreateProductData,
CreateProductError,
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 = (
options?: Options<GetModulesWithAttributesData>
) => createQueryKey("getModulesWithAttributes", options);

View File

@ -50,6 +50,9 @@ import type {
CreateMarketplaceData,
CreateMarketplaceErrors,
CreateMarketplaceResponses,
CreateModuleData,
CreateModuleErrors,
CreateModuleResponses,
CreateProductData,
CreateProductErrors,
CreateProductResponses,
@ -292,6 +295,8 @@ import {
zCreateDealTagResponse2,
zCreateMarketplaceData,
zCreateMarketplaceResponse2,
zCreateModuleData,
zCreateModuleResponse2,
zCreateProductData,
zCreateProductResponse2,
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
*/

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
*/
@ -1823,7 +1854,7 @@ export type ModuleTabSchema = {
/**
* Iconname
*/
iconName: string;
iconName: string | null;
/**
* Device
*/
@ -3715,6 +3746,32 @@ export type 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 = {
body?: never;
path?: never;

View File

@ -559,6 +559,28 @@ export const zCreateMarketplaceResponse = z.object({
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
*/
@ -637,7 +659,7 @@ export const zModuleTabSchema = z.object({
id: z.int(),
key: z.string(),
label: z.string(),
iconName: z.string(),
iconName: z.union([z.string(), z.null()]),
device: z.string(),
});
@ -2058,6 +2080,17 @@ export const zGetModulesData = z.object({
*/
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({
body: 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 DealsTableFiltersModal from "@/app/deals/modals/DealsTableFiltersModal/DealsTableFiltersModal";
import AttributeEditorModal from "@/app/module-editor/[moduleId]/modals/AttributeEditorModal";
import ModuleCreatorModal from "@/app/modules/modals/ModuleCreatorModal";
import {
ServiceCategoryEditorModal,
ServiceEditorModal,
@ -44,4 +45,5 @@ export const modals = {
marketplaceEditorModal: MarketplaceEditorModal,
dealTagModal: DealTagModal,
attributeEditorModal: AttributeEditorModal,
moduleCreatorModal: ModuleCreatorModal,
};

View File

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