fix: projects editor to selected project editor, moved attributes editor

This commit is contained in:
2025-03-02 16:49:28 +04:00
parent 17e6c5f23a
commit e151e4bc5e
44 changed files with 476 additions and 512 deletions

View File

@@ -308,6 +308,7 @@ export type { ProductUploadImageResponse } from './models/ProductUploadImageResp
export type { ProfitChartDataItem } from './models/ProfitChartDataItem';
export type { ProfitTableDataItem } from './models/ProfitTableDataItem';
export type { ProfitTableGroupBy } from './models/ProfitTableGroupBy';
export type { ProjectGeneralInfoSchema } from './models/ProjectGeneralInfoSchema';
export type { ProjectSchema } from './models/ProjectSchema';
export type { ReceiptBoxSchema } from './models/ReceiptBoxSchema';
export type { ReceiptPalletSchema } from './models/ReceiptPalletSchema';

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ProjectGeneralInfoSchema = {
name: string;
id: number;
};

View File

@@ -2,8 +2,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ProjectSchema } from './ProjectSchema';
import type { ProjectGeneralInfoSchema } from './ProjectGeneralInfoSchema';
export type UpdateProjectRequest = {
project: ProjectSchema;
project: ProjectGeneralInfoSchema;
};

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { BoardSchema, BoardService } from "../../../../../client";
import { modals } from "@mantine/modals";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { useCardPageContext } from "../../../../../pages/CardsPage/contexts/CardPageContext.tsx";
import { useProjectsContext } from "../../../../../contexts/ProjectsContext.tsx";
type Props = {
boards: BoardSchema[];
@@ -11,7 +11,7 @@ type Props = {
const useBoards = ({ boards, refetchBoards }: Props) => {
const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>(null);
const { selectedProject: project } = useCardPageContext();
const { selectedProject: project } = useProjectsContext();
useEffect(() => {
if (boards.length > 0 && selectedBoard === null) {
@@ -24,7 +24,7 @@ const useBoards = ({ boards, refetchBoards }: Props) => {
let newBoard = boards.find(board => board.id === selectedBoard.id);
if (!newBoard && boards.length > 0) {
newBoard = boards[0]
newBoard = boards[0];
}
setSelectedBoard(newBoard ?? null);
}

View File

@@ -10,6 +10,7 @@ import { IconCheck, IconLayoutGridRemove, IconTrash } from "@tabler/icons-react"
import { useContextMenu } from "mantine-contextmenu";
import useCardSummaryState from "./useCardSummaryState.tsx";
import isModuleInProject, { Modules } from "../../../../pages/CardsPage/utils/isModuleInProject.ts";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
type Props = {
cardSummary: CardSummary;
@@ -18,7 +19,8 @@ type Props = {
const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
const { showContextMenu } = useContextMenu();
const { selectedProject, setSelectedCard } = useCardPageContext();
const { selectedProject } = useProjectsContext();
const { setSelectedCard } = useCardPageContext();
const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState();
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);

View File

@@ -9,8 +9,8 @@ import { groupBy, has, uniq } from "lodash";
import { CardGroupView } from "../CardGroupView/CardGroupView.tsx";
import CreateDealsFromFileButton from "../CreateCardsFromFileButton/CreateDealsFromFileButton.tsx";
import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
import { useCardPageContext } from "../../../../pages/CardsPage/contexts/CardPageContext.tsx";
import isModuleInProject, { Modules } from "../../../../pages/CardsPage/utils/isModuleInProject.ts";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
type Props = {
status: StatusSchema;
@@ -27,7 +27,7 @@ export const CardsDndColumn: FC<Props> = ({
dragState,
withCreateButton = false,
}) => {
const { selectedProject } = useCardPageContext();
const { selectedProject } = useProjectsContext();
const isCreatingDealFromFileEnabled = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
const isDropDisabled = dragState === DragState.DRAG_STATUS;
const droppableId = status.id.toString();

View File

@@ -7,8 +7,8 @@ import { CardService, StatusSchema } from "../../../../client";
import { useQueryClient } from "@tanstack/react-query";
import { dateWithoutTimezone } from "../../../../shared/lib/date.ts";
import { usePrefillCardContext } from "../../../../pages/CardsPage/contexts/PrefillCardContext.tsx";
import { useCardPageContext } from "../../../../pages/CardsPage/contexts/CardPageContext.tsx";
import isModuleInProject, { Modules } from "../../../../pages/CardsPage/utils/isModuleInProject.ts";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
type Props = {
status: StatusSchema;
@@ -20,7 +20,7 @@ const CreateCardButton = ({ status }: Props) => {
const queryClient = useQueryClient();
const { prefillCard, setPrefillCard } = usePrefillCardContext();
const { selectedProject } = useCardPageContext();
const { selectedProject } = useProjectsContext();
const isPrefillingDealEnabled = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
return (

View File

@@ -9,8 +9,8 @@ import ShippingWarehouseAutocomplete
from "../../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
import BaseMarketplaceSelect from "../../../Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import { usePrefillCardContext } from "../../../../pages/CardsPage/contexts/PrefillCardContext.tsx";
import { useCardPageContext } from "../../../../pages/CardsPage/contexts/CardPageContext.tsx";
import isModuleInProject, { Modules } from "../../../../pages/CardsPage/utils/isModuleInProject.ts";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
type Props = {
onSubmit: (quickDeal: QuickCard) => void;
@@ -18,7 +18,7 @@ type Props = {
};
const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => {
const { selectedProject } = useCardPageContext();
const { selectedProject } = useProjectsContext();
const isPrefillingEnabled = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
const { prefillOnOpen, prefillCard } = usePrefillCardContext();

View File

@@ -0,0 +1,68 @@
import React, { createContext, FC, useContext, useEffect, useState } from "react";
import { ProjectSchema } from "../client";
import useProjects from "../hooks/useProjects.tsx";
type ProjectsContextState = {
selectedProject: ProjectSchema | null;
setSelectedProject: React.Dispatch<React.SetStateAction<ProjectSchema | null>>;
refetchProjects: () => void;
projects: ProjectSchema[];
};
const ProjectsContext = createContext<ProjectsContextState | undefined>(
undefined,
);
const useProjectsContextState = () => {
const { projects, refetchProjects } = useProjects();
const [selectedProject, setSelectedProject] = useState<ProjectSchema | null>(null);
const refetch = () => {
refetchProjects();
};
useEffect(() => {
if (projects.length > 0) {
if (selectedProject) {
setSelectedProject(
projects.find(project => project.id === selectedProject.id) ?? null,
);
} else {
setSelectedProject(projects[0]);
}
} else {
setSelectedProject(null);
}
}, [projects]);
return {
projects,
refetchProjects: refetch,
selectedProject,
setSelectedProject,
};
};
type ProjectsContextProviderProps = {
children: React.ReactNode;
};
export const ProjectsContextProvider: FC<ProjectsContextProviderProps> = ({ children }) => {
const state = useProjectsContextState();
return (
<ProjectsContext.Provider value={state}>
{children}
</ProjectsContext.Provider>
);
};
export const useProjectsContext = () => {
const context = useContext(ProjectsContext);
if (!context) {
throw new Error(
"useProjectsContext must be used within a ProjectsContextProvider",
);
}
return context;
};

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { type FullProjectSchema, ProjectService } from "../../../client";
import { type FullProjectSchema, ProjectService } from "../client";
const useProjects = () => {

View File

@@ -23,6 +23,7 @@ import { DatesProvider } from "@mantine/dates";
import { modals } from "./modals/modals.ts";
import TasksProvider from "./providers/TasksProvider/TasksProvider.tsx";
import { ContextMenuProvider } from "mantine-contextmenu";
import { ProjectsContextProvider } from "./contexts/ProjectsContext.tsx";
// Configuring router
const router = createRouter({ routeTree });
@@ -43,22 +44,21 @@ const queryClient = new QueryClient();
// Configuring OpenAPI
OpenAPI.BASE = import.meta.env.VITE_API_URL;
OpenAPI.TOKEN = JSON.parse(localStorage.getItem("authState") || "{}")[
"accessToken"
];
OpenAPI.TOKEN = JSON.parse(localStorage.getItem("authState") || "{}")["accessToken"];
ReactDOM.createRoot(document.getElementById("root")!).render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<MantineProvider defaultColorScheme={"dark"}>
<ContextMenuProvider>
<ModalsProvider
labels={{ confirm: "Да", cancel: "Нет" }}
modals={modals}>
<DatesProvider settings={{ locale: "ru" }}>
<TasksProvider>
<RouterProvider router={router} />
<Notifications />
<ProjectsContextProvider>
<RouterProvider router={router} />
<Notifications />
</ProjectsContextProvider>
</TasksProvider>
</DatesProvider>
</ModalsProvider>

View File

@@ -35,9 +35,9 @@ import ReceiptModal from "../pages/ReceiptPage/components/ReceiptEditing/modals/
import SelectScannedProductModal from "../pages/ReceiptPage/modals/SelectScannedProductModal.tsx";
import BoardModal from "../pages/CardsPage/modals/BoardModal/BoardModal.tsx";
import StatusModal from "../pages/CardsPage/modals/StatusModal/StatusModal.tsx";
import AttributeModal from "../pages/CardsPage/drawers/ProjectsEditorDrawer/tabs/AttributesTab/modals/AttributeModal.tsx";
import AttributeModal from "../pages/AdminPage/tabs/Attributes/modals/AttributeModal.tsx";
import CreateProjectModal
from "../pages/CardsPage/drawers/ProjectsEditorDrawer/tabs/ProjectsTab/modals/CreateProjectModal.tsx";
from "../pages/CardsPage/drawers/ProjectEditDrawer/tabs/General/modals/CreateProjectModal.tsx";
export const modals = {
enterDeadline: EnterDeadlineModal,

View File

@@ -6,7 +6,7 @@ import {
IconCalendarUser,
IconCoins,
IconCurrencyDollar,
IconQrcode,
IconQrcode, IconSubtask,
IconTopologyStar3,
IconUser,
} from "@tabler/icons-react";
@@ -21,6 +21,7 @@ import { RootState } from "../../redux/store.ts";
import OrganizationalStructureTab from "./tabs/OrganizationalStructureTab/OrganizationalStructureTab.tsx";
import { ReactNode } from "react";
import WorkShiftsPlanning from "./tabs/WorkShiftsPlanning/WorkShiftsPlanning.tsx";
import Attributes from "./tabs/Attributes/Attributes.tsx";
const AdminPage = () => {
const userRole = useSelector((state: RootState) => state.auth.role);
@@ -88,6 +89,13 @@ const AdminPage = () => {
Доходы и расходы
</Tabs.Tab>
)}
{isAdmin && (
<Tabs.Tab
value={"attributes"}
leftSection={<IconSubtask />}>
Атрибуты карточек
</Tabs.Tab>
)}
</Tabs.List>
{getTabPanel("users", <UsersTab />)}
{getTabPanel("rolesAndPositions", <OrganizationalStructureTab />)}
@@ -96,6 +104,7 @@ const AdminPage = () => {
{getTabPanel("workShiftsPlanning", <WorkShiftsPlanning />)}
{getTabPanel("workShifts", <WorkShiftsTab />)}
{getTabPanel("transactions", <TransactionsTab />)}
{getTabPanel("attributes", <Attributes />)}
</Tabs>
</PageBlock>
</div>

View File

@@ -1,15 +1,15 @@
import { BaseTable } from "../../../../../../components/BaseTable/BaseTable.tsx";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import useAttributesTableColumns from "./hooks/attributesTableColumns.tsx";
import useAttributesList from "../../../../../../hooks/useAttributesList.tsx";
import useAttributesList from "../../../../hooks/useAttributesList.tsx";
import { ActionIcon, Flex, Group, Stack, Text, Tooltip } from "@mantine/core";
import InlineButton from "../../../../../../components/InlineButton/InlineButton.tsx";
import InlineButton from "../../../../components/InlineButton/InlineButton.tsx";
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { AttributeSchema, AttributeService } from "../../../../../../client";
import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { AttributeSchema, AttributeService } from "../../../../client";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { MRT_TableOptions } from "mantine-react-table";
const AttributesTab = () => {
const Attributes = () => {
const columns = useAttributesTableColumns();
const { objects: attributes, refetch: refetchAttributes } = useAttributesList();
@@ -66,7 +66,7 @@ const AttributesTab = () => {
return (
<Stack>
<Group>
<InlineButton onClick={onCreateAttributeClick}>
<InlineButton onClick={onCreateAttributeClick} mt={"md"}>
<IconPlus />
Добавить атрибут
</InlineButton>
@@ -109,4 +109,4 @@ const AttributesTab = () => {
);
};
export default AttributesTab;
export default Attributes;

View File

@@ -1,5 +1,5 @@
import ObjectSelect, { ObjectSelectProps } from "../../../../../../../components/ObjectSelect/ObjectSelect.tsx";
import { AttributeTypeSchema } from "../../../../../../../client";
import ObjectSelect, { ObjectSelectProps } from "../../../../../components/ObjectSelect/ObjectSelect.tsx";
import { AttributeTypeSchema } from "../../../../../client";
import useAttributeTypesList from "../hooks/useAttributeTypesList.tsx";
type Props = Omit<ObjectSelectProps<AttributeTypeSchema>, "data">;

View File

@@ -1,7 +1,7 @@
import { Checkbox, NumberInput, TextInput } from "@mantine/core";
import { UseFormReturnType } from "@mantine/form";
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
import { AttributeSchema } from "../../../../../../../client";
import { AttributeSchema } from "../../../../../client";
type Props = {
form: UseFormReturnType<Partial<AttributeSchema>>;

View File

@@ -1,8 +1,8 @@
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { AttributeSchema } from "../../../../../../../client";
import { AttributeSchema } from "../../../../../client";
import { IconCheck, IconX } from "@tabler/icons-react";
import { formatDate, formatDateTime } from "../../../../../../../types/utils.ts";
import { formatDate, formatDateTime } from "../../../../../types/utils.ts";
const useAttributesTableColumns = () => {

View File

@@ -1,5 +1,5 @@
import { AttributeService } from "../../../../../../../client";
import ObjectList from "../../../../../../../hooks/objectList.tsx";
import { AttributeService } from "../../../../../client";
import ObjectList from "../../../../../hooks/objectList.tsx";
const useAttributeTypesList = () =>
ObjectList({

View File

@@ -2,11 +2,11 @@ import { ContextModalProps } from "@mantine/modals";
import { Button, Checkbox, Stack, Textarea, TextInput } from "@mantine/core";
import { useEffect, useState } from "react";
import AttributeTypeSelect from "../components/AttributeTypeSelect.tsx";
import { AttributeSchema, AttributeService } from "../../../../../../../client";
import { convertRussianToSnakeCase } from "../../../utils/stringConverting.ts";
import { AttributeSchema, AttributeService } from "../../../../../client";
import { convertRussianToSnakeCase } from "../../../../CardsPage/drawers/ProjectEditDrawer/utils/stringConverting.ts";
import { useForm } from "@mantine/form";
import DefaultAttributeValueInput from "../components/DefaultAttributeValueInput.tsx";
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
import { notifications } from "../../../../../shared/lib/notifications.ts";
type Props = {

View File

@@ -1,5 +1,5 @@
import { ActionIcon, Flex, rem, Text } from "@mantine/core";
import { IconEdit, IconMenu2, IconMenuDeep } from "@tabler/icons-react";
import { IconEdit, IconMenu2, IconMenuDeep, IconPlus } from "@tabler/icons-react";
import { motion } from "framer-motion";
import styles from "../../ui/CardsPage.module.css";
import PageBlock from "../../../../components/PageBlock/PageBlock.tsx";
@@ -7,30 +7,41 @@ import DisplayMode from "../../enums/DisplayMode.ts";
import { UseFormReturnType } from "@mantine/form";
import { CardsPageState } from "../../hooks/useCardsPageState.tsx";
import React from "react";
import { ProjectSchema } from "../../../../client";
import ObjectSelect from "../../../../components/ObjectSelect/ObjectSelect.tsx";
import CardsTableFiltersModal from "../../modals/CardsTableFiltersModal.tsx";
import { useProjectsEditorContext } from "../../contexts/ProjectsEditorContext.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import { modals } from "@mantine/modals";
type Props = {
displayMode: DisplayMode;
setDisplayMode: React.Dispatch<React.SetStateAction<DisplayMode>>;
form: UseFormReturnType<CardsPageState>;
projects: ProjectSchema[];
}
const CardsPageHeader = ({
displayMode,
setDisplayMode,
form,
projects,
}: Props) => {
const { openProjectsEditor } = useProjectsEditorContext();
const { selectedProject, setSelectedProject, projects, refetchProjects } = useProjectsContext();
const userRole = useSelector((state: RootState) => state.auth.role);
const isAdmin = userRole === "admin";
const handleCreateClick = () => {
modals.openContextModal({
modal: "createProjectModal",
title: "Создание проекта",
withCloseButton: false,
innerProps: {
refetchProjects,
},
});
};
const getHeaderInputsBoard = () => {
return (
<div
@@ -40,17 +51,26 @@ const CardsPageHeader = ({
}}
>
{isAdmin && (
<ActionIcon
size={"lg"}
onClick={openProjectsEditor}
variant={"default"}>
<IconEdit />
</ActionIcon>
<>
<ActionIcon
size={"lg"}
onClick={handleCreateClick}
variant={"default"}>
<IconPlus />
</ActionIcon>
<ActionIcon
size={"lg"}
onClick={openProjectsEditor}
variant={"default"}>
<IconEdit />
</ActionIcon>
</>
)}
<ObjectSelect
placeholder={"Выберите проект"}
data={projects}
{...form.getInputProps("project")}
value={selectedProject}
onChange={setSelectedProject}
/>
</div>
);

View File

@@ -1,12 +1,11 @@
import React, { createContext, FC, useContext, useEffect, useState } from "react";
import { CardSchema, CardService, ProjectSchema } from "../../../client";
import { CardSchema, CardService } from "../../../client";
type CardPageContextState = {
selectedCard?: CardSchema;
setSelectedCard: (card: CardSchema | undefined) => void;
refetchCards: () => Promise<void>;
refetchCard: () => void;
selectedProject?: ProjectSchema | null;
};
const CardPageContext = createContext<CardPageContextState | undefined>(
@@ -16,7 +15,6 @@ const CardPageContext = createContext<CardPageContextState | undefined>(
type CardPageContextStateProps = {
refetchCards: () => Promise<void>;
defaultCardId?: number;
selectedProject?: ProjectSchema | null;
}
const useCardPageContextState = (props: CardPageContextStateProps) => {
@@ -41,7 +39,6 @@ const useCardPageContextState = (props: CardPageContextStateProps) => {
return {
selectedCard,
setSelectedCard,
selectedProject: props.selectedProject,
refetchCards,
refetchCard,
};

View File

@@ -5,37 +5,28 @@ type ProjectsEditorContextState = {
openedProjectsEditor: boolean;
openProjectsEditor: () => void;
closeProjectsEditor: () => void;
onUpdate: () => void;
};
const ProjectsEditorContext = createContext<ProjectsEditorContextState | undefined>(
undefined,
);
type ProjectsEditorContextStateProps = {
onUpdate: () => void;
}
const useProjectsEditorContextState = ({ onUpdate }: ProjectsEditorContextStateProps) => {
const useProjectsEditorContextState = () => {
const [opened, { open, close }] = useDisclosure(false);
return {
openedProjectsEditor: opened,
openProjectsEditor: open,
closeProjectsEditor: close,
onUpdate,
};
};
type ProjectsEditorContextProviderProps = {
children: React.ReactNode;
} & ProjectsEditorContextStateProps;
};
export const ProjectsEditorContextProvider: FC<ProjectsEditorContextProviderProps> = ({
children,
...props
}) => {
const state = useProjectsEditorContextState(props);
export const ProjectsEditorContextProvider: FC<ProjectsEditorContextProviderProps> = ({ children }) => {
const state = useProjectsEditorContextState();
return (
<ProjectsEditorContext.Provider value={state}>
@@ -48,7 +39,7 @@ export const useProjectsEditorContext = () => {
const context = useContext(ProjectsEditorContext);
if (!context) {
throw new Error(
"useProjectsEditorContext must be used within a ProjectsEditorContextProvider",
"useProjectEditorContext must be used within a ProjectEditorContextProvider",
);
}
return context;

View File

@@ -1,13 +1,14 @@
import { Box, Drawer, rem, Tabs } from "@mantine/core";
import { IconSettings, IconSubtask } from "@tabler/icons-react";
import { IconHexagons, IconSettings, IconSubtask } from "@tabler/icons-react";
import { ReactNode } from "react";
import { motion } from "framer-motion";
import { useProjectsEditorContext } from "../../contexts/ProjectsEditorContext.tsx";
import ProjectsTab from "./tabs/ProjectsTab/ProjectsTab.tsx";
import AttributesTab from "./tabs/AttributesTab/AttributesTab.tsx";
import General from "./tabs/General/General.tsx";
import Attributes from "./tabs/Attributes/Attributes.tsx";
import Modules from "./tabs/Modules/Modules.tsx";
const ProjectsEditorDrawer = () => {
const ProjectEditDrawer = () => {
const { closeProjectsEditor, openedProjectsEditor } = useProjectsEditorContext();
const getTabPanel = (value: string, component: ReactNode) => {
@@ -45,16 +46,21 @@ const ProjectsEditorDrawer = () => {
},
}}>
<Tabs
defaultValue={"projects"}
defaultValue={"general"}
flex={1}
variant={"outline"}
orientation={"vertical"}
keepMounted={false}>
<Tabs.List>
<Tabs.Tab
value={"projects"}
value={"general"}
leftSection={<IconSettings />}>
Проекты
Общее
</Tabs.Tab>
<Tabs.Tab
value={"modules"}
leftSection={<IconHexagons />}>
Модули
</Tabs.Tab>
<Tabs.Tab
value={"attributes"}
@@ -63,11 +69,12 @@ const ProjectsEditorDrawer = () => {
</Tabs.Tab>
</Tabs.List>
{getTabPanel("projects", <ProjectsTab />)}
{getTabPanel("attributes", <AttributesTab />)}
{getTabPanel("general", <General/>)}
{getTabPanel("attributes", <Attributes/>)}
{getTabPanel("modules", <Modules />)}
</Tabs>
</Drawer>
);
};
export default ProjectsEditorDrawer;
export default ProjectEditDrawer;

View File

@@ -0,0 +1,71 @@
import { ProjectService } from "../../../../../../client";
import useAttributesList from "../../../../../../hooks/useAttributesList.tsx";
import { useEffect, useState } from "react";
import { useSet } from "@mantine/hooks";
import useAttributesTableColumns from "./hooks/attributesTableColumns.tsx";
import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { rem, Stack } from "@mantine/core";
import { BaseTable } from "../../../../../../components/BaseTable/BaseTable.tsx";
import eqSet from "../../utils/eqSet.ts";
import InlineButton from "../../../../../../components/InlineButton/InlineButton.tsx";
import { IconCheck } from "@tabler/icons-react";
import { useProjectsContext } from "../../../../../../contexts/ProjectsContext.tsx";
const Attributes = () => {
const { selectedProject: project, refetchProjects } = useProjectsContext();
const { objects: attributes } = useAttributesList();
const [defaultSelectedAttributes, setDefaultSelectedAttributes] = useState(new Set<number>(project?.attributes.map(m => m.id)));
const selectedAttributes = useSet<number>(project?.attributes.map(a => a.id));
const columns = useAttributesTableColumns({ selectedAttributes });
useEffect(() => {
selectedAttributes.clear();
project?.attributes.forEach(attribute => {
selectedAttributes.add(attribute.id);
});
setDefaultSelectedAttributes(new Set([...selectedAttributes]));
}, [project]);
const onUpdateAttributesClick = () => {
if (!project) return;
ProjectService.updateProjectAttributes({
requestBody: {
projectId: project.id,
attributeIds: selectedAttributes.values().toArray(),
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
refetchProjects();
})
.catch(err => console.log(err));
};
return (
<Stack gap={rem(10)}>
<BaseTable
data={attributes}
columns={columns}
restProps={{
enableSorting: false,
enableColumnActions: false,
enableRowVirtualization: true,
mantineTableContainerProps: { style: { maxHeight: "88vh" } },
}}
/>
{!eqSet(selectedAttributes, defaultSelectedAttributes) && (
<InlineButton onClick={onUpdateAttributesClick}>
<IconCheck />
Сохранить
</InlineButton>
)}
</Stack>
);
};
export default Attributes;

View File

@@ -0,0 +1,114 @@
import { Button, Fieldset, Flex, rem, Stack, Text, TextInput } from "@mantine/core";
import { ProjectService } from "../../../../../../client";
import { useForm } from "@mantine/form";
import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { isEqual } from "lodash";
import { useProjectsContext } from "../../../../../../contexts/ProjectsContext.tsx";
import { modals } from "@mantine/modals";
import { useProjectsEditorContext } from "../../../../contexts/ProjectsEditorContext.tsx";
type ProjectForm = {
name: string;
}
const General = () => {
const { selectedProject: project, refetchProjects } = useProjectsContext();
const { closeProjectsEditor } = useProjectsEditorContext();
if (!project) return;
const form = useForm<ProjectForm>({
initialValues: project,
validate: {
name: name => !name && "Название проекта не введено",
},
});
const onProjectDelete = () => {
ProjectService.deleteProject({
projectId: project.id,
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
closeProjectsEditor();
refetchProjects();
})
.catch(err => console.log(err));
};
const onDeleteProjectClick = () => {
modals.openConfirmModal({
title: "Удаление проекта",
children: (
<Text size="sm">
Вы уверены что хотите удалить проект "{project.name}"?
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onProjectDelete(),
});
};
const onSubmit = (values: ProjectForm) => {
ProjectService.updateProject({
requestBody: {
project: {
id: project.id,
name: values.name,
},
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
refetchProjects();
})
.catch(err => console.log(err));
};
return (
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
<Stack>
<Fieldset legend={"Общие параметры"}>
<Stack>
<TextInput
label={"Название"}
{...form.getInputProps("name")}
/>
</Stack>
</Fieldset>
<Flex direction={"row-reverse"} gap={rem(10)}>
<Button
variant={"default"}
type={"submit"}
disabled={isEqual(project, form.values)}
>
Сохранить изменения
</Button>
<Button
type={"reset"}
variant={"default"}
disabled={isEqual(project, form.values)}
onClick={() => form.reset()}
>
Отменить изменения
</Button>
<Button
variant={"default"}
onClick={() => onDeleteProjectClick()}
>
Удалить
</Button>
</Flex>
</Stack>
</form>
);
};
export default General;

View File

@@ -0,0 +1,70 @@
import useModulesList from "./hooks/useModulesList.tsx";
import { useEffect, useState } from "react";
import { useSet } from "@mantine/hooks";
import useModulesTableColumns from "./hooks/modulesTableColumns.tsx";
import { ProjectService } from "../../../../../../client";
import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { rem, Stack } from "@mantine/core";
import { BaseTable } from "../../../../../../components/BaseTable/BaseTable.tsx";
import eqSet from "../../utils/eqSet.ts";
import InlineButton from "../../../../../../components/InlineButton/InlineButton.tsx";
import { IconCheck } from "@tabler/icons-react";
import { useProjectsContext } from "../../../../../../contexts/ProjectsContext.tsx";
const Modules = () => {
const { selectedProject: project, refetchProjects } = useProjectsContext();
const { objects: modules } = useModulesList();
const [defaultSelectedModules, setDefaultSelectedModules] = useState(
new Set<number>(project?.modules.map(m => m.id)),
);
const selectedModules = useSet<number>();
const columns = useModulesTableColumns({ selectedModules });
useEffect(() => {
selectedModules.clear();
project?.modules.forEach(module => {
selectedModules.add(module.id);
});
setDefaultSelectedModules(new Set([...selectedModules]));
}, [project]);
const updateProjectModules = () => {
if (!project) return;
ProjectService.updateProjectModules({
requestBody: {
projectId: project.id,
moduleIds: selectedModules.values().toArray(),
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
refetchProjects();
})
.catch(err => console.log(err));
};
return (
<Stack gap={rem(10)}>
<BaseTable
data={modules}
columns={columns}
restProps={{
enableSorting: false,
enableColumnActions: false,
}}
/>
{!eqSet(selectedModules, defaultSelectedModules) && (
<InlineButton onClick={updateProjectModules}>
<IconCheck />
Сохранить
</InlineButton>
)}
</Stack>
);
};
export default Modules;

View File

@@ -1,43 +0,0 @@
import { Group } from "@mantine/core";
import { ProjectSchema } from "../../../../../../client";
import ModulesPicker from "./components/ModulesPicker.tsx";
import { useEffect, useState } from "react";
import AttributesPicker from "./components/AttributesPicker.tsx";
import useProjects from "../../../../hooks/useProjects.tsx";
import ProjectsEditor from "./components/ProjectsEditor.tsx";
const ProjectsTab = () => {
const { projects, refetchProjects } = useProjects();
const [selectedProject, setSelectedProject] = useState<ProjectSchema | null>(null);
useEffect(() => {
if (projects.length > 0) {
if (!selectedProject) {
setSelectedProject(projects[0]);
} else {
setSelectedProject(projects.find((project) => project.id === selectedProject.id) ?? null);
}
}
}, [projects]);
return (
<Group align={"flex-start"}>
<ProjectsEditor
projects={projects}
refetchProjects={refetchProjects}
selectedProject={selectedProject}
setSelectedProject={setSelectedProject}
/>
<ModulesPicker
project={selectedProject}
refetchProjects={refetchProjects}
/>
<AttributesPicker
project={selectedProject}
refetchProjects={refetchProjects}
/>
</Group>
);
};
export default ProjectsTab;

View File

@@ -1,79 +0,0 @@
import { ProjectSchema, ProjectService } from "../../../../../../../client";
import styles from "../../../ProjectsEditorDrawer.module.css";
import { Center, rem, Stack, Title } from "@mantine/core";
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
import useAttributesList from "../../../../../../../hooks/useAttributesList.tsx";
import useAttributesTableColumns from "../hooks/attributesTableColumns.tsx";
import { useSet } from "@mantine/hooks";
import { useEffect, useState } from "react";
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
import InlineButton from "../../../../../../../components/InlineButton/InlineButton.tsx";
import { IconCheck } from "@tabler/icons-react";
import eqSet from "../../../utils/eqSet.ts";
type Props = {
project: ProjectSchema | null;
refetchProjects: () => void;
}
const AttributesPicker = ({ project, refetchProjects }: Props) => {
const { objects: attributes } = useAttributesList();
const [defaultSelectedAttributes, setDefaultSelectedAttributes] = useState(new Set<number>(project?.attributes.map(m => m.id)));
const selectedAttributes = useSet<number>(project?.attributes.map(a => a.id));
const columns = useAttributesTableColumns({ selectedAttributes });
useEffect(() => {
selectedAttributes.clear();
project?.attributes.forEach(attribute => {
selectedAttributes.add(attribute.id);
});
setDefaultSelectedAttributes(new Set([...selectedAttributes]));
}, [project]);
const onUpdateAttributesClick = () => {
if (!project) return;
ProjectService.updateProjectAttributes({
requestBody: {
projectId: project.id,
attributeIds: selectedAttributes.values().toArray(),
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
refetchProjects();
})
.catch(err => console.log(err));
};
return (
<div className={styles["container-wrapper"]} style={{ flex: 2 }}>
<Stack gap={rem(10)}>
<Center>
<Title order={4}>Атрибуты проекта</Title>
</Center>
<BaseTable
data={attributes}
columns={columns}
restProps={{
enableSorting: false,
enableColumnActions: false,
enableRowVirtualization: true,
mantineTableContainerProps: { style: { maxHeight: "88vh" } },
}}
/>
{!eqSet(selectedAttributes, defaultSelectedAttributes) && (
<InlineButton onClick={onUpdateAttributesClick}>
<IconCheck />
Сохранить
</InlineButton>
)}
</Stack>
</div>
);
};
export default AttributesPicker;

View File

@@ -1,78 +0,0 @@
import { ProjectSchema, ProjectService } from "../../../../../../../client";
import useModulesList from "../hooks/useModulesList.tsx";
import styles from "../../../ProjectsEditorDrawer.module.css";
import { useSet } from "@mantine/hooks";
import useModulesTableColumns from "../hooks/modulesTableColumns.tsx";
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
import { Center, rem, Stack, Title } from "@mantine/core";
import { useEffect, useState } from "react";
import InlineButton from "../../../../../../../components/InlineButton/InlineButton.tsx";
import { IconCheck } from "@tabler/icons-react";
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
import eqSet from "../../../utils/eqSet.ts";
type Props = {
project: ProjectSchema | null;
refetchProjects: () => void;
}
const ModulesPicker = ({ project, refetchProjects }: Props) => {
const { objects: modules } = useModulesList();
const [defaultSelectedModules, setDefaultSelectedModules] = useState(
new Set<number>(project?.modules.map(m => m.id)),
);
const selectedModules = useSet<number>();
const columns = useModulesTableColumns({ selectedModules });
useEffect(() => {
selectedModules.clear();
project?.modules.forEach(module => {
selectedModules.add(module.id);
});
setDefaultSelectedModules(new Set([...selectedModules]));
}, [project]);
const updateProjectModules = () => {
if (!project) return;
ProjectService.updateProjectModules({
requestBody: {
projectId: project.id,
moduleIds: selectedModules.values().toArray(),
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
refetchProjects();
})
.catch(err => console.log(err));
};
return (
<div className={styles["container-wrapper"]}>
<Stack gap={rem(10)}>
<Center>
<Title order={4}>Модули проекта</Title>
</Center>
<BaseTable
data={modules}
columns={columns}
restProps={{
enableSorting: false,
enableColumnActions: false,
}}
/>
{!eqSet(selectedModules, defaultSelectedModules) && (
<InlineButton onClick={updateProjectModules}>
<IconCheck />
Сохранить
</InlineButton>
)}
</Stack>
</div>
);
};
export default ModulesPicker;

View File

@@ -1,95 +0,0 @@
import styles from "../../../ProjectsEditorDrawer.module.css";
import { ActionIcon, Center, Flex, rem, Stack, Title, Tooltip } from "@mantine/core";
import InlineButton from "../../../../../../../components/InlineButton/InlineButton.tsx";
import { IconCheck, IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
import { MRT_TableOptions } from "mantine-react-table";
import { FullProjectSchema, ProjectSchema } from "../../../../../../../client";
import useProjectsTableColumns from "../hooks/projectsTableColumns.tsx";
import useProjectsTab from "../hooks/useProjectsTab.tsx";
import React from "react";
type Props = {
projects: ProjectSchema[];
refetchProjects: () => void;
selectedProject: ProjectSchema | null;
setSelectedProject: React.Dispatch<React.SetStateAction<ProjectSchema | null>>;
}
const ProjectsEditor = ({
projects,
refetchProjects,
selectedProject,
setSelectedProject,
}: Props) => {
const {
editingProjects,
handleEditClick,
handleDeleteClick,
handleCreateClick,
} = useProjectsTab({
refetchProjects,
});
const columns = useProjectsTableColumns({ editingProjects, selectedProject, setSelectedProject });
return (
<div className={styles["container-wrapper"]} style={{ flex: 1.5 }}>
<Stack gap={rem(10)}>
<Center>
<Title order={4}>Проекты</Title>
</Center>
<BaseTable
data={projects}
columns={columns}
restProps={
{
enableSorting: false,
enableColumnActions: false,
enableRowActions: true,
positionActionsColumn: "last",
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => handleEditClick(row.original)}
variant={"default"}>
{
editingProjects.has(row.original.id) ? (
<IconCheck />
) : (
<IconEdit />
)
}
</ActionIcon>
</Tooltip>
<Tooltip label={"Удалить"}>
<ActionIcon
onClick={() => handleDeleteClick(row.original)}
disabled={row.original.boardsCount > 0}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<FullProjectSchema>
}
/>
<InlineButton
variant={"default"}
onClick={handleCreateClick}
style={{ border: "dashed var(--item-border-size) var(--mantine-color-default-border)" }}
>
<IconPlus />
Добавить
</InlineButton>
</Stack>
</div>
);
};
export default ProjectsEditor;

View File

@@ -1,83 +0,0 @@
import { ProjectSchema, ProjectService } from "../../../../../../../client";
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
import { useProjectsEditorContext } from "../../../../../contexts/ProjectsEditorContext.tsx";
import { useMap } from "@mantine/hooks";
import { modals } from "@mantine/modals";
type Props = {
refetchProjects: () => void;
}
const useProjectsTab = ({ refetchProjects }: Props) => {
const { onUpdate } = useProjectsEditorContext();
const editingProjects = useMap<number, ProjectSchema>();
const updateProject = (project: ProjectSchema) => {
ProjectService.updateProject({
requestBody: {
project,
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
editingProjects.delete(project.id);
refetchProjects();
onUpdate();
})
.catch(err => console.log(err));
};
const handleEditClick = (project: ProjectSchema) => {
const editedProject = editingProjects.get(project.id);
if (!editedProject) {
editingProjects.set(project.id, project);
return;
}
if (editedProject.name.length === 0) {
notifications.error({ message: "Имя проекта не может быть пустым" });
return;
}
updateProject(project);
};
const handleDeleteClick = (project: ProjectSchema) => {
ProjectService.deleteProject({
projectId: project.id,
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
refetchProjects();
onUpdate();
})
.catch(err => console.log(err));
};
const handleCreateClick = () => {
modals.openContextModal({
modal: "createProjectModal",
title: "Создание проекта",
withCloseButton: false,
innerProps: {
refetchProjects,
},
});
};
return {
editingProjects,
handleEditClick,
handleDeleteClick,
handleCreateClick,
};
};
export default useProjectsTab;

View File

@@ -1,18 +1,17 @@
import { useEffect, useState } from "react";
import { BoardSchema, BoardService } from "../../../client";
import { useProjectsContext } from "../../../contexts/ProjectsContext.tsx";
type Props = {
projectId?: number;
}
const useBoards = ({ projectId }: Props) => {
const useBoards = () => {
const { selectedProject } = useProjectsContext();
const [boards, setBoards] = useState<BoardSchema[]>([]);
const refetchBoards = () => {
if (!projectId) return;
if (!selectedProject) return;
BoardService.getBoards({
projectId,
projectId: selectedProject.id,
})
.then(data => {
setBoards(data.boards);
@@ -22,7 +21,7 @@ const useBoards = ({ projectId }: Props) => {
useEffect(() => {
refetchBoards();
}, [projectId]);
}, [selectedProject]);
return {
boards,

View File

@@ -3,28 +3,21 @@ import { useForm } from "@mantine/form";
import { useEffect, useState } from "react";
import { BaseMarketplaceSchema, BoardSchema, ClientSchema, ProjectSchema, StatusSchema } from "../../../client";
type Props = {
projects: ProjectSchema[];
}
export type CardsPageState = {
id: number | null;
marketplace: BaseMarketplaceSchema | null;
client: ClientSchema | null;
project: ProjectSchema | null;
projectForTable: ProjectSchema | null;
board: BoardSchema | null;
status: StatusSchema | null;
};
const useCardsPageState = ({ projects }: Props) => {
const useCardsPageState = () => {
const { objects } = useCardSummariesFull();
const form = useForm<CardsPageState>({
initialValues: {
project: null,
id: null,
marketplace: null,
client: null,
@@ -51,7 +44,7 @@ const useCardsPageState = ({ projects }: Props) => {
}
if (form.values.projectForTable) {
result = result.filter(
obj => obj.board.projectId === form.values.project?.id,
obj => obj.board.projectId === form.values.projectForTable?.id,
);
if (form.values.board) {
@@ -78,12 +71,6 @@ const useCardsPageState = ({ projects }: Props) => {
applyFilters();
}, [form.values, objects]);
useEffect(() => {
if (projects.length > 0 && form.values.project === null) {
form.setFieldValue("project", projects[0]);
}
}, [projects]);
return { data, form };
};

View File

@@ -13,16 +13,14 @@ import { useParams } from "@tanstack/react-router";
import { PrefillCardsWithExcelContextProvider } from "../contexts/PrefillDealsWithExcelContext.tsx";
import DisplayMode from "../enums/DisplayMode.ts";
import CardsPageHeader from "../components/CardsPageHeader/CardsPageHeader.tsx";
import useProjects from "../hooks/useProjects.tsx";
import Boards from "../../../components/Dnd/Boards/Boards/Boards.tsx";
import useBoards from "../hooks/useBoards.tsx";
import { ProjectsEditorContextProvider } from "../contexts/ProjectsEditorContext.tsx";
import ProjectsEditorDrawer from "../drawers/ProjectsEditorDrawer/ProjectsEditorDrawer.tsx";
import ProjectEditDrawer from "../drawers/ProjectEditDrawer/ProjectEditDrawer.tsx";
export const CardsPage: FC = () => {
const { projects, refetchProjects } = useProjects();
const { data, form } = useCardsPageState({ projects });
const { boards, refetchBoards } = useBoards({ projectId: form.values.project?.id });
const { data, form } = useCardsPageState();
const { boards, refetchBoards } = useBoards();
const { dealId } = useParams({ strict: false });
const { summariesRaw, refetch: refetchSummaries } = useCardSummaries();
@@ -74,39 +72,37 @@ export const CardsPage: FC = () => {
padding: 0,
}}
>
<CardPageContextProvider
defaultCardId={(dealId && parseInt(dealId)) || undefined}
refetchCards={async () => {
await refetchSummaries();
}}
selectedProject={form.values.project}
>
<PrefillCardContextProvider>
<PrefillCardsWithExcelContextProvider>
<ProjectsEditorContextProvider onUpdate={refetchProjects}>
<ProjectsEditorContextProvider>
<CardPageContextProvider
defaultCardId={(dealId && parseInt(dealId)) || undefined}
refetchCards={async () => {
await refetchSummaries();
}}
>
<PrefillCardContextProvider>
<PrefillCardsWithExcelContextProvider>
<CardsPageHeader
form={form}
displayMode={displayMode}
setDisplayMode={setDisplayMode}
projects={projects}
/>
<ProjectsEditorDrawer />
</ProjectsEditorContextProvider>
<PageBlock
style={{
display: "flex",
flexDirection: "column",
flex: 1,
height: "100%",
}}
>
{getBody()}
</PageBlock>
<CardEditDrawer />
<CardPrefillDrawer />
</PrefillCardsWithExcelContextProvider>
</PrefillCardContextProvider>
</CardPageContextProvider>
<PageBlock
style={{
display: "flex",
flexDirection: "column",
flex: 1,
height: "100%",
}}
>
{getBody()}
</PageBlock>
<CardEditDrawer />
<ProjectEditDrawer />
<CardPrefillDrawer />
</PrefillCardsWithExcelContextProvider>
</PrefillCardContextProvider>
</CardPageContextProvider>
</ProjectsEditorContextProvider>
</PageBlock>
);
};

View File

@@ -6,6 +6,7 @@ export enum Modules {
EMPLOYEES = "employees",
CLIENTS = "clients",
MANAGERS = "managers",
MEGA_MODULE = "hui",
}
const isModuleInProject = (module: Modules, project?: ProjectSchema | null) => {