feat: tags for expenses, filters by tags in statistics

This commit is contained in:
2024-11-27 15:57:56 +04:00
parent 564895c26f
commit 431bc35d8b
37 changed files with 687 additions and 248 deletions

View File

@@ -0,0 +1,23 @@
import { FC } from "react";
import useExpenseTagsList from "../../../AdminPage/hooks/useExpenseTagsList.tsx";
import { ExpenseTagSchema } from "../../../../client";
import ObjectSelect, { ObjectSelectProps } from "../../../../components/ObjectSelect/ObjectSelect.tsx";
type Props = Omit<
ObjectSelectProps<ExpenseTagSchema | null>,
"data" | "getValueFn" | "getLabelFn"
>;
const ExpenseTagSelect: FC<Props> = props => {
const { objects: tags } = useExpenseTagsList();
return (
<ObjectSelect
data={tags}
getLabelFn={(tag: ExpenseTagSchema) => tag.name}
getValueFn={(tag: ExpenseTagSchema) => tag.id.toString()}
clearable
{...props}
onClear={() => props.onChange(null)}
/>
);
};
export default ExpenseTagSelect;

View File

@@ -1,12 +1,15 @@
import { ProfitChart } from "./components/ProfitChart/ProfitChart.tsx";
import styles from "../../ui/StatisticsPage.module.css";
import { ProfitTable } from "./components/ProfitTable/ProfitTable.tsx";
import { ProfitTabContextProvider } from "./contexts/ProfitTabContext.tsx";
export const ProfitTab = () => {
return (
<div className={styles["page-container"]}>
<ProfitChart />
<ProfitTable />
</div>
<ProfitTabContextProvider>
<div className={styles["page-container"]}>
<ProfitChart />
<ProfitTable />
</div>
</ProfitTabContextProvider>
);
};

View File

