feat: tags for expenses, filters by tags in statistics
This commit is contained in:
@@ -25,6 +25,7 @@ export type { BarcodeTemplateUpdateRequest } from './models/BarcodeTemplateUpdat
|
||||
export type { BarcodeTemplateUpdateResponse } from './models/BarcodeTemplateUpdateResponse';
|
||||
export type { BaseEnumListSchema } from './models/BaseEnumListSchema';
|
||||
export type { BaseEnumSchema } from './models/BaseEnumSchema';
|
||||
export type { BaseExpenseTagSchema } from './models/BaseExpenseTagSchema';
|
||||
export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema';
|
||||
export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouseSchema';
|
||||
export type { BillPaymentStatus } from './models/BillPaymentStatus';
|
||||
@@ -47,6 +48,7 @@ export type { CreateBarcodeTemplateAttributeRequest } from './models/CreateBarco
|
||||
export type { CreateBarcodeTemplateAttributeResponse } from './models/CreateBarcodeTemplateAttributeResponse';
|
||||
export type { CreateDealBillRequest } from './models/CreateDealBillRequest';
|
||||
export type { CreateDealBillResponse } from './models/CreateDealBillResponse';
|
||||
export type { CreateExpenseTagRequest } from './models/CreateExpenseTagRequest';
|
||||
export type { CreateMarketplaceRequest } from './models/CreateMarketplaceRequest';
|
||||
export type { CreateMarketplaceResponse } from './models/CreateMarketplaceResponse';
|
||||
export type { CreatePaymentRecordRequest } from './models/CreatePaymentRecordRequest';
|
||||
@@ -133,6 +135,7 @@ export type { DealUpdateServiceQuantityResponse } from './models/DealUpdateServi
|
||||
export type { DealUpdateServiceRequest } from './models/DealUpdateServiceRequest';
|
||||
export type { DealUpdateServiceResponse } from './models/DealUpdateServiceResponse';
|
||||
export type { DeleteExpenseResponse } from './models/DeleteExpenseResponse';
|
||||
export type { DeleteExpenseTagResponse } from './models/DeleteExpenseTagResponse';
|
||||
export type { DeleteMarketplaceRequest } from './models/DeleteMarketplaceRequest';
|
||||
export type { DeleteMarketplaceResponse } from './models/DeleteMarketplaceResponse';
|
||||
export type { DeletePaymentRecordRequest } from './models/DeletePaymentRecordRequest';
|
||||
@@ -147,6 +150,7 @@ export type { DeleteShiftResponse } from './models/DeleteShiftResponse';
|
||||
export type { DeleteShippingWarehouseRequest } from './models/DeleteShippingWarehouseRequest';
|
||||
export type { DeleteShippingWarehouseResponse } from './models/DeleteShippingWarehouseResponse';
|
||||
export type { ExpenseSchemaBase } from './models/ExpenseSchemaBase';
|
||||
export type { ExpenseTagSchema } from './models/ExpenseTagSchema';
|
||||
export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse';
|
||||
export type { FinishShiftResponse } from './models/FinishShiftResponse';
|
||||
export type { GetAllBarcodeTemplateAttributesResponse } from './models/GetAllBarcodeTemplateAttributesResponse';
|
||||
@@ -154,6 +158,7 @@ export type { GetAllBarcodeTemplateSizesResponse } from './models/GetAllBarcodeT
|
||||
export type { GetAllBarcodeTemplatesResponse } from './models/GetAllBarcodeTemplatesResponse';
|
||||
export type { GetAllBaseMarketplacesResponse } from './models/GetAllBaseMarketplacesResponse';
|
||||
export type { GetAllExpensesResponse } from './models/GetAllExpensesResponse';
|
||||
export type { GetAllExpenseTagsResponse } from './models/GetAllExpenseTagsResponse';
|
||||
export type { GetAllPayRatesResponse } from './models/GetAllPayRatesResponse';
|
||||
export type { GetAllPayrollSchemeResponse } from './models/GetAllPayrollSchemeResponse';
|
||||
export type { GetAllPositionsResponse } from './models/GetAllPositionsResponse';
|
||||
@@ -249,6 +254,8 @@ export type { TimeTrackingRecord } from './models/TimeTrackingRecord';
|
||||
export type { UpdateExpenseRequest } from './models/UpdateExpenseRequest';
|
||||
export type { UpdateExpenseResponse } from './models/UpdateExpenseResponse';
|
||||
export type { UpdateExpenseSchema } from './models/UpdateExpenseSchema';
|
||||
export type { UpdateExpenseTagRequest } from './models/UpdateExpenseTagRequest';
|
||||
export type { UpdateExpenseTagResponse } from './models/UpdateExpenseTagResponse';
|
||||
export type { UpdateMarketplaceRequest } from './models/UpdateMarketplaceRequest';
|
||||
export type { UpdateMarketplaceResponse } from './models/UpdateMarketplaceResponse';
|
||||
export type { UpdatePayRateRequest } from './models/UpdatePayRateRequest';
|
||||
|
||||
8
src/client/models/BaseExpenseTagSchema.ts
Normal file
8
src/client/models/BaseExpenseTagSchema.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type BaseExpenseTagSchema = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
9
src/client/models/CreateExpenseTagRequest.ts
Normal file
9
src/client/models/CreateExpenseTagRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { BaseExpenseTagSchema } from './BaseExpenseTagSchema';
|
||||
export type CreateExpenseTagRequest = {
|
||||
tag: BaseExpenseTagSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/DeleteExpenseTagResponse.ts
Normal file
9
src/client/models/DeleteExpenseTagResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DeleteExpenseTagResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ExpenseTagSchema } from './ExpenseTagSchema';
|
||||
import type { UserSchema } from './UserSchema';
|
||||
export type ExpenseSchemaBase = {
|
||||
id: number;
|
||||
@@ -10,5 +11,6 @@ export type ExpenseSchemaBase = {
|
||||
amount: number;
|
||||
createdByUser: UserSchema;
|
||||
spentDate: string;
|
||||
tags: Array<ExpenseTagSchema>;
|
||||
};
|
||||
|
||||
|
||||
9
src/client/models/ExpenseTagSchema.ts
Normal file
9
src/client/models/ExpenseTagSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ExpenseTagSchema = {
|
||||
name: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
9
src/client/models/GetAllExpenseTagsResponse.ts
Normal file
9
src/client/models/GetAllExpenseTagsResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ExpenseTagSchema } from './ExpenseTagSchema';
|
||||
export type GetAllExpenseTagsResponse = {
|
||||
tags: Array<ExpenseTagSchema>;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type PaginationInfoSchema = {
|
||||
totalPages: number;
|
||||
totalItems: number;
|
||||
totalPages?: number;
|
||||
totalItems?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,5 +8,6 @@ export type UpdateExpenseSchema = {
|
||||
comment?: (string | null);
|
||||
amount: number;
|
||||
spentDate: string;
|
||||
tags?: Array<string>;
|
||||
};
|
||||
|
||||
|
||||
9
src/client/models/UpdateExpenseTagRequest.ts
Normal file
9
src/client/models/UpdateExpenseTagRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ExpenseTagSchema } from './ExpenseTagSchema';
|
||||
export type UpdateExpenseTagRequest = {
|
||||
tag: ExpenseTagSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateExpenseTagResponse.ts
Normal file
9
src/client/models/UpdateExpenseTagResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateExpenseTagResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { CreateExpenseTagRequest } from '../models/CreateExpenseTagRequest';
|
||||
import type { DeleteExpenseResponse } from '../models/DeleteExpenseResponse';
|
||||
import type { DeleteExpenseTagResponse } from '../models/DeleteExpenseTagResponse';
|
||||
import type { GetAllExpensesResponse } from '../models/GetAllExpensesResponse';
|
||||
import type { GetAllExpenseTagsResponse } from '../models/GetAllExpenseTagsResponse';
|
||||
import type { UpdateExpenseRequest } from '../models/UpdateExpenseRequest';
|
||||
import type { UpdateExpenseResponse } from '../models/UpdateExpenseResponse';
|
||||
import type { UpdateExpenseTagRequest } from '../models/UpdateExpenseTagRequest';
|
||||
import type { UpdateExpenseTagResponse } from '../models/UpdateExpenseTagResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
@@ -75,4 +80,76 @@ export class ExpenseService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get All
|
||||
* @returns GetAllExpenseTagsResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getAllExpenseTags(): CancelablePromise<GetAllExpenseTagsResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/expense/get-all-tags',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update Expense
|
||||
* @returns UpdateExpenseTagResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createExpenseTag({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: CreateExpenseTagRequest,
|
||||
}): CancelablePromise<UpdateExpenseTagResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/expense/create-tag',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update Expense
|
||||
* @returns UpdateExpenseTagResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static updateExpenseTag({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: UpdateExpenseTagRequest,
|
||||
}): CancelablePromise<UpdateExpenseTagResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/expense/update-tag',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update Expense
|
||||
* @returns DeleteExpenseTagResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deleteExpenseTag({
|
||||
tagId,
|
||||
}: {
|
||||
tagId: number,
|
||||
}): CancelablePromise<DeleteExpenseTagResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/expense/delete-tag/{tag_id}',
|
||||
path: {
|
||||
'tag_id': tagId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
type Props<T> = {
|
||||
onCreate: (element: T) => void;
|
||||
type Props<T, C = void> = {
|
||||
onCreate: (element: T | C) => void;
|
||||
onChange: (element: T) => void;
|
||||
onDelete: (element: T) => void;
|
||||
};
|
||||
|
||||
export function useCRUD<T>(props: Props<T>) {
|
||||
const onCreate = (element: T) => {
|
||||
export function useCRUD<T, C>(props: Props<T, C>) {
|
||||
const onCreate = (element: T | C) => {
|
||||
props.onCreate(element);
|
||||
};
|
||||
const onChange = (element: T) => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import MarketplaceFormModal from "../pages/MarketplacesPage/modals/MarketplaceFo
|
||||
import ServicePriceCategoryForm from "../pages/ServicesPage/modals/ServicePriceCategoryForm.tsx";
|
||||
import ScanningModal from "./ScanningModal/ScanningModal.tsx";
|
||||
import ExpenseFormModal from "../pages/AdminPage/tabs/Expenses/modals/ExpenseFormModal.tsx";
|
||||
import ExpenseTagsModal from "../pages/AdminPage/tabs/Expenses/modals/ExpenseTagsModal.tsx";
|
||||
|
||||
export const modals = {
|
||||
enterDeadline: EnterDeadlineModal,
|
||||
@@ -50,4 +51,5 @@ export const modals = {
|
||||
servicePriceCategoryForm: ServicePriceCategoryForm,
|
||||
scanningModal: ScanningModal,
|
||||
expenseFormModal: ExpenseFormModal,
|
||||
expenseTagsModal: ExpenseTagsModal,
|
||||
};
|
||||
|
||||
10
src/pages/AdminPage/hooks/useExpenseTagsList.tsx
Normal file
10
src/pages/AdminPage/hooks/useExpenseTagsList.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
import { ExpenseService } from "../../../client";
|
||||
|
||||
const useExpenseTagsList = () =>
|
||||
ObjectList({
|
||||
queryFn: ExpenseService.getAllExpenseTags,
|
||||
getObjectsFn: response => response.tags,
|
||||
queryKey: "getAllExpenseTags",
|
||||
});
|
||||
export default useExpenseTagsList;
|
||||
@@ -9,6 +9,7 @@ import { Pagination, Flex, rem, Button, Tooltip, ActionIcon } from "@mantine/cor
|
||||
import { useEffect, useState } from "react";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { Expense } from "./types/Expense.tsx";
|
||||
|
||||
|
||||
export const ExpensesTab = () => {
|
||||
@@ -22,11 +23,12 @@ export const ExpensesTab = () => {
|
||||
const columns = useExpensesTableColumns();
|
||||
|
||||
useEffect(() => {
|
||||
if (!paginationInfo) return;
|
||||
setTotalPages(paginationInfo.totalPages);
|
||||
if (!paginationInfo ) return;
|
||||
if (!paginationInfo.totalPages) setTotalPages(0);
|
||||
else setTotalPages(paginationInfo.totalPages);
|
||||
}, [paginationInfo]);
|
||||
|
||||
const onUpdate = (expense: ExpenseSchemaBase) => {
|
||||
const onUpdate = (expense: Expense) => {
|
||||
ExpenseService.updateExpense({
|
||||
requestBody: {
|
||||
expense: {
|
||||
@@ -54,13 +56,14 @@ export const ExpensesTab = () => {
|
||||
};
|
||||
|
||||
const onEditClick = (expense: ExpenseSchemaBase) => {
|
||||
const expenseToEdit = { ...expense, tags: expense.tags.map(tag => tag.name) };
|
||||
modals.openContextModal({
|
||||
modal: "expenseFormModal",
|
||||
title: "Редактирование записи о расходах",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: event => onUpdate(event),
|
||||
element: expense,
|
||||
element: expenseToEdit,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -76,6 +79,19 @@ export const ExpensesTab = () => {
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onTagChange = async () => {
|
||||
await refetch();
|
||||
}
|
||||
|
||||
const onTagsEditClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "expenseTagsModal",
|
||||
title: "Редактирование тегов",
|
||||
withCloseButton: false,
|
||||
innerProps: { onChange: onTagChange },
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
@@ -91,12 +107,17 @@ export const ExpensesTab = () => {
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Flex p={rem(10)} gap={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Создать запись о расходах
|
||||
</Button>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onTagsEditClick()}>
|
||||
Редактировать теги
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
renderRowActions: ({ row }) => (
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { BaseExpenseTagSchema } from "../../../../../client";
|
||||
import { Button, Stack, TextInput } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
onCreate: (tag: BaseExpenseTagSchema) => void;
|
||||
}
|
||||
|
||||
const CreateExpenseTagForm = ({ onCreate }: Props) => {
|
||||
const [expenseTag, setExpenseTag] = useState<string>("");
|
||||
|
||||
const onCreateClick = () => {
|
||||
if (expenseTag.length === 0) {
|
||||
notifications.error({ message: "Нельзя добавить тег без названия" });
|
||||
return;
|
||||
}
|
||||
onCreate({ name: expenseTag });
|
||||
setExpenseTag("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={"Добавить тег"}
|
||||
value={expenseTag}
|
||||
onChange={event => setExpenseTag(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={onCreateClick}>
|
||||
Добавить
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateExpenseTagForm;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { TagsInput, TagsInputProps } from "@mantine/core";
|
||||
import useExpenseTagsList from "../../../hooks/useExpenseTagsList.tsx";
|
||||
|
||||
|
||||
type Props = Omit<TagsInputProps, "data">;
|
||||
|
||||
const ExpenseTagsInput = (props: Props) => {
|
||||
const { objects } = useExpenseTagsList();
|
||||
return (
|
||||
<TagsInput
|
||||
data={objects.map(object => object.name)}
|
||||
{...props}
|
||||
label={"Теги"}
|
||||
placeholder={"Выберите теги"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseTagsInput;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { CRUDTableProps } from "../../../../../types/CRUDTable.tsx";
|
||||
import { BaseExpenseTagSchema, ExpenseTagSchema } from "../../../../../client";
|
||||
import { FC, useState } from "react";
|
||||
import { ActionIcon, Flex, Stack, Tooltip } from "@mantine/core";
|
||||
import { IconCheck, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { useExpenseTagsTableColumns } from "../hooks/useExpenseTagsTableColumns.tsx";
|
||||
import CreateExpenseTagForm from "./CreateExpenseTagForm.tsx";
|
||||
|
||||
|
||||
type Props = CRUDTableProps<ExpenseTagSchema, BaseExpenseTagSchema>;
|
||||
|
||||
const ExpenseTagsTable: FC<Props> = ({ items, onCreate, onDelete, onChange }) => {
|
||||
const [editingTagId, setEditingTagId] = useState<number>(-1);
|
||||
const [tagName, setTagName] = useState<string>("");
|
||||
|
||||
const columns = useExpenseTagsTableColumns({ editingTagId, setTagName });
|
||||
|
||||
const onStartEditing = (tag: ExpenseTagSchema) => {
|
||||
setEditingTagId(tag.id);
|
||||
setTagName(tag.name);
|
||||
};
|
||||
|
||||
const onFinishEditing = (tag: ExpenseTagSchema) => {
|
||||
if (!onChange) return;
|
||||
if (tag.name !== tagName) {
|
||||
onChange({
|
||||
id: editingTagId,
|
||||
name: tagName,
|
||||
});
|
||||
}
|
||||
|
||||
setEditingTagId(-1);
|
||||
setTagName("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CreateExpenseTagForm onCreate={item => onCreate && onCreate(item)} />
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={
|
||||
{
|
||||
enableRowActions: true,
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
{
|
||||
editingTagId === row.original.id ? (
|
||||
<ActionIcon
|
||||
onClick={() => onFinishEditing(row.original)}
|
||||
variant={"default"}>
|
||||
<IconCheck />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<ActionIcon
|
||||
onClick={() => onStartEditing(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDelete && onDelete(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<ExpenseTagSchema>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseTagsTable;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { ExpenseTagSchema } from "../../../../../client";
|
||||
import { TextInput } from "@mantine/core";
|
||||
|
||||
|
||||
type Props = {
|
||||
editingTagId: number;
|
||||
setTagName: (tagName: string) => void;
|
||||
}
|
||||
|
||||
export const useExpenseTagsTableColumns = ({
|
||||
editingTagId, setTagName,
|
||||
}: Props) => {
|
||||
return useMemo<MRT_ColumnDef<ExpenseTagSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Наименование",
|
||||
Cell: ({ row }) => {
|
||||
if (row.original.id === editingTagId) {
|
||||
return (
|
||||
<TextInput
|
||||
defaultValue={row.original.name}
|
||||
onChange={event => setTagName(event.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return row.original.name;
|
||||
},
|
||||
},
|
||||
],
|
||||
[editingTagId],
|
||||
);
|
||||
};
|
||||
@@ -24,6 +24,11 @@ export const useExpensesTableColumns = () => {
|
||||
accessorKey: "amount",
|
||||
header: "Сумма",
|
||||
},
|
||||
{
|
||||
accessorKey: "tags",
|
||||
header: "Теги",
|
||||
Cell: ({ row }) => row.original.tags.map(tag => tag.name).join(", "),
|
||||
},
|
||||
{
|
||||
accessorKey: "createdByUser",
|
||||
header: "Создал запись",
|
||||
|
||||
@@ -2,10 +2,11 @@ import { ContextModalProps } from "@mantine/modals";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { Flex, NumberInput, rem, TextInput } from "@mantine/core";
|
||||
import BaseFormModal, { CreateEditFormProps } from "../../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { ExpenseSchemaBase } from "../../../../../client";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import ExpenseTagsInput from "../components/ExpenseTagsInput.tsx";
|
||||
import { Expense } from "../types/Expense.tsx";
|
||||
|
||||
type Props = CreateEditFormProps<ExpenseSchemaBase>;
|
||||
type Props = CreateEditFormProps<Expense>;
|
||||
|
||||
const ExpenseFormModal = ({
|
||||
context,
|
||||
@@ -13,11 +14,11 @@ const ExpenseFormModal = ({
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValue: Partial<ExpenseSchemaBase> = isEditing
|
||||
const initialValue: Partial<Expense> = isEditing
|
||||
? innerProps.element
|
||||
: {};
|
||||
|
||||
const form = useForm<Partial<ExpenseSchemaBase>>({
|
||||
const form = useForm<Partial<Expense>>({
|
||||
initialValues: initialValue,
|
||||
validate: {
|
||||
name: name => !name && "Необходимо указать наименование",
|
||||
@@ -58,6 +59,9 @@ const ExpenseFormModal = ({
|
||||
placeholder={"Укажите дату расхода"}
|
||||
valueFormat={"DD.MM.YYYY"}
|
||||
/>
|
||||
<ExpenseTagsInput
|
||||
{...form.getInputProps("tags")}
|
||||
/>
|
||||
</Flex>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { BaseExpenseTagSchema, ExpenseService, ExpenseTagSchema } from "../../../../../client";
|
||||
import useExpenseTagsList from "../../../hooks/useExpenseTagsList.tsx";
|
||||
import ExpenseTagsTable from "../components/ExpenseTagsTable.tsx";
|
||||
import { useCRUD } from "../../../../../hooks/useCRUD.tsx";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
onChange: () => Promise<void>;
|
||||
};
|
||||
|
||||
const ExpenseTagsModal = ({
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const { onChange: onTagChange } = innerProps;
|
||||
const { objects: expenseTags, refetch } = useExpenseTagsList();
|
||||
|
||||
const crud = useCRUD<ExpenseTagSchema, BaseExpenseTagSchema>({
|
||||
onChange: tag => {
|
||||
ExpenseService.updateExpenseTag({
|
||||
requestBody: {
|
||||
tag,
|
||||
},
|
||||
})
|
||||
.then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
await onTagChange();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
},
|
||||
onCreate: tag => {
|
||||
ExpenseService.createExpenseTag({
|
||||
requestBody: {
|
||||
tag,
|
||||
},
|
||||
})
|
||||
.then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
await onTagChange();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
},
|
||||
onDelete: (tag) => {
|
||||
ExpenseService.deleteExpenseTag({
|
||||
tagId: tag.id,
|
||||
})
|
||||
.then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ExpenseTagsTable {...crud} items={expenseTags} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseTagsModal;
|
||||
11
src/pages/AdminPage/tabs/Expenses/types/Expense.tsx
Normal file
11
src/pages/AdminPage/tabs/Expenses/types/Expense.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { UserSchema } from "../../../../../client";
|
||||
|
||||
export type Expense = {
|
||||
id: number;
|
||||
name: string;
|
||||
comment: string;
|
||||
amount: number;
|
||||
createdByUser: UserSchema;
|
||||
spentDate: string;
|
||||
tags: Array<string>;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { RefObject } from "react";
|
||||
import { BaseTableRef } from "../components/BaseTable/BaseTable.tsx";
|
||||
import { MRT_RowData } from "mantine-react-table";
|
||||
|
||||
export interface CRUDTableProps<T extends MRT_RowData> {
|
||||
export interface CRUDTableProps<T extends MRT_RowData, C = void> {
|
||||
items: T[];
|
||||
onCreate?: (item: T) => void;
|
||||
onCreate?: (item: C | T) => void;
|
||||
onDelete?: (item: T) => void;
|
||||
onChange?: (item: T) => void;
|
||||
onSelectionChange?: (selectedItems: T[]) => void;
|
||||
|
||||
Reference in New Issue
Block a user