feat: modules, products, services, services kits

This commit is contained in:
2025-09-16 10:56:10 +04:00
parent f2746b8b65
commit 553e76d610
92 changed files with 8404 additions and 103 deletions

View File

@ -7,7 +7,8 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client" "generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client",
"generate-modules": "sudo npx tsc ./src/modules/modulesFileGen/modulesFileGen.ts && mv -f ./src/modules/modulesFileGen/modulesFileGen.js ./src/modules/modulesFileGen/modulesFileGen.cjs && sudo node ./src/modules/modulesFileGen/modulesFileGen.cjs"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
@ -15,6 +16,7 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@mantine/core": "8.1.2", "@mantine/core": "8.1.2",
"@mantine/dates": "^8.2.7", "@mantine/dates": "^8.2.7",
"@mantine/dropzone": "^8.3.1",
"@mantine/form": "^8.1.3", "@mantine/form": "^8.1.3",
"@mantine/hooks": "8.1.2", "@mantine/hooks": "8.1.2",
"@mantine/modals": "^8.2.1", "@mantine/modals": "^8.2.1",
@ -31,6 +33,7 @@
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"dayjs": "^1.11.15", "dayjs": "^1.11.15",
"framer-motion": "^12.23.7", "framer-motion": "^12.23.7",
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.14.0", "i18n-iso-countries": "^7.14.0",
"lexorank": "^1.0.5", "lexorank": "^1.0.5",
"libphonenumber-js": "^1.12.10", "libphonenumber-js": "^1.12.10",

View File

@ -3,11 +3,13 @@ import { IconMoodSad } from "@tabler/icons-react";
import { Group, Pagination, Stack, Text } from "@mantine/core"; import { Group, Pagination, Stack, Text } from "@mantine/core";
import useDealsTableColumns from "@/app/deals/components/desktop/DealsTable/useDealsTableColumns"; import useDealsTableColumns from "@/app/deals/components/desktop/DealsTable/useDealsTableColumns";
import { useDealsContext } from "@/app/deals/contexts/DealsContext"; import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import BaseTable from "@/components/ui/BaseTable/BaseTable"; import BaseTable from "@/components/ui/BaseTable/BaseTable";
import { useDrawersContext } from "@/drawers/DrawersContext"; import { useDrawersContext } from "@/drawers/DrawersContext";
import { DealSchema } from "@/lib/client"; import { DealSchema } from "@/lib/client";
const DealsTable: FC = () => { const DealsTable: FC = () => {
const { selectedProject } = useProjectsContext();
const { deals, paginationInfo, page, setPage, sortingForm, dealsCrud } = const { deals, paginationInfo, page, setPage, sortingForm, dealsCrud } =
useDealsContext(); useDealsContext();
const { openDrawer } = useDrawersContext(); const { openDrawer } = useDrawersContext();
@ -20,6 +22,7 @@ const DealsTable: FC = () => {
value: deal, value: deal,
onChange: deal => dealsCrud.onUpdate(deal.id, deal), onChange: deal => dealsCrud.onUpdate(deal.id, deal),
onDelete: dealsCrud.onDelete, onDelete: dealsCrud.onDelete,
project: selectedProject,
}, },
}); });
}, },

View File

@ -35,7 +35,7 @@ const TopToolPanel: FC<Props> = ({ view, setView }) => {
title: "Создание проекта", title: "Создание проекта",
withCloseButton: true, withCloseButton: true,
innerProps: { innerProps: {
onChange: values => projectsCrud.onCreate(values.name), onChange: projectsCrud.onCreate,
}, },
}); });
}; };

View File

@ -11,7 +11,7 @@ const CreateBoardButton = () => {
<Flex style={{ borderBottom: "2px solid gray" }}> <Flex style={{ borderBottom: "2px solid gray" }}>
<InPlaceInput <InPlaceInput
placeholder={"Название доски"} placeholder={"Название доски"}
onChange={boardsCrud.onCreate} onChange={name => boardsCrud.onCreate({ name })}
getChildren={startEditing => ( getChildren={startEditing => (
<Box <Box
onClick={startEditing} onClick={startEditing}

View File

@ -11,7 +11,7 @@ const CreateCardButton = () => {
const { dealsCrud } = useDealsContext(); const { dealsCrud } = useDealsContext();
const onSubmit = (values: CreateDealForm) => { const onSubmit = (values: CreateDealForm) => {
dealsCrud.onCreate(values.name); dealsCrud.onCreate(values);
setIsCreating(prevState => !prevState); setIsCreating(prevState => !prevState);
setIsTransitionEnded(false); setIsTransitionEnded(false);
}; };

View File

@ -17,7 +17,7 @@ const CreateStatusButton = () => {
className={styles["inner-container"]}> className={styles["inner-container"]}>
<InPlaceInput <InPlaceInput
placeholder={"Название колонки"} placeholder={"Название колонки"}
onChange={statusesCrud.onCreate} onChange={name => statusesCrud.onCreate({ name })}
getChildren={startEditing => ( getChildren={startEditing => (
<Center <Center
p={"sm"} p={"sm"}

View File

@ -1,5 +1,6 @@
import { Box, Card, Group, Pill, Stack, Text } from "@mantine/core"; import { Box, Card, Group, Pill, Stack, Text } from "@mantine/core";
import { useDealsContext } from "@/app/deals/contexts/DealsContext"; import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import { useDrawersContext } from "@/drawers/DrawersContext"; import { useDrawersContext } from "@/drawers/DrawersContext";
import { DealSchema } from "@/lib/client"; import { DealSchema } from "@/lib/client";
import styles from "./DealCard.module.css"; import styles from "./DealCard.module.css";
@ -9,6 +10,7 @@ type Props = {
}; };
const DealCard = ({ deal }: Props) => { const DealCard = ({ deal }: Props) => {
const { selectedProject } = useProjectsContext();
const { dealsCrud } = useDealsContext(); const { dealsCrud } = useDealsContext();
const { openDrawer } = useDrawersContext(); const { openDrawer } = useDrawersContext();
@ -19,6 +21,7 @@ const DealCard = ({ deal }: Props) => {
value: deal, value: deal,
onChange: deal => dealsCrud.onUpdate(deal.id, deal), onChange: deal => dealsCrud.onUpdate(deal.id, deal),
onDelete: dealsCrud.onDelete, onDelete: dealsCrud.onDelete,
project: selectedProject,
}, },
}); });
}; };

View File

@ -2,9 +2,10 @@ import { FC } from "react";
import { IconPlus } from "@tabler/icons-react"; import { IconPlus } from "@tabler/icons-react";
import { Box, Group, Text } from "@mantine/core"; import { Box, Group, Text } from "@mantine/core";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { CreateBoardSchema } from "@/lib/client";
type Props = { type Props = {
onCreateBoard: (name: string) => void; onCreateBoard: (data: Partial<CreateBoardSchema>) => void;
}; };
const CreateBoardButton: FC<Props> = ({ onCreateBoard }) => { const CreateBoardButton: FC<Props> = ({ onCreateBoard }) => {
@ -14,7 +15,7 @@ const CreateBoardButton: FC<Props> = ({ onCreateBoard }) => {
title: "Создание доски", title: "Создание доски",
withCloseButton: true, withCloseButton: true,
innerProps: { innerProps: {
onChange: values => onCreateBoard(values.name), onChange: values => onCreateBoard(values),
}, },
}); });
}; };

View File

@ -6,12 +6,13 @@ import DealEditorBody from "@/app/deals/drawers/DealEditorDrawer/components/Deal
import Header from "@/app/deals/drawers/DealEditorDrawer/components/Header"; import Header from "@/app/deals/drawers/DealEditorDrawer/components/Header";
import { DrawerProps } from "@/drawers/types"; import { DrawerProps } from "@/drawers/types";
import useIsMobile from "@/hooks/utils/useIsMobile"; import useIsMobile from "@/hooks/utils/useIsMobile";
import { DealSchema } from "@/lib/client"; import { DealSchema, ProjectSchema } from "@/lib/client";
type Props = { type Props = {
value: DealSchema; value: DealSchema;
onChange: (deal: DealSchema) => void; onChange: (deal: DealSchema) => void;
onDelete: (deal: DealSchema, onSuccess: () => void) => void; onDelete: (deal: DealSchema, onSuccess: () => void) => void;
project: ProjectSchema | null;
}; };
const DealEditorDrawer: FC<DrawerProps<Props>> = ({ const DealEditorDrawer: FC<DrawerProps<Props>> = ({
@ -24,7 +25,7 @@ const DealEditorDrawer: FC<DrawerProps<Props>> = ({
return ( return (
<Drawer <Drawer
size={isMobile ? "100%" : "40%"} size={isMobile ? "100%" : "60%"}
position={"right"} position={"right"}
onClose={onClose} onClose={onClose}
removeScrollProps={{ allowPinchZoom: true }} removeScrollProps={{ allowPinchZoom: true }}
@ -49,6 +50,7 @@ const DealEditorDrawer: FC<DrawerProps<Props>> = ({
props.onChange(deal); props.onChange(deal);
}} }}
onDelete={value => props.onDelete(value, onClose)} onDelete={value => props.onDelete(value, onClose)}
project={props.project}
/> />
</Drawer> </Drawer>
); );

View File

@ -1,17 +1,55 @@
import React, { FC } from "react"; import React, { FC, ReactNode } from "react";
import { IconCircleDotted, IconEdit } from "@tabler/icons-react"; import { IconEdit } from "@tabler/icons-react";
import { Tabs, Text } from "@mantine/core"; import { motion } from "framer-motion";
import { Box, Tabs } from "@mantine/core";
import GeneralTab from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/GeneralTab"; import GeneralTab from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/GeneralTab";
import { DealSchema } from "@/lib/client"; import { DealSchema, ProjectSchema } from "@/lib/client";
import { MODULES } from "@/modules/modules";
import styles from "../DealEditorDrawer.module.css"; import styles from "../DealEditorDrawer.module.css";
type Props = { type Props = {
value: DealSchema; value: DealSchema;
onChange: (deal: DealSchema) => void; onChange: (deal: DealSchema) => void;
onDelete: (deal: DealSchema) => void; onDelete: (deal: DealSchema) => void;
project: ProjectSchema | null;
}; };
const DealEditorBody: FC<Props> = props => { const DealEditorBody: FC<Props> = props => {
const getTabPanel = (value: string, component: ReactNode): ReactNode => (
<Tabs.Panel
key={value}
value={value}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
<Box
h={"100%"}
w={"100%"}>
{component}
</Box>
</motion.div>
</Tabs.Panel>
);
const getModuleTabs = () =>
props.project?.builtInModules.map(module => {
const moduleRender = MODULES[module.key].renderInfo;
return (
<Tabs.Tab
key={moduleRender.key}
value={moduleRender.key}
leftSection={moduleRender.icon}>
{moduleRender.label}
</Tabs.Tab>
);
});
const getModuleTabPanels = () =>
props.project?.builtInModules.map(module =>
getTabPanel(module.key, MODULES[module.key]?.getTab?.(props))
);
return ( return (
<Tabs <Tabs
defaultValue="general" defaultValue="general"
@ -22,19 +60,11 @@ const DealEditorBody: FC<Props> = props => {
leftSection={<IconEdit />}> leftSection={<IconEdit />}>
Общая информация Общая информация
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab {getModuleTabs()}
value="mock"
leftSection={<IconCircleDotted />}>
Mock
</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value="general"> {getTabPanel("general", <GeneralTab {...props} />)}
<GeneralTab {...props} /> {getModuleTabPanels()}
</Tabs.Panel>
<Tabs.Panel value="mock">
<Text>mock</Text>
</Tabs.Panel>
</Tabs> </Tabs>
); );
}; };

View File

@ -1,7 +1,7 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Stack, Text, TextInput } from "@mantine/core"; import { Stack, Text, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import Footer from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/Footer"; import Footer from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/components/Footer";
import BoardSelect from "@/components/selects/BoardSelect/BoardSelect"; import BoardSelect from "@/components/selects/BoardSelect/BoardSelect";
import StatusSelect from "@/components/selects/StatusSelect/StatusSelect"; import StatusSelect from "@/components/selects/StatusSelect/StatusSelect";
import { BoardSchema, DealSchema, StatusSchema } from "@/lib/client"; import { BoardSchema, DealSchema, StatusSchema } from "@/lib/client";

View File

@ -1,7 +1,10 @@
import { FC } from "react"; import { FC } from "react";
import { IconEdit } from "@tabler/icons-react"; import { IconBlocks, IconEdit } from "@tabler/icons-react";
import { Tabs } from "@mantine/core"; import { Tabs } from "@mantine/core";
import GeneralTab from "@/app/deals/drawers/ProjectEditorDrawer/tabs/GeneralTab/GeneralTab"; import {
GeneralTab,
ModulesTab,
} from "@/app/deals/drawers/ProjectEditorDrawer/tabs";
import { ProjectSchema } from "@/lib/client"; import { ProjectSchema } from "@/lib/client";
import styles from "../ProjectEditorDrawer.module.css"; import styles from "../ProjectEditorDrawer.module.css";
@ -22,10 +25,18 @@ const ProjectEditorBody: FC<Props> = props => {
leftSection={<IconEdit />}> leftSection={<IconEdit />}>
Общая информация Общая информация
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab
value="modules"
leftSection={<IconBlocks />}>
Модули
</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value="general"> <Tabs.Panel value="general">
<GeneralTab {...props} /> <GeneralTab {...props} />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="modules">
<ModulesTab {...props} />
</Tabs.Panel>
</Tabs> </Tabs>
); );
}; };

View File

@ -1,7 +1,7 @@
import { FC } from "react"; import { FC } from "react";
import { Stack, TextInput } from "@mantine/core"; import { Stack, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import Footer from "@/app/deals/drawers/ProjectEditorDrawer/tabs/GeneralTab/Footer"; import Footer from "@/app/deals/drawers/ProjectEditorDrawer/tabs/GeneralTab/components/Footer";
import { ProjectSchema } from "@/lib/client"; import { ProjectSchema } from "@/lib/client";
type Props = { type Props = {
@ -10,7 +10,7 @@ type Props = {
onDelete: (value: ProjectSchema) => void; onDelete: (value: ProjectSchema) => void;
}; };
const GeneralTab: FC<Props> = ({ value, onChange, onDelete }) => { export const GeneralTab: FC<Props> = ({ value, onChange, onDelete }) => {
const form = useForm<ProjectSchema>({ const form = useForm<ProjectSchema>({
initialValues: value, initialValues: value,
validate: { validate: {
@ -38,5 +38,3 @@ const GeneralTab: FC<Props> = ({ value, onChange, onDelete }) => {
</form> </form>
); );
}; };
export default GeneralTab;

View File

@ -0,0 +1,41 @@
import { FC } from "react";
import { isEqual } from "lodash";
import { Button, Stack } from "@mantine/core";
import { useForm } from "@mantine/form";
import { ProjectSchema } from "@/lib/client";
import ModulesTable from "./components/ModulesTable";
type Props = {
value: ProjectSchema;
onChange: (value: ProjectSchema) => void;
};
export const ModulesTab: FC<Props> = ({ value, onChange }) => {
const form = useForm<ProjectSchema>({
initialValues: value,
});
const onSubmit = (values: ProjectSchema) => {
onChange(values);
form.setInitialValues(values);
};
return (
<form onSubmit={form.onSubmit(onSubmit)}>
<Stack p={"md"}>
<ModulesTable
selectedRecords={form.values.builtInModules}
onSelectedRecordsChange={modules =>
form.setFieldValue("builtInModules", modules)
}
/>
<Button
type={"submit"}
variant={"default"}
disabled={isEqual(value, form.values)}>
Сохранить
</Button>
</Stack>
</form>
);
};

View File

@ -0,0 +1,32 @@
import { FC, useRef } from "react";
import { Divider, Stack } from "@mantine/core";
import useModulesTableColumns from "@/app/deals/drawers/ProjectEditorDrawer/tabs/ModulesTab/hooks/useModulesTableColumns";
import BaseTable from "@/components/ui/BaseTable/BaseTable";
import { BuiltInModuleSchema } from "@/lib/client";
import { MODULES } from "@/modules/modules";
type Props = {
selectedRecords: BuiltInModuleSchema[];
onSelectedRecordsChange: (records: BuiltInModuleSchema[]) => void;
};
const ModulesTable: FC<Props> = props => {
const columns = useModulesTableColumns();
const modules = useRef(
Object.values(MODULES).map(module => module.modelData)
);
return (
<Stack gap={0}>
<Divider />
<BaseTable
records={modules.current}
columns={columns}
verticalSpacing={"md"}
{...props}
/>
</Stack>
);
};
export default ModulesTable;

View File

@ -0,0 +1,24 @@
import { useMemo } from "react";
import { DataTableColumn } from "mantine-datatable";
import { BuiltInModuleSchema } from "@/lib/client";
const useModulesTableColumns = () => {
return useMemo(
() =>
[
{
accessor: "label",
title: "Название",
width: "30%",
},
{
title: "Описание",
accessor: "description",
width: "70%",
},
] as DataTableColumn<BuiltInModuleSchema>[],
[]
);
};
export default useModulesTableColumns;

View File

@ -0,0 +1,2 @@
export { GeneralTab } from "./GeneralTab/GeneralTab";
export { ModulesTab } from "./ModulesTab/ModulesTab";

View File

@ -13,7 +13,7 @@ const CreateProjectButton: FC = () => {
title: "Создание проекта", title: "Создание проекта",
withCloseButton: true, withCloseButton: true,
innerProps: { innerProps: {
onChange: values => projectsCrud.onCreate(values.name), onChange: projectsCrud.onCreate,
}, },
}); });
}; };

View File

@ -13,7 +13,7 @@ const CreateStatusButton: FC = () => {
title: "Создание колонки", title: "Создание колонки",
withCloseButton: true, withCloseButton: true,
innerProps: { innerProps: {
onChange: values => statusesCrud.onCreate(values.name), onChange: statusesCrud.onCreate,
}, },
}); });
}; };

View File

@ -0,0 +1,108 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { groupBy, omit } from "lodash";
import { MultiSelect, MultiSelectProps } from "@mantine/core";
interface ObjectWithIdAndName {
id: number;
name: string;
}
export type MultiselectObjectType<T> = T;
type ControlledValueProps<T> = {
value: MultiselectObjectType<T>[];
onChange: (value: MultiselectObjectType<T>[]) => void;
};
type CustomLabelAndKeyProps<T> = {
getLabelFn: (item: MultiselectObjectType<T>) => string;
getValueFn: (item: MultiselectObjectType<T>) => string;
};
type RestProps<T> = {
defaultValue?: MultiselectObjectType<T>[];
onChange: (value: MultiselectObjectType<T>[]) => void;
data: MultiselectObjectType<T>[];
groupBy?: (item: MultiselectObjectType<T>) => string;
filterBy?: (item: MultiselectObjectType<T>) => boolean;
};
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
return item.name;
};
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
return item.id.toString();
};
export type ObjectMultiSelectProps<T> = (RestProps<T> &
Partial<ControlledValueProps<T>>) &
Omit<MultiSelectProps, "value" | "onChange" | "data"> &
(T extends ObjectWithIdAndName
? Partial<CustomLabelAndKeyProps<T>>
: CustomLabelAndKeyProps<T>);
const ObjectMultiSelect = <T,>(props: ObjectMultiSelectProps<T>) => {
const isControlled = "value" in props;
const haveGetValueFn = "getValueFn" in props;
const haveGetLabelFn = "getLabelFn" in props;
const [internalValue, setInternalValue] = useState<
MultiselectObjectType<T>[] | undefined
>(props.defaultValue);
const value = (isControlled ? props.value : internalValue) || [];
const getValueFn =
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
const getLabelFn =
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
const data = useMemo(() => {
const propsData = props.filterBy
? props.data.filter(props.filterBy)
: props.data;
if (props.groupBy) {
const groupedData = groupBy(propsData, props.groupBy);
return Object.entries(groupedData).map(([group, items]) => ({
group,
items: items.map(item => ({
label: getLabelFn(item),
value: getValueFn(item),
})),
}));
}
return propsData.map(item => ({
label: getLabelFn(item),
value: getValueFn(item),
}));
}, [props.data, props.groupBy]);
const handleOnChange = (event: string[]) => {
const objects = props.data.filter(item =>
event.includes(getValueFn(item))
);
if (isControlled) {
props.onChange(objects);
return;
}
setInternalValue(objects);
};
useEffect(() => {
if (isControlled || !internalValue) return;
props.onChange(internalValue);
}, [internalValue]);
const restProps = omit(props, "getValueFn", "getLabelFn", "filterBy");
return (
<MultiSelect
{...restProps}
value={value.map(item => getValueFn(item))}
onChange={handleOnChange}
data={data}
/>
);
};
export default ObjectMultiSelect;

