diff --git a/package.json b/package.json index beef21f..6e3b53b 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,9 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", "@hello-pangea/dnd": "^17.0.0", + "@mantine/charts": "^7.13.5", "@mantine/core": "^7.11.2", - "@mantine/dates": "^7.11.2", + "@mantine/dates": "^7.13.5", "@mantine/dropzone": "^7.11.2", "@mantine/form": "^7.11.2", "@mantine/hooks": "^7.11.2", @@ -51,6 +52,7 @@ "react-redux": "^9.1.2", "react-to-print": "^2.15.1", "reactflow": "^11.11.4", + "recharts": "^2.13.3", "zod": "^3.23.8" }, "devDependencies": { diff --git a/src/client/index.ts b/src/client/index.ts index 26d067b..ba7a230 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -166,11 +166,16 @@ export type { GetClientMarketplacesResponse } from './models/GetClientMarketplac export type { GetDealBillById } from './models/GetDealBillById'; export type { GetDealProductsBarcodesPdfRequest } from './models/GetDealProductsBarcodesPdfRequest'; export type { GetDealProductsBarcodesPdfResponse } from './models/GetDealProductsBarcodesPdfResponse'; +export type { GetManagersResponse } from './models/GetManagersResponse'; export type { GetPaymentRecordsResponse } from './models/GetPaymentRecordsResponse'; export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfRequest'; export type { GetProductBarcodePdfResponse } from './models/GetProductBarcodePdfResponse'; export type { GetProductBarcodeRequest } from './models/GetProductBarcodeRequest'; export type { GetProductBarcodeResponse } from './models/GetProductBarcodeResponse'; +export type { GetProfitChartDataRequest } from './models/GetProfitChartDataRequest'; +export type { GetProfitChartDataResponse } from './models/GetProfitChartDataResponse'; +export type { GetProfitTableDataRequest } from './models/GetProfitTableDataRequest'; +export type { GetProfitTableDataResponse } from './models/GetProfitTableDataResponse'; export type { GetServiceKitSchema } from './models/GetServiceKitSchema'; export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest'; export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse'; @@ -205,6 +210,9 @@ export type { ProductUpdateRequest } from './models/ProductUpdateRequest'; export type { ProductUpdateResponse } from './models/ProductUpdateResponse'; export type { ProductUploadBarcodeImageResponse } from './models/ProductUploadBarcodeImageResponse'; export type { ProductUploadImageResponse } from './models/ProductUploadImageResponse'; +export type { ProfitChartDataItem } from './models/ProfitChartDataItem'; +export type { ProfitTableDataItem } from './models/ProfitTableDataItem'; +export type { ProfitTableGroupBy } from './models/ProfitTableGroupBy'; export type { RoleSchema } from './models/RoleSchema'; export type { ServiceCategoryPriceSchema } from './models/ServiceCategoryPriceSchema'; export type { ServiceCategoryReorderRequest } from './models/ServiceCategoryReorderRequest'; @@ -267,6 +275,7 @@ export { ProductService } from './services/ProductService'; export { RoleService } from './services/RoleService'; export { ServiceService } from './services/ServiceService'; export { ShippingWarehouseService } from './services/ShippingWarehouseService'; +export { StatisticsService } from './services/StatisticsService'; export { TaskService } from './services/TaskService'; export { TimeTrackingService } from './services/TimeTrackingService'; export { UserService } from './services/UserService'; diff --git a/src/client/models/DealGeneralInfoSchema.ts b/src/client/models/DealGeneralInfoSchema.ts index 210cbb2..4d5906c 100644 --- a/src/client/models/DealGeneralInfoSchema.ts +++ b/src/client/models/DealGeneralInfoSchema.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { UserSchema } from './UserSchema'; export type DealGeneralInfoSchema = { name: string; isDeleted: boolean; @@ -10,5 +11,6 @@ export type DealGeneralInfoSchema = { shippingWarehouse?: (string | null); deliveryDate?: (string | null); receivingSlotDate?: (string | null); + manager?: (UserSchema | null); }; diff --git a/src/client/models/DealSchema.ts b/src/client/models/DealSchema.ts index 439ff03..6e0f322 100644 --- a/src/client/models/DealSchema.ts +++ b/src/client/models/DealSchema.ts @@ -4,12 +4,14 @@ /* eslint-disable */ import type { ClientSchema } from './ClientSchema'; import type { DealBillRequestSchema } from './DealBillRequestSchema'; -import type { DealGroupSchema } from './DealGroupSchema'; import type { DealProductSchema } from './DealProductSchema'; import type { DealServiceSchema } from './DealServiceSchema'; import type { DealStatusHistorySchema } from './DealStatusHistorySchema'; import type { ServicePriceCategorySchema } from './ServicePriceCategorySchema'; import type { ShippingWarehouseSchema } from './ShippingWarehouseSchema'; +import type { UserSchema } from './UserSchema'; +import type { DealGroupSchema } from './DealGroupSchema'; + export type DealSchema = { id: number; name: string; @@ -28,6 +30,8 @@ export type DealSchema = { billRequest?: (DealBillRequestSchema | null); category?: (ServicePriceCategorySchema | null); group?: (DealGroupSchema | null); + manager?: (UserSchema | null); + deliveryDate?: (string | null); receivingSlotDate?: (string | null); }; diff --git a/src/client/models/GetProfitChartDataRequest.ts b/src/client/models/GetProfitChartDataRequest.ts new file mode 100644 index 0000000..0d586c4 --- /dev/null +++ b/src/client/models/GetProfitChartDataRequest.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type GetProfitChartDataRequest = { + dateRange: any[]; + clientId: number; + baseMarketplaceKey: string; + dealStatusId: number; +}; + diff --git a/src/client/models/GetProfitChartDataResponse.ts b/src/client/models/GetProfitChartDataResponse.ts new file mode 100644 index 0000000..99ff762 --- /dev/null +++ b/src/client/models/GetProfitChartDataResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ProfitChartDataItem } from './ProfitChartDataItem'; +export type GetProfitChartDataResponse = { + data: Array; +}; + diff --git a/src/client/models/GetProfitTableDataRequest.ts b/src/client/models/GetProfitTableDataRequest.ts new file mode 100644 index 0000000..898bde3 --- /dev/null +++ b/src/client/models/GetProfitTableDataRequest.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ProfitTableGroupBy } from './ProfitTableGroupBy'; +export type GetProfitTableDataRequest = { + dateRange: any[]; + groupTableBy: ProfitTableGroupBy; +}; + diff --git a/src/client/models/GetProfitTableDataResponse.ts b/src/client/models/GetProfitTableDataResponse.ts new file mode 100644 index 0000000..e03f713 --- /dev/null +++ b/src/client/models/GetProfitTableDataResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ProfitTableDataItem } from './ProfitTableDataItem'; +export type GetProfitTableDataResponse = { + data: Array; +}; + diff --git a/src/client/models/ProfitChartDataItem.ts b/src/client/models/ProfitChartDataItem.ts new file mode 100644 index 0000000..bb63817 --- /dev/null +++ b/src/client/models/ProfitChartDataItem.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ProfitChartDataItem = { + date: string; + revenue: number; + profit: number; + dealsCount: number; +}; + diff --git a/src/client/models/ProfitTableDataItem.ts b/src/client/models/ProfitTableDataItem.ts new file mode 100644 index 0000000..b755b96 --- /dev/null +++ b/src/client/models/ProfitTableDataItem.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ProfitTableDataItem = { + groupedValue: (string | number); + revenue: number; + profit: number; + dealsCount: number; +}; + diff --git a/src/client/models/ProfitTableGroupBy.ts b/src/client/models/ProfitTableGroupBy.ts new file mode 100644 index 0000000..c9a9c1d --- /dev/null +++ b/src/client/models/ProfitTableGroupBy.ts @@ -0,0 +1,5 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ProfitTableGroupBy = 0 | 1 | 2 | 3 | 4; diff --git a/src/client/services/StatisticsService.ts b/src/client/services/StatisticsService.ts new file mode 100644 index 0000000..6a9c77e --- /dev/null +++ b/src/client/services/StatisticsService.ts @@ -0,0 +1,53 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { GetProfitChartDataRequest } from '../models/GetProfitChartDataRequest'; +import type { GetProfitChartDataResponse } from '../models/GetProfitChartDataResponse'; +import type { GetProfitTableDataRequest } from '../models/GetProfitTableDataRequest'; +import type { GetProfitTableDataResponse } from '../models/GetProfitTableDataResponse'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class StatisticsService { + /** + * Get Profit Chart Data + * @returns GetProfitChartDataResponse Successful Response + * @throws ApiError + */ + public static getProfitChartData({ + requestBody, + }: { + requestBody: GetProfitChartDataRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/statistics/get-profit-chart-data', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Profit Table Data + * @returns GetProfitTableDataResponse Successful Response + * @throws ApiError + */ + public static getProfitTableData({ + requestBody, + }: { + requestBody: GetProfitTableDataRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/statistics/get-profit-table-data', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } +} diff --git a/src/client/services/UserService.ts b/src/client/services/UserService.ts index b1037fa..3339e4d 100644 --- a/src/client/services/UserService.ts +++ b/src/client/services/UserService.ts @@ -5,6 +5,7 @@ import type { CreateUserRequest } from '../models/CreateUserRequest'; import type { CreateUserResponse } from '../models/CreateUserResponse'; import type { GetAllUsersResponse } from '../models/GetAllUsersResponse'; +import type { GetManagersResponse } from '../models/GetManagersResponse'; import type { UpdateUserRequest } from '../models/UpdateUserRequest'; import type { UpdateUserResponse } from '../models/UpdateUserResponse'; import type { CancelablePromise } from '../core/CancelablePromise'; @@ -62,4 +63,15 @@ export class UserService { }, }); } + /** + * Get Managers + * @returns GetManagersResponse Successful Response + * @throws ApiError + */ + public static getManagers(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/user/get-managers', + }); + } } diff --git a/src/components/ManagerSelect/ManagerSelect.tsx b/src/components/ManagerSelect/ManagerSelect.tsx new file mode 100644 index 0000000..6cdb7d9 --- /dev/null +++ b/src/components/ManagerSelect/ManagerSelect.tsx @@ -0,0 +1,23 @@ +import { FC } from "react"; +import ObjectSelect, { ObjectSelectProps } from "../ObjectSelect/ObjectSelect.tsx"; +import { UserSchema } from "../../client"; +import useManagersList from "../../pages/LeadsPage/hooks/useManagersList.tsx"; + +type Props = Omit< + ObjectSelectProps, + "data" | "getValueFn" | "getLabelFn" +>; +const UserSelect: FC = props => { + const { objects: managers } = useManagersList(); + return ( + `${manager.firstName} ${manager.secondName}`} + getValueFn={(manager: UserSchema) => manager.id.toString()} + clearable + {...props} + onClear={() => props.onChange(null)} + /> + ); +}; +export default UserSelect; diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index dc4ecbf..fac1aaf 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -4,7 +4,7 @@ import { IconBarcode, IconBox, IconBuildingWarehouse, - IconCash, + IconCash, IconChartDots, IconDashboard, IconFileBarcode, IconHome2, @@ -93,6 +93,11 @@ const mockdata = [ label: "Маркетплейсы", href: "/marketplaces", }, + { + icon: IconChartDots, + label: "Статистика", + href: "/statistics", + } ]; export function Navbar() { diff --git a/src/pages/LeadsPage/drawers/DealEditDrawer/tabs/DealEditDrawerGeneralTab.tsx b/src/pages/LeadsPage/drawers/DealEditDrawer/tabs/DealEditDrawerGeneralTab.tsx index ba4b291..ae66a0d 100644 --- a/src/pages/LeadsPage/drawers/DealEditDrawer/tabs/DealEditDrawerGeneralTab.tsx +++ b/src/pages/LeadsPage/drawers/DealEditDrawer/tabs/DealEditDrawerGeneralTab.tsx @@ -30,6 +30,7 @@ import { IconBarcode, IconPrinter } from "@tabler/icons-react"; import styles from "../../../ui/LeadsPage.module.css"; import { base64ToBlob } from "../../../../../shared/lib/utils.ts"; import { DatePickerInput } from "@mantine/dates"; +import ManagerSelect from "../../../../../components/ManagerSelect/ManagerSelect.tsx"; type Props = { deal: DealSchema; @@ -211,6 +212,11 @@ const Content: FC = ({ deal }) => { {...form.getInputProps("receivingSlotDate")} /> + + ObjectList({ + queryFn: UserService.getManagers, + getObjectsFn: response => response.managers, + queryKey: "getManagers", + }); + +export default useManagersList; diff --git a/src/pages/StatisticsPage/components/StatisticsTabSegmentedControl/StatisticsTabSegmentedControl.tsx b/src/pages/StatisticsPage/components/StatisticsTabSegmentedControl/StatisticsTabSegmentedControl.tsx new file mode 100644 index 0000000..10e25e7 --- /dev/null +++ b/src/pages/StatisticsPage/components/StatisticsTabSegmentedControl/StatisticsTabSegmentedControl.tsx @@ -0,0 +1,28 @@ +import { SegmentedControl, SegmentedControlProps } from "@mantine/core"; +import { FC } from "react"; + +export enum StatisticsTab { + PROFIT, + SALARIES, +} + +type Props = Omit; +const data = [ + { + label: "Выручка по сделкам", + value: StatisticsTab.PROFIT.toString(), + }, + { + label: "Зарплаты", + value: StatisticsTab.SALARIES.toString(), + }, +]; + +export const StatisticsTabSegmentedControl: FC = props => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/index.ts b/src/pages/StatisticsPage/index.ts new file mode 100644 index 0000000..568eecd --- /dev/null +++ b/src/pages/StatisticsPage/index.ts @@ -0,0 +1 @@ +export { StatisticsPage } from "./ui/StatisticsPage.tsx"; diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/ProfitTab.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/ProfitTab.tsx new file mode 100644 index 0000000..286a583 --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/ProfitTab.tsx @@ -0,0 +1,12 @@ +import { ProfitChart } from "./components/ProfitChart/ProfitChart.tsx"; +import styles from "../../ui/StatisticsPage.module.css"; +import { ProfitTable } from "./components/ProfitTable/ProfitTable.tsx"; + +export const ProfitTab = () => { + return ( +
+ + +
+ ); +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/components/Filters/Filters.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/components/Filters/Filters.tsx new file mode 100644 index 0000000..c7b4e35 --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/components/Filters/Filters.tsx @@ -0,0 +1,94 @@ +import { DatePickerInput, DatePickerInputProps } from "@mantine/dates"; +import { Stack, Text } from "@mantine/core"; +import ClientSelectNew from "../../../../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx"; +import { BaseMarketplaceSchema, ClientSchema } 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 { ProfitTableSegmentedControl } from "../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx"; + + +type FiltersProps = { + datePickerProps?: DatePickerInputProps<"range">; + + clientSelectProps?: Omit, "data">; + onClientClear?: () => void; + + baseMarketplaceSelectProps?: Omit< + ObjectSelectProps, + "data" | "getValueFn" | "getLabelFn" + >; + onBaseMarketplaceClear?: () => void; + + dealStatusSelectProps?: Omit, "data">; + onDealStatusClear?: () => void; + + groupTableByProps?: { + value: string, + onChange: (value: string) => void, + }; +} + +export const Filters = (props: FiltersProps) => { + const { + datePickerProps, + clientSelectProps, + onClientClear, + baseMarketplaceSelectProps, + onBaseMarketplaceClear, + dealStatusSelectProps, + onDealStatusClear, + groupTableByProps, + } = props; + + return ( + + {datePickerProps && + + } + {dealStatusSelectProps && + + } + {clientSelectProps && + + } + {baseMarketplaceSelectProps && + + } + {groupTableByProps && + <> + Группировать: + + + } + + ); +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitChart/ProfitChart.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitChart/ProfitChart.tsx new file mode 100644 index 0000000..dc9a16c --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitChart/ProfitChart.tsx @@ -0,0 +1,55 @@ +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"; + + +export const ProfitChart = () => { + const { + profitData, + form, + isLoading, + } = useProfitChart(); + + const getChartsSeries = [ + [ + { name: "profit", label: "Прибыль", color: "indigo.6" }, + { name: "revenue", label: "Выручка", color: "teal.6" }, + ], + [ + { name: "dealsCount", label: "Количество сделок", color: "indigo.6" }, + ], + ]; + + const units = ["₽", "шт"]; + + return ( + + + + + {getChartsSeries.map((series, idx) => { + return ( + new Intl.NumberFormat("ru-RU").format(value)} + series={series} + fillOpacity={0.5} + /> + ); + })} + + + + ); +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitChart/hooks/useProfitChart.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitChart/hooks/useProfitChart.tsx new file mode 100644 index 0000000..00a41cf --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitChart/hooks/useProfitChart.tsx @@ -0,0 +1,59 @@ +import { ChartFormFilters } from "../../../../../types/ChartFormFilters.ts"; +import { useForm } from "@mantine/form"; +import { dateToString, getDefaultDates } from "../../../../../utils/dates.ts"; +import { useEffect, useState } from "react"; +import { ProfitChartDataItem, StatisticsService } from "../../../../../../../client"; + + +export const useProfitChart = () => { + const form = useForm({ + mode: "controlled", + initialValues: { + dateRange: getDefaultDates(), + client: null, + marketplace: null, + dealStatus: null, + }, + }); + const [profitData, setProfitData] = useState([]); + 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, + }; + }; + + 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, + }; +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/ProfitTable.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/ProfitTable.tsx new file mode 100644 index 0000000..61a1f1f --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/ProfitTable.tsx @@ -0,0 +1,19 @@ +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"; + + +export const ProfitTable = () => { + const { table, form, isLoading } = useProfitTable(); + + return ( + + + + + + + ); +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/hooks/columns.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/hooks/columns.tsx new file mode 100644 index 0000000..408fce1 --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/hooks/columns.tsx @@ -0,0 +1,57 @@ +import { useMemo } from "react"; +import { MRT_ColumnDef } from "mantine-react-table"; +import { ProfitTableDataItem } from "../../../../../../../client"; +import { GroupStatisticsTable } from "../../ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx"; +import { DealStatus, DealStatusDictionary } from "../../../../../../../shared/enums/DealStatus.ts"; + +type Props = { + groupTableBy: GroupStatisticsTable; +} + +export const useProfitTableColumns = ({ groupTableBy }: Props) => { + const groupedValueHeader = { + [GroupStatisticsTable.BY_DATES]: "Дата", + [GroupStatisticsTable.BY_CLIENTS]: "Клиент", + [GroupStatisticsTable.BY_STATUSES]: "Статус", + [GroupStatisticsTable.BY_MARKETPLACES]: "Маркетплейс", + [GroupStatisticsTable.BY_WAREHOUSES]: "Склад отгрузки", + }; + + return useMemo[]>( + () => [ + { + accessorKey: "groupedValue", + header: groupedValueHeader[groupTableBy], + enableSorting: groupTableBy === GroupStatisticsTable.BY_DATES, + Cell: ({ row }) => { + if (groupTableBy === GroupStatisticsTable.BY_STATUSES) { + const statusIndex = row.original.groupedValue as DealStatus; + return DealStatusDictionary[statusIndex]; + } + return row.original.groupedValue; + }, + size: 60, + }, + { + accessorKey: "dealsCount", + header: "Кол-во", + size: 40, + }, + { + accessorKey: "profit", + header: "Прибыль", + Cell: ({ row }) => + row.original.profit.toLocaleString("ru-RU") + "₽", + size: 50, + }, + { + accessorKey: "revenue", + header: "Выручка", + Cell: ({ row }) => + row.original.revenue.toLocaleString("ru-RU") + "₽", + size: 50, + }, + ], + [groupTableBy], + ); +}; diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/hooks/useProfitMantineTable.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/hooks/useProfitMantineTable.tsx new file mode 100644 index 0000000..2abed81 --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/hooks/useProfitMantineTable.tsx @@ -0,0 +1,36 @@ +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 }; +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/hooks/useProfitTable.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/hooks/useProfitTable.tsx new file mode 100644 index 0000000..3717aeb --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTable/hooks/useProfitTable.tsx @@ -0,0 +1,63 @@ +import { useForm } from "@mantine/form"; +import { TableFormFilters } from "../../../../../types/TableFormFilters.ts"; +import { dateToString, 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"; + + +export const useProfitTable = () => { + const form = useForm({ + mode: "controlled", + initialValues: { + dateRange: getDefaultDates(), + groupTableBy: GroupStatisticsTable.BY_DATES, + }, + }); + const [isLoading, setIsLoading] = useState(false); + + const [profitData, setProfitData] = useState([]); + + const { table } = useProfitMantineTable({ + groupTableBy: form.values.groupTableBy, + profitData, + }); + + const getFilters = () => { + const dateRange = form.values.dateRange; + + return { + dateRange: [ + dateToString(dateRange[0]), + dateToString(dateRange[1]), + ], + groupTableBy: form.values.groupTableBy, + }; + }; + + 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]); + + return { + table, + form, + isLoading, + }; +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx new file mode 100644 index 0000000..5f1aee1 --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/components/ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx @@ -0,0 +1,43 @@ +import { SegmentedControl, SegmentedControlProps } from "@mantine/core"; +import { FC } from "react"; + +export enum GroupStatisticsTable { + BY_DATES, + BY_CLIENTS, + BY_STATUSES, + BY_WAREHOUSES, + BY_MARKETPLACES, +} + +type Props = Omit; +const data = [ + { + label: "По датам", + value: GroupStatisticsTable.BY_DATES.toString(), + }, + { + label: "По клиентам", + value: GroupStatisticsTable.BY_CLIENTS.toString(), + }, + { + label: "По статусам", + value: GroupStatisticsTable.BY_STATUSES.toString(), + }, + { + label: "По складам отгрузки", + value: GroupStatisticsTable.BY_WAREHOUSES.toString(), + }, + { + label: "По маркетплейсам", + value: GroupStatisticsTable.BY_MARKETPLACES.toString(), + }, +]; + +export const ProfitTableSegmentedControl: FC = props => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/modals/ProfitChartFiltersModal.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/modals/ProfitChartFiltersModal.tsx new file mode 100644 index 0000000..3e7ce40 --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/modals/ProfitChartFiltersModal.tsx @@ -0,0 +1,45 @@ +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; +} + +export const ProfitChartFiltersModal = ({ form }: Props) => { + const [opened, { open, close }] = useDisclosure(); + + return ( + <> + + + form.setFieldValue("client", null)} + baseMarketplaceSelectProps={form.getInputProps("marketplace")} + onBaseMarketplaceClear={() => form.setFieldValue("marketplace", null)} + dealStatusSelectProps={form.getInputProps("dealStatus")} + onDealStatusClear={() => form.setFieldValue("dealStatus", null)} + /> + + + ); +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/tabs/ProfitTab/modals/ProfitTableFiltersModal.tsx b/src/pages/StatisticsPage/tabs/ProfitTab/modals/ProfitTableFiltersModal.tsx new file mode 100644 index 0000000..b95d3ee --- /dev/null +++ b/src/pages/StatisticsPage/tabs/ProfitTab/modals/ProfitTableFiltersModal.tsx @@ -0,0 +1,43 @@ +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 { UseFormReturnType } from "@mantine/form"; +import { useDisclosure } from "@mantine/hooks"; + + +type Props = { + form: UseFormReturnType; +} + +export const ProfitTableFiltersModal = ({ form }: Props) => { + const [opened, { open, close }] = useDisclosure(); + + return ( + <> + + + form.setFieldValue("groupTableBy", parseInt(value)), + }} + /> + + + ); +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/types/ChartFormFilters.ts b/src/pages/StatisticsPage/types/ChartFormFilters.ts new file mode 100644 index 0000000..3815461 --- /dev/null +++ b/src/pages/StatisticsPage/types/ChartFormFilters.ts @@ -0,0 +1,9 @@ +import { BaseMarketplaceSchema, ClientSchema } from "../../../client"; +import { DealStatusType } from "../../../shared/enums/DealStatus.ts"; + +export interface ChartFormFilters { + dateRange: [Date | null, Date | null]; + client: ClientSchema | null; + marketplace: BaseMarketplaceSchema | null; + dealStatus: DealStatusType | null; +} \ No newline at end of file diff --git a/src/pages/StatisticsPage/types/TableFormFilters.ts b/src/pages/StatisticsPage/types/TableFormFilters.ts new file mode 100644 index 0000000..689493e --- /dev/null +++ b/src/pages/StatisticsPage/types/TableFormFilters.ts @@ -0,0 +1,6 @@ +import { GroupStatisticsTable } from "../tabs/ProfitTab/components/ProfitTableSegmentedControl/ProfitTableSegmentedControl.tsx"; + +export interface TableFormFilters { + dateRange: [Date | null, Date | null]; + groupTableBy: GroupStatisticsTable; +} \ No newline at end of file diff --git a/src/pages/StatisticsPage/ui/StatisticsPage.module.css b/src/pages/StatisticsPage/ui/StatisticsPage.module.css new file mode 100644 index 0000000..2407bc3 --- /dev/null +++ b/src/pages/StatisticsPage/ui/StatisticsPage.module.css @@ -0,0 +1,20 @@ +.container { + display: flex; + flex-direction: column; + flex: 1; + gap: rem(10); +} + +.page-container { + display: flex; + flex-direction: row; + gap: rem(10); + height: 96vh; + flex-wrap: wrap; +} + +.top-panel { + padding: rem(5); + gap: rem(10); + display: flex; +} \ No newline at end of file diff --git a/src/pages/StatisticsPage/ui/StatisticsPage.tsx b/src/pages/StatisticsPage/ui/StatisticsPage.tsx new file mode 100644 index 0000000..491d599 --- /dev/null +++ b/src/pages/StatisticsPage/ui/StatisticsPage.tsx @@ -0,0 +1,38 @@ +import { useState } from "react"; +import { + StatisticsTab, + StatisticsTabSegmentedControl, +} from "../components/StatisticsTabSegmentedControl/StatisticsTabSegmentedControl.tsx"; +import styles from "./StatisticsPage.module.css"; +import PageBlock from "../../../components/PageBlock/PageBlock.tsx"; +import { ProfitTab } from "../tabs/ProfitTab/ProfitTab.tsx"; + +export const StatisticsPage = () => { + const [serviceType, setServiceType] = useState(StatisticsTab.PROFIT); + + const getBody = () => { + switch (serviceType) { + case StatisticsTab.PROFIT: + return ( + + ); + case StatisticsTab.SALARIES: + return ( + <>Статистика по ЗП + ); + } + }; + + return ( +
+ {/**/} + {/* setServiceType(parseInt(event))}*/} + {/* />*/} + {/**/} + {getBody()} +
+ ); +}; \ No newline at end of file diff --git a/src/pages/StatisticsPage/utils/dates.ts b/src/pages/StatisticsPage/utils/dates.ts new file mode 100644 index 0000000..f25830d --- /dev/null +++ b/src/pages/StatisticsPage/utils/dates.ts @@ -0,0 +1,15 @@ +export const getDefaultDates = (): [Date, Date] => { + const dateTo = new Date(); + const dateFrom = new Date(); + dateFrom.setDate(dateTo.getDate() - 28); + return [dateFrom, dateTo]; +}; + +export const dateToString = (date: Date | null) => { + if (date === null) return null; + const month = date.getMonth() + 1; + const day = date.getDate(); + const monthStr = month < 10 ? `0${month}` : month; + const dayStr = day < 10 ? `0${day}` : day; + return `${date.getFullYear()}-${monthStr}-${dayStr}`; +}; diff --git a/src/routes/statistics.lazy.tsx b/src/routes/statistics.lazy.tsx new file mode 100644 index 0000000..f5fef96 --- /dev/null +++ b/src/routes/statistics.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from "@tanstack/react-router"; +import { StatisticsPage } from "../pages/StatisticsPage"; + +export const Route = createLazyFileRoute("/statistics")({ + component: StatisticsPage, +});