1 Commits

Author SHA1 Message Date
36c2a3a2af feat: funnel dnd using pragmatic, not finished groups 2025-10-16 15:26:53 +04:00
237 changed files with 4614 additions and 13476 deletions

View File

@ -1,25 +0,0 @@
import * as fs from "fs";
const zodPath = "src/lib/client/zod.gen.ts";
let content = fs.readFileSync(zodPath, "utf8");
// Replace only for the upload schema
const zodTarget = "upload_file: z.string";
while (content.includes(zodTarget)) {
content = content.replace(zodTarget, "upload_file: z.any");
}
fs.writeFileSync(zodPath, content);
console.log("✅ Fixed zod schema for upload_file");
const utilsPath = "src/lib/client/client/utils.ts";
content = fs.readFileSync(utilsPath, "utf8");
const utilsTarget = "@ts-expect-error";
while (content.includes(utilsTarget)) {
content = content.replace(utilsTarget, "@ts-ignore");
}
content = content.replace(
"...(mergedHeaders[key] ?? []),",
"...(mergedHeaders[key] ?? []) as any,"
);
fs.writeFileSync(utilsPath, content);
console.log("✅ Fixed utils.ts");

View File

@ -7,10 +7,16 @@
"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 & sudo npx tsc fix-client.ts && mv -f ./fix-client.js ./fix-client.cjs && sudo node ./fix-client.cjs", "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" "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": {
"@atlaskit/avatar": "^25.4.2",
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
"@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.7",
"@atlaskit/pragmatic-drag-and-drop-live-region": "^1.3.1",
"@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^3.2.7",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
@ -27,23 +33,23 @@
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.34.0",
"@tailwindcss/postcss": "^4.1.11", "@tailwindcss/postcss": "^4.1.11",
"@tanstack/react-query": "^5.83.0", "@tanstack/react-query": "^5.83.0",
"@types/react-dom": "19.1.2",
"axios": "1.12.0", "axios": "1.12.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"dayjs": "^1.11.18", "dayjs": "^1.11.15",
"framer-motion": "^12.23.7", "framer-motion": "^12.23.7",
"handlebars": "^4.7.8", "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",
"mantine-contextmenu": "^8.2.0",
"mantine-datatable": "^8.2.0", "mantine-datatable": "^8.2.0",
"next": "15.4.7", "next": "15.4.7",
"phone": "^3.1.67", "phone": "^3.1.67",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "^19.2.0", "react-dom": "19.1.0",
"react-imask": "^7.6.1", "react-imask": "^7.6.1",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
@ -69,7 +75,6 @@
"@types/lodash": "^4.17.20", "@types/lodash": "^4.17.20",
"@types/node": "^22.13.11", "@types/node": "^22.13.11",
"@types/react": "19.1.8", "@types/react": "19.1.8",
"@types/react-dom": "^19",
"@types/react-redux": "^7.1.34", "@types/react-redux": "^7.1.34",
"@types/react-slick": "^0", "@types/react-slick": "^0",
"@types/redux-persist": "^4.3.1", "@types/redux-persist": "^4.3.1",

View File

@ -1,14 +1,12 @@
"use client"; "use client";
import { RefObject, useMemo, useRef } from "react"; import { useMemo } from "react";
import { IconApps, IconList, IconTag } from "@tabler/icons-react";
import { SimpleGrid, Stack } from "@mantine/core"; import { SimpleGrid, Stack } from "@mantine/core";
import Action from "@/app/actions/components/Action/Action"; import Action from "@/app/actions/components/Action/Action";
import mobileButtonsData from "@/app/actions/data/mobileButtonsData"; import mobileButtonsData from "@/app/actions/data/mobileButtonsData";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import PageBlock from "@/components/layout/PageBlock/PageBlock"; import PageBlock from "@/components/layout/PageBlock/PageBlock";
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect"; import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
import BuiltInLinkData from "@/types/BuiltInLinkData";
const PageBody = () => { const PageBody = () => {
const { selectedProject, setSelectedProjectId, projects, modulesSet } = const { selectedProject, setSelectedProjectId, projects, modulesSet } =
@ -22,24 +20,6 @@ const PageBody = () => {
[modulesSet] [modulesSet]
); );
const commonActionsData: RefObject<BuiltInLinkData[]> = useRef([
{
icon: IconList,
label: "Атрибуты",
href: "/attributes",
},
{
icon: IconApps,
label: "Модули",
href: "/modules",
},
{
icon: IconTag,
label: "Теги",
href: "/tags",
},
]);
return ( return (
<PageBlock fullScreenMobile> <PageBlock fullScreenMobile>
<Stack p={"xs"}> <Stack p={"xs"}>
@ -53,10 +33,7 @@ const PageBody = () => {
<SimpleGrid <SimpleGrid
type={"container"} type={"container"}
cols={2}> cols={2}>
{[ {filteredMobileButtonsData.map((data, index) => (
...commonActionsData.current,
...filteredMobileButtonsData,
].map((data, index) => (
<Action <Action
linkData={data} linkData={data}
key={index} key={index}

View File

@ -28,7 +28,7 @@ const mobileButtonsData: LinkData[] = [
}, },
{ {
icon: IconFileBarcode, icon: IconFileBarcode,
label: "Шаблоны ШК", label: "Шаблоны штрихкодов",
href: "/barcode-templates", href: "/barcode-templates",
moduleName: ModuleNames.FULFILLMENT_BASE, moduleName: ModuleNames.FULFILLMENT_BASE,
}, },

View File

@ -1,27 +0,0 @@
import { FC } from "react";
import AttributePageView from "@/app/attributes/types/view";
import BaseSegmentedControl, {
BaseSegmentedControlProps,
} from "@/components/ui/BaseSegmentedControl/BaseSegmentedControl";
type Props = Omit<BaseSegmentedControlProps<AttributePageView>, "data">;
const data = [
{
label: "Аттрибуты",
value: AttributePageView.ATTRIBUTES,
},
{
label: "Справочники",
value: AttributePageView.SELECTS,
},
];
const AttrViewSegmentedControl: FC<Props> = props => (
<BaseSegmentedControl
data={data}
{...props}
/>
);
export default AttrViewSegmentedControl;

View File

@ -1,87 +0,0 @@
"use client";
import { Dispatch, FC, SetStateAction, useMemo } from "react";
import { Divider, Flex, Group, TextInput } from "@mantine/core";
import AttrViewSegmentedControl from "@/app/attributes/components/AttrViewSegmentedControl";
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
import AttributePageView from "@/app/attributes/types/view";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
import useIsMobile from "@/hooks/utils/useIsMobile";
type Props = {
view: AttributePageView;
setView: Dispatch<SetStateAction<AttributePageView>>;
};
const AttributesHeader: FC<Props> = ({ view, setView }) => {
const {
attributesActions,
selectsActions,
attrSearch,
setAttrSearch,
selectSearch,
setSelectSearch,
} = useAttributesContext();
const isMobile = useIsMobile();
const attributeActions = useMemo(
() => (
<Group wrap={"nowrap"}>
<InlineButton
onClick={attributesActions.onCreate}
w={isMobile ? "100%" : "auto"}>
Создать атрибут
</InlineButton>
<TextInput
value={attrSearch}
onChange={e => setAttrSearch(e.currentTarget.value)}
w={isMobile ? "100%" : "auto"}
placeholder={"Поиск..."}
/>
</Group>
),
[isMobile, attrSearch]
);
const selectActions = useMemo(
() => (
<Group wrap={"nowrap"}>
<InlineButton
onClick={selectsActions.onCreate}
w={isMobile ? "100%" : "auto"}>
Создать справочник
</InlineButton>
<TextInput
value={selectSearch}
onChange={e => setSelectSearch(e.currentTarget.value)}
w={isMobile ? "100%" : "auto"}
placeholder={"Поиск..."}
/>
</Group>
),
[isMobile, selectSearch]
);
return (
<Flex
wrap={"nowrap"}
gap={"xs"}
align={isMobile ? "unset" : "center"}
mt={isMobile ? "xs" : ""}
mx={isMobile ? "xs" : ""}
direction={isMobile ? "column-reverse" : "row"}
justify={"space-between"}>
{view === AttributePageView.ATTRIBUTES
? attributeActions
: selectActions}
{isMobile && <Divider />}
<AttrViewSegmentedControl
value={view}
onChange={setView}
styles={{ root: { margin: 0, padding: 0 } }}
/>
</Flex>
);
};
export default AttributesHeader;

View File

@ -1,40 +0,0 @@
"use client";
import { FC } from "react";
import { IconMoodSad } from "@tabler/icons-react";
import { Group, Text } from "@mantine/core";
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
import useAttributesTableColumns from "@/app/attributes/hooks/useAttributesTableColumns";
import BaseTable from "@/components/ui/BaseTable/BaseTable";
import useIsMobile from "@/hooks/utils/useIsMobile";
const AttributesTable: FC = () => {
const isMobile = useIsMobile();
const { attributes } = useAttributesContext();
const columns = useAttributesTableColumns();
return (
<BaseTable
withTableBorder
columns={columns}
records={attributes}
verticalSpacing={"md"}
emptyState={
<Group mt={attributes.length === 0 ? "xl" : 0}>
<Text>Нет атрибутов</Text>
<IconMoodSad />
</Group>
}
groups={undefined}
styles={{
table: {
width: "100%",
},
header: { zIndex: 1 },
}}
mx={isMobile ? "xs" : 0}
/>
);
};
export default AttributesTable;

View File

@ -1,42 +0,0 @@
"use client";
import { useState } from "react";
import AttributesHeader from "@/app/attributes/components/AttributesHeader";
import AttributesTable from "@/app/attributes/components/AttributesTable";
import SelectsTable from "@/app/attributes/components/SelectsTable";
import AttributePageView from "@/app/attributes/types/view";
import PageBlock from "@/components/layout/PageBlock/PageBlock";
const PageBody = () => {
const [view, setView] = useState<AttributePageView>(
AttributePageView.ATTRIBUTES
);
return (
<PageBlock
style={{ flex: 1, minHeight: 0 }}
fullScreenMobile>
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
gap: "var(--mantine-spacing-md)",
}}>
<AttributesHeader
view={view}
setView={setView}
/>
<div style={{ flex: 1, overflow: "auto" }}>
{view === AttributePageView.ATTRIBUTES ? (
<AttributesTable />
) : (
<SelectsTable />
)}
</div>
</div>
</PageBlock>
);
};
export default PageBody;

View File

@ -1,36 +0,0 @@
import { IconMoodSad } from "@tabler/icons-react";
import { Group, Text } from "@mantine/core";
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
import useSelectsTableColumns from "@/app/attributes/hooks/useSelectsTableColumns";
import BaseTable from "@/components/ui/BaseTable/BaseTable";
import useIsMobile from "@/hooks/utils/useIsMobile";
const SelectsTable = () => {
const { selects } = useAttributesContext();
const isMobile = useIsMobile();
const columns = useSelectsTableColumns();
return (
<BaseTable
withTableBorder
columns={columns}
records={selects}
verticalSpacing={"md"}
emptyState={
<Group mt={selects.length === 0 ? "xl" : 0}>
<Text>Нет справочников</Text>
<IconMoodSad />
</Group>
}
groups={undefined}
styles={{
table: {
width: "100%",
},
}}
mx={isMobile ? "xs" : 0}
/>
);
};
export default SelectsTable;

View File

@ -1,62 +0,0 @@
"use client";
import { Dispatch, SetStateAction } from "react";
import useFilteredAttributes from "@/app/attributes/hooks/useFilteredAttributes";
import useSelectsActions, {
SelectsActions,
} from "@/app/attributes/hooks/useSelectsActions";
import useAttributesActions, {
AttributesActions,
} from "@/app/module-editor/[moduleId]/hooks/useAttributesActions";
import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList";
import useAttrSelectsList from "@/hooks/lists/useAttrSelectsList";
import { AttributeSchema, AttrSelectSchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
import useFilteredSelects from "@/app/attributes/hooks/useFilteredSelects";
type AttributesContextState = {
attributes: AttributeSchema[];
attributesActions: AttributesActions;
attrSearch: string;
setAttrSearch: Dispatch<SetStateAction<string>>;
selects: AttrSelectSchema[];
selectsActions: SelectsActions;
selectSearch: string;
setSelectSearch: Dispatch<SetStateAction<string>>;
};
const useAttributesContextState = (): AttributesContextState => {
const { attributes, refetch: refetchAttributes } = useAttributesList();
const attributesActions = useAttributesActions({
refetchAttributes,
});
const {
search: attrSearch,
setSearch: setAttrSearch,
filteredAttributes,
} = useFilteredAttributes({ attributes });
const { selects, queryKey } = useAttrSelectsList();
const selectsActions = useSelectsActions({ queryKey });
const {
search: selectSearch,
setSearch: setSelectSearch,
filteredSelects,
} = useFilteredSelects({ selects });
return {
attributes: filteredAttributes,
attributesActions,
attrSearch,
setAttrSearch,
selects: filteredSelects,
selectsActions,
selectSearch,
setSelectSearch,
};
};
export const [AttributesContextProvider, useAttributesContext] =
makeContext<AttributesContextState>(useAttributesContextState, "Attribute");

View File

@ -1,49 +0,0 @@
"use client";
import React, { FC } from "react";
import { Drawer } from "@mantine/core";
import EditorBody from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/EditorBody";
import { SelectEditorContextProvider } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import { DrawerProps } from "@/drawers/types";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { AttrSelectSchema, UpdateAttrSelectSchema } from "@/lib/client";
type Props = {
select: AttrSelectSchema;
onSelectChange: (
values: UpdateAttrSelectSchema,
onSuccess: () => void
) => void;
};
const AttrSelectEditorDrawer: FC<DrawerProps<Props>> = ({
onClose,
opened,
props,
}) => {
const isMobile = useIsMobile();
return (
<Drawer
size={isMobile ? "100%" : "30%"}
title={"Редактирование справочника"}
position={"left"}
onClose={onClose}
removeScrollProps={{ allowPinchZoom: true }}
withCloseButton
opened={opened}
trapFocus={false}
styles={{
body: {
display: "flex",
flexDirection: "column",
},
}}>
<SelectEditorContextProvider {...props}>
<EditorBody />
</SelectEditorContextProvider>
</Drawer>
);
};
export default AttrSelectEditorDrawer;

View File

@ -1,38 +0,0 @@
import { Button, Flex, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import { UpdateAttrSelectSchema } from "@/lib/client";
const CommonInfoEditor = () => {
const { select, onSelectChange } = useSelectEditorContext();
const form = useForm<UpdateAttrSelectSchema>({
initialValues: select || {
name: "",
},
validate: {
name: name => !name && "Введите название",
},
});
return (
<form onSubmit={form.onSubmit(values => onSelectChange(values))}>
<Flex
gap={"xs"}
direction={"column"}>
<TextInput
label={"Название справочника"}
{...form.getInputProps("name")}
data-autofocus
/>
<Button
variant={"default"}
type={"submit"}>
Сохранить
</Button>
</Flex>
</form>
);
};
export default CommonInfoEditor;

View File

@ -1,47 +0,0 @@
import { IconCheck } from "@tabler/icons-react";
import { Flex, TextInput } from "@mantine/core";
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
const CreateOptionButton = () => {
const {
optionsActions: {
isCreatingOption,
createOptionForm,
onStartCreating,
onFinishCreating,
},
} = useSelectEditorContext();
if (!isCreatingOption) {
return (
<Flex flex={1}>
<InlineButton
fullWidth
onClick={onStartCreating}>
Добавить опцию
</InlineButton>
</Flex>
);
}
return (
<Flex
gap={"xs"}
flex={1}>
<TextInput
{...createOptionForm.getInputProps("name")}
flex={1}
placeholder={"Название"}
/>
<ActionIconWithTip
tipLabel={"Сохранить"}
onClick={onFinishCreating}>
<IconCheck />
</ActionIconWithTip>
</Flex>
);
};
export default CreateOptionButton;

View File

@ -1,22 +0,0 @@
import { Divider, Flex } from "@mantine/core";
import CommonInfoEditor from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/CommonInfoEditor";
import CreateOptionButton from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/CreateOptionButton";
import OptionsTable from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/OptionsTable";
const EditorBody = () => {
return (
<Flex
gap={"xs"}
direction={"column"}>
<CommonInfoEditor />
<Divider
label={"Опции"}
my={"xs"}
/>
<CreateOptionButton />
<OptionsTable />
</Flex>
);
};
export default EditorBody;

View File

@ -1,82 +0,0 @@
import React, { FC, ReactNode } from "react";
import { IconCheck, IconEdit, IconTrash } from "@tabler/icons-react";
import { Divider, Flex, Group, Stack, TextInput } from "@mantine/core";
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import { AttrOptionSchema } from "@/lib/client";
type Props = {
option: AttrOptionSchema;
renderDraggable?: (item: AttrOptionSchema) => ReactNode;
};
const OptionTableRow: FC<Props> = ({ option, renderDraggable }) => {
const {
optionsActions: {
onStartEditing,
onFinishEditing,
onDelete,
editingOptionsData,
setEditingOptionsData,
},
} = useSelectEditorContext();
const onChange = (
e: React.ChangeEvent<HTMLInputElement>,
optionId: number
) => {
setEditingOptionsData(prev => {
prev.set(optionId, e.currentTarget.value);
return new Map(prev);
});
};
return (
<Stack
gap={"xs"}
mt={"xs"}>
<Group
wrap={"nowrap"}
justify={"space-between"}>
<Group wrap={"nowrap"}>
{renderDraggable && renderDraggable(option)}
{editingOptionsData.has(option.id) ? (
<TextInput
value={editingOptionsData.get(option.id)}
onChange={e => onChange(e, option.id)}
/>
) : (
option.name
)}
</Group>
<Flex
justify={"center"}
gap={"xs"}>
{editingOptionsData.has(option.id) ? (
<ActionIconWithTip
onClick={() => onFinishEditing(option)}
tipLabel={"Сохранить"}>
<IconCheck />
</ActionIconWithTip>
) : (
<ActionIconWithTip
onClick={() => onStartEditing(option)}
tipLabel={"Редактировать"}>
<IconEdit />
</ActionIconWithTip>
)}
<ActionIconWithTip
color={"red"}
onClick={() => onDelete(option)}
tipLabel={"Удалить"}>
<IconTrash />
</ActionIconWithTip>
</Flex>
</Group>
<Divider />
</Stack>
);
};
export default OptionTableRow;

View File

@ -1,38 +0,0 @@
import React from "react";
import { IconGripVertical } from "@tabler/icons-react";
import { Box, Divider, Stack } from "@mantine/core";
import OptionTableRow from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/OptionTableRow";
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
import SortableDnd from "@/components/dnd/SortableDnd";
const OptionsTable = () => {
const { options } = useSelectEditorContext();
const { onDragEnd } = useSelectEditorContext();
const renderDraggable = () => (
<Box p={"xs"}>
<IconGripVertical />
</Box>
);
return (
<Stack gap={0}>
<Divider />
<SortableDnd
initialItems={options}
onDragEnd={onDragEnd}
renderItem={(item, renderDraggable) => (
<OptionTableRow
option={item}
renderDraggable={renderDraggable}
/>
)}
renderDraggable={renderDraggable}
dragHandleStyle={{ width: "auto" }}
vertical
/>
</Stack>
);
};
export default OptionsTable;

View File

@ -1,62 +0,0 @@
"use client";
import useAttrOptionsList from "@/app/attributes/drawers/AttrSelectEditorDrawer/hooks/useAttrOptionsList";
import {
AttrOptionSchema,
AttrSelectSchema,
UpdateAttrSelectSchema,
} from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
import { notifications } from "@/lib/notifications";
import useOptionsActions, { OptionsActions } from "../hooks/useOptionsActions";
type SelectEditorContextState = {
select: AttrSelectSchema;
onSelectChange: (values: UpdateAttrSelectSchema) => void;
options: AttrOptionSchema[];
optionsActions: OptionsActions;
onDragEnd: (itemId: number, newLexorank: string) => void;
};
type Props = {
select: AttrSelectSchema;
onSelectChange: (
values: UpdateAttrSelectSchema,
onSuccess: () => void
) => void;
};
const useSelectEditorContextState = ({
select,
onSelectChange,
}: Props): SelectEditorContextState => {
const { options, queryKey } = useAttrOptionsList({ selectId: select.id });
const optionsActions = useOptionsActions({ queryKey, select, options });
const onSelectChangeWithMsg = (values: UpdateAttrSelectSchema) => {
onSelectChange(values, () => {
notifications.success({
message: "Название справочника сохранено",
});
});
};
const onDragEnd = (itemId: number, newLexorank: string) => {
optionsActions.onUpdate(itemId, { lexorank: newLexorank });
};
return {
select,
onSelectChange: onSelectChangeWithMsg,
options,
optionsActions,
onDragEnd,
};
};
export const [SelectEditorContextProvider, useSelectEditorContext] =
makeContext<SelectEditorContextState, Props>(
useSelectEditorContextState,
"SelectEditor"
);

View File

@ -1,68 +0,0 @@
import { LexoRank } from "lexorank";
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
import {
AttrOptionSchema,
CreateAttrOptionSchema,
UpdateAttrOptionSchema,
} from "@/lib/client";
import {
createAttrOptionMutation,
deleteAttrOptionMutation,
updateAttrOptionMutation,
} from "@/lib/client/@tanstack/react-query.gen";
import { getNewLexorank } from "@/utils/lexorank/generation";
import { getMaxByLexorank } from "@/utils/lexorank/max";
type Props = {
queryKey: any[];
options: AttrOptionSchema[];
};
export type AttrOptionsCrud = {
onCreate: (
data: Partial<CreateAttrOptionSchema>,
onSuccess?: () => void
) => void;
onUpdate: (
optionId: number,
option: UpdateAttrOptionSchema,
onSuccess?: () => void
) => void;
onDelete: (option: AttrOptionSchema, onSuccess?: () => void) => void;
};
export const useAttrOptionsCrud = ({
queryKey,
options,
}: Props): AttrOptionsCrud => {
return useCrudOperations<
AttrOptionSchema,
UpdateAttrOptionSchema,
CreateAttrOptionSchema
>({
key: "getAttrOptions",
queryKey,
mutations: {
create: createAttrOptionMutation(),
update: updateAttrOptionMutation(),
delete: deleteAttrOptionMutation(),
},
getCreateEntity: data => {
const lastOption = getMaxByLexorank(options);
const newLexorank = getNewLexorank(
lastOption ? LexoRank.parse(lastOption.lexorank) : null
);
return {
name: data.name!,
selectId: data.selectId!,
lexorank: newLexorank.toString(),
};
},
getUpdateEntity: (old, update) => ({
...old,
name: update.name ?? old.name,
lexorank: update.lexorank ?? old.lexorank,
}),
getDeleteConfirmTitle: () => "Удаление опции",
});
};

View File

@ -1,38 +0,0 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { AttrOptionSchema } from "@/lib/client";
import {
getAttrOptionsOptions,
getAttrOptionsQueryKey,
} from "@/lib/client/@tanstack/react-query.gen";
import { sortByLexorank } from "@/utils/lexorank/sort";
type Props = {
selectId: number;
};
const useAttrOptionsList = ({ selectId }: Props) => {
const queryClient = useQueryClient();
const options = { path: { selectId } };
const { data, refetch } = useQuery(getAttrOptionsOptions(options));
const queryKey = getAttrOptionsQueryKey(options);
const setOptions = (options: AttrOptionSchema[]) => {
queryClient.setQueryData(
queryKey,
(old: { items: AttrOptionSchema[] }) => ({
...old,
items: options,
})
);
};
return {
options: sortByLexorank(data?.items ?? []),
setOptions,
refetch,
queryKey,
};
};
export default useAttrOptionsList;

View File

@ -1,107 +0,0 @@
import { Dispatch, SetStateAction, useState } from "react";
import { useForm, UseFormReturnType } from "@mantine/form";
import {
AttrOptionSchema,
AttrSelectSchema,
CreateAttrOptionSchema,
UpdateAttrOptionSchema,
} from "@/lib/client";
import { notifications } from "@/lib/notifications";
import { useAttrOptionsCrud } from "@/app/attributes/drawers/AttrSelectEditorDrawer/hooks/useAttrOptionsCrud";
type Props = {
queryKey: any[];
select: AttrSelectSchema;
options: AttrOptionSchema[];
};
export type OptionsActions = {
isCreatingOption: boolean;
createOptionForm: UseFormReturnType<CreateAttrOptionSchema>;
onStartCreating: () => void;
onFinishCreating: () => void;
editingOptionsData: Map<number, string>;
setEditingOptionsData: Dispatch<SetStateAction<Map<number, string>>>;
onStartEditing: (option: AttrOptionSchema) => void;
onFinishEditing: (option: AttrOptionSchema) => void;
onUpdate: (optionId: number, data: UpdateAttrOptionSchema) => void;
onDelete: (option: AttrOptionSchema) => void;
};
const useOptionsActions = ({ queryKey, select, options }: Props) => {
const [isCreatingOption, setIsCreatingOption] = useState<boolean>(false);
const [editingOptionsData, setEditingOptionsData] = useState<
Map<number, string>
>(new Map());
const createOptionForm = useForm<CreateAttrOptionSchema>({
initialValues: {
name: "",
lexorank: "",
selectId: select.id,
},
validate: {
name: name => !name && "Введите название",
},
});
const optionCrud = useAttrOptionsCrud({ queryKey, options });
const onStartCreating = () => {
setIsCreatingOption(true);
};
const onFinishCreating = () => {
if (createOptionForm.validate().hasErrors) return;
optionCrud.onCreate(createOptionForm.values, () => {
notifications.success({ message: "Опция успешно создана" });
createOptionForm.reset();
setIsCreatingOption(false);
});
};
const onStartEditing = (option: AttrOptionSchema) => {
setEditingOptionsData(prev => {
prev.set(option.id, option.name);
return new Map(prev);
});
};
const onFinishEditing = (option: AttrOptionSchema) => {
if (!editingOptionsData.has(option.id)) return;
const newName = editingOptionsData.get(option.id);
if (!newName) {
notifications.error({ message: "Название не может быть пустым" });
return;
}
optionCrud.onUpdate(option.id, { ...option, name: newName }, () => {
notifications.success({ message: "Опция сохранена" });
setEditingOptionsData(prev => {
prev.delete(option.id);
return new Map(prev);
});
});
};
const onDelete = (option: AttrOptionSchema) => {
optionCrud.onDelete(option, () =>
notifications.success({ message: "Опция удалена" })
);
};
const onUpdate = optionCrud.onUpdate;
return {
isCreatingOption,
createOptionForm,
onStartCreating,
onFinishCreating,
editingOptionsData,
setEditingOptionsData,
onStartEditing,
onFinishEditing,
onDelete,
onUpdate,
};
};
export default useOptionsActions;

View File

@ -1,72 +0,0 @@
"use client";
import { useMemo } from "react";
import { IconCheck, IconX } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable";
import { Box, Center } from "@mantine/core";
import AttributeTableActions from "@/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions";
import AttributeDefaultValue from "@/components/ui/AttributeDefaultValue/AttributeDefaultValue";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { AttributeSchema } from "@/lib/client";
import { useAttributesContext } from "../contexts/AttributesContext";
const useAttributesTableColumns = () => {
const isMobile = useIsMobile();
const { attributesActions } = useAttributesContext();
const renderCheck = (value: boolean) => (value ? <IconCheck /> : <IconX />);
return useMemo(
() =>
[
{
title: "Название атрибута",
accessor: "label",
},
{
title: "Тип",
accessor: "type.name",
render: attr =>
attr.type.type === "select"
? `Выбор "${attr.label}"`
: attr.type.name,
},
{
title: "Значение по умолчанию",
accessor: "defaultValue",
render: attr => <AttributeDefaultValue attribute={attr} />,
},
{
title: isMobile
? "Синх. в группе"
: "Синхронизировано в группе",
accessor: "isApplicableToGroup",
render: attr => renderCheck(attr.isApplicableToGroup),
},
{
title: "Может быть пустым",
accessor: "isNullable",
render: attr => renderCheck(attr.isNullable),
},
{
title: "Описаниие",
accessor: "description",
render: attr => <Box>{attr.description}</Box>,
},
{
accessor: "actions",
title: <Center>Действия</Center>,
width: "0%",
render: attribute => (
<AttributeTableActions
attribute={attribute}
onUpdate={attributesActions.onUpdate}
onDelete={attributesActions.onDelete}
/>
),
},
] as DataTableColumn<AttributeSchema>[],
[isMobile]
);
};
export default useAttributesTableColumns;

View File

@ -1,29 +0,0 @@
import { useMemo, useState } from "react";
import { AttributeSchema } from "@/lib/client";
type Props = {
attributes: AttributeSchema[];
};
const useFilteredAttributes = ({ attributes }: Props) => {
const [search, setSearch] = useState<string>("");
const filteredAttributes = useMemo(
() =>
attributes.filter(
attr =>
attr.type.name.includes(search) ||
attr.label.includes(search) ||
attr.description.includes(search)
),
[attributes, search]
);
return {
search,
setSearch,
filteredAttributes,
};
};
export default useFilteredAttributes;

View File

@ -1,23 +0,0 @@
import { useMemo, useState } from "react";
import { AttrSelectSchema } from "@/lib/client";
type Props = {
selects: AttrSelectSchema[];
};
const useFilteredSelects = ({ selects }: Props) => {
const [search, setSearch] = useState<string>("");
const filteredSelects = useMemo(
() => selects.filter(s => s.name.includes(search)),
[selects, search]
);
return {
search,
setSearch,
filteredSelects,
};
};
export default useFilteredSelects;

View File

@ -1,50 +0,0 @@
import { modals } from "@mantine/modals";
import { useDrawersContext } from "@/drawers/DrawersContext";
import { useAttrSelectsCrud } from "@/hooks/cruds/useSelectsCrud";
import { AttrSelectSchema } from "@/lib/client";
type Props = {
queryKey: any[];
};
export type SelectsActions = {
onCreate: () => void;
onUpdate: (select: AttrSelectSchema) => void;
onDelete: (select: AttrSelectSchema) => void;
};
const useSelectsActions = (props: Props): SelectsActions => {
const attrSelectsCrud = useAttrSelectsCrud(props);
const { openDrawer } = useDrawersContext();
const onCreate = () => {
modals.openContextModal({
modal: "enterNameModal",
title: "Создание справочника",
innerProps: {
onChange: values => attrSelectsCrud.onCreate(values),
},
});
};
const onUpdate = (select: AttrSelectSchema) => {
openDrawer({
key: "attrSelectEditorDrawer",
props: {
onSelectChange: (values, onSuccess) =>
attrSelectsCrud.onUpdate(select.id, values, onSuccess),
select,
},
});
};
const onDelete = attrSelectsCrud.onDelete;
return {
onCreate,
onUpdate,
onDelete,
};
};
export default useSelectsActions;

View File

@ -1,38 +0,0 @@
"use client";
import { useMemo } from "react";
import { DataTableColumn } from "mantine-datatable";
import { Center } from "@mantine/core";
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { AttrSelectSchema } from "@/lib/client";
import { useAttributesContext } from "../contexts/AttributesContext";
const useSelectsTableColumns = () => {
const isMobile = useIsMobile();
const { selectsActions } = useAttributesContext();
return useMemo(
() =>
[
{
title: "Название справочника",
accessor: "name",
},
{
accessor: "actions",
title: <Center>Действия</Center>,
width: "0%",
render: select => (
<UpdateDeleteTableActions
onDelete={() => selectsActions.onDelete(select)}
onChange={() => selectsActions.onUpdate(select)}
/>
),
},
] as DataTableColumn<AttrSelectSchema>[],
[isMobile]
);
};
export default useSelectsTableColumns;

View File

@ -1,22 +0,0 @@
import { Suspense } from "react";
import { Center, Loader } from "@mantine/core";
import PageBody from "@/app/attributes/components/PageBody";
import { AttributesContextProvider } from "@/app/attributes/contexts/AttributesContext";
import PageContainer from "@/components/layout/PageContainer/PageContainer";
export default async function AttributesPage() {
return (
<Suspense
fallback={
<Center h="50vh">
<Loader size="lg" />
</Center>
}>
<PageContainer>
<AttributesContextProvider>
<PageBody />
</AttributesContextProvider>
</PageContainer>
</Suspense>
);
}

View File

@ -1,6 +0,0 @@
enum AttributePageView {
ATTRIBUTES,
SELECTS,
}
export default AttributePageView;

View File

@ -1,41 +0,0 @@
import React, { FC } from "react";
import { IconCheckbox, IconDotsVertical, IconTrash } from "@tabler/icons-react";
import { Box, Menu } from "@mantine/core";
import DropdownMenuItem from "@/components/ui/DropdownMenuItem/DropdownMenuItem";
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
type Props = {
onDelete: () => void;
startDealsSelecting: () => void;
};
const GroupMenu: FC<Props> = ({ onDelete, startDealsSelecting }) => {
return (
<Menu>
<Menu.Target>
<Box
px={"md"}
style={{ cursor: "pointer" }}
onClick={e => e.stopPropagation()}>
<ThemeIcon size={"sm"}>
<IconDotsVertical />
</ThemeIcon>
</Box>
</Menu.Target>
<Menu.Dropdown>
<DropdownMenuItem
onClick={onDelete}
icon={<IconTrash />}
label={"Удалить группу"}
/>
<DropdownMenuItem
onClick={startDealsSelecting}
icon={<IconCheckbox />}
label={"Добавить/удалить сделки"}
/>
</Menu.Dropdown>
</Menu>
);
};
export default GroupMenu;

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { IconChevronLeft, IconSettings } from "@tabler/icons-react"; import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
import { Box, Group, Stack, Title } from "@mantine/core"; import { Box, Group, Stack, Text } from "@mantine/core";
import Boards from "@/app/deals/components/shared/Boards/Boards"; import Boards from "@/app/deals/components/shared/Boards/Boards";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
@ -51,7 +51,7 @@ const MainBlockHeader = () => {
onClick={openProjectsEditorDrawer}> onClick={openProjectsEditorDrawer}>
<IconChevronLeft /> <IconChevronLeft />
</Box> </Box>
<Title order={6}>{selectedProject?.name}</Title> <Text>{selectedProject?.name}</Text>
<Box <Box
p={"md"} p={"md"}
onClick={openBoardsEditorDrawer}> onClick={openBoardsEditorDrawer}>

View File

@ -1,13 +1,11 @@
.create-button { .create-button {
cursor: pointer; cursor: pointer;
min-height: max-content; min-height: max-content;
border: 1px dashed;
@mixin light { @mixin light {
background-color: var(--color-light-white-blue); background-color: var(--color-light-white-blue);
border-color: lightblue;
} }
@mixin dark { @mixin dark {
background-color: var(--mantine-color-dark-7); background-color: var(--mantine-color-dark-7);
border-color: var(--mantine-color-dark-5);
} }
} }

View File

@ -1,47 +1,12 @@
.container { .container {
flex: 1;
padding: 0; padding: 0;
border: 1px dashed;
@mixin light { @mixin light {
background-color: var(--color-light-white-blue); background-color: var(--color-light-white-blue);
border-color: lightblue;
} }
@mixin dark { @mixin dark {
background-color: var(--mantine-color-dark-7); background-color: var(--mantine-color-dark-7);
border-color: var(--mantine-color-dark-5);
}
}
.container-selected {
border: 2px dashed !important;
@mixin light {
border-color: dodgerblue !important;
}
@mixin dark {
border-color: dodgerblue !important;
}
}
.container-mainly-selected {
border: 2px solid;
@mixin light {
border-color: dodgerblue !important;
}
@mixin dark {
border-color: dodgerblue !important;
}
}
.container-in-group {
padding: 0;
border: 1px dashed;
@mixin light {
background-color: var(--color-light-aqua);
border-color: lightblue;
}
@mixin dark {
background-color: var(--mantine-color-dark-6);
border-color: var(--mantine-color-dark-5);
} }
} }

View File

@ -1,33 +1,21 @@
import { IconCategoryPlus } from "@tabler/icons-react"; import { Box, Card, Group, Pill, Stack, Text } from "@mantine/core";
import classNames from "classnames";
import { useContextMenu } from "mantine-contextmenu";
import { Box, Card, Group, 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 { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import { useDrawersContext } from "@/drawers/DrawersContext"; import { useDrawersContext } from "@/drawers/DrawersContext";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { DealSchema } from "@/lib/client"; import { DealSchema } from "@/lib/client";
import { ModuleNames } from "@/modules/modules"; import { ModuleNames } from "@/modules/modules";
import styles from "./DealCard.module.css"; import styles from "./DealCard.module.css";
import DealTags from "@/components/ui/DealTags/DealTags";
type Props = { type Props = {
deal: DealSchema; deal: DealSchema;
isInGroup?: boolean;
}; };
const DealCard = ({ deal, isInGroup = false }: Props) => { const DealCard = ({ deal }: Props) => {
const { selectedProject, modulesSet } = useProjectsContext(); const { selectedProject, modulesSet } = useProjectsContext();
const { dealsCrud, refetchDeals, groupDealsSelection } = useDealsContext(); const { dealsCrud, refetchDeals } = useDealsContext();
const { openDrawer } = useDrawersContext(); const { openDrawer } = useDrawersContext();
const isMobile = useIsMobile();
const onClick = () => { const onClick = () => {
if (groupDealsSelection.isDealsSelecting) {
groupDealsSelection.toggleDeal(deal);
return;
}
openDrawer({ openDrawer({
key: "dealEditorDrawer", key: "dealEditorDrawer",
props: { props: {
@ -40,38 +28,10 @@ const DealCard = ({ deal, isInGroup = false }: Props) => {
}); });
}; };
const { showContextMenu } = useContextMenu();
const dealContextMenu =
deal.group || isMobile
? []
: [
{
key: "startGroupForming",
onClick: () =>
groupDealsSelection.startSelectingWithDeal(deal.id),
title: "Создать группу",
icon: <IconCategoryPlus />,
},
];
const getSelectedStyles = () => {
if (groupDealsSelection.selectedBaseDealId === deal.id) {
return styles["container-mainly-selected"];
}
if (groupDealsSelection.selectedDealIds.has(deal.id)) {
return styles["container-selected"];
}
};
return ( return (
<Card <Card
onClick={onClick} onClick={onClick}
className={classNames( className={styles.container}>
getSelectedStyles(),
isInGroup ? styles["container-in-group"] : styles.container
)}
onContextMenu={showContextMenu(dealContextMenu)}>
<Group <Group
justify={"space-between"} justify={"space-between"}
wrap={"nowrap"} wrap={"nowrap"}
@ -101,7 +61,10 @@ const DealCard = ({ deal, isInGroup = false }: Props) => {
</> </>
)} )}
</Stack> </Stack>
{!deal.group && <DealTags dealId={deal.id} tags={deal.tags} />} <Group gap={"xs"}>
<Pill className={styles["first-tag"]}>Срочно</Pill>
<Pill className={styles["second-tag"]}>Бесплатно</Pill>
</Group>
</Stack> </Stack>
</Card> </Card>
); );

View File

@ -0,0 +1,89 @@
import { FC, useEffect, useState } from "react";
import { IconGripHorizontal } from "@tabler/icons-react";
import { Flex, rem, TextInput, useMantineColorScheme } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
import FulfillmentGroupInfo from "@/app/deals/components/shared/DealGroupCard/components/FulfillmentGroupInfo";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import { notifications } from "@/lib/notifications";
import { ModuleNames } from "@/modules/modules";
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
type Props = {
group: GroupWithDealsSchema;
};
const DealGroupCard: FC<Props> = ({ group }) => {
const theme = useMantineColorScheme();
const [name, setName] = useState<string>(group.name ?? "");
const [debouncedName] = useDebouncedValue(name, 200);
const { modulesSet } = useProjectsContext();
const isServicesAndProductsIncluded = modulesSet.has(
ModuleNames.FULFILLMENT_BASE
);
const updateName = () => {
if (debouncedName === group.name) return;
CardGroupService.updateCardGroup({
requestBody: {
data: {
...group,
name: debouncedName,
},
},
}).then(response => {
if (response.ok) return;
setName(group.name || "");
notifications.guess(response.ok, { message: response.message });
});
};
useEffect(() => {
updateName();
}, [debouncedName]);
return (
<Flex
style={{
border: "dashed var(--item-border-size) var(--mantine-color-default-border)",
borderRadius: "0.5rem",
}}
p={rem(5)}
py={"xs"}
bg={
theme.colorScheme === "dark"
? "var(--mantine-color-dark-5)"
: "var(--mantine-color-gray-1)"
}
gap={"xs"}
direction={"column"}>
<Flex
justify={"space-between"}
align={"center"}
gap={"xs"}
px={"xs"}>
<TextInput
value={name}
onChange={event => setName(event.currentTarget.value)}
variant={"unstyled"}
/>
<IconGripHorizontal />
</Flex>
<Flex
direction={"column"}
gap={"xs"}>
{group.deals?.map(deal => (
<DealCard
key={deal.id}
deal={deal}
/>
))}
</Flex>
{isServicesAndProductsIncluded && (
<FulfillmentGroupInfo group={group} />
)}
</Flex>
);
};
export default DealGroupCard;

View File

@ -0,0 +1,51 @@
import { Flex, Text, useMantineColorScheme } from "@mantine/core";
import { FC, useMemo } from "react";
import { DealGroupSchema } from "@/lib/client";
type Props = {
group: DealGroupSchema;
}
const FulfillmentGroupInfo: FC<Props> = ({ group }) => {
const theme = useMantineColorScheme();
const totalPrice = useMemo(
() =>
group.deals?.reduce((acc, deal) => acc + (deal.totalPrice ?? 0), 0),
[group.deals]
);
const totalProducts = useMemo(
() =>
group.deals?.reduce(
(acc, deal) => acc + (deal.productsQuantity ?? 0),
0
),
[group.deals]
);
return (
<Flex
p={"xs"}
direction={"column"}
bg={
theme.colorScheme === "dark"
? "var(--mantine-color-dark-6)"
: "var(--mantine-color-gray-2)"
}
style={{ borderRadius: "0.5rem" }}>
<Text
c={"gray.6"}
size={"xs"}>
Сумма: {totalPrice?.toLocaleString("ru-RU")} руб.
</Text>
<Text
c={"gray.6"}
size={"xs"}>
Всего товаров: {totalProducts?.toLocaleString("ru-RU")}{" "}
шт.
</Text>
</Flex>
)
}
export default FulfillmentGroupInfo;

View File

@ -1,22 +0,0 @@
.group-container {
border: 1px dashed;
@mixin light {
background-color: var(--color-light-white-blue);
border-color: lightblue;
}
@mixin dark {
background-color: var(--mantine-color-dark-7);
border-color: var(--mantine-color-dark-5);
}
}
.selected-group {
border: 2px solid;
@mixin light {
border-color: dodgerblue;
}
@mixin dark {
border-color: dodgerblue;
}
}

View File

@ -1,101 +0,0 @@
import React, { FC, useEffect, useState } from "react";
import { IconCheckbox, IconTrash } from "@tabler/icons-react";
import classNames from "classnames";
import { useContextMenu } from "mantine-contextmenu";
import { Flex, Stack, TextInput } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import GroupMenu from "@/app/deals/components/mobile/GroupMenu/GroupMenu";
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import DealTags from "@/components/ui/DealTags/DealTags";
import useIsMobile from "@/hooks/utils/useIsMobile";
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
import styles from "./DealsGroup.module.css";
type Props = {
group: GroupWithDealsSchema;
};
const DealsGroup: FC<Props> = ({ group }) => {
const [groupName, setGroupName] = useState(group.name ?? "");
const [debouncedGroupName] = useDebouncedValue(groupName, 600);
const {
groupsCrud,
groupDealsSelection: {
startSelectingWithExistingGroup,
selectedGroupId,
},
} = useDealsContext();
const { showContextMenu } = useContextMenu();
const isMobile = useIsMobile();
useEffect(() => {
if (debouncedGroupName === group.name) return;
groupsCrud.onUpdate(group.id, { name: debouncedGroupName });
}, [debouncedGroupName]);
const dealContextMenu = isMobile
? []
: [
{
key: "delete",
onClick: () => groupsCrud.onDelete(group.id),
title: "Удалить группу",
icon: <IconTrash />,
},
{
key: "startDealsSelecting",
onClick: () => startSelectingWithExistingGroup(group),
title: "Добавить/удалить сделки",
icon: <IconCheckbox />,
},
];
return (
<Stack
className={classNames(
styles["group-container"],
selectedGroupId === group.id && styles["selected-group"]
)}
gap={"xs"}
bdrs={"lg"}
p={"xs"}
onContextMenu={showContextMenu(dealContextMenu)}>
<Flex
mx={"xs"}
align={"center"}
w={"100%"}>
<TextInput
value={groupName}
onChange={e => setGroupName(e.target.value)}
variant={"unstyled"}
onKeyDown={e => e.stopPropagation()}
flex={1}
/>
{isMobile && (
<GroupMenu
startDealsSelecting={() =>
startSelectingWithExistingGroup(group)
}
onDelete={() => groupsCrud.onDelete(group.id)}
/>
)}
</Flex>
{group.items.map(deal => (
<DealCard
deal={deal}
isInGroup
key={deal.id}
/>
))}
{group.items.length > 0 && (
<DealTags
groupId={group.id}
tags={group.items[0].tags}
/>
)}
</Stack>
);
};
export default DealsGroup;

View File

@ -1,92 +1,67 @@
"use client"; "use client";
import React, { FC, ReactNode } from "react"; import React, { FC } from "react";
import { Box } from "@mantine/core";
import DealCard from "@/app/deals/components/shared/DealCard/DealCard"; import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
import DealsGroup from "@/app/deals/components/shared/DealsGroup/DealsGroup";
import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader"; import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader";
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { useDealsContext } from "@/app/deals/contexts/DealsContext"; import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd"; import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd"; import DndFunnel from "@/components/dnd-pragmatic/DndFunnel/DndFunnel";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { DealSchema, StatusSchema } from "@/lib/client";
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
import { sortByLexorank } from "@/utils/lexorank/sort"; import { sortByLexorank } from "@/utils/lexorank/sort";
const Funnel: FC = () => { const Funnel: FC = () => {
const { selectedBoard } = useBoardsContext(); const { statuses, setStatuses, statusesCrud } = useStatusesContext();
const { dealsWithoutGroup, groupsWithDeals } = useDealsContext(); const { dealsWithoutGroup, groupsWithDeals, deals, setDeals, dealsCrud } =
const isMobile = useIsMobile(); useDealsContext();
const { sortedStatuses, handleDragOver, handleDragEnd, swiperRef } = const updateStatus = (statusId: number, lexorank: string) => {
useDealsAndStatusesDnd(); setStatuses(
statuses.map(status =>
status.id === statusId ? { ...status, lexorank } : status
)
);
statusesCrud.onUpdate(statusId, { lexorank });
};
const updateDeal = (dealId: number, lexorank: string, statusId: number) => {
const status = statuses.find(s => s.id === statusId);
if (!status) return;
setDeals(
deals.map(deal =>
deal.id === dealId ? { ...deal, lexorank, status } : deal
)
);
dealsCrud.onUpdate(dealId, { lexorank, statusId });
};
return ( return (
<FunnelDnd<StatusSchema, DealSchema, GroupWithDealsSchema> <DndFunnel
containers={sortedStatuses} columns={statuses}
itemsAndGroups={sortByLexorank([ updateColumn={updateStatus}
...dealsWithoutGroup, items={dealsWithoutGroup}
...groupsWithDeals, groups={groupsWithDeals}
])} updateItem={updateDeal}
onDragOver={handleDragOver} getColumnItemsGroups={statusId =>
onDragEnd={handleDragEnd}
swiperRef={swiperRef}
getItemsByContainer={(status: StatusSchema) =>
sortByLexorank([ sortByLexorank([
...dealsWithoutGroup.filter( ...dealsWithoutGroup.filter(d => d.status.id === statusId),
deal => deal.status.id === status.id
),
...groupsWithDeals.filter( ...groupsWithDeals.filter(
group => group.items[0].status.id === status.id g =>
g.items.length > 0 &&
g.items[0].status.id === statusId
), ),
]) ])
} }
renderContainer={( renderColumnHeader={status => (
status: StatusSchema, <StatusColumnHeader status={status} />
funnelColumnComponent: ReactNode,
renderDraggable,
index
) => (
<StatusColumnWrapper
status={status}
renderHeader={renderDraggable}
createFormEnabled={index === 0}>
{funnelColumnComponent}
</StatusColumnWrapper>
)} )}
renderContainerHeader={status => ( renderItem={deal => (
<StatusColumnHeader
status={status}
isDragging={false}
/>
)}
renderItem={(deal: DealSchema) => (
<DealCard <DealCard
key={deal.id} key={deal.id}
deal={deal} deal={deal}
/> />
)} )}
renderGroup={(group: GroupWithDealsSchema) => ( renderGroup={group => <Box flex={1}>{group.name}</Box>}
<DealsGroup
key={`${group.id}group`}
group={group}
/>
)}
renderContainerOverlay={(status: StatusSchema, children) => (
<StatusColumnWrapper
status={status}
renderHeader={() => (
<StatusColumnHeader
status={status}
isDragging
/>
)}>
{children}
</StatusColumnWrapper>
)}
disabledColumns={isMobile}
isCreatingContainerEnabled={!!selectedBoard}
/> />
); );
}; };

View File

@ -1,9 +0,0 @@
.shadow {
@mixin light {
box-shadow: var(--light-shadow);
}
@mixin dark {
background-color: var(--mantine-color-dark-7);
box-shadow: var(--dark-shadow);
}
}

View File

@ -1,52 +0,0 @@
"use client";
import { Affix, Flex, Stack, Title, Transition } from "@mantine/core";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
import styles from "./GroupDealsSelectionAffix.module.css";
const GroupDealsSelectionAffix = () => {
const {
groupDealsSelection: {
finishDealsSelecting,
cancelDealsSelecting,
isDealsSelecting,
},
} = useDealsContext();
return (
<Affix position={{ bottom: 35, right: 35 }}>
<Transition
transition="slide-up"
mounted={isDealsSelecting}>
{transitionStyles => (
<Stack
bdrs={"xl"}
bd={"1px solid var(--mantine-color-default-border"}
className={styles.shadow}
p={"md"}
gap={"md"}
style={transitionStyles}>
<Title
order={5}
ta={"center"}>
Выбор сделок для группы
</Title>
<Flex gap={"xs"}>
<InlineButton onClick={cancelDealsSelecting}>
Отмена
</InlineButton>
<InlineButton
variant={"filled"}
onClick={finishDealsSelecting}>
Сохранить
</InlineButton>
</Flex>
</Stack>
)}
</Transition>
</Affix>
);
};
export default GroupDealsSelectionAffix;

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { Box } from "@mantine/core"; import { Flex } from "@mantine/core";
import TopToolPanel from "@/app/deals/components/desktop/TopToolPanel/TopToolPanel"; import TopToolPanel from "@/app/deals/components/desktop/TopToolPanel/TopToolPanel";
import { import {
BoardView, BoardView,
@ -10,12 +10,10 @@ import {
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { DealsContextProvider } from "@/app/deals/contexts/DealsContext"; import { DealsContextProvider } from "@/app/deals/contexts/DealsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import useCreateFirstProject from "@/app/deals/hooks/useCreateFirstProject";
import useView from "@/app/deals/hooks/useView"; import useView from "@/app/deals/hooks/useView";
import PageBlock from "@/components/layout/PageBlock/PageBlock"; import PageBlock from "@/components/layout/PageBlock/PageBlock";
const PageBody = () => { const PageBody = () => {
useCreateFirstProject();
const { selectedBoard } = useBoardsContext(); const { selectedBoard } = useBoardsContext();
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
@ -36,7 +34,7 @@ const PageBody = () => {
if (view === "table") { if (view === "table") {
return { withPagination: true, projectId: selectedProject?.id }; return { withPagination: true, projectId: selectedProject?.id };
} }
return { boardId: selectedBoard?.id, projectId: selectedProject?.id }; return { boardId: selectedBoard?.id };
}; };
return ( return (
@ -48,7 +46,11 @@ const PageBody = () => {
<PageBlock <PageBlock
fullScreenMobile fullScreenMobile
style={{ flex: 1 }}> style={{ flex: 1 }}>
<Box h={"100%"}>{getViewContent()}</Box> <Flex
direction={"column"}
h={"100%"}>
{getViewContent()}
</Flex>
</PageBlock> </PageBlock>
</DealsContextProvider> </DealsContextProvider>
); );

View File

@ -2,20 +2,17 @@ import React, { FC } from "react";
import { Group, Text } from "@mantine/core"; import { Group, Text } from "@mantine/core";
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu"; import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext"; import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput"; import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
import { StatusSchema } from "@/lib/client"; import { StatusSchema } from "@/lib/client";
type Props = { type Props = {
status: StatusSchema; status: StatusSchema;
isDragging: boolean;
}; };
const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => { const StatusColumnHeader: FC<Props> = ({ status }) => {
const { statusesCrud, refetchStatuses } = useStatusesContext(); const { statusesCrud, refetchStatuses } = useStatusesContext();
const { selectedBoard } = useBoardsContext(); const { selectedBoard } = useBoardsContext();
const { groupDealsSelection } = useDealsContext();
const handleSave = (value: string) => { const handleSave = (value: string) => {
const newValue = value.trim(); const newValue = value.trim();
@ -30,6 +27,7 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
p={"sm"} p={"sm"}
wrap={"nowrap"} wrap={"nowrap"}
mb={"xs"} mb={"xs"}
w={"100%"}
style={{ style={{
borderBottom: `solid ${status.color} 3px`, borderBottom: `solid ${status.color} 3px`,
}}> }}>
@ -44,14 +42,7 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
}} }}
getChildren={startEditing => ( getChildren={startEditing => (
<> <>
<Text <Text>{status.name}</Text>
style={{
cursor: "grab",
userSelect: "none",
opacity: isDragging ? 0.5 : 1,
}}>
{status.name}
</Text>
<StatusMenu <StatusMenu
board={selectedBoard} board={selectedBoard}
status={status} status={status}
@ -61,9 +52,6 @@ const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
} }
refetchStatuses={refetchStatuses} refetchStatuses={refetchStatuses}
onDeleteStatus={statusesCrud.onDelete} onDeleteStatus={statusesCrud.onDelete}
startDealsSelecting={
groupDealsSelection.startSelecting
}
/> />
</> </>
)} )}

View File

@ -1,6 +1,5 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { import {
IconCheckbox,
IconDotsVertical, IconDotsVertical,
IconEdit, IconEdit,
IconExchange, IconExchange,
@ -22,7 +21,6 @@ type Props = {
onStatusColorChange: (color: string) => void; onStatusColorChange: (color: string) => void;
board: BoardSchema | null; board: BoardSchema | null;
onDeleteStatus: (status: StatusSchema) => void; onDeleteStatus: (status: StatusSchema) => void;
startDealsSelecting?: () => void;
refetchStatuses?: () => void; refetchStatuses?: () => void;
withChangeOrderButton?: boolean; withChangeOrderButton?: boolean;
}; };
@ -33,7 +31,6 @@ const StatusMenu: FC<Props> = ({
onStatusColorChange, onStatusColorChange,
board, board,
onDeleteStatus, onDeleteStatus,
startDealsSelecting,
refetchStatuses, refetchStatuses,
withChangeOrderButton = true, withChangeOrderButton = true,
}) => { }) => {
@ -99,13 +96,6 @@ const StatusMenu: FC<Props> = ({
label={"Изменить порядок"} label={"Изменить порядок"}
/> />
)} )}
{isMobile && startDealsSelecting && (
<DropdownMenuItem
onClick={startDealsSelecting}
icon={<IconCheckbox />}
label={"Создать группу сделок"}
/>
)}
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
); );

View File

@ -1,13 +1,11 @@
import { Space } from "@mantine/core"; import { Space } from "@mantine/core";
import MainBlockHeader from "@/app/deals/components/mobile/MainBlockHeader/MainBlockHeader"; import MainBlockHeader from "@/app/deals/components/mobile/MainBlockHeader/MainBlockHeader";
import Funnel from "@/app/deals/components/shared/Funnel/Funnel"; import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
import GroupDealsSelectionAffix from "@/app/deals/components/shared/GroupDealsSelectionAffix/GroupDealsSelectionAffix";
export const BoardView = () => ( export const BoardView = () => (
<> <>
<MainBlockHeader /> <MainBlockHeader />
<Space h="md" /> <Space h="md" />
<Funnel /> <Funnel />
<GroupDealsSelectionAffix />
</> </>
); );

View File

@ -1,14 +1,10 @@
"use client"; "use client";
import { Dispatch, SetStateAction } from "react"; import React from "react";
import { UseFormReturnType } from "@mantine/form"; import { UseFormReturnType } from "@mantine/form";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext"; import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import useDealsAndGroups from "@/app/deals/hooks/useDealsAndGroups"; import useDealsAndGroups from "@/app/deals/hooks/useDealsAndGroups";
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters"; import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
import useGroupDealsSelection, {
GroupDealsSelection,
} from "@/app/deals/hooks/useGroupDealsSelection";
import useDealGroupCrud, { GroupsCrud } from "@/hooks/cruds/useDealGroupCrud";
import { DealsCrud, useDealsCrud } from "@/hooks/cruds/useDealsCrud"; import { DealsCrud, useDealsCrud } from "@/hooks/cruds/useDealsCrud";
import useDealsList from "@/hooks/lists/useDealsList"; import useDealsList from "@/hooks/lists/useDealsList";
import { SortingForm } from "@/hooks/utils/useSorting"; import { SortingForm } from "@/hooks/utils/useSorting";
@ -23,14 +19,12 @@ type DealsContextState = {
groupsWithDeals: GroupWithDealsSchema[]; groupsWithDeals: GroupWithDealsSchema[];
refetchDeals: () => void; refetchDeals: () => void;
dealsCrud: DealsCrud; dealsCrud: DealsCrud;
groupsCrud: GroupsCrud;
paginationInfo?: PaginationInfoSchema; paginationInfo?: PaginationInfoSchema;
page: number; page: number;
setPage: Dispatch<SetStateAction<number>>; setPage: React.Dispatch<React.SetStateAction<number>>;
dealsFiltersForm: UseFormReturnType<DealsFiltersForm>; dealsFiltersForm: UseFormReturnType<DealsFiltersForm>;
isChangedFilters: boolean; isChangedFilters: boolean;
sortingForm: UseFormReturnType<SortingForm>; sortingForm: UseFormReturnType<SortingForm>;
groupDealsSelection: GroupDealsSelection;
}; };
type Props = { type Props = {
@ -54,25 +48,18 @@ const useDealsContextState = ({
const dealsCrud = useDealsCrud({ const dealsCrud = useDealsCrud({
...dealsListObjects, ...dealsListObjects,
projectId,
boardId, boardId,
statuses, statuses,
}); });
const groupsCrud = useDealGroupCrud();
const groupDealsSelection = useGroupDealsSelection({ groupsCrud });
const { dealsWithoutGroup, groupsWithDeals } = const { dealsWithoutGroup, groupsWithDeals } =
useDealsAndGroups(dealsListObjects); useDealsAndGroups(dealsListObjects);
return { return {
...dealsListObjects, ...dealsListObjects,
dealsCrud,
groupsCrud,
dealsWithoutGroup, dealsWithoutGroup,
groupsWithDeals, groupsWithDeals,
groupDealsSelection, dealsCrud,
}; };
}; };

View File

@ -10,22 +10,16 @@ import makeContext from "@/lib/contextFactory/contextFactory";
import { ModuleNames } from "@/modules/modules"; import { ModuleNames } from "@/modules/modules";
type ProjectsContextState = { type ProjectsContextState = {
projects: ProjectSchema[];
isLoading: boolean;
selectedProject: ProjectSchema | null; selectedProject: ProjectSchema | null;
setSelectedProjectId: (id: number | null) => void; setSelectedProjectId: (id: number | null) => void;
refetchProjects: () => void; refetchProjects: () => void;
projects: ProjectSchema[];
projectsCrud: ProjectsCrud; projectsCrud: ProjectsCrud;
modulesSet: Set<ModuleNames>; modulesSet: Set<ModuleNames>;
}; };
const useProjectsContextState = (): ProjectsContextState => { const useProjectsContextState = (): ProjectsContextState => {
const { const { projects, refetch: refetchProjects, queryKey } = useProjectsList();
projects,
refetch: refetchProjects,
queryKey,
isLoading,
} = useProjectsList();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
@ -40,7 +34,10 @@ const useProjectsContextState = (): ProjectsContextState => {
); );
const modulesSet = useMemo( const modulesSet = useMemo(
() => new Set(selectedProject?.modules.map(m => m.key as ModuleNames)), () =>
new Set(
selectedProject?.builtInModules.map(m => m.key as ModuleNames)
),
[selectedProject] [selectedProject]
); );
@ -57,7 +54,6 @@ const useProjectsContextState = (): ProjectsContextState => {
return { return {
projects, projects,
isLoading,
selectedProject, selectedProject,
refetchProjects, refetchProjects,
setSelectedProjectId: handleSetSelectedProjectId, setSelectedProjectId: handleSetSelectedProjectId,

View File

@ -18,8 +18,8 @@ const BoardsMobileEditorDrawer: FC<DrawerProps<Props>> = ({
}) => { }) => {
return ( return (
<Drawer <Drawer
size={"50%"} size={"100%"}
position={"left"} position={"right"}
onClose={onClose} onClose={onClose}
removeScrollProps={{ allowPinchZoom: true }} removeScrollProps={{ allowPinchZoom: true }}
withCloseButton={false} withCloseButton={false}
@ -30,6 +30,7 @@ const BoardsMobileEditorDrawer: FC<DrawerProps<Props>> = ({
height: "100%", height: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "xs",
}, },
}}> }}>
<BoardsMobileContextProvider project={project}> <BoardsMobileContextProvider project={project}>

View File

@ -1,6 +1,6 @@
import React, { FC, ReactNode } from "react"; import React, { FC, ReactNode } from "react";
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react"; import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
import { Box, Center, Divider, Group, Stack, Title } from "@mantine/core"; import { Box, Center, Divider, Group, Text } from "@mantine/core";
import BoardMobile from "@/app/deals/drawers/BoardsMobileEditorDrawer/components/BoardMobile"; import BoardMobile from "@/app/deals/drawers/BoardsMobileEditorDrawer/components/BoardMobile";
import CreateBoardButton from "@/app/deals/drawers/BoardsMobileEditorDrawer/components/CreateBoardButton"; import CreateBoardButton from "@/app/deals/drawers/BoardsMobileEditorDrawer/components/CreateBoardButton";
import { useBoardsMobileContext } from "@/app/deals/drawers/BoardsMobileEditorDrawer/contexts/BoardsMobileContext"; import { useBoardsMobileContext } from "@/app/deals/drawers/BoardsMobileEditorDrawer/contexts/BoardsMobileContext";
@ -37,7 +37,7 @@ const BoardsDrawerBody: FC<Props> = ({ onClose }) => {
}; };
return ( return (
<Stack gap={"xs"}> <>
<Group justify={"space-between"}> <Group justify={"space-between"}>
<Box <Box
onClick={onClose} onClick={onClose}
@ -45,7 +45,7 @@ const BoardsDrawerBody: FC<Props> = ({ onClose }) => {
<IconChevronLeft /> <IconChevronLeft />
</Box> </Box>
<Center> <Center>
<Title order={6}>{project.name}</Title> <Text>{project.name}</Text>
</Center> </Center>
<Box p={"lg"} /> <Box p={"lg"} />
</Group> </Group>
@ -58,9 +58,8 @@ const BoardsDrawerBody: FC<Props> = ({ onClose }) => {
dragHandleStyle={{ width: "auto" }} dragHandleStyle={{ width: "auto" }}
vertical vertical
/> />
{boards.length > 0 && <Divider />}
<CreateBoardButton onCreateBoard={boardsCrud.onCreate} /> <CreateBoardButton onCreateBoard={boardsCrud.onCreate} />
</Stack> </>
); );
}; };

View File

@ -1,50 +0,0 @@
import { useEffect, useState } from "react";
import { omit } from "lodash";
import ObjectSelect from "@/components/selects/ObjectSelect/ObjectSelect";
import useAttributeOptionsList from "@/hooks/lists/useAttributeOptionsList";
import { AttrOptionSchema } from "@/lib/client";
type Props = {
value: number;
onChange: (val: any) => void;
selectId: number;
error?: string;
label?: string;
};
const AttrOptionSelect = (props: Props) => {
const { options } = useAttributeOptionsList(props);
const [selectedOption, setSelectedOption] = useState<AttrOptionSchema>();
useEffect(() => {
if (!props.value) {
setSelectedOption(undefined);
return;
}
setSelectedOption(options.find(option => option.id === props.value));
}, [props.value, options]);
const restProps = omit(props, ["value, onChange", "selectId"]);
return (
<ObjectSelect
label={"Значение"}
{...restProps}
data={options}
value={selectedOption}
onChange={option => {
setSelectedOption(option);
props.onChange(option.id);
}}
onClear={() => {
setSelectedOption(undefined);
props.onChange(null);
}}
clearable
searchable
/>
);
};
export default AttrOptionSelect;

View File

@ -1,112 +0,0 @@
import { CSSProperties, FC, JSX } from "react";
import { Checkbox, NumberInput, TextInput } from "@mantine/core";
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
import AttrOptionSelect from "@/app/deals/drawers/DealEditorDrawer/components/AttrOptionSelect";
import { DealModuleAttributeSchema } from "@/lib/client";
import { naiveDateTimeStringToUtc } from "@/utils/datetime";
type Props = {
attrInfo: DealModuleAttributeSchema;
value: any;
onChange: (value: any) => void;
error?: string;
style?: CSSProperties;
};
const AttributeValueInput: FC<Props> = ({
attrInfo,
value,
onChange,
error,
style,
}) => {
const commonProps = {
label: attrInfo.label,
style,
error,
};
const renderCheckbox = () => (
<Checkbox
{...commonProps}
checked={Boolean(value)}
onChange={e => onChange(e.currentTarget.checked)}
/>
);
const renderDatePicker = () => (
<DatePickerInput
{...commonProps}
value={value ? new Date(String(value)) : null}
onChange={val => {
onChange(val ? new Date(val) : null);
}}
clearable
locale={"ru"}
valueFormat={"DD.MM.YYYY"}
/>
);
const renderDateTimePicker = () => (
<DateTimePicker
{...commonProps}
value={value ? naiveDateTimeStringToUtc(value) : null}
onChange={val => {
if (!val) return;
const localDate = new Date(val.replace(" ", "T"));
const utcString = localDate
.toISOString()
.substring(0, 19)
.replace("T", " ");
onChange(utcString);
}}
clearable
locale={"ru"}
valueFormat={"DD.MM.YYYY HH:mm"}
/>
);
const renderTextInput = () => (
<TextInput
{...commonProps}
value={value ?? ""}
onChange={e => onChange(e.currentTarget.value)}
/>
);
const renderNumberInput = () => (
<NumberInput
{...commonProps}
allowDecimal={attrInfo.type.type === "float"}
value={value ? Number(value) : undefined}
onChange={value => onChange(Number(value))}
/>
);
const renderSelect = () => {
if (!attrInfo.select?.id) return <></>;
return (
<AttrOptionSelect
{...commonProps}
value={value}
onChange={onChange}
selectId={attrInfo.select.id}
/>
);
};
const renderingFuncMap: Record<string, () => JSX.Element> = {
bool: renderCheckbox,
date: renderDatePicker,
datetime: renderDateTimePicker,
str: renderTextInput,
int: renderNumberInput,
float: renderNumberInput,
select: renderSelect,
};
const render = renderingFuncMap[attrInfo.type.type];
return render();
};
export default AttributeValueInput;

View File

@ -1,9 +1,8 @@
import React, { FC, ReactNode } from "react"; import React, { FC, ReactNode } from "react";
import { IconBlocks, IconEdit, IconHistory } from "@tabler/icons-react"; import { IconEdit, IconHistory } from "@tabler/icons-react";
import { motion } from "framer-motion";
import { Box, Tabs, Text } from "@mantine/core"; import { Box, Tabs, Text } from "@mantine/core";
import DealEditorTabPanel from "@/app/deals/drawers/DealEditorDrawer/components/DealEditorTabPanel";
import TabsList from "@/app/deals/drawers/DealEditorDrawer/components/TabsList"; import TabsList from "@/app/deals/drawers/DealEditorDrawer/components/TabsList";
import CustomTab from "@/app/deals/drawers/DealEditorDrawer/tabs/CustomTab/CustomTab";
import DealStatusHistoryTab from "@/app/deals/drawers/DealEditorDrawer/tabs/DealStatusHistoryTab/DealStatusHistoryTab"; import DealStatusHistoryTab from "@/app/deals/drawers/DealEditorDrawer/tabs/DealStatusHistoryTab/DealStatusHistoryTab";
import GeneralTab from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/GeneralTab"; import GeneralTab from "@/app/deals/drawers/DealEditorDrawer/tabs/GeneralTab/GeneralTab";
import useIsMobile from "@/hooks/utils/useIsMobile"; import useIsMobile from "@/hooks/utils/useIsMobile";
@ -21,6 +20,23 @@ type Props = {
const DealEditorBody: FC<Props> = props => { const DealEditorBody: FC<Props> = props => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
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 getTab = (key: string, label: string, icon: ReactNode) => ( const getTab = (key: string, label: string, icon: ReactNode) => (
<Tabs.Tab <Tabs.Tab
key={key} key={key}
@ -39,14 +55,7 @@ const DealEditorBody: FC<Props> = props => {
if (!props.project) return []; if (!props.project) return [];
const tabs: ReactNode[] = []; const tabs: ReactNode[] = [];
for (const module of props.project.modules) { for (const module of props.project.builtInModules) {
if (!module.isBuiltIn) {
for (const tab of module.tabs) {
tabs.push(getTab(tab.key, tab.label, <IconBlocks />));
}
continue;
}
const moduleInfo = MODULES[module.key]; const moduleInfo = MODULES[module.key];
for (const tab of moduleInfo.tabs) { for (const tab of moduleInfo.tabs) {
if ( if (
@ -66,24 +75,7 @@ const DealEditorBody: FC<Props> = props => {
if (!props.project) return []; if (!props.project) return [];
const tabPanels: ReactNode[] = []; const tabPanels: ReactNode[] = [];
for (const module of props.project.modules) { for (const module of props.project.builtInModules) {
if (!module.isBuiltIn) {
const tabPanel = (
<DealEditorTabPanel
key={module.key}
value={module.key}>
<CustomTab
key={module.key}
moduleId={module.id}
deal={props.value}
/>
</DealEditorTabPanel>
);
tabPanels.push(tabPanel);
continue;
}
const moduleInfo = MODULES[module.key]; const moduleInfo = MODULES[module.key];
for (const tab of moduleInfo.tabs) { for (const tab of moduleInfo.tabs) {
if ( if (
@ -92,14 +84,7 @@ const DealEditorBody: FC<Props> = props => {
) )
continue; continue;
const tabPanel = ( tabPanels.push(getTabPanel(tab.key, tab.tab(props)));
<DealEditorTabPanel
key={tab.key}
value={tab.key}>
{tab.tab(props)}
</DealEditorTabPanel>
);
tabPanels.push(tabPanel);
} }
} }
@ -120,12 +105,8 @@ const DealEditorBody: FC<Props> = props => {
{getModuleTabs()} {getModuleTabs()}
</TabsList> </TabsList>
<DealEditorTabPanel value={"general"}> {getTabPanel("general", <GeneralTab {...props} />)}
<GeneralTab {...props} /> {getTabPanel("history", <DealStatusHistoryTab {...props} />)}
</DealEditorTabPanel>
<DealEditorTabPanel value={"history"}>
<DealStatusHistoryTab {...props} />
</DealEditorTabPanel>
{getModuleTabPanels()} {getModuleTabPanels()}
</Tabs> </Tabs>
); );

View File

@ -1,31 +0,0 @@
import React, { FC, PropsWithChildren } from "react";
import { motion } from "framer-motion";
import { Box, Tabs } from "@mantine/core";
type Props = {
value: string;
};
const DealEditorTabPanel: FC<PropsWithChildren<Props>> = ({
value,
children,
}) => {
return (
<Tabs.Panel
key={value}
value={value}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
<Box
h={"100%"}
w={"100%"}>
{children}
</Box>
</motion.div>
</Tabs.Panel>
);
};
export default DealEditorTabPanel;

View File

@ -1,22 +0,0 @@
import React, { FC } from "react";
import AttributeEditor from "@/components/ui/AttributesEditor/AttributesEditor";
import { DealSchema } from "@/lib/client";
type Props = {
moduleId: number;
deal: DealSchema;
};
const CustomTab: FC<Props> = props => {
return (
<AttributeEditor
{...props}
containerStyle={{
paddingBlock: "var(--mantine-spacing-xs)",
paddingInline: "var(--mantine-spacing-md)",
}}
/>
);
};
export default CustomTab;

View File

@ -2,10 +2,10 @@
import React, { FC, useState } from "react"; import React, { FC, useState } from "react";
import { Drawer } from "@mantine/core"; import { Drawer } from "@mantine/core";
import ProjectEditorBody from "@/app/deals/drawers/ProjectEditorDrawer/components/ProjectEditorBody";
import { DrawerProps } from "@/drawers/types"; import { DrawerProps } from "@/drawers/types";
import useIsMobile from "@/hooks/utils/useIsMobile"; import useIsMobile from "@/hooks/utils/useIsMobile";
import { ProjectSchema } from "@/lib/client"; import { ProjectSchema } from "@/lib/client";
import ProjectEditorBody from "@/drawers/common/ProjectEditorDrawer/components/ProjectEditorBody";
type Props = { type Props = {
value: ProjectSchema; value: ProjectSchema;

View File

@ -1,11 +1,10 @@
import { FC } from "react"; import { FC } from "react";
import { IconBlocks, IconEdit, IconTags } from "@tabler/icons-react"; import { IconBlocks, IconEdit } from "@tabler/icons-react";
import { Tabs } from "@mantine/core"; import { Tabs } from "@mantine/core";
import { import {
GeneralTab, GeneralTab,
ModulesTab, ModulesTab,
} from "@/drawers/common/ProjectEditorDrawer/tabs"; } from "@/app/deals/drawers/ProjectEditorDrawer/tabs";
import TagsTab from "@/drawers/common/ProjectEditorDrawer/tabs/TagsTab/TagsTab";
import { ProjectSchema } from "@/lib/client"; import { ProjectSchema } from "@/lib/client";
import styles from "../ProjectEditorDrawer.module.css"; import styles from "../ProjectEditorDrawer.module.css";
@ -31,21 +30,13 @@ const ProjectEditorBody: FC<Props> = props => {
leftSection={<IconBlocks />}> leftSection={<IconBlocks />}>
Модули Модули
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab
value={"tags"}
leftSection={<IconTags />}>
Теги
</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"}> <Tabs.Panel value="modules">
<ModulesTab {...props} /> <ModulesTab {...props} />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value={"tags"}>
<TagsTab {...props} />
</Tabs.Panel>
</Tabs> </Tabs>
); );
}; };

View File

@ -0,0 +1,3 @@
import ProjectEditorDrawer from "@/app/deals/drawers/ProjectEditorDrawer/ProjectEditorDrawer";
export default ProjectEditorDrawer;

View File

@ -1,8 +1,8 @@
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/components/Footer";
import { ProjectSchema } from "@/lib/client"; import { ProjectSchema } from "@/lib/client";
import Footer from "./components/Footer";
type Props = { type Props = {
value: ProjectSchema; value: ProjectSchema;

View File

@ -1,7 +1,7 @@
import { FC } from "react"; import { FC } from "react";
import { Button, Stack } from "@mantine/core"; import { Button, Stack } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import resolveDependencies from "@/drawers/common/ProjectEditorDrawer/tabs/ModulesTab/utils/resolveDependencies"; import resolveDependencies from "@/app/deals/drawers/ProjectEditorDrawer/tabs/ModulesTab/utils/resolveDependencies";
import { ProjectSchema } from "@/lib/client"; import { ProjectSchema } from "@/lib/client";
import ModulesTable from "./components/ModulesTable"; import ModulesTable from "./components/ModulesTable";
@ -16,10 +16,12 @@ export const ModulesTab: FC<Props> = ({ value, onChange }) => {
}); });
const onSubmit = (values: ProjectSchema) => { const onSubmit = (values: ProjectSchema) => {
const modulesWithDependencies = resolveDependencies(values.modules); const modulesWithDependencies = resolveDependencies(
values.builtInModules
);
const updatedValues = { const updatedValues = {
...values, ...values,
modules: modulesWithDependencies, builtInModules: modulesWithDependencies,
}; };
form.setValues(updatedValues); form.setValues(updatedValues);
form.resetDirty(); form.resetDirty();
@ -30,9 +32,9 @@ export const ModulesTab: FC<Props> = ({ value, onChange }) => {
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack p={"md"}> <Stack p={"md"}>
<ModulesTable <ModulesTable
selectedRecords={form.values.modules} selectedRecords={form.values.builtInModules}
onSelectedRecordsChange={modules => onSelectedRecordsChange={modules =>
form.setFieldValue("modules", modules) form.setFieldValue("builtInModules", modules)
} }
/> />
<Button <Button

View File

@ -1,24 +1,24 @@
import { FC } from "react"; import { FC } from "react";
import { Divider, Stack } from "@mantine/core"; 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 BaseTable from "@/components/ui/BaseTable/BaseTable";
import useModulesTableColumns from "@/drawers/common/ProjectEditorDrawer/tabs/ModulesTab/hooks/useModulesTableColumns"; import useBuiltInModulesList from "@/hooks/lists/useBuiltInModulesList";
import useModulesList from "@/hooks/lists/useModulesList"; import { BuiltInModuleSchemaOutput } from "@/lib/client";
import { ModuleSchemaOutput } from "@/lib/client";
type Props = { type Props = {
selectedRecords: ModuleSchemaOutput[]; selectedRecords: BuiltInModuleSchemaOutput[];
onSelectedRecordsChange: (records: ModuleSchemaOutput[]) => void; onSelectedRecordsChange: (records: BuiltInModuleSchemaOutput[]) => void;
}; };
const ModulesTable: FC<Props> = props => { const ModulesTable: FC<Props> = props => {
const columns = useModulesTableColumns(); const columns = useModulesTableColumns();
const { modules } = useModulesList(); const { builtInModules } = useBuiltInModulesList();
return ( return (
<Stack gap={0}> <Stack gap={0}>
<Divider /> <Divider />
<BaseTable <BaseTable
records={modules} records={builtInModules}
columns={columns} columns={columns}
verticalSpacing={"md"} verticalSpacing={"md"}
allRecordsSelectionCheckboxProps={{ allRecordsSelectionCheckboxProps={{

View File

@ -2,7 +2,7 @@ import { useMemo } from "react";
import { IconInfoCircle } from "@tabler/icons-react"; import { IconInfoCircle } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable"; import { DataTableColumn } from "mantine-datatable";
import { em, Group, Text, Tooltip } from "@mantine/core"; import { em, Group, Text, Tooltip } from "@mantine/core";
import { ModuleSchemaOutput } from "@/lib/client"; import { BuiltInModuleSchemaOutput } from "@/lib/client";
const useModulesTableColumns = () => { const useModulesTableColumns = () => {
return useMemo( return useMemo(
@ -38,7 +38,7 @@ const useModulesTableColumns = () => {
</Text> </Text>
), ),
}, },
] as DataTableColumn<ModuleSchemaOutput>[], ] as DataTableColumn<BuiltInModuleSchemaOutput>[],
[] []
); );
}; };

View File

@ -1,13 +1,13 @@
import { uniqBy } from "lodash"; import { uniqBy } from "lodash";
import { ModuleSchemaOutput } from "@/lib/client"; import { BuiltInModuleSchemaOutput } from "@/lib/client";
const resolveDependencies = ( const resolveDependencies = (
modules: ModuleSchemaOutput[] modules: BuiltInModuleSchemaOutput[]
): ModuleSchemaOutput[] => { ): BuiltInModuleSchemaOutput[] => {
const resolved = new Set<number>(); const resolved = new Set<number>();
const result: ModuleSchemaOutput[] = []; const result: BuiltInModuleSchemaOutput[] = [];
const addModule = (module: ModuleSchemaOutput) => { const addModule = (module: BuiltInModuleSchemaOutput) => {
if (resolved.has(module.id)) return; if (resolved.has(module.id)) return;
resolved.add(module.id); resolved.add(module.id);

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import React, { FC } from "react"; import React, { FC } from "react";
import { Center, Divider, Drawer, Stack, Title } from "@mantine/core"; import { Center, Divider, Drawer, Text } from "@mantine/core";
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext"; import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
import ProjectsDrawerBody from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/ProjectsDrawerBody"; import ProjectsDrawerBody from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/ProjectsDrawerBody";
import { DrawerProps } from "@/drawers/types"; import { DrawerProps } from "@/drawers/types";
@ -30,20 +30,19 @@ const ProjectsMobileEditorDrawer: FC<DrawerProps<Props>> = ({
height: "100%", height: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "xs",
}, },
}}> }}>
<Stack gap={"xs"}> <Center>
<Center p={"xs"}> <Text>Проекты</Text>
<Title order={6}>Проекты</Title> </Center>
</Center> <Divider />
<Divider /> <ProjectsContextProvider>
<ProjectsContextProvider> <ProjectsDrawerBody
<ProjectsDrawerBody onSelect={onSelect}
onSelect={onSelect} onClose={onClose}
onClose={onClose} />
/> </ProjectsContextProvider>
</ProjectsContextProvider>
</Stack>
</Drawer> </Drawer>
); );
}; };

View File

@ -34,7 +34,9 @@ const ProjectMobile: FC<Props> = ({ project, onSelect, closeDrawer }) => {
return ( return (
<Group <Group
p={"xs"} w={"100%"}
pl={"xs"}
py={"xs"}
justify={"space-between"} justify={"space-between"}
wrap={"nowrap"} wrap={"nowrap"}
className={styles.project} className={styles.project}

View File

@ -30,6 +30,7 @@ const StatusesMobileEditorDrawer: FC<DrawerProps<Props>> = ({
height: "100%", height: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "xs",
}, },
}}> }}>
<StatusesMobileContextProvider board={board}> <StatusesMobileContextProvider board={board}>

View File

@ -1,5 +1,5 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Divider, Stack } from "@mantine/core"; import { Stack } from "@mantine/core";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import CreateProjectButton from "@/app/deals/drawers/ProjectsMobileEditorDrawer/components/CreateProjectButton"; import CreateProjectButton from "@/app/deals/drawers/ProjectsMobileEditorDrawer/components/CreateProjectButton";
import ProjectMobile from "@/app/deals/drawers/ProjectsMobileEditorDrawer/components/ProjectMobile"; import ProjectMobile from "@/app/deals/drawers/ProjectsMobileEditorDrawer/components/ProjectMobile";
@ -25,7 +25,6 @@ const ProjectsDrawerBody: FC<Props> = ({ onSelect, onClose }) => {
/> />
))} ))}
</Stack> </Stack>
<Divider />
<CreateProjectButton /> <CreateProjectButton />
</> </>
); );

View File

@ -40,9 +40,7 @@ const StatusMobile: FC<Props> = ({ status, board }) => {
board={board} board={board}
onDeleteStatus={statusesCrud.onDelete} onDeleteStatus={statusesCrud.onDelete}
handleEdit={startEditing} handleEdit={startEditing}
onStatusColorChange={color => onStatusColorChange={color => statusesCrud.onUpdate(status.id, { color })}
statusesCrud.onUpdate(status.id, { color })
}
withChangeOrderButton={false} withChangeOrderButton={false}
/> />
</Group> </Group>

View File

@ -1,6 +1,6 @@
import React, { FC, ReactNode } from "react"; import React, { FC, ReactNode } from "react";
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react"; import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
import { Box, Center, Divider, Group, Stack, Title } from "@mantine/core"; import { Box, Center, Divider, Group, Text } from "@mantine/core";
import CreateStatusButton from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/CreateStatusButton"; import CreateStatusButton from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/CreateStatusButton";
import StatusMobile from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/StatusMobile"; import StatusMobile from "@/app/deals/drawers/StatusesMobileEditorDrawer/components/StatusMobile";
import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext"; import { useStatusesMobileContext } from "@/app/deals/drawers/StatusesMobileEditorDrawer/contexts/BoardStatusesContext";
@ -39,7 +39,7 @@ const StatusesDrawerBody: FC<Props> = ({ onClose }) => {
statusesCrud.onUpdate(itemId, { lexorank: newLexorank }); statusesCrud.onUpdate(itemId, { lexorank: newLexorank });
return ( return (
<Stack gap={"xs"}> <>
<Group justify={"space-between"}> <Group justify={"space-between"}>
<Box <Box
onClick={onClose} onClick={onClose}
@ -47,7 +47,7 @@ const StatusesDrawerBody: FC<Props> = ({ onClose }) => {
<IconChevronLeft /> <IconChevronLeft />
</Box> </Box>
<Center> <Center>
<Title order={6}>{board.name}</Title> <Text>{board.name}</Text>
</Center> </Center>
<Box p={"lg"} /> <Box p={"lg"} />
</Group> </Group>
@ -60,9 +60,8 @@ const StatusesDrawerBody: FC<Props> = ({ onClose }) => {
dragHandleStyle={{ width: "auto" }} dragHandleStyle={{ width: "auto" }}
vertical vertical
/> />
{statuses.length > 0 && <Divider />}
<CreateStatusButton /> <CreateStatusButton />
</Stack> </>
); );
}; };

View File

@ -1,36 +0,0 @@
import { useEffect, useMemo, useRef } from "react";
import { modals } from "@mantine/modals";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
const useCreateFirstProject = () => {
const hasOpened = useRef(false);
const { projects, projectsCrud, isLoading } = useProjectsContext();
const shouldOpen = useMemo(
() => !isLoading && projects.length === 0,
[isLoading, projects]
);
useEffect(() => {
if (!shouldOpen || hasOpened.current) return;
hasOpened.current = true;
modals.openContextModal({
modal: "createFirstProjectModal",
withCloseButton: false,
closeOnClickOutside: false,
closeOnEscape: false,
overlayProps: {
color: "black",
blur: 6,
backgroundOpacity: 0.45,
},
innerProps: {
onSubmit: values =>
projectsCrud.onCreate(values, modals.closeAll),
},
});
}, [shouldOpen]);
};
export default useCreateFirstProject;

View File

@ -25,16 +25,15 @@ const useDealsAndGroups = ({ deals }: Props) => {
groupData.items.push(deal); groupData.items.push(deal);
groupsWithDealMap.set(deal.group.id, groupData); groupsWithDealMap.set(deal.group.id, groupData);
} else { } else {
groupsWithDealMap.set(deal.group.id, { groupsWithDealMap.set(deal.group.id, { ...deal.group, items: [] });
...deal.group,
items: [deal],
});
} }
} }
return sortByLexorank(groupsWithDealMap.values().toArray()); return sortByLexorank(groupsWithDealMap.values().toArray());
}, [deals]); }, [deals]);
console.log(groupsWithDeals);
return { return {
dealsWithoutGroup, dealsWithoutGroup,
groupsWithDeals, groupsWithDeals,

View File

@ -1,441 +0,0 @@
import { RefObject, useMemo, useRef } from "react";
import { DragOverEvent, Over } from "@dnd-kit/core";
import { SwiperRef } from "swiper/swiper-react";
import { useDebouncedCallback } from "@mantine/hooks";
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
import isItemGroup from "@/app/deals/utils/isItemGroup";
import {
getContainerId,
isContainerId,
} from "@/components/dnd/FunnelDnd/utils/columnId";
import {
getGroupId,
isGroupId,
} from "@/components/dnd/FunnelDnd/utils/groupId";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { StatusSchema } from "@/lib/client";
import { sortByLexorank } from "@/utils/lexorank/sort";
type ReturnType = {
sortedStatuses: StatusSchema[];
handleDragOver: ({ active, over }: DragOverEvent) => void;
handleDragEnd: ({ active, over }: DragOverEvent) => void;
swiperRef: RefObject<SwiperRef | null>;
};
const useDealsAndStatusesDnd = (): ReturnType => {
const swiperRef = useRef<SwiperRef>(null);
const { statuses, setStatuses, statusesCrud } = useStatusesContext();
const {
deals,
dealsWithoutGroup,
groupsWithDeals,
setDeals,
dealsCrud,
groupsCrud,
} = useDealsContext();
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
const isMobile = useIsMobile();
const {
getNewRankForSameStatus,
getNewRankForAnotherStatus,
getNewStatusRank,
} = useGetNewRank();
const debouncedSetStatuses = useDebouncedCallback(setStatuses, 200);
const debouncedSetDeals = useDebouncedCallback(setDeals, 200);
const getStatusByDealId = (dealId: number) => {
const deal = dealsWithoutGroup.find(deal => deal.id === dealId);
if (!deal) return;
return statuses.find(status => status.id === deal.status.id);
};
const getStatusByGroupId = (groupId: number) => {
const group = groupsWithDeals.find(group => group.id === groupId);
if (!group || group.items.length === 0) return;
return statuses.find(status => status.id === group.items[0].status.id);
};
const getStatusById = (statusId: number) => {
return statuses.find(status => status.id === statusId);
};
const getStatusDealsAndGroups = (statusId: number) =>
sortByLexorank([
...dealsWithoutGroup.filter(d => d.status.id === statusId),
...groupsWithDeals.filter(
g => g.items.length > 0 && g.items[0].status.id === statusId
),
]);
const swipeSliderDuringDrag = (activeId: number | string, over: Over) => {
let activeStatus: StatusSchema | undefined;
if (typeof activeId === "string") {
activeStatus = getStatusByGroupId(getGroupId(activeId));
} else {
activeStatus = getStatusByDealId(Number(activeId));
}
const swiperActiveStatus =
statuses[swiperRef.current?.swiper.activeIndex ?? 0];
if (swiperActiveStatus.id !== activeStatus?.id) return;
const activeStatusLexorank = activeStatus?.lexorank;
let overStatusLexorank: string | undefined;
if (typeof over.id === "string") {
if (isContainerId(over.id)) {
const overStatusId = getContainerId(over.id);
overStatusLexorank = statuses.find(
s => s.id === overStatusId
)?.lexorank;
} else {
const overGroupId = getGroupId(over.id);
overStatusLexorank = getStatusByGroupId(overGroupId)?.lexorank;
}
} else {
overStatusLexorank = getStatusByDealId(Number(over.id))?.lexorank;
}
if (
!activeStatusLexorank ||
!overStatusLexorank ||
!swiperRef.current?.swiper
)
return;
const activeIndex = sortedStatuses.findIndex(
s => s.lexorank === activeStatusLexorank
);
const overIndex = sortedStatuses.findIndex(
s => s.lexorank === overStatusLexorank
);
if (activeIndex > overIndex) {
swiperRef.current.swiper.slidePrev();
return;
}
if (activeIndex < overIndex) {
swiperRef.current.swiper.slideNext();
}
};
const handleDragOver = ({ active, over }: DragOverEvent) => {
if (!over) return;
const activeId = active.id as string | number;
if (isMobile && (typeof activeId !== "string" || isGroupId(activeId))) {
swipeSliderDuringDrag(activeId, over);
}
if (typeof activeId !== "string") {
handleDealDragOver(activeId, over);
return;
}
if (isContainerId(activeId)) {
handleColumnDragOver(activeId, over);
return;
}
handleGroupDragOver(activeId, over);
};
const handleDealDragOver = (activeId: string | number, over: Over) => {
const activeDealId = Number(activeId);
const activeStatusId = getStatusByDealId(activeDealId)?.id;
if (!activeStatusId) return;
const { overStatus, newLexorank } = getDropTarget(
over.id,
activeDealId,
undefined,
activeStatusId
);
if (!overStatus) return;
debouncedSetDeals(
deals.map(deal =>
deal.id === activeDealId
? {
...deal,
status: overStatus,
lexorank: newLexorank || deal.lexorank,
}
: deal
)
);
};
const handleGroupDragOver = (activeId: string, over: Over) => {
const activeGroupId = getGroupId(activeId);
const activeStatusId = getStatusByGroupId(activeGroupId)?.id;
if (!activeStatusId) return;
const { overStatus, newLexorank } = getDropTarget(
over.id,
undefined,
activeGroupId,
activeStatusId
);
if (!overStatus) return;
debouncedSetDeals(
deals.map(deal =>
deal.group && deal.group.id === activeGroupId
? {
...deal,
status: overStatus,
group: {
...deal.group,
lexorank: newLexorank || deal.group.lexorank,
},
}
: deal
)
);
};
const handleColumnDragOver = (activeId: string, over: Over) => {
const activeStatusId = getContainerId(activeId);
let overStatusId: number;
if (typeof over.id === "string") {
if (isContainerId(over.id)) {
overStatusId = getContainerId(over.id);
} else {
const status = getStatusByGroupId(getGroupId(over.id));
if (!status) return;
overStatusId = status.id;
}
} else {
const deal = dealsWithoutGroup.find(deal => deal.id === over.id);
if (!deal) return;
overStatusId = deal.status.id;
}
if (!overStatusId || activeStatusId === overStatusId) return;
const newRank = getNewStatusRank(activeStatusId, overStatusId);
if (!newRank) return;
debouncedSetStatuses(
statuses.map(status =>
status.id === activeStatusId
? { ...status, lexorank: newRank }
: status
)
);
};
const getDropTarget = (
overId: string | number,
activeDealId: number | undefined,
activeGroupId: number | undefined,
activeStatusId: number,
isOnDragEnd: boolean = false
): { overStatus?: StatusSchema; newLexorank?: string } => {
if (typeof overId === "string") {
if (isContainerId(overId)) {
return getStatusDropTarget(overId);
}
if (isGroupId(overId)) {
return getGroupDropTarget(
overId,
activeGroupId,
activeStatusId,
isOnDragEnd
);
}
}
return getDealDropTarget(
Number(overId),
activeDealId,
activeStatusId,
isOnDragEnd
);
};
const getStatusDropTarget = (overId: string) => ({
overStatus: getStatusById(getContainerId(overId)),
newLexorank: undefined,
});
const getDealDropTarget = (
overId: number,
activeDealId: number | undefined,
activeStatusId: number,
isOnDragEnd: boolean = false
) => {
const overDealId = Number(overId);
const overStatus = getStatusByDealId(overDealId);
if (!overStatus || (!isOnDragEnd && activeDealId === overDealId)) {
return { overStatus: undefined, newLexorank: undefined };
}
const statusItems = getStatusDealsAndGroups(overStatus.id);
const overDealIndex = statusItems.findIndex(
deal => !isItemGroup(deal) && deal.id === overDealId
);
const activeDealIndex = statusItems.findIndex(
deal => !isItemGroup(deal) && deal.id === activeDealId
);
if (activeStatusId === overStatus.id) {
const newLexorank = getNewRankForSameStatus(
statusItems,
overDealIndex,
activeDealIndex
);
return { overStatus, newLexorank };
}
const newLexorank = getNewRankForAnotherStatus(
statusItems,
overDealIndex
);
return { overStatus, newLexorank };
};
const getGroupDropTarget = (
overId: string,
activeGroupId: number | undefined,
activeStatusId: number,
isOnDragEnd: boolean = false
) => {
const overGroupId = getGroupId(overId);
const overStatus = getStatusByGroupId(overGroupId);
if (!overStatus || (!isOnDragEnd && activeGroupId === overGroupId)) {
return { overStatus: undefined, newLexorank: undefined };
}
const statusItems = getStatusDealsAndGroups(overStatus.id);
const overGroupIndex = statusItems.findIndex(
group => isItemGroup(group) && group.id === overGroupId
);
const activeGroupIndex = statusItems.findIndex(
group => isItemGroup(group) && group.id === activeGroupId
);
if (activeStatusId === overStatus.id) {
const newLexorank = getNewRankForSameStatus(
statusItems,
overGroupIndex,
activeGroupIndex
);
return { overStatus, newLexorank };
}
const newLexorank = getNewRankForAnotherStatus(
statusItems,
overGroupIndex
);
return { overStatus, newLexorank };
};
const handleDragEnd = ({ active, over }: DragOverEvent) => {
if (!over) return;
const activeId: string | number = active.id;
if (typeof activeId !== "string") {
handleDealDragEnd(activeId, over);
return;
}
if (isContainerId(activeId)) {
handleStatusColumnDragEnd(activeId, over);
return;
}
handleGroupDragEnd(activeId, over);
};
const handleStatusColumnDragEnd = (activeId: string, over: Over) => {
const activeStatusId = getContainerId(activeId);
let overStatusId: number;
if (typeof over.id === "string" && isContainerId(over.id)) {
overStatusId = getContainerId(over.id);
} else {
const deal = dealsWithoutGroup.find(
deal => deal.status.id === over.id
);
if (!deal) return;
overStatusId = deal.status.id;
}
if (!overStatusId) return;
const newRank = getNewStatusRank(activeStatusId, overStatusId);
if (!newRank) return;
onStatusDragEnd?.(activeStatusId, newRank);
};
const onStatusDragEnd = (statusId: number, lexorank: string) => {
statusesCrud.onUpdate(statusId, { lexorank });
};
const handleDealDragEnd = (activeId: number | string, over: Over) => {
const activeDealId = Number(activeId);
const activeStatusId = getStatusByDealId(activeDealId)?.id;
if (!activeStatusId) return;
const { overStatus, newLexorank } = getDropTarget(
over.id,
activeDealId,
undefined,
activeStatusId,
true
);
if (!overStatus) return;
onDealDragEnd(activeDealId, overStatus.id, newLexorank);
};
const onDealDragEnd = (
dealId: number,
statusId: number,
lexorank?: string
) => {
dealsCrud.onUpdate(dealId, { statusId, lexorank, name: null });
};
const handleGroupDragEnd = (activeId: string, over: Over) => {
const activeGroupId = getGroupId(activeId);
const activeStatusId = getStatusByGroupId(activeGroupId)?.id;
if (!activeStatusId) return;
const { overStatus, newLexorank } = getDropTarget(
over.id,
undefined,
activeGroupId,
activeStatusId,
true
);
if (!overStatus) return;
onGroupDragEnd(activeGroupId, overStatus.id, newLexorank);
};
const onGroupDragEnd = (
groupId: number,
statusId: number,
lexorank?: string
) => {
groupsCrud.onUpdate(groupId, { statusId, lexorank, name: null });
};
return {
swiperRef,
sortedStatuses,
handleDragOver,
handleDragEnd,
};
};
export default useDealsAndStatusesDnd;

View File

@ -1,112 +0,0 @@
import { LexoRank } from "lexorank";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import {
BaseDraggable,
BaseGroupDraggable,
} from "@/components/dnd/types/types";
import { getNewLexorank } from "@/utils/lexorank/generation";
import { sortByLexorank } from "@/utils/lexorank/sort";
type NewRankGetters<
TItem extends BaseDraggable,
TGroup extends BaseGroupDraggable<TItem>,
> = {
getNewRankForSameStatus: (
statusItemsAndGroups: (TItem | TGroup)[],
overItemOrGroupIndex: number,
activeItemOrGroupIndex: number
) => string;
getNewRankForAnotherStatus: (
statusItemsAndGroups: (TItem | TGroup)[],
overItemOrGroupIndex: number
) => string;
getNewStatusRank: (
activeStatusId: number,
overStatusId: number
) => string | null;
};
const useGetNewRank = <
TItem extends BaseDraggable,
TGroup extends BaseGroupDraggable<TItem>,
>(): NewRankGetters<TItem, TGroup> => {
const { statuses } = useStatusesContext();
const getNewRankForSameStatus = (
statusItemsAndGroups: (TItem | TGroup)[],
overItemOrGroupIndex: number,
activeItemOrGroupIndex: number
): string => {
const [leftIndex, rightIndex] =
overItemOrGroupIndex < activeItemOrGroupIndex
? [overItemOrGroupIndex - 1, overItemOrGroupIndex]
: [overItemOrGroupIndex, overItemOrGroupIndex + 1];
const leftLexorank =
leftIndex >= 0
? LexoRank.parse(statusItemsAndGroups[leftIndex].lexorank)
: null;
const rightLexorank =
rightIndex < statusItemsAndGroups.length
? LexoRank.parse(statusItemsAndGroups[rightIndex].lexorank)
: null;
return getNewLexorank(leftLexorank, rightLexorank).toString();
};
const getNewRankForAnotherStatus = (
statusItemsAndGroups: (TItem | TGroup)[],
overItemOrGroupIndex: number
): string => {
const leftLexorank =
overItemOrGroupIndex > 0
? LexoRank.parse(
statusItemsAndGroups[overItemOrGroupIndex - 1].lexorank
)
: null;
const rightLexorank = LexoRank.parse(
statusItemsAndGroups[overItemOrGroupIndex].lexorank
);
return getNewLexorank(leftLexorank, rightLexorank).toString();
};
const getNewStatusRank = (
activeStatusId: number,
overStatusId: number
): string | null => {
const sortedStatusList = sortByLexorank(statuses);
const overIndex = sortedStatusList.findIndex(
s => s.id === overStatusId
);
const activeIndex = sortedStatusList.findIndex(
s => s.id === activeStatusId
);
if (overIndex === -1 || activeIndex === -1) return null;
const [leftIndex, rightIndex] =
overIndex < activeIndex
? [overIndex - 1, overIndex]
: [overIndex, overIndex + 1];
const leftLexorank =
leftIndex >= 0
? LexoRank.parse(statuses[leftIndex].lexorank)
: null;
const rightLexorank =
rightIndex < statuses.length
? LexoRank.parse(statuses[rightIndex].lexorank)
: null;
return getNewLexorank(leftLexorank, rightLexorank).toString();
};
return {
getNewRankForSameStatus,
getNewRankForAnotherStatus,
getNewStatusRank,
};
};
export default useGetNewRank;

View File

@ -1,114 +0,0 @@
import { Dispatch, SetStateAction, useState } from "react";
import { GroupsCrud } from "@/hooks/cruds/useDealGroupCrud";
import { DealSchema } from "@/lib/client";
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
type Props = {
groupsCrud: GroupsCrud;
};
export type GroupDealsSelection = {
isDealsSelecting: boolean;
selectedBaseDealId: number | null;
startSelectingWithDeal: (dealId: number) => void;
selectedGroupId: number | null;
startSelectingWithExistingGroup: (group: GroupWithDealsSchema) => void;
startSelecting: () => void;
selectedDealIds: Set<number>;
setSelectedDealIds: Dispatch<SetStateAction<Set<number>>>;
toggleDeal: (deal: DealSchema) => void;
finishDealsSelecting: () => void;
cancelDealsSelecting: () => void;
};
const useGroupDealsSelection = ({ groupsCrud }: Props): GroupDealsSelection => {
const [selectedDealIds, setSelectedDealIds] = useState<Set<number>>(
new Set()
);
const [isDealsSelecting, setIsDealsSelecting] = useState<boolean>(false);
const [selectedBaseDealId, setSelectedBaseDealId] = useState<number | null>(
null
);
const [selectedGroupId, setSelectedGroupId] = useState<number | null>(null);
const toggleDeal = (deal: DealSchema) => {
if (selectedBaseDealId === deal.id) return;
if (selectedDealIds.has(deal.id)) {
selectedDealIds.delete(deal.id);
} else {
if (!selectedBaseDealId && !selectedGroupId) {
if (deal.group) return;
setSelectedBaseDealId(deal.id);
return;
}
selectedDealIds.add(deal.id);
}
setSelectedDealIds(new Set(selectedDealIds));
};
const finishDealsSelecting = () => {
if (selectedBaseDealId) {
groupsCrud.onCreate(
selectedBaseDealId,
selectedDealIds.values().toArray()
);
setSelectedBaseDealId(null);
} else if (selectedGroupId) {
groupsCrud.onUpdateDealsInGroup(
selectedGroupId,
selectedDealIds.values().toArray()
);
setSelectedGroupId(null);
}
setIsDealsSelecting(false);
setSelectedDealIds(new Set());
};
const cancelDealsSelecting = () => {
setSelectedDealIds(new Set());
setSelectedBaseDealId(null);
setSelectedGroupId(null);
setIsDealsSelecting(false);
};
// For editing group
const startSelectingWithExistingGroup = (group: GroupWithDealsSchema) => {
setSelectedDealIds(new Set(group.items.map(item => item.id)));
setSelectedBaseDealId(null);
setSelectedGroupId(group.id);
setIsDealsSelecting(true);
};
// For creating group on desktop
const startSelectingWithDeal = (dealId: number) => {
setSelectedDealIds(new Set([dealId]));
setSelectedBaseDealId(dealId);
setSelectedGroupId(null);
setIsDealsSelecting(true);
};
// For creating group on mobile
const startSelecting = () => {
setSelectedDealIds(new Set());
setSelectedBaseDealId(null);
setSelectedGroupId(null);
setIsDealsSelecting(true);
};
return {
isDealsSelecting,
selectedBaseDealId,
startSelectingWithDeal,
selectedGroupId,
startSelectingWithExistingGroup,
startSelecting,
selectedDealIds,
setSelectedDealIds,
toggleDeal,
finishDealsSelecting,
cancelDealsSelecting,
};
};
export default useGroupDealsSelection;

View File

@ -1,72 +0,0 @@
"use client";
import {
Button,
Center,
Divider,
Flex,
Space,
Text,
TextInput,
Title,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import Logo from "@/components/ui/Logo/Logo";
import { CreateProjectSchema } from "@/lib/client";
type Props = {
onSubmit: (values: CreateProjectSchema, onSuccess?: () => void) => void;
};
const CreateFirstProjectModal = ({
id,
context,
innerProps,
}: ContextModalProps<Props>) => {
const form = useForm<CreateProjectSchema>({
initialValues: {
name: "",
},
validate: {
name: name =>
(!name || name.trim().length === 0) && "Введите название",
},
});
const onClose = () => context.closeModal(id);
return (
<form
onSubmit={form.onSubmit(values =>
innerProps.onSubmit(values, onClose)
)}>
<Flex
gap={"xs"}
direction={"column"}>
<Logo title={"Fulfillment & Delivery"} />
<Divider />
<Space />
<Center>
<Title order={4}>Создайте свой первый проект</Title>
</Center>
<Space />
<Divider />
<Space />
<TextInput
label={"Название"}
placeholder={"Введите название"}
{...form.getInputProps("name")}
/>
<Space />
<Button
variant={"filled"}
type={"submit"}>
<Text>Сохранить</Text>
</Button>
</Flex>
</form>
);
};
export default CreateFirstProjectModal;

View File

@ -17,19 +17,14 @@ import { combineProviders } from "@/utils/combineProviders";
async function prefetchData() { async function prefetchData() {
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const projectsData = await queryClient.fetchQuery(getProjectsOptions());
try { const firstProjectId = projectsData.items?.[0]?.id;
const projectsData = await queryClient.fetchQuery(getProjectsOptions()); if (!firstProjectId) return queryClient;
const firstProjectId = projectsData.items?.[0]?.id; await queryClient.prefetchQuery(
if (!firstProjectId) return queryClient; getBoardsOptions({ path: { projectId: firstProjectId } })
);
await queryClient.prefetchQuery(
getBoardsOptions({ path: { projectId: firstProjectId } })
);
} catch (e) {
console.warn("Prefetch failed, continuing without data:", e);
}
return queryClient; return queryClient;
} }

View File

@ -1,15 +0,0 @@
import {
BaseDraggable,
BaseGroupDraggable,
} from "@/components/dnd/types/types";
const isItemGroup = <
TItem extends BaseDraggable,
TGroup extends BaseGroupDraggable<TItem>,
>(
item: TItem | TGroup
): boolean => {
return "items" in item;
};
export default isItemGroup;

View File

@ -0,0 +1,6 @@
const STATUS_POSTFIX = "-status";
export const isStatusId = (rawId: string) => rawId.endsWith(STATUS_POSTFIX);
export const getStatusId = (rawId: string) =>
Number(rawId.replace(STATUS_POSTFIX, ""));

View File

@ -2,7 +2,6 @@ import "@mantine/core/styles.css";
import "mantine-datatable/styles.layer.css"; import "mantine-datatable/styles.layer.css";
import "@mantine/notifications/styles.css"; import "@mantine/notifications/styles.css";
import "@mantine/dates/styles.css"; import "@mantine/dates/styles.css";
import "mantine-contextmenu/styles.css";
import "swiper/css"; import "swiper/css";
import "swiper/css/pagination"; import "swiper/css/pagination";
import "swiper/css/scrollbar"; import "swiper/css/scrollbar";
@ -15,8 +14,6 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { theme } from "@/theme"; import { theme } from "@/theme";
import "@/app/global.css"; import "@/app/global.css";
import { ContextMenuProvider } from "mantine-contextmenu";
import { DatesProvider } from "@mantine/dates";
import { ModalsProvider } from "@mantine/modals"; import { ModalsProvider } from "@mantine/modals";
import { Notifications } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext"; import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
@ -66,44 +63,40 @@ export default function RootLayout({ children }: Props) {
<MantineProvider <MantineProvider
theme={theme} theme={theme}
defaultColorScheme={"auto"}> defaultColorScheme={"auto"}>
<ContextMenuProvider> <ReactQueryProvider>
<ReactQueryProvider> <ReduxProvider>
<ReduxProvider> <ModalsProvider
<ModalsProvider labels={{ confirm: "Да", cancel: "Нет" }}
labels={{ confirm: "Да", cancel: "Нет" }} modals={modals}>
modals={modals}> <DrawersContextProvider>
<DatesProvider settings={{ locale: "ru" }}> <ProjectsContextProvider>
<DrawersContextProvider> <AppShell
<ProjectsContextProvider> layout={"alt"}
<AppShell withBorder={false}
layout={"alt"} navbar={{
withBorder={false} width: 220,
navbar={{ breakpoint: "sm",
width: 220, collapsed: {
breakpoint: "sm", desktop: false,
collapsed: { mobile: true,
desktop: false, },
mobile: true, }}>
}, <AppShellNavbarWrapper>
}}> <Navbar />
<AppShellNavbarWrapper> </AppShellNavbarWrapper>
<Navbar /> <AppShellMainWrapper>
</AppShellNavbarWrapper> {children}
<AppShellMainWrapper> </AppShellMainWrapper>
{children} <AppShellFooterWrapper>
</AppShellMainWrapper> <Footer />
<AppShellFooterWrapper> </AppShellFooterWrapper>
<Footer /> </AppShell>
</AppShellFooterWrapper> </ProjectsContextProvider>
</AppShell> </DrawersContextProvider>
</ProjectsContextProvider> </ModalsProvider>
</DrawersContextProvider> </ReduxProvider>
</DatesProvider> <Notifications position="bottom-right" />
</ModalsProvider> </ReactQueryProvider>
</ReduxProvider>
<Notifications position="bottom-right" />
</ReactQueryProvider>
</ContextMenuProvider>
</MantineProvider> </MantineProvider>
</body> </body>
</html> </html>

View File

@ -1,12 +0,0 @@
.container {
padding: var(--mantine-spacing-sm);
border: dashed 1px var(--mantine-color-default-border);
border-radius: var(--mantine-radius-lg);
}
.module-attr-container {
@mixin light {
background-color: var(--color-light-gray-blue);
border: 1px dashed var(--color-light-aqua);
}
}

View File

@ -1,40 +0,0 @@
"use client";
import { FC, useMemo } from "react";
import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList";
import ObjectSelect, {
ObjectSelectProps,
} from "@/components/selects/ObjectSelect/ObjectSelect";
import { AttributeSchema } from "@/lib/client";
type Props = Omit<
ObjectSelectProps<AttributeSchema | null>,
"data" | "getLabelFn" | "getValueFn"
> & {
attributesToExclude?: AttributeSchema[];
};
const AttributeSelect: FC<Props> = ({ attributesToExclude, ...props }) => {
const { attributes } = useAttributesList();
const availableAttributes = useMemo(() => {
const attrIdsToExcludeSet = new Set(
attributesToExclude?.map(a => a.id)
);
return attributes.filter(a => !attrIdsToExcludeSet.has(a.id));
}, [attributes, attributesToExclude]);
return (
<ObjectSelect
searchable
placeholder={"Выберите статус"}
onClear={() => props.onChange(null)}
getLabelFn={(option: AttributeSchema) => option.label}
data={availableAttributes}
{...props}
/>
);
};
export default AttributeSelect;

View File

@ -1,21 +0,0 @@
import { redirect } from "next/navigation";
import { IconArrowLeft } from "@tabler/icons-react";
import { Box, Button, Group, Title } from "@mantine/core";
const MobileHeader = () => {
return (
<Group
justify={"space-between"}
wrap={"nowrap"}>
<Button
variant={"default"}
onClick={() => redirect("/modules")}>
<IconArrowLeft />
</Button>
<Title order={5}>Редактирование модуля</Title>
<Box mx={"lg"} />
</Group>
);
};
export default MobileHeader;

View File

@ -1,21 +0,0 @@
import ObjectSelect, {
ObjectSelectProps,
} from "@/components/selects/ObjectSelect/ObjectSelect";
import { AttrSelectSchema } from "@/lib/client";
import useAttributeSelectsList from "./useAttributeSelectsList";
type Props = Omit<ObjectSelectProps<AttrSelectSchema>, "data" | "getLabelFn">;
const AttrSelectSelect = (props: Props) => {
const { selects } = useAttributeSelectsList();
return (
<ObjectSelect
label={"Объект для выбора"}
data={selects}
{...props}
/>
);
};
export default AttrSelectSelect;

View File

@ -1,13 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { getAttrSelectsOptions } from "@/lib/client/@tanstack/react-query.gen";
const useAttributeSelectsList = () => {
const { data, refetch } = useQuery(getAttrSelectsOptions());
return {
selects: data?.items ?? [],
refetch,
};
};
export default useAttributeSelectsList;

View File

@ -1,47 +0,0 @@
import { FC } from "react";
import { IconArrowRight, IconEdit, IconTrash } from "@tabler/icons-react";
import { Center, Flex } from "@mantine/core";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import { AttributeSchema } from "@/lib/client";
type Props = {
attribute: AttributeSchema;
onUpdate: (attribute: AttributeSchema) => void;
onDelete: (attribute: AttributeSchema) => void;
addAttributeToModule?: (attribute: AttributeSchema) => void;
};
const AttributeTableActions: FC<Props> = ({
attribute,
onUpdate,
onDelete,
addAttributeToModule,
}) => {
return (
<Center>
<Flex gap={"xs"}>
<ActionIconWithTip
onClick={() => onUpdate(attribute)}
disabled={attribute.isBuiltIn}
tipLabel={"Редактировать"}>
<IconEdit />
</ActionIconWithTip>
<ActionIconWithTip
onClick={() => onDelete(attribute)}
disabled={attribute.isBuiltIn}
tipLabel={"Удалить"}>
<IconTrash />
</ActionIconWithTip>
{addAttributeToModule && (
<ActionIconWithTip
onClick={() => addAttributeToModule(attribute)}
tipLabel={"Добавить в модуль"}>
<IconArrowRight />
</ActionIconWithTip>
)}
</Flex>
</Center>
);
};
export default AttributeTableActions;

View File

@ -1,21 +0,0 @@
import useAttributeTypesList from "@/app/module-editor/[moduleId]/hooks/useAttributeTypesList";
import ObjectSelect, {
ObjectSelectProps,
} from "@/components/selects/ObjectSelect/ObjectSelect";
import { AttributeTypeSchema } from "@/lib/client";
type Props = Omit<ObjectSelectProps<AttributeTypeSchema>, "data">;
const AttributeTypeSelect = (props: Props) => {
const { attributeTypes } = useAttributeTypesList();
return (
<ObjectSelect
label={"Тип атрибута"}
data={attributeTypes}
{...props}
/>
);
};
export default AttributeTypeSelect;

View File

@ -1,40 +0,0 @@
import { useMemo } from "react";
import { Box, Center, Text } from "@mantine/core";
import useAttributesTableColumns from "@/app/module-editor/[moduleId]/components/shared/AttributesTable/useAttributesTableColumns";
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
import BaseTable from "@/components/ui/BaseTable/BaseTable";
const AttributesTable = () => {
const { attributes, module } = useModuleEditorContext();
const unusedAttributes = useMemo(() => {
const usedAttrIds = new Set(module?.attributes.map(a => a.id));
return attributes.filter(a => !usedAttrIds.has(a.id));
}, [module, attributes]);
const columns = useAttributesTableColumns();
if (attributes.length === 0) {
return (
<Center my={"md"}>
<Text>Нет атрибутов</Text>
</Center>
);
}
return (
<Box
h="100%"
style={{ overflow: "auto" }}>
<BaseTable
withTableBorder
columns={columns}
records={unusedAttributes}
verticalSpacing={"md"}
groups={undefined}
styles={{ table: { width: "100%" } }}
/>
</Box>
);
};
export default AttributesTable;

View File

@ -1,46 +0,0 @@
"use client";
import { useMemo } from "react";
import { DataTableColumn } from "mantine-datatable";
import { Center } from "@mantine/core";
import AttributeTableActions from "@/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions";
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { AttributeSchema } from "@/lib/client";
const useAttributesTableColumns = () => {
const isMobile = useIsMobile();
const { attributeActions } = useModuleEditorContext();
return useMemo(
() =>
[
{
title: "Название",
accessor: "label",
},
{
title: "Тип",
accessor: "type.name",
render: attr =>
attr.type.type === "select"
? `Выбор "${attr.select?.name}"`
: attr.type.name,
},
{
accessor: "actions",
title: <Center>Действия</Center>,
width: "0%",
render: attribute => (
<AttributeTableActions
attribute={attribute}
{...attributeActions}
/>
),
},
] as DataTableColumn<AttributeSchema>[],
[isMobile]
);
};
export default useAttributesTableColumns;

View File

@ -1,18 +0,0 @@
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
const CreateAttributeButton = () => {
const {
attributeActions: { onCreate },
} = useModuleEditorContext();
return (
<InlineButton
onClick={onCreate}
w={"100%"}>
Создать атрибут
</InlineButton>
);
};
export default CreateAttributeButton;

View File

@ -1,28 +0,0 @@
import ObjectSelect from "@/components/selects/ObjectSelect/ObjectSelect";
import useAttributeOptionsList from "@/hooks/lists/useAttributeOptionsList";
import { AttrOptionSchema } from "@/lib/client";
type Props = {
value?: AttrOptionSchema | null;
onChange: (val: AttrOptionSchema | null) => void;
selectId: number;
error?: string;
label?: string;
};
const DefaultAttrOptionSelect = ({ selectId, ...props }: Props) => {
const { options } = useAttributeOptionsList({ selectId });
return (
<ObjectSelect
label={"Значение"}
{...props}
data={options}
onClear={() => props.onChange(null)}
clearable
searchable
/>
);
};
export default DefaultAttrOptionSelect;

View File

@ -1,109 +0,0 @@
import { Checkbox, NumberInput, TextInput } from "@mantine/core";
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
import { UseFormReturnType } from "@mantine/form";
import DefaultAttrOptionSelect from "@/app/module-editor/[moduleId]/components/shared/DefaultAttrOptionSelect/DefaultAttrOptionSelect";
import { AttributeSchema } from "@/lib/client";
import { naiveDateTimeStringToUtc } from "@/utils/datetime";
type Props = {
form: UseFormReturnType<Partial<AttributeSchema>>;
};
const DefaultAttributeValueInput = ({ form }: Props) => {
const type = form.values.type?.type;
const label = "Значение по умолчанию";
const inputName = "defaultValue";
const value = form.getValues().defaultValue;
if (type === "bool") {
return (
<Checkbox
label={label}
{...form.getInputProps(inputName, { type: "checkbox" })}
checked={value as boolean}
onChange={e =>
form.setFieldValue("defaultValue", e.currentTarget.checked)
}
/>
);
} else if (type === "date") {
return (
<DatePickerInput
label={label}
{...form.getInputProps(inputName)}
value={
form.values.defaultValue
? new Date(String(form.values.defaultValue))
: null
}
onChange={value => form.setFieldValue("defaultValue", value)}
clearable
locale={"ru"}
valueFormat={"DD.MM.YYYY"}
/>
);
} else if (type === "datetime") {
return (
<DateTimePicker
label={label}
{...form.getInputProps(inputName)}
value={
form.values.defaultValue
? naiveDateTimeStringToUtc(
form.values.defaultValue as string
)
: null
}
onChange={val => {
if (!val) return;
const localDate = new Date(val.replace(" ", "T"));
const utcString = localDate
.toISOString()
.substring(0, 19)
.replace("T", " ");
form.setFieldValue("defaultValue", utcString);
}}
clearable
locale={"ru"}
valueFormat={"DD.MM.YYYY HH:mm"}
/>
);
} else if (type === "str") {
return (
<TextInput
label={label}
{...form.getInputProps(inputName)}
/>
);
} else if (type === "int" || type === "float") {
return (
<NumberInput
allowDecimal={type === "float"}
label={label}
{...form.getInputProps(inputName)}
value={Number(form.values.defaultValue)}
onChange={value =>
form.setFieldValue("defaultValue", Number(value))
}
/>
);
} else if (type === "select") {
if (!form.values.select?.id) return <></>;
return (
<DefaultAttrOptionSelect
label={"Значение по умолчанию"}
{...form.getInputProps("defaultOption")}
value={form.values.defaultOption}
onChange={value => {
form.setFieldValue("defaultOption", value);
form.setFieldValue("defaultOptionId", value?.id);
}}
selectId={form.values.select?.id}
/>
);
}
return <></>;
};
export default DefaultAttributeValueInput;

View File

@ -1,71 +0,0 @@
import { FC } from "react";
import { IconEditCircle, IconX } from "@tabler/icons-react";
import { Card, Group, Stack, Text, Title } from "@mantine/core";
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
import styles from "@/app/module-editor/[moduleId]/ModuleEditor.module.css";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import { ModuleAttributeSchema } from "@/lib/client";
type Props = {
attribute: ModuleAttributeSchema;
};
const ModuleAttribute: FC<Props> = ({ attribute }) => {
const {
attributeActions: { removeAttributeFromModule, onEditAttributeLabel },
} = useModuleEditorContext();
const getAttrLabelText = () => {
const order = 6;
if (attribute.label === attribute.originalLabel) {
return <Title order={order}>Название: {attribute.label}</Title>;
}
return (
<Group gap={"xs"}>
<Title order={order}>Название: {attribute.label}</Title>{" "}
<Text>(Ориг. {attribute.originalLabel})</Text>
</Group>
);
};
return (
<Card
flex={1}
className={styles["module-attr-container"]}>
<Group
w={"100%"}
h={"100%"}
justify={"space-between"}
align={"center"}>
<Stack gap={7}>
<>{getAttrLabelText()}</>
<Text>
Тип: {attribute.type.name}{" "}
{attribute.select && `"${attribute.select.name}"`}
</Text>
</Stack>
<Group
justify={"end"}
wrap={"nowrap"}
w={"100%"}
gap={"xs"}>
<ActionIconWithTip
tipLabel={
"Редактировать название (только для данного модуля)"
}
onClick={() => onEditAttributeLabel(attribute)}>
<IconEditCircle />
</ActionIconWithTip>
<ActionIconWithTip
tipLabel={"Удалить из модуля"}
onClick={() => removeAttributeFromModule(attribute)}>
<IconX />
</ActionIconWithTip>
</Group>
</Group>
</Card>
);
};
export default ModuleAttribute;

View File

@ -1,65 +0,0 @@
import React, { ReactNode } from "react";
import { Flex } from "@mantine/core";
import { modals } from "@mantine/modals";
import ModuleAttribute from "@/app/module-editor/[moduleId]/components/shared/ModuleAttribute/ModuleAttribute";
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
import FormFlexRow from "@/components/ui/FormFlexRow/FormFlexRow";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
import useIsMobile from "@/hooks/utils/useIsMobile";
const ModuleAttributesEditor = () => {
const {
module,
attributeActions: { addAttributeToModule },
} = useModuleEditorContext();
const isMobile = useIsMobile();
const getAttributesRows = () => {
if (!module?.attributes) return [];
const rows: ReactNode[] = [];
for (let i = 0; i < module.attributes.length; i += 2) {
const rightIdx = i + 1;
rows.push(
<FormFlexRow key={i}>
<ModuleAttribute attribute={module.attributes[i]} />
{rightIdx < module.attributes.length && (
<ModuleAttribute
attribute={module.attributes[rightIdx]}
/>
)}
</FormFlexRow>
);
}
return rows;
};
const onAddAttributeClick = () => {
modals.openContextModal({
modal: "addAttributeModal",
title: "Добавление атрибута",
withCloseButton: true,
innerProps: {
onSubmit: addAttributeToModule,
usedAttributes: module?.attributes ?? [],
},
});
};
return (
<Flex
gap={isMobile ? "md" : "xs"}
direction={"column"}>
{isMobile && (
<InlineButton onClick={onAddAttributeClick}>
Добавить атрибут
</InlineButton>
)}
{getAttributesRows()}
</Flex>
);
};
export default ModuleAttributesEditor;

View File

@ -1,55 +0,0 @@
import { Button, Flex, Group, Textarea, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { ModuleInfo } from "../../../hooks/useModulesActions";
const ModuleCommonInfoEditor = () => {
const {
module,
moduleActions: { updateCommonInfo },
} = useModuleEditorContext();
const isMobile = useIsMobile();
const form = useForm<ModuleInfo>({
initialValues: module ?? {
label: "",
description: "",
},
validate: {
label: label => !label && "Введите название модуля",
},
});
const onSubmit = (values: ModuleInfo) => {
updateCommonInfo(values, form.resetDirty);
};
return (
<form onSubmit={form.onSubmit(onSubmit)}>
<Flex
gap={"xs"}
direction={"column"}>
<TextInput
label={"Название"}
{...form.getInputProps("label")}
/>
<Textarea
label={"Описание"}
{...form.getInputProps("description")}
/>
<Group>
<Button
variant={"default"}
w={isMobile ? "100%" : "auto"}
disabled={!form.isDirty()}
type={"submit"}>
Сохранить
</Button>
</Group>
</Flex>
</form>
);
};
export default ModuleCommonInfoEditor;

View File

@ -1,89 +0,0 @@
"use client";
import { Box, Divider, Fieldset, Flex, Stack } from "@mantine/core";
import AttributesTable from "@/app/module-editor/[moduleId]/components/shared/AttributesTable/AttributesTable";
import CreateAttributeButton from "@/app/module-editor/[moduleId]/components/shared/CreateAttributeButton/CreateAttributeButton";
import ModuleAttributesEditor from "@/app/module-editor/[moduleId]/components/shared/ModuleAttributesEditor/ModuleAttributesEditor";
import ModuleCommonInfoEditor from "@/app/module-editor/[moduleId]/components/shared/ModuleCommonInfoEditor/ModuleCommonInfoEditor";
import { useModuleEditorContext } from "@/app/module-editor/[moduleId]/contexts/ModuleEditorContext";
import PageBlock from "@/components/layout/PageBlock/PageBlock";
import useIsMobile from "@/hooks/utils/useIsMobile";
import MobileHeader from "@/app/module-editor/[moduleId]/components/mobile/MobileHeader/MobileHeader";
const PageBody = () => {
const { module } = useModuleEditorContext();
const isMobile = useIsMobile();
if (!module) return;
const renderMobile = () => (
<Flex
m={"xs"}
gap={"xs"}
flex={2}
direction={"column"}>
<MobileHeader />
<Divider />
<Fieldset
flex={1}
legend={"Общие данные модуля"}>
{module && <ModuleCommonInfoEditor />}
</Fieldset>
<Fieldset
flex={3}
legend={"Атрибуты модуля"}>
<ModuleAttributesEditor />
</Fieldset>
</Flex>
);
const renderDesktop = () => (
<Flex
w={"100%"}
h={"100%"}
flex={3}
style={{ overflow: "auto" }}
gap={"md"}>
<Fieldset
flex={1}
legend={"Атрибуты"}>
<Flex
direction={"column"}
h={"100%"}
gap={"xs"}>
<Box style={{ flex: 1, minHeight: 0 }}>
<AttributesTable />
</Box>
<CreateAttributeButton />
</Flex>
</Fieldset>
<Flex
gap={"xs"}
flex={2}
direction={"column"}>
<Fieldset
flex={1}
legend={"Общие данные модуля"}>
{module && <ModuleCommonInfoEditor />}
</Fieldset>
<Fieldset
flex={3}
legend={"Атрибуты модуля"}>
<ModuleAttributesEditor />
</Fieldset>
</Flex>
</Flex>
);
return (
<Stack h={"100%"}>
<PageBlock
style={{ flex: 1, minHeight: 0 }}
fullScreenMobile>
{isMobile ? renderMobile() : renderDesktop()}
</PageBlock>
</Stack>
);
};
export default PageBody;

View File

@ -1,71 +0,0 @@
"use client";
import { useEffect } from "react";
import { redirect } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import useAttributesActions, {
AttributesActions,
} from "@/app/module-editor/[moduleId]/hooks/useAttributesActions";
import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList";
import useModulesActions, {
ModulesActions,
} from "@/app/module-editor/[moduleId]/hooks/useModulesActions";
import { AttributeSchema, ModuleWithAttributesSchema } from "@/lib/client";
import { getModuleWithAttributesOptions } from "@/lib/client/@tanstack/react-query.gen";
import makeContext from "@/lib/contextFactory/contextFactory";
type ModuleEditorContextState = {
module?: ModuleWithAttributesSchema;
refetchModule: () => void;
attributes: AttributeSchema[];
refetchAttributes: () => void;
attributeActions: AttributesActions;
moduleActions: ModulesActions;
};
type Props = {
moduleId: number;
};
const useModuleEditorContextState = ({
moduleId,
}: Props): ModuleEditorContextState => {
const { data, refetch: refetchModule } = useQuery(
getModuleWithAttributesOptions({ path: { pk: moduleId } })
);
const module = data?.entity;
const { attributes, refetch: refetchAttributes } = useAttributesList();
useEffect(() => {
if (module?.isBuiltIn) {
redirect("modules");
}
}, [module]);
const attributeActions = useAttributesActions({
module,
refetchModule,
refetchAttributes,
});
const moduleActions = useModulesActions({
module,
refetchModule,
});
return {
module,
refetchModule,
attributes,
refetchAttributes,
attributeActions,
moduleActions,
};
};
export const [ModuleEditorContextProvider, useModuleEditorContext] =
makeContext<ModuleEditorContextState, Props>(
useModuleEditorContextState,
"ModuleEditor"
);

View File

@ -1,13 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { getAttributeTypesOptions } from "@/lib/client/@tanstack/react-query.gen";
const useAttributeTypesList = () => {
const { data, refetch } = useQuery(getAttributeTypesOptions());
return {
attributeTypes: data?.items ?? [],
refetch,
};
};
export default useAttributeTypesList;

View File

@ -1,110 +0,0 @@
import React from "react";
import { Text } from "@mantine/core";
import { modals } from "@mantine/modals";
import useAttributesCrud from "@/app/module-editor/[moduleId]/hooks/useAttributesCrud";
import { AttributeSchema, ModuleSchemaOutput } from "@/lib/client";
export type AttributesActions = {
addAttributeToModule: (
attribute: AttributeSchema,
onSuccess?: () => void
) => void;
removeAttributeFromModule: (attribute: AttributeSchema) => void;
onEditAttributeLabel: (attribute: AttributeSchema) => void;
onCreate: () => void;
onUpdate: (attribute: AttributeSchema) => void;
onDelete: (attribute: AttributeSchema) => void;
};
type Props = {
module?: ModuleSchemaOutput;
refetchModule?: () => void;
refetchAttributes: () => void;
};
const useAttributesActions = (props: Props): AttributesActions => {
const crud = useAttributesCrud(props);
const addAttributeToModule = (
attribute: AttributeSchema,
onSuccess?: () => void
) => crud.onToggleAttributeInModule(attribute, true, onSuccess);
const removeAttributeFromModule = (attribute: AttributeSchema) => {
modals.openConfirmModal({
title: "Удаление атрибута из модуля",
children: (
<Text>
Вы уверены, что хотите удалить атрибут "{attribute.label}"
из модуля?
</Text>
),
confirmProps: { color: "red" },
onConfirm: () => crud.onToggleAttributeInModule(attribute, false),
});
};
const onEditAttributeLabel = (attribute: AttributeSchema) => {
if (!props.module) return;
modals.openContextModal({
modal: "enterNameModal",
title: "Редактирование имени атрибута в модуле",
withCloseButton: true,
innerProps: {
onChange: values =>
crud.onUpdateLabel(attribute.id, values.name),
value: { name: attribute.label },
},
});
};
const onCreate = () => {
modals.openContextModal({
modal: "attributeEditorModal",
title: "Создание атрибута",
withCloseButton: true,
innerProps: {
onCreate: crud.onCreate,
isEditing: false,
},
});
};
const onUpdate = (attribute: AttributeSchema) => {
modals.openContextModal({
modal: "attributeEditorModal",
title: "Редактирование атрибута",
withCloseButton: true,
innerProps: {
onChange: values => crud.onUpdate(attribute.id, values),
entity: attribute,
isEditing: true,
},
});
};
const onDelete = (attribute: AttributeSchema) => {
modals.openConfirmModal({
title: "Удаление атрибута",
children: (
<Text>
Вы уверены, что хотите удалить атрибут "{attribute.label}"?
</Text>
),
confirmProps: { color: "red" },
onConfirm: () => crud.onDelete(attribute.id),
});
};
return {
addAttributeToModule,
removeAttributeFromModule,
onEditAttributeLabel,
onCreate,
onUpdate,
onDelete,
};
};
export default useAttributesActions;

View File

@ -1,181 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios";
import {
AttributeSchema,
CreateAttributeSchema,
HttpValidationError,
ModuleSchemaOutput,
UpdateAttributeSchema,
} from "@/lib/client";
import {
addAttributeToModuleMutation,
createAttributeMutation,
deleteAttributeMutation,
removeAttributeFromModuleMutation,
updateAttributeLabelMutation,
updateAttributeMutation,
} from "@/lib/client/@tanstack/react-query.gen";
import { notifications } from "@/lib/notifications";
type Props = {
module?: ModuleSchemaOutput;
refetchModule?: () => void;
refetchAttributes: () => void;
};
type AttributesCrud = {
onToggleAttributeInModule: (
attribute: AttributeSchema,
isAdding: boolean,
onSuccess?: () => void,
) => void;
onUpdateLabel: (attributeId: number, name: string) => void;
onCreate: (attribute: CreateAttributeSchema) => void;
onUpdate: (attributeId: number, values: UpdateAttributeSchema) => void;
onDelete: (attributeId: number) => void;
};
const useAttributesCrud = ({
module,
refetchModule,
refetchAttributes,
}: Props): AttributesCrud => {
const queryClient = useQueryClient();
const onError = (error: AxiosError<HttpValidationError>) => {
console.error(error);
notifications.error({
message: error.response?.data?.detail as string | undefined,
});
};
const addAttrToModuleMutation = useMutation({
...addAttributeToModuleMutation(),
onError,
});
const removeAttrFromModuleMutation = useMutation({
...removeAttributeFromModuleMutation(),
onError,
});
const removeGetDealModuleAttrQuery = () => {
if (!module) return;
queryClient.removeQueries({
predicate: query => {
const key = query.queryKey[0] as {
_id?: string;
path?: { moduleId?: number };
};
return (
key?._id === "getDealModuleAttributes" &&
key?.path?.moduleId === module.id
);
},
});
};
const onToggleAttributeInModule = (
attribute: AttributeSchema,
isAdding: boolean,
onSuccess?: () => void,
) => {
if (!module) return;
const mutation = isAdding
? addAttrToModuleMutation
: removeAttrFromModuleMutation;
mutation.mutate(
{
path: {
moduleId: module.id,
attributeId: attribute.id,
},
},
{
onSuccess: ({ message }) => {
notifications.success({ message });
refetchModule && refetchModule();
refetchAttributes();
removeGetDealModuleAttrQuery();
onSuccess && onSuccess();
},
}
);
};
const updateAttrLabelMutation = useMutation({
...updateAttributeLabelMutation(),
onError,
onSuccess: refetchModule,
});
const onUpdateLabel = (attributeId: number, name: string) => {
if (!module) return;
updateAttrLabelMutation.mutate({
body: {
moduleId: module.id,
attributeId,
label: name,
},
});
};
const createAttrMutation = useMutation({
...createAttributeMutation(),
onError,
onSuccess: refetchAttributes,
});
const onCreate = (values: CreateAttributeSchema) => {
createAttrMutation.mutate({
body: {
entity: values,
},
});
};
const updateAttrMutation = useMutation({
...updateAttributeMutation(),
onError,
onSuccess: () => {
refetchModule && refetchModule();
refetchAttributes();
},
});
const onUpdate = (attributeId: number, values: UpdateAttributeSchema) => {
updateAttrMutation.mutate({
path: {
pk: attributeId,
},
body: {
entity: values,
},
});
};
const deleteAttrMutation = useMutation({
...deleteAttributeMutation(),
onError,
onSuccess: () => {
refetchModule && refetchModule();
refetchAttributes();
},
});
const onDelete = (attributeId: number) =>
deleteAttrMutation.mutate({ path: { pk: attributeId } });
return {
onToggleAttributeInModule,
onUpdateLabel,
onCreate,
onUpdate,
onDelete,
};
};
export default useAttributesCrud;

View File

@ -1,32 +0,0 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { AttributeSchema } from "@/lib/client";
import {
getAttributesOptions,
getProjectsQueryKey,
} from "@/lib/client/@tanstack/react-query.gen";
const useAttributesList = () => {
const queryClient = useQueryClient();
const { data, refetch } = useQuery(getAttributesOptions());
const queryKey = getProjectsQueryKey();
const setAttributes = (attributes: AttributeSchema[]) => {
queryClient.setQueryData(
queryKey,
(old: { items: AttributeSchema[] }) => ({
...old,
items: attributes,
})
);
};
return {
attributes: data?.items ?? [],
setAttributes,
refetch,
queryKey,
};
};
export default useAttributesList;

Some files were not shown because too many files have changed in this diff Show More