View File

@ -1,3 +1,5 @@
"use client";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { groupBy, omit } from "lodash"; import { groupBy, omit } from "lodash";
import { Select, SelectProps } from "@mantine/core"; import { Select, SelectProps } from "@mantine/core";
@ -33,6 +35,7 @@ const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
if (!item) return item; if (!item) return item;
return item.id.toString(); return item.id.toString();
}; };
export type ObjectSelectProps<T> = (RestProps<T> & export type ObjectSelectProps<T> = (RestProps<T> &
Partial<ControlledValueProps<T>>) & Partial<ControlledValueProps<T>>) &
Omit<SelectProps, "value" | "onChange" | "data"> & Omit<SelectProps, "value" | "onChange" | "data"> &

View File

@ -4,11 +4,10 @@ import { DataTable, DataTableProps } from "mantine-datatable";
function BaseTable<T>(props: DataTableProps<T>) { function BaseTable<T>(props: DataTableProps<T>) {
return ( return (
<DataTable <DataTable
withTableBorder={false} withTableBorder
withRowBorders withRowBorders
striped={false} striped={false}
verticalAlign={"center"} verticalAlign={"center"}
borderRadius={"lg"}
backgroundColor={"transparent"} backgroundColor={"transparent"}
{...props} {...props}
/> />

View File

@ -0,0 +1,119 @@
import { FC } from "react";
import { IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { omit } from "lodash";
import {
Button,
Fieldset,
Flex,
Group,
Image,
Loader,
rem,
Text,
} from "@mantine/core";
import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone";
import UseImageDropzone from "./types";
interface RestProps {
imageDropzone: UseImageDropzone;
onDrop: (files: FileWithPath[]) => void;
}
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
const ImageDropzone: FC<Props> = (props: Props) => {
const { showDropzone, setShowDropzone, isLoading, imageUrlInputProps } =
props.imageDropzone;
const restProps = omit(props, ["imageDropzone"]);
const getDropzone = () => (
<Dropzone
{...restProps}
accept={[
"image/png",
"image/jpeg",
"image/gif",
"image/bmp",
"image/tiff",
"image/x-icon",
"image/webp",
"image/svg+xml",
"image/heic",
]}
multiple={false}
onDrop={props.onDrop}>
<Group
justify="center"
gap="xl"
style={{ pointerEvents: "none" }}>
<Dropzone.Accept>
<IconUpload
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-blue-6)",
}}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-red-6)",
}}
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-dimmed)",
}}
stroke={1.5}
/>
</Dropzone.Idle>
<div style={{ textAlign: "center" }}>
<Text
size="xl"
inline>
Перенесите изображение или нажмите чтоб выбрать файл
</Text>
</div>
</Group>
</Dropzone>
);
const getBody = () => {
if (imageUrlInputProps?.value && !showDropzone) {
return <Image src={imageUrlInputProps.value} />;
}
return getDropzone();
};
return (
<Flex
gap={rem(10)}
direction={"column"}>
<Fieldset legend={"Изображение"}>
<Flex justify={"center"}>
{isLoading ? <Loader /> : getBody()}
</Flex>
</Fieldset>
{!showDropzone && (
<Button
onClick={() => setShowDropzone(true)}
variant={"default"}>
Заменить изображение {}
</Button>
)}
</Flex>
);
};
export default ImageDropzone;

View File

@ -0,0 +1,12 @@
import { Dispatch, SetStateAction } from "react";
import BaseFormInputProps from "@/utils/baseFormInputProps";
type UseImageDropzone = {
showDropzone: boolean;
setShowDropzone: Dispatch<SetStateAction<boolean>>;
isLoading: boolean;
setIsLoading: Dispatch<SetStateAction<boolean>>;
imageUrlInputProps?: BaseFormInputProps<string>;
};
export default UseImageDropzone;

View File

@ -0,0 +1,38 @@
import { useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { HttpValidationError } from "@/lib/client";
import { notifications } from "@/lib/notifications";
type Props = {
key: string;
queryKey: any[];
};
const getCommonQueryClient = ({ key, queryKey }: Props) => {
const queryClient = useQueryClient();
const onError = (
error: AxiosError<HttpValidationError>,
_: any,
context: any
) => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
if (context?.previous) {
queryClient.setQueryData(queryKey, context.previous);
}
};
const onSettled = () => {
queryClient.invalidateQueries({
predicate: (query: { queryKey: any }) =>
query.queryKey[0]?._id === key,
});
};
return { queryClient, onError, onSettled };
};
export default getCommonQueryClient;

View File

@ -1,14 +1,10 @@
import React from "react"; import React from "react";
import { import { useMutation, UseMutationOptions } from "@tanstack/react-query";
useMutation,
UseMutationOptions,
useQueryClient,
} from "@tanstack/react-query";
import { AxiosError } from "axios"; 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 getCommonQueryClient from "@/hooks/cruds/baseCrud/getCommonQueryClient";
import { HttpValidationError } from "@/lib/client"; import { HttpValidationError } from "@/lib/client";
import { notifications } from "@/lib/notifications";
import { sortByLexorank } from "@/utils/lexorank"; import { sortByLexorank } from "@/utils/lexorank";
import { import {
BaseEntity, BaseEntity,
@ -17,10 +13,17 @@ import {
UpdateMutationOptions, UpdateMutationOptions,
} from "./types"; } from "./types";
type CrudOperations<TEntity, TUpdate> = { type CrudOperations<
onCreate: (name: string) => void; TEntity,
onUpdate: (id: number, update: TUpdate) => void; TUpdate,
onDelete: (entity: TEntity) => void; TCreate,
HasGetCreateEntity extends boolean,
> = {
onCreate: HasGetCreateEntity extends true
? (data: Partial<TCreate>, onSuccess?: () => void) => void
: (data: TCreate, onSuccess?: () => void) => void;
onUpdate: (id: number, update: TUpdate, onSuccess?: () => void) => void;
onDelete: (entity: TEntity, onSuccess?: () => void) => void;
}; };
type UseEntityOperationsProps<TEntity extends BaseEntity, TUpdate, TCreate> = { type UseEntityOperationsProps<TEntity extends BaseEntity, TUpdate, TCreate> = {
@ -43,12 +46,32 @@ type UseEntityOperationsProps<TEntity extends BaseEntity, TUpdate, TCreate> = {
DeleteMutationOptions DeleteMutationOptions
>; >;
}; };
getCreateEntity: (name: string) => TCreate | null; getCreateEntity?: (data: Partial<TCreate>) => TCreate | null;
getUpdateEntity: (oldEntity: TEntity, update: TUpdate) => TEntity; getUpdateEntity: (oldEntity: TEntity, update: TUpdate) => TEntity;
getDeleteConfirmTitle: (entity: TEntity) => string; getDeleteConfirmTitle: (entity: TEntity) => string;
}; };
const useCrudOperations = < function useCrudOperations<
TEntity extends BaseEntity,
TUpdate extends object,
TCreate extends object,
>(
props: UseEntityOperationsProps<TEntity, TUpdate, TCreate> & {
getCreateEntity: (data: Partial<TCreate>) => TCreate | null;
}
): CrudOperations<TEntity, TUpdate, TCreate, true>;
function useCrudOperations<
TEntity extends BaseEntity,
TUpdate extends object,
TCreate extends object,
>(
props: UseEntityOperationsProps<TEntity, TUpdate, TCreate> & {
getCreateEntity?: undefined;
}
): CrudOperations<TEntity, TUpdate, TCreate, false>;
function useCrudOperations<
TEntity extends BaseEntity, TEntity extends BaseEntity,
TUpdate extends object, TUpdate extends object,
TCreate extends object, TCreate extends object,
@ -61,30 +84,14 @@ const useCrudOperations = <
getDeleteConfirmTitle, getDeleteConfirmTitle,
}: UseEntityOperationsProps<TEntity, TUpdate, TCreate>): CrudOperations< }: UseEntityOperationsProps<TEntity, TUpdate, TCreate>): CrudOperations<
TEntity, TEntity,
TUpdate TUpdate,
> => { TCreate,
const queryClient = useQueryClient(); boolean
> {
const onError = ( const { queryClient, onError, onSettled } = getCommonQueryClient({
error: AxiosError<HttpValidationError>, queryKey,
_: any, key,
context: any
) => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
}); });
if (context?.previous) {
queryClient.setQueryData(queryKey, context.previous);
}
};
const onSettled = () => {
queryClient.invalidateQueries({
predicate: (query: { queryKey: any }) =>
query.queryKey[0]?._id === key,
});
};
const createMutation = useMutation({ const createMutation = useMutation({
...mutations.create, ...mutations.create,
@ -102,6 +109,7 @@ const useCrudOperations = <
const previous = queryClient.getQueryData(queryKey); const previous = queryClient.getQueryData(queryKey);
queryClient.setQueryData(queryKey, (old: { items: TEntity[] }) => { queryClient.setQueryData(queryKey, (old: { items: TEntity[] }) => {
if (!old) return;
let updated = old.items.map((entity: TEntity) => let updated = old.items.map((entity: TEntity) =>
entity.id === update.id entity.id === update.id
? getUpdateEntity(entity, update) ? getUpdateEntity(entity, update)
@ -145,26 +153,43 @@ const useCrudOperations = <
}, },
}); });
const onCreate = (name: string) => { const onCreate = (data: any, onSuccess?: () => void) => {
const entity = getCreateEntity(name); let entity: TCreate;
if (!entity) return; if (getCreateEntity) {
createMutation.mutate({ const result = getCreateEntity(data);
if (!result) return;
entity = result;
} else {
entity = data;
}
createMutation.mutate(
{
body: { body: {
entity, entity,
}, },
path: undefined, path: undefined,
query: undefined, query: undefined,
}); },
{
onSuccess,
}
);
}; };
const onUpdate = async (id: number, update: TUpdate) => { const onUpdate = (id: number, update: TUpdate, onSuccess?: () => void) => {
updateMutation.mutate({ updateMutation.mutate(
{
body: { body: {
entity: update, entity: update,
}, },
path: { pk: id }, path: { pk: id },
query: undefined, query: undefined,
}); },
{
onSuccess,
}
);
}; };
const onDelete = (entity: TEntity, onSuccess?: () => void) => { const onDelete = (entity: TEntity, onSuccess?: () => void) => {
@ -175,13 +200,14 @@ const useCrudOperations = <
), ),
confirmProps: { color: "red" }, confirmProps: { color: "red" },
onConfirm: () => { onConfirm: () => {
deleteMutation.mutate({ path: { pk: entity.id } } as any); deleteMutation.mutate({ path: { pk: entity.id } } as any, {
onSuccess && onSuccess(); onSuccess,
});
}, },
}); });
}; };
return { onCreate, onUpdate, onDelete }; return { onCreate, onUpdate, onDelete };
}; }
export default useCrudOperations; export default useCrudOperations;

View File

