Merge remote-tracking branch 'origin/cards'
# Conflicts: # src/client/index.ts
This commit is contained in:
@@ -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>
|
||||
|
||||
112
src/pages/AdminPage/tabs/Attributes/Attributes.tsx
Normal file
112
src/pages/AdminPage/tabs/Attributes/Attributes.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useAttributesTableColumns from "./hooks/attributesTableColumns.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 { 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 { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
const Attributes = () => {
|
||||
const columns = useAttributesTableColumns();
|
||||
const { objects: attributes, refetch: refetchAttributes } = useAttributesList();
|
||||
|
||||
const onCreateAttributeClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "attributeModal",
|
||||
title: "Создание атрибута",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
refetchAttributes,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onEditAttributeClick = (attribute: AttributeSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "attributeModal",
|
||||
title: "Редактирование атрибута",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
refetchAttributes,
|
||||
attribute,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttribute = (attribute: AttributeSchema) => {
|
||||
AttributeService.delete({
|
||||
attributeId: attribute.id,
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
refetchAttributes();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onDeleteAttributeClick = (attribute: AttributeSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление атрибута",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены, что хотите удалить атрибут "{attribute.label}"?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => deleteAttribute(attribute),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Group>
|
||||
<InlineButton onClick={onCreateAttributeClick} mt={"md"}>
|
||||
<IconPlus />
|
||||
Добавить атрибут
|
||||
</InlineButton>
|
||||
</Group>
|
||||
<BaseTable
|
||||
data={attributes}
|
||||
columns={columns}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableBottomToolbar: false,
|
||||
enableRowActions: true,
|
||||
enableRowVirtualization: true,
|
||||
mantineTableContainerProps: { style: { maxHeight: "86vh" } },
|
||||
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditAttributeClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteAttributeClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<AttributeSchema>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Attributes;
|
||||
@@ -0,0 +1,21 @@
|
||||
import ObjectSelect, { ObjectSelectProps } from "../../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import { AttributeTypeSchema } from "../../../../../client";
|
||||
import useAttributeTypesList from "../hooks/useAttributeTypesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<AttributeTypeSchema>, "data">;
|
||||
|
||||
const AttributeTypeSelect = (props: Props) => {
|
||||
const { objects: attributeTypes } = useAttributeTypesList();
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
label={"Тип атрибута"}
|
||||
getLabelFn={type => type.name}
|
||||
getValueFn={type => type.id.toString()}
|
||||
data={attributeTypes}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributeTypeSelect;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Checkbox, NumberInput, TextInput } from "@mantine/core";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
|
||||
import { AttributeSchema } from "../../../../../client";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturnType<Partial<AttributeSchema>>;
|
||||
}
|
||||
|
||||
const DefaultAttributeValueInput = ({ form }: Props) => {
|
||||
const type = form.values.type?.type;
|
||||
const label = "Значение по умолчанию";
|
||||
const inputName = "defaultValue";
|
||||
|
||||
if (type === "bool") {
|
||||
return (
|
||||
<Checkbox
|
||||
label={label}
|
||||
{...form.getInputProps(inputName, { type: "checkbox" })}
|
||||
/>
|
||||
);
|
||||
} else if (type === "date") {
|
||||
return (
|
||||
<DatePickerInput
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={form.values.defaultValue ? new Date(String(form.values.defaultValue)) : null}
|
||||
clearable
|
||||
locale={"ru-RU"}
|
||||
valueFormat="DD.MM.YYYY"
|
||||
/>
|
||||
);
|
||||
} else if (type === "datetime") {
|
||||
return (
|
||||
<DateTimePicker
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={form.values.defaultValue ? new Date(String(form.values.defaultValue)) : null}
|
||||
clearable
|
||||
locale={"ru-RU"}
|
||||
valueFormat="DD.MM.YYYY HH:mm"
|
||||
/>
|
||||
);
|
||||
} else if (type === "str") {
|
||||
return (
|
||||
<TextInput
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={form.getInputProps(inputName).value ?? ""}
|
||||
/>
|
||||
);
|
||||
} else if (type === "int" || type === "float") {
|
||||
return (
|
||||
<NumberInput
|
||||
allowDecimal={type === "float"}
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={form.values.defaultValue ? Number(form.values.defaultValue) : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default DefaultAttributeValueInput;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { AttributeSchema } from "../../../../../client";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
import { formatDate, formatDateTime } from "../../../../../types/utils.ts";
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
|
||||
const useAttributesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<AttributeSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "label",
|
||||
},
|
||||
{
|
||||
header: "Тип",
|
||||
accessorKey: "type.name",
|
||||
},
|
||||
{
|
||||
header: "Значение по умолчанию",
|
||||
accessorKey: "defaultValue",
|
||||
Cell: ({ cell, row }) => {
|
||||
const value = cell.getValue();
|
||||
if (value === null) return <>-</>;
|
||||
|
||||
const type = row.original.type.type;
|
||||
if (type === "datetime") {
|
||||
return formatDateTime(value as string);
|
||||
}
|
||||
if (type === "date") {
|
||||
return formatDate(value as string);
|
||||
}
|
||||
if (type === "bool") {
|
||||
return value ? <IconCheck /> : <IconX />;
|
||||
}
|
||||
|
||||
return <>{value}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Синхронизировано в группе",
|
||||
accessorKey: "isApplicableToGroup",
|
||||
Cell: ({ cell }) => cell.getValue() ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconX />
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Может быть пустым",
|
||||
accessorKey: "isNullable",
|
||||
Cell: ({ cell }) => cell.getValue() ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconX />
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Описаниие",
|
||||
accessorKey: "description",
|
||||
Cell: ({ row }) => <Box>{row.original.description}</Box>,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
export default useAttributesTableColumns;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AttributeService } from "../../../../../client";
|
||||
import ObjectList from "../../../../../hooks/objectList.tsx";
|
||||
|
||||
const useAttributeTypesList = () =>
|
||||
ObjectList({
|
||||
queryFn: AttributeService.getTypes,
|
||||
getObjectsFn: response => response.types,
|
||||
queryKey: "getAllAttributeTypes",
|
||||
});
|
||||
|
||||
export default useAttributeTypesList;
|
||||
170
src/pages/AdminPage/tabs/Attributes/modals/AttributeModal.tsx
Normal file
170
src/pages/AdminPage/tabs/Attributes/modals/AttributeModal.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
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 "../../../../CardsPage/drawers/ProjectEditDrawer/utils/stringConverting.ts";
|
||||
import { useForm } from "@mantine/form";
|
||||
import DefaultAttributeValueInput from "../components/DefaultAttributeValueInput.tsx";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
refetchAttributes: () => void;
|
||||
attribute?: AttributeSchema;
|
||||
};
|
||||
|
||||
const AttributeModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "attribute" in innerProps;
|
||||
const [isInitial, setIsInitial] = useState(true);
|
||||
const [isNullableInputShown, setIsNullableInputShown] = useState(true);
|
||||
const [copyTypeId, setCopyTypeId] = useState<number>();
|
||||
const closeModal = () => context.closeContextModal(id);
|
||||
|
||||
const form = useForm<Partial<AttributeSchema>>({
|
||||
initialValues: isEditing
|
||||
? innerProps.attribute
|
||||
: {
|
||||
label: "",
|
||||
name: "",
|
||||
type: undefined,
|
||||
isApplicableToGroup: false,
|
||||
isNullable: false,
|
||||
defaultValue: null,
|
||||
description: "",
|
||||
},
|
||||
validate: {
|
||||
label: label => !label?.trim() && "Название не заполнено",
|
||||
name: name => !name?.trim() && "Название генерирует некорректный уникальный ключ. Измените название",
|
||||
type: type => !type && "Тип атрибута не выбран",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldValue("name", convertRussianToSnakeCase(form.values.label ?? ""));
|
||||
}, [form.values.label]);
|
||||
|
||||
useEffect(() => {
|
||||
const type = form.values.type?.type;
|
||||
setIsNullableInputShown(type !== "bool");
|
||||
|
||||
if (!isInitial) {
|
||||
if (type === "bool") {
|
||||
form.setFieldValue("isNullable", false);
|
||||
form.setFieldValue("defaultValue", false);
|
||||
} else {
|
||||
form.setFieldValue("defaultValue", null);
|
||||
}
|
||||
}
|
||||
setIsInitial(false);
|
||||
setCopyTypeId(form.values.type?.id);
|
||||
}, [form.values.type?.id]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
if (form.values.defaultValue === null && !form.values.isNullable) {
|
||||
notifications.error({ message: "Укажите значение по умолчанию или разрешите пустое значение." });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const onCreate = (attribute: AttributeSchema) => {
|
||||
AttributeService.create({
|
||||
requestBody: {
|
||||
attribute: {
|
||||
...attribute,
|
||||
typeId: attribute.type.id,
|
||||
name: attribute.name.trim(),
|
||||
label: attribute.label.trim(),
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
innerProps.refetchAttributes();
|
||||
closeModal();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onChange = (attribute: AttributeSchema) => {
|
||||
AttributeService.update({
|
||||
requestBody: {
|
||||
attribute: {
|
||||
...attribute,
|
||||
typeId: attribute.type.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
innerProps.refetchAttributes();
|
||||
closeModal();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onSubmit = (values: AttributeSchema) => {
|
||||
if (!validate()) return;
|
||||
if (isEditing) {
|
||||
onChange(values);
|
||||
} else {
|
||||
onCreate(values);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values as AttributeSchema))}>
|
||||
<Stack gap={"md"}>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
{...form.getInputProps("label")}
|
||||
/>
|
||||
<TextInput
|
||||
label={"Уникальный ключ"}
|
||||
disabled
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<AttributeTypeSelect
|
||||
disabled={isEditing}
|
||||
{...form.getInputProps("type")}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Значение синхронизировано в группе"}
|
||||
{...form.getInputProps("isApplicableToGroup", { type: "checkbox" })}
|
||||
/>
|
||||
{isNullableInputShown && (
|
||||
<Checkbox
|
||||
label={"Может быть пустым"}
|
||||
{...form.getInputProps("isNullable", { type: "checkbox" })}
|
||||
/>
|
||||
)}
|
||||
{form.values.type && copyTypeId === form.values.type.id && (
|
||||
<DefaultAttributeValueInput form={form} />
|
||||
)}
|
||||
<Textarea
|
||||
label={"Описание"}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributeModal;
|
||||
@@ -176,7 +176,7 @@ export const TransactionsTab = () => {
|
||||
/>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
style={{ alignSelf: "flex-end", marginTop: rem(10) }}
|
||||
withEdges
|
||||
onChange={event => setPage(event)}
|
||||
value={page}
|
||||
|
||||
1
src/pages/CardPage/index.ts
Normal file
1
src/pages/CardPage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CardPage } from "./ui/CardPage.tsx";
|
||||
38
src/pages/CardPage/ui/CardPage.tsx
Normal file
38
src/pages/CardPage/ui/CardPage.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { CardPageContextProvider, useCardPageContext } from "../../CardsPage/contexts/CardPageContext.tsx";
|
||||
import ProductAndServiceTab from "../../../modules/cardModules/cardEditorTabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
|
||||
import React, { FC, useEffect } from "react";
|
||||
import { CardService } from "../../../client";
|
||||
|
||||
export type Props = {
|
||||
cardId: number;
|
||||
};
|
||||
const CardPageContent: FC<Props> = ({ cardId }) => {
|
||||
const { setSelectedCard } = useCardPageContext();
|
||||
useEffect(() => {
|
||||
CardService.getCardById({ cardId }).then(card => {
|
||||
setSelectedCard(card);
|
||||
});
|
||||
}, []);
|
||||
return <ProductAndServiceTab />;
|
||||
};
|
||||
|
||||
const CardPageWrapper: FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<CardPageContextProvider
|
||||
refetchCards={async () => {
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CardPageContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardPage = () => {
|
||||
const { dealId } = useParams({ strict: false });
|
||||
return (
|
||||
<CardPageWrapper>
|
||||
<CardPageContent cardId={parseInt(dealId || "-1")} />
|
||||
</CardPageWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { TagsInput, TagsInputProps } from "@mantine/core";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
|
||||
type Props = Omit<TagsInputProps, "data">
|
||||
|
||||
const CardTagsInput = (props: Props) => {
|
||||
const { selectedProject } = useProjectsContext();
|
||||
|
||||
return (
|
||||
<TagsInput
|
||||
{...props}
|
||||
data={selectedProject?.tags.map(tag => tag.name)}
|
||||
label={"Теги"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardTagsInput;
|
||||
@@ -1,58 +1,76 @@
|
||||
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/DealsPage.module.css";
|
||||
import styles from "../../ui/CardsPage.module.css";
|
||||
import PageBlock from "../../../../components/PageBlock/PageBlock.tsx";
|
||||
import DisplayMode from "../../enums/DisplayMode.ts";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { DealsPageState } from "../../hooks/useDealsPageState.tsx";
|
||||
import { CardsPageState } from "../../hooks/useCardsPageState.tsx";
|
||||
import React from "react";
|
||||
import { ProjectSchema } from "../../../../client";
|
||||
import { modals } from "@mantine/modals";
|
||||
import ObjectSelect from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import DealsTableFiltersModal from "../../modals/DealsTableFiltersModal.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<DealsPageState>;
|
||||
projects: ProjectSchema[];
|
||||
refetchProjects: () => void;
|
||||
form: UseFormReturnType<CardsPageState>;
|
||||
}
|
||||
|
||||
const LeadsPageHeader = ({
|
||||
const CardsPageHeader = ({
|
||||
displayMode,
|
||||
setDisplayMode,
|
||||
form,
|
||||
projects,
|
||||
refetchProjects,
|
||||
}: Props) => {
|
||||
const openModal = () => {
|
||||
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: "projectsModal",
|
||||
title: "Проекты",
|
||||
modal: "createProjectModal",
|
||||
title: "Создание проекта",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onUpdate: refetchProjects,
|
||||
refetchProjects,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getHeaderInputsBoard = () => {
|
||||
return (
|
||||
<div className={styles["top-panel"]}
|
||||
style={{
|
||||
display: displayMode === DisplayMode.BOARD ? "flex" : "none",
|
||||
}}>
|
||||
<ActionIcon
|
||||
size={"lg"}
|
||||
onClick={openModal}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
<div
|
||||
className={styles["top-panel"]}
|
||||
style={{
|
||||
display: displayMode === DisplayMode.BOARD ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
@@ -66,7 +84,7 @@ const LeadsPageHeader = ({
|
||||
display: displayMode === DisplayMode.TABLE ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
<DealsTableFiltersModal
|
||||
<CardsTableFiltersModal
|
||||
form={form}
|
||||
projects={projects}
|
||||
/>
|
||||
@@ -124,4 +142,4 @@ const LeadsPageHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default LeadsPageHeader;
|
||||
export default CardsPageHeader;
|
||||
@@ -1,24 +1,24 @@
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { DealService, DealSummary } from "../../../../client";
|
||||
import { CardService, CardSummary } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import useDealsTableColumns from "./columns.tsx";
|
||||
import useCardsTableColumns from "./columns.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { useDealPageContext } from "../../contexts/DealPageContext.tsx";
|
||||
import { useCardPageContext } from "../../contexts/CardPageContext.tsx";
|
||||
|
||||
type RestProps = {
|
||||
viewOnly?: boolean;
|
||||
};
|
||||
type Props = CRUDTableProps<DealSummary> & RestProps;
|
||||
type Props = CRUDTableProps<CardSummary> & RestProps;
|
||||
|
||||
const DealsTable: FC<Props> = ({ items, onSelectionChange, viewOnly = false }) => {
|
||||
const columns = useDealsTableColumns();
|
||||
const { setSelectedDeal } = useDealPageContext();
|
||||
const onEditClick = (dealSummary: DealSummary) => {
|
||||
DealService.getDealById({ dealId: dealSummary.id }).then(deal => {
|
||||
setSelectedDeal(deal);
|
||||
const CardsTable: FC<Props> = ({ items, onSelectionChange, viewOnly = false }) => {
|
||||
const columns = useCardsTableColumns();
|
||||
const { setSelectedCard } = useCardPageContext();
|
||||
const onEditClick = (cardSummary: CardSummary) => {
|
||||
CardService.getCardById({ cardId: cardSummary.id }).then(card => {
|
||||
setSelectedCard(card);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -48,10 +48,10 @@ const DealsTable: FC<Props> = ({ items, onSelectionChange, viewOnly = false }) =
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<DealSummary>
|
||||
} as MRT_TableOptions<CardSummary>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealsTable;
|
||||
export default CardsTable;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { DealSummary } from "../../../../client";
|
||||
import { CardSummary } from "../../../../client";
|
||||
import { ActionIcon, Image } from "@mantine/core";
|
||||
|
||||
const useDealsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<DealSummary>[]>(
|
||||
const useCardsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<CardSummary>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
@@ -53,4 +53,4 @@ const useDealsTableColumns = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default useDealsTableColumns;
|
||||
export default useCardsTableColumns;
|
||||
70
src/pages/CardsPage/contexts/CardPageContext.tsx
Normal file
70
src/pages/CardsPage/contexts/CardPageContext.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { createContext, FC, useContext, useEffect, useState } from "react";
|
||||
import { CardSchema, CardService } from "../../../client";
|
||||
|
||||
type CardPageContextState = {
|
||||
selectedCard?: CardSchema;
|
||||
setSelectedCard: (card: CardSchema | undefined) => void;
|
||||
refetchCards: () => Promise<void>;
|
||||
refetchCard: () => void;
|
||||
};
|
||||
|
||||
const CardPageContext = createContext<CardPageContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
type CardPageContextStateProps = {
|
||||
refetchCards: () => Promise<void>;
|
||||
defaultCardId?: number;
|
||||
}
|
||||
|
||||
const useCardPageContextState = (props: CardPageContextStateProps) => {
|
||||
const { refetchCards, defaultCardId } = props;
|
||||
const [selectedCard, setSelectedCard] = useState<CardSchema | undefined>(undefined);
|
||||
|
||||
const refetchCard = () => {
|
||||
const cardId = selectedCard?.id ?? defaultCardId;
|
||||
if (!cardId) return;
|
||||
|
||||
CardService.getCardById({ cardId }).then(card => {
|
||||
setSelectedCard(card);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetchCard();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
selectedCard,
|
||||
setSelectedCard,
|
||||
refetchCards,
|
||||
refetchCard,
|
||||
};
|
||||
};
|
||||
|
||||
type CardPageContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
} & CardPageContextStateProps;
|
||||
|
||||
export const CardPageContextProvider: FC<CardPageContextProviderProps> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const state = useCardPageContextState(props);
|
||||
|
||||
return (
|
||||
<CardPageContext.Provider value={state}>
|
||||
{children}
|
||||
</CardPageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCardPageContext = () => {
|
||||
const context = useContext(CardPageContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useCardPageContext must be used within a CardPageContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
91
src/pages/CardsPage/contexts/DndContext.tsx
Normal file
91
src/pages/CardsPage/contexts/DndContext.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { createContext, FC, useContext, useState } from "react";
|
||||
import { CardSummary } from "../../../client";
|
||||
import DragState from "../enums/DragState.ts";
|
||||
import useCardsDnd from "../../../components/Dnd/Cards/CardsDndColumn/hooks/useCardsDnd.tsx";
|
||||
import useStatusesDnd from "../../../components/Dnd/Statuses/Statuses/hooks/useStatusesDnd.tsx";
|
||||
import { DragStart, DropResult } from "@hello-pangea/dnd";
|
||||
import useBoardsDnd from "../../../components/Dnd/Boards/Boards/hooks/useBoardsDnd.tsx";
|
||||
|
||||
type DndContextState = {
|
||||
summaries: CardSummary[],
|
||||
dragState: DragState,
|
||||
onDragStart: (start: DragStart) => void,
|
||||
onDragEnd: (result: DropResult) => Promise<void>,
|
||||
};
|
||||
|
||||
const DndContext = createContext<DndContextState | undefined>(undefined);
|
||||
|
||||
type DndContextProps = {
|
||||
summariesRaw: CardSummary[];
|
||||
refetchSummaries: () => void;
|
||||
}
|
||||
|
||||
const useDndContextState = ({
|
||||
summariesRaw,
|
||||
refetchSummaries,
|
||||
}: DndContextProps) => {
|
||||
const [dragState, setDragState] = useState<DragState>(DragState.DRAG_ENDED);
|
||||
|
||||
const {
|
||||
summaries,
|
||||
onCardDragEnd,
|
||||
} = useCardsDnd({
|
||||
summariesRaw,
|
||||
refetchSummaries,
|
||||
});
|
||||
|
||||
const { onStatusDragEnd } = useStatusesDnd();
|
||||
|
||||
const { onBoardDragEnd } = useBoardsDnd();
|
||||
|
||||
const onDragEnd = async (result: DropResult) => {
|
||||
setDragState(DragState.DRAG_ENDED);
|
||||
if (result.draggableId.includes("status")) {
|
||||
return onStatusDragEnd(result);
|
||||
}
|
||||
if (result.draggableId.includes("board")) {
|
||||
return onBoardDragEnd(result);
|
||||
}
|
||||
return onCardDragEnd(result);
|
||||
};
|
||||
|
||||
const onDragStart = (start: DragStart) => {
|
||||
if (start.source.droppableId.includes("status")) {
|
||||
setDragState(DragState.DRAG_STATUS);
|
||||
} else if (start.source.droppableId.includes("board")) {
|
||||
setDragState(DragState.DRAG_BOARD);
|
||||
} else {
|
||||
setDragState(DragState.DRAG_CARD);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
summaries,
|
||||
dragState,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
};
|
||||
};
|
||||
|
||||
type DndContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
} & DndContextProps;
|
||||
|
||||
export const DndContextProvider: FC<DndContextProviderProps> = ({ children, ...props }) => {
|
||||
const state = useDndContextState(props);
|
||||
return (
|
||||
<DndContext.Provider value={state}>
|
||||
{children}
|
||||
</DndContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useDndContext = () => {
|
||||
const context = useContext(DndContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useDndContext must be used within a DndContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
71
src/pages/CardsPage/contexts/PrefillCardContext.tsx
Normal file
71
src/pages/CardsPage/contexts/PrefillCardContext.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createContext, Dispatch, FC, SetStateAction, useContext, useState } from "react";
|
||||
import { CardSchema, CardService } from "../../../client";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
type PrefillCardContextState = {
|
||||
prefillOpened: boolean;
|
||||
prefillOnClose: () => void;
|
||||
prefillOnOpen: () => void;
|
||||
|
||||
selectedPrefillCard?: CardSchema;
|
||||
selectPrefillCard: (cardId: number) => void;
|
||||
prefillCard?: CardSchema;
|
||||
setPrefillCard: Dispatch<SetStateAction<CardSchema | undefined>>;
|
||||
};
|
||||
|
||||
const PrefillCardContext = createContext<PrefillCardContextState | undefined>(
|
||||
undefined
|
||||
);
|
||||
const usePrefillCardContextState = () => {
|
||||
const [selectedPrefillCard, setSelectedPrefillCard] = useState<CardSchema | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [prefillCard, setPrefillCard] = useState<CardSchema | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [prefillOpened, { open, close }] = useDisclosure(false);
|
||||
const prefillOnClose = close;
|
||||
const prefillOnOpen = open;
|
||||
|
||||
const selectPrefillCard = (cardId: number) => {
|
||||
CardService.getCardById({ cardId }).then(card => {
|
||||
setSelectedPrefillCard(card);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
prefillOpened,
|
||||
prefillOnClose,
|
||||
prefillOnOpen,
|
||||
|
||||
selectedPrefillCard,
|
||||
selectPrefillCard,
|
||||
prefillCard,
|
||||
setPrefillCard,
|
||||
};
|
||||
};
|
||||
|
||||
type PrefillCardContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const PrefillCardContextProvider: FC<PrefillCardContextProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const state = usePrefillCardContextState();
|
||||
return (
|
||||
<PrefillCardContext.Provider value={state}>
|
||||
{children}
|
||||
</PrefillCardContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePrefillCardContext = () => {
|
||||
const context = useContext(PrefillCardContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"usePrefillCardContext must be used within a PrefillCardContextProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,14 +1,14 @@
|
||||
import { createContext, FC, useContext, useState } from "react";
|
||||
import React, { createContext, FC, useContext, useState } from "react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { DealsWithExcelForm, ProductExcelData } from "../drawers/PrefillDealWithExcelDrawer/types.tsx";
|
||||
import { CardsWithExcelForm, ProductExcelData } from "../drawers/PrefillCardWithExcelDrawer/types.tsx";
|
||||
import { FileWithPath } from "@mantine/dropzone";
|
||||
import { notifications } from "../../../shared/lib/notifications.ts";
|
||||
import { DealService, type ProductFromExcelSchema, ProductSchema, StatusSchema } from "../../../client";
|
||||
import { CardService, type ProductFromExcelSchema, ProductSchema, StatusSchema } from "../../../client";
|
||||
import UseExcelDropzone from "../../../types/UseExcelDropzone.tsx";
|
||||
import { useForm, UseFormReturnType } from "@mantine/form";
|
||||
import { useDealPageContext } from "./DealPageContext.tsx";
|
||||
import { useCardPageContext } from "./CardPageContext.tsx";
|
||||
|
||||
type PrefillDealsWithExcelContextState = {
|
||||
type PrefillCardsWithExcelContextState = {
|
||||
prefillWithExcelOpened: boolean;
|
||||
prefillWithExcelOnClose: () => void;
|
||||
prefillWithExcelOnOpen: () => void;
|
||||
@@ -16,18 +16,18 @@ type PrefillDealsWithExcelContextState = {
|
||||
onProductSelectChange: (barcode: string, selectedProduct: ProductSchema) => void,
|
||||
onDrop: (files: FileWithPath[]) => void;
|
||||
excelDropzone: UseExcelDropzone;
|
||||
createDeals: (values: DealsWithExcelForm, status: StatusSchema) => void;
|
||||
form: UseFormReturnType<DealsWithExcelForm>;
|
||||
createCards: (values: CardsWithExcelForm, status: StatusSchema) => void;
|
||||
form: UseFormReturnType<CardsWithExcelForm>;
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
const PrefillDealsWithExcelContext = createContext<PrefillDealsWithExcelContextState | undefined>(
|
||||
const PrefillCardsWithExcelContext = createContext<PrefillCardsWithExcelContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const usePrefillDealsWithExcelContextState = () => {
|
||||
const usePrefillCardsWithExcelContextState = () => {
|
||||
const [prefillWithExcelOpened, { open, close }] = useDisclosure(false);
|
||||
const { refetchDeals } = useDealPageContext();
|
||||
const { refetchCards } = useCardPageContext();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const excelDropzone: UseExcelDropzone = {
|
||||
@@ -35,7 +35,7 @@ const usePrefillDealsWithExcelContextState = () => {
|
||||
setIsLoading,
|
||||
};
|
||||
|
||||
const form = useForm<DealsWithExcelForm>({
|
||||
const form = useForm<CardsWithExcelForm>({
|
||||
validate: {
|
||||
client: client => !client && "Выберите клиента",
|
||||
},
|
||||
@@ -51,7 +51,7 @@ const usePrefillDealsWithExcelContextState = () => {
|
||||
const file = files[0];
|
||||
|
||||
setIsLoading(true);
|
||||
DealService.parseDealsExcel({
|
||||
CardService.parseDealsExcel({
|
||||
formData: {
|
||||
upload_file: file,
|
||||
},
|
||||
@@ -91,7 +91,7 @@ const usePrefillDealsWithExcelContextState = () => {
|
||||
form.reset();
|
||||
};
|
||||
|
||||
const createDeals = (values: DealsWithExcelForm, status: StatusSchema) => {
|
||||
const createCards = (values: CardsWithExcelForm, status: StatusSchema) => {
|
||||
const products: ProductFromExcelSchema[] = barcodeProductsMap.entries().map(([, productData]) => {
|
||||
return {
|
||||
productId: productData.selectedProduct!.id,
|
||||
@@ -99,7 +99,7 @@ const usePrefillDealsWithExcelContextState = () => {
|
||||
};
|
||||
}).toArray();
|
||||
|
||||
DealService.createDealsExcel({
|
||||
CardService.createDealsExcel({
|
||||
requestBody: {
|
||||
products,
|
||||
clientId: values.client?.id ?? -1,
|
||||
@@ -109,7 +109,7 @@ const usePrefillDealsWithExcelContextState = () => {
|
||||
.then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (ok) prefillWithExcelOnClose();
|
||||
refetchDeals();
|
||||
refetchCards();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
@@ -122,30 +122,30 @@ const usePrefillDealsWithExcelContextState = () => {
|
||||
onProductSelectChange,
|
||||
onDrop,
|
||||
excelDropzone,
|
||||
createDeals,
|
||||
createCards,
|
||||
form,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
type PrefillDealsWithExcelContextProviderProps = {
|
||||
type PrefillCardsWithExcelContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const PrefillDealsWithExcelContextProvider: FC<PrefillDealsWithExcelContextProviderProps> = ({ children }) => {
|
||||
const state = usePrefillDealsWithExcelContextState();
|
||||
export const PrefillCardsWithExcelContextProvider: FC<PrefillCardsWithExcelContextProviderProps> = ({ children }) => {
|
||||
const state = usePrefillCardsWithExcelContextState();
|
||||
return (
|
||||
<PrefillDealsWithExcelContext.Provider value={state}>
|
||||
<PrefillCardsWithExcelContext.Provider value={state}>
|
||||
{children}
|
||||
</PrefillDealsWithExcelContext.Provider>
|
||||
</PrefillCardsWithExcelContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePrefillDealsWithExcelContext = () => {
|
||||
const context = useContext(PrefillDealsWithExcelContext);
|
||||
export const usePrefillCardsWithExcelContext = () => {
|
||||
const context = useContext(PrefillCardsWithExcelContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"usePrefillDealsWithExcelContext must be used within a PrefillDealsWithExcelContextProvider",
|
||||
"usePrefillCardsWithExcelContext must be used within a PrefillCardsWithExcelContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
46
src/pages/CardsPage/contexts/ProjectsEditorContext.tsx
Normal file
46
src/pages/CardsPage/contexts/ProjectsEditorContext.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { createContext, FC, useContext } from "react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
type ProjectsEditorContextState = {
|
||||
openedProjectsEditor: boolean;
|
||||
openProjectsEditor: () => void;
|
||||
closeProjectsEditor: () => void;
|
||||
};
|
||||
|
||||
const ProjectsEditorContext = createContext<ProjectsEditorContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const useProjectsEditorContextState = () => {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return {
|
||||
openedProjectsEditor: opened,
|
||||
openProjectsEditor: open,
|
||||
closeProjectsEditor: close,
|
||||
};
|
||||
};
|
||||
|
||||
type ProjectsEditorContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ProjectsEditorContextProvider: FC<ProjectsEditorContextProviderProps> = ({ children }) => {
|
||||
const state = useProjectsEditorContextState();
|
||||
|
||||
return (
|
||||
<ProjectsEditorContext.Provider value={state}>
|
||||
{children}
|
||||
</ProjectsEditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useProjectsEditorContext = () => {
|
||||
const context = useContext(ProjectsEditorContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useProjectEditorContext must be used within a ProjectEditorContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
130
src/pages/CardsPage/drawers/CardEditDrawer/CardEditDrawer.tsx
Normal file
130
src/pages/CardsPage/drawers/CardEditDrawer/CardEditDrawer.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Box, Drawer, rem, Tabs } from "@mantine/core";
|
||||
import { FC, ReactNode, useEffect } from "react";
|
||||
import { useCardPageContext } from "../../contexts/CardPageContext.tsx";
|
||||
import { IconCalendarUser, IconSettings } from "@tabler/icons-react";
|
||||
import CardStatusChangeTable from "./tabs/CardStatusChangeTable/CardStatusChangeTable.tsx";
|
||||
import GeneralTab from "./tabs/GeneralTab/GeneralTab.tsx";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { motion } from "framer-motion";
|
||||
import { useModulesContext } from "../../../../modules/context/ModulesContext.tsx";
|
||||
|
||||
const useCardStatusChangeState = () => {
|
||||
const { selectedCard } = useCardPageContext();
|
||||
return {
|
||||
statusHistory: selectedCard?.statusHistory || [],
|
||||
};
|
||||
};
|
||||
|
||||
const CardEditDrawerStatusChangeTable = () => {
|
||||
const { statusHistory } = useCardStatusChangeState();
|
||||
|
||||
return <CardStatusChangeTable items={statusHistory} />;
|
||||
};
|
||||
|
||||
const useCardEditDrawerState = () => {
|
||||
const { selectedCard, setSelectedCard } = useCardPageContext();
|
||||
return {
|
||||
isVisible: selectedCard !== undefined,
|
||||
onClose: () => setSelectedCard(undefined),
|
||||
};
|
||||
};
|
||||
|
||||
const CardEditDrawer: FC = () => {
|
||||
const { isVisible, onClose } = useCardEditDrawerState();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { modules } = useModulesContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) return;
|
||||
queryClient.invalidateQueries({ queryKey: ["getCardSummaries"] });
|
||||
}, [isVisible]);
|
||||
|
||||
const getTabPanel = (value: string, component: ReactNode) => {
|
||||
return (
|
||||
<Tabs.Panel key={value} value={value}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<Box
|
||||
h={"100%"}
|
||||
w={"100%"}
|
||||
p={rem(10)}>
|
||||
{component}
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const getTabs = () => {
|
||||
const moduleTabs = modules.map(module => (
|
||||
<Tabs.Tab
|
||||
key={module.info.key}
|
||||
value={module.info.key}
|
||||
leftSection={module.info.icon}
|
||||
>
|
||||
{module.info.label}
|
||||
</Tabs.Tab>
|
||||
));
|
||||
|
||||
return (
|
||||
<>{moduleTabs}</>
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanels = () => {
|
||||
const moduleTabPanels = modules.map(
|
||||
module => getTabPanel(module.info.key, module.tab),
|
||||
);
|
||||
return (
|
||||
<>{moduleTabPanels}</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
size={"calc(100vw - 150px)"}
|
||||
position={"right"}
|
||||
onClose={onClose}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
withCloseButton={false}
|
||||
opened={isVisible}
|
||||
styles={{
|
||||
body: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: rem(10),
|
||||
},
|
||||
}}>
|
||||
<Tabs
|
||||
defaultValue={"general"}
|
||||
flex={1}
|
||||
variant={"outline"}
|
||||
orientation={"vertical"}
|
||||
keepMounted={false}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab
|
||||
value={"general"}
|
||||
leftSection={<IconSettings />}>
|
||||
Общее
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"history"}
|
||||
leftSection={<IconCalendarUser />}>
|
||||
История
|
||||
</Tabs.Tab>
|
||||
{getTabs()}
|
||||
</Tabs.List>
|
||||
|
||||
{getTabPanel("general", <GeneralTab />)}
|
||||
{getTabPanel("history", <CardEditDrawerStatusChangeTable />)}
|
||||
{getTabPanels()}
|
||||
</Tabs>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardEditDrawer;
|
||||
@@ -1,13 +1,13 @@
|
||||
import { DealStatusHistorySchema } from "../../../../client";
|
||||
import { CardStatusHistorySchema } from "../../../../../../client";
|
||||
import { useDealStatusChangeTableColumns } from "./columns.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { BaseTable } from "../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
items: DealStatusHistorySchema[];
|
||||
items: CardStatusHistorySchema[];
|
||||
};
|
||||
|
||||
const DealStatusChangeTable: FC<Props> = (props: Props) => {
|
||||
const CardStatusChangeTable: FC<Props> = (props: Props) => {
|
||||
const { items } = props;
|
||||
|
||||
return (
|
||||
@@ -26,4 +26,4 @@ const DealStatusChangeTable: FC<Props> = (props: Props) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default DealStatusChangeTable;
|
||||
export default CardStatusChangeTable;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { DealStatusHistorySchema } from "../../../../client";
|
||||
import { CardStatusHistorySchema } from "../../../../../../client";
|
||||
import { Spoiler, Text } from "@mantine/core";
|
||||
|
||||
export const useDealStatusChangeTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<DealStatusHistorySchema>[]>(
|
||||
return useMemo<MRT_ColumnDef<CardStatusHistorySchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "changedAt",
|
||||
@@ -0,0 +1,274 @@
|
||||
import { FC, useState } from "react";
|
||||
import { useCardPageContext } from "../../../../contexts/CardPageContext.tsx";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Fieldset,
|
||||
Flex,
|
||||
Group,
|
||||
rem,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { CardSchema, CardService, ClientService, ProjectSchema, StatusSchema } from "../../../../../../client";
|
||||
import { isEqual } from "lodash";
|
||||
import { notifications } from "../../../../../../shared/lib/notifications.ts";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import ProjectSelect from "../../../../../../components/ProjectSelect/ProjectSelect.tsx";
|
||||
import BoardSelect from "../../../../../../components/BoardSelect/BoardSelect.tsx";
|
||||
import CardStatusSelect from "../../../../../../components/CardStatusSelect/CardStatusSelect.tsx";
|
||||
import CardAttributeFields from "../../../../../../components/CardAttributeFields/CardAttributeFields.tsx";
|
||||
import getAttributesFromCard from "../../../../../../components/CardAttributeFields/utils/getAttributesFromCard.ts";
|
||||
import CardTagsInput from "../../../../components/CardTagsInput/CardTagsInput.tsx";
|
||||
|
||||
type Props = {
|
||||
card: CardSchema;
|
||||
};
|
||||
|
||||
type Attributes = {
|
||||
[key: string]: number | boolean | string;
|
||||
};
|
||||
|
||||
export type CardGeneralFormType = Omit<CardSchema, "statusHistory" | "services" | "products">;
|
||||
|
||||
const Content: FC<Props> = ({ card }) => {
|
||||
const { setSelectedCard } = useCardPageContext();
|
||||
const clipboard = useClipboard();
|
||||
const queryClient = useQueryClient();
|
||||
const [project, setProject] = useState<ProjectSchema | null>(card.board.project);
|
||||
const [cardTags, setCardTags] = useState<string[]>(card.tags?.map(tag => tag.name) ?? []);
|
||||
|
||||
const getInitialValues = (card: CardSchema): CardGeneralFormType => {
|
||||
return {
|
||||
...card,
|
||||
...getAttributesFromCard(card),
|
||||
};
|
||||
};
|
||||
|
||||
let initialValues = getInitialValues(card);
|
||||
|
||||
const form = useForm<CardGeneralFormType>({
|
||||
initialValues,
|
||||
validate: {
|
||||
name: (value: string) =>
|
||||
value.length > 0
|
||||
? null
|
||||
: "Название не может быть пустым",
|
||||
status: (value: StatusSchema) =>
|
||||
!value && "Статус не выбран",
|
||||
},
|
||||
});
|
||||
|
||||
const updateCardInfo = async (values: CardGeneralFormType) => {
|
||||
const formCardAttrs = values as unknown as Attributes;
|
||||
|
||||
const attributes = project?.attributes.reduce((attrs, projectAttr) => {
|
||||
return {
|
||||
...attrs,
|
||||
[projectAttr.name]: formCardAttrs[projectAttr.name],
|
||||
};
|
||||
}, {});
|
||||
|
||||
return CardService.updateCardGeneralInfo({
|
||||
requestBody: {
|
||||
cardId: card.id,
|
||||
data: {
|
||||
...values,
|
||||
statusId: values.status.id,
|
||||
boardId: values.board.id,
|
||||
clientId: values.client?.id ?? null,
|
||||
attributes,
|
||||
tags: cardTags,
|
||||
},
|
||||
},
|
||||
}).then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
CardService.getCardById({ cardId: card.id }).then(data => {
|
||||
setSelectedCard(data);
|
||||
initialValues = getInitialValues(data);
|
||||
form.setValues(initialValues);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["getCardSummaries"],
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
const updateClientInfo = async (values: CardGeneralFormType) => {
|
||||
if (!values.client) return;
|
||||
return ClientService.updateClient({
|
||||
requestBody: {
|
||||
data: values.client,
|
||||
},
|
||||
}).then(({ ok, message }) => notifications.guess(ok, { message }));
|
||||
};
|
||||
const handleSubmit = async (values: CardGeneralFormType) => {
|
||||
// Updating client info if there changes
|
||||
if (!isEqual(values.client, card.client) && values.client?.id === card.clientId) {
|
||||
await updateClientInfo(values);
|
||||
}
|
||||
|
||||
await updateCardInfo(values);
|
||||
};
|
||||
|
||||
const onCopyGuestUrlClick = () => {
|
||||
CardService.createDealGuestUrl({
|
||||
requestBody: {
|
||||
cardId: card.id,
|
||||
},
|
||||
}).then(({ ok, message, url }) => {
|
||||
if (!ok) notifications.guess(ok, { message });
|
||||
clipboard.copy(`${window.location.origin}/${url}`);
|
||||
});
|
||||
};
|
||||
|
||||
const cancelChanges = () => {
|
||||
form.reset();
|
||||
setCardTags(card.tags?.map(tag => tag.name) ?? []);
|
||||
};
|
||||
|
||||
const isEqualValues = () => {
|
||||
const initialCardTagsSet = new Set(card.tags?.map(tag => tag.name) ?? []);
|
||||
|
||||
const tagsEqual = initialCardTagsSet.size === cardTags.length &&
|
||||
cardTags.every(element => initialCardTagsSet.has(element));
|
||||
|
||||
return isEqual(initialValues, form.values) && tagsEqual;
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => handleSubmit(values))}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
h={"95vh"}
|
||||
>
|
||||
<ScrollArea>
|
||||
<Stack>
|
||||
<Fieldset legend={`Общие параметры [ID: ${card.id}]`}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
<TextInput
|
||||
placeholder={"Название сделки"}
|
||||
label={"Название сделки"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
disabled
|
||||
placeholder={"Дата создания"}
|
||||
label={"Дата создания"}
|
||||
value={new Date(card.createdAt).toLocaleString(
|
||||
"ru-RU",
|
||||
)}
|
||||
/>
|
||||
<ProjectSelect
|
||||
value={project}
|
||||
onChange={setProject}
|
||||
label={"Проект"}
|
||||
disabled
|
||||
/>
|
||||
<BoardSelect
|
||||
project={project}
|
||||
{...form.getInputProps("board")}
|
||||
label={"Доска"}
|
||||
/>
|
||||
<CardStatusSelect
|
||||
board={form.values.board}
|
||||
{...form.getInputProps("status")}
|
||||
label={"Статус"}
|
||||
/>
|
||||
<Textarea
|
||||
h={rem(120)}
|
||||
styles={{
|
||||
wrapper: { height: "90%" },
|
||||
input: { height: "90%" },
|
||||
}}
|
||||
label={"Коментарий"}
|
||||
placeholder={"Введите коментарий"}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
{project && project?.tags.length > 0 && (
|
||||
<CardTagsInput
|
||||
value={cardTags}
|
||||
onChange={setCardTags}
|
||||
/>
|
||||
)}
|
||||
{project && (
|
||||
<CardAttributeFields
|
||||
project={project}
|
||||
form={form}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
<Flex
|
||||
mt={"md"}
|
||||
gap={rem(10)}
|
||||
align={"center"}
|
||||
justify={"flex-end"}>
|
||||
<Flex
|
||||
align={"center"}
|
||||
gap={rem(10)}
|
||||
justify={"center"}>
|
||||
<ButtonCopyControlled
|
||||
onCopyClick={onCopyGuestUrlClick}
|
||||
onCopiedLabel={
|
||||
"Ссылка скопирована в буфер обмена"
|
||||
}
|
||||
copied={clipboard.copied}
|
||||
>
|
||||
Ссылка на редактирование
|
||||
</ButtonCopyControlled>
|
||||
<Flex gap={rem(10)}>
|
||||
<Checkbox
|
||||
label={"Завершена"}
|
||||
{...form.getInputProps("isCompleted", { type: "checkbox" })}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Удалена"}
|
||||
{...form.getInputProps("isDeleted", { type: "checkbox" })}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Divider orientation={"vertical"} />
|
||||
<Group
|
||||
align={"center"}
|
||||
justify={"center"}>
|
||||
<Button
|
||||
color={"red"}
|
||||
type={"reset"}
|
||||
disabled={isEqualValues()}
|
||||
onClick={cancelChanges}>
|
||||
Отменить изменения
|
||||
</Button>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
disabled={isEqualValues()}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const GeneralTab: FC = () => {
|
||||
const { selectedCard } = useCardPageContext();
|
||||
if (!selectedCard) return <>No card selected</>;
|
||||
return (
|
||||
<Content card={selectedCard} />
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralTab;
|
||||
@@ -5,20 +5,20 @@
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.deal-container {
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: rem(10);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deal-container-wrapper {
|
||||
.card-container-wrapper {
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
}
|
||||
|
||||
.deal-container-buttons {
|
||||
.card-container-buttons {
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1,16 +1,16 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import { Button, Drawer, Flex, rem, TextInput } from "@mantine/core";
|
||||
import DealsTable from "./components/tables/DealsTable/DealsTable.tsx";
|
||||
import CardsTable from "./components/tables/CardsTable/CardsTable.tsx";
|
||||
import Preview from "./components/Preview/Preview.tsx";
|
||||
import styles from "./DealPrefillDrawer.module.css";
|
||||
import styles from "./CardPrefillDrawer.module.css";
|
||||
import BaseMarketplaceSelect from "../../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||
import usePrefillDeal from "./hooks/usePrefillDeal.tsx";
|
||||
import usePrefillCard from "./hooks/usePrefillCard.tsx";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { usePrefillDealContext } from "../../contexts/PrefillDealContext.tsx";
|
||||
import { usePrefillCardContext } from "../../contexts/PrefillCardContext.tsx";
|
||||
|
||||
const DealPrefillDrawer: FC = () => {
|
||||
const { prefillOpened, prefillOnClose, selectedPrefillDeal, setPrefillDeal, prefillDeal } = usePrefillDealContext();
|
||||
const { data, form } = usePrefillDeal();
|
||||
const CardPrefillDrawer: FC = () => {
|
||||
const { prefillOpened, prefillOnClose, selectedPrefillCard, setPrefillCard, prefillCard } = usePrefillCardContext();
|
||||
const { data, form } = usePrefillCard();
|
||||
|
||||
useEffect(() => {
|
||||
if (prefillOpened) return;
|
||||
@@ -33,8 +33,8 @@ const DealPrefillDrawer: FC = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className={styles["deal-container"]}>
|
||||
<div className={styles["deal-container-wrapper"]}>
|
||||
<div className={styles["card-container"]}>
|
||||
<div className={styles["card-container-wrapper"]}>
|
||||
<div className={styles["top-panel"]}>
|
||||
<TextInput
|
||||
placeholder={"Введите название / id"}
|
||||
@@ -49,22 +49,22 @@ const DealPrefillDrawer: FC = () => {
|
||||
{...form.getInputProps("marketplace")}
|
||||
/>
|
||||
</div>
|
||||
<DealsTable items={data} />
|
||||
<CardsTable items={data} />
|
||||
<Flex direction={"row"} gap="sm">
|
||||
<Button mt={10} w={"100%"} onClick={() => {
|
||||
if (!selectedPrefillDeal) {
|
||||
notifications.error({ message: "Сделка не выбрана." });
|
||||
if (!selectedPrefillCard) {
|
||||
notifications.error({ message: "Карточка не выбрана." });
|
||||
return;
|
||||
}
|
||||
setPrefillDeal(selectedPrefillDeal);
|
||||
setPrefillCard(selectedPrefillCard);
|
||||
prefillOnClose();
|
||||
}}>
|
||||
Предзаполнить
|
||||
</Button>
|
||||
{
|
||||
prefillDeal &&
|
||||
prefillCard &&
|
||||
<Button mt={10} w={"100%"} variant={"outline"} onClick={() => {
|
||||
setPrefillDeal(undefined);
|
||||
setPrefillCard(undefined);
|
||||
notifications.success({ message: "Предзаполнение отменено." });
|
||||
prefillOnClose();
|
||||
}}>
|
||||
@@ -79,4 +79,4 @@ const DealPrefillDrawer: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DealPrefillDrawer;
|
||||
export default CardPrefillDrawer;
|
||||
@@ -11,21 +11,21 @@
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.deal-container {
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: rem(10);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deal-container-wrapper {
|
||||
.card-container-wrapper {
|
||||
width: 100%;
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
}
|
||||
|
||||
.deal-container-buttons {
|
||||
.card-container-buttons {
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1,16 +1,16 @@
|
||||
import { FC } from "react";
|
||||
import styles from "./Preview.module.css";
|
||||
import { ScrollArea, Skeleton, Title } from "@mantine/core";
|
||||
import DealServicesTable from "../tables/DealServicesTable/DealServicesTable.tsx";
|
||||
import CardServicesTable from "../tables/CardServicesTable/CardServicesTable.tsx";
|
||||
import ProductPreview from "../ProductPreview/ProductPreview.tsx";
|
||||
import { usePrefillDealContext } from "../../../../contexts/PrefillDealContext.tsx";
|
||||
import { usePrefillCardContext } from "../../../../contexts/PrefillCardContext.tsx";
|
||||
|
||||
const Preview: FC = () => {
|
||||
const { selectedPrefillDeal } = usePrefillDealContext();
|
||||
const { selectedPrefillCard } = usePrefillCardContext();
|
||||
|
||||
const getTotalPrice = () => {
|
||||
if (!selectedPrefillDeal) return 0;
|
||||
const productServicesPrice = selectedPrefillDeal.products.reduce(
|
||||
if (!selectedPrefillCard) return 0;
|
||||
const productServicesPrice = selectedPrefillCard.products.reduce(
|
||||
(acc, row) =>
|
||||
acc +
|
||||
row.services.reduce(
|
||||
@@ -19,28 +19,28 @@ const Preview: FC = () => {
|
||||
),
|
||||
0,
|
||||
);
|
||||
const dealServicesPrice = selectedPrefillDeal.services.reduce(
|
||||
const cardServicesPrice = selectedPrefillCard.services.reduce(
|
||||
(acc, row) => acc + row.price * row.quantity,
|
||||
0,
|
||||
);
|
||||
return dealServicesPrice + productServicesPrice;
|
||||
return cardServicesPrice + productServicesPrice;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["container"]}>
|
||||
<div className={styles["deal-container-wrapper"]}>
|
||||
<div className={styles["card-container-wrapper"]}>
|
||||
<ScrollArea offsetScrollbars={"y"} w={"100%"}>
|
||||
<div style={{ height: "93vh" }}>
|
||||
<Skeleton visible={!selectedPrefillDeal}>
|
||||
<Skeleton visible={!selectedPrefillCard}>
|
||||
<Title order={4} mb={18}>
|
||||
Общая стоимость всех услуг:{" "}
|
||||
{getTotalPrice().toLocaleString("ru")}₽
|
||||
</Title>
|
||||
|
||||
<DealServicesTable items={selectedPrefillDeal?.services} />
|
||||
<CardServicesTable items={selectedPrefillCard?.services} />
|
||||
|
||||
<div className={styles["products-list"]}>
|
||||
{selectedPrefillDeal?.products.map(product => (
|
||||
{selectedPrefillCard?.products.map(product => (
|
||||
<ProductPreview
|
||||
key={product.product.id}
|
||||
product={product}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { FC } from "react";
|
||||
import { DealProductSchema, ProductSchema } from "../../../../../../client";
|
||||
import { CardProductSchema, ProductSchema } from "../../../../../../client";
|
||||
import { Image, rem, Text, Title } from "@mantine/core";
|
||||
import { isNil } from "lodash";
|
||||
import { ProductFieldNames } from "../../../../tabs/ProductAndServiceTab/components/ProductView/ProductView.tsx";
|
||||
import { ProductFieldNames } from "../../../../../../modules/cardModules/cardEditorTabs/ProductAndServiceTab/components/ProductView/ProductView.tsx";
|
||||
import ProductServicesTable from "../tables/ProductServicesTable/ProductServicesTable.tsx";
|
||||
import styles from "./ProductPreview.module.css";
|
||||
|
||||
type Props = {
|
||||
product: DealProductSchema;
|
||||
product: CardProductSchema;
|
||||
};
|
||||
|
||||
const ProductPreview: FC<Props> = ({ product }) => {
|
||||
@@ -1,16 +1,16 @@
|
||||
import { FC } from "react";
|
||||
import { Flex, rem, Title } from "@mantine/core";
|
||||
import { DealServiceSchema, DealSummary } from "../../../../../../../client";
|
||||
import useDealServicesTableColumns from "./columns.tsx";
|
||||
import { CardServiceSchema, CardSummary } from "../../../../../../../client";
|
||||
import useCardServicesTableColumns from "./columns.tsx";
|
||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
type Props = {
|
||||
items?: DealServiceSchema[];
|
||||
items?: CardServiceSchema[];
|
||||
};
|
||||
|
||||
const DealServicesTable: FC<Props> = ({ items }) => {
|
||||
const columns = useDealServicesTableColumns();
|
||||
const CardServicesTable: FC<Props> = ({ items }) => {
|
||||
const columns = useCardServicesTableColumns();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -33,7 +33,7 @@ const DealServicesTable: FC<Props> = ({ items }) => {
|
||||
enableColumnActions: false,
|
||||
enablePagination: false,
|
||||
enableBottomToolbar: false,
|
||||
} as MRT_TableOptions<DealSummary>
|
||||
} as MRT_TableOptions<CardSummary>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -55,4 +55,4 @@ const DealServicesTable: FC<Props> = ({ items }) => {
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
export default DealServicesTable;
|
||||
export default CardServicesTable;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { DealServiceSchema } from "../../../../../../../client";
|
||||
import { CardServiceSchema } from "../../../../../../../client";
|
||||
|
||||
const useDealServicesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<DealServiceSchema>[]>(
|
||||
const useCardServicesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<CardServiceSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Название",
|
||||
@@ -27,4 +27,4 @@ const useDealServicesTableColumns = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default useDealServicesTableColumns;
|
||||
export default useCardServicesTableColumns;
|
||||
@@ -1,21 +1,21 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import useDealsTableColumns from "./columns.tsx";
|
||||
import { DealSummary } from "../../../../../../../client";
|
||||
import useCardsTableColumns from "./columns.tsx";
|
||||
import { CardSummary } from "../../../../../../../client";
|
||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { usePrefillDealContext } from "../../../../../contexts/PrefillDealContext.tsx";
|
||||
import { usePrefillCardContext } from "../../../../../contexts/PrefillCardContext.tsx";
|
||||
|
||||
type Props = {
|
||||
items: DealSummary[];
|
||||
items: CardSummary[];
|
||||
};
|
||||
|
||||
const DealsTable: FC<Props> = ({ items }) => {
|
||||
const { selectPrefillDeal } = usePrefillDealContext();
|
||||
const columns = useDealsTableColumns();
|
||||
const CardsTable: FC<Props> = ({ items }) => {
|
||||
const { selectPrefillCard } = usePrefillCardContext();
|
||||
const columns = useCardsTableColumns();
|
||||
const defaultSorting = [{ id: "createdAt", desc: false }];
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length < 1) return;
|
||||
selectPrefillDeal(items[0].id);
|
||||
selectPrefillCard(items[0].id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -45,4 +45,4 @@ const DealsTable: FC<Props> = ({ items }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DealsTable;
|
||||
export default CardsTable;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { ActionIcon, Image, Radio } from "@mantine/core";
|
||||
import { DealSummary } from "../../../../../../../client";
|
||||
import { usePrefillDealContext } from "../../../../../contexts/PrefillDealContext.tsx";
|
||||
import { CardSummary } from "../../../../../../../client";
|
||||
import { usePrefillCardContext } from "../../../../../contexts/PrefillCardContext.tsx";
|
||||
|
||||
const useDealsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<DealSummary>[]>(
|
||||
const useCardsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<CardSummary>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "select",
|
||||
@@ -13,13 +13,13 @@ const useDealsTableColumns = () => {
|
||||
size: 5,
|
||||
enableSorting: false,
|
||||
Cell: ({ row }) => {
|
||||
const { selectPrefillDeal, selectedPrefillDeal } = usePrefillDealContext();
|
||||
const checked = row.original.id === selectedPrefillDeal?.id;
|
||||
const { selectPrefillCard, selectedPrefillCard } = usePrefillCardContext();
|
||||
const checked = row.original.id === selectedPrefillCard?.id;
|
||||
return (
|
||||
<Radio
|
||||
checked={checked}
|
||||
onChange={() => {
|
||||
selectPrefillDeal(row.original.id);
|
||||
selectPrefillCard(row.original.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -69,4 +69,4 @@ const useDealsTableColumns = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default useDealsTableColumns;
|
||||
export default useCardsTableColumns;
|
||||
@@ -1,12 +1,12 @@
|
||||
import { FC } from "react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { DealProductServiceSchema } from "../../../../../../../client";
|
||||
import { CardProductServiceSchema } from "../../../../../../../client";
|
||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useProductServicesTableColumns from "./columns.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
items: DealProductServiceSchema[];
|
||||
items: CardProductServiceSchema[];
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ const ProductServicesTable: FC<Props> = ({ items, quantity }) => {
|
||||
enableSorting: false,
|
||||
enableRowActions: false,
|
||||
enableBottomToolbar: false,
|
||||
} as MRT_TableOptions<DealProductServiceSchema>
|
||||
} as MRT_TableOptions<CardProductServiceSchema>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { DealProductServiceSchema } from "../../../../../../../client";
|
||||
import { CardProductServiceSchema } from "../../../../../../../client";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../../../../redux/store.ts";
|
||||
|
||||
type Props = {
|
||||
data: DealProductServiceSchema[];
|
||||
data: CardProductServiceSchema[];
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ const useProductServicesTableColumns = (props: Props) => {
|
||||
[data, quantity]
|
||||
);
|
||||
const hideGuestColumns = ["service.cost"];
|
||||
return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(
|
||||
return useMemo<MRT_ColumnDef<CardProductServiceSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "service.name",
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BaseMarketplaceSchema } from "../../../../../client";
|
||||
import { useDealSummariesFull } from "../../../hooks/useDealSummaries.tsx";
|
||||
import { useCardSummariesFull } from "../../../hooks/useCardSummaries.tsx";
|
||||
import isModuleInProject, { Modules } from "../../../../../modules/utils/isModuleInProject.ts";
|
||||
|
||||
type State = {
|
||||
idOrName: string | null;
|
||||
marketplace: BaseMarketplaceSchema | null;
|
||||
};
|
||||
|
||||
const usePrefillDeal = () => {
|
||||
const { objects } = useDealSummariesFull();
|
||||
const usePrefillCard = () => {
|
||||
const { objects } = useCardSummariesFull();
|
||||
const form = useForm<State>({
|
||||
initialValues: {
|
||||
idOrName: null,
|
||||
@@ -20,14 +21,16 @@ const usePrefillDeal = () => {
|
||||
|
||||
const applyFilters = () => {
|
||||
let result = objects;
|
||||
|
||||
result = result.filter(obj => isModuleInProject(Modules.SERVICES_AND_PRODUCTS, obj.board.project));
|
||||
|
||||
if (form.values.idOrName) {
|
||||
if (isNaN(parseInt(form.values.idOrName))) {
|
||||
const name: string = form.values.idOrName.toLowerCase();
|
||||
result = result.filter(
|
||||
obj => obj.name.toLowerCase().search(name) !== -1,
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const id = parseInt(form.values.idOrName);
|
||||
result = result.filter(
|
||||
obj => obj.id === id,
|
||||
@@ -49,4 +52,4 @@ const usePrefillDeal = () => {
|
||||
return { data, form };
|
||||
};
|
||||
|
||||
export default usePrefillDeal;
|
||||
export default usePrefillCard;
|
||||
@@ -5,20 +5,20 @@
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.deal-container {
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: rem(10);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deal-container-wrapper {
|
||||
.card-container-wrapper {
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
}
|
||||
|
||||
.deal-container-buttons {
|
||||
.card-container-buttons {
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Drawer, rem } from "@mantine/core";
|
||||
import ExcelDropzone from "../../../../components/ExcelDropzone/ExcelDropzone.tsx";
|
||||
import styles from "../PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.module.css";
|
||||
import { usePrefillDealsWithExcelContext } from "../../contexts/PrefillDealsWithExcelContext.tsx";
|
||||
import styles from "./PrefillCardsWithExcelDrawer.module.css";
|
||||
import { usePrefillCardsWithExcelContext } from "../../contexts/PrefillDealsWithExcelContext.tsx";
|
||||
import ProductsPreview from "./components/ProductsPreview.tsx";
|
||||
import { BoardSchema } from "../../../../client";
|
||||
|
||||
@@ -9,22 +9,22 @@ type Props = {
|
||||
board: BoardSchema | null;
|
||||
}
|
||||
|
||||
const PrefillDealsWithExcelDrawer = ({ board }: Props) => {
|
||||
const PrefillCardsWithExcelDrawer = ({ board }: Props) => {
|
||||
const {
|
||||
prefillWithExcelOpened,
|
||||
prefillWithExcelOnClose,
|
||||
barcodeProductsMap,
|
||||
onDrop,
|
||||
excelDropzone,
|
||||
} = usePrefillDealsWithExcelContext();
|
||||
} = usePrefillCardsWithExcelContext();
|
||||
|
||||
const getBody = () => {
|
||||
if (!board || board.dealStatuses.length === 0) return;
|
||||
if (!board || board.statuses.length === 0) return;
|
||||
|
||||
if (barcodeProductsMap?.size === 0) {
|
||||
return <ExcelDropzone dropzone={excelDropzone} onDrop={onDrop} />;
|
||||
}
|
||||
return <ProductsPreview status={board.dealStatuses[0]}/>;
|
||||
return <ProductsPreview status={board.statuses[0]}/>;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -44,11 +44,11 @@ const PrefillDealsWithExcelDrawer = ({ board }: Props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className={styles["deal-container"]}>
|
||||
<div className={styles["card-container"]}>
|
||||
{getBody()}
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrefillDealsWithExcelDrawer;
|
||||
export default PrefillCardsWithExcelDrawer;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { usePrefillDealsWithExcelContext } from "../../../contexts/PrefillDealsWithExcelContext.tsx";
|
||||
import { usePrefillCardsWithExcelContext } from "../../../contexts/PrefillDealsWithExcelContext.tsx";
|
||||
import { Text, Tooltip } from "@mantine/core";
|
||||
import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react";
|
||||
|
||||
const ParsingResultsTooltip = () => {
|
||||
const { errors } = usePrefillDealsWithExcelContext();
|
||||
const { errors } = usePrefillCardsWithExcelContext();
|
||||
const isError = errors.length !== 0;
|
||||
|
||||
const errorLines = errors.map((error, i) => <Text key={i}>{error}</Text>);
|
||||
@@ -1,6 +1,6 @@
|
||||
import styles from "../PrefillDealsWithExcelDrawer.module.css";
|
||||
import styles from "../PrefillCardsWithExcelDrawer.module.css";
|
||||
import ProductsTable from "./ProductsTable.tsx";
|
||||
import { usePrefillDealsWithExcelContext } from "../../../contexts/PrefillDealsWithExcelContext.tsx";
|
||||
import { usePrefillCardsWithExcelContext } from "../../../contexts/PrefillDealsWithExcelContext.tsx";
|
||||
import { Box, Button, Flex, Group, Stack, Title } from "@mantine/core";
|
||||
import { ProductExcelData } from "../types.tsx";
|
||||
import BreakdownByCityTable from "./BreakdownByCityTable.tsx";
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const ProductsPreview = ({ status }: Props) => {
|
||||
const { barcodeProductsMap, createDeals, form } = usePrefillDealsWithExcelContext();
|
||||
const { barcodeProductsMap, createCards, form } = usePrefillCardsWithExcelContext();
|
||||
|
||||
const getTitle = (barcode: string, productsData: ProductExcelData) => {
|
||||
if (productsData.products.length === 1) {
|
||||
@@ -24,7 +24,7 @@ const ProductsPreview = ({ status }: Props) => {
|
||||
|
||||
const getProductsData = () => {
|
||||
return barcodeProductsMap.entries().map(([barcode, productsData]) => (
|
||||
<div key={barcode} className={styles["deal-container-wrapper"]}>
|
||||
<div key={barcode} className={styles["card-container-wrapper"]}>
|
||||
<Stack>
|
||||
<Title order={5}>
|
||||
{getTitle(barcode, productsData)}
|
||||
@@ -46,7 +46,7 @@ const ProductsPreview = ({ status }: Props) => {
|
||||
return (
|
||||
<Stack gap={"md"}>
|
||||
<Title order={3}>Предпросмотр</Title>
|
||||
<form onSubmit={form.onSubmit((values) => createDeals(values, status))}>
|
||||
<form onSubmit={form.onSubmit((values) => createCards(values, status))}>
|
||||
<ClientSelect
|
||||
{...form.getInputProps("client")}
|
||||
inputContainer={(children) => (
|
||||
@@ -57,7 +57,7 @@ const ProductsPreview = ({ status }: Props) => {
|
||||
variant="outline"
|
||||
type="submit"
|
||||
>
|
||||
Создать сделки
|
||||
Создать
|
||||
</Button>
|
||||
<ParsingResultsTooltip />
|
||||
</Group>
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { ProductSchema } from "../../../../../client";
|
||||
import { Radio } from "@mantine/core";
|
||||
import { usePrefillDealsWithExcelContext } from "../../../contexts/PrefillDealsWithExcelContext.tsx";
|
||||
import { usePrefillCardsWithExcelContext } from "../../../contexts/PrefillDealsWithExcelContext.tsx";
|
||||
import { ProductExcelData } from "../types.tsx";
|
||||
|
||||
type Props = {
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
}
|
||||
|
||||
export const useProductsTableColumns = ({ barcode }: Props) => {
|
||||
const { onProductSelectChange, barcodeProductsMap } = usePrefillDealsWithExcelContext();
|
||||
const { onProductSelectChange, barcodeProductsMap } = usePrefillCardsWithExcelContext();
|
||||
const [productData, setProductData] = useState<ProductExcelData>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -6,6 +6,6 @@ export type ProductExcelData = {
|
||||
selectedProduct?: ProductSchema;
|
||||
}
|
||||
|
||||
export type DealsWithExcelForm = {
|
||||
export type CardsWithExcelForm = {
|
||||
client?: ClientSchema;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.container-wrapper {
|
||||
flex: 1;
|
||||
height: 95vh;
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Box, Drawer, rem, Tabs } from "@mantine/core";
|
||||
import { IconHexagons, IconSettings, IconSubtask, IconTags } from "@tabler/icons-react";
|
||||
import { ReactNode } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useProjectsEditorContext } from "../../contexts/ProjectsEditorContext.tsx";
|
||||
import General from "./tabs/General/General.tsx";
|
||||
import Attributes from "./tabs/Attributes/Attributes.tsx";
|
||||
import Modules from "./tabs/Modules/Modules.tsx";
|
||||
import Tags from "./tabs/Tags/Tags.tsx";
|
||||
|
||||
|
||||
const ProjectEditDrawer = () => {
|
||||
const { closeProjectsEditor, openedProjectsEditor } = useProjectsEditorContext();
|
||||
|
||||
const getTabPanel = (value: string, component: ReactNode) => {
|
||||
return (
|
||||
<Tabs.Panel value={value}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<Box
|
||||
h={"100%"}
|
||||
w={"100%"}
|
||||
p={rem(10)}>
|
||||
{component}
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
size={"calc(100vw - 150px)"}
|
||||
position={"right"}
|
||||
onClose={closeProjectsEditor}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
withCloseButton={false}
|
||||
opened={openedProjectsEditor}
|
||||
styles={{
|
||||
body: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: rem(10),
|
||||
},
|
||||
}}>
|
||||
<Tabs
|
||||
defaultValue={"general"}
|
||||
flex={1}
|
||||
variant={"outline"}
|
||||
orientation={"vertical"}
|
||||
keepMounted={false}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab
|
||||
value={"general"}
|
||||
leftSection={<IconSettings />}>
|
||||
Общее
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"modules"}
|
||||
leftSection={<IconHexagons />}>
|
||||
Модули
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"attributes"}
|
||||
leftSection={<IconSubtask />}>
|
||||
Атрибуты
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"tags"}
|
||||
leftSection={<IconTags />}>
|
||||
Теги
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
{getTabPanel("general", <General />)}
|
||||
{getTabPanel("attributes", <Attributes />)}
|
||||
{getTabPanel("modules", <Modules />)}
|
||||
{getTabPanel("tags", <Tags />)}
|
||||
</Tabs>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectEditDrawer;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { AttributeSchema } from "../../../../../../../client";
|
||||
import { Box, Center, Checkbox } from "@mantine/core";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
import { formatDate, formatDateTime } from "../../../../../../../types/utils.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
selectedAttributes: Set<number>;
|
||||
}
|
||||
|
||||
const useAttributesTableColumns = ({ selectedAttributes }: Props) => {
|
||||
return useMemo<MRT_ColumnDef<AttributeSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: " ",
|
||||
Cell: ({ row }) => (
|
||||
<Center>
|
||||
<Checkbox
|
||||
checked={selectedAttributes.has(row.original.id)}
|
||||
onChange={() => {
|
||||
if (selectedAttributes.has(row.original.id)) {
|
||||
selectedAttributes.delete(row.original.id);
|
||||
} else {
|
||||
selectedAttributes.add(row.original.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
),
|
||||
size: 5,
|
||||
},
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "label",
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
header: "Тип",
|
||||
accessorKey: "type.name",
|
||||
size: 120,
|
||||
},
|
||||
// {
|
||||
// header: " ",
|
||||
// Cell: ({ row }) => {
|
||||
// const description = row.original.description ? `Описание: ${row.original.description}` : "";
|
||||
// const info = (
|
||||
// <Box>
|
||||
// <Text>Может быть пустым: {row.original.isNullable ? "да" : "нет"}</Text>
|
||||
// <Text>{defaultValueToStr(row.original.defaultValue, row.original.type.type)}</Text>
|
||||
// <Text>Синхронизировано в группе: {row.original.isApplicableToGroup ? "да" : "нет"}</Text>
|
||||
// <Text>{description}</Text>
|
||||
// </Box>
|
||||
// );
|
||||
// return (
|
||||
// <Tooltip label={info} multiline w={rem(300)}>
|
||||
// <IconInfoCircle />
|
||||
// </Tooltip>
|
||||
// );
|
||||
// },
|
||||
// size: 5,
|
||||
// },
|
||||
{
|
||||
header: "Значение по умолчанию",
|
||||
accessorKey: "defaultValue",
|
||||
Cell: ({ cell, row }) => {
|
||||
const value = cell.getValue();
|
||||
if (value === null) return <>-</>;
|
||||
|
||||
const type = row.original.type.type;
|
||||
if (type === "datetime") {
|
||||
return formatDateTime(value as string);
|
||||
}
|
||||
if (type === "date") {
|
||||
return formatDate(value as string);
|
||||
}
|
||||
if (type === "bool") {
|
||||
return value ? <IconCheck /> : <IconX />;
|
||||
}
|
||||
|
||||
return <>{value}</>;
|
||||
},
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
header: "Синхронизировано в группе",
|
||||
accessorKey: "isApplicableToGroup",
|
||||
Cell: ({ cell }) => cell.getValue() ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconX />
|
||||
),
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: "Может быть пустым",
|
||||
accessorKey: "isNullable",
|
||||
Cell: ({ cell }) => cell.getValue() ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconX />
|
||||
),
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: "Описаниие",
|
||||
accessorKey: "description",
|
||||
Cell: ({ row }) => <Box>{row.original.description}</Box>,
|
||||
size: 250,
|
||||
},
|
||||
],
|
||||
[selectedAttributes],
|
||||
);
|
||||
};
|
||||
|
||||
export default useAttributesTableColumns;
|
||||
@@ -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;
|
||||
@@ -1,16 +1,28 @@
|
||||
import { useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { BaseProjectSchema, ProjectSchema } from "../../../../../client";
|
||||
import { TextInput } from "@mantine/core";
|
||||
import { BaseProjectSchema, ProjectSchema } from "../../../../../../../client";
|
||||
import { Radio, TextInput } from "@mantine/core";
|
||||
|
||||
|
||||
type Props = {
|
||||
editingProjects: Map<number, ProjectSchema | BaseProjectSchema>;
|
||||
selectedProject: ProjectSchema | null;
|
||||
setSelectedProject: React.Dispatch<React.SetStateAction<ProjectSchema | null>>;
|
||||
}
|
||||
|
||||
const useProjectsTableColumns = ({ editingProjects }: Props) => {
|
||||
const useProjectsTableColumns = ({ editingProjects, selectedProject, setSelectedProject }: Props) => {
|
||||
return useMemo<MRT_ColumnDef<ProjectSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: " ",
|
||||
Cell: ({ row }) => (
|
||||
<Radio
|
||||
checked={row.original.id === selectedProject?.id}
|
||||
onChange={() => setSelectedProject(row.original)}
|
||||
/>
|
||||
),
|
||||
size: 10,
|
||||
},
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "name",
|
||||
@@ -39,7 +51,7 @@ const useProjectsTableColumns = ({ editingProjects }: Props) => {
|
||||
size: 10,
|
||||
},
|
||||
],
|
||||
[],
|
||||
[selectedProject],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Stack, TextInput } from "@mantine/core";
|
||||
import { BaseProjectSchema, ProjectService } from "../../../../../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
refetchProjects: () => void;
|
||||
};
|
||||
|
||||
const CreateProjectModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const closeModal = () => context.closeContextModal(id);
|
||||
|
||||
const form = useForm<BaseProjectSchema>({
|
||||
initialValues: { name: "" },
|
||||
validate: {
|
||||
name: name => !name && "Название не заполнено",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (project: BaseProjectSchema) => {
|
||||
ProjectService.createProject({
|
||||
requestBody: { project },
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
innerProps.refetchProjects();
|
||||
closeModal();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<Stack gap={"md"}>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateProjectModal;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { ModuleSchema } from "../../../../../../../client";
|
||||
import { Center, Checkbox } from "@mantine/core";
|
||||
|
||||
|
||||
type Props = {
|
||||
selectedModules: Set<number>;
|
||||
}
|
||||
|
||||
const useModulesTableColumns = ({ selectedModules }: Props) => {
|
||||
return useMemo<MRT_ColumnDef<ModuleSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: " ",
|
||||
Cell: ({ row }) => (
|
||||
<Center>
|
||||
<Checkbox
|
||||
checked={selectedModules.has(row.original.id)}
|
||||
onChange={() => {
|
||||
if (selectedModules.has(row.original.id)) {
|
||||
selectedModules.delete(row.original.id);
|
||||
} else {
|
||||
selectedModules.add(row.original.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
),
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "label",
|
||||
size: 10000,
|
||||
},
|
||||
],
|
||||
[selectedModules],
|
||||
);
|
||||
};
|
||||
|
||||
export default useModulesTableColumns;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ProjectService } from "../../../../../../../client";
|
||||
import ObjectList from "../../../../../../../hooks/objectList.tsx";
|
||||
|
||||
const useModulesList = () =>
|
||||
ObjectList({
|
||||
queryFn: ProjectService.getAllModules,
|
||||
getObjectsFn: response => response.modules,
|
||||
queryKey: "getAllModules",
|
||||
});
|
||||
|
||||
export default useModulesList;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ActionIcon, Flex, Group, rem, Stack, Tooltip } from "@mantine/core";
|
||||
import { BaseTable } from "../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import tagsTableColumns from "./hooks/tagsTableColumns.tsx";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { CardTagSchema } from "../../../../../../client";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import useTags from "./hooks/useTags.tsx";
|
||||
import InlineButton from "../../../../../../components/InlineButton/InlineButton.tsx";
|
||||
|
||||
|
||||
const Tags = () => {
|
||||
const columns = tagsTableColumns();
|
||||
|
||||
const {
|
||||
project,
|
||||
onDeleteClick,
|
||||
onChangeClick,
|
||||
onCreateClick,
|
||||
} = useTags();
|
||||
|
||||
return (
|
||||
<Stack gap={rem(10)}>
|
||||
<Group>
|
||||
<InlineButton onClick={onCreateClick}>
|
||||
<IconPlus />
|
||||
Создать
|
||||
</InlineButton>
|
||||
</Group>
|
||||
<BaseTable
|
||||
data={project?.tags}
|
||||
columns={columns}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
|
||||
renderRowActions: ({ row }) => {
|
||||
return (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onChangeClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
} as MRT_TableOptions<CardTagSchema>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { CardTagSchema } from "../../../../../../../client";
|
||||
|
||||
const useTagsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<CardTagSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "name",
|
||||
size: 1000,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
export default useTagsTableColumns;
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
CancelablePromise,
|
||||
CardTagSchema,
|
||||
CardTagService,
|
||||
CreateTagResponse,
|
||||
DeleteTagResponse,
|
||||
UpdateTagResponse,
|
||||
} from "../../../../../../../client";
|
||||
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { Text } from "@mantine/core";
|
||||
import { useProjectsContext } from "../../../../../../../contexts/ProjectsContext.tsx";
|
||||
import { useCardPageContext } from "../../../../../contexts/CardPageContext.tsx";
|
||||
|
||||
|
||||
const useTags = () => {
|
||||
const { selectedProject: project, refetchProjects } = useProjectsContext();
|
||||
const { refetchCards } = useCardPageContext();
|
||||
|
||||
const processResponse = (
|
||||
response: CancelablePromise<DeleteTagResponse | UpdateTagResponse | CreateTagResponse>,
|
||||
) => {
|
||||
response
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
refetchProjects();
|
||||
refetchCards();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onDelete = (tag: CardTagSchema) => {
|
||||
const response = CardTagService.deleteTag({
|
||||
cardTagId: tag.id,
|
||||
});
|
||||
|
||||
processResponse(response);
|
||||
};
|
||||
|
||||
const onDeleteClick = (tag: CardTagSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление тега",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить тег {tag.name}
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(tag),
|
||||
});
|
||||
};
|
||||
|
||||
const onChange = (tag: CardTagSchema) => {
|
||||
const response = CardTagService.updateTag({
|
||||
requestBody: { tag },
|
||||
});
|
||||
|
||||
processResponse(response);
|
||||
};
|
||||
|
||||
const onChangeClick = (tag: CardTagSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "cardTagModal",
|
||||
innerProps: {
|
||||
element: tag,
|
||||
onChange,
|
||||
},
|
||||
withCloseButton: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onCreate = (tag: CardTagSchema) => {
|
||||
if (!project) return;
|
||||
|
||||
const response = CardTagService.createTag({
|
||||
requestBody: {
|
||||
tag: {
|
||||
name: tag.name,
|
||||
projectId: project.id,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
processResponse(response);
|
||||
};
|
||||
|
||||
const onCreateClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "cardTagModal",
|
||||
innerProps: {
|
||||
onCreate,
|
||||
},
|
||||
withCloseButton: false,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
project,
|
||||
onDeleteClick,
|
||||
onChangeClick,
|
||||
onCreateClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTags;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../../../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { CardTagSchema } from "../../../../../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { TextInput } from "@mantine/core";
|
||||
|
||||
type Props = CreateEditFormProps<CardTagSchema>;
|
||||
|
||||
const CardTagModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValue: Partial<CardTagSchema> = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const form = useForm<Partial<CardTagSchema>>({
|
||||
initialValues: initialValue,
|
||||
validate: {
|
||||
name: name => !name && "Необходимо указать название тега",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
placeholder={"Введите название тега"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardTagModal;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { formatDate, formatDateTime } from "../../../../../types/utils.ts";
|
||||
|
||||
const defaultValueToStr = (defaultValue: string | null | number | boolean, type: string): string => {
|
||||
if (defaultValue === null) {
|
||||
return "Значения по умолчанию нет";
|
||||
}
|
||||
let strValue;
|
||||
const prefix = "Значение по умолчанию: ";
|
||||
|
||||
if (type === "bool") {
|
||||
strValue = defaultValue ? "true" : "false";
|
||||
} else if (type === "datetime") {
|
||||
strValue = formatDateTime(defaultValue as string);
|
||||
} else if (type === "date") {
|
||||
strValue = formatDate(defaultValue as string);
|
||||
} else {
|
||||
strValue = String(defaultValue);
|
||||
}
|
||||
|
||||
return prefix + strValue;
|
||||
};
|
||||
|
||||
export default defaultValueToStr;
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
const eqSet = (firstSet: Set<number>, secondSet: Set<number>) =>
|
||||
firstSet.size === secondSet.size &&
|
||||
[...firstSet].every((x) => secondSet.has(x));
|
||||
|
||||
export default eqSet;
|
||||
@@ -0,0 +1,25 @@
|
||||
const translitMap: Record<string, string> = {
|
||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
|
||||
'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
|
||||
'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
|
||||
'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'shch', 'ъ': '',
|
||||
'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya',
|
||||
};
|
||||
|
||||
const transliterate = (text: string): string => {
|
||||
return text.toLowerCase().split('').map(char => translitMap[char] || char).join('');
|
||||
};
|
||||
|
||||
const toSnakeCase = (text: string): string => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '_') // Replace spaces with underscores
|
||||
.replace(/[^a-z_0-9]/g, '') // Remove non-English letters and special chars
|
||||
.replace(/__+/g, '_') // Replace multiple underscores with single one
|
||||
.replace(/^_+|_+$/g, ''); // Trim leading/trailing underscores
|
||||
};
|
||||
|
||||
export const convertRussianToSnakeCase = (input: string): string => {
|
||||
const transliterated = transliterate(input);
|
||||
return toSnakeCase(transliterated);
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
enum DragState {
|
||||
DRAG_ENDED,
|
||||
DRAG_DEAL,
|
||||
DRAG_CARD,
|
||||
DRAG_STATUS,
|
||||
DRAG_BOARD,
|
||||
}
|
||||
|
||||
export default DragState;
|
||||
@@ -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,
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { DealService } from "../../../client";
|
||||
import { CardService } from "../../../client";
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
|
||||
export const useDealSummaries = () => {
|
||||
export const useCardSummaries = () => {
|
||||
const { data: summariesRaw = [], refetch } = useQuery({
|
||||
queryKey: ["getDealSummaries"],
|
||||
queryFn: () => DealService.getDealSummaries({ full: false }),
|
||||
queryKey: ["getCardSummaries"],
|
||||
queryFn: () => CardService.getCardSummaries({ full: false }),
|
||||
select: data => data.summaries || [], // Трансформируем полученные данные
|
||||
});
|
||||
|
||||
@@ -14,9 +14,9 @@ export const useDealSummaries = () => {
|
||||
|
||||
return { summariesRaw, refetch };
|
||||
};
|
||||
export const useDealSummariesFull = () =>
|
||||
export const useCardSummariesFull = () =>
|
||||
ObjectList({
|
||||
queryFn: () => DealService.getDealSummaries({ full: true }),
|
||||
queryKey: "getDealSummariesFull",
|
||||
queryFn: () => CardService.getCardSummaries({ full: true }),
|
||||
queryKey: "getCardSummariesFull",
|
||||
getObjectsFn: response => response.summaries,
|
||||
});
|
||||
@@ -1,37 +1,30 @@
|
||||
import { useDealSummariesFull } from "./useDealSummaries.tsx";
|
||||
import { useCardSummariesFull } from "./useCardSummaries.tsx";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BaseMarketplaceSchema, BoardSchema, ClientSchema, ProjectSchema, StatusSchema } from "../../../client";
|
||||
|
||||
|
||||
type Props = {
|
||||
projects: ProjectSchema[];
|
||||
}
|
||||
|
||||
export type DealsPageState = {
|
||||
export type CardsPageState = {
|
||||
id: number | null;
|
||||
marketplace: BaseMarketplaceSchema | null;
|
||||
client: ClientSchema | null;
|
||||
project: ProjectSchema | null;
|
||||
|
||||
projectForTable: ProjectSchema | null;
|
||||
board: BoardSchema | null;
|
||||
dealStatus: StatusSchema | null;
|
||||
status: StatusSchema | null;
|
||||
};
|
||||
|
||||
const useDealsPageState = ({ projects }: Props) => {
|
||||
const { objects } = useDealSummariesFull();
|
||||
const useCardsPageState = () => {
|
||||
const { objects } = useCardSummariesFull();
|
||||
|
||||
const form = useForm<DealsPageState>({
|
||||
const form = useForm<CardsPageState>({
|
||||
initialValues: {
|
||||
project: null,
|
||||
id: null,
|
||||
marketplace: null,
|
||||
client: null,
|
||||
|
||||
projectForTable: null,
|
||||
board: null,
|
||||
dealStatus: null,
|
||||
status: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -51,7 +44,7 @@ const useDealsPageState = ({ 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) {
|
||||
@@ -59,9 +52,9 @@ const useDealsPageState = ({ projects }: Props) => {
|
||||
obj => obj.board.id === form.values.board?.id,
|
||||
);
|
||||
|
||||
if (form.values.dealStatus) {
|
||||
if (form.values.status) {
|
||||
result = result.filter(
|
||||
obj => obj.status.id === form.values.dealStatus?.id,
|
||||
obj => obj.status.id === form.values.status?.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -78,13 +71,7 @@ const useDealsPageState = ({ projects }: Props) => {
|
||||
applyFilters();
|
||||
}, [form.values, objects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projects.length > 0 && form.values.project === null) {
|
||||
form.setFieldValue("project", projects[0]);
|
||||
}
|
||||
}, [projects]);
|
||||
|
||||
return { data, form };
|
||||
};
|
||||
|
||||
export default useDealsPageState;
|
||||
export default useCardsPageState;
|
||||
1
src/pages/CardsPage/index.ts
Normal file
1
src/pages/CardsPage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CardsPage } from "./ui/CardsPage.tsx";
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ProjectSchema } from "../../../client";
|
||||
import { Flex, Modal, NumberInput, rem } from "@mantine/core";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { DealsPageState } from "../hooks/useDealsPageState.tsx";
|
||||
import { CardsPageState } from "../hooks/useCardsPageState.tsx";
|
||||
import ObjectSelect from "../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import DealStatusSelect from "../../../components/DealStatusSelect/DealStatusSelect.tsx";
|
||||
import CardStatusSelect from "../../../components/CardStatusSelect/CardStatusSelect.tsx";
|
||||
import BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
@@ -12,11 +12,11 @@ import { IconFilter } from "@tabler/icons-react";
|
||||
import BoardSelect from "../../../components/BoardSelect/BoardSelect.tsx";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturnType<DealsPageState>;
|
||||
form: UseFormReturnType<CardsPageState>;
|
||||
projects: ProjectSchema[];
|
||||
};
|
||||
|
||||
const DealsTableFiltersModal = ({ form, projects }: Props) => {
|
||||
const CardsTableFiltersModal = ({ form, projects }: Props) => {
|
||||
const [opened, { open, close }] = useDisclosure();
|
||||
|
||||
return (
|
||||
@@ -25,7 +25,7 @@ const DealsTableFiltersModal = ({ form, projects }: Props) => {
|
||||
<IconFilter />
|
||||
Фильтры
|
||||
</InlineButton>
|
||||
<Modal title={"Фильтры для сделок"} opened={opened} onClose={close}>
|
||||
<Modal title={"Фильтры"} opened={opened} onClose={close}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
@@ -49,9 +49,9 @@ const DealsTableFiltersModal = ({ form, projects }: Props) => {
|
||||
{...form.getInputProps("board")}
|
||||
clearable
|
||||
/>
|
||||
<DealStatusSelect
|
||||
<CardStatusSelect
|
||||
board={form.values.board}
|
||||
{...form.getInputProps("dealStatus")}
|
||||
{...form.getInputProps("status")}
|
||||
clearable
|
||||
/>
|
||||
<BaseMarketplaceSelect
|
||||
@@ -75,4 +75,4 @@ const DealsTableFiltersModal = ({ form, projects }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DealsTableFiltersModal;
|
||||
export default CardsTableFiltersModal;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DealProductSchema } from "../../../client";
|
||||
import { CardProductSchema } from "../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Flex, rem } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
@@ -6,36 +6,36 @@ import ObjectMultiSelect from "../../../components/ObjectMultiSelect/ObjectMulti
|
||||
import { notifications } from "../../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
dealProducts: DealProductSchema[];
|
||||
dealProduct: DealProductSchema;
|
||||
cardProducts: CardProductSchema[];
|
||||
cardProduct: CardProductSchema;
|
||||
onSelect: (
|
||||
sourceProduct: DealProductSchema,
|
||||
destinationProducts: DealProductSchema[]
|
||||
sourceProduct: CardProductSchema,
|
||||
destinationProducts: CardProductSchema[]
|
||||
) => void;
|
||||
};
|
||||
|
||||
const SelectDealProductsModal = ({
|
||||
const SelectCardProductsModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const [dealProducts, setDealProducts] = useState<DealProductSchema[]>([]);
|
||||
const [cardProducts, setCardProducts] = useState<CardProductSchema[]>([]);
|
||||
const onSelectClick = () => {
|
||||
if (!dealProducts) {
|
||||
if (!cardProducts) {
|
||||
notifications.error({
|
||||
message:
|
||||
"Выберите товары на которые необходимо продублировать услуги",
|
||||
});
|
||||
return;
|
||||
}
|
||||
innerProps.onSelect(innerProps.dealProduct, dealProducts);
|
||||
innerProps.onSelect(innerProps.cardProduct, cardProducts);
|
||||
context.closeContextModal(id);
|
||||
};
|
||||
const onDuplicateAllClick = () => {
|
||||
innerProps.onSelect(
|
||||
innerProps.dealProduct,
|
||||
innerProps.dealProducts.filter(
|
||||
item => item !== innerProps.dealProduct
|
||||
innerProps.cardProduct,
|
||||
innerProps.cardProducts.filter(
|
||||
item => item !== innerProps.cardProduct
|
||||
)
|
||||
);
|
||||
context.closeContextModal(id);
|
||||
@@ -45,18 +45,18 @@ const SelectDealProductsModal = ({
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<Flex>
|
||||
<ObjectMultiSelect<DealProductSchema>
|
||||
<ObjectMultiSelect<CardProductSchema>
|
||||
w={"100%"}
|
||||
label={"Товары"}
|
||||
placeholder={
|
||||
"Выберите товары на которые нужно продублировать услуги"
|
||||
}
|
||||
onChange={setDealProducts}
|
||||
value={dealProducts}
|
||||
data={innerProps.dealProducts}
|
||||
onChange={setCardProducts}
|
||||
value={cardProducts}
|
||||
data={innerProps.cardProducts}
|
||||
getLabelFn={item => item.product.name}
|
||||
getValueFn={item => item.product.id.toString()}
|
||||
filterBy={item => item !== innerProps.dealProduct}
|
||||
filterBy={item => item !== innerProps.cardProduct}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
@@ -80,4 +80,4 @@ const SelectDealProductsModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectDealProductsModal;
|
||||
export default SelectCardProductsModal;
|
||||
@@ -21,6 +21,7 @@
|
||||
align-items: stretch;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.delete {
|
||||
101
src/pages/CardsPage/ui/CardsPage.tsx
Normal file
101
src/pages/CardsPage/ui/CardsPage.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { FC, useState } from "react";
|
||||
import { useCardSummaries } from "../hooks/useCardSummaries.tsx";
|
||||
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
|
||||
import CardEditDrawer from "../drawers/CardEditDrawer/CardEditDrawer.tsx";
|
||||
import { CardPageContextProvider } from "../contexts/CardPageContext.tsx";
|
||||
import { rem } from "@mantine/core";
|
||||
import useCardsPageState from "../hooks/useCardsPageState.tsx";
|
||||
import CardsTable from "../components/CardsTable/CardsTable.tsx";
|
||||
import { motion } from "framer-motion";
|
||||
import CardPrefillDrawer from "../drawers/CardPrefillDrawer/CardPrefillDrawer.tsx";
|
||||
import { PrefillCardContextProvider } from "../contexts/PrefillCardContext.tsx";
|
||||
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 { ProjectsEditorContextProvider } from "../contexts/ProjectsEditorContext.tsx";
|
||||
import ProjectEditDrawer from "../drawers/ProjectEditDrawer/ProjectEditDrawer.tsx";
|
||||
import Boards from "../../../components/Dnd/Boards/Boards/Boards.tsx";
|
||||
import { DndContextProvider } from "../contexts/DndContext.tsx";
|
||||
|
||||
export const CardsPage: FC = () => {
|
||||
const { data, form } = useCardsPageState();
|
||||
const { dealId } = useParams({ strict: false });
|
||||
const { summariesRaw, refetch: refetchSummaries } = useCardSummaries();
|
||||
|
||||
const [displayMode, setDisplayMode] = useState<DisplayMode>(DisplayMode.BOARD);
|
||||
|
||||
const tableBody = (
|
||||
<CardsTable items={data} />
|
||||
);
|
||||
|
||||
const boardsBody = (
|
||||
<DndContextProvider
|
||||
summariesRaw={summariesRaw}
|
||||
refetchSummaries={refetchSummaries}
|
||||
>
|
||||
<Boards />
|
||||
</DndContextProvider>
|
||||
);
|
||||
|
||||
const getBody = () => {
|
||||
return (
|
||||
<motion.div
|
||||
key={displayMode}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
{displayMode === DisplayMode.TABLE
|
||||
? tableBody
|
||||
: boardsBody
|
||||
}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageBlock
|
||||
fullHeight
|
||||
style={{
|
||||
gap: rem(10),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "transparent",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<ProjectsEditorContextProvider>
|
||||
<CardPageContextProvider
|
||||
defaultCardId={(dealId && parseInt(dealId)) || undefined}
|
||||
refetchCards={async () => {
|
||||
await refetchSummaries();
|
||||
}}
|
||||
>
|
||||
<PrefillCardContextProvider>
|
||||
<PrefillCardsWithExcelContextProvider>
|
||||
<CardsPageHeader
|
||||
form={form}
|
||||
displayMode={displayMode}
|
||||
setDisplayMode={setDisplayMode}
|
||||
/>
|
||||
<PageBlock
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{getBody()}
|
||||
</PageBlock>
|
||||
<CardEditDrawer />
|
||||
<ProjectEditDrawer />
|
||||
<CardPrefillDrawer />
|
||||
</PrefillCardsWithExcelContextProvider>
|
||||
</PrefillCardContextProvider>
|
||||
</CardPageContextProvider>
|
||||
</ProjectsEditorContextProvider>
|
||||
</PageBlock>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { DealPage } from "./ui/DealPage";
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { DealPageContextProvider, useDealPageContext } from "../../DealsPage/contexts/DealPageContext.tsx";
|
||||
import ProductAndServiceTab from "../../DealsPage/tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
|
||||
import { FC, useEffect } from "react";
|
||||
import { DealService } from "../../../client";
|
||||
|
||||
export type Props = {
|
||||
dealId: number;
|
||||
};
|
||||
const DealPageContent: FC<Props> = ({ dealId }) => {
|
||||
const { setSelectedDeal } = useDealPageContext();
|
||||
useEffect(() => {
|
||||
DealService.getDealById({ dealId }).then(deal => {
|
||||
setSelectedDeal(deal);
|
||||
});
|
||||
}, []);
|
||||
return <ProductAndServiceTab />;
|
||||
};
|
||||
const DealPageWrapper: FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return <DealPageContextProvider refetchDeals={async () => {
|
||||
}}>{children}</DealPageContextProvider>;
|
||||
};
|
||||
export const DealPage = () => {
|
||||
const { dealId } = useParams({ strict: false });
|
||||
return (
|
||||
<DealPageWrapper>
|
||||
<DealPageContent dealId={parseInt(dealId || "-1")} />
|
||||
</DealPageWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
ComboboxItem,
|
||||
ComboboxItemGroup,
|
||||
Flex,
|
||||
Input,
|
||||
OptionsFilter,
|
||||
rem,
|
||||
} from "@mantine/core";
|
||||
import { BaseFormInputProps } from "../../../../types/utils.ts";
|
||||
import { DealProductServiceSchema, ServiceSchema } from "../../../../client";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import ServiceWithPriceInput from "../../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
|
||||
import { isNumber } from "lodash";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
|
||||
type RestProps = {
|
||||
quantity: number;
|
||||
};
|
||||
type Props = BaseFormInputProps<DealProductServiceSchema[]> & RestProps;
|
||||
const DealProductServiceTable: FC<Props> = (props: Props) => {
|
||||
const { value, onChange, quantity, error } = props;
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const [innerValue, setInnerValue] = useState<
|
||||
Partial<DealProductServiceSchema>[]
|
||||
>(value || []);
|
||||
const onServiceChange = (idx: number, value: ServiceSchema) => {
|
||||
setInnerValue(oldValue =>
|
||||
oldValue.map((item, i) =>
|
||||
i === idx ? { ...item, service: value } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
const onQuantityChange = (idx: number, value: string | number) => {
|
||||
if (!isNumber(value)) return;
|
||||
setInnerValue(oldValue =>
|
||||
oldValue.map((item, i) =>
|
||||
i === idx ? { ...item, price: value } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
const onCreate = () => {
|
||||
if (innerValue.length > 0 && !innerValue.at(-1)?.service) {
|
||||
notifications.error({ message: "Заполните последнюю услугу" });
|
||||
return;
|
||||
}
|
||||
setInnerValue(prevState => [
|
||||
...prevState,
|
||||
{ service: undefined, quantity: 1 },
|
||||
]);
|
||||
};
|
||||
const onDelete = (idx: number) => {
|
||||
setInnerValue(oldValue => oldValue.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
const serviceOptionsFilter = ({
|
||||
options,
|
||||
}: {
|
||||
options: ComboboxItemGroup[];
|
||||
}) => {
|
||||
const productServiceIds = innerValue.map(
|
||||
service => service.service?.id
|
||||
);
|
||||
return (options as ComboboxItemGroup[]).map(({ items, group }) => {
|
||||
return {
|
||||
group,
|
||||
items: items.filter(
|
||||
item =>
|
||||
!productServiceIds.includes(
|
||||
parseInt((item as ComboboxItem).value)
|
||||
)
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
onChange(innerValue as DealProductServiceSchema[]);
|
||||
}, [innerValue]);
|
||||
return (
|
||||
<Input.Wrapper error={error}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
{innerValue.map((service, idx) => (
|
||||
<Flex
|
||||
key={service.service?.name || idx}
|
||||
direction={"row"}
|
||||
gap={rem(10)}
|
||||
align={"center"}
|
||||
justify={"stretch"}>
|
||||
<ActionIcon
|
||||
onClick={() => onDelete(idx)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
<ServiceWithPriceInput
|
||||
serviceProps={{
|
||||
onChange: event => onServiceChange(idx, event),
|
||||
value: service.service,
|
||||
placeholder: "Выберите услугу",
|
||||
style: { width: "100%" },
|
||||
filter: serviceOptionsFilter as OptionsFilter,
|
||||
}}
|
||||
priceProps={{
|
||||
onChange: event => onQuantityChange(idx, event),
|
||||
value: service.price,
|
||||
placeholder: "Введите стоимость",
|
||||
hideControls: true,
|
||||
style: { width: "100%" },
|
||||
suffix: "₽",
|
||||
disabled: authState.isGuest,
|
||||
}}
|
||||
containerProps={{ w: "100%" }}
|
||||
quantity={quantity}
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
<Button
|
||||
onClick={onCreate}
|
||||
variant={"default"}>
|
||||
Добавить услугу
|
||||
</Button>
|
||||
</Flex>
|
||||
</Input.Wrapper>
|
||||
);
|
||||
};
|
||||
export default DealProductServiceTable;
|
||||
@@ -1,168 +0,0 @@
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useDealProductsTableColumns from "./columns.tsx";
|
||||
import { FC } from "react";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { DealProductSchema, ProductService } from "../../../../client";
|
||||
import { ActionIcon, Button, Flex, rem, Tooltip } from "@mantine/core";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IconBarcode, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { CreateProductRequest } from "../../../ProductsPage/types.ts";
|
||||
|
||||
type RestProps = {
|
||||
clientId: number;
|
||||
onMultipleDelete?: (items: DealProductSchema[]) => void;
|
||||
};
|
||||
type Props = CRUDTableProps<DealProductSchema> & RestProps;
|
||||
const DealProductsTable: FC<Props> = (props: Props) => {
|
||||
const { items, clientId, onChange, onCreate, onDelete, onMultipleDelete } =
|
||||
props;
|
||||
|
||||
const columns = useDealProductsTableColumns({
|
||||
onChange: (product, quantity) => {
|
||||
if (!onChange) return;
|
||||
if (quantity <= 0 && onDelete) {
|
||||
onDelete(product);
|
||||
return;
|
||||
}
|
||||
onChange({ ...product, quantity });
|
||||
},
|
||||
data: items,
|
||||
});
|
||||
|
||||
const onCreateClick = () => {
|
||||
if (!onCreate) return;
|
||||
modals.openContextModal({
|
||||
modal: "addDealProduct",
|
||||
title: "Добавление товара",
|
||||
innerProps: {
|
||||
onCreate: product => onCreate(product as DealProductSchema),
|
||||
clientId,
|
||||
},
|
||||
size: "lg",
|
||||
});
|
||||
};
|
||||
const onPrintBarcodeClick = (product: DealProductSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "printBarcode",
|
||||
title: "Печать штрихкода",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
productId: product.product.id,
|
||||
defaultQuantity: product.quantity,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onCreateProduct = (newProduct: CreateProductRequest) => {
|
||||
ProductService.createProduct({
|
||||
requestBody: newProduct,
|
||||
}).then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message: message });
|
||||
});
|
||||
};
|
||||
const onCreateProductClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "createProduct",
|
||||
title: "Создание товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
clientId: clientId,
|
||||
onCreate: onCreateProduct,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onEditClick = (product: DealProductSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "addDealProduct",
|
||||
title: "Создание товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
clientId: clientId,
|
||||
element: product,
|
||||
onChange: onChange,
|
||||
},
|
||||
size: "lg",
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={
|
||||
{
|
||||
enableBottomToolbar: true,
|
||||
enableRowActions: true,
|
||||
enableRowSelection: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onDelete && onDelete(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Печать штрихкода">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onPrintBarcodeClick(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconBarcode />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
renderBottomToolbar: ({ table }) => (
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
gap={rem(10)}
|
||||
p={rem(10)}>
|
||||
{onMultipleDelete &&
|
||||
table.getSelectedRowModel().rows.length > 0 && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onMultipleDelete(
|
||||
table
|
||||
.getSelectedRowModel()
|
||||
.rows.map(
|
||||
row => row.original
|
||||
)
|
||||
);
|
||||
}}
|
||||
variant={"filled"}
|
||||
color={"red"}>
|
||||
Удалить выбранные
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={onCreateProductClick}>
|
||||
Создать товар
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={onCreateClick}
|
||||
variant={"default"}>
|
||||
Добавить товар в сделку
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<DealProductSchema>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealProductsTable;
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { DealProductSchema } from "../../../../client";
|
||||
import { List } from "@mantine/core";
|
||||
|
||||
type Props = {
|
||||
onChange: (product: DealProductSchema, quantity: number) => void;
|
||||
data: DealProductSchema[];
|
||||
};
|
||||
const useDealProductsTableColumns = (props: Props) => {
|
||||
const { onChange, data } = props;
|
||||
const totalQuantity = useMemo(
|
||||
() => data.reduce((acc, row) => acc + row.quantity, 0),
|
||||
[data]
|
||||
);
|
||||
const totalPrice = useMemo(
|
||||
() =>
|
||||
data.reduce(
|
||||
(totalAcc, row) =>
|
||||
totalAcc +
|
||||
row.services.reduce(
|
||||
(singleAcc, service) =>
|
||||
singleAcc + service.price * row.quantity,
|
||||
0
|
||||
),
|
||||
0
|
||||
),
|
||||
[data]
|
||||
);
|
||||
return useMemo<MRT_ColumnDef<DealProductSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "product.article",
|
||||
header: "Артикул",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "product.name",
|
||||
header: "Название",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "product.barcodes",
|
||||
header: "Штрихкоды",
|
||||
Cell: ({ cell }) => {
|
||||
return (
|
||||
<List size={"sm"}>
|
||||
{cell
|
||||
.getValue<string[]>()
|
||||
?.map(barcode => (
|
||||
<List.Item key={barcode}>
|
||||
{barcode}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "quantity",
|
||||
header: "Количество",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
Footer: <>Всего товаров: {totalQuantity} </>,
|
||||
},
|
||||
{
|
||||
header: "Услуги",
|
||||
Cell: ({ row }) => (
|
||||
<List size={"sm"}>
|
||||
{row.original.services
|
||||
.map(
|
||||
service =>
|
||||
`${service.service.name} (${service.price}₽ за шт)`
|
||||
)
|
||||
.map(serviceText => (
|
||||
<List.Item key={serviceText}>
|
||||
{serviceText}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
),
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
header: "Итоговая стоимость услуг",
|
||||
Cell: ({ row }) => (
|
||||
<>
|
||||
{row.original.services.reduce(
|
||||
(acc, service) =>
|
||||
acc + row.original.quantity * service.price,
|
||||
0
|
||||
)}
|
||||
</>
|
||||
),
|
||||
enableColumnActions: false,
|
||||
Footer: <>Всего стоимость услуг: {totalPrice}</>,
|
||||
},
|
||||
],
|
||||
[onChange, data]
|
||||
);
|
||||
};
|
||||
|
||||
export default useDealProductsTableColumns;
|
||||
@@ -1,129 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { useDealServicesTableColumns } from "./columns.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { DealServiceSchema } from "../../../../client";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { ActionIcon, Button, Flex, rem, Tooltip } from "@mantine/core";
|
||||
import { openContextModal } from "@mantine/modals";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
type RestProps = {
|
||||
onMultipleDelete?: (items: DealServiceSchema[]) => void;
|
||||
};
|
||||
type Props = CRUDTableProps<DealServiceSchema> & RestProps;
|
||||
const DealServicesTable: FC<Props> = ({
|
||||
items,
|
||||
onChange,
|
||||
onDelete,
|
||||
onCreate,
|
||||
onSelectionChange,
|
||||
onMultipleDelete,
|
||||
tableRef,
|
||||
}) => {
|
||||
const serviceIds = items.map(item => item.service.id);
|
||||
|
||||
const columns = useDealServicesTableColumns({
|
||||
data: items,
|
||||
});
|
||||
const onCreateClick = () => {
|
||||
if (!onCreate) return;
|
||||
openContextModal({
|
||||
title: "Добавление услуги",
|
||||
modal: "addDealService",
|
||||
innerProps: {
|
||||
onCreate: event => onCreate(event as DealServiceSchema),
|
||||
serviceIds,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onEditClick = (service: DealServiceSchema) => {
|
||||
if (!onChange) return;
|
||||
openContextModal({
|
||||
title: "Добавление услуги",
|
||||
modal: "addDealService",
|
||||
innerProps: {
|
||||
element: service,
|
||||
onChange,
|
||||
serviceIds,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
data={items}
|
||||
columns={columns}
|
||||
onSelectionChange={onSelectionChange}
|
||||
restProps={
|
||||
{
|
||||
enableGrouping: true,
|
||||
initialState: { grouping: ["service.category"] },
|
||||
enableColumnActions: false,
|
||||
enableSorting: false,
|
||||
enableBottomToolbar: true,
|
||||
enableRowActions: true,
|
||||
enableRowSelection: true,
|
||||
renderBottomToolbar: ({ table }) => (
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
gap={rem(10)}
|
||||
p={rem(10)}>
|
||||
{onMultipleDelete &&
|
||||
table.getSelectedRowModel().rows.length >
|
||||
0 && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onMultipleDelete(
|
||||
table
|
||||
.getSelectedRowModel()
|
||||
.rows.map(
|
||||
row => row.original
|
||||
)
|
||||
);
|
||||
}}
|
||||
variant={"filled"}
|
||||
color={"red"}>
|
||||
Удалить выбранные
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={onCreateClick}
|
||||
variant={"default"}>
|
||||
Добавить услугу
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
if (onDelete)
|
||||
onDelete(row.original);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
onEditClick(row.original);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<DealServiceSchema>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealServicesTable;
|
||||
@@ -1,63 +0,0 @@
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { useMemo } from "react";
|
||||
import { DealServiceSchema } from "../../../../client";
|
||||
|
||||
type Props = {
|
||||
data: DealServiceSchema[];
|
||||
};
|
||||
|
||||
export const useDealServicesTableColumns = (props: Props) => {
|
||||
const { data } = props;
|
||||
const totalPrice = useMemo(
|
||||
() => data.reduce((acc, row) => acc + row.quantity * row.price, 0),
|
||||
[data]
|
||||
);
|
||||
|
||||
return useMemo<MRT_ColumnDef<DealServiceSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "service.category",
|
||||
header: "Категория",
|
||||
accessorFn: row => row.service.category.name,
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "service.name",
|
||||
header: "Услуга",
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "price",
|
||||
header: "Цена",
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "quantity",
|
||||
header: "Количество",
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
header: "Сумма",
|
||||
Cell: ({ row }) => {
|
||||
return row.original.quantity * row.original.price;
|
||||
},
|
||||
aggregationFn: "sum",
|
||||
AggregatedCell: ({ cell }) => {
|
||||
return (
|
||||
<>
|
||||
Итоговая сумма по категории:{" "}
|
||||
{cell.row.subRows?.reduce(
|
||||
(acc, row) =>
|
||||
acc +
|
||||
row.original.quantity * row.original.price,
|
||||
0
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
Footer: <>Итоговая сумма по услугам: {totalPrice}</>,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import { createContext, FC, useContext, useEffect, useState } from "react";
|
||||
import { DealSchema, DealService } from "../../../client";
|
||||
|
||||
type DealPageContextState = {
|
||||
selectedDeal?: DealSchema;
|
||||
setSelectedDeal: (deal: DealSchema | undefined) => void;
|
||||
refetchDeals: () => Promise<void>;
|
||||
refetchDeal: () => void;
|
||||
};
|
||||
|
||||
const DealPageContext = createContext<DealPageContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
type DealPageContextStateProps = {
|
||||
refetchDeals: () => Promise<void>;
|
||||
defaultDealId?: number;
|
||||
}
|
||||
|
||||
const useDealPageContextState = (props: DealPageContextStateProps) => {
|
||||
const { refetchDeals, defaultDealId } = props;
|
||||
const [selectedDeal, setSelectedDeal] = useState<DealSchema | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const refetchDeal = () => {
|
||||
const dealId = selectedDeal?.id ?? defaultDealId;
|
||||
if (!dealId) return;
|
||||
|
||||
DealService.getDealById({ dealId }).then(deal => {
|
||||
setSelectedDeal(deal);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetchDeal();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
selectedDeal,
|
||||
setSelectedDeal,
|
||||
refetchDeals,
|
||||
refetchDeal,
|
||||
};
|
||||
};
|
||||
|
||||
type DealPageContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
refetchDeals: () => Promise<void>;
|
||||
defaultDealId?: number;
|
||||
};
|
||||
|
||||
export const DealPageContextProvider: FC<DealPageContextProviderProps> = ({
|
||||
children,
|
||||
refetchDeals,
|
||||
defaultDealId,
|
||||
}) => {
|
||||
const state = useDealPageContextState({ refetchDeals, defaultDealId });
|
||||
return (
|
||||
<DealPageContext.Provider value={state}>
|
||||
{children}
|
||||
</DealPageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useDealPageContext = () => {
|
||||
const context = useContext(DealPageContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useDealPageContext must be used within a DealPageContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import { createContext, Dispatch, FC, SetStateAction, useContext, useState } from "react";
|
||||
import { DealSchema, DealService } from "../../../client";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
type PrefillDealContextState = {
|
||||
prefillOpened: boolean;
|
||||
prefillOnClose: () => void;
|
||||
prefillOnOpen: () => void;
|
||||
|
||||
selectedPrefillDeal?: DealSchema;
|
||||
selectPrefillDeal: (dealId: number) => void;
|
||||
prefillDeal?: DealSchema;
|
||||
setPrefillDeal: Dispatch<SetStateAction<DealSchema | undefined>>;
|
||||
};
|
||||
|
||||
const PrefillDealContext = createContext<PrefillDealContextState | undefined>(
|
||||
undefined
|
||||
);
|
||||
const usePrefillDealContextState = () => {
|
||||
const [selectedPrefillDeal, setSelectedPrefillDeal] = useState<DealSchema | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [prefillDeal, setPrefillDeal] = useState<DealSchema | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [prefillOpened, { open, close }] = useDisclosure(false);
|
||||
const prefillOnClose = close;
|
||||
const prefillOnOpen = open;
|
||||
|
||||
const selectPrefillDeal = (dealId: number) => {
|
||||
DealService.getDealById({ dealId }).then(deal => {
|
||||
setSelectedPrefillDeal(deal);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
prefillOpened,
|
||||
prefillOnClose,
|
||||
prefillOnOpen,
|
||||
|
||||
selectedPrefillDeal,
|
||||
selectPrefillDeal,
|
||||
prefillDeal,
|
||||
setPrefillDeal,
|
||||
};
|
||||
};
|
||||
|
||||
type PrefillDealContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const PrefillDealContextProvider: FC<PrefillDealContextProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const state = usePrefillDealContextState();
|
||||
return (
|
||||
<PrefillDealContext.Provider value={state}>
|
||||
{children}
|
||||
</PrefillDealContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePrefillDealContext = () => {
|
||||
const context = useContext(PrefillDealContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"usePrefillDealContext must be used within a PrefillDealContextProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,126 +0,0 @@
|
||||
import { Box, Drawer, rem, Tabs } from "@mantine/core";
|
||||
import { FC, ReactNode, useEffect } from "react";
|
||||
import { useDealPageContext } from "../../contexts/DealPageContext.tsx";
|
||||
import { IconBox, IconCalendarUser, IconCubeSend, IconSettings, IconUser, IconUsersGroup } from "@tabler/icons-react";
|
||||
import DealStatusChangeTable from "../../components/DealStatusChangeTable/DealStatusChangeTable.tsx";
|
||||
import DealEditDrawerGeneralTab from "./tabs/DealEditDrawerGeneralTab.tsx";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import ProductAndServiceTab from "../../tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
|
||||
import { motion } from "framer-motion";
|
||||
import ShippingTab from "../../tabs/ShippingTab/ShippingTab.tsx";
|
||||
import EmployeesTab from "../../tabs/EmployeesTab/EmployeesTab.tsx";
|
||||
import ClientTab from "./tabs/ClientTab.tsx";
|
||||
|
||||
const useDealStatusChangeState = () => {
|
||||
const { selectedDeal } = useDealPageContext();
|
||||
return {
|
||||
statusHistory: selectedDeal?.statusHistory || [],
|
||||
};
|
||||
};
|
||||
|
||||
const DealEditDrawerStatusChangeTable = () => {
|
||||
const { statusHistory } = useDealStatusChangeState();
|
||||
|
||||
return <DealStatusChangeTable items={statusHistory} />;
|
||||
};
|
||||
|
||||
const useDealEditDrawerState = () => {
|
||||
const { selectedDeal, setSelectedDeal } = useDealPageContext();
|
||||
return {
|
||||
isVisible: selectedDeal !== undefined,
|
||||
onClose: () => setSelectedDeal(undefined),
|
||||
};
|
||||
};
|
||||
|
||||
const DealEditDrawer: FC = () => {
|
||||
const { isVisible, onClose } = useDealEditDrawerState();
|
||||
const queryClient = useQueryClient();
|
||||
useEffect(() => {
|
||||
if (isVisible) return;
|
||||
queryClient.invalidateQueries({ queryKey: ["getDealSummaries"] });
|
||||
}, [isVisible]);
|
||||
|
||||
const getTabPanel = (value: string, component: ReactNode) => {
|
||||
return (
|
||||
<Tabs.Panel value={value}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<Box
|
||||
h={"100%"}
|
||||
w={"100%"}
|
||||
p={rem(10)}>
|
||||
{component}
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
size={"calc(100vw - 150px)"}
|
||||
position={"right"}
|
||||
onClose={onClose}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
withCloseButton={false}
|
||||
opened={isVisible}
|
||||
styles={{
|
||||
body: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: rem(10),
|
||||
},
|
||||
}}>
|
||||
<Tabs
|
||||
defaultValue={"general"}
|
||||
flex={1}
|
||||
variant={"outline"}
|
||||
orientation={"vertical"}
|
||||
keepMounted={false}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab
|
||||
value={"general"}
|
||||
leftSection={<IconSettings />}>
|
||||
Общее
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"client"}
|
||||
leftSection={<IconUser />}>
|
||||
Клиент
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"history"}
|
||||
leftSection={<IconCalendarUser />}>
|
||||
История
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"servicesAndProducts"}
|
||||
leftSection={<IconBox />}>
|
||||
Товары и услуги
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"shipment"}
|
||||
leftSection={<IconCubeSend />}>
|
||||
Отгрузка
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"employees"}
|
||||
leftSection={<IconUsersGroup />}>
|
||||
Исполнители
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
{getTabPanel("general", <DealEditDrawerGeneralTab />)}
|
||||
{getTabPanel("client", <ClientTab />)}
|
||||
{getTabPanel("history", <DealEditDrawerStatusChangeTable />)}
|
||||
{getTabPanel("servicesAndProducts", <ProductAndServiceTab />)}
|
||||
{getTabPanel("shipment", <ShippingTab />)}
|
||||
{getTabPanel("employees", <EmployeesTab />)}
|
||||
</Tabs>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealEditDrawer;
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Button, Fieldset, Flex, rem, Textarea, TextInput } from "@mantine/core";
|
||||
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { DealGeneralFormType } from "./DealEditDrawerGeneralTab.tsx";
|
||||
import { ClientService, DealSchema, DealService } from "../../../../../client";
|
||||
import { isEqual } from "lodash";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
const ClientTab = () => {
|
||||
const { selectedDeal: deal, setSelectedDeal } = useDealPageContext();
|
||||
const initialValues: DealGeneralFormType = deal as DealSchema;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useForm<DealGeneralFormType>(
|
||||
{
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
name: (value: string) => value.length > 0 ? null : "Название сделки не может быть пустым",
|
||||
},
|
||||
},
|
||||
);
|
||||
const hasChanges = !isEqual(form.values, initialValues);
|
||||
const updateClientInfo = async (values: DealGeneralFormType) => {
|
||||
return ClientService.updateClient({
|
||||
requestBody: {
|
||||
data: values.client,
|
||||
},
|
||||
}).then(({ ok, message }) => notifications.guess(ok, { message }));
|
||||
};
|
||||
const update = async () => {
|
||||
return DealService.getDealById({ dealId: form.values.id }).then(data => {
|
||||
setSelectedDeal(data);
|
||||
form.setInitialValues(data);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["getDealSummaries"],
|
||||
});
|
||||
});
|
||||
};
|
||||
const handleSave = () => {
|
||||
updateClientInfo(form.values).then(async () => {
|
||||
await update();
|
||||
});
|
||||
};
|
||||
const handleCancel = () => {
|
||||
form.setInitialValues(initialValues);
|
||||
};
|
||||
return (
|
||||
<Flex direction={"column"} flex={1} gap={rem(10)}>
|
||||
<Flex flex={1}>
|
||||
<Fieldset legend={"Клиент"} flex={1}>
|
||||
<TextInput
|
||||
disabled
|
||||
placeholder={"Название"}
|
||||
label={"Название"}
|
||||
value={deal?.client.name}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите телефон"}
|
||||
label={"Телефон клиента"}
|
||||
{...form.getInputProps("client.details.phoneNumber")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите email"}
|
||||
label={"Email"}
|
||||
{...form.getInputProps("client.details.email")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите телеграм"}
|
||||
label={"Телеграм"}
|
||||
{...form.getInputProps("client.details.telegram")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите ИНН"}
|
||||
label={"ИНН"}
|
||||
{...form.getInputProps("client.details.inn")}
|
||||
/>
|
||||
<Textarea
|
||||
placeholder={"Введите комментарий"}
|
||||
label={"Комментарий"}
|
||||
{...form.getInputProps("client.comment")}
|
||||
/>
|
||||
|
||||
</Fieldset>
|
||||
</Flex>
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
justify={"flex-end"}
|
||||
display={!hasChanges ? "none" : "flex"}
|
||||
>
|
||||
<Button onClick={handleCancel} variant={"default"}>Отмена</Button>
|
||||
<Button onClick={handleSave} variant={"default"}>Сохранить</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
);
|
||||
};
|
||||
export default ClientTab;
|
||||
@@ -1,373 +0,0 @@
|
||||
import { FC, useState } from "react";
|
||||
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Fieldset,
|
||||
Flex,
|
||||
Group,
|
||||
rem,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import {
|
||||
ClientService,
|
||||
DealSchema,
|
||||
DealService,
|
||||
ProjectSchema,
|
||||
ShippingWarehouseSchema,
|
||||
StatusSchema,
|
||||
} from "../../../../../client";
|
||||
import { isEqual } from "lodash";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import ShippingWarehouseAutocomplete
|
||||
from "../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
|
||||
import { ButtonCopyControlled } from "../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import ButtonCopy from "../../../../../components/ButtonCopy/ButtonCopy.tsx";
|
||||
import FileSaver from "file-saver";
|
||||
import { dateWithoutTimezone, getCurrentDateTimeForFilename } from "../../../../../shared/lib/date.ts";
|
||||
import { IconBarcode, IconPrinter } from "@tabler/icons-react";
|
||||
import styles from "../../../ui/DealsPage.module.css";
|
||||
import { base64ToBlob } from "../../../../../shared/lib/utils.ts";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import ManagerSelect from "../../../../../components/ManagerSelect/ManagerSelect.tsx";
|
||||
import ProjectSelect from "../../../../../components/ProjectSelect/ProjectSelect.tsx";
|
||||
import BoardSelect from "../../../../../components/BoardSelect/BoardSelect.tsx";
|
||||
import DealStatusSelect from "../../../../../components/DealStatusSelect/DealStatusSelect.tsx";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema;
|
||||
};
|
||||
|
||||
export type DealGeneralFormType = Omit<DealSchema, "statusHistory" | "services" | "products">;
|
||||
|
||||
const Content: FC<Props> = ({ deal }) => {
|
||||
const { setSelectedDeal } = useDealPageContext();
|
||||
const clipboard = useClipboard();
|
||||
const queryClient = useQueryClient();
|
||||
const [project, setProject] = useState<ProjectSchema | null>(deal.board.project);
|
||||
|
||||
// ignore typescript
|
||||
|
||||
const initialValues: DealGeneralFormType = {
|
||||
...deal,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
deliveryDate: deal.deliveryDate ? new Date(deal.deliveryDate) : null,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
receivingSlotDate: deal.receivingSlotDate ? new Date(deal.receivingSlotDate) : null,
|
||||
};
|
||||
const form = useForm<DealGeneralFormType>({
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
name: (value: string) =>
|
||||
value.length > 0
|
||||
? null
|
||||
: "Название сделки не может быть пустым",
|
||||
status: (value: StatusSchema) =>
|
||||
!value && "Статус для сделки не выбран",
|
||||
},
|
||||
});
|
||||
const updateDealInfo = async (values: DealGeneralFormType) => {
|
||||
return DealService.updateDealGeneralInfo({
|
||||
requestBody: {
|
||||
dealId: deal.id,
|
||||
data: {
|
||||
...values,
|
||||
shippingWarehouse: values.shippingWarehouse?.toString(),
|
||||
},
|
||||
},
|
||||
}).then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
DealService.getDealById({ dealId: deal.id }).then(data => {
|
||||
setSelectedDeal(data);
|
||||
form.setInitialValues(data);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["getDealSummaries"],
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
const updateClientInfo = async (values: DealGeneralFormType) => {
|
||||
return ClientService.updateClient({
|
||||
requestBody: {
|
||||
data: values.client,
|
||||
},
|
||||
}).then(({ ok, message }) => notifications.guess(ok, { message }));
|
||||
};
|
||||
const handleSubmit = async (values: DealGeneralFormType) => {
|
||||
// Updating client info if there changes
|
||||
if (!isEqual(values.client, deal.client)) {
|
||||
await updateClientInfo(values);
|
||||
}
|
||||
// updating deal info
|
||||
// get shimpent warehouse name from object if its object, otherwise just pass it
|
||||
const shippingWarehouse = isShippingWarehouse(values.shippingWarehouse) ? values.shippingWarehouse.name : values.shippingWarehouse;
|
||||
await updateDealInfo({
|
||||
...values,
|
||||
deliveryDate: values.deliveryDate ? dateWithoutTimezone(new Date(values.deliveryDate)) : null,
|
||||
receivingSlotDate: values.receivingSlotDate ? dateWithoutTimezone(new Date(values.receivingSlotDate)) : null,
|
||||
shippingWarehouse: shippingWarehouse,
|
||||
});
|
||||
};
|
||||
const isShippingWarehouse = (
|
||||
value: ShippingWarehouseSchema | string | null | undefined,
|
||||
): value is ShippingWarehouseSchema => {
|
||||
return !["string", "null", "undefined"].includes(typeof value);
|
||||
};
|
||||
|
||||
const onCopyGuestUrlClick = () => {
|
||||
DealService.createDealGuestUrl({
|
||||
requestBody: {
|
||||
dealId: deal.id,
|
||||
},
|
||||
}).then(({ ok, message, url }) => {
|
||||
if (!ok) notifications.guess(ok, { message });
|
||||
clipboard.copy(`${window.location.origin}/${url}`);
|
||||
});
|
||||
};
|
||||
const billRequestPdfUrl = deal?.billRequest?.pdfUrl || deal?.group?.billRequest?.pdfUrl;
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => handleSubmit(values))}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
justify={"space-between"}
|
||||
h={"100%"}>
|
||||
<Fieldset legend={`Общие параметры [ID: ${deal.id}]`}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<TextInput
|
||||
placeholder={"Название сделки"}
|
||||
label={"Название сделки"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
disabled
|
||||
placeholder={"Дата создания"}
|
||||
label={"Дата создания"}
|
||||
value={new Date(deal.createdAt).toLocaleString(
|
||||
"ru-RU",
|
||||
)}
|
||||
/>
|
||||
<ProjectSelect
|
||||
value={project}
|
||||
onChange={setProject}
|
||||
label={"Проект"}
|
||||
/>
|
||||
<BoardSelect
|
||||
project={project}
|
||||
{...form.getInputProps("board")}
|
||||
label={"Доска"}
|
||||
/>
|
||||
<DealStatusSelect
|
||||
board={form.values.board}
|
||||
{...form.getInputProps("status")}
|
||||
label={"Статус"}
|
||||
/>
|
||||
<Textarea
|
||||
h={rem(150)}
|
||||
styles={{
|
||||
wrapper: {height:'90%'},
|
||||
input: {height:'90%'}
|
||||
}}
|
||||
label={"Коментарий к сделке"}
|
||||
placeholder={"Введите коментарий к сделке"}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
<ShippingWarehouseAutocomplete
|
||||
placeholder={"Введите склад отгрузки"}
|
||||
label={"Склад отгрузки"}
|
||||
value={
|
||||
isShippingWarehouse(
|
||||
form.values.shippingWarehouse,
|
||||
)
|
||||
? form.values.shippingWarehouse
|
||||
: undefined
|
||||
}
|
||||
onChange={event => {
|
||||
if (isShippingWarehouse(event)) {
|
||||
form.getInputProps(
|
||||
"shippingWarehouse",
|
||||
).onChange(event.name);
|
||||
return;
|
||||
}
|
||||
form.getInputProps(
|
||||
"shippingWarehouse",
|
||||
).onChange(event);
|
||||
}}
|
||||
/>
|
||||
<DatePickerInput
|
||||
minDate={new Date()}
|
||||
placeholder={"Укажите дату передачи в доставку"}
|
||||
label={"Дата передачи в доставку"}
|
||||
{...form.getInputProps("deliveryDate")}
|
||||
|
||||
/>
|
||||
<DatePickerInput
|
||||
minDate={new Date()}
|
||||
|
||||
placeholder={"Укажите слот приемки"}
|
||||
label={"Слот приемки"}
|
||||
{...form.getInputProps("receivingSlotDate")}
|
||||
|
||||
/>
|
||||
<ManagerSelect
|
||||
placeholder={"Укажите менеджера"}
|
||||
label={"Менеджер"}
|
||||
{...form.getInputProps("manager")}
|
||||
/>
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
<Flex
|
||||
mt={"md"}
|
||||
gap={rem(10)}
|
||||
align={"center"}
|
||||
justify={"flex-end"}>
|
||||
<Flex
|
||||
align={"center"}
|
||||
gap={rem(10)}
|
||||
justify={"center"}>
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
align={"center"}
|
||||
justify={"space-between"}>
|
||||
<Tooltip
|
||||
className={styles["print-deals-button"]}
|
||||
label={"Распечатать штрихкоды сделки"}
|
||||
>
|
||||
<ActionIcon
|
||||
onClick={async () => {
|
||||
const response =
|
||||
await DealService.getDealProductsBarcodesPdf({
|
||||
requestBody: {
|
||||
dealId: deal.id,
|
||||
},
|
||||
});
|
||||
const pdfBlob = base64ToBlob(
|
||||
response.base64String,
|
||||
response.mimeType,
|
||||
);
|
||||
const pdfUrl = URL.createObjectURL(pdfBlob);
|
||||
const pdfWindow = window.open(pdfUrl);
|
||||
if (!pdfWindow) {
|
||||
notifications.error({ message: "Ошибка" });
|
||||
return;
|
||||
}
|
||||
pdfWindow.onload = () => {
|
||||
pdfWindow.print();
|
||||
};
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconBarcode />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={"Распечатать сделку"}>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
const pdfWindow = window.open(
|
||||
`${import.meta.env.VITE_API_URL}/deal/tech-spec/${deal.id}`,
|
||||
);
|
||||
if (!pdfWindow) return;
|
||||
pdfWindow.print();
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconPrinter />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Flex gap={rem(10)}>
|
||||
{billRequestPdfUrl ? (
|
||||
<ButtonCopy
|
||||
onCopiedLabel={
|
||||
"Ссылка скопирована в буфер обмена"
|
||||
}
|
||||
value={billRequestPdfUrl}>
|
||||
Ссылка на оплату
|
||||
</ButtonCopy>
|
||||
) : (
|
||||
<ButtonCopyControlled
|
||||
onCopyClick={() => {
|
||||
const date =
|
||||
getCurrentDateTimeForFilename();
|
||||
FileSaver.saveAs(
|
||||
`${import.meta.env.VITE_API_URL}/deal/billing-document/${deal.id}`,
|
||||
`bill_${deal.id}_${date}.pdf`,
|
||||
);
|
||||
}}
|
||||
copied={false}
|
||||
onCopiedLabel={
|
||||
"Ссылка скопирована в буфер обмена"
|
||||
}>
|
||||
Ссылка на оплату (PDF)
|
||||
</ButtonCopyControlled>
|
||||
)}
|
||||
<ButtonCopyControlled
|
||||
onCopyClick={onCopyGuestUrlClick}
|
||||
onCopiedLabel={
|
||||
"Ссылка скопирована в буфер обмена"
|
||||
}
|
||||
copied={clipboard.copied}>
|
||||
Ссылка на редактирование
|
||||
</ButtonCopyControlled>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap={rem(10)}>
|
||||
<Checkbox
|
||||
label={"Оплачен"}
|
||||
checked={deal.billRequest?.paid || deal.group?.billRequest?.paid || false}
|
||||
disabled
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Учет выручки"}
|
||||
{...form.getInputProps("isAccounted", { type: "checkbox" })}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Завершена"}
|
||||
{...form.getInputProps("isCompleted", { type: "checkbox" })}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Удалена"}
|
||||
{...form.getInputProps("isDeleted", { type: "checkbox" })}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Divider orientation={"vertical"} />
|
||||
<Group
|
||||
align={"center"}
|
||||
justify={"center"}>
|
||||
<Button
|
||||
color={"red"}
|
||||
type={"reset"}
|
||||
disabled={isEqual(initialValues, form.values)}
|
||||
onClick={() => form.reset()}>
|
||||
Отменить изменения
|
||||
</Button>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
disabled={isEqual(initialValues, form.values)}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
const DealEditDrawerGeneralTab: FC = () => {
|
||||
const { selectedDeal } = useDealPageContext();
|
||||
if (!selectedDeal) return <>No deal selected</>;
|
||||
return <Content deal={selectedDeal} />;
|
||||
};
|
||||
export default DealEditDrawerGeneralTab;
|
||||
@@ -1,63 +0,0 @@
|
||||
import { BoardSchema, DealSummary } from "../../../client";
|
||||
import { DragStart, DropResult } from "@hello-pangea/dnd";
|
||||
import { useState } from "react";
|
||||
import DragState from "../enums/DragState.ts";
|
||||
import useDealsDnd from "../../../components/Dnd/Deals/DealsDndColumn/hooks/useDealsDnd.tsx";
|
||||
import useStatusesDnd from "../../../components/Dnd/Statuses/Statuses/hooks/useStatusesDnd.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
selectedBoard: BoardSchema | null;
|
||||
summariesRaw: DealSummary[];
|
||||
refetchSummaries: () => void;
|
||||
refetchBoards: () => void;
|
||||
}
|
||||
|
||||
const useDnd = ({
|
||||
selectedBoard,
|
||||
summariesRaw,
|
||||
refetchSummaries,
|
||||
refetchBoards,
|
||||
}: Props) => {
|
||||
const [dragState, setDragState] = useState<DragState>(DragState.DRAG_ENDED);
|
||||
|
||||
const {
|
||||
summaries,
|
||||
onDealDragEnd,
|
||||
} = useDealsDnd({
|
||||
summariesRaw,
|
||||
refetchSummaries,
|
||||
})
|
||||
|
||||
const {
|
||||
onStatusDragEnd,
|
||||
} = useStatusesDnd({
|
||||
board: selectedBoard,
|
||||
refetch: refetchBoards,
|
||||
});
|
||||
|
||||
const onDragEnd = async (result: DropResult) => {
|
||||
setDragState(DragState.DRAG_ENDED);
|
||||
if (result.draggableId.includes("status")) {
|
||||
return onStatusDragEnd(result);
|
||||
}
|
||||
return onDealDragEnd(result);
|
||||
}
|
||||
|
||||
const onDragStart = (start: DragStart) => {
|
||||
if (start.source.droppableId.includes("status")) {
|
||||
setDragState(DragState.DRAG_STATUS);
|
||||
} else {
|
||||
setDragState(DragState.DRAG_DEAL);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
summaries,
|
||||
dragState,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDnd;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { type ProjectSchemaWithCount, ProjectService } from "../../../client";
|
||||
|
||||
|
||||
const useProjects = () => {
|
||||
const [projects, setProjects] = useState<ProjectSchemaWithCount[]>([]);
|
||||
|
||||
const refetchProjects = () => {
|
||||
ProjectService.getProjects()
|
||||
.then(data => {
|
||||
setProjects(data.projects);
|
||||
})
|
||||
.catch(e => console.log(e));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetchProjects();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
projects,
|
||||
refetchProjects,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjects;
|
||||
@@ -1 +0,0 @@
|
||||
export { DealsPage } from "./ui/DealsPage.tsx";
|
||||
@@ -1,90 +0,0 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { DealProductSchema, DealProductServiceSchema } from "../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { NumberInput } from "@mantine/core";
|
||||
import ProductSelect from "../../../components/ProductSelect/ProductSelect.tsx";
|
||||
import { omit } from "lodash";
|
||||
|
||||
type RestProps = {
|
||||
clientId: number;
|
||||
productIds?: number[];
|
||||
};
|
||||
|
||||
type Props = CreateEditFormProps<DealProductSchema> & RestProps;
|
||||
const AddDealProductModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const restProps = omit(innerProps, ["clientId"]);
|
||||
|
||||
const validateServices = (services?: DealProductServiceSchema[]) => {
|
||||
if (!services || services.length == 0) return null;
|
||||
return services.find(service => service.service === undefined)
|
||||
? "Удалите пустые услуги"
|
||||
: null;
|
||||
};
|
||||
const form = useForm<Partial<DealProductSchema>>({
|
||||
initialValues: isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
product: undefined,
|
||||
services: [],
|
||||
quantity: 1,
|
||||
},
|
||||
validate: {
|
||||
product: (product?: DealProductSchema["product"]) =>
|
||||
product !== undefined ? null : "Необходимо выбрать товар",
|
||||
quantity: (quantity?: number) =>
|
||||
quantity && quantity > 0
|
||||
? null
|
||||
: "Количество должно быть больше 0",
|
||||
services: validateServices,
|
||||
},
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
context.closeContextModal(id);
|
||||
};
|
||||
return (
|
||||
<BaseFormModal
|
||||
{...(restProps as CreateEditFormProps<DealProductSchema>)}
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={onClose}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<ProductSelect
|
||||
placeholder={"Выберите товар"}
|
||||
label={"Товар"}
|
||||
clientId={innerProps.clientId}
|
||||
disabled={isEditing}
|
||||
filterBy={item =>
|
||||
!(innerProps.productIds || []).includes(item.id)
|
||||
}
|
||||
{...form.getInputProps("product")}
|
||||
/>
|
||||
<NumberInput
|
||||
placeholder={"Введите количество"}
|
||||
label={"Количество"}
|
||||
min={1}
|
||||
{...form.getInputProps("quantity")}
|
||||
/>
|
||||
{/*<Fieldset legend={'Услуги'}>*/}
|
||||
{/* <DealProductServiceTable*/}
|
||||
{/* quantity={form.values.quantity || 1}*/}
|
||||
{/* {...form.getInputProps('services') as*/}
|
||||
{/* BaseFormInputProps<DealProductServiceSchema[]>}*/}
|
||||
{/* />*/}
|
||||
{/*</Fieldset>*/}
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddDealProductModal;
|
||||
@@ -1,106 +0,0 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, { CreateEditFormProps } from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { DealServiceSchema } from "../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { ComboboxItem, ComboboxItemGroup, NumberInput, OptionsFilter } from "@mantine/core";
|
||||
import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
|
||||
import { ServiceType } from "../../../shared/enums/ServiceType.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../redux/store.ts";
|
||||
|
||||
type RestProps = {
|
||||
serviceIds?: number[];
|
||||
};
|
||||
type Props = CreateEditFormProps<Partial<DealServiceSchema>> & RestProps;
|
||||
const AddDealServiceModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
const isEditing = "element" in innerProps;
|
||||
const form = useForm<Partial<DealServiceSchema>>({
|
||||
initialValues: isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
service: undefined,
|
||||
quantity: 1,
|
||||
employees: [],
|
||||
},
|
||||
validate: {
|
||||
service: (service?: DealServiceSchema["service"]) =>
|
||||
service !== undefined ? null : "Необходимо выбрать услугу",
|
||||
quantity: (quantity?: number) =>
|
||||
quantity && quantity > 0
|
||||
? null
|
||||
: "Количество должно быть больше 0",
|
||||
},
|
||||
});
|
||||
const onClose = () => {
|
||||
context.closeContextModal(id);
|
||||
};
|
||||
|
||||
const serviceOptionsFilter = ({
|
||||
options,
|
||||
}: {
|
||||
options: ComboboxItemGroup[];
|
||||
}) => {
|
||||
if (!innerProps.serviceIds) return options;
|
||||
const productServiceIds = innerProps.serviceIds;
|
||||
return (options as ComboboxItemGroup[]).map(({ items, group }) => {
|
||||
return {
|
||||
group,
|
||||
items: items.filter(
|
||||
item =>
|
||||
!productServiceIds.includes(
|
||||
parseInt((item as ComboboxItem).value)
|
||||
)
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BaseFormModal
|
||||
{...innerProps}
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={onClose}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<ServiceWithPriceInput
|
||||
serviceProps={{
|
||||
...form.getInputProps("service"),
|
||||
label: "Услуга",
|
||||
placeholder: "Выберите услугу",
|
||||
style: { width: "100%" },
|
||||
disabled: isEditing,
|
||||
filter: serviceOptionsFilter as OptionsFilter,
|
||||
}}
|
||||
priceProps={{
|
||||
...form.getInputProps("price"),
|
||||
label: "Цена",
|
||||
placeholder: "Введите цену",
|
||||
style: { width: "100%" },
|
||||
disabled: authState.isGuest,
|
||||
}}
|
||||
quantity={form.values.quantity || 1}
|
||||
containerProps={{
|
||||
direction: "column",
|
||||
style: { width: "100%" },
|
||||
}}
|
||||
filterType={ServiceType.DEAL_SERVICE}
|
||||
lockOnEdit={isEditing}
|
||||
/>
|
||||
<NumberInput
|
||||
placeholder={"Введите количество"}
|
||||
label={"Количество"}
|
||||
min={1}
|
||||
{...form.getInputProps("quantity")}
|
||||
/>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddDealServiceModal;
|
||||
@@ -1,97 +0,0 @@
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {
|
||||
DealProductServiceSchema,
|
||||
ServiceSchema,
|
||||
} from "../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { useForm, UseFormReturnType } from "@mantine/form";
|
||||
import { isNil, isNumber } from "lodash";
|
||||
import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
|
||||
import { Checkbox, Flex, rem } from "@mantine/core";
|
||||
import { ServiceType } from "../../../shared/enums/ServiceType.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../redux/store.ts";
|
||||
|
||||
type RestProps = {
|
||||
quantity: number;
|
||||
serviceIds: number[];
|
||||
};
|
||||
type Props = CreateEditFormProps<DealProductServiceSchema> & RestProps;
|
||||
|
||||
const ProductServiceFormModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const isEditing = "onChange" in innerProps;
|
||||
const initialValues: Partial<DealProductServiceSchema> = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
service: undefined,
|
||||
price: undefined,
|
||||
employees: [],
|
||||
isFixedPrice: false,
|
||||
};
|
||||
const form = useForm<Partial<DealProductServiceSchema>>({
|
||||
initialValues,
|
||||
validate: {
|
||||
service: (service?: ServiceSchema) =>
|
||||
isNil(service) || service.id < 0 ? "Укажите услугу" : null,
|
||||
price: (price?: number) =>
|
||||
!isNumber(price) || price < 0 ? "Укажите цену" : null,
|
||||
},
|
||||
});
|
||||
const onClose = () => {
|
||||
context.closeContextModal(id);
|
||||
};
|
||||
return (
|
||||
<BaseFormModal
|
||||
{...innerProps}
|
||||
form={form as UseFormReturnType<DealProductServiceSchema>}
|
||||
onClose={onClose}
|
||||
closeOnSubmit>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Flex w={"100%"} direction={"column"} gap={rem(10)}>
|
||||
<ServiceWithPriceInput
|
||||
serviceProps={{
|
||||
...form.getInputProps("service"),
|
||||
label: "Услуга",
|
||||
placeholder: "Выберите услугу",
|
||||
disabled: isEditing,
|
||||
filterBy: item =>
|
||||
!innerProps.serviceIds.includes(item.id) ||
|
||||
isEditing,
|
||||
style: { width: "100%" },
|
||||
}}
|
||||
priceProps={{
|
||||
...form.getInputProps("price"),
|
||||
label: "Цена",
|
||||
placeholder: "Введите цену",
|
||||
style: { width: "100%" },
|
||||
disabled: authState.isGuest,
|
||||
}}
|
||||
filterType={ServiceType.PRODUCT_SERVICE}
|
||||
containerProps={{
|
||||
direction: "column",
|
||||
style: { width: "100%" },
|
||||
}}
|
||||
lockOnEdit={isEditing}
|
||||
quantity={innerProps.quantity}
|
||||
/>
|
||||
<Checkbox
|
||||
{...form.getInputProps("isFixedPrice", { type: "checkbox" })}
|
||||
label={"Зафиксировать цену"}
|
||||
placeholder={"Зафиксировать цену"}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
export default ProductServiceFormModal;
|
||||
@@ -1,85 +0,0 @@
|
||||
import { type ProjectSchemaWithCount } from "../../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { ActionIcon, Flex, rem, Stack, TextInput, Tooltip } from "@mantine/core";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useProjectsTableColumns from "./hooks/projectsTableColumns.tsx";
|
||||
import { IconCheck, IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import InlineButton from "../../../../components/InlineButton/InlineButton.tsx";
|
||||
import useProjectModal from "./hooks/useProjectModal.tsx";
|
||||
|
||||
type Props = {
|
||||
onUpdate: () => void;
|
||||
};
|
||||
|
||||
const ProjectsModal = ({ innerProps }: ContextModalProps<Props>) => {
|
||||
const {
|
||||
projects,
|
||||
name,
|
||||
setName,
|
||||
editingProjects,
|
||||
handleEditClick,
|
||||
handleDeleteClick,
|
||||
handleCreateClick,
|
||||
} = useProjectModal(innerProps);
|
||||
|
||||
const columns = useProjectsTableColumns({ editingProjects });
|
||||
|
||||
return (
|
||||
<Stack gap={rem(10)}>
|
||||
<TextInput
|
||||
label={"Добавить проект"}
|
||||
variant={"default"}
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
<InlineButton
|
||||
variant={"default"}
|
||||
onClick={handleCreateClick}>
|
||||
<IconPlus />
|
||||
Добавить
|
||||
</InlineButton>
|
||||
|
||||
<BaseTable
|
||||
data={projects}
|
||||
columns={columns}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
|
||||
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<ProjectSchemaWithCount>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsModal;
|
||||
@@ -1,100 +0,0 @@
|
||||
import { BaseProjectSchema, ProjectSchema, ProjectService } from "../../../../../client";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { useMap } from "@mantine/hooks";
|
||||
import { useState } from "react";
|
||||
import useProjects from "../../../hooks/useProjects.tsx";
|
||||
|
||||
type Props = {
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
const useProjectModal = ({ onUpdate }: Props) => {
|
||||
const editingProjects = useMap<number, ProjectSchema>();
|
||||
const { projects, refetchProjects } = useProjects();
|
||||
const [name, setName] = useState("");
|
||||
|
||||
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 createProject = (project: BaseProjectSchema) => {
|
||||
ProjectService.createProject({
|
||||
requestBody: { project },
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
setName("");
|
||||
refetchProjects();
|
||||
onUpdate();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
if (name.length === 0) {
|
||||
notifications.error({ message: "Имя проекта не может быть пустым" });
|
||||
return;
|
||||
}
|
||||
createProject({ name });
|
||||
};
|
||||
|
||||
return {
|
||||
projects,
|
||||
name,
|
||||
setName,
|
||||
editingProjects,
|
||||
handleEditClick,
|
||||
handleDeleteClick,
|
||||
handleCreateClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjectModal;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user