feat: attributes in cards and projects
This commit is contained in:
@@ -25,6 +25,7 @@ export type { BarcodeTemplateSchema } from './models/BarcodeTemplateSchema';
|
||||
export type { BarcodeTemplateSizeSchema } from './models/BarcodeTemplateSizeSchema';
|
||||
export type { BarcodeTemplateUpdateRequest } from './models/BarcodeTemplateUpdateRequest';
|
||||
export type { BarcodeTemplateUpdateResponse } from './models/BarcodeTemplateUpdateResponse';
|
||||
export type { BaseAttributeSchema } from './models/BaseAttributeSchema';
|
||||
export type { BaseBoardSchema } from './models/BaseBoardSchema';
|
||||
export type { BaseEnumListSchema } from './models/BaseEnumListSchema';
|
||||
export type { BaseEnumSchema } from './models/BaseEnumSchema';
|
||||
@@ -123,6 +124,8 @@ export type { ClientSchema } from './models/ClientSchema';
|
||||
export type { ClientUpdateDetailsRequest } from './models/ClientUpdateDetailsRequest';
|
||||
export type { ClientUpdateRequest } from './models/ClientUpdateRequest';
|
||||
export type { ClientUpdateResponse } from './models/ClientUpdateResponse';
|
||||
export type { CreateAttributeRequest } from './models/CreateAttributeRequest';
|
||||
export type { CreateAttributeResponse } from './models/CreateAttributeResponse';
|
||||
export type { CreateBarcodeTemplateAttributeRequest } from './models/CreateBarcodeTemplateAttributeRequest';
|
||||
export type { CreateBarcodeTemplateAttributeResponse } from './models/CreateBarcodeTemplateAttributeResponse';
|
||||
export type { CreateBoardRequest } from './models/CreateBoardRequest';
|
||||
@@ -168,6 +171,7 @@ export type { CreateTaskResponse } from './models/CreateTaskResponse';
|
||||
export type { CreateTransactionTagRequest } from './models/CreateTransactionTagRequest';
|
||||
export type { CreateUserRequest } from './models/CreateUserRequest';
|
||||
export type { CreateUserResponse } from './models/CreateUserResponse';
|
||||
export type { DeleteAttributeResponse } from './models/DeleteAttributeResponse';
|
||||
export type { DeleteBoardResponse } from './models/DeleteBoardResponse';
|
||||
export type { DeleteBoxResponse } from './models/DeleteBoxResponse';
|
||||
export type { DeleteDepartmentResponse } from './models/DeleteDepartmentResponse';
|
||||
@@ -208,6 +212,7 @@ export type { GetAllBarcodeTemplateAttributesResponse } from './models/GetAllBar
|
||||
export type { GetAllBarcodeTemplateSizesResponse } from './models/GetAllBarcodeTemplateSizesResponse';
|
||||
export type { GetAllBarcodeTemplatesResponse } from './models/GetAllBarcodeTemplatesResponse';
|
||||
export type { GetAllBaseMarketplacesResponse } from './models/GetAllBaseMarketplacesResponse';
|
||||
export type { GetAllModulesResponse } from './models/GetAllModulesResponse';
|
||||
export type { GetAllPayRatesResponse } from './models/GetAllPayRatesResponse';
|
||||
export type { GetAllPayrollSchemeResponse } from './models/GetAllPayrollSchemeResponse';
|
||||
export type { GetAllPositionsResponse } from './models/GetAllPositionsResponse';
|
||||
@@ -218,6 +223,8 @@ export type { GetAllTransactionsRequest } from './models/GetAllTransactionsReque
|
||||
export type { GetAllTransactionsResponse } from './models/GetAllTransactionsResponse';
|
||||
export type { GetAllTransactionTagsResponse } from './models/GetAllTransactionTagsResponse';
|
||||
export type { GetAllUsersResponse } from './models/GetAllUsersResponse';
|
||||
export type { GetAttributesResponse } from './models/GetAttributesResponse';
|
||||
export type { GetAttributeTypesResponse } from './models/GetAttributeTypesResponse';
|
||||
export type { GetAvailableEmployeesToAssignResponse } from './models/GetAvailableEmployeesToAssignResponse';
|
||||
export type { GetAvailableUsersForDepartmentSectionResponse } from './models/GetAvailableUsersForDepartmentSectionResponse';
|
||||
export type { GetBarcodeTemplateByIdRequest } from './models/GetBarcodeTemplateByIdRequest';
|
||||
@@ -339,6 +346,10 @@ export type { TimeTrackingData } from './models/TimeTrackingData';
|
||||
export type { TimeTrackingRecord } from './models/TimeTrackingRecord';
|
||||
export type { TransactionSchemaBase } from './models/TransactionSchemaBase';
|
||||
export type { TransactionTagSchema } from './models/TransactionTagSchema';
|
||||
export type { UpdateAttributeRequest } from './models/UpdateAttributeRequest';
|
||||
export type { UpdateAttributeResponse } from './models/UpdateAttributeResponse';
|
||||
export type { UpdateAttributesRequest } from './models/UpdateAttributesRequest';
|
||||
export type { UpdateAttributesResponse } from './models/UpdateAttributesResponse';
|
||||
export type { UpdateBoardOrderRequest } from './models/UpdateBoardOrderRequest';
|
||||
export type { UpdateBoardOrderResponse } from './models/UpdateBoardOrderResponse';
|
||||
export type { UpdateBoardRequest } from './models/UpdateBoardRequest';
|
||||
@@ -352,6 +363,8 @@ export type { UpdateDepartmentSectionRequest } from './models/UpdateDepartmentSe
|
||||
export type { UpdateDepartmentSectionResponse } from './models/UpdateDepartmentSectionResponse';
|
||||
export type { UpdateMarketplaceRequest } from './models/UpdateMarketplaceRequest';
|
||||
export type { UpdateMarketplaceResponse } from './models/UpdateMarketplaceResponse';
|
||||
export type { UpdateModulesRequest } from './models/UpdateModulesRequest';
|
||||
export type { UpdateModulesResponse } from './models/UpdateModulesResponse';
|
||||
export type { UpdatePayRateRequest } from './models/UpdatePayRateRequest';
|
||||
export type { UpdatePayRateResponse } from './models/UpdatePayRateResponse';
|
||||
export type { UpdatePlanningWorkShiftRequest } from './models/UpdatePlanningWorkShiftRequest';
|
||||
@@ -394,6 +407,7 @@ export type { ValidationError } from './models/ValidationError';
|
||||
export type { WorkShiftRowSchema } from './models/WorkShiftRowSchema';
|
||||
export type { WorkShiftSchema } from './models/WorkShiftSchema';
|
||||
|
||||
export { AttributeService } from './services/AttributeService';
|
||||
export { AuthService } from './services/AuthService';
|
||||
export { BarcodeService } from './services/BarcodeService';
|
||||
export { BillingService } from './services/BillingService';
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
/* eslint-disable */
|
||||
import type { AttributeTypeSchema } from './AttributeTypeSchema';
|
||||
export type AttributeSchema = {
|
||||
id: number;
|
||||
label: string;
|
||||
name: string;
|
||||
isApplicableToGroup: boolean;
|
||||
isNullable: boolean;
|
||||
defaultValue: (boolean | number | string | null);
|
||||
typeId: number;
|
||||
description: string;
|
||||
id: number;
|
||||
type: AttributeTypeSchema;
|
||||
isDeleted: boolean;
|
||||
};
|
||||
|
||||
|
||||
14
src/client/models/BaseAttributeSchema.ts
Normal file
14
src/client/models/BaseAttributeSchema.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type BaseAttributeSchema = {
|
||||
label: string;
|
||||
name: string;
|
||||
isApplicableToGroup: boolean;
|
||||
isNullable: boolean;
|
||||
defaultValue: (boolean | number | string | null);
|
||||
typeId: number;
|
||||
description: string;
|
||||
};
|
||||
|
||||
@@ -14,5 +14,6 @@ export type CardGeneralInfoSchema = {
|
||||
boardId: number;
|
||||
statusId: number;
|
||||
isServicesProfitAccounted: boolean;
|
||||
clientId: (number | null);
|
||||
};
|
||||
|
||||
|
||||
9
src/client/models/CreateAttributeRequest.ts
Normal file
9
src/client/models/CreateAttributeRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { BaseAttributeSchema } from './BaseAttributeSchema';
|
||||
export type CreateAttributeRequest = {
|
||||
attribute: BaseAttributeSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/CreateAttributeResponse.ts
Normal file
9
src/client/models/CreateAttributeResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CreateAttributeResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/DeleteAttributeResponse.ts
Normal file
9
src/client/models/DeleteAttributeResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DeleteAttributeResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/GetAllModulesResponse.ts
Normal file
9
src/client/models/GetAllModulesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ModuleSchema } from './ModuleSchema';
|
||||
export type GetAllModulesResponse = {
|
||||
modules: Array<ModuleSchema>;
|
||||
};
|
||||
|
||||
9
src/client/models/GetAttributeTypesResponse.ts
Normal file
9
src/client/models/GetAttributeTypesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { AttributeTypeSchema } from './AttributeTypeSchema';
|
||||
export type GetAttributeTypesResponse = {
|
||||
types: Array<AttributeTypeSchema>;
|
||||
};
|
||||
|
||||
9
src/client/models/GetAttributesResponse.ts
Normal file
9
src/client/models/GetAttributesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { AttributeSchema } from './AttributeSchema';
|
||||
export type GetAttributesResponse = {
|
||||
attributes: Array<AttributeSchema>;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export type ModuleSchema = {
|
||||
id: number;
|
||||
key: string;
|
||||
label: string;
|
||||
isDeleted: boolean;
|
||||
};
|
||||
|
||||
|
||||
9
src/client/models/UpdateAttributeRequest.ts
Normal file
9
src/client/models/UpdateAttributeRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { AttributeSchema } from './AttributeSchema';
|
||||
export type UpdateAttributeRequest = {
|
||||
attribute: AttributeSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateAttributeResponse.ts
Normal file
9
src/client/models/UpdateAttributeResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateAttributeResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateAttributesRequest.ts
Normal file
9
src/client/models/UpdateAttributesRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateAttributesRequest = {
|
||||
projectId: number;
|
||||
attributeIds: Array<number>;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateAttributesResponse.ts
Normal file
9
src/client/models/UpdateAttributesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateAttributesResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateModulesRequest.ts
Normal file
9
src/client/models/UpdateModulesRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateModulesRequest = {
|
||||
projectId: number;
|
||||
moduleIds: Array<number>;
|
||||
};
|
||||
|
||||
9
src/client/models/UpdateModulesResponse.ts
Normal file
9
src/client/models/UpdateModulesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateModulesResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
99
src/client/services/AttributeService.ts
Normal file
99
src/client/services/AttributeService.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { CreateAttributeRequest } from '../models/CreateAttributeRequest';
|
||||
import type { CreateAttributeResponse } from '../models/CreateAttributeResponse';
|
||||
import type { DeleteAttributeResponse } from '../models/DeleteAttributeResponse';
|
||||
import type { GetAttributesResponse } from '../models/GetAttributesResponse';
|
||||
import type { GetAttributeTypesResponse } from '../models/GetAttributeTypesResponse';
|
||||
import type { UpdateAttributeRequest } from '../models/UpdateAttributeRequest';
|
||||
import type { UpdateAttributeResponse } from '../models/UpdateAttributeResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class AttributeService {
|
||||
/**
|
||||
* Get All
|
||||
* @returns GetAttributesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getAll(): CancelablePromise<GetAttributesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/attribute/',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create
|
||||
* @returns CreateAttributeResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static create({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: CreateAttributeRequest,
|
||||
}): CancelablePromise<CreateAttributeResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/attribute/',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update
|
||||
* @returns UpdateAttributeResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static update({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: UpdateAttributeRequest,
|
||||
}): CancelablePromise<UpdateAttributeResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PATCH',
|
||||
url: '/attribute/',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get Types
|
||||
* @returns GetAttributeTypesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getTypes(): CancelablePromise<GetAttributeTypesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/attribute/types',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Delete
|
||||
* @returns DeleteAttributeResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static delete({
|
||||
attributeId,
|
||||
}: {
|
||||
attributeId: number,
|
||||
}): CancelablePromise<DeleteAttributeResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/attribute/{attribute_id}',
|
||||
path: {
|
||||
'attribute_id': attributeId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,12 @@
|
||||
import type { CreateProjectRequest } from '../models/CreateProjectRequest';
|
||||
import type { CreateProjectResponse } from '../models/CreateProjectResponse';
|
||||
import type { DeleteProjectResponse } from '../models/DeleteProjectResponse';
|
||||
import type { GetAllModulesResponse } from '../models/GetAllModulesResponse';
|
||||
import type { GetProjectsResponse } from '../models/GetProjectsResponse';
|
||||
import type { UpdateAttributesRequest } from '../models/UpdateAttributesRequest';
|
||||
import type { UpdateAttributesResponse } from '../models/UpdateAttributesResponse';
|
||||
import type { UpdateModulesRequest } from '../models/UpdateModulesRequest';
|
||||
import type { UpdateModulesResponse } from '../models/UpdateModulesResponse';
|
||||
import type { UpdateProjectRequest } from '../models/UpdateProjectRequest';
|
||||
import type { UpdateProjectResponse } from '../models/UpdateProjectResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
@@ -84,4 +89,55 @@ export class ProjectService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get All Modules
|
||||
* @returns GetAllModulesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getAllModules(): CancelablePromise<GetAllModulesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/project/modules',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update Project Modules
|
||||
* @returns UpdateModulesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static updateProjectModules({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: UpdateModulesRequest,
|
||||
}): CancelablePromise<UpdateModulesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/project/modules',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update Project Attributes
|
||||
* @returns UpdateAttributesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static updateProjectAttributes({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: UpdateAttributesRequest,
|
||||
}): CancelablePromise<UpdateAttributesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/project/attributes',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AttributeSchema } from "../../../client";
|
||||
import { Checkbox, Tooltip } from "@mantine/core";
|
||||
import { Checkbox, NumberInput, TextInput } from "@mantine/core";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { ReactNode } from "react";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
|
||||
import { CardGeneralFormType } from "../../../pages/CardsPage/tabs/GeneralTab/GeneralTab.tsx";
|
||||
|
||||
type Props = {
|
||||
@@ -11,41 +10,65 @@ type Props = {
|
||||
}
|
||||
|
||||
const CardAttributeField = ({ attribute, form }: Props) => {
|
||||
const tooltipLabel = attribute.isApplicableToGroup
|
||||
? "Применяется ко всей группе"
|
||||
: "Применяется только к данной сделке";
|
||||
const type = attribute.type.type;
|
||||
|
||||
let component: ReactNode;
|
||||
const getDateValue = (): Date | null => {
|
||||
const value = form.getInputProps(attribute.name).value;
|
||||
if (!value) return null;
|
||||
return new Date(value);
|
||||
};
|
||||
|
||||
if (attribute.type.type === "bool") {
|
||||
component = (
|
||||
const labelData = {
|
||||
label: attribute.label,
|
||||
description: attribute.description,
|
||||
};
|
||||
|
||||
if (type === "bool") {
|
||||
return (
|
||||
<Checkbox
|
||||
label={attribute.label}
|
||||
{...labelData}
|
||||
{...form.getInputProps(attribute.name, { type: "checkbox" })}
|
||||
// defaultChecked={form.getInputProps(attribute.name, { type: "checkbox"}).value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else if (attribute.type.type === "datetime" || attribute.type.type === "date") {
|
||||
console.log("value = ");
|
||||
console.log(form.getInputProps(attribute.name).value);
|
||||
console.log("type = ")
|
||||
console.log(typeof form.getInputProps(attribute.name).value);
|
||||
|
||||
component = (
|
||||
} else if (type === "date") {
|
||||
return (
|
||||
<DatePickerInput
|
||||
label={attribute.label}
|
||||
{...labelData}
|
||||
{...form.getInputProps(attribute.name)}
|
||||
value={getDateValue()}
|
||||
clearable
|
||||
locale={"ru-RU"}
|
||||
valueFormat="DD.MM.YYYY"
|
||||
/>
|
||||
);
|
||||
} else if (type === "datetime") {
|
||||
return (
|
||||
<DateTimePicker
|
||||
{...labelData}
|
||||
{...form.getInputProps(attribute.name)}
|
||||
value={getDateValue()}
|
||||
clearable
|
||||
locale={"ru-RU"}
|
||||
valueFormat="DD.MM.YYYY HH:mm"
|
||||
/>
|
||||
);
|
||||
} else if (type === "str") {
|
||||
return (
|
||||
<TextInput
|
||||
{...labelData}
|
||||
{...form.getInputProps(attribute.name)}
|
||||
value={form.getInputProps(attribute.name).value ?? ""}
|
||||
/>
|
||||
);
|
||||
} else if (type === "int" || type === "float") {
|
||||
return (
|
||||
<NumberInput
|
||||
allowDecimal={type === "float"}
|
||||
{...labelData}
|
||||
{...form.getInputProps(attribute.name)}
|
||||
clearable={attribute.isNullable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltipLabel}>
|
||||
{component}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardAttributeField;
|
||||
|
||||
@@ -28,7 +28,9 @@ const useCardsDnd = ({
|
||||
cardId,
|
||||
},
|
||||
}).then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) {
|
||||
notifications.guess(ok, { message });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -144,7 +146,7 @@ const useCardsDnd = ({
|
||||
});
|
||||
};
|
||||
|
||||
const onDealDragEnd = async (result: DropResult) => {
|
||||
const onCardDragEnd = async (result: DropResult) => {
|
||||
if (result.combine) {
|
||||
return onCombine(result);
|
||||
}
|
||||
@@ -206,7 +208,7 @@ const useCardsDnd = ({
|
||||
|
||||
return {
|
||||
summaries,
|
||||
onDealDragEnd,
|
||||
onCardDragEnd,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styles from "../../../../pages/CardsPage/ui/CardsPage.module.css";
|
||||
import { Divider, Text, Title } from "@mantine/core";
|
||||
import getColumnColor from "../../Cards/CardsDndColumn/utils/getColumnColor.ts";
|
||||
import { CardSummary, StatusSchema } from "../../../../client";
|
||||
import { BoardSchema, CardSummary, StatusSchema } from "../../../../client";
|
||||
import { getPluralForm } from "../../../../shared/lib/utils.ts";
|
||||
import { sum } from "lodash";
|
||||
import { Draggable, Droppable } from "@hello-pangea/dnd";
|
||||
@@ -9,18 +9,21 @@ import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
|
||||
import { useContextMenu } from "mantine-contextmenu";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import useStatus from "./hooks/useStatus.tsx";
|
||||
import isModuleInProject, { Modules } from "../../../../pages/CardsPage/utils/isModuleInProject.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
board: BoardSchema | null;
|
||||
index: number;
|
||||
summaries: CardSummary[];
|
||||
dragState: DragState;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
const Status = ({ summaries, status, dragState, index, refetch }: Props) => {
|
||||
const isDropDisabled = dragState === DragState.DRAG_DEAL;
|
||||
const Status = ({ summaries, status, board, dragState, index, refetch }: Props) => {
|
||||
const isDropDisabled = dragState === DragState.DRAG_CARD;
|
||||
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, board?.project);
|
||||
|
||||
const {
|
||||
onEditStatusClick,
|
||||
@@ -34,7 +37,10 @@ const Status = ({ summaries, status, dragState, index, refetch }: Props) => {
|
||||
"сделки",
|
||||
"сделок",
|
||||
);
|
||||
return `${summaries.length} ${pluralForm}: ${sum(summaries.map(summary => summary.totalPrice)).toLocaleString("ru-RU")}₽`;
|
||||
const priceLabel = isServicesAndProductsIncluded
|
||||
? `: ${sum(summaries.map(summary => summary.totalPrice)).toLocaleString("ru-RU")}₽`
|
||||
: "";
|
||||
return `${summaries.length} ${pluralForm}` + priceLabel;
|
||||
};
|
||||
|
||||
const { showContextMenu } = useContextMenu();
|
||||
|
||||
@@ -44,6 +44,7 @@ const Statuses = ({
|
||||
summaries={filteredSummaries}
|
||||
index={index}
|
||||
status={status}
|
||||
board={selectedBoard}
|
||||
dragState={dragState}
|
||||
refetch={refetchBoards}
|
||||
/>
|
||||
|
||||
11
src/hooks/useAttributesList.tsx
Normal file
11
src/hooks/useAttributesList.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AttributeService } from "../client";
|
||||
import ObjectList from "./objectList.tsx";
|
||||
|
||||
const useAttributesList = () =>
|
||||
ObjectList({
|
||||
queryFn: AttributeService.getAll,
|
||||
getObjectsFn: response => response.attributes,
|
||||
queryKey: "getAllAttributes",
|
||||
});
|
||||
|
||||
export default useAttributesList;
|
||||
@@ -39,7 +39,7 @@ function usePollingEffect(
|
||||
// Immediately invoke the polling callback when the effect runs
|
||||
pollingCallback();
|
||||
|
||||
// Clean up function to clear the timeout if the component unmounts or dependencies change
|
||||
// Clean up function to clear the timeout if the components unmounts or dependencies change
|
||||
return () => {
|
||||
stopped = true; // This will prevent new timeouts from being scheduled
|
||||
if (timeoutIdRef.current) {
|
||||
|
||||
@@ -28,14 +28,16 @@ import ShippingProductModal from "../pages/CardsPage/tabs/ShippingTab/modals/Shi
|
||||
import DepartmentModal from "../pages/AdminPage/tabs/OrganizationalStructureTab/modals/DepartmentModal.tsx";
|
||||
import AddUserToDepartmentModal
|
||||
from "../pages/AdminPage/tabs/OrganizationalStructureTab/modals/AddUserToDepartmentModal.tsx";
|
||||
import AssignUserModal from "../pages/CardsPage/tabs/EmployeesTab/modals/AssignUserModal.tsx";
|
||||
import AssignEmployeeModal from "../pages/CardsPage/tabs/EmployeesTab/modals/AssignEmployeeModal.tsx";
|
||||
import ResidualProductModal from "../pages/ResiduesPage/modals/ResidualProductModal/ResidualProductModal.tsx";
|
||||
import NewReceiptModal from "../pages/ReceiptPage/components/NewReceipt/modals/NewReceiptModal.tsx";
|
||||
import ReceiptModal from "../pages/ReceiptPage/components/ReceiptEditing/modals/ReceiptModal.tsx";
|
||||
import SelectScannedProductModal from "../pages/ReceiptPage/modals/SelectScannedProductModal.tsx";
|
||||
import ProjectsModal from "../pages/CardsPage/modals/ProjectsModal/ProjectsModal.tsx";
|
||||
import BoardModal from "../pages/CardsPage/modals/BoardModal/BoardModal.tsx";
|
||||
import StatusModal from "../pages/CardsPage/modals/StatusModal/StatusModal.tsx";
|
||||
import AttributeModal from "../pages/CardsPage/drawers/ProjectsEditorDrawer/tabs/AttributesTab/modals/AttributeModal.tsx";
|
||||
import CreateProjectModal
|
||||
from "../pages/CardsPage/drawers/ProjectsEditorDrawer/tabs/ProjectsTab/modals/CreateProjectModal.tsx";
|
||||
|
||||
export const modals = {
|
||||
enterDeadline: EnterDeadlineModal,
|
||||
@@ -70,8 +72,9 @@ export const modals = {
|
||||
receiptModal: ReceiptModal,
|
||||
departmentModal: DepartmentModal,
|
||||
addUserToDepartmentModal: AddUserToDepartmentModal,
|
||||
assignUserModal: AssignUserModal,
|
||||
projectsModal: ProjectsModal,
|
||||
assignEmployeeModal: AssignEmployeeModal,
|
||||
boardModal: BoardModal,
|
||||
statusModal: StatusModal,
|
||||
attributeModal: AttributeModal,
|
||||
createProjectModal: CreateProjectModal,
|
||||
};
|
||||
|
||||
@@ -176,7 +176,7 @@ export const TransactionsTab = () => {
|
||||
/>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
style={{ alignSelf: "flex-end", marginTop: rem(10) }}
|
||||
withEdges
|
||||
onChange={event => setPage(event)}
|
||||
value={page}
|
||||
|
||||
@@ -8,16 +8,17 @@ import { UseFormReturnType } from "@mantine/form";
|
||||
import { CardsPageState } from "../../hooks/useCardsPageState.tsx";
|
||||
import React from "react";
|
||||
import { ProjectSchema } from "../../../../client";
|
||||
import { modals } from "@mantine/modals";
|
||||
import ObjectSelect from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import CardsTableFiltersModal from "../../modals/CardsTableFiltersModal.tsx";
|
||||
import { useProjectsEditorContext } from "../../contexts/ProjectsEditorContext.tsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
|
||||
type Props = {
|
||||
displayMode: DisplayMode;
|
||||
setDisplayMode: React.Dispatch<React.SetStateAction<DisplayMode>>;
|
||||
form: UseFormReturnType<CardsPageState>;
|
||||
projects: ProjectSchema[];
|
||||
refetchProjects: () => void;
|
||||
}
|
||||
|
||||
const CardsPageHeader = ({
|
||||
@@ -25,30 +26,27 @@ const CardsPageHeader = ({
|
||||
setDisplayMode,
|
||||
form,
|
||||
projects,
|
||||
refetchProjects,
|
||||
}: Props) => {
|
||||
const openModal = () => {
|
||||
modals.openContextModal({
|
||||
modal: "projectsModal",
|
||||
title: "Проекты",
|
||||
innerProps: {
|
||||
onUpdate: refetchProjects,
|
||||
},
|
||||
});
|
||||
};
|
||||
const { openProjectsEditor } = useProjectsEditorContext();
|
||||
const userRole = useSelector((state: RootState) => state.auth.role);
|
||||
const isAdmin = userRole === "admin";
|
||||
|
||||
const getHeaderInputsBoard = () => {
|
||||
return (
|
||||
<div className={styles["top-panel"]}
|
||||
style={{
|
||||
display: displayMode === DisplayMode.BOARD ? "flex" : "none",
|
||||
}}>
|
||||
<ActionIcon
|
||||
size={"lg"}
|
||||
onClick={openModal}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
<div
|
||||
className={styles["top-panel"]}
|
||||
style={{
|
||||
display: displayMode === DisplayMode.BOARD ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
{isAdmin && (
|
||||
<ActionIcon
|
||||
size={"lg"}
|
||||
onClick={openProjectsEditor}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ObjectSelect
|
||||
placeholder={"Выберите проект"}
|
||||
data={projects}
|
||||
|
||||
55
src/pages/CardsPage/contexts/ProjectsEditorContext.tsx
Normal file
55
src/pages/CardsPage/contexts/ProjectsEditorContext.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { createContext, FC, useContext } from "react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
type ProjectsEditorContextState = {
|
||||
openedProjectsEditor: boolean;
|
||||
openProjectsEditor: () => void;
|
||||
closeProjectsEditor: () => void;
|
||||
onUpdate: () => void;
|
||||
};
|
||||
|
||||
const ProjectsEditorContext = createContext<ProjectsEditorContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
type ProjectsEditorContextStateProps = {
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
const useProjectsEditorContextState = ({ onUpdate }: ProjectsEditorContextStateProps) => {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return {
|
||||
openedProjectsEditor: opened,
|
||||
openProjectsEditor: open,
|
||||
closeProjectsEditor: close,
|
||||
onUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
type ProjectsEditorContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
} & ProjectsEditorContextStateProps;
|
||||
|
||||
export const ProjectsEditorContextProvider: FC<ProjectsEditorContextProviderProps> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const state = useProjectsEditorContextState(props);
|
||||
|
||||
return (
|
||||
<ProjectsEditorContext.Provider value={state}>
|
||||
{children}
|
||||
</ProjectsEditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useProjectsEditorContext = () => {
|
||||
const context = useContext(ProjectsEditorContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useProjectsEditorContext must be used within a ProjectsEditorContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -24,7 +24,7 @@ const CardEditDrawerStatusChangeTable = () => {
|
||||
return <CardStatusChangeTable items={statusHistory} />;
|
||||
};
|
||||
|
||||
const useDealEditDrawerState = () => {
|
||||
const useCardEditDrawerState = () => {
|
||||
const { selectedCard, setSelectedCard } = useCardPageContext();
|
||||
return {
|
||||
isVisible: selectedCard !== undefined,
|
||||
@@ -33,7 +33,7 @@ const useDealEditDrawerState = () => {
|
||||
};
|
||||
|
||||
const CardEditDrawer: FC = () => {
|
||||
const { isVisible, onClose } = useDealEditDrawerState();
|
||||
const { isVisible, onClose } = useCardEditDrawerState();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { selectedCard } = useCardPageContext();
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.deal-container {
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: rem(10);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deal-container-wrapper {
|
||||
.card-container-wrapper {
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
}
|
||||
|
||||
.deal-container-buttons {
|
||||
.card-container-buttons {
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import { Button, Drawer, Flex, rem, TextInput } from "@mantine/core";
|
||||
import DealsTable from "./components/tables/DealsTable/DealsTable.tsx";
|
||||
import CardsTable from "./components/tables/CardsTable/CardsTable.tsx";
|
||||
import Preview from "./components/Preview/Preview.tsx";
|
||||
import styles from "./CardPrefillDrawer.module.css";
|
||||
import BaseMarketplaceSelect from "../../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||
import usePrefillDeal from "./hooks/usePrefillDeal.tsx";
|
||||
import usePrefillCard from "./hooks/usePrefillCard.tsx";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { usePrefillCardContext } from "../../contexts/PrefillCardContext.tsx";
|
||||
|
||||
const CardPrefillDrawer: FC = () => {
|
||||
const { prefillOpened, prefillOnClose, selectedPrefillCard, setPrefillCard, prefillCard } = usePrefillCardContext();
|
||||
const { data, form } = usePrefillDeal();
|
||||
const { data, form } = usePrefillCard();
|
||||
|
||||
useEffect(() => {
|
||||
if (prefillOpened) return;
|
||||
@@ -33,8 +33,8 @@ const CardPrefillDrawer: FC = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className={styles["deal-container"]}>
|
||||
<div className={styles["deal-container-wrapper"]}>
|
||||
<div className={styles["card-container"]}>
|
||||
<div className={styles["card-container-wrapper"]}>
|
||||
<div className={styles["top-panel"]}>
|
||||
<TextInput
|
||||
placeholder={"Введите название / id"}
|
||||
@@ -49,7 +49,7 @@ const CardPrefillDrawer: FC = () => {
|
||||
{...form.getInputProps("marketplace")}
|
||||
/>
|
||||
</div>
|
||||
<DealsTable items={data} />
|
||||
<CardsTable items={data} />
|
||||
<Flex direction={"row"} gap="sm">
|
||||
<Button mt={10} w={"100%"} onClick={() => {
|
||||
if (!selectedPrefillCard) {
|
||||
|
||||
@@ -11,21 +11,21 @@
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.deal-container {
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: rem(10);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deal-container-wrapper {
|
||||
.card-container-wrapper {
|
||||
width: 100%;
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
}
|
||||
|
||||
.deal-container-buttons {
|
||||
.card-container-buttons {
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import styles from "./Preview.module.css";
|
||||
import { ScrollArea, Skeleton, Title } from "@mantine/core";
|
||||
import DealServicesTable from "../tables/DealServicesTable/DealServicesTable.tsx";
|
||||
import CardServicesTable from "../tables/CardServicesTable/CardServicesTable.tsx";
|
||||
import ProductPreview from "../ProductPreview/ProductPreview.tsx";
|
||||
import { usePrefillCardContext } from "../../../../contexts/PrefillCardContext.tsx";
|
||||
|
||||
@@ -28,7 +28,7 @@ const Preview: FC = () => {
|
||||
|
||||
return (
|
||||
<div className={styles["container"]}>
|
||||
<div className={styles["deal-container-wrapper"]}>
|
||||
<div className={styles["card-container-wrapper"]}>
|
||||
<ScrollArea offsetScrollbars={"y"} w={"100%"}>
|
||||
<div style={{ height: "93vh" }}>
|
||||
<Skeleton visible={!selectedPrefillCard}>
|
||||
@@ -37,7 +37,7 @@ const Preview: FC = () => {
|
||||
{getTotalPrice().toLocaleString("ru")}₽
|
||||
</Title>
|
||||
|
||||
<DealServicesTable items={selectedPrefillCard?.services} />
|
||||
<CardServicesTable items={selectedPrefillCard?.services} />
|
||||
|
||||
<div className={styles["products-list"]}>
|
||||
{selectedPrefillCard?.products.map(product => (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { Flex, rem, Title } from "@mantine/core";
|
||||
import { CardServiceSchema, CardSummary } from "../../../../../../../client";
|
||||
import useDealServicesTableColumns from "./columns.tsx";
|
||||
import useCardServicesTableColumns from "./columns.tsx";
|
||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
@@ -9,8 +9,8 @@ type Props = {
|
||||
items?: CardServiceSchema[];
|
||||
};
|
||||
|
||||
const DealServicesTable: FC<Props> = ({ items }) => {
|
||||
const columns = useDealServicesTableColumns();
|
||||
const CardServicesTable: FC<Props> = ({ items }) => {
|
||||
const columns = useCardServicesTableColumns();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -55,4 +55,4 @@ const DealServicesTable: FC<Props> = ({ items }) => {
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
export default DealServicesTable;
|
||||
export default CardServicesTable;
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { CardServiceSchema } from "../../../../../../../client";
|
||||
|
||||
const useDealServicesTableColumns = () => {
|
||||
const useCardServicesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<CardServiceSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
@@ -27,4 +27,4 @@ const useDealServicesTableColumns = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default useDealServicesTableColumns;
|
||||
export default useCardServicesTableColumns;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import useDealsTableColumns from "./columns.tsx";
|
||||
import useCardsTableColumns from "./columns.tsx";
|
||||
import { CardSummary } from "../../../../../../../client";
|
||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { usePrefillCardContext } from "../../../../../contexts/PrefillCardContext.tsx";
|
||||
@@ -8,9 +8,9 @@ type Props = {
|
||||
items: CardSummary[];
|
||||
};
|
||||
|
||||
const DealsTable: FC<Props> = ({ items }) => {
|
||||
const CardsTable: FC<Props> = ({ items }) => {
|
||||
const { selectPrefillCard } = usePrefillCardContext();
|
||||
const columns = useDealsTableColumns();
|
||||
const columns = useCardsTableColumns();
|
||||
const defaultSorting = [{ id: "createdAt", desc: false }];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -45,4 +45,4 @@ const DealsTable: FC<Props> = ({ items }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DealsTable;
|
||||
export default CardsTable;
|
||||
@@ -4,7 +4,7 @@ import { ActionIcon, Image, Radio } from "@mantine/core";
|
||||
import { CardSummary } from "../../../../../../../client";
|
||||
import { usePrefillCardContext } from "../../../../../contexts/PrefillCardContext.tsx";
|
||||
|
||||
const useDealsTableColumns = () => {
|
||||
const useCardsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<CardSummary>[]>(
|
||||
() => [
|
||||
{
|
||||
@@ -69,4 +69,4 @@ const useDealsTableColumns = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default useDealsTableColumns;
|
||||
export default useCardsTableColumns;
|
||||
@@ -2,13 +2,14 @@ import { useForm } from "@mantine/form";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BaseMarketplaceSchema } from "../../../../../client";
|
||||
import { useCardSummariesFull } from "../../../hooks/useCardSummaries.tsx";
|
||||
import isModuleInProject, { Modules } from "../../../utils/isModuleInProject.ts";
|
||||
|
||||
type State = {
|
||||
idOrName: string | null;
|
||||
marketplace: BaseMarketplaceSchema | null;
|
||||
};
|
||||
|
||||
const usePrefillDeal = () => {
|
||||
const usePrefillCard = () => {
|
||||
const { objects } = useCardSummariesFull();
|
||||
const form = useForm<State>({
|
||||
initialValues: {
|
||||
@@ -20,14 +21,16 @@ const usePrefillDeal = () => {
|
||||
|
||||
const applyFilters = () => {
|
||||
let result = objects;
|
||||
|
||||
result = result.filter(obj => isModuleInProject(Modules.SERVICES_AND_PRODUCTS, obj.board.project));
|
||||
|
||||
if (form.values.idOrName) {
|
||||
if (isNaN(parseInt(form.values.idOrName))) {
|
||||
const name: string = form.values.idOrName.toLowerCase();
|
||||
result = result.filter(
|
||||
obj => obj.name.toLowerCase().search(name) !== -1,
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const id = parseInt(form.values.idOrName);
|
||||
result = result.filter(
|
||||
obj => obj.id === id,
|
||||
@@ -49,4 +52,4 @@ const usePrefillDeal = () => {
|
||||
return { data, form };
|
||||
};
|
||||
|
||||
export default usePrefillDeal;
|
||||
export default usePrefillCard;
|
||||
@@ -5,20 +5,20 @@
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.deal-container {
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: rem(10);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deal-container-wrapper {
|
||||
.card-container-wrapper {
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
}
|
||||
|
||||
.deal-container-buttons {
|
||||
.card-container-buttons {
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -44,7 +44,7 @@ const PrefillCardsWithExcelDrawer = ({ board }: Props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className={styles["deal-container"]}>
|
||||
<div className={styles["card-container"]}>
|
||||
{getBody()}
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
@@ -24,7 +24,7 @@ const ProductsPreview = ({ status }: Props) => {
|
||||
|
||||
const getProductsData = () => {
|
||||
return barcodeProductsMap.entries().map(([barcode, productsData]) => (
|
||||
<div key={barcode} className={styles["deal-container-wrapper"]}>
|
||||
<div key={barcode} className={styles["card-container-wrapper"]}>
|
||||
<Stack>
|
||||
<Title order={5}>
|
||||
{getTitle(barcode, productsData)}
|
||||
@@ -57,7 +57,7 @@ const ProductsPreview = ({ status }: Props) => {
|
||||
variant="outline"
|
||||
type="submit"
|
||||
>
|
||||
Создать сделки
|
||||
Создать
|
||||
</Button>
|
||||
<ParsingResultsTooltip />
|
||||
</Group>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.container-wrapper {
|
||||
flex: 1;
|
||||
height: 95vh;
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Box, Drawer, rem, Tabs } from "@mantine/core";
|
||||
import { IconSettings, IconSubtask } from "@tabler/icons-react";
|
||||
import { ReactNode } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useProjectsEditorContext } from "../../contexts/ProjectsEditorContext.tsx";
|
||||
import ProjectsTab from "./tabs/ProjectsTab/ProjectsTab.tsx";
|
||||
import AttributesTab from "./tabs/AttributesTab/AttributesTab.tsx";
|
||||
|
||||
|
||||
const ProjectsEditorDrawer = () => {
|
||||
const { closeProjectsEditor, openedProjectsEditor } = useProjectsEditorContext();
|
||||
|
||||
const getTabPanel = (value: string, component: ReactNode) => {
|
||||
return (
|
||||
<Tabs.Panel value={value}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<Box
|
||||
h={"100%"}
|
||||
w={"100%"}
|
||||
p={rem(10)}>
|
||||
{component}
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
size={"calc(100vw - 150px)"}
|
||||
position={"right"}
|
||||
onClose={closeProjectsEditor}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
withCloseButton={false}
|
||||
opened={openedProjectsEditor}
|
||||
styles={{
|
||||
body: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: rem(10),
|
||||
},
|
||||
}}>
|
||||
<Tabs
|
||||
defaultValue={"projects"}
|
||||
flex={1}
|
||||
variant={"outline"}
|
||||
orientation={"vertical"}
|
||||
keepMounted={false}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab
|
||||
value={"projects"}
|
||||
leftSection={<IconSettings />}>
|
||||
Проекты
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value={"attributes"}
|
||||
leftSection={<IconSubtask />}>
|
||||
Атрибуты
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
{getTabPanel("projects", <ProjectsTab />)}
|
||||
{getTabPanel("attributes", <AttributesTab />)}
|
||||
</Tabs>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsEditorDrawer;
|
||||
@@ -0,0 +1,112 @@
|
||||
import { BaseTable } from "../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useAttributesTableColumns from "./hooks/attributesTableColumns.tsx";
|
||||
import useAttributesList from "../../../../../../hooks/useAttributesList.tsx";
|
||||
import { ActionIcon, Flex, Group, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import InlineButton from "../../../../../../components/InlineButton/InlineButton.tsx";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { AttributeSchema, AttributeService } from "../../../../../../client";
|
||||
import { notifications } from "../../../../../../shared/lib/notifications.ts";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
const AttributesTab = () => {
|
||||
const columns = useAttributesTableColumns();
|
||||
const { objects: attributes, refetch: refetchAttributes } = useAttributesList();
|
||||
|
||||
const onCreateAttributeClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "attributeModal",
|
||||
title: "Создание атрибута",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
refetchAttributes,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onEditAttributeClick = (attribute: AttributeSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "attributeModal",
|
||||
title: "Редактирование атрибута",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
refetchAttributes,
|
||||
attribute,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttribute = (attribute: AttributeSchema) => {
|
||||
AttributeService.delete({
|
||||
attributeId: attribute.id,
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
refetchAttributes();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onDeleteAttributeClick = (attribute: AttributeSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление атрибута",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены, что хотите удалить атрибут "{attribute.label}"?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => deleteAttribute(attribute),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Group>
|
||||
<InlineButton onClick={onCreateAttributeClick}>
|
||||
<IconPlus />
|
||||
Добавить атрибут
|
||||
</InlineButton>
|
||||
</Group>
|
||||
<BaseTable
|
||||
data={attributes}
|
||||
columns={columns}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableBottomToolbar: false,
|
||||
enableRowActions: true,
|
||||
enableRowVirtualization: true,
|
||||
mantineTableContainerProps: { style: { maxHeight: "86vh" } },
|
||||
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditAttributeClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteAttributeClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<AttributeSchema>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributesTab;
|
||||
@@ -0,0 +1,21 @@
|
||||
import ObjectSelect, { ObjectSelectProps } from "../../../../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import { AttributeTypeSchema } from "../../../../../../../client";
|
||||
import useAttributeTypesList from "../hooks/useAttributeTypesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<AttributeTypeSchema>, "data">;
|
||||
|
||||
const AttributeTypeSelect = (props: Props) => {
|
||||
const { objects: attributeTypes } = useAttributeTypesList();
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
label={"Тип атрибута"}
|
||||
getLabelFn={type => type.name}
|
||||
getValueFn={type => type.id.toString()}
|
||||
data={attributeTypes}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributeTypeSelect;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Checkbox, NumberInput, TextInput } from "@mantine/core";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
|
||||
import { AttributeSchema } from "../../../../../../../client";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturnType<Partial<AttributeSchema>>;
|
||||
}
|
||||
|
||||
const DefaultAttributeValueInput = ({ form }: Props) => {
|
||||
const type = form.values.type?.type;
|
||||
const label = "Значение по умолчанию";
|
||||
const inputName = "defaultValue";
|
||||
|
||||
if (type === "bool") {
|
||||
return (
|
||||
<Checkbox
|
||||
label={label}
|
||||
{...form.getInputProps(inputName, { type: "checkbox" })}
|
||||
/>
|
||||
);
|
||||
} else if (type === "date") {
|
||||
return (
|
||||
<DatePickerInput
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={form.values.defaultValue ? new Date(String(form.values.defaultValue)) : null}
|
||||
clearable
|
||||
locale={"ru-RU"}
|
||||
valueFormat="DD.MM.YYYY"
|
||||
/>
|
||||
);
|
||||
} else if (type === "datetime") {
|
||||
return (
|
||||
<DateTimePicker
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={form.values.defaultValue ? new Date(String(form.values.defaultValue)) : null}
|
||||
clearable
|
||||
locale={"ru-RU"}
|
||||
valueFormat="DD.MM.YYYY HH:mm"
|
||||
/>
|
||||
);
|
||||
} else if (type === "str") {
|
||||
return (
|
||||
<TextInput
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={form.getInputProps(inputName).value ?? ""}
|
||||
/>
|
||||
);
|
||||
} else if (type === "int" || type === "float") {
|
||||
return (
|
||||
<NumberInput
|
||||
allowDecimal={type === "float"}
|
||||
label={label}
|
||||
{...form.getInputProps(inputName)}
|
||||
value={Number(form.values.defaultValue)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default DefaultAttributeValueInput;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { AttributeSchema } from "../../../../../../../client";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
import { formatDate, formatDateTime } from "../../../../../../../types/utils.ts";
|
||||
|
||||
|
||||
const useAttributesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<AttributeSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "label",
|
||||
},
|
||||
{
|
||||
header: "Тип",
|
||||
accessorKey: "type.name",
|
||||
},
|
||||
{
|
||||
header: "Значение по умолчанию",
|
||||
accessorKey: "defaultValue",
|
||||
Cell: ({ cell, row }) => {
|
||||
const value = cell.getValue();
|
||||
if (value === null) return <>-</>;
|
||||
|
||||
const type = row.original.type.type;
|
||||
if (type === "datetime") {
|
||||
return formatDateTime(value as string);
|
||||
}
|
||||
if (type === "date") {
|
||||
return formatDate(value as string);
|
||||
}
|
||||
if (type === "bool") {
|
||||
return value ? <IconCheck /> : <IconX />;
|
||||
}
|
||||
|
||||
return <>{value}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Синхронизировано в группе",
|
||||
accessorKey: "isApplicableToGroup",
|
||||
Cell: ({ cell }) => cell.getValue() ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconX />
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Может быть пустым",
|
||||
accessorKey: "isNullable",
|
||||
Cell: ({ cell }) => cell.getValue() ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconX />
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Описаниие",
|
||||
accessorKey: "description",
|
||||
}
|
||||
],
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
export default useAttributesTableColumns;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AttributeService } from "../../../../../../../client";
|
||||
import ObjectList from "../../../../../../../hooks/objectList.tsx";
|
||||
|
||||
const useAttributeTypesList = () =>
|
||||
ObjectList({
|
||||
queryFn: AttributeService.getTypes,
|
||||
getObjectsFn: response => response.types,
|
||||
queryKey: "getAllAttributeTypes",
|
||||
});
|
||||
|
||||
export default useAttributeTypesList;
|
||||
@@ -0,0 +1,171 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Checkbox, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import AttributeTypeSelect from "../components/AttributeTypeSelect.tsx";
|
||||
import { AttributeSchema, AttributeService } from "../../../../../../../client";
|
||||
import { convertRussianToSnakeCase } from "../../../utils/stringConverting.ts";
|
||||
import { useForm } from "@mantine/form";
|
||||
import DefaultAttributeValueInput from "../components/DefaultAttributeValueInput.tsx";
|
||||
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
refetchAttributes: () => void;
|
||||
attribute?: AttributeSchema;
|
||||
};
|
||||
|
||||
const AttributeModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "attribute" in innerProps;
|
||||
const [isInitial, setIsInitial] = useState(true);
|
||||
const [isNullableInputShown, setIsNullableInputShown] = useState(true);
|
||||
const [isDefaultValueInputShown, setIsDefaultValueInputShown] = useState(true);
|
||||
const closeModal = () => context.closeContextModal(id);
|
||||
|
||||
const form = useForm<Partial<AttributeSchema>>({
|
||||
initialValues: isEditing
|
||||
? innerProps.attribute
|
||||
: {
|
||||
label: "",
|
||||
name: "",
|
||||
type: undefined,
|
||||
isApplicableToGroup: false,
|
||||
isNullable: false,
|
||||
defaultValue: null,
|
||||
description: "",
|
||||
},
|
||||
validate: {
|
||||
label: label => !label?.trim() && "Название не заполнено",
|
||||
name: name => !name?.trim() && "Название генерирует некорректный уникальный ключ. Измените название",
|
||||
type: type => !type && "Тип атрибута не выбран",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldValue("name", convertRussianToSnakeCase(form.values.label ?? ""));
|
||||
}, [form.values.label]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDefaultValueInputShown(false);
|
||||
const type = form.values.type?.type;
|
||||
setIsNullableInputShown(type !== "bool");
|
||||
|
||||
if (!isInitial) {
|
||||
if (type === "bool") {
|
||||
form.setFieldValue("isNullable", false);
|
||||
form.setFieldValue("defaultValue", false);
|
||||
} else {
|
||||
form.setFieldValue("defaultValue", null);
|
||||
}
|
||||
}
|
||||
setIsInitial(false);
|
||||
setIsDefaultValueInputShown(true);
|
||||
}, [form.values.type?.id]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
if (form.values.defaultValue === null && !form.values.isNullable) {
|
||||
notifications.error({ message: "Укажите значение по умолчанию или разрешите пустое значение." });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const onCreate = (attribute: AttributeSchema) => {
|
||||
AttributeService.create({
|
||||
requestBody: {
|
||||
attribute: {
|
||||
...attribute,
|
||||
typeId: attribute.type.id,
|
||||
name: attribute.name.trim(),
|
||||
label: attribute.label.trim(),
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
innerProps.refetchAttributes();
|
||||
closeModal();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onChange = (attribute: AttributeSchema) => {
|
||||
AttributeService.update({
|
||||
requestBody: {
|
||||
attribute: {
|
||||
...attribute,
|
||||
typeId: attribute.type.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
innerProps.refetchAttributes();
|
||||
closeModal();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onSubmit = (values: AttributeSchema) => {
|
||||
if (!validate()) return;
|
||||
if (isEditing) {
|
||||
onChange(values);
|
||||
} else {
|
||||
onCreate(values);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values as AttributeSchema))}>
|
||||
<Stack gap={"md"}>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
{...form.getInputProps("label")}
|
||||
/>
|
||||
<TextInput
|
||||
label={"Уникальный ключ"}
|
||||
disabled
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<AttributeTypeSelect
|
||||
disabled={isEditing}
|
||||
{...form.getInputProps("type")}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Значение синхронизировано в группе"}
|
||||
{...form.getInputProps("isApplicableToGroup", { type: "checkbox" })}
|
||||
/>
|
||||
{isNullableInputShown && (
|
||||
<Checkbox
|
||||
label={"Может быть пустым"}
|
||||
{...form.getInputProps("isNullable", { type: "checkbox" })}
|
||||
/>
|
||||
)}
|
||||
{form.values.type && isDefaultValueInputShown && (
|
||||
<DefaultAttributeValueInput form={form} />
|
||||
)}
|
||||
<Textarea
|
||||
label={"Описание"}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributeModal;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Group } from "@mantine/core";
|
||||
import { ProjectSchema } from "../../../../../../client";
|
||||
import ModulesPicker from "./components/ModulesPicker.tsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import AttributesPicker from "./components/AttributesPicker.tsx";
|
||||
import useProjects from "../../../../hooks/useProjects.tsx";
|
||||
import ProjectsEditor from "./components/ProjectsEditor.tsx";
|
||||
|
||||
const ProjectsTab = () => {
|
||||
const { projects, refetchProjects } = useProjects();
|
||||
const [selectedProject, setSelectedProject] = useState<ProjectSchema | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (projects.length > 0) {
|
||||
if (!selectedProject) {
|
||||
setSelectedProject(projects[0]);
|
||||
} else {
|
||||
setSelectedProject(projects.find((project) => project.id === selectedProject.id) ?? null);
|
||||
}
|
||||
}
|
||||
}, [projects]);
|
||||
|
||||
return (
|
||||
<Group align={"flex-start"}>
|
||||
<ProjectsEditor
|
||||
projects={projects}
|
||||
refetchProjects={refetchProjects}
|
||||
selectedProject={selectedProject}
|
||||
setSelectedProject={setSelectedProject}
|
||||
/>
|
||||
<ModulesPicker
|
||||
project={selectedProject}
|
||||
refetchProjects={refetchProjects}
|
||||
/>
|
||||
<AttributesPicker
|
||||
project={selectedProject}
|
||||
refetchProjects={refetchProjects}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsTab;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ProjectSchema, ProjectService } from "../../../../../../../client";
|
||||
import styles from "../../../ProjectsEditorDrawer.module.css";
|
||||
import { Center, rem, Stack, Title } from "@mantine/core";
|
||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useAttributesList from "../../../../../../../hooks/useAttributesList.tsx";
|
||||
import useAttributesTableColumns from "../hooks/attributesTableColumns.tsx";
|
||||
import { useSet } from "@mantine/hooks";
|
||||
import { useEffect, useState } from "react";
|
||||
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
|
||||
import InlineButton from "../../../../../../../components/InlineButton/InlineButton.tsx";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import eqSet from "../../../utils/eqSet.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
project: ProjectSchema | null;
|
||||
refetchProjects: () => void;
|
||||
}
|
||||
|
||||
const AttributesPicker = ({ project, refetchProjects }: Props) => {
|
||||
const { objects: attributes } = useAttributesList();
|
||||
const [defaultSelectedAttributes, setDefaultSelectedAttributes] = useState(new Set<number>(project?.attributes.map(m => m.id)));
|
||||
const selectedAttributes = useSet<number>(project?.attributes.map(a => a.id));
|
||||
const columns = useAttributesTableColumns({ selectedAttributes });
|
||||
|
||||
useEffect(() => {
|
||||
selectedAttributes.clear();
|
||||
project?.attributes.forEach(attribute => {
|
||||
selectedAttributes.add(attribute.id);
|
||||
});
|
||||
setDefaultSelectedAttributes(new Set([...selectedAttributes]));
|
||||
}, [project]);
|
||||
|
||||
const onUpdateAttributesClick = () => {
|
||||
if (!project) return;
|
||||
ProjectService.updateProjectAttributes({
|
||||
requestBody: {
|
||||
projectId: project.id,
|
||||
attributeIds: selectedAttributes.values().toArray(),
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
refetchProjects();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["container-wrapper"]} style={{ flex: 2 }}>
|
||||
<Stack gap={rem(10)}>
|
||||
<Center>
|
||||
<Title order={4}>Атрибуты проекта</Title>
|
||||
</Center>
|
||||
<BaseTable
|
||||
data={attributes}
|
||||
columns={columns}
|
||||
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowVirtualization: true,
|
||||
mantineTableContainerProps: { style: { maxHeight: "88vh" } },
|
||||
}}
|
||||
/>
|
||||
{!eqSet(selectedAttributes, defaultSelectedAttributes) && (
|
||||
<InlineButton onClick={onUpdateAttributesClick}>
|
||||
<IconCheck />
|
||||
Сохранить
|
||||
</InlineButton>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributesPicker;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ProjectSchema, ProjectService } from "../../../../../../../client";
|
||||
import useModulesList from "../hooks/useModulesList.tsx";
|
||||
import styles from "../../../ProjectsEditorDrawer.module.css";
|
||||
import { useSet } from "@mantine/hooks";
|
||||
import useModulesTableColumns from "../hooks/modulesTableColumns.tsx";
|
||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { Center, rem, Stack, Title } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import InlineButton from "../../../../../../../components/InlineButton/InlineButton.tsx";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
|
||||
import eqSet from "../../../utils/eqSet.ts";
|
||||
|
||||
type Props = {
|
||||
project: ProjectSchema | null;
|
||||
refetchProjects: () => void;
|
||||
}
|
||||
|
||||
const ModulesPicker = ({ project, refetchProjects }: Props) => {
|
||||
const { objects: modules } = useModulesList();
|
||||
const [defaultSelectedModules, setDefaultSelectedModules] = useState(
|
||||
new Set<number>(project?.modules.map(m => m.id)),
|
||||
);
|
||||
const selectedModules = useSet<number>();
|
||||
const columns = useModulesTableColumns({ selectedModules });
|
||||
|
||||
useEffect(() => {
|
||||
selectedModules.clear();
|
||||
project?.modules.forEach(module => {
|
||||
selectedModules.add(module.id);
|
||||
});
|
||||
setDefaultSelectedModules(new Set([...selectedModules]));
|
||||
}, [project]);
|
||||
|
||||
const updateProjectModules = () => {
|
||||
if (!project) return;
|
||||
ProjectService.updateProjectModules({
|
||||
requestBody: {
|
||||
projectId: project.id,
|
||||
moduleIds: selectedModules.values().toArray(),
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
refetchProjects();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["container-wrapper"]}>
|
||||
<Stack gap={rem(10)}>
|
||||
<Center>
|
||||
<Title order={4}>Модули проекта</Title>
|
||||
</Center>
|
||||
<BaseTable
|
||||
data={modules}
|
||||
columns={columns}
|
||||
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
}}
|
||||
/>
|
||||
{!eqSet(selectedModules, defaultSelectedModules) && (
|
||||
<InlineButton onClick={updateProjectModules}>
|
||||
<IconCheck />
|
||||
Сохранить
|
||||
</InlineButton>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModulesPicker;
|
||||
@@ -0,0 +1,95 @@
|
||||
import styles from "../../../ProjectsEditorDrawer.module.css";
|
||||
import { ActionIcon, Center, Flex, rem, Stack, Title, Tooltip } from "@mantine/core";
|
||||
import InlineButton from "../../../../../../../components/InlineButton/InlineButton.tsx";
|
||||
import { IconCheck, IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { BaseTable } from "../../../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { FullProjectSchema, ProjectSchema } from "../../../../../../../client";
|
||||
import useProjectsTableColumns from "../hooks/projectsTableColumns.tsx";
|
||||
import useProjectsTab from "../hooks/useProjectsTab.tsx";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
projects: ProjectSchema[];
|
||||
refetchProjects: () => void;
|
||||
selectedProject: ProjectSchema | null;
|
||||
setSelectedProject: React.Dispatch<React.SetStateAction<ProjectSchema | null>>;
|
||||
}
|
||||
|
||||
const ProjectsEditor = ({
|
||||
projects,
|
||||
refetchProjects,
|
||||
selectedProject,
|
||||
setSelectedProject,
|
||||
}: Props) => {
|
||||
const {
|
||||
editingProjects,
|
||||
handleEditClick,
|
||||
handleDeleteClick,
|
||||
handleCreateClick,
|
||||
} = useProjectsTab({
|
||||
refetchProjects,
|
||||
});
|
||||
|
||||
const columns = useProjectsTableColumns({ editingProjects, selectedProject, setSelectedProject });
|
||||
|
||||
return (
|
||||
<div className={styles["container-wrapper"]} style={{ flex: 1.5 }}>
|
||||
<Stack gap={rem(10)}>
|
||||
<Center>
|
||||
<Title order={4}>Проекты</Title>
|
||||
</Center>
|
||||
|
||||
<BaseTable
|
||||
data={projects}
|
||||
columns={columns}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
positionActionsColumn: "last",
|
||||
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => handleEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
{
|
||||
editingProjects.has(row.original.id) ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconEdit />
|
||||
)
|
||||
}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={"Удалить"}>
|
||||
<ActionIcon
|
||||
onClick={() => handleDeleteClick(row.original)}
|
||||
disabled={row.original.boardsCount > 0}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<FullProjectSchema>
|
||||
}
|
||||
/>
|
||||
<InlineButton
|
||||
variant={"default"}
|
||||
onClick={handleCreateClick}
|
||||
style={{ border: "dashed var(--item-border-size) var(--mantine-color-default-border)" }}
|
||||
>
|
||||
<IconPlus />
|
||||
Добавить
|
||||
</InlineButton>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsEditor;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { AttributeSchema } from "../../../../../../../client";
|
||||
import { Box, Center, Checkbox, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import defaultValueToStr from "../../../utils/defaultValueToStr.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
selectedAttributes: Set<number>;
|
||||
}
|
||||
|
||||
const useAttributesTableColumns = ({ selectedAttributes }: Props) => {
|
||||
return useMemo<MRT_ColumnDef<AttributeSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "label",
|
||||
size: 25,
|
||||
},
|
||||
{
|
||||
header: "Тип",
|
||||
accessorKey: "type.name",
|
||||
size: 25,
|
||||
},
|
||||
{
|
||||
header: " ",
|
||||
Cell: ({ row }) => {
|
||||
const description = row.original.description ? `Описание: ${row.original.description}` : "";
|
||||
const info = (
|
||||
<Box>
|
||||
<Text>Может быть пустым: {row.original.isNullable ? "да" : "нет"}</Text>
|
||||
<Text>{defaultValueToStr(row.original.defaultValue, row.original.type.type)}</Text>
|
||||
<Text>Синхронизировано в группе: {row.original.isApplicableToGroup ? "да" : "нет"}</Text>
|
||||
<Text>{description}</Text>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Tooltip label={info} multiline w={rem(300)}>
|
||||
<IconInfoCircle />
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
size: 5,
|
||||
},
|
||||
{
|
||||
header: " ",
|
||||
Cell: ({ row }) => (
|
||||
<Center>
|
||||
<Checkbox
|
||||
checked={selectedAttributes.has(row.original.id)}
|
||||
onChange={() => {
|
||||
if (selectedAttributes.has(row.original.id)) {
|
||||
selectedAttributes.delete(row.original.id);
|
||||
} else {
|
||||
selectedAttributes.add(row.original.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
),
|
||||
size: 5,
|
||||
},
|
||||
],
|
||||
[selectedAttributes],
|
||||
);
|
||||
};
|
||||
|
||||
export default useAttributesTableColumns;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { ModuleSchema } from "../../../../../../../client";
|
||||
import { Center, Checkbox } from "@mantine/core";
|
||||
|
||||
|
||||
type Props = {
|
||||
selectedModules: Set<number>;
|
||||
}
|
||||
|
||||
const useModulesTableColumns = ({ selectedModules }: Props) => {
|
||||
return useMemo<MRT_ColumnDef<ModuleSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "label",
|
||||
size: 25,
|
||||
},
|
||||
{
|
||||
header: " ",
|
||||
Cell: ({ row }) => (
|
||||
<Center>
|
||||
<Checkbox
|
||||
checked={selectedModules.has(row.original.id)}
|
||||
onChange={() => {
|
||||
if (selectedModules.has(row.original.id)) {
|
||||
selectedModules.delete(row.original.id);
|
||||
} else {
|
||||
selectedModules.add(row.original.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
),
|
||||
size: 5,
|
||||
},
|
||||
],
|
||||
[selectedModules],
|
||||
);
|
||||
};
|
||||
|
||||
export default useModulesTableColumns;
|
||||
@@ -1,16 +1,28 @@
|
||||
import { useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { BaseProjectSchema, ProjectSchema } from "../../../../../client";
|
||||
import { TextInput } from "@mantine/core";
|
||||
import { BaseProjectSchema, ProjectSchema } from "../../../../../../../client";
|
||||
import { Radio, TextInput } from "@mantine/core";
|
||||
|
||||
|
||||
type Props = {
|
||||
editingProjects: Map<number, ProjectSchema | BaseProjectSchema>;
|
||||
selectedProject: ProjectSchema | null;
|
||||
setSelectedProject: React.Dispatch<React.SetStateAction<ProjectSchema | null>>;
|
||||
}
|
||||
|
||||
const useProjectsTableColumns = ({ editingProjects }: Props) => {
|
||||
const useProjectsTableColumns = ({ editingProjects, selectedProject, setSelectedProject }: Props) => {
|
||||
return useMemo<MRT_ColumnDef<ProjectSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: " ",
|
||||
Cell: ({ row }) => (
|
||||
<Radio
|
||||
checked={row.original.id === selectedProject?.id}
|
||||
onChange={() => setSelectedProject(row.original)}
|
||||
/>
|
||||
),
|
||||
size: 10,
|
||||
},
|
||||
{
|
||||
header: "Название",
|
||||
accessorKey: "name",
|
||||
@@ -39,7 +51,7 @@ const useProjectsTableColumns = ({ editingProjects }: Props) => {
|
||||
size: 10,
|
||||
},
|
||||
],
|
||||
[],
|
||||
[selectedProject],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ProjectService } from "../../../../../../../client";
|
||||
import ObjectList from "../../../../../../../hooks/objectList.tsx";
|
||||
|
||||
const useModulesList = () =>
|
||||
ObjectList({
|
||||
queryFn: ProjectService.getAllModules,
|
||||
getObjectsFn: response => response.modules,
|
||||
queryKey: "getAllModules",
|
||||
});
|
||||
|
||||
export default useModulesList;
|
||||
@@ -1,17 +1,16 @@
|
||||
import { BaseProjectSchema, ProjectSchema, ProjectService } from "../../../../../client";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { ProjectSchema, ProjectService } from "../../../../../../../client";
|
||||
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
|
||||
import { useProjectsEditorContext } from "../../../../../contexts/ProjectsEditorContext.tsx";
|
||||
import { useMap } from "@mantine/hooks";
|
||||
import { useState } from "react";
|
||||
import useProjects from "../../../hooks/useProjects.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
type Props = {
|
||||
onUpdate: () => void;
|
||||
refetchProjects: () => void;
|
||||
}
|
||||
|
||||
const useProjectModal = ({ onUpdate }: Props) => {
|
||||
const useProjectsTab = ({ refetchProjects }: Props) => {
|
||||
const { onUpdate } = useProjectsEditorContext();
|
||||
const editingProjects = useMap<number, ProjectSchema>();
|
||||
const { projects, refetchProjects } = useProjects();
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const updateProject = (project: ProjectSchema) => {
|
||||
ProjectService.updateProject({
|
||||
@@ -62,34 +61,18 @@ const useProjectModal = ({ onUpdate }: Props) => {
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const createProject = (project: BaseProjectSchema) => {
|
||||
ProjectService.createProject({
|
||||
requestBody: { project },
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
setName("");
|
||||
refetchProjects();
|
||||
onUpdate();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
if (name.length === 0) {
|
||||
notifications.error({ message: "Имя проекта не может быть пустым" });
|
||||
return;
|
||||
}
|
||||
createProject({ name });
|
||||
modals.openContextModal({
|
||||
modal: "createProjectModal",
|
||||
title: "Создание проекта",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
refetchProjects,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
projects,
|
||||
name,
|
||||
setName,
|
||||
editingProjects,
|
||||
handleEditClick,
|
||||
handleDeleteClick,
|
||||
@@ -97,4 +80,4 @@ const useProjectModal = ({ onUpdate }: Props) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjectModal;
|
||||
export default useProjectsTab;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Stack, TextInput } from "@mantine/core";
|
||||
import { BaseProjectSchema, ProjectService } from "../../../../../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { notifications } from "../../../../../../../shared/lib/notifications.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
refetchProjects: () => void;
|
||||
};
|
||||
|
||||
const CreateProjectModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const closeModal = () => context.closeContextModal(id);
|
||||
|
||||
const form = useForm<BaseProjectSchema>({
|
||||
initialValues: { name: "" },
|
||||
validate: {
|
||||
name: name => !name && "Название не заполнено",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (project: BaseProjectSchema) => {
|
||||
ProjectService.createProject({
|
||||
requestBody: { project },
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
innerProps.refetchProjects();
|
||||
closeModal();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<Stack gap={"md"}>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateProjectModal;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { formatDate, formatDateTime } from "../../../../../types/utils.ts";
|
||||
|
||||
const defaultValueToStr = (defaultValue: string | null | number | boolean, type: string): string => {
|
||||
if (defaultValue === null) {
|
||||
return "Значения по умолчанию нет";
|
||||
}
|
||||
let strValue;
|
||||
const prefix = "Значение по умолчанию: ";
|
||||
|
||||
if (type === "bool") {
|
||||
strValue = defaultValue ? "true" : "false";
|
||||
} else if (type === "datetime") {
|
||||
strValue = formatDateTime(defaultValue as string);
|
||||
} else if (type === "date") {
|
||||
strValue = formatDate(defaultValue as string);
|
||||
} else {
|
||||
strValue = String(defaultValue);
|
||||
}
|
||||
|
||||
return prefix + strValue;
|
||||
};
|
||||
|
||||
export default defaultValueToStr;
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
const eqSet = (firstSet: Set<number>, secondSet: Set<number>) =>
|
||||
firstSet.size === secondSet.size &&
|
||||
[...firstSet].every((x) => secondSet.has(x));
|
||||
|
||||
export default eqSet;
|
||||
@@ -0,0 +1,25 @@
|
||||
const translitMap: Record<string, string> = {
|
||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
|
||||
'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
|
||||
'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
|
||||
'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'shch', 'ъ': '',
|
||||
'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya',
|
||||
};
|
||||
|
||||
const transliterate = (text: string): string => {
|
||||
return text.toLowerCase().split('').map(char => translitMap[char] || char).join('');
|
||||
};
|
||||
|
||||
const toSnakeCase = (text: string): string => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '_') // Replace spaces with underscores
|
||||
.replace(/[^a-z_0-9]/g, '') // Remove non-English letters and special chars
|
||||
.replace(/__+/g, '_') // Replace multiple underscores with single one
|
||||
.replace(/^_+|_+$/g, ''); // Trim leading/trailing underscores
|
||||
};
|
||||
|
||||
export const convertRussianToSnakeCase = (input: string): string => {
|
||||
const transliterated = transliterate(input);
|
||||
return toSnakeCase(transliterated);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
enum DragState {
|
||||
DRAG_ENDED,
|
||||
DRAG_DEAL,
|
||||
DRAG_CARD,
|
||||
DRAG_STATUS,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const useDnd = ({
|
||||
|
||||
const {
|
||||
summaries,
|
||||
onDealDragEnd,
|
||||
onCardDragEnd,
|
||||
} = useCardsDnd({
|
||||
summariesRaw,
|
||||
refetchSummaries,
|
||||
@@ -41,14 +41,14 @@ const useDnd = ({
|
||||
if (result.draggableId.includes("status")) {
|
||||
return onStatusDragEnd(result);
|
||||
}
|
||||
return onDealDragEnd(result);
|
||||
return onCardDragEnd(result);
|
||||
}
|
||||
|
||||
const onDragStart = (start: DragStart) => {
|
||||
if (start.source.droppableId.includes("status")) {
|
||||
setDragState(DragState.DRAG_STATUS);
|
||||
} else {
|
||||
setDragState(DragState.DRAG_DEAL);
|
||||
setDragState(DragState.DRAG_CARD);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { ActionIcon, Flex, rem, Stack, TextInput, Tooltip } from "@mantine/core";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useProjectsTableColumns from "./hooks/projectsTableColumns.tsx";
|
||||
import { IconCheck, IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import InlineButton from "../../../../components/InlineButton/InlineButton.tsx";
|
||||
import useProjectModal from "./hooks/useProjectModal.tsx";
|
||||
import { FullProjectSchema } from "../../../../client";
|
||||
|
||||
type Props = {
|
||||
onUpdate: () => void;
|
||||
};
|
||||
|
||||
const ProjectsModal = ({ innerProps }: ContextModalProps<Props>) => {
|
||||
const {
|
||||
projects,
|
||||
name,
|
||||
setName,
|
||||
editingProjects,
|
||||
handleEditClick,
|
||||
handleDeleteClick,
|
||||
handleCreateClick,
|
||||
} = useProjectModal(innerProps);
|
||||
|
||||
const columns = useProjectsTableColumns({ editingProjects });
|
||||
|
||||
return (
|
||||
<Stack gap={rem(10)}>
|
||||
<TextInput
|
||||
label={"Добавить проект"}
|
||||
variant={"default"}
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
<InlineButton
|
||||
variant={"default"}
|
||||
onClick={handleCreateClick}>
|
||||
<IconPlus />
|
||||
Добавить
|
||||
</InlineButton>
|
||||
|
||||
<BaseTable
|
||||
data={projects}
|
||||
columns={columns}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => handleEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
{
|
||||
editingProjects.has(row.original.id) ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconEdit />
|
||||
)
|
||||
}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={"Удалить"}>
|
||||
<ActionIcon
|
||||
onClick={() => handleDeleteClick(row.original)}
|
||||
disabled={row.original.boardsCount > 0}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<FullProjectSchema>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsModal;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Button, Fieldset, Flex, rem, Textarea, TextInput } from "@mantine/core";
|
||||
import { Button, Fieldset, Flex, rem, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import { useCardPageContext } from "../../contexts/CardPageContext.tsx";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { CardGeneralFormType } from "../GeneralTab/GeneralTab.tsx";
|
||||
import { ClientService, CardSchema, CardService } from "../../../../client";
|
||||
import { CardSchema, CardService, ClientService } from "../../../../client";
|
||||
import { isEqual } from "lodash";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
@@ -53,38 +53,39 @@ const ClientTab = () => {
|
||||
<Flex direction={"column"} flex={1} gap={rem(10)}>
|
||||
<Flex flex={1}>
|
||||
<Fieldset legend={"Клиент"} flex={1}>
|
||||
<TextInput
|
||||
disabled
|
||||
placeholder={"Название"}
|
||||
label={"Название"}
|
||||
value={card?.client.name}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите телефон"}
|
||||
label={"Телефон клиента"}
|
||||
{...form.getInputProps("client.details.phoneNumber")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите email"}
|
||||
label={"Email"}
|
||||
{...form.getInputProps("client.details.email")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите телеграм"}
|
||||
label={"Телеграм"}
|
||||
{...form.getInputProps("client.details.telegram")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите ИНН"}
|
||||
label={"ИНН"}
|
||||
{...form.getInputProps("client.details.inn")}
|
||||
/>
|
||||
<Textarea
|
||||
placeholder={"Введите комментарий"}
|
||||
label={"Комментарий"}
|
||||
{...form.getInputProps("client.comment")}
|
||||
/>
|
||||
|
||||
<Stack gap={rem(10)}>
|
||||
<TextInput
|
||||
disabled
|
||||
placeholder={"Название"}
|
||||
label={"Название"}
|
||||
value={card?.client.name}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите телефон"}
|
||||
label={"Телефон клиента"}
|
||||
{...form.getInputProps("client.details.phoneNumber")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите email"}
|
||||
label={"Email"}
|
||||
{...form.getInputProps("client.details.email")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите телеграм"}
|
||||
label={"Телеграм"}
|
||||
{...form.getInputProps("client.details.telegram")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите ИНН"}
|
||||
label={"ИНН"}
|
||||
{...form.getInputProps("client.details.inn")}
|
||||
/>
|
||||
<Textarea
|
||||
placeholder={"Введите комментарий"}
|
||||
label={"Комментарий"}
|
||||
{...form.getInputProps("client.comment")}
|
||||
/>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
</Flex>
|
||||
<Flex
|
||||
|
||||
@@ -20,6 +20,7 @@ const UserForDepartmentSelect: FC<Props> = ({ cardId, ...selectProps }) => {
|
||||
data={employees}
|
||||
getLabelFn={(user: UserSchema) => `${user.firstName} ${user.secondName}`}
|
||||
getValueFn={(user: UserSchema) => user.id.toString()}
|
||||
searchable
|
||||
{...selectProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ const useEmployeesTab = () => {
|
||||
if (!card) return;
|
||||
|
||||
modals.openContextModal({
|
||||
modal: "assignUserModal",
|
||||
modal: "assignEmployeeModal",
|
||||
title: `Назначение исполнителя`,
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
|
||||
@@ -38,6 +38,7 @@ import getAttributesFromCard from "../../../../components/CardAttributeFields/ut
|
||||
import isModuleInProject, { Modules } from "../../utils/isModuleInProject.ts";
|
||||
import PaymentLinkButton from "./components/PaymentLinkButton.tsx";
|
||||
import PrintDealBarcodesButton from "./components/PrintDealBarcodesButton.tsx";
|
||||
import ClientSelect from "../../../../components/Selects/ClientSelect/ClientSelect.tsx";
|
||||
|
||||
type Props = {
|
||||
card: CardSchema;
|
||||
@@ -57,6 +58,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
|
||||
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, card.board.project);
|
||||
const isManagerIncluded = isModuleInProject(Modules.MANAGERS, card.board.project);
|
||||
const isClientIncluded = isModuleInProject(Modules.CLIENTS, card.board.project);
|
||||
|
||||
const getInitialValues = (card: CardSchema): CardGeneralFormType => {
|
||||
return {
|
||||
@@ -96,6 +98,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
...values,
|
||||
statusId: values.status.id,
|
||||
boardId: values.board.id,
|
||||
clientId: values.client?.id ?? null,
|
||||
shippingWarehouse: values.shippingWarehouse?.toString(),
|
||||
attributes,
|
||||
},
|
||||
@@ -124,7 +127,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
};
|
||||
const handleSubmit = async (values: CardGeneralFormType) => {
|
||||
// Updating client info if there changes
|
||||
if (!isEqual(values.client, card.client)) {
|
||||
if (!isEqual(values.client, card.client) && values.client?.id === card.clientId) {
|
||||
await updateClientInfo(values);
|
||||
}
|
||||
|
||||
@@ -199,7 +202,7 @@ const Content: FC<Props> = ({ card }) => {
|
||||
label={"Статус"}
|
||||
/>
|
||||
<Textarea
|
||||
h={rem(150)}
|
||||
h={rem(120)}
|
||||
styles={{
|
||||
wrapper: { height: "90%" },
|
||||
input: { height: "90%" },
|
||||
@@ -208,6 +211,12 @@ const Content: FC<Props> = ({ card }) => {
|
||||
placeholder={"Введите коментарий"}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
{isClientIncluded && (
|
||||
<ClientSelect
|
||||
{...form.getInputProps("client")}
|
||||
withLabel
|
||||
/>
|
||||
)}
|
||||
{isServicesAndProductsIncluded && (
|
||||
<ShippingWarehouseAutocomplete
|
||||
placeholder={"Введите склад отгрузки"}
|
||||
|
||||
@@ -16,20 +16,20 @@
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.deal-container {
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: rem(10);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deal-container-wrapper {
|
||||
.card-container-wrapper {
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
}
|
||||
|
||||
.deal-container-buttons {
|
||||
.card-container-buttons {
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -234,18 +234,18 @@ const ProductAndServiceTab: FC = () => {
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className={styles["deal-container"]}>
|
||||
<div className={styles["card-container"]}>
|
||||
<ScrollArea offsetScrollbars>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
className={styles["deal-container-wrapper"]}>
|
||||
className={styles["card-container-wrapper"]}>
|
||||
<CardServicesTable
|
||||
onKitAdd={onCardKitAdd}
|
||||
{...cardServicesState}
|
||||
/>
|
||||
|
||||
<Divider my={rem(15)} />
|
||||
<div className={styles["deal-container-buttons"]}>
|
||||
<div className={styles["card-container-buttons"]}>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
variant={"default"}
|
||||
@@ -262,7 +262,7 @@ const ProductAndServiceTab: FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
<Divider my={rem(15)} />
|
||||
<div className={styles["deal-container-buttons"]}>
|
||||
<div className={styles["card-container-buttons"]}>
|
||||
{isLocked ? (
|
||||
<Button
|
||||
onClick={onCancelBillClick}
|
||||
@@ -282,7 +282,7 @@ const ProductAndServiceTab: FC = () => {
|
||||
</Flex>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
className={styles["deal-container-wrapper"]}>
|
||||
className={styles["card-container-wrapper"]}>
|
||||
<Title order={3}>
|
||||
Общая стоимость всех услуг:{" "}
|
||||
{getTotalPrice().toLocaleString("ru")}₽
|
||||
|
||||
@@ -16,6 +16,8 @@ import CardsPageHeader from "../components/CardsPageHeader/CardsPageHeader.tsx";
|
||||
import useProjects from "../hooks/useProjects.tsx";
|
||||
import Boards from "../../../components/Dnd/Boards/Boards/Boards.tsx";
|
||||
import useBoards from "../hooks/useBoards.tsx";
|
||||
import { ProjectsEditorContextProvider } from "../contexts/ProjectsEditorContext.tsx";
|
||||
import ProjectsEditorDrawer from "../drawers/ProjectsEditorDrawer/ProjectsEditorDrawer.tsx";
|
||||
|
||||
export const CardsPage: FC = () => {
|
||||
const { projects, refetchProjects } = useProjects();
|
||||
@@ -81,13 +83,15 @@ export const CardsPage: FC = () => {
|
||||
>
|
||||
<PrefillCardContextProvider>
|
||||
<PrefillCardsWithExcelContextProvider>
|
||||
<CardsPageHeader
|
||||
form={form}
|
||||
displayMode={displayMode}
|
||||
setDisplayMode={setDisplayMode}
|
||||
projects={projects}
|
||||
refetchProjects={refetchProjects}
|
||||
/>
|
||||
<ProjectsEditorContextProvider onUpdate={refetchProjects}>
|
||||
<CardsPageHeader
|
||||
form={form}
|
||||
displayMode={displayMode}
|
||||
setDisplayMode={setDisplayMode}
|
||||
projects={projects}
|
||||
/>
|
||||
<ProjectsEditorDrawer />
|
||||
</ProjectsEditorContextProvider>
|
||||
<PageBlock
|
||||
style={{
|
||||
display: "flex",
|
||||
|
||||
@@ -15,10 +15,22 @@ export type BaseFormInputProps<T> = {
|
||||
value: T;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
export const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString("ru-RU");
|
||||
};
|
||||
|
||||
export const formatDateTime = (dateTimeStr: string) => {
|
||||
const date = new Date(dateTimeStr);
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${day}.${month}.${year} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
export const getProductFields = (product: ProductSchema) => {
|
||||
return (
|
||||
Object.entries(product)
|
||||
|
||||
Reference in New Issue
Block a user