@ -19,7 +19,7 @@ type UseBoardsOperationsProps = {
}; };
export type BoardsCrud = { export type BoardsCrud = {
onCreate: (name: string) => void; onCreate: (data: Partial<CreateBoardSchema>) => void;
onUpdate: (boardId: number, board: UpdateBoardSchema) => void; onUpdate: (boardId: number, board: UpdateBoardSchema) => void;
onDelete: (board: BoardSchema) => void; onDelete: (board: BoardSchema) => void;
}; };
@ -38,14 +38,14 @@ export const useBoardsCrud = ({
update: updateBoardMutation(), update: updateBoardMutation(),
delete: deleteBoardMutation(), delete: deleteBoardMutation(),
}, },
getCreateEntity: name => { getCreateEntity: data => {
if (!projectId) return null; if (!projectId) return null;
const lastBoard = getMaxByLexorank(boards); const lastBoard = getMaxByLexorank(boards);
const newLexorank = getNewLexorank( const newLexorank = getNewLexorank(
lastBoard ? LexoRank.parse(lastBoard.lexorank) : null lastBoard ? LexoRank.parse(lastBoard.lexorank) : null
); );
return { return {
name, name: data.name!,
projectId, projectId,
lexorank: newLexorank.toString(), lexorank: newLexorank.toString(),
}; };

View File

@ -21,7 +21,7 @@ type UseDealsOperationsProps = {
}; };
export type DealsCrud = { export type DealsCrud = {
onCreate: (name: string) => void; onCreate: (entity: Partial<CreateDealSchema>) => void;
onUpdate: (dealId: number, deal: UpdateDealSchema) => void; onUpdate: (dealId: number, deal: UpdateDealSchema) => void;
onDelete: (deal: DealSchema, onSuccess?: () => void) => void; onDelete: (deal: DealSchema, onSuccess?: () => void) => void;
}; };
@ -40,7 +40,7 @@ export const useDealsCrud = ({
update: updateDealMutation(), update: updateDealMutation(),
delete: deleteDealMutation(), delete: deleteDealMutation(),
}, },
getCreateEntity: name => { getCreateEntity: data => {
if (!boardId || statuses.length === 0) return null; if (!boardId || statuses.length === 0) return null;
const firstStatus = statuses[0]; const firstStatus = statuses[0];
const filteredDeals = deals.filter( const filteredDeals = deals.filter(
@ -55,7 +55,7 @@ export const useDealsCrud = ({
firstDeal ? LexoRank.parse(firstDeal.lexorank) : null firstDeal ? LexoRank.parse(firstDeal.lexorank) : null
); );
return { return {
name, name: data.name!,
boardId, boardId,
statusId: firstStatus.id, statusId: firstStatus.id,
lexorank: newLexorank.toString(), lexorank: newLexorank.toString(),

View File

@ -15,7 +15,7 @@ type Props = {
}; };
export type ProjectsCrud = { export type ProjectsCrud = {
onCreate: (name: string) => void; onCreate: (data: Partial<CreateProjectSchema>) => void;
onUpdate: (projectId: number, project: UpdateProjectSchema) => void; onUpdate: (projectId: number, project: UpdateProjectSchema) => void;
onDelete: (project: ProjectSchema, onSuccess?: () => void) => void; onDelete: (project: ProjectSchema, onSuccess?: () => void) => void;
}; };
@ -33,7 +33,7 @@ export const useProjectsCrud = ({ queryKey }: Props): ProjectsCrud => {
update: updateProjectMutation(), update: updateProjectMutation(),
delete: deleteProjectMutation(), delete: deleteProjectMutation(),
}, },
getCreateEntity: name => ({ name }), getCreateEntity: data => ({ name: data.name! }),
getUpdateEntity: (old, update) => ({ getUpdateEntity: (old, update) => ({
...old, ...old,
name: update.name ?? old.name, name: update.name ?? old.name,

View File

@ -19,7 +19,7 @@ type Props = {
}; };
export type StatusesCrud = { export type StatusesCrud = {
onCreate: (name: string) => void; onCreate: (entity: Partial<CreateStatusSchema>) => void;
onUpdate: (statusId: number, status: UpdateStatusSchema) => void; onUpdate: (statusId: number, status: UpdateStatusSchema) => void;
onDelete: (status: StatusSchema) => void; onDelete: (status: StatusSchema) => void;
}; };
@ -41,14 +41,14 @@ export const useStatusesCrud = ({
update: updateStatusMutation(), update: updateStatusMutation(),
delete: deleteStatusMutation(), delete: deleteStatusMutation(),
}, },
getCreateEntity: name => { getCreateEntity: data => {
if (!boardId) return null; if (!boardId) return null;
const lastBoard = getMaxByLexorank(statuses); const lastBoard = getMaxByLexorank(statuses);
const newLexorank = getNewLexorank( const newLexorank = getNewLexorank(
lastBoard ? LexoRank.parse(lastBoard.lexorank) : null lastBoard ? LexoRank.parse(lastBoard.lexorank) : null
); );
return { return {
name, name: data.name!,
boardId, boardId,
lexorank: newLexorank.toString(), lexorank: newLexorank.toString(),
}; };

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,36 @@
import type { Client, Options as ClientOptions, TDataShape } from "./client"; import type { Client, Options as ClientOptions, TDataShape } from "./client";
import { client as _heyApiClient } from "./client.gen"; import { client as _heyApiClient } from "./client.gen";
import type { import type {
AddKitToDealProductData,
AddKitToDealProductErrors,
AddKitToDealProductResponses,
CreateBoardData, CreateBoardData,
CreateBoardErrors, CreateBoardErrors,
CreateBoardResponses, CreateBoardResponses,
CreateDealData, CreateDealData,
CreateDealErrors, CreateDealErrors,
CreateDealProductData,
CreateDealProductErrors,
CreateDealProductResponses,
CreateDealProductServiceData,
CreateDealProductServiceErrors,
CreateDealProductServiceResponses,
CreateDealResponses, CreateDealResponses,
CreateDealServiceData,
CreateDealServiceErrors,
CreateDealServiceResponses,
CreateProductData,
CreateProductErrors,
CreateProductResponses,
CreateProjectData, CreateProjectData,
CreateProjectErrors, CreateProjectErrors,
CreateProjectResponses, CreateProjectResponses,
CreateServiceData,
CreateServiceErrors,
CreateServiceResponses,
CreateServicesKitData,
CreateServicesKitErrors,
CreateServicesKitResponses,
CreateStatusData, CreateStatusData,
CreateStatusErrors, CreateStatusErrors,
CreateStatusResponses, CreateStatusResponses,
@ -20,21 +41,57 @@ import type {
DeleteBoardResponses, DeleteBoardResponses,
DeleteDealData, DeleteDealData,
DeleteDealErrors, DeleteDealErrors,
DeleteDealProductData,
DeleteDealProductErrors,
DeleteDealProductResponses,
DeleteDealProductServiceData,
DeleteDealProductServiceErrors,
DeleteDealProductServiceResponses,
DeleteDealResponses, DeleteDealResponses,
DeleteDealServiceData,
DeleteDealServiceErrors,
DeleteDealServiceResponses,
DeleteProductData,
DeleteProductErrors,
DeleteProductResponses,
DeleteProjectData, DeleteProjectData,
DeleteProjectErrors, DeleteProjectErrors,
DeleteProjectResponses, DeleteProjectResponses,
DeleteServiceData,
DeleteServiceErrors,
DeleteServiceResponses,
DeleteServicesKitData,
DeleteServicesKitErrors,
DeleteServicesKitResponses,
DeleteStatusData, DeleteStatusData,
DeleteStatusErrors, DeleteStatusErrors,
DeleteStatusResponses, DeleteStatusResponses,
DuplicateProductServicesData,
DuplicateProductServicesErrors,
DuplicateProductServicesResponses,
GetBoardsData, GetBoardsData,
GetBoardsErrors, GetBoardsErrors,
GetBoardsResponses, GetBoardsResponses,
GetBuiltInModulesData,
GetBuiltInModulesResponses,
GetDealProductsData,
GetDealProductsErrors,
GetDealProductsResponses,
GetDealsData, GetDealsData,
GetDealsErrors, GetDealsErrors,
GetDealServicesData,
GetDealServicesErrors,
GetDealServicesResponses,
GetDealsResponses, GetDealsResponses,
GetProductsData,
GetProductsErrors,
GetProductsResponses,
GetProjectsData, GetProjectsData,
GetProjectsResponses, GetProjectsResponses,
GetServicesData,
GetServicesKitsData,
GetServicesKitsResponses,
GetServicesResponses,
GetStatusesData, GetStatusesData,
GetStatusesErrors, GetStatusesErrors,
GetStatusesResponses, GetStatusesResponses,
@ -43,45 +100,115 @@ import type {
UpdateBoardResponses, UpdateBoardResponses,
UpdateDealData, UpdateDealData,
UpdateDealErrors, UpdateDealErrors,
UpdateDealProductData,
UpdateDealProductErrors,
UpdateDealProductResponses,
UpdateDealProductServiceData,
UpdateDealProductServiceErrors,
UpdateDealProductServiceResponses,
UpdateDealResponses, UpdateDealResponses,
UpdateDealServiceData,
UpdateDealServiceErrors,
UpdateDealServiceResponses,
UpdateProductData,
UpdateProductErrors,
UpdateProductResponses,
UpdateProjectData, UpdateProjectData,
UpdateProjectErrors, UpdateProjectErrors,
UpdateProjectResponses, UpdateProjectResponses,
UpdateServiceData,
UpdateServiceErrors,
UpdateServiceResponses,
UpdateServicesKitData,
UpdateServicesKitErrors,
UpdateServicesKitResponses,
UpdateStatusData, UpdateStatusData,
UpdateStatusErrors, UpdateStatusErrors,
UpdateStatusResponses, UpdateStatusResponses,
} from "./types.gen"; } from "./types.gen";
import { import {
zAddKitToDealProductData,
zAddKitToDealProductResponse,
zCreateBoardData, zCreateBoardData,
zCreateBoardResponse2, zCreateBoardResponse2,
zCreateDealData, zCreateDealData,
zCreateDealProductData,
zCreateDealProductResponse2,
zCreateDealProductServiceData,
zCreateDealProductServiceResponse,
zCreateDealResponse2, zCreateDealResponse2,
zCreateDealServiceData,
zCreateDealServiceResponse2,
zCreateProductData,
zCreateProductResponse2,
zCreateProjectData, zCreateProjectData,
zCreateProjectResponse2, zCreateProjectResponse2,
zCreateServiceData,
zCreateServiceResponse2,
zCreateServicesKitData,
zCreateServicesKitResponse2,
zCreateStatusData, zCreateStatusData,
zCreateStatusResponse2, zCreateStatusResponse2,
zDeleteBoardData, zDeleteBoardData,
zDeleteBoardResponse2, zDeleteBoardResponse2,
zDeleteDealData, zDeleteDealData,
zDeleteDealProductData,
zDeleteDealProductResponse2,
zDeleteDealProductServiceData,
zDeleteDealProductServiceResponse,
zDeleteDealResponse2, zDeleteDealResponse2,
zDeleteDealServiceData,
zDeleteDealServiceResponse2,
zDeleteProductData,
zDeleteProductResponse2,
zDeleteProjectData, zDeleteProjectData,
zDeleteProjectResponse2, zDeleteProjectResponse2,
zDeleteServiceData,
zDeleteServiceResponse2,
zDeleteServicesKitData,
zDeleteServicesKitResponse2,
zDeleteStatusData, zDeleteStatusData,
zDeleteStatusResponse2, zDeleteStatusResponse2,
zDuplicateProductServicesData,
zDuplicateProductServicesResponse,
zGetBoardsData, zGetBoardsData,
zGetBoardsResponse2, zGetBoardsResponse2,
zGetBuiltInModulesData,
zGetBuiltInModulesResponse,
zGetDealProductsData,
zGetDealProductsResponse2,
zGetDealsData, zGetDealsData,
zGetDealServicesData,
zGetDealServicesResponse2,
zGetDealsResponse2, zGetDealsResponse2,
zGetProductsData,
zGetProductsResponse2,
zGetProjectsData, zGetProjectsData,
zGetProjectsResponse2, zGetProjectsResponse2,
zGetServicesData,
zGetServicesKitsData,
zGetServicesKitsResponse,
zGetServicesResponse2,
zGetStatusesData, zGetStatusesData,
zGetStatusesResponse2, zGetStatusesResponse2,
zUpdateBoardData, zUpdateBoardData,
zUpdateBoardResponse2, zUpdateBoardResponse2,
zUpdateDealData, zUpdateDealData,
zUpdateDealProductData,
zUpdateDealProductResponse2,
zUpdateDealProductServiceData,
zUpdateDealProductServiceResponse,
zUpdateDealResponse2, zUpdateDealResponse2,
zUpdateDealServiceData,
zUpdateDealServiceResponse2,
zUpdateProductData,
zUpdateProductResponse2,
zUpdateProjectData, zUpdateProjectData,
zUpdateProjectResponse2, zUpdateProjectResponse2,
zUpdateServiceData,
zUpdateServiceResponse2,
zUpdateServicesKitData,
zUpdateServicesKitResponse2,
zUpdateStatusData, zUpdateStatusData,
zUpdateStatusResponse2, zUpdateStatusResponse2,
} from "./zod.gen"; } from "./zod.gen";
@ -303,6 +430,29 @@ export const updateDeal = <ThrowOnError extends boolean = false>(
}); });
}; };
/**
* Get Built In Modules
*/
export const getBuiltInModules = <ThrowOnError extends boolean = false>(
options?: Options<GetBuiltInModulesData, ThrowOnError>
) => {
return (options?.client ?? _heyApiClient).get<
GetBuiltInModulesResponses,
unknown,
ThrowOnError
>({
requestValidator: async data => {
return await zGetBuiltInModulesData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zGetBuiltInModulesResponse.parseAsync(data);
},
url: "/module/built-in/",
...options,
});
};
/** /**
* Get Projects * Get Projects
*/ */
@ -502,3 +652,634 @@ export const updateStatus = <ThrowOnError extends boolean = false>(
}, },
}); });
}; };
/**
* Get Deal Products
*/
export const getDealProducts = <ThrowOnError extends boolean = false>(
options: Options<GetDealProductsData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).get<
GetDealProductsResponses,
GetDealProductsErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zGetDealProductsData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zGetDealProductsResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-product/{dealId}",
...options,
});
};
/**
* Create Deal Product
*/
export const createDealProduct = <ThrowOnError extends boolean = false>(
options: Options<CreateDealProductData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
CreateDealProductResponses,
CreateDealProductErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zCreateDealProductData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zCreateDealProductResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-product/",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Delete Deal Product
*/
export const deleteDealProduct = <ThrowOnError extends boolean = false>(
options: Options<DeleteDealProductData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).delete<
DeleteDealProductResponses,
DeleteDealProductErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zDeleteDealProductData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zDeleteDealProductResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-product/{dealId}/product/{productId}",
...options,
});
};
/**
* Update Deal Product
*/
export const updateDealProduct = <ThrowOnError extends boolean = false>(
options: Options<UpdateDealProductData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).patch<
UpdateDealProductResponses,
UpdateDealProductErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zUpdateDealProductData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zUpdateDealProductResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-product/{dealId}/product/{productId}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Add Kit To Deal Product
*/
export const addKitToDealProduct = <ThrowOnError extends boolean = false>(
options: Options<AddKitToDealProductData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
AddKitToDealProductResponses,
AddKitToDealProductErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zAddKitToDealProductData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zAddKitToDealProductResponse.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-product/add-services-kit",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Create Deal Product Service
*/
export const createDealProductService = <ThrowOnError extends boolean = false>(
options: Options<CreateDealProductServiceData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
CreateDealProductServiceResponses,
CreateDealProductServiceErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zCreateDealProductServiceData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zCreateDealProductServiceResponse.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-product/service",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Delete Deal Product Service
*/
export const deleteDealProductService = <ThrowOnError extends boolean = false>(
options: Options<DeleteDealProductServiceData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).delete<
DeleteDealProductServiceResponses,
DeleteDealProductServiceErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zDeleteDealProductServiceData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zDeleteDealProductServiceResponse.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-product/{dealId}/product/{productId}/service/{serviceId}",
...options,
});
};
/**
* Update Deal Product Service
*/
export const updateDealProductService = <ThrowOnError extends boolean = false>(
options: Options<UpdateDealProductServiceData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).patch<
UpdateDealProductServiceResponses,
UpdateDealProductServiceErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zUpdateDealProductServiceData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zUpdateDealProductServiceResponse.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-product/{dealId}/product/{productId}/service/{serviceId}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Copy Product Services
*/
export const duplicateProductServices = <ThrowOnError extends boolean = false>(
options: Options<DuplicateProductServicesData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
DuplicateProductServicesResponses,
DuplicateProductServicesErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zDuplicateProductServicesData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zDuplicateProductServicesResponse.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-product/services/duplicate",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Get Deal Services
*/
export const getDealServices = <ThrowOnError extends boolean = false>(
options: Options<GetDealServicesData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).get<
GetDealServicesResponses,
GetDealServicesErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zGetDealServicesData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zGetDealServicesResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-service/{dealId}",
...options,
});
};
/**
* Create Deal Service
*/
export const createDealService = <ThrowOnError extends boolean = false>(
options: Options<CreateDealServiceData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
CreateDealServiceResponses,
CreateDealServiceErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zCreateDealServiceData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zCreateDealServiceResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-service/",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Delete Deal Service
*/
export const deleteDealService = <ThrowOnError extends boolean = false>(
options: Options<DeleteDealServiceData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).delete<
DeleteDealServiceResponses,
DeleteDealServiceErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zDeleteDealServiceData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zDeleteDealServiceResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-service/{dealId}/service/{serviceId}",
...options,
});
};
/**
* Update Deal Service
*/
export const updateDealService = <ThrowOnError extends boolean = false>(
options: Options<UpdateDealServiceData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).patch<
UpdateDealServiceResponses,
UpdateDealServiceErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zUpdateDealServiceData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zUpdateDealServiceResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/deal-service/{dealId}/service/{serviceId}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Get Products
*/
export const getProducts = <ThrowOnError extends boolean = false>(
options?: Options<GetProductsData, ThrowOnError>
) => {
return (options?.client ?? _heyApiClient).get<
GetProductsResponses,
GetProductsErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zGetProductsData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zGetProductsResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/product/",
...options,
});
};
/**
* Create Product
*/
export const createProduct = <ThrowOnError extends boolean = false>(
options: Options<CreateProductData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
CreateProductResponses,
CreateProductErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zCreateProductData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zCreateProductResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/product/",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Delete Product
*/
export const deleteProduct = <ThrowOnError extends boolean = false>(
options: Options<DeleteProductData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).delete<
DeleteProductResponses,
DeleteProductErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zDeleteProductData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zDeleteProductResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/product/{pk}",
...options,
});
};
/**
* Update Product
*/
export const updateProduct = <ThrowOnError extends boolean = false>(
options: Options<UpdateProductData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).patch<
UpdateProductResponses,
UpdateProductErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zUpdateProductData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zUpdateProductResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/product/{pk}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Get Services
*/
export const getServices = <ThrowOnError extends boolean = false>(
options?: Options<GetServicesData, ThrowOnError>
) => {
return (options?.client ?? _heyApiClient).get<
GetServicesResponses,
unknown,
ThrowOnError
>({
requestValidator: async data => {
return await zGetServicesData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zGetServicesResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/service/",
...options,
});
};
/**
* Create Service
*/
export const createService = <ThrowOnError extends boolean = false>(
options: Options<CreateServiceData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
CreateServiceResponses,
CreateServiceErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zCreateServiceData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zCreateServiceResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/service/",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Delete Service
*/
export const deleteService = <ThrowOnError extends boolean = false>(
options: Options<DeleteServiceData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).delete<
DeleteServiceResponses,
DeleteServiceErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zDeleteServiceData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zDeleteServiceResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/service/{pk}",
...options,
});
};
/**
* Update Service
*/
export const updateService = <ThrowOnError extends boolean = false>(
options: Options<UpdateServiceData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).patch<
UpdateServiceResponses,
UpdateServiceErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zUpdateServiceData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zUpdateServiceResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/service/{pk}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Get Services Kits
*/
export const getServicesKits = <ThrowOnError extends boolean = false>(
options?: Options<GetServicesKitsData, ThrowOnError>
) => {
return (options?.client ?? _heyApiClient).get<
GetServicesKitsResponses,
unknown,
ThrowOnError
>({
requestValidator: async data => {
return await zGetServicesKitsData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zGetServicesKitsResponse.parseAsync(data);
},
url: "/modules/fulfillment-base/services-kit/",
...options,
});
};
/**
* Create Services Kit
*/
export const createServicesKit = <ThrowOnError extends boolean = false>(
options: Options<CreateServicesKitData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).post<
CreateServicesKitResponses,
CreateServicesKitErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zCreateServicesKitData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zCreateServicesKitResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/services-kit/",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Delete Services Kit
*/
export const deleteServicesKit = <ThrowOnError extends boolean = false>(
options: Options<DeleteServicesKitData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).delete<
DeleteServicesKitResponses,
DeleteServicesKitErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zDeleteServicesKitData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zDeleteServicesKitResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/services-kit/{pk}",
...options,
});
};
/**
* Update Services Kit
*/
export const updateServicesKit = <ThrowOnError extends boolean = false>(
options: Options<UpdateServicesKitData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).patch<
UpdateServicesKitResponses,
UpdateServicesKitErrors,
ThrowOnError
>({
requestValidator: async data => {
return await zUpdateServicesKitData.parseAsync(data);
},
responseType: "json",
responseValidator: async data => {
return await zUpdateServicesKitResponse2.parseAsync(data);
},
url: "/modules/fulfillment-base/services-kit/{pk}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,17 @@ export const zBoardSchema = z.object({
projectId: z.int(), projectId: z.int(),
}); });
/**
* BuiltInModuleSchema
*/
export const zBuiltInModuleSchema = z.object({
id: z.int(),
key: z.string(),
label: z.string(),
iconName: z.string(),
description: z.string(),
});
/** /**
* CreateBoardSchema * CreateBoardSchema
*/ */
@ -36,6 +47,104 @@ export const zCreateBoardResponse = z.object({
entity: zBoardSchema, entity: zBoardSchema,
}); });
/**
* CreateDealProductSchema
*/
export const zCreateDealProductSchema = z.object({
dealId: z.int(),
productId: z.int(),
quantity: z.int(),
comment: z.string(),
});
/**
* CreateDealProductRequest
*/
export const zCreateDealProductRequest = z.object({
entity: zCreateDealProductSchema,
});
/**
* ProductSchema
*/
export const zProductSchema = z.object({
name: z.string(),
article: z.string(),
factoryArticle: z.string(),
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()]),
id: z.int(),
});
/**
* ServiceCategorySchema
*/
export const zServiceCategorySchema = z.object({
id: z.int(),
name: z.string(),
dealServiceRank: z.string(),
productServiceRank: z.string(),
});
/**
* ServicePriceRangeSchema
*/
export const zServicePriceRangeSchema = z.object({
id: z.union([z.int(), z.null()]),
fromQuantity: z.int(),
toQuantity: z.int(),
price: z.number(),
});
/**
* ServiceSchema
*/
export const zServiceSchema = z.object({
id: z.int(),
name: z.string(),
category: zServiceCategorySchema,
price: z.number(),
serviceType: z.int(),
priceRanges: z.array(zServicePriceRangeSchema),
cost: z.union([z.number(), z.null()]),
lexorank: z.string(),
});
/**
* ProductServiceSchema
*/
export const zProductServiceSchema = z.object({
dealId: z.int(),
productId: z.int(),
serviceId: z.int(),
service: zServiceSchema,
price: z.number(),
isFixedPrice: z.boolean(),
});
/**
* DealProductSchema
*/
export const zDealProductSchema = z.object({
dealId: z.int(),
productId: z.int(),
product: zProductSchema,
quantity: z.int(),
comment: z.string(),
productServices: z.array(zProductServiceSchema),
});
/**
* CreateDealProductResponse
*/
export const zCreateDealProductResponse = z.object({
message: z.string(),
entity: zDealProductSchema,
});
/** /**
* CreateDealSchema * CreateDealSchema
*/ */
@ -84,6 +193,97 @@ export const zCreateDealResponse = z.object({
entity: zDealSchema, entity: zDealSchema,
}); });
/**
* CreateDealServiceSchema
*/
export const zCreateDealServiceSchema = z.object({
dealId: z.int(),
serviceId: z.int(),
quantity: z.int(),
price: z.number(),
});
/**
* CreateDealServiceRequest
*/
export const zCreateDealServiceRequest = z.object({
entity: zCreateDealServiceSchema,
});
/**
* DealServiceSchema
*/
export const zDealServiceSchema = z.object({
dealId: z.int(),
serviceId: z.int(),
service: zServiceSchema,
quantity: z.int(),
price: z.number(),
isFixedPrice: z.boolean(),
});
/**
* CreateDealServiceResponse
*/
export const zCreateDealServiceResponse = z.object({
message: z.string(),
entity: zDealServiceSchema,
});
/**
* CreateProductSchema
*/
export const zCreateProductSchema = z.object({
name: z.string(),
article: z.string(),
factoryArticle: z.string(),
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()]),
});
/**
* CreateProductRequest
*/
export const zCreateProductRequest = z.object({
entity: zCreateProductSchema,
});
/**
* CreateProductResponse
*/
export const zCreateProductResponse = z.object({
message: z.string(),
entity: zProductSchema,
});
/**
* CreateProductServiceSchema
*/
export const zCreateProductServiceSchema = z.object({
dealId: z.int(),
productId: z.int(),
serviceId: z.int(),
price: z.number(),
});
/**
* CreateProductServiceRequest
*/
export const zCreateProductServiceRequest = z.object({
entity: zCreateProductServiceSchema,
});
/**
* CreateProductServiceResponse
*/
export const zCreateProductServiceResponse = z.object({
message: z.string(),
entity: zProductServiceSchema,
});
/** /**
* CreateProjectSchema * CreateProjectSchema
*/ */
@ -104,6 +304,7 @@ export const zCreateProjectRequest = z.object({
export const zProjectSchema = z.object({ export const zProjectSchema = z.object({
id: z.int(), id: z.int(),
name: z.string(), name: z.string(),
builtInModules: z.array(zBuiltInModuleSchema),
}); });
/** /**
@ -114,6 +315,69 @@ export const zCreateProjectResponse = z.object({
entity: zProjectSchema, entity: zProjectSchema,
}); });
/**
* CreateServiceSchema
*/
export const zCreateServiceSchema = z.object({
id: z.int(),
name: z.string(),
category: zServiceCategorySchema,
price: z.number(),
serviceType: z.int(),
priceRanges: z.array(zServicePriceRangeSchema),
cost: z.union([z.number(), z.null()]),
lexorank: z.string(),
});
/**
* CreateServiceRequest
*/
export const zCreateServiceRequest = z.object({
entity: zCreateServiceSchema,
});
/**
* CreateServiceResponse
*/
export const zCreateServiceResponse = z.object({
message: z.string(),
entity: zServiceSchema,
});
/**
* CreateServicesKitSchema
*/
export const zCreateServicesKitSchema = z.object({
name: z.string(),
serviceType: z.int(),
servicesIds: z.array(z.int()),
});
/**
* CreateServicesKitRequest
*/
export const zCreateServicesKitRequest = z.object({
entity: zCreateServicesKitSchema,
});
/**
* ServicesKitSchema
*/
export const zServicesKitSchema = z.object({
name: z.string(),
serviceType: z.int(),
id: z.int(),
services: z.array(zServiceSchema),
});
/**
* CreateServicesKitResponse
*/
export const zCreateServicesKitResponse = z.object({
message: z.string(),
entity: zServicesKitSchema,
});
/** /**
* CreateStatusSchema * CreateStatusSchema
*/ */
@ -138,6 +402,22 @@ export const zCreateStatusResponse = z.object({
entity: zStatusSchema, entity: zStatusSchema,
}); });
/**
* DealProductAddKitRequest
*/
export const zDealProductAddKitRequest = z.object({
dealId: z.int(),
productId: z.int(),
kitId: z.int(),
});
/**
* DealProductAddKitResponse
*/
export const zDealProductAddKitResponse = z.object({
message: z.string(),
});
/** /**
* DeleteBoardResponse * DeleteBoardResponse
*/ */
@ -145,6 +425,13 @@ export const zDeleteBoardResponse = z.object({
message: z.string(), message: z.string(),
}); });
/**
* DeleteDealProductResponse
*/
export const zDeleteDealProductResponse = z.object({
message: z.string(),
});
/** /**
* DeleteDealResponse * DeleteDealResponse
*/ */
@ -152,6 +439,27 @@ export const zDeleteDealResponse = z.object({
message: z.string(), message: z.string(),
}); });
/**
* DeleteDealServiceResponse
*/
export const zDeleteDealServiceResponse = z.object({
message: z.string(),
});
/**
* DeleteProductResponse
*/
export const zDeleteProductResponse = z.object({
message: z.string(),
});
/**
* DeleteProductServiceResponse
*/
export const zDeleteProductServiceResponse = z.object({
message: z.string(),
});
/** /**
* DeleteProjectResponse * DeleteProjectResponse
*/ */
@ -159,6 +467,20 @@ export const zDeleteProjectResponse = z.object({
message: z.string(), message: z.string(),
}); });
/**
* DeleteServiceResponse
*/
export const zDeleteServiceResponse = z.object({
message: z.string(),
});
/**
* DeleteServicesKitResponse
*/
export const zDeleteServicesKitResponse = z.object({
message: z.string(),
});
/** /**
* DeleteStatusResponse * DeleteStatusResponse
*/ */
@ -166,6 +488,13 @@ export const zDeleteStatusResponse = z.object({
message: z.string(), message: z.string(),
}); });
/**
* GetAllBuiltInModulesResponse
*/
export const zGetAllBuiltInModulesResponse = z.object({
items: z.array(zBuiltInModuleSchema),
});
/** /**
* GetBoardsResponse * GetBoardsResponse
*/ */
@ -173,6 +502,20 @@ export const zGetBoardsResponse = z.object({
items: z.array(zBoardSchema), items: z.array(zBoardSchema),
}); });
/**
* GetDealProductsResponse
*/
export const zGetDealProductsResponse = z.object({
items: z.array(zDealProductSchema),
});
/**
* GetDealServicesResponse
*/
export const zGetDealServicesResponse = z.object({
items: z.array(zDealServiceSchema),
});
/** /**
* PaginationInfoSchema * PaginationInfoSchema
*/ */
@ -189,6 +532,13 @@ export const zGetDealsResponse = z.object({
paginationInfo: zPaginationInfoSchema, paginationInfo: zPaginationInfoSchema,
}); });
/**
* GetProductsResponse
*/
export const zGetProductsResponse = z.object({
items: z.array(zProductSchema),
});
/** /**
* GetProjectsResponse * GetProjectsResponse
*/ */
@ -196,6 +546,20 @@ export const zGetProjectsResponse = z.object({
items: z.array(zProjectSchema), items: z.array(zProjectSchema),
}); });
/**
* GetServicesKitResponse
*/
export const zGetServicesKitResponse = z.object({
items: z.array(zServicesKitSchema),
});
/**
* GetServicesResponse
*/
export const zGetServicesResponse = z.object({
items: z.array(zServiceSchema),
});
/** /**
* GetStatusesResponse * GetStatusesResponse
*/ */
@ -219,6 +583,31 @@ export const zHttpValidationError = z.object({
detail: z.optional(z.array(zValidationError)), detail: z.optional(z.array(zValidationError)),
}); });
/**
* ProductImageSchema
*/
export const zProductImageSchema = z.object({
id: z.int(),
productId: z.int(),
imageUrl: z.string(),
});
/**
* ProductServicesDuplicateRequest
*/
export const zProductServicesDuplicateRequest = z.object({
dealId: z.int(),
sourceDealProductId: z.int(),
targetDealProductIds: z.array(z.int()),
});
/**
* ProductServicesDuplicateResponse
*/
export const zProductServicesDuplicateResponse = z.object({
message: z.string(),
});
export const zSortDir = z.enum(["asc", "desc"]); export const zSortDir = z.enum(["asc", "desc"]);
/** /**
@ -243,6 +632,28 @@ export const zUpdateBoardResponse = z.object({
message: z.string(), message: z.string(),
}); });
/**
* UpdateDealProductSchema
*/
export const zUpdateDealProductSchema = z.object({
quantity: z.int(),
comment: z.string(),
});
/**
* UpdateDealProductRequest
*/
export const zUpdateDealProductRequest = z.object({
entity: zUpdateDealProductSchema,
});
/**
* UpdateDealProductResponse
*/
export const zUpdateDealProductResponse = z.object({
message: z.string(),
});
/** /**
* UpdateDealSchema * UpdateDealSchema
*/ */
@ -267,11 +678,86 @@ export const zUpdateDealResponse = z.object({
message: z.string(), message: z.string(),
}); });
/**
* UpdateDealServiceSchema
*/
export const zUpdateDealServiceSchema = z.object({
quantity: z.int(),
price: z.number(),
isFixedPrice: z.boolean(),
});
/**
* UpdateDealServiceRequest
*/
export const zUpdateDealServiceRequest = z.object({
entity: zUpdateDealServiceSchema,
});
/**
* UpdateDealServiceResponse
*/
export const zUpdateDealServiceResponse = z.object({
message: z.string(),
});
/**
* UpdateProductSchema
*/
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()])),
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()])),
images: z.optional(z.union([z.array(zProductImageSchema), z.null()])),
});
/**
* UpdateProductRequest
*/
export const zUpdateProductRequest = z.object({
entity: zUpdateProductSchema,
});
/**
* UpdateProductResponse
*/
export const zUpdateProductResponse = z.object({
message: z.string(),
});
/**
* UpdateProductServiceSchema
*/
export const zUpdateProductServiceSchema = z.object({
price: z.number(),
isFixedPrice: z.boolean(),
});
/**
* UpdateProductServiceRequest
*/
export const zUpdateProductServiceRequest = z.object({
entity: zUpdateProductServiceSchema,
});
/**
* UpdateProductServiceResponse
*/
export const zUpdateProductServiceResponse = z.object({
message: z.string(),
});
/** /**
* UpdateProjectSchema * UpdateProjectSchema
*/ */
export const zUpdateProjectSchema = z.object({ export const zUpdateProjectSchema = z.object({
name: z.optional(z.union([z.string(), z.null()])), name: z.optional(z.union([z.string(), z.null()])),
builtInModules: z.optional(z.array(zBuiltInModuleSchema)),
}); });
/** /**
@ -288,6 +774,57 @@ export const zUpdateProjectResponse = z.object({
message: z.string(), message: z.string(),
}); });
/**
* UpdateServiceSchema
*/
export const zUpdateServiceSchema = z.object({
id: z.int(),
name: z.string(),
category: zServiceCategorySchema,
price: z.number(),
serviceType: z.int(),
priceRanges: z.array(zServicePriceRangeSchema),
cost: z.union([z.number(), z.null()]),
lexorank: z.string(),
});
/**
* UpdateServiceRequest
*/
export const zUpdateServiceRequest = z.object({
entity: zUpdateServiceSchema,
});
/**
* UpdateServiceResponse
*/
export const zUpdateServiceResponse = z.object({
message: z.string(),
});
/**
* UpdateServicesKitSchema
*/
export const zUpdateServicesKitSchema = z.object({
name: z.string(),
serviceType: z.int(),
servicesIds: z.array(z.int()),
});
/**
* UpdateServicesKitRequest
*/
export const zUpdateServicesKitRequest = z.object({
entity: zUpdateServicesKitSchema,
});
/**
* UpdateServicesKitResponse
*/
export const zUpdateServicesKitResponse = z.object({
message: z.string(),
});
/** /**
* UpdateStatusSchema * UpdateStatusSchema
*/ */
@ -420,6 +957,17 @@ export const zUpdateDealData = z.object({
*/ */
export const zUpdateDealResponse2 = zUpdateDealResponse; export const zUpdateDealResponse2 = zUpdateDealResponse;
export const zGetBuiltInModulesData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zGetBuiltInModulesResponse = zGetAllBuiltInModulesResponse;
export const zGetProjectsData = z.object({ export const zGetProjectsData = z.object({
body: z.optional(z.never()), body: z.optional(z.never()),
path: z.optional(z.never()), path: z.optional(z.never()),
@ -517,3 +1065,321 @@ export const zUpdateStatusData = z.object({
* Successful Response * Successful Response
*/ */
export const zUpdateStatusResponse2 = zUpdateStatusResponse; export const zUpdateStatusResponse2 = zUpdateStatusResponse;
export const zGetDealProductsData = z.object({
body: z.optional(z.never()),
path: z.object({
dealId: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zGetDealProductsResponse2 = zGetDealProductsResponse;
export const zCreateDealProductData = z.object({
body: zCreateDealProductRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zCreateDealProductResponse2 = zCreateDealProductResponse;
export const zDeleteDealProductData = z.object({
body: z.optional(z.never()),
path: z.object({
dealId: z.int(),
productId: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zDeleteDealProductResponse2 = zDeleteDealProductResponse;
export const zUpdateDealProductData = z.object({
body: zUpdateDealProductRequest,
path: z.object({
dealId: z.int(),
productId: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zUpdateDealProductResponse2 = zUpdateDealProductResponse;
export const zAddKitToDealProductData = z.object({
body: zDealProductAddKitRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zAddKitToDealProductResponse = zDealProductAddKitResponse;
export const zCreateDealProductServiceData = z.object({
body: zCreateProductServiceRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zCreateDealProductServiceResponse = zCreateProductServiceResponse;
export const zDeleteDealProductServiceData = z.object({
body: z.optional(z.never()),
path: z.object({
dealId: z.int(),
productId: z.int(),
serviceId: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zDeleteDealProductServiceResponse = zDeleteProductServiceResponse;
export const zUpdateDealProductServiceData = z.object({
body: zUpdateProductServiceRequest,
path: z.object({
dealId: z.int(),
productId: z.int(),
serviceId: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zUpdateDealProductServiceResponse = zUpdateProductServiceResponse;
export const zDuplicateProductServicesData = z.object({
body: zProductServicesDuplicateRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zDuplicateProductServicesResponse =
zProductServicesDuplicateResponse;
export const zGetDealServicesData = z.object({
body: z.optional(z.never()),
path: z.object({
dealId: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zGetDealServicesResponse2 = zGetDealServicesResponse;
export const zCreateDealServiceData = z.object({
body: zCreateDealServiceRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zCreateDealServiceResponse2 = zCreateDealServiceResponse;
export const zDeleteDealServiceData = z.object({
body: z.optional(z.never()),
path: z.object({
dealId: z.int(),
serviceId: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zDeleteDealServiceResponse2 = zDeleteDealServiceResponse;
export const zUpdateDealServiceData = z.object({
body: zUpdateDealServiceRequest,
path: z.object({
dealId: z.int(),
serviceId: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zUpdateDealServiceResponse2 = zUpdateDealServiceResponse;
export const zGetProductsData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.optional(
z.object({
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()])),
})
),
});
/**
* Successful Response
*/
export const zGetProductsResponse2 = zGetProductsResponse;
export const zCreateProductData = z.object({
body: zCreateProductRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zCreateProductResponse2 = zCreateProductResponse;
export const zDeleteProductData = z.object({
body: z.optional(z.never()),
path: z.object({
pk: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zDeleteProductResponse2 = zDeleteProductResponse;
export const zUpdateProductData = z.object({
body: zUpdateProductRequest,
path: z.object({
pk: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zUpdateProductResponse2 = zUpdateProductResponse;
export const zGetServicesData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zGetServicesResponse2 = zGetServicesResponse;
export const zCreateServiceData = z.object({
body: zCreateServiceRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zCreateServiceResponse2 = zCreateServiceResponse;
export const zDeleteServiceData = z.object({
body: z.optional(z.never()),
path: z.object({
pk: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zDeleteServiceResponse2 = zDeleteServiceResponse;
export const zUpdateServiceData = z.object({
body: zUpdateServiceRequest,
path: z.object({
pk: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zUpdateServiceResponse2 = zUpdateServiceResponse;
export const zGetServicesKitsData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zGetServicesKitsResponse = zGetServicesKitResponse;
export const zCreateServicesKitData = z.object({
body: zCreateServicesKitRequest,
path: z.optional(z.never()),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zCreateServicesKitResponse2 = zCreateServicesKitResponse;
export const zDeleteServicesKitData = z.object({
body: z.optional(z.never()),
path: z.object({
pk: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zDeleteServicesKitResponse2 = zDeleteServicesKitResponse;
export const zUpdateServicesKitData = z.object({
body: zUpdateServicesKitRequest,
path: z.object({
pk: z.int(),
}),
query: z.optional(z.never()),
});
/**
* Successful Response
*/
export const zUpdateServicesKitResponse2 = zUpdateServicesKitResponse;

View File

@ -0,0 +1,63 @@
import { ReactNode } from "react";
import { Flex } from "@mantine/core";
import { UseFormReturnType } from "@mantine/form";
import BaseFormModalActions from "@/modals/base/BaseFormModal/BaseFormModalActions";
export type CreateProps<TCreate> = {
onCreate: (values: TCreate) => void;
isEditing: false;
};
export type EditProps<TEntity, TUpdate> = {
onChange: (values: TUpdate) => void;
entity: TEntity;
isEditing: true;
};
export type CreateEditFormProps<
TCreate,
TUpdate = TCreate,
TEntity = TUpdate,
> = CreateProps<TCreate> | EditProps<TEntity, TUpdate>;
export type BaseFormProps<T> = {
form: UseFormReturnType<Partial<T>>;
onClose: () => void;
closeOnSubmit?: boolean;
children: ReactNode;
};
type Props<TCreate, TUpdate, TEntity> = BaseFormProps<TEntity> &
CreateEditFormProps<TCreate, TUpdate, TEntity>;
const BaseFormModal = <TCreate, TUpdate = TCreate, TEntity = TUpdate>(
props: Props<TCreate, TUpdate, TEntity>
) => {
const { closeOnSubmit = false } = props;
const onSubmit = (values: Partial<TEntity>) => {
if (props.isEditing) {
props.onChange({ ...props.entity, ...values } as TUpdate);
} else {
props.onCreate(values as TCreate);
}
if (closeOnSubmit) {
props.form.reset();
props.onClose();
}
};
return (
<form onSubmit={props.form.onSubmit(values => onSubmit(values))}>
<Flex
gap={"xs"}
direction={"column"}>
{props.children}
<BaseFormModalActions {...props} />
</Flex>
</form>
);
};
export default BaseFormModal;

View File

@ -0,0 +1,25 @@
import { FC } from "react";
import { Button, Flex } from "@mantine/core";
type Props = {
onClose: () => void;
};
const BaseFormModalActions: FC<Props> = ({ onClose }) => (
<Flex
justify={"flex-end"}
gap={"xs"}>
<Button
variant={"subtle"}
onClick={onClose}>
Отменить
</Button>
<Button
type={"submit"}
variant={"default"}>
Сохранить
</Button>
</Flex>
);
export default BaseFormModalActions;

View File

@ -2,10 +2,22 @@ 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 EnterNameModal from "@/modals/EnterNameModal/EnterNameModal"; import EnterNameModal from "@/modals/EnterNameModal/EnterNameModal";
import DealProductEditorModal from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/modals/DealProductEditorModal/DealProductEditorModal";
import DealServiceEditorModal from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/modals/DealServiceEditorModal/DealServiceEditorModal";
import DuplicateServicesModal from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/modals/DuplicateServicesModal/DuplicateServicesModal";
import ProductEditorModal from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/modals/ProductEditorModal/ProductEditorModal";
import ProductServiceEditorModal from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/modals/ProductServiceEditorModal/ProductServiceEditorModal";
import ServicesKitSelectModal from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/modals/ServicesKitSelectModal/ServicesKitSelectModal";
export const modals = { export const modals = {
enterNameModal: EnterNameModal, enterNameModal: EnterNameModal,
dealsTableFiltersModal: DealsTableFiltersModal, dealsTableFiltersModal: DealsTableFiltersModal,
dealsBoardFiltersModal: DealsBoardFiltersModal, dealsBoardFiltersModal: DealsBoardFiltersModal,
dealsScheduleFiltersModal: DealsScheduleFiltersModal, dealsScheduleFiltersModal: DealsScheduleFiltersModal,
productEditorModal: ProductEditorModal,
dealProductEditorModal: DealProductEditorModal,
dealServiceEditorModal: DealServiceEditorModal,
productServiceEditorModal: ProductServiceEditorModal,
duplicateServicesModal: DuplicateServicesModal,
servicesKitSelectModal: ServicesKitSelectModal,
}; };

View File

@ -0,0 +1,13 @@
import FulfillmentBaseTab from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/FulfillmentBaseTab";
import { ModuleNames } from "./modules";
import ModulesType from "./types";
const connectModules = (modules: ModulesType) => {
modules[ModuleNames.FULFILLMENT_BASE].getTab = props => (
<FulfillmentBaseTab {...props} />
);
return modules;
};
export default connectModules;

View File

@ -0,0 +1,38 @@
.container {
display: flex;
//flex-direction: column;
gap: rem(10);
max-height: 95vh;
}
.container-disabled {
}
.products-list {
width: 52%;
display: flex;
flex-direction: column;
gap: rem(10);
flex: 2;
}
.card-container {
display: flex;
flex-direction: column;
gap: rem(10);
flex: 1;
}
.card-container-wrapper {
border: dashed var(--item-border-size) var(--mantine-color-default-border);
border-radius: var(--item-border-radius);
padding: rem(10);
}
.card-container-buttons {
gap: rem(10);
display: flex;
flex-direction: column;
margin-top: auto;
width: 100%;
}

View File

@ -0,0 +1,18 @@
import { FC } from "react";
import { DealSchema } from "@/lib/client";
import FulfillmentBaseTabBody from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/FulfillmentBaseTabBody/FulfillmentBaseTabBody";
import { FulfillmentBaseContextProvider } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
type Props = {
value: DealSchema;
};
const FulfillmentBaseTab: FC<Props> = ({ value }) => {
return (
<FulfillmentBaseContextProvider deal={value}>
<FulfillmentBaseTabBody />
</FulfillmentBaseContextProvider>
);
};
export default FulfillmentBaseTab;

View File

@ -0,0 +1,71 @@
import { FC } from "react";
import { Flex, rem } from "@mantine/core";
import DealServiceRow from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/components/DealServiceRow";
import DealServicesTitle from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/components/DealServicesTitle";
import DealServicesTotalLabel from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/components/DealServicesTotalLabel";
import ServicesActions from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/components/ServicesActions";
import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
const DealServicesTable: FC = () => {
const { dealServicesList, dealServicesCrud } = useFulfillmentBaseContext();
// const isLocked = isDealLocked(deal); // TODO bills
// const [currentService, setCurrentService] = useState<
// DealServiceSchema | undefined
// >();
// const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
// const onEmployeeClick = (item: CardServiceSchema) => {
// if (!onChange) return;
// setCurrentService(item);
// setEmployeesModalVisible(true);
// };
// const onEmployeeModalClose = () => {
// setEmployeesModalVisible(false);
// setCurrentService(undefined);
// };
// const getCurrentEmployees = (): UserSchema[] => {
// if (!currentService) return [];
// const item = items.find(
// i => i.service.id === currentService.service.id
// );
// if (!item) return [];
// return item.employees;
// };
// const onEmployeesChange = (items: UserSchema[]) => {
// if (!currentService || !onChange) return;
// debouncedOnChange({
// ...currentService,
// employees: items,
// });
// };
return (
<Flex
direction={"column"}
gap={rem(10)}
h={"100%"}>
<Flex
h={"100%"}
direction={"column"}>
<DealServicesTitle />
<Flex
direction={"column"}
gap={rem(10)}>
{dealServicesList.dealServices.map(dealService => (
<DealServiceRow
key={dealService.service.id}
value={dealService}
onDelete={dealServicesCrud.onDelete}
onChange={dealServicesCrud.onUpdate}
/>
))}
</Flex>
</Flex>
<DealServicesTotalLabel />
<ServicesActions />
</Flex>
);
};
export default DealServicesTable;

View File

@ -0,0 +1,114 @@
import { FC } from "react";
import { IconTrash } from "@tabler/icons-react";
import { isNumber } from "lodash";
import {
ActionIcon,
Divider,
Group,
NumberInput,
Stack,
Text,
Tooltip,
} from "@mantine/core";
import { useDebouncedCallback } from "@mantine/hooks";
import { DealServiceSchema } from "@/lib/client";
import LockCheckbox from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/LockCheckbox/LockCheckbox";
type Props = {
value: DealServiceSchema;
onChange: (
dealId: number,
serviceId: number,
value: DealServiceSchema
) => void;
onDelete: (value: DealServiceSchema) => void;
};
const DealServiceRow: FC<Props> = ({ value, onChange, onDelete }) => {
const debouncedOnChange = useDebouncedCallback(
async (item: DealServiceSchema) => {
onChange(item.dealId, item.serviceId, item);
},
200
);
const onQuantityChange = (item: DealServiceSchema, quantity: number) => {
debouncedOnChange({ ...item, quantity });
};
const onPriceChange = (item: DealServiceSchema, price: number) => {
debouncedOnChange({
...item,
price,
isFixedPrice: true,
});
};
return (
<Stack
w={"100%"}
gap={"xs"}>
<Divider />
<Text>{value.service.name}</Text>
<Group>
<Tooltip
onClick={() => onDelete(value)}
label="Удалить услугу">
<ActionIcon variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
{/*<Tooltip label="Сотрудники">*/}
{/* <ActionIcon*/}
{/* onClick={() => onEmployeeClick(service)}*/}
{/* variant={"default"}>*/}
{/* <IconUsersGroup />*/}
{/* </ActionIcon>*/}
{/*</Tooltip>*/}
<NumberInput
flex={1}
suffix={" шт."}
onChange={event =>
isNumber(event) && onQuantityChange(value, event)
}
value={value.quantity}
min={1}
allowNegative={false}
/>
<NumberInput
flex={1}
onChange={event =>
isNumber(event) && onPriceChange(value, event)
}
suffix={"₽"}
value={value.price}
disabled={value.isFixedPrice}
min={1}
allowNegative={false}
rightSectionProps={{
style: {
display: "flex",
cursor: "pointer",
pointerEvents: "auto",
},
}}
rightSection={
<LockCheckbox
label={"Зафиксировать цену"}
variant={"default"}
value={value.isFixedPrice}
onChange={isFixedPrice =>
debouncedOnChange({
...value,
isFixedPrice,
})
}
/>
}
/>
</Group>
</Stack>
);
};
export default DealServiceRow;

View File

@ -0,0 +1,12 @@
import { rem, Title } from "@mantine/core";
const DealServicesTitle = () => (
<Title
order={3}
w={"100%"}
mb={rem(10)}>
Общие услуги
</Title>
);
export default DealServicesTitle;

View File

@ -0,0 +1,26 @@
import { useMemo } from "react";
import { rem, Title } from "@mantine/core";
import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
const DealServicesTotalLabel = () => {
const { dealServicesList } = useFulfillmentBaseContext();
const total = useMemo(
() =>
dealServicesList.dealServices.reduce(
(acc, item) => acc + item.price * item.quantity,
0
),
[dealServicesList.dealServices]
);
return (
<Title
style={{ textAlign: "end" }}
mt={rem(10)}
order={3}>
Итог: {total}
</Title>
);
};
export default DealServicesTotalLabel;

View File

@ -0,0 +1,58 @@
import { Button, Flex, rem } from "@mantine/core";
import { modals } from "@mantine/modals";
import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
const ServicesActions = () => {
const { dealServicesList, dealServicesCrud, deal } =
useFulfillmentBaseContext();
const onCreateClick = () => {
const serviceIdsToExclude = dealServicesList.dealServices.map(
service => service.service.id
);
modals.openContextModal({
modal: "dealServiceEditorModal",
innerProps: {
onCreate: values =>
dealServicesCrud.onCreate({ ...values, dealId: deal.id }),
serviceIdsToExclude,
isEditing: false,
},
withCloseButton: false,
});
};
// const onAddKitClick = () => {
// if (!onKitAdd) return;
// modals.openContextModal({
// modal: "servicesKitSelectModal",
// innerProps: {
// onSelect: onKitAdd,
// serviceType: ServiceType.DEAL_SERVICE,
// },
// withCloseButton: false,
// });
// };
return (
<Flex
direction={"column"}
gap={rem(10)}
mt={"auto"}>
<Button
onClick={onCreateClick}
fullWidth
variant={"default"}>
Добавить услугу
</Button>
{/*<Button*/}
{/* onClick={onAddKitClick}*/}
{/* fullWidth*/}
{/* variant={"default"}>*/}
{/* Добавить набор услуг*/}
{/*</Button>*/}
</Flex>
);
};
export default ServicesActions;

View File

@ -0,0 +1,34 @@
import { Flex, ScrollArea, Stack } from "@mantine/core";
import DealServicesTable from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/DealServicesTable/DealServicesTable";
import ProductsActions from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductsActions/ProductsActions";
import ProductView from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductView/ProductView";
import { useFulfillmentBaseContext } from "../../contexts/FulfillmentBaseContext";
const FulfillmentBaseTabBody = () => {
const { dealProductsList } = useFulfillmentBaseContext();
return (
<Flex
p={"md"}
gap={"xs"}>
<ScrollArea
offsetScrollbars={"y"}
mah={"91vh"}>
<Stack>
{dealProductsList.dealProducts.map((dealProduct, index) => (
<ProductView
dealProduct={dealProduct}
key={index}
/>
))}
</Stack>
</ScrollArea>
<Stack>
<DealServicesTable />
<ProductsActions />
</Stack>
</Flex>
);
};
export default FulfillmentBaseTabBody;

View File

@ -0,0 +1,115 @@
import ShippingWarehouseAutocomplete
from "../../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
import { CardService, ShippingWarehouseSchema } from "../../../../../../client";
import { useForm } from "@mantine/form";
import { useCardPageContext } from "../../../../../../pages/CardsPage/contexts/CardPageContext.tsx";
import { Button, Checkbox, Stack } from "@mantine/core";
import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { useEffect, useState } from "react";
import { isEqual } from "lodash";
import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts";
type GeneralDataFormType = {
shippingWarehouse?: ShippingWarehouseSchema | null | string;
isServicesProfitAccounted: boolean;
}
const GeneralDataForm = () => {
const { selectedCard: card, refetchCard } = useCardPageContext();
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
if (!card) return <></>;
const [initialValues, setInitialValues] = useState<GeneralDataFormType>(card);
const form = useForm<GeneralDataFormType>({
initialValues,
});
useEffect(() => {
const data = card ?? {};
setInitialValues(data);
form.setValues(data);
}, [card]);
const isShippingWarehouse = (
value: ShippingWarehouseSchema | string | null | undefined,
): value is ShippingWarehouseSchema => {
return !!value && !["string"].includes(typeof value);
};
const onSubmit = (values: GeneralDataFormType) => {
if (!card) return;
const shippingWarehouse = isShippingWarehouse(values.shippingWarehouse)
? values.shippingWarehouse.name
: values.shippingWarehouse;
CardService.updateProductsAndServicesGeneralInfo({
requestBody: {
cardId: card.id,
data: {
...values,
shippingWarehouse,
},
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
refetchCard();
})
.catch(err => console.log(err));
};
return (
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
<Stack>
<ShippingWarehouseAutocomplete
placeholder={isDealsViewer ? "" : "Введите склад отгрузки"}
label={"Склад отгрузки"}
value={
isShippingWarehouse(
form.values.shippingWarehouse,
)
? form.values.shippingWarehouse
: undefined
}
onChange={event => {
if (isShippingWarehouse(event)) {
form.getInputProps(
"shippingWarehouse",
).onChange(event.name);
return;
}
form.getInputProps(
"shippingWarehouse",
).onChange(event);
}}
readOnly={isDealsViewer}
/>
{!isDealsViewer && (
<>
<Checkbox
label={"Учет выручки в статистике"}
{...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
/>
<Button
type={"submit"}
variant={"default"}
disabled={isEqual(initialValues, form.values)}
>
Сохранить
</Button>
</>
)}
</Stack>
</form>
);
};
export default GeneralDataForm;

View File

@ -0,0 +1,28 @@
import { FC } from "react";
import { IconLock, IconLockOpen } from "@tabler/icons-react";
import { ActionIcon, CheckboxProps, Tooltip } from "@mantine/core";
type RestProps = {
value: boolean;
onChange: (value: boolean) => void;
};
type Props = Omit<CheckboxProps, "value" | "onChange"> & RestProps;
const LockCheckbox: FC<Props> = props => {
const getIcon = () => (props.value ? <IconLock /> : <IconLockOpen />);
const handleChange = () => props.onChange(!props.value);
return (
<Tooltip label={props.label}>
<ActionIcon
onClick={handleChange}
variant={props.variant}>
{getIcon()}
</ActionIcon>
</Tooltip>
);
};
export default LockCheckbox;

View File

@ -0,0 +1,68 @@
import { CardSchema } from "../../../../../../client";
import ButtonCopy from "../../../../../../components/ButtonCopy/ButtonCopy.tsx";
import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
import { getCurrentDateTimeForFilename } from "../../../../../../shared/lib/date.ts";
import FileSaver from "file-saver";
import { Button, Popover, Stack } from "@mantine/core";
type Props = {
card: CardSchema;
}
const PaymentLinkButton = ({ card }: Props) => {
if ((!card.billRequests || card.billRequests.length === 0) && (!card?.group?.billRequests || card?.group?.billRequests.length === 0)) {
return (
<ButtonCopyControlled
onCopyClick={() => {
const date =
getCurrentDateTimeForFilename();
FileSaver.saveAs(
`${import.meta.env.VITE_API_URL}/card/billing-document/${card.id}`,
`bill_${card.id}_${date}.pdf`,
);
}}
copied={false}
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
>
Ссылка на оплату (PDF)
</ButtonCopyControlled>
);
}
const requests = (card?.group ? card?.group?.billRequests : card.billRequests) ?? [];
const urls = requests.map(request => request.pdfUrl).filter(url => url !== null);
if (urls.length === 1) {
return (
<ButtonCopy
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
value={urls[0]}
>
Ссылка на оплату
</ButtonCopy>
);
}
return (
<Popover width={380} position="bottom" withArrow shadow="md">
<Popover.Target>
<Button variant={"default"}>Ссылки на оплату</Button>
</Popover.Target>
<Popover.Dropdown>
<Stack gap={"md"}>
{urls.map((url, i) => (
<ButtonCopy
key={i}
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
value={url}
>
{`Ссылка на оплату (часть ${String(i + 1)})`}
</ButtonCopy>
))}
</Stack>
</Popover.Dropdown>
</Popover>
);
};
export default PaymentLinkButton;

View File

@ -0,0 +1,63 @@
import { ActionIcon, Group, Tooltip } from "@mantine/core";
import styles from "../../../../../../pages/CardsPage/ui/CardsPage.module.css";
import { CardSchema, CardService } from "../../../../../../client";
import { base64ToBlob } from "../../../../../../shared/lib/utils.ts";
import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { IconBarcode, IconPrinter } from "@tabler/icons-react";
type Props = {
card: CardSchema;
}
const PrintDealBarcodesButton = ({ card }: Props) => {
return (
<Group wrap={"nowrap"}>
<Tooltip
className={styles["print-deals-button"]}
label={"Распечатать штрихкоды сделки"}
>
<ActionIcon
onClick={async () => {
const response =
await CardService.getCardProductsBarcodesPdf({
requestBody: {
cardId: card.id,
},
});
const pdfBlob = base64ToBlob(
response.base64String,
response.mimeType,
);
const pdfUrl = URL.createObjectURL(pdfBlob);
const pdfWindow = window.open(pdfUrl);
if (!pdfWindow) {
notifications.error({ message: "Ошибка" });
return;
}
pdfWindow.onload = () => {
pdfWindow.print();
};
}}
variant={"default"}>
<IconBarcode />
</ActionIcon>
</Tooltip>
<Tooltip label={"Распечатать сделку"}>
<ActionIcon
onClick={() => {
const pdfWindow = window.open(
`${import.meta.env.VITE_API_URL}/card/tech-spec/${card.id}`,
);
if (!pdfWindow) return;
pdfWindow.print();
}}
variant={"default"}>
<IconPrinter />
</ActionIcon>
</Tooltip>
</Group>
);
};
export default PrintDealBarcodesButton;

View File

@ -0,0 +1,68 @@
import { FC } from "react";
import { DropzoneProps, FileWithPath } from "@mantine/dropzone";
import ImageDropzone from "@/components/ui/ImageDropzone/ImageDropzone";
import { notifications } from "@/lib/notifications";
import BaseFormInputProps from "@/utils/baseFormInputProps";
import useImageDropzone from "./useImageDropzone";
interface RestProps {
imageUrlInputProps?: BaseFormInputProps<string>;
productId?: number;
}
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
const ProductImageDropzone: FC<Props> = ({
imageUrlInputProps,
productId,
}: Props) => {
const imageDropzoneProps = useImageDropzone({
imageUrlInputProps,
});
const onDrop = (files: FileWithPath[]) => {
if (!productId || !imageUrlInputProps) return;
if (files.length > 1) {
notifications.error({ message: "Прикрепите одно изображение" });
return;
}
const { setIsLoading, setShowDropzone } = imageDropzoneProps;
const file = files[0];
setIsLoading(true);
// TODO SEND REQUEST
// ProductService.uploadProductImage({
// productId,
// formData: {
// upload_file: file,
// },
// })
// .then(({ ok, message, imageUrl }) => {
// notifications.guess(ok, { message });
// setIsLoading(false);
//
// if (!ok || !imageUrl) {
// setShowDropzone(true);
// return;
// }
// imageUrlInputProps?.onChange(imageUrl);
// setShowDropzone(false);
// })
// .catch(error => {
// notifications.error({ message: error.toString() });
// setShowDropzone(true);
// setIsLoading(false);
// });
};
return (
<ImageDropzone
onDrop={onDrop}
imageDropzone={imageDropzoneProps}
/>
);
};
export default ProductImageDropzone;

View File

@ -0,0 +1,27 @@
import { useState } from "react";
import BaseFormInputProps from "@/utils/baseFormInputProps";
type Props = {
imageUrlInputProps?: BaseFormInputProps<string>;
};
const useImageDropzone = ({ imageUrlInputProps }: Props) => {
const [showDropzone, setShowDropzone] = useState(
!(
typeof imageUrlInputProps?.value === "string" &&
imageUrlInputProps.value.trim() !== ""
)
);
const [isLoading, setIsLoading] = useState(false);
return {
showDropzone,
setShowDropzone,
isLoading,
setIsLoading,
imageUrlInputProps,
};
};
export default useImageDropzone;

View File

@ -0,0 +1,55 @@
import { FC, useState } from "react";
import { omit } from "lodash";
import { Loader, OptionsFilter } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import ObjectSelect, {
ObjectSelectProps,
} from "@/components/selects/ObjectSelect/ObjectSelect";
import { ProductSchema } from "@/lib/client";
import useProductsList from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/hooks/lists/useProductsList";
import renderProductOption from "./utils/renderProductOption";
type RestProps = {
clientId: number;
};
const MAX_PRODUCTS = 200;
type Props = Omit<ObjectSelectProps<ProductSchema>, "data"> & RestProps;
const ProductSelect: FC<Props> = (props: Props) => {
const [searchValue, setSearchValue] = useState("");
const [debounced] = useDebouncedValue(searchValue, 500);
const { products, isLoading } = useProductsList({
// clientId: props.clientId,
searchInput: debounced,
page: 0,
itemsPerPage: MAX_PRODUCTS,
});
const restProps = omit(props, ["clientId"]);
const optionsFilter: OptionsFilter = ({ options }) => options;
const setSearchValueImpl = (value: string) => {
const names = products.map(product => product.name);
if (names.includes(value)) return;
setSearchValue(value);
};
return (
<ObjectSelect
rightSection={
isLoading || searchValue !== debounced ? (
<Loader size={"sm"} />
) : null
}
onSearchChange={setSearchValueImpl}
renderOption={renderProductOption(products)}
searchable
{...restProps}
data={products}
filter={optionsFilter}
/>
);
};
export default ProductSelect;

View File

@ -0,0 +1,51 @@
import {
ComboboxItem,
ComboboxLikeRenderOptionInput,
SelectProps,
Tooltip,
} from "@mantine/core";
import { ProductSchema } from "@/lib/client";
import ProductFieldsList from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductView/components/ProductFieldsList";
const renderProductOption = (
products: ProductSchema[]
): SelectProps["renderOption"] => {
return (item: ComboboxLikeRenderOptionInput<ComboboxItem>) => {
const product = products.find(
product => product.id === Number(item.option.value)
);
if (!product) return item.option.label;
// const imageUrl =
// product.images && product.images[0]
// ? product.images[0].imageUrl
// : undefined;
return (
<Tooltip
style={{ whiteSpace: "pre-line" }}
multiline
label={
<>
<ProductFieldsList product={product} />
{/*{imageUrl && (*/}
{/* <Image*/}
{/* src={imageUrl}*/}
{/* alt={product.name}*/}
{/* maw={rem(250)}*/}
{/* />*/}
{/*)}*/}
</>
}>
<div>
{product.name}
<br />
{/*{product.barcodes && (*/}
{/* <Text size={"xs"}>{product.barcodes[0]}</Text>*/}
{/*)}*/}
</div>
</Tooltip>
);
};
};
export default renderProductOption;

View File

@ -0,0 +1,36 @@
.container {
display: flex;
border: dashed var(--item-border-size) var(--mantine-color-default-border);
border-radius: var(--item-border-radius);
gap: rem(20);
padding: rem(10);
margin-bottom: rem(10);
flex: 1;
}
.image-container {
display: flex;
max-height: rem(250);
max-width: rem(250);
height: 100%;
}
.services-container {
width: 100%;
display: flex;
flex-direction: column;
gap: rem(10);
flex: 1;
}
.data-container {
max-width: rem(250);
display: flex;
flex-direction: column;
gap: rem(10);
flex: 1;
}
.attributes-container {
overflow-wrap: break-word;
}

View File

@ -0,0 +1,156 @@
import { FC } from "react";
import { isNumber } from "lodash";
import {
Box,
Card,
Image,
NumberInput,
rem,
Stack,
Textarea,
Title,
} from "@mantine/core";
import { useDebouncedCallback } from "@mantine/hooks";
import { modals } from "@mantine/modals";
import {
addKitToDealProduct,
DealProductSchema,
duplicateProductServices,
ServicesKitSchema,
} from "@/lib/client";
import ProductFieldsList from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductView/components/ProductFieldsList";
import ProductViewActions from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductView/components/ProductViewActions";
import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
import { ServiceType } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/types/service";
import ProductServicesTable from "./components/ProductServicesTable";
type Props = {
dealProduct: DealProductSchema;
};
const ProductView: FC<Props> = ({ dealProduct }) => {
const { dealProductsCrud, deal, dealProductsList } =
useFulfillmentBaseContext();
const debouncedOnChange = useDebouncedCallback(
(newValues: Partial<DealProductSchema>) => {
dealProductsCrud.onUpdate(
dealProduct.dealId,
dealProduct.productId,
{
...dealProduct,
...newValues,
}
);
},
200
);
const duplicateServices = (
sourceDealProduct: DealProductSchema,
targetDealProducts: DealProductSchema[]
) => {
duplicateProductServices({
body: {
dealId: deal.id,
sourceDealProductId: sourceDealProduct.productId,
targetDealProductIds: targetDealProducts.map(p => p.productId),
},
})
.then(() => dealProductsList.refetch())
.catch(err => console.error(err));
};
const onDuplicateServices = (sourceDealProduct: DealProductSchema) => {
modals.openContextModal({
modal: "duplicateServicesModal",
title: "Дублирование услуг",
size: "lg",
innerProps: {
dealProducts: dealProductsList.dealProducts,
sourceDealProduct,
duplicateServices,
},
withCloseButton: false,
});
};
const onServicesKitAdd = (servicesKit: ServicesKitSchema) => {
addKitToDealProduct({
body: {
dealId: dealProduct.dealId,
productId: dealProduct.productId,
kitId: servicesKit.id,
},
})
.then(() => dealProductsList.refetch())
.catch(err => console.error(err));
};
const onAddKitClick = () => {
modals.openContextModal({
modal: "servicesKitSelectModal",
innerProps: {
onSelect: onServicesKitAdd,
serviceType: ServiceType.PRODUCT_SERVICE,
},
withCloseButton: false,
});
};
return (
<Card
p={"sm"}
style={{ display: "flex", flexDirection: "row" }}>
<Stack gap={"sm"}>
<Image
flex={1}
radius={rem(10)}
fit={"cover"}
// src={dealProduct.product.imageUrl}
/>
<Title order={3}>{dealProduct.product.name}</Title>
<ProductFieldsList product={dealProduct.product} />
{/*<Text>*/}
{/* Штрихкоды:*/}
{/*{value.product.barcodes.join(", ")}*/}
{/*</Text>*/}
<Box />
<NumberInput
mt={rem(10)}
suffix={" шт."}
value={dealProduct.quantity}
onChange={quantity =>
isNumber(quantity) && debouncedOnChange({ quantity })
}
placeholder={"Введите количество товара"}
/>
<Textarea
mih={rem(140)}
styles={{
wrapper: { height: "90%" },
input: { height: "90%" },
}}
my={rem(10)}
defaultValue={dealProduct.comment}
onChange={event =>
debouncedOnChange({
comment: event.currentTarget.value,
})
}
placeholder={"Комментарий"}
/>
</Stack>
<Stack>
<ProductServicesTable
dealProduct={dealProduct}
onDuplicateServices={() => onDuplicateServices(dealProduct)}
onKitAdd={onAddKitClick}
/>
<ProductViewActions dealProduct={dealProduct} />
</Stack>
</Card>
);
};
export default ProductView;

View File

@ -0,0 +1,37 @@
import { FC } from "react";
import { isNil } from "lodash";
import { Text } from "@mantine/core";
import { ProductSchema } from "@/lib/client";
type ProductFieldNames = {
[K in keyof ProductSchema]: string;
};
export const ProductFieldNames: Partial<ProductFieldNames> = {
color: "Цвет",
article: "Артикул",
size: "Размер",
brand: "Бренд",
composition: "Состав",
additionalInfo: "Доп. информация",
};
type Props = {
product: ProductSchema;
};
const ProductFieldsList: FC<Props> = ({ product }) => {
const fieldList = Object.entries(product).map(([key, value]) => {
const fieldName = ProductFieldNames[key as keyof ProductSchema];
if (!fieldName || isNil(value) || value === "") return null;
return (
<Text key={fieldName}>
{fieldName}: {value.toString()}{" "}
</Text>
);
});
return <>{fieldList}</>;
};
export default ProductFieldsList;

View File

@ -0,0 +1,146 @@
import { FC } from "react";
import { IconMoodSad } from "@tabler/icons-react";
import { Button, Flex, Group, Stack, Text } from "@mantine/core";
import { modals } from "@mantine/modals";
import BaseTable from "@/components/ui/BaseTable/BaseTable";
import { DealProductSchema, ProductServiceSchema } from "@/lib/client";
import useProductServicesTableColumns from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductView/hooks/useProductServicesTableColumns";
import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
type Props = {
dealProduct: DealProductSchema;
onDuplicateServices?: () => void;
onKitAdd?: () => void;
};
const ProductServicesTable: FC<Props> = ({
dealProduct,
onDuplicateServices,
onKitAdd,
}) => {
const { productServiceCrud } = useFulfillmentBaseContext();
const onChange = (item: ProductServiceSchema) => {
const excludeServiceIds = dealProduct.productServices.map(
productService => productService.service.id
);
modals.openContextModal({
modal: "productServiceEditorModal",
innerProps: {
entity: item,
onChange: values =>
productServiceCrud.onUpdate(
item.dealId,
item.productId,
item.serviceId,
values
),
excludeServiceIds,
quantity: dealProduct.quantity,
isEditing: true,
},
withCloseButton: false,
});
};
const columns = useProductServicesTableColumns({
data: dealProduct.productServices,
quantity: dealProduct.quantity,
onDelete: productServiceCrud.onDelete,
onChange,
});
// const [currentService, setCurrentService] = useState<
// ProductServiceSchema | undefined
// >();
// const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
const onCreateClick = () => {
const excludeServiceIds = dealProduct.productServices.map(
productService => productService.service.id
);
modals.openContextModal({
modal: "productServiceEditorModal",
innerProps: {
onCreate: values =>
productServiceCrud.onCreate({ ...dealProduct, ...values }),
excludeServiceIds,
quantity: dealProduct.quantity,
isEditing: false,
},
withCloseButton: false,
});
};
// const onEmployeeClick = (item: CardProductServiceSchema) => {
// if (!onChange) return;
// setCurrentService(item);
// setEmployeesModalVisible(true);
// };
// const onEmployeeModalClose = () => {
// setEmployeesModalVisible(false);
// setCurrentService(undefined);
// };
// const getCurrentEmployees = (): UserSchema[] => {
// if (!currentService) return [];
// const item = items.find(
// i => i.service.id === currentService.service.id
// );
// if (!item) return [];
// return item.employees;
// };
// const onEmployeesChange = (items: UserSchema[]) => {
// if (!currentService || !onChange) return;
// onChange({
// ...currentService,
// employees: items,
// });
// };
const isEmptyTable = dealProduct.productServices.length === 0;
return (
<Stack gap={0}>
<BaseTable
records={dealProduct.productServices}
columns={columns}
groups={undefined}
idAccessor={"serviceId"}
style={{
height: isEmptyTable ? "8rem" : "auto",
}}
emptyState={
<Group
gap={"xs"}
mt={isEmptyTable ? "xl" : 0}>
<Text>Нет услуг</Text>
<IconMoodSad />
</Group>
}
/>
<Flex
justify={"flex-end"}
gap={"xs"}
pt={isEmptyTable ? 0 : "xs"}>
<Button
onClick={() => onKitAdd && onKitAdd()}
variant={"default"}>
Добавить набор услуг
</Button>
<Button
onClick={() => onDuplicateServices && onDuplicateServices()}
variant={"default"}>
Продублировать услуги
</Button>
<Button
onClick={onCreateClick}
variant={"default"}>
Добавить услугу
</Button>
</Flex>
</Stack>
);
};
export default ProductServicesTable;

View File

@ -0,0 +1,64 @@
import { FC } from "react";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { ActionIcon, Flex, rem, Tooltip } from "@mantine/core";
import { modals } from "@mantine/modals";
import { DealProductSchema } from "@/lib/client";
import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
type Props = {
dealProduct: DealProductSchema;
};
const ProductViewActions: FC<Props> = ({ dealProduct }) => {
const { dealProductsCrud, dealProductsList, productsCrud } =
useFulfillmentBaseContext();
const onProductEditClick = () => {
modals.openContextModal({
modal: "productEditorModal",
title: "Редактирование товара",
withCloseButton: false,
innerProps: {
onChange: values =>
productsCrud.onUpdate(
dealProduct.productId,
values,
dealProductsList.refetch
),
entity: dealProduct.product,
isEditing: true,
},
});
};
return (
<Flex
mt={"auto"}
ml={"auto"}
gap={rem(10)}>
{/*<Tooltip*/}
{/* onClick={onPrintBarcodeClick}*/}
{/* label="Печать штрихкода">*/}
{/* <ActionIcon variant={"default"}>*/}
{/* <IconBarcode />*/}
{/* </ActionIcon>*/}
{/*</Tooltip>*/}
<Tooltip
onClick={onProductEditClick}
label="Редактировать товар">
<ActionIcon variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
<Tooltip
onClick={() => dealProductsCrud.onDelete(dealProduct)}
label="Удалить товар">
<ActionIcon variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
</Flex>
);
};
export default ProductViewActions;

View File

@ -0,0 +1,70 @@
import { useMemo } from "react";
import { DataTableColumn } from "mantine-datatable";
import { ProductServiceSchema } from "@/lib/client";
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
import { IconEdit, IconTrash } from "@tabler/icons-react";
type Props = {
data: ProductServiceSchema[];
quantity: number;
onChange: (dealProductService: ProductServiceSchema) => void;
onDelete: (dealProductService: ProductServiceSchema) => void;
};
const useProductServicesTableColumns = ({ data, quantity, onChange, onDelete }: Props) => {
const totalPrice = useMemo(
() => data.reduce((acc, row) => acc + row.price * quantity, 0),
[data, quantity]
);
return useMemo(
() =>
[
{
accessor: "actions",
title: "Действия",
textAlign: "center",
width: "0%",
render: dealProductService => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() => onDelete(dealProductService)}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onChange(dealProductService)}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
{/*<Tooltip label="Сотрудники">*/}
{/* <ActionIcon*/}
{/* onClick={() =>*/}
{/* onEmployeeClick(row.original)*/}
{/* }*/}
{/* variant={"default"}>*/}
{/* <IconUsersGroup />*/}
{/* </ActionIcon>*/}
{/*</Tooltip>*/}
</Flex>
),
},
{
accessor: "service.name",
title: "Услуга",
},
{
accessor: "price",
title: "Цена",
Footer: () => <>Итог: {totalPrice.toLocaleString("ru")}</>,
},
] as DataTableColumn<ProductServiceSchema>[],
[totalPrice]
);
};
export default useProductServicesTableColumns;

View File

@ -0,0 +1,60 @@
import { FC } from "react";
import { Button, Flex } from "@mantine/core";
import { modals } from "@mantine/modals";
import { useFulfillmentBaseContext } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/contexts/FulfillmentBaseContext";
const ProductsActions: FC = () => {
const { deal, dealProductsList, productsCrud, dealProductsCrud } =
useFulfillmentBaseContext();
const onCreateProductClick = () => {
modals.openContextModal({
modal: "productEditorModal",
title: "Создание товара",
withCloseButton: false,
innerProps: {
onCreate: productsCrud.onCreate,
isEditing: false,
},
});
};
const onCreateDealProductClick = () => {
const productIdsToExclude = dealProductsList.dealProducts.map(
product => product.product.id
);
modals.openContextModal({
modal: "dealProductEditorModal",
title: "Добавление товара",
withCloseButton: false,
innerProps: {
onCreate: values =>
dealProductsCrud.onCreate({ ...values, dealId: deal.id }),
productIdsToExclude,
isEditing: false,
clientId: 0, // TODO add clients
},
});
};
return (
<Flex
w={"100%"}
direction={"column"}
gap={"sm"}>
<Button
variant={"default"}
onClick={onCreateProductClick}>
Создать товар
</Button>
<Button
variant={"default"}
onClick={onCreateDealProductClick}>
Добавить товар
</Button>
</Flex>
);
};
export default ProductsActions;

View File

@ -0,0 +1,51 @@
import { FC } from "react";
import { omit } from "lodash";
import {
ComboboxItem,
ComboboxParsedItemGroup,
OptionsFilter,
} from "@mantine/core";
import ObjectSelect, {
ObjectSelectProps,
} from "@/components/selects/ObjectSelect/ObjectSelect";
import { ServiceSchema } from "@/lib/client";
import useServicesList from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/hooks/lists/useServicesList";
import { ServiceType } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/types/service";
type RestProps = {
filterType?: ServiceType;
};
type Props = Omit<ObjectSelectProps<ServiceSchema>, "data"> & RestProps;
const ServiceSelect: FC<Props> = props => {
const { services } = useServicesList();
const data = props.filterType
? services.filter(service => service.serviceType === props.filterType)
: services;
const restProps = omit(props, ["filterType"]);
const optionsFilter: OptionsFilter = ({ options, search }) => {
return (options as ComboboxParsedItemGroup[]).map(option => {
return {
...option,
items: option.items.filter((item: ComboboxItem) =>
item.label.toLowerCase().includes(search.toLowerCase())
),
};
});
};
return (
<ObjectSelect
{...restProps}
data={data}
searchable
groupBy={service => service.category.name}
filter={optionsFilter}
/>
);
};
export default ServiceSelect;

View File

@ -0,0 +1,68 @@
"use client";
import { DealSchema } from "@/lib/client";
import { getProductsQueryKey } from "@/lib/client/@tanstack/react-query.gen";
import makeContext from "@/lib/contextFactory/contextFactory";
import useDealServicesCrud, {
DealServicesCrud,
} from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/hooks/cruds/useDealServiceCrud";
import useProductServiceCrud, {
DealProductServicesCrud,
} from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/hooks/cruds/useProductServiceCrud";
import useDealServicesList, {
DealServicesList,
} from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/hooks/lists/useDealServicesList";
import useDealProductCrud, {
DealProductsCrud,
} from "../hooks/cruds/useDealProductCrud";
import { ProductsCrud, useProductsCrud } from "../hooks/cruds/useProductsCrud";
import useDealProductsList, {
DealProductsList,
} from "../hooks/lists/useDealProductsList";
type FulfillmentBaseContextState = {
deal: DealSchema;
productsCrud: ProductsCrud;
dealProductsList: DealProductsList;
dealProductsCrud: DealProductsCrud;
dealServicesList: DealServicesList;
dealServicesCrud: DealServicesCrud;
productServiceCrud: DealProductServicesCrud;
};
type Props = {
deal: DealSchema;
};
const useFulfillmentBaseContextState = ({
deal,
}: Props): FulfillmentBaseContextState => {
const productQueryKey = getProductsQueryKey();
const productsCrud = useProductsCrud({ queryKey: productQueryKey });
const dealProductsList = useDealProductsList({ dealId: deal.id });
const dealProductsCrud = useDealProductCrud({ dealId: deal.id });
const dealServicesList = useDealServicesList({ dealId: deal.id });
const dealServicesCrud = useDealServicesCrud({ dealId: deal.id });
const productServiceCrud = useProductServiceCrud({
refetchDealProducts: dealProductsList.refetch,
});
return {
deal,
productsCrud,
dealProductsList,
dealProductsCrud,
dealServicesList,
dealServicesCrud,
productServiceCrud,
};
};
export const [FulfillmentBaseContextProvider, useFulfillmentBaseContext] =
makeContext<FulfillmentBaseContextState, Props>(
useFulfillmentBaseContextState,
"FulfillmentBase"
);

View File

@ -0,0 +1,153 @@
import React from "react";
import { useMutation } from "@tanstack/react-query";
import { Text } from "@mantine/core";
import { modals } from "@mantine/modals";
import getCommonQueryClient from "@/hooks/cruds/baseCrud/getCommonQueryClient";
import {
CreateDealProductSchema,
DealProductSchema,
UpdateDealProductSchema,
} from "@/lib/client";
import {
createDealProductMutation,
deleteDealProductMutation,
getDealProductsQueryKey,
updateDealProductMutation,
} from "@/lib/client/@tanstack/react-query.gen";
type Props = {
dealId: number;
};
export type DealProductsCrud = {
onCreate: (data: CreateDealProductSchema) => void;
onUpdate: (
dealId: number,
serviceId: number,
data: UpdateDealProductSchema
) => void;
onDelete: (data: DealProductSchema, onSuccess?: () => void) => void;
};
const useDealProductCrud = ({ dealId }: Props): DealProductsCrud => {
const queryKey = getDealProductsQueryKey({ path: { dealId } });
const key = "getDealProducts";
const { queryClient, onError, onSettled } = getCommonQueryClient({
queryKey,
key,
});
const createMutation = useMutation({
...createDealProductMutation(),
onError,
onSettled,
});
const updateMutation = useMutation({
...updateDealProductMutation(),
onError,
onSettled,
onMutate: async ({
body: { entity: update },
path: { dealId, productId },
}) => {
await queryClient.cancelQueries({ queryKey: [key] });
const previous = queryClient.getQueryData(queryKey);
queryClient.setQueryData(
queryKey,
(old: { items: DealProductSchema[] }) => {
const updated: DealProductSchema[] = old.items.map(
(entity: DealProductSchema) =>
entity.dealId === dealId &&
entity.productId === productId
? { ...entity, ...update }
: entity
);
return {
...old,
items: updated,
};
}
);
return { previous };
},
});
const deleteMutation = useMutation({
...deleteDealProductMutation(),
onError,
onSettled,
onMutate: async ({ path: { dealId, productId } }) => {
await queryClient.cancelQueries({ queryKey: [key] });
const previous = queryClient.getQueryData(queryKey);
queryClient.setQueryData(
queryKey,
(old: { items: DealProductSchema[] }) => {
const filtered = old.items.filter(
e => e.dealId !== dealId && e.productId !== productId
);
return {
...old,
items: filtered,
};
}
);
return { previous };
},
});
const onCreate = (entity: CreateDealProductSchema) => {
createMutation.mutate({
body: {
entity,
},
});
};
const onUpdate = (
dealId: number,
productId: number,
entity: UpdateDealProductSchema
) => {
updateMutation.mutate({
body: {
entity,
},
path: {
dealId,
productId,
},
});
};
const onDelete = (entity: DealProductSchema, onSuccess?: () => void) => {
modals.openConfirmModal({
title: "Удаление товара из сделки",
children: (
<Text>
Вы уверены, что хотите удалить "{entity.product.name}" из
сделки?
</Text>
),
confirmProps: { color: "red" },
onConfirm: () => {
deleteMutation.mutate({ path: entity }, { onSuccess });
},
});
};
return {
onCreate,
onUpdate,
onDelete,
};
};
export default useDealProductCrud;

View File

@ -0,0 +1,153 @@
import React from "react";
import { useMutation } from "@tanstack/react-query";
import { Text } from "@mantine/core";
import { modals } from "@mantine/modals";
import getCommonQueryClient from "@/hooks/cruds/baseCrud/getCommonQueryClient";
import {
CreateDealServiceSchema,
DealServiceSchema,
UpdateDealServiceSchema,
} from "@/lib/client";
import {
createDealServiceMutation,
deleteDealServiceMutation,
getDealServicesQueryKey,
updateDealServiceMutation,
} from "@/lib/client/@tanstack/react-query.gen";
type Props = {
dealId: number;
};
export type DealServicesCrud = {
onCreate: (data: CreateDealServiceSchema) => void;
onUpdate: (
dealId: number,
serviceId: number,
data: UpdateDealServiceSchema
) => void;
onDelete: (data: DealServiceSchema, onSuccess?: () => void) => void;
};
const useDealServiceCrud = ({ dealId }: Props): DealServicesCrud => {
const queryKey = getDealServicesQueryKey({ path: { dealId } });
const key = "getDealServices";
const { queryClient, onError, onSettled } = getCommonQueryClient({
queryKey,
key,
});
const createMutation = useMutation({
...createDealServiceMutation(),
onError,
onSettled,
});
const updateMutation = useMutation({
...updateDealServiceMutation(),
onError,
onSettled,
onMutate: async ({
body: { entity: update },
path: { dealId, serviceId },
}) => {
await queryClient.cancelQueries({ queryKey: [key] });
const previous = queryClient.getQueryData(queryKey);
queryClient.setQueryData(
queryKey,
(old: { items: DealServiceSchema[] }) => {
const updated: DealServiceSchema[] = old.items.map(
(entity: DealServiceSchema) =>
entity.dealId === dealId &&
entity.serviceId === serviceId
? { ...entity, ...update }
: entity
);
return {
...old,
items: updated,
};
}
);
return { previous };
},
});
const deleteMutation = useMutation({
...deleteDealServiceMutation(),
onError,
onSettled,
onMutate: async ({ path: { dealId, serviceId } }) => {
await queryClient.cancelQueries({ queryKey: [key] });
const previous = queryClient.getQueryData(queryKey);
queryClient.setQueryData(
queryKey,
(old: { items: DealServiceSchema[] }) => {
const filtered = old.items.filter(
e => e.dealId !== dealId && e.serviceId !== serviceId
);
return {
...old,
items: filtered,
};
}
);
return { previous };
},
});
const onCreate = (entity: CreateDealServiceSchema) => {
createMutation.mutate({
body: {
entity,
},
});
};
const onUpdate = (
dealId: number,
serviceId: number,
entity: UpdateDealServiceSchema
) => {
updateMutation.mutate({
body: {
entity,
},
path: {
dealId,
serviceId,
},
});
};
const onDelete = (entity: DealServiceSchema, onSuccess?: () => void) => {
modals.openConfirmModal({
title: "Удаление услуги из сделки",
children: (
<Text>
Вы уверены, что хотите удалить "{entity.service.name}" из
сделки?
</Text>
),
confirmProps: { color: "red" },
onConfirm: () => {
deleteMutation.mutate({ path: entity }, { onSuccess });
},
});
};
return {
onCreate,
onUpdate,
onDelete,
};
};
export default useDealServiceCrud;

View File

@ -0,0 +1,102 @@
import { useMutation } from "@tanstack/react-query";
import { AxiosError } from "axios";
import {
CreateProductServiceSchema,
ProductServiceSchema,
HttpValidationError,
UpdateProductServiceSchema,
} from "@/lib/client";
import {
createDealProductServiceMutation,
deleteDealProductServiceMutation,
updateDealProductServiceMutation,
} from "@/lib/client/@tanstack/react-query.gen";
import { notifications } from "@/lib/notifications";
export type DealProductServicesCrud = {
onCreate: (data: CreateProductServiceSchema) => void;
onUpdate: (
dealId: number,
productId: number,
serviceId: number,
data: UpdateProductServiceSchema
) => void;
onDelete: (data: ProductServiceSchema) => void;
};
type Props = {
refetchDealProducts: () => void;
};
const useProductServiceCrud = ({
refetchDealProducts,
}: Props): DealProductServicesCrud => {
const onError = (error: AxiosError<HttpValidationError>, _: any) => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
};
const createMutation = useMutation({
...createDealProductServiceMutation(),
onError,
});
const updateMutation = useMutation({
...updateDealProductServiceMutation(),
onError,
});
const deleteMutation = useMutation({
...deleteDealProductServiceMutation(),
onError,
});
const onCreate = (entity: CreateProductServiceSchema) => {
createMutation.mutate(
{
body: {
entity,
},
},
{ onSuccess: refetchDealProducts }
);
};
const onUpdate = (
dealId: number,
productId: number,
serviceId: number,
entity: UpdateProductServiceSchema
) => {
updateMutation.mutate(
{
body: {
entity,
},
path: {
dealId,
productId,
serviceId,
},
},
{ onSuccess: refetchDealProducts }
);
};
const onDelete = (entity: ProductServiceSchema) => {
deleteMutation.mutate(
{ path: entity },
{ onSuccess: refetchDealProducts }
);
};
return {
onCreate,
onUpdate,
onDelete,
};
};
export default useProductServiceCrud;

View File

@ -0,0 +1,49 @@
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
import {
CreateProductSchema,
ProductSchema,
UpdateProductSchema,
} from "@/lib/client";
import {
createProductMutation,
deleteProductMutation,
updateProductMutation,
} from "@/lib/client/@tanstack/react-query.gen";
type UseProductsProps = {
queryKey: any[];
};
export type ProductsCrud = {
onCreate: (product: CreateProductSchema) => void;
onUpdate: (
productId: number,
product: UpdateProductSchema,
onSuccess?: () => void
) => void;
onDelete: (product: ProductSchema) => void;
};
export const useProductsCrud = ({
queryKey,
}: UseProductsProps): ProductsCrud => {
return useCrudOperations<
ProductSchema,
UpdateProductSchema,
CreateProductSchema
>({
key: "getProducts",
queryKey,
mutations: {
create: createProductMutation(),
update: updateProductMutation(),
delete: deleteProductMutation(),
},
getUpdateEntity: (old, update) =>
({
...old,
...update,
}) as ProductSchema,
getDeleteConfirmTitle: () => "Удаление товара",
});
};

View File

@ -0,0 +1,49 @@
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
import {
CreateServiceSchema,
ServiceSchema,
UpdateServiceSchema,
} from "@/lib/client";
import {
createServiceMutation,
deleteServiceMutation,
updateServiceMutation,
} from "@/lib/client/@tanstack/react-query.gen";
type UseServicesProps = {
queryKey: any[];
};
export type ServicesCrud = {
onCreate: (service: CreateServiceSchema) => void;
onUpdate: (
serviceId: number,
service: UpdateServiceSchema,
onSuccess?: () => void
) => void;
onDelete: (service: ServiceSchema) => void;
};
export const useServicesCrud = ({
queryKey,
}: UseServicesProps): ServicesCrud => {
return useCrudOperations<
ServiceSchema,
UpdateServiceSchema,
CreateServiceSchema
>({
key: "getServices",
queryKey,
mutations: {
create: createServiceMutation(),
update: updateServiceMutation(),
delete: deleteServiceMutation(),
},
getUpdateEntity: (old, update) =>
({
...old,
...update,
}) as ServiceSchema,
getDeleteConfirmTitle: () => "Удаление услуги",
});
};

View File

@ -0,0 +1,49 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { DealProductSchema } from "@/lib/client";
import {
getDealProductsOptions,
getDealProductsQueryKey,
} from "@/lib/client/@tanstack/react-query.gen";
type Props = {
dealId: number;
};
export type DealProductsList = {
dealProducts: DealProductSchema[];
setDealProducts: (dealProducts: DealProductSchema[]) => void;
refetch: () => void;
queryKey: any[];
};
const useDealProductsList = ({ dealId }: Props): DealProductsList => {
const queryClient = useQueryClient();
const options = {
path: { dealId },
};
const { data, refetch } = useQuery({
...getDealProductsOptions(options),
enabled: !!dealId,
});
const queryKey = getDealProductsQueryKey(options);
const setDealProducts = (dealProducts: DealProductSchema[]) => {
queryClient.setQueryData(
queryKey,
(old: { items: DealProductSchema[] }) => ({
...old,
items: dealProducts,
})
);
};
return {
dealProducts: data?.items ?? [],
setDealProducts,
refetch,
queryKey,
};
};
export default useDealProductsList;

View File

@ -0,0 +1,49 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { DealServiceSchema } from "@/lib/client";
import {
getDealServicesOptions,
getDealServicesQueryKey,
} from "@/lib/client/@tanstack/react-query.gen";
type Props = {
dealId: number;
};
export type DealServicesList = {
dealServices: DealServiceSchema[];
setDealServices: (dealServices: DealServiceSchema[]) => void;
refetch: () => void;
queryKey: any[];
};
const useDealServicesList = ({ dealId }: Props): DealServicesList => {
const queryClient = useQueryClient();
const options = {
path: { dealId },
};
const { data, refetch } = useQuery({
...getDealServicesOptions(options),
enabled: !!dealId,
});
const queryKey = getDealServicesQueryKey(options);
const setDealServices = (dealServices: DealServiceSchema[]) => {
queryClient.setQueryData(
queryKey,
(old: { items: DealServiceSchema[] }) => ({
...old,
items: dealServices,
})
);
};
return {
dealServices: data?.items ?? [],
setDealServices,
refetch,
queryKey,
};
};
export default useDealServicesList;

View File

@ -0,0 +1,42 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { ProductSchema } from "@/lib/client";
import {
getProductsOptions,
getProductsQueryKey,
} from "@/lib/client/@tanstack/react-query.gen";
type Props = {
searchInput: string;
page?: number;
itemsPerPage?: number;
};
const useProductsList = ({ searchInput, page, itemsPerPage }: Props) => {
const queryClient = useQueryClient();
const options = {
query: { searchInput, page, itemsPerPage },
};
const { data, refetch, isLoading } = useQuery(getProductsOptions(options));
const queryKey = getProductsQueryKey(options);
const setProducts = (products: ProductSchema[]) => {
queryClient.setQueryData(
queryKey,
(old: { items: ProductSchema[] }) => ({
...old,
items: products,
})
);
};
return {
products: data?.items ?? [],
setProducts,
refetch,
queryKey,
isLoading,
};
};
export default useProductsList;

View File

@ -0,0 +1,33 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { ServicesKitSchema } from "@/lib/client";
import {
getServicesKitsOptions,
getServicesKitsQueryKey,
} from "@/lib/client/@tanstack/react-query.gen";
const useServicesKitsList = () => {
const queryClient = useQueryClient();
const { data, refetch, isLoading } = useQuery(getServicesKitsOptions());
const queryKey = getServicesKitsQueryKey();
const setServicesKits = (servicesKits: ServicesKitSchema[]) => {
queryClient.setQueryData(
queryKey,
(old: { items: ServicesKitSchema[] }) => ({
...old,
items: servicesKits,
})
);
};
return {
servicesKits: data?.items ?? [],
setServicesKits,
refetch,
queryKey,
isLoading,
};
};
export default useServicesKitsList;

View File

@ -0,0 +1,34 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { ServiceSchema } from "@/lib/client";
import {
getServicesOptions,
getServicesQueryKey,
} from "@/lib/client/@tanstack/react-query.gen";
const useServicesList = () => {
const queryClient = useQueryClient();
const { data, refetch, isLoading } = useQuery(getServicesOptions());
const queryKey = getServicesQueryKey();
const setServices = (services: ServiceSchema[]) => {
queryClient.setQueryData(
queryKey,
(old: { items: ServiceSchema[] }) => ({
...old,
items: services,
})
);
};
return {
services: data?.items ?? [],
setServices,
refetch,
queryKey,
isLoading,
};
};
export default useServicesList;

View File

@ -0,0 +1,148 @@
import { CRUDTableProps } from "../../../../../types/CRUDTable.tsx";
import { CardService, CardServiceSchema, CardProductSchema } from "../../../../../client";
import { useCardPageContext } from "../../../../../pages/CardsPage/contexts/CardPageContext.tsx";
import { notifications } from "../../../../../shared/lib/notifications.ts";
const useCardState = () => {
const { selectedCard, setSelectedCard } = useCardPageContext();
const recalculate = async () => {
return CardService.recalculateCardPrice({
requestBody: {
cardId: selectedCard?.id || -1,
},
});
};
const refetchCard = async () => {
if (!selectedCard) return;
return CardService.getCardById({ cardId: selectedCard.id }).then(
async card => {
setSelectedCard(card);
},
);
};
const refetch = async () => {
if (!selectedCard) return;
await refetchCard();
const { ok, message } = await recalculate();
if (!ok) notifications.guess(ok, { message });
await refetchCard();
};
return {
card: selectedCard,
refetch,
};
};
const useCardServicesState = (): CRUDTableProps<CardServiceSchema> => {
const { card, refetch } = useCardState();
const refetchAndRecalculate = async () => {
await refetch();
};
const onCreate = (item: CardServiceSchema) => {
if (!card) return;
CardService.addCardService({
requestBody: {
cardId: card.id,
serviceId: item.service.id,
quantity: item.quantity,
price: item.price,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetchAndRecalculate();
});
};
const onDelete = (item: CardServiceSchema) => {
if (!card) return;
CardService.deleteCardService({
requestBody: {
cardId: card.id,
serviceId: item.service.id,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetchAndRecalculate();
});
};
const onChange = (item: CardServiceSchema) => {
if (!card) return;
CardService.updateCardService({
requestBody: {
cardId: card.id,
service: item,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetchAndRecalculate();
});
};
return {
items: card?.services || [],
onCreate,
onDelete,
onChange,
};
};
const useDealProductsState = (): CRUDTableProps<CardProductSchema> => {
const { card, refetch } = useCardState();
const refetchAndRecalculate = async () => {
await refetch();
};
const onCreate = (item: CardProductSchema) => {
if (!card) return;
CardService.addCardProduct({
requestBody: {
cardId: card.id,
product: item,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetchAndRecalculate();
});
};
const onDelete = (item: CardProductSchema) => {
if (!card) return;
CardService.deleteCardProduct({
requestBody: {
cardId: card.id,
productId: item.product.id,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetchAndRecalculate();
});
};
const onChange = (item: CardProductSchema) => {
if (!card) return;
CardService.updateCardProduct({
requestBody: {
cardId: card.id,
product: item,
},
}).then(async ({ ok, message }) => {
if (!ok) notifications.guess(ok, { message });
if (ok) await refetchAndRecalculate();
});
};
return {
items: card?.products || [],
onCreate,
onDelete,
onChange,
};
};
const useCardProductAndServiceTabState = () => {
const cardState = useCardState();
const cardProductsState = useDealProductsState();
const cardServicesState = useCardServicesState();
return {
cardState,
cardProductsState,
cardServicesState,
};
};
export default useCardProductAndServiceTabState;

View File

@ -0,0 +1,87 @@
"use client";
import { NumberInput, Textarea } from "@mantine/core";
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import {
CreateDealProductSchema,
DealProductSchema,
UpdateDealProductSchema,
} from "@/lib/client";
import BaseFormModal, {
CreateEditFormProps,
} from "@/modals/base/BaseFormModal/BaseFormModal";
import ProductSelect from "../../components/ProductSelect/ProductSelect";
type RestProps = {
clientId: number;
productIdsToExclude?: number[];
};
type Props = CreateEditFormProps<
CreateDealProductSchema,
UpdateDealProductSchema,
DealProductSchema
> &
RestProps;
const DealProductEditorModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const form = useForm<Partial<DealProductSchema>>({
initialValues: innerProps.isEditing
? innerProps.entity
: {
quantity: 1,
comment: "",
},
validate: {
product: product => !product && "Необходимо выбрать товар",
quantity: quantity =>
(!quantity || quantity === 0) &&
"Количество должно быть больше 0",
},
});
const onClose = () => {
context.closeContextModal(id);
};
return (
<BaseFormModal
{...innerProps}
form={form}
closeOnSubmit
onClose={onClose}>
<ProductSelect
placeholder={"Выберите товар"}
label={"Товар"}
clientId={innerProps.clientId}
disabled={innerProps.isEditing}
filterBy={item =>
!(innerProps.productIdsToExclude || []).includes(item.id)
}
{...form.getInputProps("product")}
onChange={product => {
form.setFieldValue("product", product);
form.setFieldValue("productId", product.id);
}}
/>
<NumberInput
placeholder={"Введите количество"}
label={"Количество"}
min={1}
{...form.getInputProps("quantity")}
/>
<Textarea
placeholder={"Введите комментарий"}
label={"Комментарий"}
{...form.getInputProps("comment")}
/>
</BaseFormModal>
);
};
export default DealProductEditorModal;

View File

@ -0,0 +1,114 @@
"use client";
import {
ComboboxItem,
ComboboxItemGroup,
NumberInput,
OptionsFilter,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import {
CreateDealServiceSchema,
DealServiceSchema,
UpdateDealServiceSchema,
} from "@/lib/client";
import BaseFormModal, {
CreateEditFormProps,
} from "@/modals/base/BaseFormModal/BaseFormModal";
import { ServiceType } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/types/service";
import ServiceWithPriceInput from "./components/ServiceWithPriceInput";
type RestProps = {
serviceIdsToExclude?: number[];
};
type Props = CreateEditFormProps<
CreateDealServiceSchema,
UpdateDealServiceSchema,
DealServiceSchema
> &
RestProps;
const DealServiceEditorModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const form = useForm<Partial<DealServiceSchema>>({
initialValues: innerProps.isEditing
? innerProps.entity
: {
service: undefined,
serviceId: undefined,
quantity: 1,
isFixedPrice: false,
},
validate: {
service: service => !service && "Необходимо выбрать услугу",
quantity: quantity =>
(!quantity || quantity === 0) &&
"Количество должно быть больше 0",
},
});
const onClose = () => context.closeContextModal(id);
const serviceOptionsFilter = ({
options,
}: {
options: ComboboxItemGroup[];
}) => {
if (!innerProps.serviceIdsToExclude) return options;
const productServiceIds = innerProps.serviceIdsToExclude;
return (options as ComboboxItemGroup[]).map(({ items, group }) => {
return {
group,
items: items.filter(
item =>
!productServiceIds.includes(
Number((item as ComboboxItem).value)
)
),
};
});
};
return (
<BaseFormModal
{...innerProps}
form={form}
closeOnSubmit
onClose={onClose}>
<ServiceWithPriceInput
serviceProps={{
...form.getInputProps("service"),
onChange: service => {
form.setFieldValue("service", service);
form.setFieldValue("serviceId", service.id);
},
label: "Услуга",
placeholder: "Выберите услугу",
disabled: innerProps.isEditing,
filter: serviceOptionsFilter as OptionsFilter,
}}
priceProps={{
...form.getInputProps("price"),
label: "Цена",
placeholder: "Введите цену",
}}
quantity={form.values.quantity || 1}
filterType={ServiceType.DEAL_SERVICE}
lockOnEdit={innerProps.isEditing}
/>
<NumberInput
placeholder={"Введите количество"}
label={"Количество"}
min={1}
{...form.getInputProps("quantity")}
/>
</BaseFormModal>
);
};
export default DealServiceEditorModal;

View File

@ -0,0 +1,115 @@
"use client";
import { FC, useEffect, useRef, useState } from "react";
import {
NumberInput,
NumberInputProps,
Stack,
StackProps,
} from "@mantine/core";
import { ObjectSelectProps } from "@/components/selects/ObjectSelect/ObjectSelect";
import { ServiceSchema } from "@/lib/client";
import ServiceSelect from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ServiceSelect/ServiceSelect";
import { ServiceType } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/types/service";
type ServiceProps = Omit<ObjectSelectProps<ServiceSchema>, "data">;
type PriceProps = NumberInputProps;
type Props = {
serviceProps: ServiceProps;
priceProps: PriceProps;
quantity: number;
containerProps?: StackProps;
filterType?: ServiceType;
lockOnEdit?: boolean;
};
const ServiceWithPriceInput: FC<Props> = ({
serviceProps,
priceProps,
quantity,
containerProps,
filterType = ServiceType.PRODUCT_SERVICE,
lockOnEdit = true,
}) => {
const [price, setPrice] = useState<number | undefined>(
typeof priceProps.value === "number" ? priceProps.value : undefined
);
const [service, setService] = useState<ServiceSchema | undefined>(
serviceProps.value
);
const isFirstRender = useRef(true);
const getPriceBasedOnQuantity = (): number | null => {
if (!service || !service.priceRanges.length) return null;
const range =
service.priceRanges.find(
priceRange =>
quantity >= priceRange.fromQuantity &&
quantity <= priceRange.toQuantity
) || service.priceRanges[0];
return range.price;
};
const setPriceBasedOnService = () => {
if (!service) return;
const rangePrice = getPriceBasedOnQuantity();
setPrice(rangePrice || service.price);
};
const onPriceManualChange = (value: number | string) => {
if (typeof value !== "number") return;
setPrice(value);
};
useEffect(() => {
if (isFirstRender.current && lockOnEdit) return;
const price = getPriceBasedOnQuantity();
if (price) setPrice(price);
}, [quantity]);
useEffect(() => {
if (isFirstRender.current && lockOnEdit) return;
if (!priceProps.onChange || price === undefined) return;
priceProps.onChange(price);
}, [price]);
useEffect(() => {
if (
!serviceProps.onChange ||
!service ||
(price && isFirstRender.current && lockOnEdit)
)
return;
setPriceBasedOnService();
serviceProps.onChange(service);
}, [service]);
useEffect(() => {
isFirstRender.current = false;
}, []);
return (
<Stack
gap={"xs"}
{...containerProps}>
<ServiceSelect
{...serviceProps}
value={service}
onChange={setService}
filterType={filterType}
disabled={lockOnEdit}
/>
<NumberInput
{...priceProps}
onChange={onPriceManualChange}
defaultValue={priceProps.value}
min={1}
allowNegative={false}
/>
</Stack>
);
};
export default ServiceWithPriceInput;

View File

@ -0,0 +1,93 @@
"use client";
import { useState } from "react";
import { Button, Flex, Text } from "@mantine/core";
import { ContextModalProps } from "@mantine/modals";
import ObjectMultiSelect from "@/components/selects/ObjectMultiSelect/ObjectMultiSelect";
import { DealProductSchema } from "@/lib/client";
import { notifications } from "@/lib/notifications";
type Props = {
dealProducts: DealProductSchema[];
sourceDealProduct: DealProductSchema;
duplicateServices: (
sourceDealProduct: DealProductSchema,
targetDealProducts: DealProductSchema[]
) => void;
};
const DuplicateServicesModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const [selectedDealProducts, setSelectedDealProducts] = useState<
DealProductSchema[]
>([]);
const onDealProductSelect = () => {
if (!selectedDealProducts) {
notifications.error({
message:
"Выберите товары на которые необходимо продублировать услуги",
});
return;
}
innerProps.duplicateServices(
innerProps.sourceDealProduct,
selectedDealProducts
);
context.closeContextModal(id);
};
const onDuplicateAllClick = () => {
innerProps.duplicateServices(
innerProps.sourceDealProduct,
innerProps.dealProducts.filter(
item => item !== innerProps.sourceDealProduct
)
);
context.closeContextModal(id);
};
return (
<Flex
direction={"column"}
gap={"xs"}>
<ObjectMultiSelect
w={"100%"}
label={"Товары"}
placeholder={
"Выберите товары на которые нужно продублировать услуги"
}
onChange={setSelectedDealProducts}
value={selectedDealProducts}
data={innerProps.dealProducts}
getLabelFn={item => item.product.name}
getValueFn={item => item.product.id.toString()}
filterBy={item => item !== innerProps.sourceDealProduct}
/>
<Flex
gap={"xs"}
justify={"flex-end"}>
<Button
variant={"subtle"}
onClick={() => context.closeContextModal(id)}>
<Text>Отменить</Text>
</Button>
<Button
variant={"default"}
onClick={onDuplicateAllClick}>
<Text>Продублировать на все товары</Text>
</Button>
<Button
onClick={onDealProductSelect}
variant={"default"}>
<Text>Продублировать</Text>
</Button>
</Flex>
</Flex>
);
};
export default DuplicateServicesModal;

View File

@ -0,0 +1,122 @@
"use client";
import { Fieldset, Flex, rem, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import {
CreateProductSchema,
ProductSchema,
UpdateProductSchema,
} from "@/lib/client";
import BaseFormModal, {
CreateEditFormProps,
} from "@/modals/base/BaseFormModal/BaseFormModal";
import ProductImageDropzone from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/components/ProductImageDropzone/ProductImageDropzone";
import BaseFormInputProps from "@/utils/baseFormInputProps";
type Props = CreateEditFormProps<
CreateProductSchema,
UpdateProductSchema,
ProductSchema
>;
const ProductEditorModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const isEditing = "entity" in innerProps;
const initialValues: Partial<ProductSchema> = isEditing
? innerProps.entity!
: {
name: "",
article: "",
factoryArticle: "",
brand: "",
composition: "",
color: "",
size: "",
additionalInfo: "",
};
const form = useForm<Partial<ProductSchema>>({
initialValues,
validate: {
name: name =>
!name || name.trim() !== ""
? null
: "Необходимо ввести название товара",
},
});
const onClose = () => context.closeContextModal(id);
return (
<BaseFormModal
{...innerProps}
form={form}
closeOnSubmit
onClose={onClose}>
<Flex
gap={rem(10)}
direction={"column"}>
<Fieldset legend={"Основные характеристики"}>
<TextInput
placeholder={"Введите название товара"}
label={"Название товара"}
{...form.getInputProps("name")}
/>
<TextInput
placeholder={"Введите артикул"}
label={"Артикул"}
{...form.getInputProps("article")}
/>
<TextInput
placeholder={"Введите складской артикул"}
label={"Складской артикул"}
{...form.getInputProps("factoryArticle")}
/>
</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")}
/>
</Fieldset>
{isEditing && (
<ProductImageDropzone
imageUrlInputProps={
form.getInputProps(
"imageUrl"
) as BaseFormInputProps<string>
}
productId={innerProps.entity.id}
/>
)}
</Flex>
</BaseFormModal>
);
};
export default ProductEditorModal;

View File

@ -0,0 +1,100 @@
"use client";
import { isNil, isNumber } from "lodash";
import { Checkbox, Flex } from "@mantine/core";
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import {
CreateProductServiceSchema,
ProductServiceSchema,
UpdateProductServiceSchema,
} from "@/lib/client/index.js";
import BaseFormModal, {
CreateEditFormProps,
} from "@/modals/base/BaseFormModal/BaseFormModal";
import ServiceWithPriceInput from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/modals/DealServiceEditorModal/components/ServiceWithPriceInput";
import { ServiceType } from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/types/service";
type RestProps = {
quantity: number;
excludeServiceIds: number[];
};
type Props = CreateEditFormProps<
CreateProductServiceSchema,
UpdateProductServiceSchema,
ProductServiceSchema
> &
RestProps;
const ProductServiceEditorModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const initialValues: Partial<ProductServiceSchema> = innerProps.isEditing
? innerProps.entity
: {
service: undefined,
serviceId: undefined,
price: undefined,
// employees: [],
isFixedPrice: false,
};
const form = useForm<Partial<ProductServiceSchema>>({
initialValues,
validate: {
service: service =>
(isNil(service) || service.id < 0) && "Укажите услугу",
price: price => (!isNumber(price) || price < 0) && "Укажите цену",
},
});
const onClose = () => context.closeContextModal(id);
return (
<BaseFormModal
{...innerProps}
form={form}
onClose={onClose}
closeOnSubmit>
<Flex
w={"100%"}
direction={"column"}
gap={"xs"}>
<ServiceWithPriceInput
serviceProps={{
...form.getInputProps("service"),
onChange: value => {
form.setFieldValue("service", value);
form.setFieldValue("serviceId", value.id);
},
label: "Услуга",
placeholder: "Выберите услугу",
disabled: innerProps.isEditing,
filterBy: item =>
!innerProps.excludeServiceIds.includes(item.id) ||
innerProps.isEditing,
}}
priceProps={{
...form.getInputProps("price"),
label: "Цена",
placeholder: "Введите цену",
}}
filterType={ServiceType.PRODUCT_SERVICE}
lockOnEdit={innerProps.isEditing}
quantity={innerProps.quantity}
/>
<Checkbox
{...form.getInputProps("isFixedPrice", {
type: "checkbox",
})}
label={"Зафиксировать цену"}
placeholder={"Зафиксировать цену"}
/>
</Flex>
</BaseFormModal>
);
};
export default ProductServiceEditorModal;

View File

@ -0,0 +1,57 @@
"use client";
import { Flex } from "@mantine/core";
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import { ServicesKitSchema } from "@/lib/client";
import BaseFormModalActions from "@/modals/base/BaseFormModal/BaseFormModalActions";
import ServicesKitSelect from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/modals/ServicesKitSelectModal/components/ServicesKitSelect";
type Props = {
onSelect: (kit: ServicesKitSchema) => void;
serviceType: number;
};
type ServicesKitForm = {
servicesKit: ServicesKitSchema;
};
const ServicesKitSelectModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const form = useForm<ServicesKitForm>({
validate: {
servicesKit: servicesKit => !servicesKit && "Выберите сервис",
},
});
const onClose = () => context.closeContextModal(id);
const onSubmit = (values: ServicesKitForm) => {
innerProps.onSelect(values.servicesKit);
onClose();
};
return (
<form onSubmit={form.onSubmit(onSubmit)}>
<Flex
gap={"xs"}
direction={"column"}>
<Flex>
<ServicesKitSelect
w={"100%"}
{...form.getInputProps("servicesKit")}
filterBy={item =>
item.serviceType === innerProps.serviceType
}
/>
</Flex>
<BaseFormModalActions onClose={onClose} />
</Flex>
</form>
);
};
export default ServicesKitSelectModal;

View File

@ -0,0 +1,23 @@
import { FC } from "react";
import ObjectSelect, {
ObjectSelectProps,
} from "@/components/selects/ObjectSelect/ObjectSelect";
import { ServicesKitSchema } from "@/lib/client";
import useServicesKitsList from "@/modules/dealModules/dealEditorTabs/FulfillmentBaseTab/hooks/lists/useServicesKitsList";
type Props = Omit<ObjectSelectProps<ServicesKitSchema>, "data">;
const ServicesKitSelect: FC<Props> = props => {
const { servicesKits } = useServicesKitsList();
return (
<ObjectSelect
label={"Набор услуг"}
placeholder={"Выберите набор услуг"}
data={servicesKits}
{...props}
/>
);
};
export default ServicesKitSelect;

View File

@ -0,0 +1,4 @@
export enum ServiceType {
DEAL_SERVICE,
PRODUCT_SERVICE,
}

28
src/modules/modules.tsx Normal file
View File

@ -0,0 +1,28 @@
import {
IconBox,
} from "@tabler/icons-react";
import ModulesType from "./types";
import connectModules from "./connectModules";
export enum ModuleNames {
FULFILLMENT_BASE = "fulfillment_base",
}
const modules: ModulesType = {
[ModuleNames.FULFILLMENT_BASE]: {
renderInfo: {
label: "Фулфиллмент",
key: "fulfillment_base",
icon: <IconBox />,
},
modelData: {
id: 1,
key: "fulfillment_base",
label: "Фулфиллмент",
iconName: "IconBox",
description: "Создание товаров и услуг, их привязка к сделкам",
},
},
};
export const MODULES = connectModules(modules);

View File

@ -0,0 +1,86 @@
import * as fs from "fs";
import * as path from "path";
import axios, { AxiosResponse } from "axios";
import * as handlebars from "handlebars";
// region types
type Module = {
id: number;
key: string;
label: string;
iconName: string;
description: string;
};
type ModulesResponse = {
items: Module[];
};
type Args = {
[key: string]: string | boolean;
};
// endregion
// region utils
const getArgs = (): Args =>
process.argv.slice(2).reduce((args: Args, arg: string) => {
if (arg.startsWith("--")) {
// Handle long arguments like --port=8000
const [key, value] = arg.slice(2).split("=");
args[key] = value !== undefined ? value : true;
} else if (arg.startsWith("-") && arg.length > 1) {
// Handle short arguments like -p=8000
const [key, value] = arg.slice(1).split("=");
args[key] = value !== undefined ? value : true;
}
return args;
}, {});
// endregion
const kwargs = getArgs();
// region constants
const HOST = kwargs.host ?? kwargs.h ?? "127.0.0.1";
const PORT = kwargs.port ?? kwargs.p ?? "8000";
const ENDPOINT = `http://${HOST}:${PORT}/api/module/built-in/`;
const TEMPLATE_PATH = path.join(
__dirname,
"templates",
"modulesFileTemplate.hbs"
);
const OUTPUT_PATH = path.join(__dirname, "..", "modules.tsx");
// endregion
const templateSource = fs.readFileSync(TEMPLATE_PATH, "utf8");
const template = handlebars.compile(templateSource);
handlebars.registerHelper("uppercase", text => {
return text.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
});
const generateRows = (modules: Module[]) => {
try {
const data = {
modules,
};
const tsxContent = template(data);
fs.writeFileSync(OUTPUT_PATH, tsxContent);
console.log("File successfully generated.");
} catch (error) {
console.error(error);
}
};
const modulesFileGen = () => {
console.log("Start file generation...");
axios
.get(ENDPOINT)
.then((response: AxiosResponse<ModulesResponse>) => {
generateRows(response.data.items);
})
.catch(err => console.log(err));
};
modulesFileGen();

View File

@ -0,0 +1,34 @@
import {
{{#each modules}}
{{#if this.iconName}}{{this.iconName}},{{/if}}
{{/each}}
} from "@tabler/icons-react";
import ModulesType from "./types";
import connectModules from "./connectModules";
export enum ModuleNames {
{{#each modules}}
{{uppercase this.key}} = "{{this.key}}",
{{/each}}
}
const modules: ModulesType = {
{{#each modules}}
[ModuleNames.{{uppercase this.key}}]: {
renderInfo: {
label: "{{this.label}}",
key: "{{this.key}}",
icon: {{#if this.iconName}}<{{this.iconName}} />{{else}}None{{/if}},
},
modelData: {
id: {{this.id}},
key: "{{this.key}}",
label: "{{this.label}}",
iconName: "{{this.iconName}}",
description: "{{this.description}}",
},
},
{{/each}}
};
export const MODULES = connectModules(modules);

18
src/modules/types.tsx Normal file
View File

@ -0,0 +1,18 @@
import { ReactNode } from "react";
import { BuiltInModuleSchema } from "@/lib/client";
export type Module = {
renderInfo: {
label: string;
key: string;
icon: ReactNode;
};
modelData: BuiltInModuleSchema;
getTab?: (props: any) => ReactNode;
};
type ModulesType = {
[key: string]: Module;
};
export default ModulesType;

View File

@ -0,0 +1,8 @@
import { ProjectSchema } from "../../client";
import { ModuleNames } from "../modules.tsx";
const isModuleInProject = (module: ModuleNames, project?: ProjectSchema | null) => {
return project?.modules.findIndex(m => m.key === module.toString()) !== -1;
};
export default isModuleInProject;

View File

@ -0,0 +1,7 @@
type BaseFormInputProps<T> = {
onChange: (value: T) => void;
value: T;
error?: string | null;
};
export default BaseFormInputProps;

View File

@ -2890,6 +2890,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/dropzone@npm:^8.3.1":
version: 8.3.1
resolution: "@mantine/dropzone@npm:8.3.1"
dependencies:
react-dropzone: "npm:14.3.8"
peerDependencies:
"@mantine/core": 8.3.1
"@mantine/hooks": 8.3.1
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
checksum: 10c0/643c1224925b4575ea7fecef58c26c4bb520f097fb308464c099502ae444a22cc2abd52e96bd84513da9741a49e12ecec0f5a2147322a212c17de00659ec206a
languageName: node
linkType: hard
"@mantine/form@npm:^8.1.3": "@mantine/form@npm:^8.1.3":
version: 8.2.1 version: 8.2.1
resolution: "@mantine/form@npm:8.2.1" resolution: "@mantine/form@npm:8.2.1"
@ -5050,6 +5064,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"attr-accept@npm:^2.2.4":
version: 2.2.5
resolution: "attr-accept@npm:2.2.5"
checksum: 10c0/9b4cb82213925cab2d568f71b3f1c7a7778f9192829aac39a281e5418cd00c04a88f873eb89f187e0bf786fa34f8d52936f178e62cbefb9254d57ecd88ada99b
languageName: node
linkType: hard
"autoprefixer@npm:^10.4.21": "autoprefixer@npm:^10.4.21":
version: 10.4.21 version: 10.4.21
resolution: "autoprefixer@npm:10.4.21" resolution: "autoprefixer@npm:10.4.21"
@ -6108,6 +6129,7 @@ __metadata:
"@ianvs/prettier-plugin-sort-imports": "npm:^4.4.2" "@ianvs/prettier-plugin-sort-imports": "npm:^4.4.2"
"@mantine/core": "npm:8.1.2" "@mantine/core": "npm:8.1.2"
"@mantine/dates": "npm:^8.2.7" "@mantine/dates": "npm:^8.2.7"
"@mantine/dropzone": "npm:^8.3.1"
"@mantine/form": "npm:^8.1.3" "@mantine/form": "npm:^8.1.3"
"@mantine/hooks": "npm:8.1.2" "@mantine/hooks": "npm:8.1.2"
"@mantine/modals": "npm:^8.2.1" "@mantine/modals": "npm:^8.2.1"
@ -6146,6 +6168,7 @@ __metadata:
eslint-plugin-jsx-a11y: "npm:^6.10.2" eslint-plugin-jsx-a11y: "npm:^6.10.2"
eslint-plugin-react: "npm:^7.37.5" eslint-plugin-react: "npm:^7.37.5"
framer-motion: "npm:^12.23.7" framer-motion: "npm:^12.23.7"
handlebars: "npm:^4.7.8"
i18n-iso-countries: "npm:^7.14.0" i18n-iso-countries: "npm:^7.14.0"
jest: "npm:^30.0.0" jest: "npm:^30.0.0"
jest-environment-jsdom: "npm:^30.0.0" jest-environment-jsdom: "npm:^30.0.0"
@ -7554,6 +7577,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"file-selector@npm:^2.1.0":
version: 2.1.2
resolution: "file-selector@npm:2.1.2"
dependencies:
tslib: "npm:^2.7.0"
checksum: 10c0/fe827e0e95410aacfcc3eabc38c29cc36055257f03c1c06b631a2b5af9730c142ad2c52f5d64724d02231709617bda984701f52bd1f4b7aca50fb6585a27c1d2
languageName: node
linkType: hard
"filelist@npm:^1.0.4": "filelist@npm:^1.0.4":
version: 1.0.4 version: 1.0.4
resolution: "filelist@npm:1.0.4" resolution: "filelist@npm:1.0.4"
@ -8087,7 +8119,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"handlebars@npm:4.7.8": "handlebars@npm:4.7.8, handlebars@npm:^4.7.8":
version: 4.7.8 version: 4.7.8
resolution: "handlebars@npm:4.7.8" resolution: "handlebars@npm:4.7.8"
dependencies: dependencies:
@ -11671,6 +11703,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-dropzone@npm:14.3.8":
version: 14.3.8
resolution: "react-dropzone@npm:14.3.8"
dependencies:
attr-accept: "npm:^2.2.4"
file-selector: "npm:^2.1.0"
prop-types: "npm:^15.8.1"
peerDependencies:
react: ">= 16.8 || 18.0.0"
checksum: 10c0/e17b1832783cda7b8824fe9370e99185d1abbdd5e4980b2985d6321c5768c8de18ff7b9ad550c809ee9743269dea608ff74d5208062754ce8377ad022897b278
languageName: node
linkType: hard
"react-imask@npm:^7.6.1": "react-imask@npm:^7.6.1":
version: 7.6.1 version: 7.6.1
resolution: "react-imask@npm:7.6.1" resolution: "react-imask@npm:7.6.1"
@ -13604,7 +13649,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": "tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.7.0, tslib@npm:^2.8.0":
version: 2.8.1 version: 2.8.1
resolution: "tslib@npm:2.8.1" resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62