feat: projects and boards

This commit is contained in:
2025-02-07 20:07:10 +04:00
parent 58d397ed0b
commit 580552bd47
185 changed files with 3352 additions and 1284 deletions

View File

@@ -1,6 +1,6 @@
import { useParams } from "@tanstack/react-router";
import { DealPageContextProvider, useDealPageContext } from "../../LeadsPage/contexts/DealPageContext.tsx";
import ProductAndServiceTab from "../../LeadsPage/tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
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";

View File

@@ -1,10 +1,6 @@
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { DealStatusHistorySchema } from "../../../../client";
import {
DealStatus,
DealStatusDictionary,
} from "../../../../shared/enums/DealStatus.ts";
import { Spoiler, Text } from "@mantine/core";
export const useDealStatusChangeTableColumns = () => {
@@ -22,16 +18,12 @@ export const useDealStatusChangeTableColumns = () => {
`${row.user.firstName} ${row.user.secondName}`,
},
{
accessorKey: "fromStatus",
accessorKey: "fromStatus.name",
header: "Из статуса",
accessorFn: row =>
DealStatusDictionary[row.fromStatus as DealStatus],
},
{
accessorKey: "toStatus",
accessorKey: "toStatus.name",
header: "В статус",
accessorFn: row =>
DealStatusDictionary[row.toStatus as DealStatus],
},
{
accessorKey: "comment",

View File

@@ -1,22 +0,0 @@
import ObjectSelect, {
ObjectSelectProps,
} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
import { FC } from "react";
import { DealStatuses } from "../../../../shared/enums/DealStatus.ts";
type DealStatus = {
name: string;
id: number;
};
type Props = Omit<ObjectSelectProps<DealStatus>, "data">;
const DealStatusSelect: FC<Props> = props => {
const data: DealStatus[] = DealStatuses;
return (
<ObjectSelect
data={data}
{...props}
/>
);
};
export default DealStatusSelect;

View File

@@ -6,7 +6,7 @@ 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 "../../../LeadsPage/contexts/DealPageContext.tsx";
import { useDealPageContext } from "../../contexts/DealPageContext.tsx";
type RestProps = {
viewOnly?: boolean;

View File

@@ -42,15 +42,6 @@ const useDealsTableColumns = () => {
header: "Клиент",
enableSorting: false,
},
{
Cell: ({ row }) =>
new Date(row.original.deadline || 0).toLocaleString("ru-RU"),
accessorKey: "deadline",
header: "Дедлайн",
sortingFn: (rowA, rowB) =>
new Date(rowB.original.deadline || 0).getTime() -
new Date(rowA.original.deadline || 0).getTime(),
},
{
header: "Общая стоимость",
Cell: ({ row }) =>

View File

@@ -0,0 +1,127 @@
import { ActionIcon, Flex, rem, Text } from "@mantine/core";
import { IconEdit, IconMenu2, IconMenuDeep } from "@tabler/icons-react";
import { motion } from "framer-motion";
import styles from "../../ui/DealsPage.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 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";
type Props = {
displayMode: DisplayMode;
setDisplayMode: React.Dispatch<React.SetStateAction<DisplayMode>>;
form: UseFormReturnType<DealsPageState>;
projects: ProjectSchema[];
refetchProjects: () => void;
}
const LeadsPageHeader = ({
displayMode,
setDisplayMode,
form,
projects,
refetchProjects,
}: Props) => {
const openModal = () => {
modals.openContextModal({
modal: "projectsModal",
title: "Проекты",
innerProps: {
onUpdate: 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>
<ObjectSelect
placeholder={"Выберите проект"}
data={projects}
{...form.getInputProps("project")}
/>
</div>
);
};
const getHeaderInputsTable = () => {
return (
<div
className={styles["top-panel"]}
style={{
display: displayMode === DisplayMode.TABLE ? "flex" : "none",
}}
>
<DealsTableFiltersModal
form={form}
projects={projects}
/>
</div>
);
};
return (
<PageBlock style={{ flex: 0 }}>
<Flex
align={"center"}
justify={"space-between"}>
<Flex
gap={rem(10)}
direction={"column"}
align={"center"}>
<Text size={"xs"}>Вид</Text>
<Flex gap={rem(10)}>
<ActionIcon
onClick={() =>
setDisplayMode(DisplayMode.BOARD)
}
variant={
displayMode === DisplayMode.BOARD
? "filled"
: "default"
}>
<IconMenuDeep
style={{ rotate: "-90deg" }}
/>
</ActionIcon>
<ActionIcon
onClick={() =>
setDisplayMode(DisplayMode.TABLE)
}
variant={
displayMode === DisplayMode.TABLE
? "filled"
: "default"
}>
<IconMenu2 />
</ActionIcon>
</Flex>
</Flex>
<motion.div
key={displayMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
{getHeaderInputsTable()}
{getHeaderInputsBoard()}
</motion.div>
</Flex>
</PageBlock>
);
};
export default LeadsPageHeader;

View File

@@ -3,7 +3,7 @@ import { useDisclosure } from "@mantine/hooks";
import { DealsWithExcelForm, ProductExcelData } from "../drawers/PrefillDealWithExcelDrawer/types.tsx";
import { FileWithPath } from "@mantine/dropzone";
import { notifications } from "../../../shared/lib/notifications.ts";
import { DealService, type ProductFromExcelSchema, ProductSchema } from "../../../client";
import { DealService, type ProductFromExcelSchema, ProductSchema, StatusSchema } from "../../../client";
import UseExcelDropzone from "../../../types/UseExcelDropzone.tsx";
import { useForm, UseFormReturnType } from "@mantine/form";
import { useDealPageContext } from "./DealPageContext.tsx";
@@ -16,7 +16,7 @@ type PrefillDealsWithExcelContextState = {
onProductSelectChange: (barcode: string, selectedProduct: ProductSchema) => void,
onDrop: (files: FileWithPath[]) => void;
excelDropzone: UseExcelDropzone;
createDeals: (values: DealsWithExcelForm) => void;
createDeals: (values: DealsWithExcelForm, status: StatusSchema) => void;
form: UseFormReturnType<DealsWithExcelForm>;
errors: string[];
};
@@ -24,6 +24,7 @@ type PrefillDealsWithExcelContextState = {
const PrefillDealsWithExcelContext = createContext<PrefillDealsWithExcelContextState | undefined>(
undefined,
);
const usePrefillDealsWithExcelContextState = () => {
const [prefillWithExcelOpened, { open, close }] = useDisclosure(false);
const { refetchDeals } = useDealPageContext();
@@ -90,7 +91,7 @@ const usePrefillDealsWithExcelContextState = () => {
form.reset();
};
const createDeals = (values: DealsWithExcelForm) => {
const createDeals = (values: DealsWithExcelForm, status: StatusSchema) => {
const products: ProductFromExcelSchema[] = barcodeProductsMap.entries().map(([, productData]) => {
return {
productId: productData.selectedProduct!.id,
@@ -102,6 +103,7 @@ const usePrefillDealsWithExcelContextState = () => {
requestBody: {
products,
clientId: values.client?.id ?? -1,
statusId: status.id,
},
})
.then(({ ok, message }) => {

View File

@@ -1,4 +1,4 @@
import { FC } from "react";
import { FC, useState } from "react";
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
import {
ActionIcon,
@@ -14,8 +14,14 @@ import {
Tooltip,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { ClientService, DealSchema, DealService, ShippingWarehouseSchema } from "../../../../../client";
import { DealStatus, DealStatusDictionary } from "../../../../../shared/enums/DealStatus.ts";
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";
@@ -27,10 +33,13 @@ 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/LeadsPage.module.css";
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;
@@ -42,6 +51,7 @@ 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
@@ -61,6 +71,8 @@ const Content: FC<Props> = ({ deal }) => {
value.length > 0
? null
: "Название сделки не может быть пустым",
status: (value: StatusSchema) =>
!value && "Статус для сделки не выбран",
},
});
const updateDealInfo = async (values: DealGeneralFormType) => {
@@ -147,15 +159,20 @@ const Content: FC<Props> = ({ deal }) => {
"ru-RU",
)}
/>
<TextInput
disabled
placeholder={"Текущий статус"}
label={"Текущий статус"}
value={
DealStatusDictionary[
deal.currentStatus as DealStatus
]
}
<ProjectSelect
value={project}
onChange={setProject}
label={"Проект"}
/>
<BoardSelect
project={project}
{...form.getInputProps("board")}
label={"Доска"}
/>
<DealStatusSelect
board={form.values.board}
{...form.getInputProps("status")}
label={"Статус"}
/>
{deal.category && (
<TextInput

View File

@@ -3,8 +3,13 @@ import ExcelDropzone from "../../../../components/ExcelDropzone/ExcelDropzone.ts
import styles from "../PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.module.css";
import { usePrefillDealsWithExcelContext } from "../../contexts/PrefillDealsWithExcelContext.tsx";
import ProductsPreview from "./components/ProductsPreview.tsx";
import { BoardSchema } from "../../../../client";
const PrefillDealsWithExcelDrawer = () => {
type Props = {
board: BoardSchema | null;
}
const PrefillDealsWithExcelDrawer = ({ board }: Props) => {
const {
prefillWithExcelOpened,
prefillWithExcelOnClose,
@@ -14,10 +19,12 @@ const PrefillDealsWithExcelDrawer = () => {
} = usePrefillDealsWithExcelContext();
const getBody = () => {
if (!board || board.dealStatuses.length === 0) return;
if (barcodeProductsMap?.size === 0) {
return <ExcelDropzone dropzone={excelDropzone} onDrop={onDrop} />;
}
return <ProductsPreview />;
return <ProductsPreview status={board.dealStatuses[0]}/>;
};
return (

View File

@@ -6,8 +6,13 @@ import { ProductExcelData } from "../types.tsx";
import BreakdownByCityTable from "./BreakdownByCityTable.tsx";
import ClientSelect from "../../../../../components/Selects/ClientSelect/ClientSelect.tsx";
import ParsingResultsTooltip from "./ParsingResultsTooltip.tsx";
import { StatusSchema } from "../../../../../client";
const ProductsPreview = () => {
type Props = {
status: StatusSchema;
}
const ProductsPreview = ({ status }: Props) => {
const { barcodeProductsMap, createDeals, form } = usePrefillDealsWithExcelContext();
const getTitle = (barcode: string, productsData: ProductExcelData) => {
@@ -41,7 +46,7 @@ const ProductsPreview = () => {
return (
<Stack gap={"md"}>
<Title order={3}>Предпросмотр</Title>
<form onSubmit={form.onSubmit((values) => createDeals(values))}>
<form onSubmit={form.onSubmit((values) => createDeals(values, status))}>
<ClientSelect
{...form.getInputProps("client")}
inputContainer={(children) => (

View File

@@ -0,0 +1,6 @@
enum DisplayMode {
BOARD,
TABLE,
}
export default DisplayMode;

View File

@@ -0,0 +1,7 @@
enum DragState {
DRAG_ENDED,
DRAG_DEAL,
DRAG_STATUS,
}
export default DragState;

View File

@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import { BoardSchema, BoardService } from "../../../client";
type Props = {
projectId?: number;
}
const useBoards = ({ projectId }: Props) => {
const [boards, setBoards] = useState<BoardSchema[]>([]);
const refetchBoards = () => {
if (!projectId) return;
BoardService.getBoards({
projectId,
})
.then(data => {
setBoards(data.boards);
})
.catch(e => console.log(e));
};
useEffect(() => {
refetchBoards();
}, [projectId]);
return {
boards,
refetchBoards,
};
};
export default useBoards;

View File

@@ -1,53 +1,90 @@
import { useDealSummariesFull } from "../../LeadsPage/hooks/useDealSummaries.tsx";
import { useDealSummariesFull } from "./useDealSummaries.tsx";
import { useForm } from "@mantine/form";
import { useEffect, useState } from "react";
import { BaseMarketplaceSchema, ClientSchema } from "../../../client";
import { BaseMarketplaceSchema, BoardSchema, ClientSchema, ProjectSchema } from "../../../client";
import { DealStatusType } from "../../../shared/enums/DealStatus.ts";
type State = {
type Props = {
projects: ProjectSchema[];
}
export type DealsPageState = {
id: number | null;
marketplace: BaseMarketplaceSchema | null;
dealStatus: DealStatusType | null;
client: ClientSchema | null;
project: ProjectSchema | null;
projectForTable: ProjectSchema | null;
board: BoardSchema | null;
dealStatus: DealStatusType | null;
};
const useDealsPageState = () => {
const useDealsPageState = ({ projects }: Props) => {
const { objects } = useDealSummariesFull();
const form = useForm<State>({
const form = useForm<DealsPageState>({
initialValues: {
project: null,
id: null,
marketplace: null,
dealStatus: null,
client: null,
projectForTable: null,
board: null,
dealStatus: null,
},
});
const [data, setData] = useState(objects);
const applyFilters = () => {
let result = objects;
if (form.values.id) {
result = result.filter(
obj => obj.id === form.values.id
)
obj => obj.id === form.values.id,
);
}
if (form.values.marketplace) {
result = result.filter(
obj => obj.baseMarketplace?.key === form.values.marketplace?.key
obj => obj.baseMarketplace?.key === form.values.marketplace?.key,
);
}
if (form.values.dealStatus) {
if (form.values.projectForTable) {
result = result.filter(
obj => obj.status === form.values.dealStatus?.id
obj => obj.board.projectId === form.values.project?.id,
);
if (form.values.board) {
result = result.filter(
obj => obj.board.id === form.values.board?.id,
);
if (form.values.dealStatus) {
result = result.filter(
obj => obj.status.id === form.values.dealStatus?.id,
);
}
}
}
if (form.values.client) {
result = result.filter(
obj => obj.clientName === form.values.client?.name
obj => obj.clientName === form.values.client?.name,
);
}
setData(result);
};
useEffect(() => {
applyFilters();
}, [form.values, objects]);
useEffect(() => {
if (projects.length > 0 && form.values.project === null) {
form.setFieldValue("project", projects[0]);
}
}, [projects]);
return { data, form };
};

View File

@@ -0,0 +1,63 @@
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;

View File

@@ -0,0 +1,26 @@
import { useEffect, useState } from "react";
import { ProjectSchema, ProjectService } from "../../../client";
const useProjects = () => {
const [projects, setProjects] = useState<ProjectSchema[]>([]);
const refetchProjects = () => {
ProjectService.getProjects()
.then(data => {
setProjects(data.projects);
})
.catch(e => console.log(e));
};
useEffect(() => {
refetchProjects();
}, []);
return {
projects,
refetchProjects,
};
};
export default useProjects;

View File

@@ -0,0 +1,46 @@
import { ContextModalProps } from "@mantine/modals";
import { Button, Stack, TextInput } from "@mantine/core";
import { BoardSchema } from "../../../../client";
import useBoardModal from "./hooks/useBoardModal.tsx";
type Props = {
projectId: number;
board?: BoardSchema;
refetchBoards: () => void;
};
const BoardModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const closeModal = () => context.closeContextModal(id);
const {
form,
onSubmit,
} = useBoardModal({
...innerProps,
closeModal,
});
return (
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
<Stack>
<TextInput
{...form.getInputProps("name")}
label={"Название"}
placeholder={"Введите название"}
/>
<Button
type={"submit"}
variant={"default"}
>
Сохранить
</Button>
</Stack>
</form>
);
};
export default BoardModal;

View File

@@ -0,0 +1,87 @@
import { useForm } from "@mantine/form";
import { BoardSchema, BoardService } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts";
type BoardForm = {
name: string;
}
type Props = {
projectId: number;
board?: BoardSchema;
refetchBoards: () => void;
closeModal: () => void;
};
const useBoardModal = ({
projectId,
board,
refetchBoards,
closeModal,
}: Props) => {
const form = useForm<BoardForm>({
initialValues: {
name: board ? board.name : "",
},
validate: {
name: name => !name && "Необходимо ввести название доски",
},
});
const createBoard = (values: BoardForm) => {
BoardService.createBoard({
requestBody: {
board: {
projectId: projectId,
name: values.name,
},
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
refetchBoards();
closeModal();
})
.catch(err => console.log(err));
};
const updateBoard = (values: BoardForm) => {
if (!board) return;
BoardService.updateBoard({
requestBody: {
board: {
...board,
name: values.name,
},
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
refetchBoards();
closeModal();
})
.catch(err => console.log(err));
};
const onSubmit = (values: BoardForm) => {
if (board) {
updateBoard(values);
return;
}
createBoard(values);
};
return {
form,
onSubmit,
};
};
export default useBoardModal;

View File

@@ -0,0 +1,78 @@
import { ProjectSchema } from "../../../client";
import { Flex, Modal, NumberInput, rem } from "@mantine/core";
import { UseFormReturnType } from "@mantine/form";
import { DealsPageState } from "../hooks/useDealsPageState.tsx";
import ObjectSelect from "../../../components/ObjectSelect/ObjectSelect.tsx";
import DealStatusSelect from "../../../components/DealStatusSelect/DealStatusSelect.tsx";
import BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import { useDisclosure } from "@mantine/hooks";
import InlineButton from "../../../components/InlineButton/InlineButton.tsx";
import { IconFilter } from "@tabler/icons-react";
import BoardSelect from "../../../components/BoardSelect/BoardSelect.tsx";
type Props = {
form: UseFormReturnType<DealsPageState>;
projects: ProjectSchema[];
};
const DealsTableFiltersModal = ({ form, projects }: Props) => {
const [opened, { open, close }] = useDisclosure();
return (
<>
<InlineButton onClick={open}>
<IconFilter />
Фильтры
</InlineButton>
<Modal title={"Фильтры для сделок"} opened={opened} onClose={close}>
<Flex
direction={"column"}
gap={rem(10)}
>
<NumberInput
min={1}
placeholder={"Введите номер"}
{...form.getInputProps("id")}
hideControls
/>
<ObjectSelect
placeholder={"Выберите проект"}
data={projects}
clearable
searchable
{...form.getInputProps("projectForTable")}
onClear={() => form.setFieldValue("projectForTable", null)}
/>
<BoardSelect
project={form.values.projectForTable}
{...form.getInputProps("board")}
clearable
/>
<DealStatusSelect
board={form.values.board}
{...form.getInputProps("dealStatus")}
clearable
/>
<BaseMarketplaceSelect
onClear={() => form.setFieldValue("marketplace", null)}
clearable
placeholder={"Выберите маркетплейс"}
{...form.getInputProps("marketplace")}
/>
<ClientSelectNew
onClear={() =>
form.setFieldValue("client", null)
}
clearable
searchable
placeholder={"Выберите клиента"}
{...form.getInputProps("client")}
/>
</Flex>
</Modal>
</>
);
};
export default DealsTableFiltersModal;

View File

@@ -0,0 +1,85 @@
import { ProjectSchema } 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<ProjectSchema>
}
/>
</Stack>
);
};
export default ProjectsModal;

View File

@@ -0,0 +1,46 @@
import { useMemo } from "react";
import { MRT_ColumnDef } from "mantine-react-table";
import { BaseProjectSchema, ProjectSchema } from "../../../../../client";
import { TextInput } from "@mantine/core";
type Props = {
editingProjects: Map<number, ProjectSchema | BaseProjectSchema>;
}
const useProjectsTableColumns = ({ editingProjects }: Props) => {
return useMemo<MRT_ColumnDef<ProjectSchema>[]>(
() => [
{
header: "Название",
accessorKey: "name",
Cell: ({ row }) => {
if (editingProjects.has(row.original.id)) {
return (
<TextInput
variant={"default"}
value={editingProjects.get(row.original.id)?.name}
onChange={e => {
const project = editingProjects.get(row.original.id);
if (!project) return;
project.name = e.target.value;
editingProjects.set(row.original.id, project);
}}
/>
);
}
return row.original.name;
},
size: 25,
},
{
header: "Кол-во досок",
accessorKey: "boardsCount",
size: 10,
},
],
[],
);
};
export default useProjectsTableColumns;

View File

@@ -0,0 +1,100 @@
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;

View File

@@ -0,0 +1,46 @@
import { ContextModalProps } from "@mantine/modals";
import { Button, Stack, TextInput } from "@mantine/core";
import { StatusSchema } from "../../../../client";
import useStatusModal from "./hooks/useStatusModal.tsx";
type Props = {
boardId?: number;
status?: StatusSchema;
refetch: () => void;
};
const StatusModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const closeModal = () => context.closeContextModal(id);
const {
form,
onSubmit,
} = useStatusModal({
...innerProps,
closeModal,
});
return (
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
<Stack>
<TextInput
{...form.getInputProps("name")}
label={"Название"}
placeholder={"Введите название"}
/>
<Button
type={"submit"}
variant={"default"}
>
Сохранить
</Button>
</Stack>
</form>
);
};
export default StatusModal;

View File

@@ -0,0 +1,88 @@
import { useForm } from "@mantine/form";
import { StatusSchema, StatusService } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts";
type StatusForm = {
name: string;
}
type Props = {
boardId?: number;
status?: StatusSchema;
refetch: () => void;
closeModal: () => void;
};
const useStatusModal = ({
boardId,
status,
refetch,
closeModal,
}: Props) => {
const form = useForm<StatusForm>({
initialValues: {
name: status ? status.name : "",
},
validate: {
name: name => !name && "Необходимо ввести название статуса",
},
});
const createBoard = (values: StatusForm) => {
if (!boardId) return;
StatusService.createStatus({
requestBody: {
status: {
boardId,
name: values.name,
},
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
refetch();
closeModal();
})
.catch(err => console.log(err));
};
const updateBoard = (values: StatusForm) => {
if (!status) return;
StatusService.updateStatus({
requestBody: {
status: {
...status,
name: values.name,
},
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
refetch();
closeModal();
})
.catch(err => console.log(err));
};
const onSubmit = (values: StatusForm) => {
if (status) {
updateBoard(values);
return;
}
createBoard(values);
};
return {
form,
onSubmit,
};
};
export default useStatusModal;

View File

@@ -1,25 +1,52 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
gap: rem(10);
flex-direction: column;
height: 100%;
}
.body-container {
.statuses {
margin-top: 1rem;
flex: 1;
display: flex;
gap: 0.5rem;
justify-content: left;
/*background-color: rebeccapurple;*/
padding-right: 3%;
padding-left: 3%;
}
.header-statuses {
display: flex;
align-items: stretch;
text-align: center;
flex-direction: column;
}
.delete {
@mixin light {
border-color: var(--mantine-color-gray-1);
}
@mixin dark {
border-color: var(--mantine-color-dark-5);
}
border: dashed var(--item-border-size) var(--mantine-color-default-border);
border-radius: var(--item-border-radius);
padding: rem(30);
text-align: center;
}
.delete-hidden {
border: none;
}
.top-panel {
padding: rem(5);
gap: rem(10);
display: flex;
align-items: center;
}
.table-container {
display: flex;
gap: rem(10);
flex-direction: column;
}
.table-pagination {
align-self: flex-end;
.print-deals-button {
align-self: center;
}

View File

@@ -1,61 +1,108 @@
import { FC } from "react";
import { FC, useState } from "react";
import { useDealSummaries } from "../hooks/useDealSummaries.tsx";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import styles from "./DealsPage.module.css";
import DealStatusSelect from "../components/DealStatusSelect/DealStatusSelect.tsx";
import DealsTable from "../components/DealsTable/DealsTable.tsx";
import BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import { DealPageContextProvider } from "../../LeadsPage/contexts/DealPageContext.tsx";
import DealEditDrawer from "../../LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx";
import DealEditDrawer from "../drawers/DealEditDrawer/DealEditDrawer.tsx";
import { DealPageContextProvider } from "../contexts/DealPageContext.tsx";
import { rem } from "@mantine/core";
import useDealsPageState from "../hooks/useDealsPageState.tsx";
import DealsTable from "../components/DealsTable/DealsTable.tsx";
import { motion } from "framer-motion";
import DealPrefillDrawer from "../drawers/DealPrefillDrawer/DealPrefillDrawer.tsx";
import { PrefillDealContextProvider } from "../contexts/PrefillDealContext.tsx";
import { useParams } from "@tanstack/react-router";
import { PrefillDealsWithExcelContextProvider } from "../contexts/PrefillDealsWithExcelContext.tsx";
import DisplayMode from "../enums/DisplayMode.ts";
import LeadsPageHeader from "../components/LeadsPageHeader/LeadsPageHeader.tsx";
import useProjects from "../hooks/useProjects.tsx";
import Boards from "../../../components/Dnd/Boards/Boards/Boards.tsx";
import useBoards from "../hooks/useBoards.tsx";
export const DealsPage: FC = () => {
const { data, form } = useDealsPageState();
return (
<>
<DealPageContextProvider refetchDeals={async () => {
const { projects, refetchProjects } = useProjects();
const { data, form } = useDealsPageState({ projects });
const { boards, refetchBoards } = useBoards({ projectId: form.values.project?.id });
const { dealId } = useParams({ strict: false });
const { summariesRaw, refetch: refetchSummaries } = useDealSummaries();
}}>
<div className={styles["container"]}>
<PageBlock>
<div className={styles["top-panel"]}>
<DealStatusSelect
onClear={() =>
form.setFieldValue("dealStatus", null)
}
clearable
placeholder={"Выберите статус "}
{...form.getInputProps("dealStatus")}
/>
<BaseMarketplaceSelect
onClear={() =>
form.setFieldValue("marketplace", null)
}
clearable
placeholder={"Выберите маркетплейс"}
{...form.getInputProps("marketplace")}
/>
<ClientSelectNew
onClear={() =>
form.setFieldValue("client", null)
}
clearable
searchable
placeholder={"Выберите клиента"}
{...form.getInputProps("client")}
/>
</div>
</PageBlock>
<PageBlock>
<div className={styles["body-container"]}>
<div className={styles["table-container"]}>
<DealsTable items={data} />
</div>
</div>
</PageBlock>
</div>
<DealEditDrawer />
const [displayMode, setDisplayMode] = useState<DisplayMode>(
DisplayMode.BOARD,
);
const getTableBody = () => {
return (
<DealsTable items={data} />
);
};
const getBoardsBody = () => {
return (
<Boards
summariesRaw={summariesRaw}
refetchSummaries={refetchSummaries}
boards={boards}
refetchBoards={refetchBoards}
project={form.values.project}
/>
);
};
const getBody = () => {
return (
<motion.div
key={displayMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
{displayMode === DisplayMode.TABLE
? getTableBody()
: getBoardsBody()
}
</motion.div>
);
};
return (
<PageBlock
fullHeight
style={{
gap: rem(10),
display: "flex",
flexDirection: "column",
backgroundColor: "transparent",
boxShadow: "none",
padding: 0,
}}
>
<DealPageContextProvider
defaultDealId={(dealId && parseInt(dealId)) || undefined}
refetchDeals={async () => {
await refetchSummaries();
}}
>
<PrefillDealContextProvider>
<PrefillDealsWithExcelContextProvider>
<LeadsPageHeader
form={form}
displayMode={displayMode}
setDisplayMode={setDisplayMode}
projects={projects}
refetchProjects={refetchProjects}
/>
<PageBlock
style={{
display: "flex",
flexDirection: "column",
flex: 1,
height: "100%",
}}
>
{getBody()}
</PageBlock>
<DealEditDrawer />
<DealPrefillDrawer />
</PrefillDealsWithExcelContextProvider>
</PrefillDealContextProvider>
</DealPageContextProvider>
</>
</PageBlock>
);
};

View File

@@ -1 +0,0 @@
export { LeadsPage } from "./ui/LeadsPage.tsx";

View File

@@ -1,44 +0,0 @@
.container {
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
}
.boards {
margin-top: 1rem;
flex: 1;
display: flex;
gap: 0.5rem;
justify-content: center;
/*background-color: rebeccapurple;*/
padding-right: 5%;
padding-left: 5%;
}
.delete {
@mixin light {
border-color: var(--mantine-color-gray-1);
}
@mixin dark {
border-color: var(--mantine-color-dark-5);
}
border: dashed var(--item-border-size) var(--mantine-color-default-border);
border-radius: var(--item-border-radius);
padding: rem(30);
text-align: center;
}
.delete-hidden {
border: none;
}
.top-panel {
padding: rem(5);
gap: rem(10);
display: flex;
}
.print-deals-button {
align-self: center;
}

View File

@@ -1,508 +0,0 @@
import { FC, useEffect, useState } from "react";
import styles from "./LeadsPage.module.css";
import Board from "../../../components/Dnd/Board/Board.tsx";
import { DragDropContext, Droppable, DropResult } from "@hello-pangea/dnd";
import { useDealSummaries } from "../hooks/useDealSummaries.tsx";
import { DealStatus, getDealStatusByName } from "../../../shared/enums/DealStatus.ts";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import DealEditDrawer from "../drawers/DealEditDrawer/DealEditDrawer.tsx";
import { DealPageContextProvider } from "../contexts/DealPageContext.tsx";
import { modals } from "@mantine/modals";
import { DealService, DealSummaryReorderRequest } from "../../../client";
import { ActionIcon, Flex, NumberInput, rem, Text } from "@mantine/core";
import classNames from "classnames";
import { notifications } from "../../../shared/lib/notifications.ts";
import { IconMenu2, IconMenuDeep } from "@tabler/icons-react";
import useDealsPageState from "../../DealsPage/hooks/useDealsPageState.tsx";
import DealStatusSelect from "../../DealsPage/components/DealStatusSelect/DealStatusSelect.tsx";
import BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import DealsTable from "../../DealsPage/components/DealsTable/DealsTable.tsx";
import { motion } from "framer-motion";
import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
import DealPrefillDrawer from "../drawers/DealPrefillDrawer/DealPrefillDrawer.tsx";
import { PrefillDealContextProvider } from "../contexts/PrefillDealContext.tsx";
import { useParams } from "@tanstack/react-router";
import { PrefillDealsWithExcelContextProvider } from "../contexts/PrefillDealsWithExcelContext.tsx";
import PrefillDealsWithExcelDrawer from "../drawers/PrefillDealWithExcelDrawer/PrefillDealsWithExcelDrawer.tsx";
enum DisplayMode {
BOARD,
TABLE,
}
export const LeadsPage: FC = () => {
const { data, form } = useDealsPageState();
const { dealId } = useParams({ strict: false });
const { summariesRaw, refetch } = useDealSummaries();
const [summaries, setSummaries] = useState(summariesRaw);
const [displayMode, setDisplayMode] = useState<DisplayMode>(
DisplayMode.BOARD,
);
const [isDragEnded, setIsDragEnded] = useState(true);
// const [selectedDeals, setSelectedDeals] = useState<DealSummary[]>([]);
useEffect(() => {
setSummaries(summariesRaw);
}, [summariesRaw]);
const recalculate = async (dealId: number) => {
return DealService.recalculateDealPrice({
requestBody: {
dealId: dealId,
},
}).then(({ ok, message }) => {
notifications.guess(ok, { message });
});
};
const onDelete = (dealId: number) => {
const summary = summaries.find(summary => summary.id == dealId);
if (!summary) return;
modals.openConfirmModal({
title: "Удаление сделки",
children: (
<Flex>
Вы действительно хотите удалить сделку {summary.name}?
</Flex>
),
onConfirm: () => {
DealService.deleteDeal({
requestBody: { dealId: dealId },
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await refetch();
});
},
labels: {
confirm: "Удалить",
cancel: "Отмена",
},
});
};
const onSuccess = (dealId: number) => {
const summary = summaries.find(summary => summary.id == dealId);
if (!summary) return;
modals.openConfirmModal({
title: "Завершение сделки",
children: (
<Flex>
Вы действительно хотите завершить сделку {summary.name}?
</Flex>
),
onConfirm: () => {
DealService.completeDeal({
requestBody: { dealId: dealId },
}).then(async ({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
await refetch();
});
},
labels: {
confirm: "Завершить",
cancel: "Отмена",
},
});
};
const onCombine = async (result: DropResult) => {
if (!result.combine) return;
const destination = result.combine.draggableId;
const source = result.draggableId;
if (!destination || !source) return;
const sourceId = parseInt(source);
if (destination.includes("group")) {
const groupId = parseInt(destination.split("-")[1]);
DealService.addDealToGroup({
requestBody: {
dealId: sourceId,
groupId: groupId,
},
}).then(async response => {
if (!response.ok) {
notifications.error({ message: response.message });
return;
}
await refetch();
await recalculate(sourceId);
await refetch();
});
} else {
const destinationId = parseInt(destination);
// creating new group
DealService.createDealGroup({
requestBody: {
draggingDealId: sourceId,
hoveredDealId: destinationId,
},
}).then(async response => {
if (!response.ok) {
notifications.error({ message: response.message });
return;
}
await refetch();
await recalculate(sourceId);
await refetch();
});
return;
}
};
const moveGroup = async (result: DropResult) => {
const groupId = parseInt(result.draggableId.split("-")[1]);
const destination = result.destination?.droppableId;
if (!destination) return;
const status = getDealStatusByName(destination);
DealService.changeDealGroupStatus({
requestBody: {
groupId: groupId,
newStatus: status,
},
}).then(async response => {
if (!response.ok) {
notifications.error({ message: response.message });
return;
}
await refetch();
});
};
const onDragEnd = async (result: DropResult) => {
if (result.combine) {
return onCombine(result);
}
setIsDragEnded(true);
// If there is no changes
if (!result.destination || result.destination == result.source) return;
// Checking for valid dealId
if (result.draggableId.includes("group")) {
return moveGroup(result);
}
const dealId = parseInt(result.draggableId);
if (isNaN(dealId)) return;
// Checking for valid deal
const summary = summaries.find(summary => summary.id == dealId);
if (!summary) return;
// Checking if it is custom actions
const droppableId = result.destination.droppableId;
if (droppableId === "DELETE") {
onDelete(dealId);
return;
}
if (droppableId === "SUCCESS") {
onSuccess(dealId);
return;
}
const status = getDealStatusByName(droppableId);
const request: Partial<DealSummaryReorderRequest> = {
dealId: dealId,
index: result.destination.index,
status: status,
};
if (status == summary.status) {
DealService.reorderDealSummaries({
requestBody: request as DealSummaryReorderRequest,
}).then(async response => {
setSummaries(response.summaries);
await refetch();
});
return;
}
DealService.reorderDealSummaries({
requestBody: {
dealId: dealId,
status: status,
index: result.destination.index,
comment: "",
deadline: dateWithoutTimezone(new Date()),
},
}).then(async response => {
setSummaries(response.summaries);
await refetch();
});
};
const getTableBody = () => {
return (
<motion.div
key={displayMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
<DealsTable items={data}
// onSelectionChange={setSelectedDeals}
/>
</motion.div>
);
};
const getBoardBody = () => {
return (
<motion.div
style={{
display: "flex",
height: "100%",
flex: 1,
}}
key={displayMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
<DragDropContext
onDragStart={() => {
setIsDragEnded(false);
}}
onDragEnd={onDragEnd}>
<Flex
justify={"space-between"}
direction={"column"}
style={{ flex: 1 }}>
<div className={styles["boards"]}>
<Board
withCreateButton
summaries={summaries.filter(
summary =>
summary.status ==
DealStatus.AWAITING_ACCEPTANCE,
)}
title={"Ожидает приемки"}
droppableId={"AWAITING_ACCEPTANCE"}
color={"#4A90E2"}
/>
<Board
summaries={summaries.filter(
summary =>
summary.status == DealStatus.READY_FOR_WORK,
)}
title={"Готов к работе"}
droppableId={"READY_FOR_WORK"}
color={"#D3D3D3"}
/>
<Board
summaries={summaries.filter(
summary =>
summary.status == DealStatus.PACKAGING,
)}
title={"Упаковка"}
droppableId={"PACKAGING"}
color={"#F5A623"}
/>
<Board
summaries={summaries.filter(
summary =>
summary.status ==
DealStatus.AWAITING_SHIPMENT,
)}
title={"Ожидает отгрузки"}
droppableId={"AWAITING_SHIPMENT"}
color={"#7ED321"}
/>
<Board
summaries={summaries.filter(
summary =>
summary.status == DealStatus.IN_DELIVERY,
)}
title={"В доставке"}
droppableId={"IN_DELIVERY"}
color={"#6A0DAD"}
/>
<Board
summaries={summaries.filter(
summary =>
summary.status ==
DealStatus.AWAITING_PAYMENT,
)}
title={"Ожидает оплаты"}
droppableId={"AWAITING_PAYMENT"}
color={"#D0021B"}
/>
<Board
summaries={summaries.filter(
summary =>
summary.status == DealStatus.COMPLETED,
)}
title={"Завершена"}
droppableId={"COMPLETED"}
color={"#417505"}
/>
</div>
<Flex
justify={"space-between"}
gap={rem(10)}>
<div
className={classNames(
styles["delete"],
isDragEnded && styles["delete-hidden"],
)}>
<Droppable droppableId={"DELETE"}>
{(provided, snapshot) => (
<>
<div
{...provided.droppableProps}
ref={provided.innerRef}>
{!isDragEnded &&
!snapshot.isDraggingOver && (
<span>Удалить</span>
)}
</div>
{provided.placeholder}
</>
)}
</Droppable>
</div>
<div
className={classNames(
styles["delete"],
isDragEnded && styles["delete-hidden"],
)}>
<Droppable droppableId={"SUCCESS"}>
{(provided, snapshot) => (
<>
<div
{...provided.droppableProps}
ref={provided.innerRef}>
{!isDragEnded &&
!snapshot.isDraggingOver && (
<span>
Успешно завершена
</span>
)}
</div>
{provided.placeholder}
</>
)}
</Droppable>
</div>
</Flex>
</Flex>
</DragDropContext>
</motion.div>
);
};
const getBody = () => {
return displayMode === DisplayMode.TABLE
? getTableBody()
: getBoardBody();
};
return (
<PageBlock
fullHeight
style={{
gap: rem(10),
display: "flex",
flexDirection: "column",
backgroundColor: "transparent",
boxShadow: "none",
}}>
<DealPageContextProvider
defaultDealId={(dealId && parseInt(dealId)) || undefined}
refetchDeals={async () => {
await refetch();
}}>
<PrefillDealContextProvider>
<PrefillDealsWithExcelContextProvider>
<PageBlock style={{ flex: 0 }}>
<Flex
align={"center"}
justify={"space-between"}>
<Flex
gap={rem(10)}
direction={"column"}
align={"center"}>
<Text size={"xs"}>Вид</Text>
<Flex gap={rem(10)}>
<ActionIcon
onClick={() =>
setDisplayMode(DisplayMode.BOARD)
}
variant={
displayMode === DisplayMode.BOARD
? "filled"
: "default"
}>
<IconMenuDeep
style={{ rotate: "-90deg" }}
/>
</ActionIcon>
<ActionIcon
onClick={() =>
setDisplayMode(DisplayMode.TABLE)
}
variant={
displayMode === DisplayMode.TABLE
? "filled"
: "default"
}>
<IconMenu2 />
</ActionIcon>
</Flex>
</Flex>
<motion.div
key={displayMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}>
<div
className={styles["top-panel"]}
style={{
display:
displayMode === DisplayMode.TABLE
? "flex"
: "none",
}}>
<NumberInput
min={1}
placeholder={"Введите номер"}
{...form.getInputProps("id")}
hideControls
/>
<DealStatusSelect
onClear={() =>
form.setFieldValue("dealStatus", null)
}
clearable
placeholder={"Выберите статус "}
{...form.getInputProps("dealStatus")}
/>
<BaseMarketplaceSelect
onClear={() =>
form.setFieldValue("marketplace", null)
}
clearable
placeholder={"Выберите маркетплейс"}
{...form.getInputProps("marketplace")}
/>
<ClientSelectNew
onClear={() =>
form.setFieldValue("client", null)
}
clearable
searchable
placeholder={"Выберите клиента"}
{...form.getInputProps("client")}
/>
</div>
</motion.div>
</Flex>
</PageBlock>
<PageBlock
style={{
display: "flex",
flexDirection: "column",
flex: 1,
height: "100%",
}}>
{getBody()}
</PageBlock>
<DealEditDrawer />
<DealPrefillDrawer />
<PrefillDealsWithExcelDrawer />
</PrefillDealsWithExcelContextProvider>
</PrefillDealContextProvider>
</DealPageContextProvider>
</PageBlock>
);
};

View File

@@ -4,8 +4,8 @@ import { ContextModalProps } from "@mantine/modals";
import { useForm } from "@mantine/form";
import { Input, TextInput } from "@mantine/core";
import DealsTable from "../../DealsPage/components/DealsTable/DealsTable.tsx";
import { DealPageContextProvider } from "../../LeadsPage/contexts/DealPageContext.tsx";
import DealEditDrawer from "../../LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx";
import { DealPageContextProvider } from "../../DealsPage/contexts/DealPageContext.tsx";
import DealEditDrawer from "../../DealsPage/drawers/DealEditDrawer/DealEditDrawer.tsx";
type RestProps = {
summaries: DealSummary[];

View File

@@ -10,7 +10,7 @@ import {
ShippingWarehouseService,
} from "../../../client";
import { notifications } from "../../../shared/lib/notifications.ts";
import { useDealSummariesFull } from "../../LeadsPage/hooks/useDealSummaries.tsx";
import { useDealSummariesFull } from "../../DealsPage/hooks/useDealSummaries.tsx";
export const ShippingWarehousesPage = () => {
const { shippingWarehouses, refetch } = useShippingWarehousesList();

View File

@@ -1,15 +1,24 @@
import { DatePickerInput, DatePickerInputProps } from "@mantine/dates";
import { Divider, Stack, Text } from "@mantine/core";
import { Checkbox, CheckboxProps, Divider, Stack, Text } from "@mantine/core";
import ClientSelectNew from "../../../../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import { BaseMarketplaceSchema, ClientSchema, TransactionTagSchema, UserSchema } from "../../../../../../client";
import {
BaseMarketplaceSchema,
BoardSchema,
ClientSchema,
ProjectSchema,
StatusSchema,
TransactionTagSchema,
UserSchema,
} from "../../../../../../client";
import { ObjectSelectProps } from "../../../../../../components/ObjectSelect/ObjectSelect.tsx";
import BaseMarketplaceSelect
from "../../../../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
import DealStatusSelect from "../../../../../DealsPage/components/DealStatusSelect/DealStatusSelect.tsx";
import { DealStatusType } from "../../../../../../shared/enums/DealStatus.ts";
import DealStatusSelect from "../../../../../../components/DealStatusSelect/DealStatusSelect.tsx";
import { ProfitTableSegmentedControl } from "../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
import ManagerSelect from "../../../../../../components/ManagerSelect/ManagerSelect.tsx";
import TransactionTagSelect from "../../../../components/ExpenseTagSelect/TransactionTagSelect.tsx";
import BoardSelect from "../../../../../../components/BoardSelect/BoardSelect.tsx";
import ProjectSelect from "../../../../../../components/ProjectSelect/ProjectSelect.tsx";
type SelectProps<T> = Omit<
@@ -26,12 +35,17 @@ type FiltersProps = {
baseMarketplaceSelectProps?: SelectProps<BaseMarketplaceSchema>;
onBaseMarketplaceClear?: () => void;
dealStatusSelectProps?: Omit<ObjectSelectProps<DealStatusType>, "data">;
onDealStatusClear?: () => void;
projectSelectProps?: Omit<ObjectSelectProps<ProjectSchema | null>, "data">;
boardSelectProps?: Omit<ObjectSelectProps<BoardSchema | null>, "data">;
dealStatusSelectProps?: Omit<ObjectSelectProps<StatusSchema | null>, "data">;
managerSelectProps?: SelectProps<UserSchema | null | undefined>;
onManagerClear?: () => void;
isCompletedOnlyCheckboxProps?: CheckboxProps;
expenseTagSelectProps?: SelectProps<TransactionTagSchema | null>;
onExpenseTagClear?: () => void;
@@ -61,8 +75,8 @@ export const Filters = (props: FiltersProps) => {
isIncome={isIncome}
/>
</>
)
}
);
};
return (
<Stack mb={"lg"}>
@@ -79,12 +93,24 @@ export const Filters = (props: FiltersProps) => {
valueFormat={"DD.MM.YYYY"}
/>
}
{props.projectSelectProps &&
<ProjectSelect
{...props.projectSelectProps}
clearable
/>
}
{props.boardSelectProps &&
<BoardSelect
project={props.projectSelectProps?.value ?? null}
{...props.boardSelectProps}
clearable
/>
}
{props.dealStatusSelectProps &&
<DealStatusSelect
board={props.boardSelectProps?.value ?? null}
{...props.dealStatusSelectProps}
onClear={props.onDealStatusClear}
clearable
placeholder={"Выберите статус"}
/>
}
{props.clientSelectProps &&
@@ -111,6 +137,10 @@ export const Filters = (props: FiltersProps) => {
placeholder={"Выберите менеджера"}
/>
}
<Checkbox
{...props.isCompletedOnlyCheckboxProps}
label={"Только завершенные сделки"}
/>
{getTransactionTagsSelect(false)}
{getTransactionTagsSelect(true)}
{props.groupTableByProps &&
@@ -118,6 +148,8 @@ export const Filters = (props: FiltersProps) => {
<Divider />
<Text>Группировка таблицы:</Text>
<ProfitTableSegmentedControl
selectedProject={props.projectSelectProps?.value ?? null}
selectedBoard={props.boardSelectProps?.value ?? null}
{...props.groupTableByProps}
orientation={"vertical"}
size={"md"}

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