@@ -1,7 +1,7 @@
import { DatePickerInput, DatePickerInputProps } from "@mantine/dates";
import { Stack, Text } from "@mantine/core";
import { Divider, Stack, Text } from "@mantine/core";
import ClientSelectNew from "../../../../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import { BaseMarketplaceSchema, ClientSchema, UserSchema } from "../../../../../../client";
import { BaseMarketplaceSchema, ClientSchema, ExpenseTagSchema, UserSchema } from "../../../../../../client";
import { ObjectSelectProps } from "../../../../../../components/ObjectSelect/ObjectSelect.tsx";
import BaseMarketplaceSelect
from "../../../../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
@@ -9,6 +9,7 @@ import DealStatusSelect from "../../../../../DealsPage/components/DealStatusSele
import { DealStatusType } from "../../../../../../shared/enums/DealStatus.ts";
import { ProfitTableSegmentedControl } from "../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
import ManagerSelect from "../../../../../../components/ManagerSelect/ManagerSelect.tsx";
import ExpenseTagSelect from "../../../../components/ExpenseTagSelect/ExpenseTagSelect.tsx";
type FiltersProps = {
@@ -32,6 +33,12 @@ type FiltersProps = {
>;
onManagerClear?: () => void;
tagSelectProps?: Omit<
ObjectSelectProps<ExpenseTagSchema | null>,
"data" | "getValueFn" | "getLabelFn"
>;
onTagClear?: () => void;
groupTableByProps?: {
value: string,
onChange: (value: string) => void,
@@ -49,11 +56,17 @@ export const Filters = (props: FiltersProps) => {
onDealStatusClear,
managerSelectProps,
onManagerClear,
tagSelectProps,
onTagClear,
groupTableByProps,
} = props;
return (
<Stack mb={"lg"}>
<Divider />
<Text>
Фильтры для выручки и прибыли:
</Text>
{datePickerProps &&
<DatePickerInput
{...datePickerProps}
@@ -95,9 +108,21 @@ export const Filters = (props: FiltersProps) => {
placeholder={"Выберите менеджера"}
/>
}
{tagSelectProps &&
<>
<Divider />
<Text>Фильтры для расходов:</Text>
<ExpenseTagSelect
{...tagSelectProps}
onClear={onTagClear}
placeholder={"Выберите тег"}
/>
</>
}
{groupTableByProps &&
<>
<Text>Группировать:</Text>
<Divider />
<Text>Группировка таблицы:</Text>
<ProfitTableSegmentedControl
{...groupTableByProps}
orientation={"vertical"}

View File

@@ -1,20 +1,18 @@
import { AreaChart } from "@mantine/charts";
import "@mantine/charts/styles.css";
import PageBlock from "../../../../../../components/PageBlock/PageBlock.tsx";
import { useProfitChart } from "./hooks/useProfitChart.tsx";
import { Skeleton, Stack } from "@mantine/core";
import { ProfitChartFiltersModal } from "../../modals/ProfitChartFiltersModal.tsx";
import { Total } from "../Total/Total.tsx";
import styles from "../../../../ui/StatisticsPage.module.css";
import { formatDate } from "../../../../../../types/utils.ts";
import { useProfitTabContext } from "../../contexts/ProfitTabContext.tsx";
export const ProfitChart = () => {
const {
profitData,
form,
isLoading,
} = useProfitChart();
profitChartData: profitData,
isChartLoading: isLoading,
} = useProfitTabContext();
const formattedProfitData = profitData.map(
value => {
@@ -35,17 +33,14 @@ export const ProfitChart = () => {
const units = ["₽", "шт"];
const chartSizes = ["42vh", "28vh"];
const chartSizes = ["47vh", "28vh"];
return (
<div className={styles["profit-chart-container"]}>
<Total profitData={profitData} />
<PageBlock style={{ padding: "25px" }}>
<ProfitChartFiltersModal
form={form}
/>
<Skeleton visible={isLoading}>
<Stack gap="20px">
<Stack gap="4vh">
{getChartsSeries.map((series, idx) => {
return (
<AreaChart

View File

@@ -1,63 +0,0 @@
import { ChartFormFilters } from "../../../../../types/ChartFormFilters.ts";
import { useForm } from "@mantine/form";
import { getDefaultDates } from "../../../../../utils/dates.ts";
import { useEffect, useState } from "react";
import { ProfitChartDataItem, StatisticsService } from "../../../../../../../client";
import { defaultDealStatus } from "../../../../../utils/defaultFilterValues.ts";
import { dateToString } from "../../../../../../../types/utils.ts";
export const useProfitChart = () => {
const form = useForm<ChartFormFilters>({
mode: "controlled",
initialValues: {
dateRange: getDefaultDates(),
client: null,
marketplace: null,
dealStatus: defaultDealStatus,
manager: null,
},
});
const [profitData, setProfitData] = useState<ProfitChartDataItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const getFilters = () => {
const dateRange = form.values.dateRange;
return {
dateRange: [
dateToString(dateRange[0]),
dateToString(dateRange[1]),
],
clientId: form.values.client?.id ?? -1,
baseMarketplaceKey: form.values.marketplace?.key ?? "all",
dealStatusId: form.values.dealStatus?.id ?? -1,
managerId: form.values.manager?.id ?? -1,
};
};
const fetchProfitData = () => {
setIsLoading(true);
StatisticsService.getProfitChartData({
requestBody: getFilters(),
})
.then(res => {
setProfitData(res.data);
})
.catch(err => console.log(err))
.finally(() => setIsLoading(false));
};
useEffect(() => {
if (form.values.dateRange.length < 2 || form.values.dateRange[1] === null) {
return;
}
fetchProfitData();
}, [form.values]);
return {
profitData,
form,
isLoading,
};
};

View File

@@ -2,7 +2,7 @@ import PageBlock from "../../../../../../components/PageBlock/PageBlock.tsx";
import { MantineReactTable } from "mantine-react-table";
import { useProfitTable } from "./hooks/useProfitTable.tsx";
import { Skeleton } from "@mantine/core";
import { ProfitTableFiltersModal } from "../../modals/ProfitTableFiltersModal.tsx";
import { ProfitFiltersModal } from "../../modals/ProfitFiltersModal.tsx";
export const ProfitTable = () => {
@@ -10,7 +10,7 @@ export const ProfitTable = () => {
return (
<PageBlock style={{ flex: 2, minWidth: "600px", padding: "25px" }}>
<ProfitTableFiltersModal form={form} />
<ProfitFiltersModal form={form} />
<Skeleton visible={isLoading}>
<MantineReactTable table={table} />
</Skeleton>

View File

@@ -1,36 +0,0 @@
import { GroupStatisticsTable } from "../../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
import { useProfitTableColumns } from "./columns.tsx";
import { useMantineReactTable } from "mantine-react-table";
import { ProfitTableDataItem } from "../../../../../../../client";
import { MRT_Localization_RU } from "mantine-react-table/locales/ru/index.cjs";
type Props = {
groupTableBy: GroupStatisticsTable;
profitData: ProfitTableDataItem[];
}
export const useProfitMantineTable = ({ groupTableBy, profitData }: Props) => {
const columns = useProfitTableColumns({
groupTableBy,
});
const defaultSorting = [{ id: "groupedValue", desc: true }];
const table = useMantineReactTable({
enablePagination: false,
data: profitData,
columns,
enableTopToolbar: false,
enableBottomToolbar: false,
enableSorting: true,
initialState: {
sorting: defaultSorting,
},
localization: MRT_Localization_RU,
enableRowVirtualization: true,
mantineTableContainerProps: { style: { maxHeight: "86vh" } },
});
return { table };
};

View File

@@ -1,69 +1,36 @@
import { useForm } from "@mantine/form";
import { TableFormFilters } from "../../../../../types/TableFormFilters.ts";
import { getDefaultDates } from "../../../../../utils/dates.ts";
import { GroupStatisticsTable } from "../../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
import { useEffect, useState } from "react";
import { ProfitTableDataItem, StatisticsService } from "../../../../../../../client";
import { useProfitMantineTable } from "./useProfitMantineTable.tsx";
import { defaultDealStatus } from "../../../../../utils/defaultFilterValues.ts";
import { dateToString } from "../../../../../../../types/utils.ts";
import { useProfitTabContext } from "../../../contexts/ProfitTabContext.tsx";
import { useProfitTableColumns } from "./columns.tsx";
import { useMantineReactTable } from "mantine-react-table";
import { MRT_Localization_RU } from "mantine-react-table/locales/ru/index.cjs";
export const useProfitTable = () => {
const form = useForm<TableFormFilters>({
mode: "controlled",
initialValues: {
dateRange: getDefaultDates(),
groupTableBy: GroupStatisticsTable.BY_DATES,
client: null,
marketplace: null,
dealStatus: defaultDealStatus,
manager: null,
},
});
const [isLoading, setIsLoading] = useState(false);
const {
form,
isTableLoading: isLoading,
profitTableData
} = useProfitTabContext();
const [profitData, setProfitData] = useState<ProfitTableDataItem[]>([]);
const { table } = useProfitMantineTable({
const columns = useProfitTableColumns({
groupTableBy: form.values.groupTableBy,
profitData,
});
const getFilters = () => {
const dateRange = form.values.dateRange;
const defaultSorting = [{ id: "groupedValue", desc: true }];
return {
dateRange: [
dateToString(dateRange[0]),
dateToString(dateRange[1]),
],
groupTableBy: form.values.groupTableBy,
clientId: form.values.client?.id ?? -1,
baseMarketplaceKey: form.values.marketplace?.key ?? "all",
dealStatusId: form.values.dealStatus?.id ?? -1,
managerId: form.values.manager?.id ?? -1,
};
};
const fetchProfitData = () => {
setIsLoading(true);
StatisticsService.getProfitTableData({
requestBody: getFilters(),
})
.then(res => {
setProfitData(res.data);
})
.catch(err => console.log(err))
.finally(() => setIsLoading(false));
};
useEffect(() => {
if (form.values.dateRange.length < 2 || form.values.dateRange[1] === null) {
return;
}
fetchProfitData();
}, [form.values]);
const table = useMantineReactTable({
enablePagination: false,
data: profitTableData,
columns,
enableTopToolbar: false,
enableBottomToolbar: false,
enableSorting: true,
initialState: {
sorting: defaultSorting,
},
localization: MRT_Localization_RU,
enableRowVirtualization: true,
mantineTableContainerProps: { style: { maxHeight: "86vh" } },
});
return {
table,

View File

@@ -0,0 +1,125 @@
import { createContext, FC, useContext, useEffect, useState } from "react";
import { ProfitChartDataItem, ProfitTableDataItem, StatisticsService } from "../../../../../client";
import { useForm, UseFormReturnType } from "@mantine/form";
import { FormFilters } from "../../../types/FormFilters.ts";
import { getDefaultDates } from "../../../utils/dates.ts";
import { GroupStatisticsTable } from "../components/ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
import { defaultDealStatus } from "../../../utils/defaultFilterValues.ts";
import { dateToString } from "../../../../../types/utils.ts";
type ProfitTabContextState = {
form: UseFormReturnType<FormFilters, (values: FormFilters) => FormFilters>;
isChartLoading: boolean;
isTableLoading: boolean;
profitChartData: ProfitChartDataItem[];
profitTableData: ProfitTableDataItem[];
};
const ProfitTabContext = createContext<ProfitTabContextState | undefined>(
undefined,
);
const useProfitTabContextState = () => {
const form = useForm<FormFilters>({
mode: "controlled",
initialValues: {
dateRange: getDefaultDates(),
groupTableBy: GroupStatisticsTable.BY_DATES,
client: null,
marketplace: null,
dealStatus: defaultDealStatus,
manager: null,
tag: null,
},
});
const [isChartLoading, setIsChartLoading] = useState(false);
const [isTableLoading, setIsTableLoading] = useState(false);
const [profitChartData, setProfitChartData] = useState<ProfitChartDataItem[]>([]);
const [profitTableData, setProfitTableData] = useState<ProfitTableDataItem[]>([]);
const getChartFilters = () => {
const dateRange = form.values.dateRange;
return {
dateRange: [
dateToString(dateRange[0]),
dateToString(dateRange[1]),
],
clientId: form.values.client?.id ?? -1,
baseMarketplaceKey: form.values.marketplace?.key ?? "all",
dealStatusId: form.values.dealStatus?.id ?? -1,
managerId: form.values.manager?.id ?? -1,
tagId: form.values.tag?.id ?? -1,
};
};
const fetchChartProfitData = () => {
setIsChartLoading(true);
StatisticsService.getProfitChartData({
requestBody: getChartFilters(),
})
.then(res => {
setProfitChartData(res.data);
})
.catch(err => console.log(err))
.finally(() => setIsChartLoading(false));
};
const getTableFilters = () => {
return {
...getChartFilters(),
groupTableBy: form.values.groupTableBy,
};
};
const fetchTableProfitData = () => {
setIsTableLoading(true);
StatisticsService.getProfitTableData({
requestBody: getTableFilters(),
})
.then(res => {
setProfitTableData(res.data);
})
.catch(err => console.log(err))
.finally(() => setIsTableLoading(false));
};
useEffect(() => {
if (form.values.dateRange.length < 2 || form.values.dateRange[1] === null) {
return;
}
fetchChartProfitData();
fetchTableProfitData();
}, [form.values]);
return {
form,
isChartLoading,
isTableLoading,
profitChartData,
profitTableData,
};
};
type ProfitTabContextProviderProps = {
children: React.ReactNode;
};
export const ProfitTabContextProvider: FC<ProfitTabContextProviderProps> = ({ children }) => {
const state = useProfitTabContextState();
return (
<ProfitTabContext.Provider value={state}>
{children}
</ProfitTabContext.Provider>
);
};
export const useProfitTabContext = () => {
const context = useContext(ProfitTabContext);
if (!context) {
throw new Error(
"useProfitTabContext must be used within a ProfitTabContextProvider",
);
}
return context;
};

View File

@@ -1,47 +0,0 @@
import { Button, Group, Modal } from "@mantine/core";
import { IconFilter } from "@tabler/icons-react";
import { Filters } from "../components/Filters/Filters.tsx";
import { UseFormReturnType } from "@mantine/form";
import { useDisclosure } from "@mantine/hooks";
import { ChartFormFilters } from "../../../types/ChartFormFilters.ts";
type Props = {
form: UseFormReturnType<ChartFormFilters>;
}
export const ProfitChartFiltersModal = ({ form }: Props) => {
const [opened, { open, close }] = useDisclosure();
return (
<>
<Button
variant={"default"}
onClick={open}
mb={"lg"}
>
<Group gap={"xs"}>
<IconFilter />
Фильтры графиков
</Group>
</Button>
<Modal
opened={opened}
onClose={close}
title={"Фильтры графиков"}
>
<Filters
datePickerProps={form.getInputProps("dateRange")}
clientSelectProps={form.getInputProps("client")}
onClientClear={() => form.setFieldValue("client", null)}
baseMarketplaceSelectProps={form.getInputProps("marketplace")}
onBaseMarketplaceClear={() => form.setFieldValue("marketplace", null)}
dealStatusSelectProps={form.getInputProps("dealStatus")}
onDealStatusClear={() => form.setFieldValue("dealStatus", null)}
managerSelectProps={form.getInputProps("manager")}
onManagerClear={() => form.setFieldValue("manager", null)}
/>
</Modal>
</>
);
};

View File

@@ -1,16 +1,16 @@
import { Button, Group, Modal } from "@mantine/core";
import { IconFilter } from "@tabler/icons-react";
import { Filters } from "../components/Filters/Filters.tsx";
import { TableFormFilters } from "../../../types/TableFormFilters.ts";
import { FormFilters } from "../../../types/FormFilters.ts";
import { UseFormReturnType } from "@mantine/form";
import { useDisclosure } from "@mantine/hooks";
type Props = {
form: UseFormReturnType<TableFormFilters>;
form: UseFormReturnType<FormFilters>;
}
export const ProfitTableFiltersModal = ({ form }: Props) => {
export const ProfitFiltersModal = ({ form }: Props) => {
const [opened, { open, close }] = useDisclosure();
return (
@@ -22,13 +22,13 @@ export const ProfitTableFiltersModal = ({ form }: Props) => {
>
<Group gap={"xs"}>
<IconFilter />
Фильтры таблицы
Фильтры
</Group>
</Button>
<Modal
opened={opened}
onClose={close}
title={"Фильтры таблицы"}
title={"Фильтры"}
>
<Filters
datePickerProps={form.getInputProps("dateRange")}
@@ -44,6 +44,8 @@ export const ProfitTableFiltersModal = ({ form }: Props) => {
onDealStatusClear={() => form.setFieldValue("dealStatus", null)}
managerSelectProps={form.getInputProps("manager")}
onManagerClear={() => form.setFieldValue("manager", null)}
tagSelectProps={form.getInputProps("tag")}
onTagClear={() => form.setFieldValue("tag", null)}
/>
</Modal>
</>

View File

@@ -1,14 +1,15 @@
import {
GroupStatisticsTable,
} from "../tabs/ProfitTab/components/ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx";
import { BaseMarketplaceSchema, ClientSchema, UserSchema } from "../../../client";
import { BaseMarketplaceSchema, ClientSchema, ExpenseTagSchema, UserSchema } from "../../../client";
import { DealStatusType } from "../../../shared/enums/DealStatus.ts";
export interface TableFormFilters {
export interface FormFilters {
dateRange: [Date | null, Date | null];
groupTableBy: GroupStatisticsTable;
client: ClientSchema | null;
marketplace: BaseMarketplaceSchema | null;
dealStatus: DealStatusType | null;
manager: UserSchema | null;
tag: ExpenseTagSchema | null;
}