Compare commits
29 Commits
69c5f1419f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 27f27f4c38 | |||
| 6e19d98f81 | |||
| 7a052f7422 | |||
| b08f921c1c | |||
| d0da5155a1 | |||
| bdbdd10231 | |||
| 9dde9041b1 | |||
| 01260d0768 | |||
| 15a085721e | |||
| 655ae077ca | |||
| 50590fa41c | |||
| c866231730 | |||
| c3d135eba9 | |||
| 82e2ef6db2 | |||
| 4787151b69 | |||
| eea03bd70a | |||
| f7d514df4b | |||
| b09479302b | |||
| 74665e123b | |||
| 4fee2da42c | |||
| cc39d13b1c | |||
| 2a7a50886c | |||
| 3b082d7836 | |||
| 37136b83bc | |||
| d03ba66ebb | |||
| e4f8e90ae6 | |||
| d347c09199 | |||
| f28df5a074 | |||
| 9ce112fd63 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM oven/bun:latest AS base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
FROM base AS install
|
||||
RUN mkdir -p /temp/dev
|
||||
COPY package.json bun.lock /temp/dev/
|
||||
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||
|
||||
# install with --production (exclude devDependencies)
|
||||
RUN mkdir -p /temp/prod
|
||||
COPY package.json bun.lock /temp/prod/
|
||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||
|
||||
# copy node_modules from temp directory
|
||||
# then copy all (non-ignored) project files into the image
|
||||
FROM base AS prerelease
|
||||
COPY --from=install /temp/dev/node_modules node_modules
|
||||
COPY . .
|
||||
|
||||
RUN bun run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=prerelease /usr/src/app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
2
build-docker.sh
Executable file
2
build-docker.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
docker build -t git.denco.store/fakz9/fulfillment-frontend:latest .
|
||||
docker push git.denco.store/fakz9/fulfillment-frontend:latest
|
||||
12
nginx.conf
Normal file
12
nginx.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
sendfile on;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,9 @@ export type { BaseEnumListSchema } from './models/BaseEnumListSchema';
|
||||
export type { BaseEnumSchema } from './models/BaseEnumSchema';
|
||||
export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema';
|
||||
export type { BaseMessageSchema } from './models/BaseMessageSchema';
|
||||
export type { BasePlaceSchema } from './models/BasePlaceSchema';
|
||||
export type { BasePlaceTypeSchema } from './models/BasePlaceTypeSchema';
|
||||
export type { BasePlaceTypeWithCountSchema } from './models/BasePlaceTypeWithCountSchema';
|
||||
export type { BaseProjectSchema } from './models/BaseProjectSchema';
|
||||
export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouseSchema';
|
||||
export type { BaseStatusSchema } from './models/BaseStatusSchema';
|
||||
@@ -117,6 +120,7 @@ export type { CardUpdateServiceQuantityResponse } from './models/CardUpdateServi
|
||||
export type { CardUpdateServiceRequest } from './models/CardUpdateServiceRequest';
|
||||
export type { CardUpdateServiceResponse } from './models/CardUpdateServiceResponse';
|
||||
export type { ChatSchema } from './models/ChatSchema';
|
||||
export type { ChatsListItemSchema } from './models/ChatsListItemSchema';
|
||||
export type { CityBreakdownFromExcelSchema } from './models/CityBreakdownFromExcelSchema';
|
||||
export type { ClientCreateRequest } from './models/ClientCreateRequest';
|
||||
export type { ClientCreateResponse } from './models/ClientCreateResponse';
|
||||
@@ -151,6 +155,7 @@ export type { CreateDepartmentRequest } from './models/CreateDepartmentRequest';
|
||||
export type { CreateDepartmentResponse } from './models/CreateDepartmentResponse';
|
||||
export type { CreateDepartmentSectionRequest } from './models/CreateDepartmentSectionRequest';
|
||||
export type { CreateDepartmentSectionResponse } from './models/CreateDepartmentSectionResponse';
|
||||
export type { CreateGuestUrlResponse } from './models/CreateGuestUrlResponse';
|
||||
export type { CreateMarketplaceRequest } from './models/CreateMarketplaceRequest';
|
||||
export type { CreateMarketplaceResponse } from './models/CreateMarketplaceResponse';
|
||||
export type { CreatePalletResponse } from './models/CreatePalletResponse';
|
||||
@@ -158,6 +163,10 @@ export type { CreatePaymentRecordRequest } from './models/CreatePaymentRecordReq
|
||||
export type { CreatePaymentRecordResponse } from './models/CreatePaymentRecordResponse';
|
||||
export type { CreatePayRateRequest } from './models/CreatePayRateRequest';
|
||||
export type { CreatePayRateResponse } from './models/CreatePayRateResponse';
|
||||
export type { CreatePlaceRequest } from './models/CreatePlaceRequest';
|
||||
export type { CreatePlaceResponse } from './models/CreatePlaceResponse';
|
||||
export type { CreatePlaceTypeRequest } from './models/CreatePlaceTypeRequest';
|
||||
export type { CreatePlaceTypeResponse } from './models/CreatePlaceTypeResponse';
|
||||
export type { CreatePositionRequest } from './models/CreatePositionRequest';
|
||||
export type { CreatePositionResponse } from './models/CreatePositionResponse';
|
||||
export type { CreateProjectRequest } from './models/CreateProjectRequest';
|
||||
@@ -196,6 +205,8 @@ export type { DeletePaymentRecordRequest } from './models/DeletePaymentRecordReq
|
||||
export type { DeletePaymentRecordResponse } from './models/DeletePaymentRecordResponse';
|
||||
export type { DeletePayRateRequest } from './models/DeletePayRateRequest';
|
||||
export type { DeletePayRateResponse } from './models/DeletePayRateResponse';
|
||||
export type { DeletePlaceResponse } from './models/DeletePlaceResponse';
|
||||
export type { DeletePlaceTypeResponse } from './models/DeletePlaceTypeResponse';
|
||||
export type { DeletePositionRequest } from './models/DeletePositionRequest';
|
||||
export type { DeletePositionResponse } from './models/DeletePositionResponse';
|
||||
export type { DeleteProjectResponse } from './models/DeleteProjectResponse';
|
||||
@@ -220,10 +231,16 @@ export type { DepartmentSectionSchema } from './models/DepartmentSectionSchema';
|
||||
export type { EditMessageRequest } from './models/EditMessageRequest';
|
||||
export type { EditMessageResponse } from './models/EditMessageResponse';
|
||||
export type { EditMessageSchema } from './models/EditMessageSchema';
|
||||
export type { EditPlaceRequest } from './models/EditPlaceRequest';
|
||||
export type { EditPlaceResponse } from './models/EditPlaceResponse';
|
||||
export type { EditPlaceSchema } from './models/EditPlaceSchema';
|
||||
export type { EditPlaceTypeRequest } from './models/EditPlaceTypeRequest';
|
||||
export type { EditPlaceTypeResponse } from './models/EditPlaceTypeResponse';
|
||||
export type { FinishPauseByShiftIdResponse } from './models/FinishPauseByShiftIdResponse';
|
||||
export type { FinishPauseByUserIdResponse } from './models/FinishPauseByUserIdResponse';
|
||||
export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse';
|
||||
export type { FinishShiftResponse } from './models/FinishShiftResponse';
|
||||
export type { FlatPlaceTypeSchema } from './models/FlatPlaceTypeSchema';
|
||||
export type { FullProjectSchema } from './models/FullProjectSchema';
|
||||
export type { GenerateInviteCodeRequest } from './models/GenerateInviteCodeRequest';
|
||||
export type { GenerateInviteCodeResponse } from './models/GenerateInviteCodeResponse';
|
||||
@@ -252,16 +269,21 @@ export type { GetBoardsResponse } from './models/GetBoardsResponse';
|
||||
export type { GetCardBillById } from './models/GetCardBillById';
|
||||
export type { GetCardProductsBarcodesPdfRequest } from './models/GetCardProductsBarcodesPdfRequest';
|
||||
export type { GetCardProductsBarcodesPdfResponse } from './models/GetCardProductsBarcodesPdfResponse';
|
||||
export type { GetCardSummariesRequest } from './models/GetCardSummariesRequest';
|
||||
export type { GetChatRequest } from './models/GetChatRequest';
|
||||
export type { GetChatResponse } from './models/GetChatResponse';
|
||||
export type { GetChatsListResponse } from './models/GetChatsListResponse';
|
||||
export type { GetClientMarketplacesRequest } from './models/GetClientMarketplacesRequest';
|
||||
export type { GetClientMarketplacesResponse } from './models/GetClientMarketplacesResponse';
|
||||
export type { GetDepartmentSectionsResponse } from './models/GetDepartmentSectionsResponse';
|
||||
export type { GetDepartmentsResponse } from './models/GetDepartmentsResponse';
|
||||
export type { GetFlatPlaceTypesResponse } from './models/GetFlatPlaceTypesResponse';
|
||||
export type { GetManagersResponse } from './models/GetManagersResponse';
|
||||
export type { GetMessagesRequest } from './models/GetMessagesRequest';
|
||||
export type { GetMessagesResponse } from './models/GetMessagesResponse';
|
||||
export type { GetPaymentRecordsResponse } from './models/GetPaymentRecordsResponse';
|
||||
export type { GetPlacesResponse } from './models/GetPlacesResponse';
|
||||
export type { GetPlaceTypesResponse } from './models/GetPlaceTypesResponse';
|
||||
export type { GetPlannedWorkShiftsResponse } from './models/GetPlannedWorkShiftsResponse';
|
||||
export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfRequest';
|
||||
export type { GetProductBarcodePdfResponse } from './models/GetProductBarcodePdfResponse';
|
||||
@@ -309,6 +331,8 @@ export type { PayRateSchema } from './models/PayRateSchema';
|
||||
export type { PayRateSchemaBase } from './models/PayRateSchemaBase';
|
||||
export type { PayrollSchemeSchema } from './models/PayrollSchemeSchema';
|
||||
export type { PermissionSchema } from './models/PermissionSchema';
|
||||
export type { PlaceSchema } from './models/PlaceSchema';
|
||||
export type { PlaceTypeSchema } from './models/PlaceTypeSchema';
|
||||
export type { PlannedWorkShiftSchema } from './models/PlannedWorkShiftSchema';
|
||||
export type { PlanningTableRow } from './models/PlanningTableRow';
|
||||
export type { PositionSchema } from './models/PositionSchema';
|
||||
@@ -342,6 +366,7 @@ export type { ProjectGeneralInfoSchema } from './models/ProjectGeneralInfoSchema
|
||||
export type { ProjectSchema } from './models/ProjectSchema';
|
||||
export type { ReceiptBoxSchema } from './models/ReceiptBoxSchema';
|
||||
export type { ReceiptPalletSchema } from './models/ReceiptPalletSchema';
|
||||
export type { ReorderResponse } from './models/ReorderResponse';
|
||||
export type { RepeatSendingMessageSchema } from './models/RepeatSendingMessageSchema';
|
||||
export type { RepeatSendingTextMessageRequest } from './models/RepeatSendingTextMessageRequest';
|
||||
export type { RepeatSendingTextMessageResponse } from './models/RepeatSendingTextMessageResponse';
|
||||
@@ -480,5 +505,6 @@ export { TaskService } from './services/TaskService';
|
||||
export { TimeTrackingService } from './services/TimeTrackingService';
|
||||
export { TransactionService } from './services/TransactionService';
|
||||
export { UserService } from './services/UserService';
|
||||
export { WmsService } from './services/WmsService';
|
||||
export { WorkShiftsService } from './services/WorkShiftsService';
|
||||
export { WorkShiftsPlanningService } from './services/WorkShiftsPlanningService';
|
||||
|
||||
9
src/client/models/BasePlaceSchema.ts
Normal file
9
src/client/models/BasePlaceSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type BasePlaceSchema = {
|
||||
parentId: (number | null);
|
||||
placeTypeId: number;
|
||||
};
|
||||
|
||||
9
src/client/models/BasePlaceTypeSchema.ts
Normal file
9
src/client/models/BasePlaceTypeSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type BasePlaceTypeSchema = {
|
||||
name: string;
|
||||
parentId: (number | null);
|
||||
};
|
||||
|
||||
10
src/client/models/BasePlaceTypeWithCountSchema.ts
Normal file
10
src/client/models/BasePlaceTypeWithCountSchema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type BasePlaceTypeWithCountSchema = {
|
||||
name: string;
|
||||
parentId: (number | null);
|
||||
childCount: number;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,6 @@ export type CardGroupSchema = {
|
||||
id: number;
|
||||
name?: (string | null);
|
||||
lexorank: string;
|
||||
billRequest?: (GroupBillRequestSchema | null);
|
||||
billRequests?: Array<GroupBillRequestSchema>;
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export type CardSchema = {
|
||||
clientId: (number | null);
|
||||
client: (ClientSchema | null);
|
||||
shippingWarehouse?: (ShippingWarehouseSchema | string | null);
|
||||
billRequest?: (CardBillRequestSchema | null);
|
||||
billRequests?: Array<CardBillRequestSchema>;
|
||||
group?: (CardGroupSchema | null);
|
||||
manager?: (UserSchema | null);
|
||||
pallets?: Array<PalletSchema>;
|
||||
|
||||
@@ -24,7 +24,7 @@ export type CardSummary = {
|
||||
attributes: Array<CardAttributeSchema>;
|
||||
shipmentWarehouseId: (number | null);
|
||||
shipmentWarehouseName: (string | null);
|
||||
billRequest?: (CardBillRequestSchema | null);
|
||||
billRequests: Array<CardBillRequestSchema>;
|
||||
group?: (CardGroupSchema | null);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { CardSummary } from './CardSummary';
|
||||
import type { PaginationInfoSchema } from './PaginationInfoSchema';
|
||||
export type CardSummaryResponse = {
|
||||
summaries: Array<CardSummary>;
|
||||
paginationInfo: PaginationInfoSchema;
|
||||
};
|
||||
|
||||
|
||||
13
src/client/models/ChatsListItemSchema.ts
Normal file
13
src/client/models/ChatsListItemSchema.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { TgGroupSchema } from './TgGroupSchema';
|
||||
export type ChatsListItemSchema = {
|
||||
id: number;
|
||||
clientId: (number | null);
|
||||
cardId: (number | null);
|
||||
tgGroup: (TgGroupSchema | null);
|
||||
name: string;
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ export type ClientDetailedSchema = {
|
||||
comment?: (string | null);
|
||||
details?: (ClientDetailsSchema | null);
|
||||
chat?: (ChatSchema | null);
|
||||
isDeleted?: (boolean | null);
|
||||
pallets?: Array<ResidualPalletSchema>;
|
||||
boxes?: Array<ResidualBoxSchema>;
|
||||
};
|
||||
|
||||
@@ -13,5 +13,6 @@ export type ClientSchema = {
|
||||
comment?: (string | null);
|
||||
details?: (ClientDetailsSchema | null);
|
||||
chat?: (ChatSchema | null);
|
||||
isDeleted?: (boolean | null);
|
||||
};
|
||||
|
||||
|
||||
10
src/client/models/CreateGuestUrlResponse.ts
Normal file
10
src/client/models/CreateGuestUrlResponse.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CreateGuestUrlResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
9
src/client/models/CreatePlaceRequest.ts
Normal file
9
src/client/models/CreatePlaceRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { BasePlaceSchema } from './BasePlaceSchema';
|
||||
export type CreatePlaceRequest = {
|
||||
place: BasePlaceSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/CreatePlaceResponse.ts
Normal file
9
src/client/models/CreatePlaceResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CreatePlaceResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/CreatePlaceTypeRequest.ts
Normal file
9
src/client/models/CreatePlaceTypeRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { BasePlaceTypeSchema } from './BasePlaceTypeSchema';
|
||||
export type CreatePlaceTypeRequest = {
|
||||
placeType: BasePlaceTypeSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/CreatePlaceTypeResponse.ts
Normal file
9
src/client/models/CreatePlaceTypeResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CreatePlaceTypeResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/DeletePlaceResponse.ts
Normal file
9
src/client/models/DeletePlaceResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DeletePlaceResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/DeletePlaceTypeResponse.ts
Normal file
9
src/client/models/DeletePlaceTypeResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DeletePlaceTypeResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/EditPlaceRequest.ts
Normal file
9
src/client/models/EditPlaceRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { EditPlaceSchema } from './EditPlaceSchema';
|
||||
export type EditPlaceRequest = {
|
||||
place: EditPlaceSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/EditPlaceResponse.ts
Normal file
9
src/client/models/EditPlaceResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type EditPlaceResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
11
src/client/models/EditPlaceSchema.ts
Normal file
11
src/client/models/EditPlaceSchema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type EditPlaceSchema = {
|
||||
parentId: (number | null);
|
||||
placeTypeId: number;
|
||||
id: number;
|
||||
number: number;
|
||||
};
|
||||
|
||||
9
src/client/models/EditPlaceTypeRequest.ts
Normal file
9
src/client/models/EditPlaceTypeRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { FlatPlaceTypeSchema } from './FlatPlaceTypeSchema';
|
||||
export type EditPlaceTypeRequest = {
|
||||
placeType: FlatPlaceTypeSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/EditPlaceTypeResponse.ts
Normal file
9
src/client/models/EditPlaceTypeResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type EditPlaceTypeResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
10
src/client/models/FlatPlaceTypeSchema.ts
Normal file
10
src/client/models/FlatPlaceTypeSchema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type FlatPlaceTypeSchema = {
|
||||
name: string;
|
||||
parentId: (number | null);
|
||||
id: number;
|
||||
};
|
||||
|
||||
16
src/client/models/GetCardSummariesRequest.ts
Normal file
16
src/client/models/GetCardSummariesRequest.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GetCardSummariesRequest = {
|
||||
full: boolean;
|
||||
cardId?: (number | string | null);
|
||||
cardName?: (string | null);
|
||||
marketplaceKey?: (string | null);
|
||||
shippingWarehouseId?: (number | null);
|
||||
clientId?: (number | null);
|
||||
projectId?: (number | null);
|
||||
boardId?: (number | null);
|
||||
statusId?: (number | null);
|
||||
};
|
||||
|
||||
9
src/client/models/GetChatsListResponse.ts
Normal file
9
src/client/models/GetChatsListResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ChatsListItemSchema } from './ChatsListItemSchema';
|
||||
export type GetChatsListResponse = {
|
||||
chats: Array<ChatsListItemSchema>;
|
||||
};
|
||||
|
||||
9
src/client/models/GetFlatPlaceTypesResponse.ts
Normal file
9
src/client/models/GetFlatPlaceTypesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { FlatPlaceTypeSchema } from './FlatPlaceTypeSchema';
|
||||
export type GetFlatPlaceTypesResponse = {
|
||||
placeTypes: Array<FlatPlaceTypeSchema>;
|
||||
};
|
||||
|
||||
9
src/client/models/GetPlaceTypesResponse.ts
Normal file
9
src/client/models/GetPlaceTypesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlaceTypeSchema } from './PlaceTypeSchema';
|
||||
export type GetPlaceTypesResponse = {
|
||||
placeTypes: Array<PlaceTypeSchema>;
|
||||
};
|
||||
|
||||
9
src/client/models/GetPlacesResponse.ts
Normal file
9
src/client/models/GetPlacesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlaceSchema } from './PlaceSchema';
|
||||
export type GetPlacesResponse = {
|
||||
places: Array<PlaceSchema>;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type GetTimeTrackingRecordsRequest = {
|
||||
date: string;
|
||||
userIds: Array<number>;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
};
|
||||
|
||||
|
||||
14
src/client/models/PlaceSchema.ts
Normal file
14
src/client/models/PlaceSchema.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { BasePlaceTypeWithCountSchema } from './BasePlaceTypeWithCountSchema';
|
||||
export type PlaceSchema = {
|
||||
parentId: (number | null);
|
||||
placeTypeId: number;
|
||||
id: number;
|
||||
number: number;
|
||||
children?: Array<PlaceSchema>;
|
||||
placeType: BasePlaceTypeWithCountSchema;
|
||||
};
|
||||
|
||||
12
src/client/models/PlaceTypeSchema.ts
Normal file
12
src/client/models/PlaceTypeSchema.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type PlaceTypeSchema = {
|
||||
name: string;
|
||||
parentId: (number | null);
|
||||
id: number;
|
||||
children?: Array<PlaceTypeSchema>;
|
||||
placesCount: number;
|
||||
};
|
||||
|
||||
9
src/client/models/ReorderResponse.ts
Normal file
9
src/client/models/ReorderResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ReorderResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
@@ -56,11 +56,13 @@ import type { CreateCardsFromExcelResponse } from '../models/CreateCardsFromExce
|
||||
import type { GetAvailableEmployeesToAssignResponse } from '../models/GetAvailableEmployeesToAssignResponse';
|
||||
import type { GetCardProductsBarcodesPdfRequest } from '../models/GetCardProductsBarcodesPdfRequest';
|
||||
import type { GetCardProductsBarcodesPdfResponse } from '../models/GetCardProductsBarcodesPdfResponse';
|
||||
import type { GetCardSummariesRequest } from '../models/GetCardSummariesRequest';
|
||||
import type { ManageEmployeeRequest } from '../models/ManageEmployeeRequest';
|
||||
import type { ManageEmployeeResponse } from '../models/ManageEmployeeResponse';
|
||||
import type { ParseCardsExcelResponse } from '../models/ParseCardsExcelResponse';
|
||||
import type { ProductsAndServicesGeneralInfoRequest } from '../models/ProductsAndServicesGeneralInfoRequest';
|
||||
import type { ProductsAndServicesGeneralInfoResponse } from '../models/ProductsAndServicesGeneralInfoResponse';
|
||||
import type { ReorderResponse } from '../models/ReorderResponse';
|
||||
import type { UpdateCardClientRequest } from '../models/UpdateCardClientRequest';
|
||||
import type { UpdateCardClientResponse } from '../models/UpdateCardClientResponse';
|
||||
import type { UpdateCardManagerRequest } from '../models/UpdateCardManagerRequest';
|
||||
@@ -155,16 +157,23 @@ export class CardService {
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getCardSummaries({
|
||||
full,
|
||||
requestBody,
|
||||
page,
|
||||
itemsPerPage,
|
||||
}: {
|
||||
full: (boolean | null),
|
||||
requestBody: GetCardSummariesRequest,
|
||||
page?: (number | null),
|
||||
itemsPerPage?: (number | null),
|
||||
}): CancelablePromise<CardSummaryResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
method: 'POST',
|
||||
url: '/card/summaries',
|
||||
query: {
|
||||
'full': full,
|
||||
'page': page,
|
||||
'items_per_page': itemsPerPage,
|
||||
},
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
@@ -172,14 +181,14 @@ export class CardService {
|
||||
}
|
||||
/**
|
||||
* Reorder
|
||||
* @returns CardSummaryResponse Successful Response
|
||||
* @returns ReorderResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static reorderCardSummaries({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: CardSummaryReorderRequest,
|
||||
}): CancelablePromise<CardSummaryResponse> {
|
||||
}): CancelablePromise<ReorderResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/card/summaries/reorder',
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { EditMessageRequest } from '../models/EditMessageRequest';
|
||||
import type { EditMessageResponse } from '../models/EditMessageResponse';
|
||||
import type { GetChatRequest } from '../models/GetChatRequest';
|
||||
import type { GetChatResponse } from '../models/GetChatResponse';
|
||||
import type { GetChatsListResponse } from '../models/GetChatsListResponse';
|
||||
import type { GetMessagesRequest } from '../models/GetMessagesRequest';
|
||||
import type { GetMessagesResponse } from '../models/GetMessagesResponse';
|
||||
import type { LoadMessagesResponse } from '../models/LoadMessagesResponse';
|
||||
@@ -150,6 +151,27 @@ export class ChatService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get Client Chats List
|
||||
* @returns GetChatsListResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getClientChatsList({
|
||||
clientId,
|
||||
}: {
|
||||
clientId: number,
|
||||
}): CancelablePromise<GetChatsListResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/chat/for-client/{client_id}',
|
||||
path: {
|
||||
'client_id': clientId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create Chat
|
||||
* @returns CreateChatResponse Successful Response
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { ClientGetResponse } from '../models/ClientGetResponse';
|
||||
import type { ClientUpdateDetailsRequest } from '../models/ClientUpdateDetailsRequest';
|
||||
import type { ClientUpdateRequest } from '../models/ClientUpdateRequest';
|
||||
import type { ClientUpdateResponse } from '../models/ClientUpdateResponse';
|
||||
import type { CreateGuestUrlResponse } from '../models/CreateGuestUrlResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
@@ -148,4 +149,25 @@ export class ClientService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Client Link
|
||||
* @returns CreateGuestUrlResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static clientLink({
|
||||
clientId,
|
||||
}: {
|
||||
clientId: number,
|
||||
}): CancelablePromise<CreateGuestUrlResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/client/link/{client_id}',
|
||||
path: {
|
||||
'client_id': clientId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
211
src/client/services/WmsService.ts
Normal file
211
src/client/services/WmsService.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { CreatePlaceRequest } from '../models/CreatePlaceRequest';
|
||||
import type { CreatePlaceResponse } from '../models/CreatePlaceResponse';
|
||||
import type { CreatePlaceTypeRequest } from '../models/CreatePlaceTypeRequest';
|
||||
import type { CreatePlaceTypeResponse } from '../models/CreatePlaceTypeResponse';
|
||||
import type { DeletePlaceResponse } from '../models/DeletePlaceResponse';
|
||||
import type { DeletePlaceTypeResponse } from '../models/DeletePlaceTypeResponse';
|
||||
import type { EditPlaceRequest } from '../models/EditPlaceRequest';
|
||||
import type { EditPlaceResponse } from '../models/EditPlaceResponse';
|
||||
import type { EditPlaceTypeRequest } from '../models/EditPlaceTypeRequest';
|
||||
import type { EditPlaceTypeResponse } from '../models/EditPlaceTypeResponse';
|
||||
import type { GetFlatPlaceTypesResponse } from '../models/GetFlatPlaceTypesResponse';
|
||||
import type { GetPlacesResponse } from '../models/GetPlacesResponse';
|
||||
import type { GetPlaceTypesResponse } from '../models/GetPlaceTypesResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class WmsService {
|
||||
/**
|
||||
* Get Place Types
|
||||
* @returns GetPlaceTypesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getPlaceTypes(): CancelablePromise<GetPlaceTypesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/wms/place-type',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create Place Type
|
||||
* @returns CreatePlaceTypeResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createPlaceType({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: CreatePlaceTypeRequest,
|
||||
}): CancelablePromise<CreatePlaceTypeResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/wms/place-type',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Edit Place Type
|
||||
* @returns EditPlaceTypeResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static editPlaceType({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: EditPlaceTypeRequest,
|
||||
}): CancelablePromise<EditPlaceTypeResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PATCH',
|
||||
url: '/wms/place-type',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get Flat Place Types
|
||||
* @returns GetFlatPlaceTypesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getFlatPlaceTypes({
|
||||
parentPlaceTypeId,
|
||||
}: {
|
||||
parentPlaceTypeId: number,
|
||||
}): CancelablePromise<GetFlatPlaceTypesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/wms/place-type/flat/{parent_place_type_id}',
|
||||
path: {
|
||||
'parent_place_type_id': parentPlaceTypeId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Delete Place Type
|
||||
* @returns DeletePlaceTypeResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deletePlaceType({
|
||||
placeTypeId,
|
||||
}: {
|
||||
placeTypeId: number,
|
||||
}): CancelablePromise<DeletePlaceTypeResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/wms/place-type/{place_type_id}',
|
||||
path: {
|
||||
'place_type_id': placeTypeId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get Places
|
||||
* @returns GetPlacesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getPlaces(): CancelablePromise<GetPlacesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/wms/place',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create Place
|
||||
* @returns CreatePlaceResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createPlace({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: CreatePlaceRequest,
|
||||
}): CancelablePromise<CreatePlaceResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/wms/place',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Edit Place
|
||||
* @returns EditPlaceResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static editPlace({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: EditPlaceRequest,
|
||||
}): CancelablePromise<EditPlaceResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PATCH',
|
||||
url: '/wms/place',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Delete Place
|
||||
* @returns DeletePlaceResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deletePlace({
|
||||
placeId,
|
||||
}: {
|
||||
placeId: number,
|
||||
}): CancelablePromise<DeletePlaceResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/wms/place/{place_id}',
|
||||
path: {
|
||||
'place_id': placeId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Generate Place Qr Code Pdf
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getPlaceQrCodePdf({
|
||||
placeId,
|
||||
isShort,
|
||||
}: {
|
||||
placeId: number,
|
||||
isShort: boolean,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/wms/place/pdf/{place_id}/{is_short}',
|
||||
path: {
|
||||
'place_id': placeId,
|
||||
'is_short': isShort,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import { CardGeneralFormType } from "../../pages/CardsPage/drawers/CardEditDrawe
|
||||
type Props = {
|
||||
project: ProjectSchema;
|
||||
form: UseFormReturnType<CardGeneralFormType>;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
const CardAttributeFields = ({ project, form }: Props) => {
|
||||
const CardAttributeFields = ({ project, form, readOnly }: Props) => {
|
||||
const attributes: AttributeSchema[] = [];
|
||||
|
||||
project.attributes.forEach(attribute => {
|
||||
@@ -27,6 +28,7 @@ const CardAttributeFields = ({ project, form }: Props) => {
|
||||
key={attribute.id}
|
||||
attribute={attribute}
|
||||
form={form}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,13 +4,15 @@ import { UseFormReturnType } from "@mantine/form";
|
||||
import { DatePickerInput, DateTimePicker } from "@mantine/dates";
|
||||
import { CardGeneralFormType } from "../../../pages/CardsPage/drawers/CardEditDrawer/tabs/GeneralTab/GeneralTab.tsx";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
|
||||
|
||||
type Props = {
|
||||
attribute: AttributeSchema;
|
||||
form: UseFormReturnType<CardGeneralFormType>;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
const CardAttributeField = ({ attribute, form }: Props) => {
|
||||
const CardAttributeField = ({ attribute, form, readOnly }: Props) => {
|
||||
const type = attribute.type.type;
|
||||
|
||||
const getDateValue = (): Date | null => {
|
||||
@@ -35,6 +37,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
|
||||
<Checkbox
|
||||
label={label}
|
||||
{...form.getInputProps(attribute.name, { type: "checkbox" })}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -43,10 +46,20 @@ const CardAttributeField = ({ attribute, form }: Props) => {
|
||||
<DatePickerInput
|
||||
label={label}
|
||||
{...form.getInputProps(attribute.name)}
|
||||
onChange={(value) => {
|
||||
if (!value) {
|
||||
form.getInputProps(attribute.name).onChange(null);
|
||||
return
|
||||
}
|
||||
form.getInputProps(attribute.name).onChange(
|
||||
dateWithoutTimezone(value),
|
||||
);
|
||||
}}
|
||||
value={getDateValue()}
|
||||
clearable
|
||||
locale={"ru-RU"}
|
||||
valueFormat="DD.MM.YYYY"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -55,10 +68,20 @@ const CardAttributeField = ({ attribute, form }: Props) => {
|
||||
<DateTimePicker
|
||||
label={label}
|
||||
{...form.getInputProps(attribute.name)}
|
||||
onChange={(value) => {
|
||||
if (!value) {
|
||||
form.getInputProps(attribute.name).onChange(null);
|
||||
return
|
||||
}
|
||||
form.getInputProps(attribute.name).onChange(
|
||||
dateWithoutTimezone(value),
|
||||
);
|
||||
}}
|
||||
value={getDateValue()}
|
||||
clearable
|
||||
locale={"ru-RU"}
|
||||
valueFormat="DD.MM.YYYY HH:mm"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -68,6 +91,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
|
||||
label={label}
|
||||
{...form.getInputProps(attribute.name)}
|
||||
value={form.getInputProps(attribute.name).value ?? ""}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -77,6 +101,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
|
||||
allowDecimal={type === "float"}
|
||||
label={label}
|
||||
{...form.getInputProps(attribute.name)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@ import MessageInput from "./components/MessageInput/MessageInput.tsx";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Stack } from "@mantine/core";
|
||||
|
||||
const Chat = () => {
|
||||
type Props = {
|
||||
height?: number | string;
|
||||
padding?: number | string;
|
||||
}
|
||||
|
||||
const Chat = ({ height = "96vh", padding = 0 }: Props) => {
|
||||
const {
|
||||
messages,
|
||||
lastMessage,
|
||||
@@ -45,7 +50,7 @@ const Chat = () => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack mb={"xs"} mr={"xs"}>
|
||||
<Stack mt={"xs"} mr={"xs"} ml={padding}>
|
||||
{dateComponent}
|
||||
<Message
|
||||
key={`${sessionData.id}${index}`}
|
||||
@@ -59,14 +64,14 @@ const Chat = () => {
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<Stack h={"96vh"} justify={"flex-end"}>
|
||||
<MessageInput />
|
||||
<Stack h={height} justify={"flex-end"}>
|
||||
<MessageInput inputMargin={padding} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack h={"96vh"}>
|
||||
<Stack h={height}>
|
||||
<Virtuoso
|
||||
data={messages}
|
||||
followOutput={onFollowOutputHandler}
|
||||
@@ -78,7 +83,7 @@ const Chat = () => {
|
||||
increaseViewportBy={200}
|
||||
alignToBottom
|
||||
/>
|
||||
<MessageInput />
|
||||
<MessageInput inputMargin={padding} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.chats-list-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chats-list-item-selected {
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-3);
|
||||
}
|
||||
}
|
||||
50
src/components/Chat/components/ChatsList/ChatsList.tsx
Normal file
50
src/components/Chat/components/ChatsList/ChatsList.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Center, List, Text } from "@mantine/core";
|
||||
import { ChatSchema, ChatsListItemSchema } from "../../../../client";
|
||||
import { IconBriefcase, IconGrid3x3 } from "@tabler/icons-react";
|
||||
import classNames from "classnames";
|
||||
import styles from "./ChatsList.module.css";
|
||||
|
||||
type Props = {
|
||||
chats: ChatsListItemSchema[];
|
||||
onChatSelect: (chat: ChatSchema) => void;
|
||||
selectedChat: ChatSchema | null;
|
||||
}
|
||||
|
||||
const ChatsList = ({ chats, onChatSelect, selectedChat }: Props) => {
|
||||
const clientChat = chats.find(chat => chat.clientId);
|
||||
chats = chats.filter(chat => !chat.clientId);
|
||||
|
||||
const chatItem = (chat: ChatsListItemSchema, isGeneral: boolean) => {
|
||||
return (
|
||||
<List.Item
|
||||
p={"sm"}
|
||||
onClick={() => onChatSelect(chat)}
|
||||
key={chat.id}
|
||||
icon={(
|
||||
<Center>
|
||||
{isGeneral ? (
|
||||
<IconGrid3x3 />
|
||||
) : (
|
||||
<IconBriefcase />
|
||||
)}
|
||||
</Center>
|
||||
)}
|
||||
className={classNames(
|
||||
styles["chats-list-item"],
|
||||
selectedChat?.id === chat.id && styles["chats-list-item-selected"],
|
||||
)}
|
||||
>
|
||||
<Text>{chat.name}</Text>
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<List center>
|
||||
{clientChat && chatItem(clientChat, true)}
|
||||
{chats.map(chat => chatItem(chat, false))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatsList;
|
||||
@@ -6,7 +6,11 @@ import SelectedFile from "../SelectedFile/SelectedFile.tsx";
|
||||
import React, { useRef } from "react";
|
||||
|
||||
|
||||
const MessageInput = () => {
|
||||
type Props = {
|
||||
inputMargin?: number | string;
|
||||
}
|
||||
|
||||
const MessageInput = ({ inputMargin }: Props) => {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const {
|
||||
@@ -26,7 +30,7 @@ const MessageInput = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getFiles = files.map(file => (
|
||||
const filesList = files.map(file => (
|
||||
<SelectedFile key={file.name} file={file} />
|
||||
));
|
||||
|
||||
@@ -34,8 +38,12 @@ const MessageInput = () => {
|
||||
<form ref={formRef} onSubmit={form.onSubmit(values => submitMessage(values))}>
|
||||
<Stack gap={"xs"}>
|
||||
<Divider />
|
||||
{getFiles}
|
||||
<Group wrap={"nowrap"} align={"flex-end"}>
|
||||
{filesList.length > 0 && (
|
||||
<Stack mx={inputMargin}>
|
||||
{filesList}
|
||||
</Stack>
|
||||
)}
|
||||
<Group wrap={"nowrap"} align={"flex-end"} mx={inputMargin}>
|
||||
{chat?.tgGroup?.tgInviteLink && (
|
||||
<ActionIconCopy
|
||||
onCopiedLabel={"Ссылка на чат скопирована в буфер обмена"}
|
||||
|
||||
@@ -8,6 +8,8 @@ import useStatus from "../../Statuses/Status/hooks/useStatus.tsx";
|
||||
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
|
||||
import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
|
||||
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -22,6 +24,7 @@ const Board = ({ board }: Props) => {
|
||||
onDeleteBoardClick,
|
||||
} = useBoardsContext();
|
||||
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
const { dragState } = useDndContext();
|
||||
|
||||
const { showContextMenu } = useContextMenu();
|
||||
@@ -65,6 +68,7 @@ const Board = ({ board }: Props) => {
|
||||
<Draggable
|
||||
draggableId={"board-" + board.id.toString()}
|
||||
index={board.ordinalNumber}
|
||||
isDragDisabled={authState.isGuest}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
|
||||
@@ -8,6 +8,8 @@ import PrefillCardsWithExcelDrawer
|
||||
from "../../../../pages/CardsPage/drawers/PrefillCardWithExcelDrawer/PrefillCardsWithExcelDrawer.tsx";
|
||||
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
|
||||
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
|
||||
|
||||
const Boards = () => {
|
||||
@@ -16,6 +18,7 @@ const Boards = () => {
|
||||
selectedBoard,
|
||||
onCreateBoardClick,
|
||||
} = useBoardsContext();
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const {
|
||||
onDragEnd,
|
||||
@@ -37,14 +40,16 @@ const Boards = () => {
|
||||
board={board}
|
||||
/>
|
||||
))}
|
||||
<Center
|
||||
px={"md"}
|
||||
py={"xs"}
|
||||
style={{ cursor: "pointer", borderBottom: "solid gray 1px" }}
|
||||
onClick={onCreateBoardClick}
|
||||
>
|
||||
<IconPlus />
|
||||
</Center>
|
||||
{!authState.isGuest && (
|
||||
<Center
|
||||
px={"md"}
|
||||
py={"xs"}
|
||||
style={{ cursor: "pointer", borderBottom: "solid gray 1px" }}
|
||||
onClick={onCreateBoardClick}
|
||||
>
|
||||
<IconPlus />
|
||||
</Center>
|
||||
)}
|
||||
<Box w={"100%"} style={{ borderBottom: "solid gray 1px" }}></Box>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,11 @@ const CardAttributesInSummaryItem = ({ cardSummary }: Props) => {
|
||||
.map(cardAttr => {
|
||||
const isHighlight = isHighlightNeeded(cardAttr);
|
||||
return (
|
||||
<Text c={isHighlight ? "red" : "gray.6"} size={"sm"}>
|
||||
<Text
|
||||
key={cardAttr.attribute.id}
|
||||
c={isHighlight ? "red" : "gray.6"}
|
||||
size={"sm"}
|
||||
>
|
||||
{cardAttr.attribute.label}: {getAttrValueValue(cardAttr)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import CardTags from "../CardTags/CardTags.tsx";
|
||||
import { ModuleNames } from "../../../../modules/modules.tsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
|
||||
type Props = {
|
||||
cards: CardSummary[];
|
||||
@@ -21,6 +23,7 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
|
||||
const [debouncedName] = useDebouncedValue(name, 200);
|
||||
const { selectedProject } = useProjectsContext();
|
||||
const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const totalPrice = useMemo(() => cards.reduce((acc, card) => acc + card.totalPrice, 0), [cards]);
|
||||
const totalProducts = useMemo(() => cards.reduce((acc, card) => acc + card.totalProducts, 0), [cards]);
|
||||
@@ -77,7 +80,7 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
<CardTags groupId={group.id} tags={cards[0].tags}/>
|
||||
{!authState.isGuest && <CardTags groupId={group.id} tags={cards[0].tags}/>}
|
||||
{isServicesAndProductsIncluded && (
|
||||
<Flex
|
||||
p={rem(10)}
|
||||
@@ -95,4 +98,4 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { CardService, CardSummary } from "../../../../client";
|
||||
import styles from "./CardSummaryItem.module.css";
|
||||
|
||||
@@ -14,6 +14,9 @@ import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import CardTags from "../CardTags/CardTags.tsx";
|
||||
import CardAttributesInSummaryItem from "../CardAttributesInSummaryItem/CardAttributesInSummaryItem.tsx";
|
||||
import { ModuleNames } from "../../../../modules/modules.tsx";
|
||||
import isDealPaid from "../../../../pages/CardsPage/utils/isDealPaid.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
|
||||
type Props = {
|
||||
cardSummary: CardSummary;
|
||||
@@ -25,6 +28,8 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
const { selectedProject } = useProjectsContext();
|
||||
const { setSelectedCard } = useCardPageContext();
|
||||
const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState();
|
||||
const [isPaid, setIsPaid] = useState<boolean>(false);
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
const isClientIncluded = isModuleInProject(ModuleNames.CLIENTS, selectedProject);
|
||||
@@ -34,22 +39,23 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
setSelectedCard(card);
|
||||
});
|
||||
};
|
||||
const isPaid = () => {
|
||||
return cardSummary.billRequest?.paid || cardSummary.group?.billRequest?.paid;
|
||||
};
|
||||
const isLockedInsideGroup = () => {
|
||||
return cardSummary.group && !cardSummary.group.billRequest;
|
||||
const canBeRemovedFromGroup = () => {
|
||||
return !!(cardSummary.group && (!cardSummary.group.billRequests || cardSummary.group.billRequests?.length === 0));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsPaid(isDealPaid(cardSummary));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
onContextMenu={showContextMenu([
|
||||
...isLockedInsideGroup() ? [{
|
||||
onContextMenu={!authState.isDealsViewer ? showContextMenu([
|
||||
...!canBeRemovedFromGroup() ? [] : [{
|
||||
key: "removeFromGroup",
|
||||
onClick: () => onDeleteFromGroup(cardSummary),
|
||||
title: "Убрать из группы",
|
||||
icon: <IconLayoutGridRemove />,
|
||||
}] : [],
|
||||
}],
|
||||
{
|
||||
key: "complete",
|
||||
onClick: () => onComplete(cardSummary),
|
||||
@@ -62,7 +68,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
title: "Удалить",
|
||||
icon: <IconTrash />,
|
||||
},
|
||||
])}
|
||||
]) : undefined}
|
||||
onClick={() => onCardSummaryClick()}
|
||||
className={styles["container"]}
|
||||
style={{ backgroundColor: color }}
|
||||
@@ -105,7 +111,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
</Flex>
|
||||
)}
|
||||
<CardAttributesInSummaryItem cardSummary={cardSummary} />
|
||||
{!cardSummary.group?.id && (
|
||||
{!authState.isGuest && !cardSummary.group?.id && (
|
||||
<CardTags cardId={cardSummary.id} tags={cardSummary.tags} />
|
||||
)}
|
||||
<Flex align={"center"} justify={"space-between"}>
|
||||
@@ -147,7 +153,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
</Popover>
|
||||
)}
|
||||
</CopyButton>
|
||||
{isPaid() && (
|
||||
{isPaid && (
|
||||
<Tooltip label={"Оплачен"}>
|
||||
<ThemeIcon variant={"transparent"}>
|
||||
<IconCheck />
|
||||
|
||||
@@ -12,6 +12,8 @@ import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
|
||||
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import { ModuleNames } from "../../../../modules/modules.tsx";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
@@ -28,6 +30,7 @@ export const CardsDndColumn: FC<Props> = ({
|
||||
dragState,
|
||||
withCreateButton = false,
|
||||
}) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
const { selectedProject } = useProjectsContext();
|
||||
const isCreatingDealFromFileEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
const isDropDisabled = dragState !== DragState.DRAG_CARD;
|
||||
@@ -77,7 +80,9 @@ export const CardsDndColumn: FC<Props> = ({
|
||||
<Draggable
|
||||
draggableId={card.id.toString()}
|
||||
index={card.rank}
|
||||
key={card.id}>
|
||||
key={card.id}
|
||||
isDragDisabled={authState.isGuest}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
@@ -104,6 +109,7 @@ export const CardsDndColumn: FC<Props> = ({
|
||||
draggableId={"group-" + group.id}
|
||||
index={cards[0].rank}
|
||||
key={"group-" + group.id}
|
||||
isDragDisabled={authState.isGuest}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
@@ -135,7 +141,7 @@ export const CardsDndColumn: FC<Props> = ({
|
||||
styles["items-list-drag-over"],
|
||||
)}
|
||||
{...provided.droppableProps}>
|
||||
{withCreateButton && (
|
||||
{withCreateButton && !authState.isGuest && (
|
||||
<>
|
||||
<CreateCardButton status={status} />
|
||||
{
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import { modals } from "@mantine/modals";
|
||||
import { Flex } from "@mantine/core";
|
||||
import { DropResult } from "@hello-pangea/dnd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CardGroupService, CardService, CardSummary, CardSummaryReorderRequest } from "../../../../../client";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { dateWithoutTimezone } from "../../../../../shared/lib/date.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
summariesRaw: CardSummary[];
|
||||
summaries: CardSummary[];
|
||||
refetchSummaries: () => void;
|
||||
}
|
||||
|
||||
const useCardsDnd = ({
|
||||
summariesRaw,
|
||||
refetchSummaries,
|
||||
}: Props) => {
|
||||
const [summaries, setSummaries] = useState(summariesRaw);
|
||||
|
||||
useEffect(() => {
|
||||
setSummaries(summariesRaw);
|
||||
}, [summariesRaw]);
|
||||
|
||||
const useCardsDnd = ({ summaries, refetchSummaries }: Props) => {
|
||||
const recalculate = async (cardId: number) => {
|
||||
return CardService.recalculateCardPrice({
|
||||
requestBody: {
|
||||
@@ -103,7 +93,6 @@ const useCardsDnd = ({
|
||||
notifications.error({ message: response.message });
|
||||
return;
|
||||
}
|
||||
await refetchSummaries();
|
||||
await recalculate(sourceId);
|
||||
await refetchSummaries();
|
||||
});
|
||||
@@ -120,7 +109,6 @@ const useCardsDnd = ({
|
||||
notifications.error({ message: response.message });
|
||||
return;
|
||||
}
|
||||
await refetchSummaries();
|
||||
await recalculate(sourceId);
|
||||
await refetchSummaries();
|
||||
});
|
||||
@@ -185,8 +173,7 @@ const useCardsDnd = ({
|
||||
if (statusId == summary.status.id) {
|
||||
CardService.reorderCardSummaries({
|
||||
requestBody: request as CardSummaryReorderRequest,
|
||||
}).then(async response => {
|
||||
setSummaries(response.summaries);
|
||||
}).then(async () => {
|
||||
await refetchSummaries();
|
||||
});
|
||||
return;
|
||||
@@ -200,14 +187,12 @@ const useCardsDnd = ({
|
||||
comment: "",
|
||||
deadline: dateWithoutTimezone(new Date()),
|
||||
},
|
||||
}).then(async response => {
|
||||
setSummaries(response.summaries);
|
||||
}).then(async () => {
|
||||
await refetchSummaries();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
summaries,
|
||||
onCardDragEnd,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import { usePrefillCardContext } from "../../../../pages/CardsPage/contexts/Pref
|
||||
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import { ModuleNames } from "../../../../modules/modules.tsx";
|
||||
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
@@ -20,6 +21,7 @@ const CreateCardButton = ({ status }: Props) => {
|
||||
const [isTransitionEnded, setIsTransitionEnded] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
const { prefillCard, setPrefillCard } = usePrefillCardContext();
|
||||
const { refetchSummaries } = useDndContext();
|
||||
|
||||
const { selectedProject } = useProjectsContext();
|
||||
const isPrefillingDealEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
@@ -57,7 +59,7 @@ const CreateCardButton = ({ status }: Props) => {
|
||||
},
|
||||
}).then(async (result) => {
|
||||
if (isPrefillingDealEnabled && prefillCard) {
|
||||
CardService.prefillCard({
|
||||
await CardService.prefillCard({
|
||||
requestBody: {
|
||||
oldCardId: prefillCard.id,
|
||||
newCardId: result.cardId,
|
||||
@@ -67,6 +69,7 @@ const CreateCardButton = ({ status }: Props) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["getCardSummaries"],
|
||||
});
|
||||
refetchSummaries();
|
||||
setIsCreating(false);
|
||||
setPrefillCard(undefined);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import { useEqualHeightsContext } from "./contexts/EqualHeightContext.tsx";
|
||||
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
|
||||
import { ModuleNames } from "../../../../modules/modules.tsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -26,6 +28,7 @@ const Status = ({ summaries, status, dragState, index }: Props) => {
|
||||
const {
|
||||
selectedBoard,
|
||||
} = useBoardsContext();
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const isDropDisabled = dragState !== DragState.DRAG_STATUS;
|
||||
const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedBoard?.project);
|
||||
@@ -107,6 +110,7 @@ const Status = ({ summaries, status, dragState, index }: Props) => {
|
||||
<Draggable
|
||||
draggableId={"status-" + status.id.toString()}
|
||||
index={status.ordinalNumber}
|
||||
isDragDisabled={authState.isGuest}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
|
||||
@@ -10,11 +10,14 @@ import getRenderOptions from "./utils/getRenderOptions.tsx";
|
||||
type RestProps = {
|
||||
clientId: number;
|
||||
};
|
||||
|
||||
const MAX_PRODUCTS = 200;
|
||||
type Props = Omit<ObjectSelectProps<ProductSchema>, "data"> & RestProps;
|
||||
|
||||
const ProductSelect: FC<Props> = (props: Props) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debounced] = useDebouncedValue(searchValue, 500);
|
||||
const [searchValueServer, setSearchValueServer] = useState("");
|
||||
const [debounced] = useDebouncedValue(searchValueServer, 500);
|
||||
const { products, isLoading } = useProductsList({
|
||||
clientId: props.clientId,
|
||||
searchInput: debounced,
|
||||
@@ -22,21 +25,35 @@ const ProductSelect: FC<Props> = (props: Props) => {
|
||||
itemsPerPage: MAX_PRODUCTS,
|
||||
});
|
||||
const restProps = omit(props, ["clientId"]);
|
||||
|
||||
const optionsFilter: OptionsFilter = ({ options }) => options;
|
||||
const filterProducts = (searchFilter: string): ProductSchema[] => {
|
||||
searchFilter = searchFilter.toLowerCase().trim();
|
||||
if (!searchFilter) return products;
|
||||
const filteredByName = products.filter((v) => v.name.toLowerCase().includes(searchFilter));
|
||||
const filteredByBarcodes = products.filter((v) => v.barcodes?.some(barcode => barcode.toLowerCase().includes(searchFilter)));
|
||||
const filteredByArticle = products.filter((v) => v.article == searchFilter);
|
||||
const uniqueProducts = new Set([...filteredByName, ...filteredByBarcodes, ...filteredByArticle]);
|
||||
return Array.from(uniqueProducts).sort((a, b) => a.id - b.id);
|
||||
};
|
||||
const optionsFilter: OptionsFilter = ({ search }) => {
|
||||
return filterProducts(search).map(product => ({ label: product.name, value: product.id.toString() }));
|
||||
};
|
||||
const setSearchValueImpl = (value: string) => {
|
||||
const names = products.map(product => product.name);
|
||||
if (names.includes(value)) return;
|
||||
setSearchValue(value);
|
||||
const filtered = filterProducts(value);
|
||||
if (filtered && filtered.length > 0) {
|
||||
return;
|
||||
}
|
||||
setSearchValueServer(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
rightSection={
|
||||
isLoading || searchValue !== debounced ? (
|
||||
isLoading? (
|
||||
<Loader size={"sm"} />
|
||||
) : null
|
||||
}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValueImpl}
|
||||
renderOption={getRenderOptions(products)}
|
||||
searchable
|
||||
|
||||
@@ -1,44 +1,23 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
import { Select } from "@mantine/core";
|
||||
import { FC } from "react";
|
||||
import { ClientSchema } from "../../../client";
|
||||
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
|
||||
import ObjectSelect, { ObjectSelectProps } from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<ClientSchema>,
|
||||
"data" | "getLabelFn" | "getValueFn"
|
||||
>;
|
||||
|
||||
const ClientSelect: FC<Props> = (props: Props) => {
|
||||
const { clients } = useClientsList({ all: true });
|
||||
|
||||
|
||||
type Props = {
|
||||
value?: ClientSchema;
|
||||
onChange: (client: ClientSchema) => void;
|
||||
withLabel?: boolean;
|
||||
error?: string;
|
||||
inputContainer?: (children: ReactNode) => ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
const ClientSelect: FC<Props> = ({ value, onChange, error, inputContainer, withLabel = false, disabled = false }) => {
|
||||
const { clients } = useClientsList();
|
||||
const options = clients.map(client => ({
|
||||
label: client.name,
|
||||
value: client.id.toString(),
|
||||
}));
|
||||
return (
|
||||
<Select
|
||||
<ObjectSelect
|
||||
{...props}
|
||||
searchable
|
||||
placeholder={"Выберите клиента"}
|
||||
value={
|
||||
value &&
|
||||
options.find(client => client.value == value.id.toString())
|
||||
?.value
|
||||
}
|
||||
onChange={event => {
|
||||
if (!event) return;
|
||||
const client = clients.find(
|
||||
client => client.id == parseInt(event)
|
||||
);
|
||||
if (!client) return;
|
||||
onChange(client);
|
||||
}}
|
||||
data={options}
|
||||
label={withLabel && "Клиент"}
|
||||
error={error}
|
||||
inputContainer={inputContainer}
|
||||
disabled={disabled}
|
||||
data={clients.filter(cl => !cl.isDeleted)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx"
|
||||
type Props = Omit<ObjectSelectProps<ClientSchema>, "data">;
|
||||
|
||||
const ClientSelectNew: FC<Props> = props => {
|
||||
const { clients } = useClientsList();
|
||||
const { clients } = useClientsList({ all: false });
|
||||
return (
|
||||
<ObjectSelect
|
||||
searchable
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { jwtDecode, JwtPayload as JwtPayloadBase } from "jwt-decode";
|
||||
import { OpenAPI } from "../client";
|
||||
|
||||
interface AuthState {
|
||||
isAuthorized: boolean;
|
||||
accessToken: string;
|
||||
isDealEditor: boolean;
|
||||
isDealsViewer: boolean;
|
||||
isGuest: boolean;
|
||||
role: string;
|
||||
}
|
||||
@@ -16,6 +19,8 @@ const initialState = (): AuthState => {
|
||||
return {
|
||||
accessToken: "",
|
||||
isAuthorized: false,
|
||||
isDealEditor: false,
|
||||
isDealsViewer: false,
|
||||
isGuest: false,
|
||||
role: "user",
|
||||
};
|
||||
@@ -34,10 +39,13 @@ const authSlice = createSlice({
|
||||
const { sub, role } = jwtDecode<JwtPayload>(
|
||||
action.payload.accessToken,
|
||||
);
|
||||
OpenAPI.TOKEN = action.payload.accessToken;
|
||||
state.accessToken = action.payload.accessToken;
|
||||
state.isAuthorized = true;
|
||||
state.role = role;
|
||||
if (sub === "guest") state.isGuest = true;
|
||||
state.isDealEditor = sub === "deal_editor";
|
||||
state.isDealsViewer = sub === "deals_viewer";
|
||||
state.isGuest = state.isDealEditor || state.isDealsViewer;
|
||||
} catch (_) {
|
||||
const url = window.location.href;
|
||||
const urlObj = new URL(url);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { type FullProjectSchema, ProjectService } from "../client";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../redux/store.ts";
|
||||
|
||||
|
||||
const useProjects = () => {
|
||||
const [projects, setProjects] = useState<FullProjectSchema[]>([]);
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const refetchProjects = () => {
|
||||
ProjectService.getProjects()
|
||||
@@ -14,8 +17,10 @@ const useProjects = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetchProjects();
|
||||
}, []);
|
||||
if (authState.isAuthorized) {
|
||||
refetchProjects();
|
||||
}
|
||||
}, [authState.isAuthorized]);
|
||||
|
||||
return {
|
||||
projects,
|
||||
|
||||
@@ -42,6 +42,9 @@ import AttributeModal from "../pages/AdminPage/tabs/Attributes/modals/AttributeM
|
||||
import CreateProjectModal
|
||||
from "../pages/CardsPage/drawers/ProjectEditDrawer/tabs/General/modals/CreateProjectModal.tsx";
|
||||
import CardTagModal from "../pages/CardsPage/drawers/ProjectEditDrawer/tabs/Tags/modals/CardTagModal.tsx";
|
||||
import PlaceTypeModal from "../pages/AdminPage/tabs/WarehouseManagement/placeType/modals/PlaceTypeModal.tsx";
|
||||
import PlaceModal from "../pages/AdminPage/tabs/WarehouseManagement/place/modals/PlaceModal.tsx";
|
||||
import SelectPlaceQrType from "../pages/AdminPage/tabs/WarehouseManagement/place/modals/SelectPlaceQrType.tsx";
|
||||
|
||||
export const modals = {
|
||||
enterDeadline: EnterDeadlineModal,
|
||||
@@ -82,4 +85,7 @@ export const modals = {
|
||||
attributeModal: AttributeModal,
|
||||
createProjectModal: CreateProjectModal,
|
||||
cardTagModal: CardTagModal,
|
||||
placeTypeModal: PlaceTypeModal,
|
||||
placeModal: PlaceModal,
|
||||
selectPlaceQrType: SelectPlaceQrType,
|
||||
};
|
||||
|
||||
@@ -22,12 +22,14 @@ const ClientTab = () => {
|
||||
initialValues,
|
||||
validate: {
|
||||
details: {
|
||||
phoneNumber: value =>
|
||||
!phone(value || "", {
|
||||
phoneNumber: value => {
|
||||
if (!value || value === "+7 ") return false;
|
||||
return !phone(value || "", {
|
||||
country: "",
|
||||
strictDetection: false,
|
||||
validateMobilePrefix: false,
|
||||
}).isValid && "Неверно указан номер телефона",
|
||||
}).isValid && "Неверно указан номер телефона";
|
||||
},
|
||||
inn: (inn: string | undefined | null) => inn && !isValidInn(inn) ? "Некорректный ИНН" : null,
|
||||
},
|
||||
},
|
||||
@@ -40,9 +42,10 @@ const ClientTab = () => {
|
||||
form.setValues(data);
|
||||
}, [card]);
|
||||
|
||||
const isEditorDisabled = () => client?.id !== card?.client?.id;
|
||||
const isEditorDisabled = () => client?.id !== card?.client?.id || client?.isDeleted === true;
|
||||
|
||||
const handleSubmitClientInfo = (values: ClientSchema) => {
|
||||
if (values.details?.phoneNumber === "+7 ") values.details!.phoneNumber = "";
|
||||
ClientService.updateClient({
|
||||
requestBody: {
|
||||
data: values,
|
||||
@@ -98,6 +101,7 @@ const ClientTab = () => {
|
||||
mask="+7 000 000-00-00"
|
||||
placeholder={"Введите номер телефона"}
|
||||
{...form.getInputProps("details.phoneNumber")}
|
||||
disabled={isEditorDisabled()}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<TextInput
|
||||
@@ -145,7 +149,6 @@ const ClientTab = () => {
|
||||
<ClientSelect
|
||||
value={client}
|
||||
onChange={setClient}
|
||||
withLabel
|
||||
disabled={!isEqual(initialValues, form.values) || !!card?.chat}
|
||||
/>
|
||||
{!card?.chat && (
|
||||
@@ -164,4 +167,4 @@ const ClientTab = () => {
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
export default ClientTab;
|
||||
export default ClientTab;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import styles from "./ProductAndServiceTab.module.css";
|
||||
import ProductView from "./components/ProductView/ProductView.tsx";
|
||||
import { Button, Checkbox, Divider, Flex, Group, rem, ScrollArea, Stack, Text, Title } from "@mantine/core";
|
||||
@@ -20,10 +20,22 @@ import GeneralDataForm from "./components/GeneralDataForm/GeneralDataForm.tsx";
|
||||
import PrintDealBarcodesButton from "./components/PrintDealBarcodesButton/PrintDealBarcodesButton.tsx";
|
||||
import PaymentLinkButton from "./components/PaymentLinkButton/PaymentLinkButton.tsx";
|
||||
import isValidInn from "../../../../pages/ClientsPage/utils/isValidInn.ts";
|
||||
import isDealPaid, { isDealLocked } from "../../../../pages/CardsPage/utils/isDealPaid.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
import { useCardPageContext } from "../../../../pages/CardsPage/contexts/CardPageContext.tsx";
|
||||
|
||||
const ProductAndServiceTab: FC = () => {
|
||||
const { cardState, cardServicesState, cardProductsState } = useCardProductAndServiceTabState();
|
||||
const isLocked = Boolean(cardState.card?.billRequest || cardState.card?.group?.billRequest);
|
||||
const { selectedCard: card } = useCardPageContext();
|
||||
const isLocked = isDealLocked(cardState.card);
|
||||
const [paid, setPaid] = useState<boolean>(false);
|
||||
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
useEffect(() => {
|
||||
setPaid(isDealPaid(cardState.card));
|
||||
}, []);
|
||||
|
||||
const onAddProductClick = () => {
|
||||
if (!cardProductsState.onCreate || !cardState.card || !cardState.card.clientId) return;
|
||||
const productIds = cardState.card.products.map(
|
||||
@@ -121,7 +133,9 @@ const ProductAndServiceTab: FC = () => {
|
||||
ProductService.createProduct({
|
||||
requestBody: newProduct,
|
||||
}).then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message: message });
|
||||
notifications.guess(ok, { message });
|
||||
}).catch(err => {
|
||||
notifications.error({ message: err.toString() });
|
||||
});
|
||||
};
|
||||
const onCreateProductClick = () => {
|
||||
@@ -222,7 +236,7 @@ const ProductAndServiceTab: FC = () => {
|
||||
<div
|
||||
className={classNames(
|
||||
styles["container"],
|
||||
cardState.card?.billRequest && styles["container-disabled"],
|
||||
cardState.card?.billRequests?.length && styles["container-disabled"],
|
||||
)}>
|
||||
<div className={styles["products-list"]}>
|
||||
<ScrollArea offsetScrollbars>
|
||||
@@ -249,18 +263,18 @@ const ProductAndServiceTab: FC = () => {
|
||||
mr={"xs"}
|
||||
>
|
||||
<Group wrap={"nowrap"}>
|
||||
<PrintDealBarcodesButton card={cardState.card} />
|
||||
{!isDealsViewer && <PrintDealBarcodesButton card={cardState.card} />}
|
||||
<Checkbox
|
||||
label={"Оплачен"}
|
||||
checked={cardState.card.billRequest?.paid || cardState.card.group?.billRequest?.paid || false}
|
||||
checked={paid}
|
||||
disabled
|
||||
/>
|
||||
</Group>
|
||||
<PaymentLinkButton card={cardState.card} />
|
||||
{!isDealsViewer && <PaymentLinkButton card={cardState.card} />}
|
||||
</Group>
|
||||
)}
|
||||
<Stack className={styles["card-container-wrapper"]} mr={"xs"}>
|
||||
<GeneralDataForm />
|
||||
{card && <GeneralDataForm />}
|
||||
</Stack>
|
||||
<ScrollArea offsetScrollbars>
|
||||
<Flex
|
||||
@@ -271,41 +285,45 @@ const ProductAndServiceTab: FC = () => {
|
||||
{...cardServicesState}
|
||||
/>
|
||||
|
||||
<Divider my={rem(15)} />
|
||||
<div className={styles["card-container-buttons"]}>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
variant={"default"}
|
||||
fullWidth
|
||||
onClick={onCreateProductClick}>
|
||||
Создать товар
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onAddProductClick}
|
||||
variant={"default"}
|
||||
fullWidth>
|
||||
Добавить товар
|
||||
</Button>
|
||||
</div>
|
||||
<Divider my={rem(15)} />
|
||||
<div className={styles["card-container-buttons"]}>
|
||||
{isLocked ? (
|
||||
<Button
|
||||
onClick={onCancelBillClick}
|
||||
color={"red"}>
|
||||
Отозвать счет
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onCreateBillClick}
|
||||
variant={"default"}
|
||||
fullWidth>
|
||||
Выставить счет
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!isDealsViewer && (
|
||||
<>
|
||||
<Divider my={rem(15)} />
|
||||
<div className={styles["card-container-buttons"]}>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
variant={"default"}
|
||||
fullWidth
|
||||
onClick={onCreateProductClick}>
|
||||
Создать товар
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onAddProductClick}
|
||||
variant={"default"}
|
||||
fullWidth>
|
||||
Добавить товар
|
||||
</Button>
|
||||
</div>
|
||||
<Divider my={rem(15)} />
|
||||
<div className={styles["card-container-buttons"]}>
|
||||
{isLocked ? (
|
||||
<Button
|
||||
onClick={onCancelBillClick}
|
||||
color={"red"}>
|
||||
Отозвать счет
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onCreateBillClick}
|
||||
variant={"default"}
|
||||
fullWidth>
|
||||
Выставить счет
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { RootState } from "../../../../../../redux/store.ts";
|
||||
import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
|
||||
import LockCheckbox from "../../../../../../components/LockCheckbox/LockCheckbox.tsx";
|
||||
import { useDebouncedCallback } from "@mantine/hooks";
|
||||
import { isDealLocked } from "../../../../../../pages/CardsPage/utils/isDealPaid.ts";
|
||||
|
||||
type RestProps = {
|
||||
onKitAdd?: (kit: GetServiceKitSchema) => void;
|
||||
@@ -31,7 +32,7 @@ const CardServicesTable: FC<Props> = ({
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const { cardState } = useCardProductAndServiceTabState();
|
||||
const isLocked = Boolean(cardState.card?.billRequest);
|
||||
const isLocked = isDealLocked(cardState.card);
|
||||
|
||||
const [currentService, setCurrentService] = useState<
|
||||
CardServiceSchema | undefined
|
||||
@@ -137,15 +138,17 @@ const CardServicesTable: FC<Props> = ({
|
||||
w={"100%"}
|
||||
gap={rem(10)}
|
||||
align={"center"}>
|
||||
<Tooltip
|
||||
onClick={() => onDeleteClick(service)}
|
||||
label="Удалить услугу">
|
||||
<ActionIcon
|
||||
disabled={isLocked}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{!authState.isDealsViewer && (
|
||||
<Tooltip
|
||||
onClick={() => onDeleteClick(service)}
|
||||
label="Удалить услугу">
|
||||
<ActionIcon
|
||||
disabled={isLocked}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!authState.isGuest && (
|
||||
<Tooltip label="Сотрудники">
|
||||
<ActionIcon
|
||||
@@ -166,9 +169,8 @@ const CardServicesTable: FC<Props> = ({
|
||||
isNumber(event) &&
|
||||
onQuantityChange(service, event)
|
||||
}
|
||||
|
||||
value={service.quantity}
|
||||
|
||||
readOnly={authState.isDealsViewer}
|
||||
/>
|
||||
<NumberInput
|
||||
flex={1}
|
||||
@@ -179,6 +181,7 @@ const CardServicesTable: FC<Props> = ({
|
||||
suffix={"₽"}
|
||||
value={service.price}
|
||||
disabled={authState.isGuest || isLocked || service.isFixedPrice}
|
||||
readOnly={authState.isDealsViewer}
|
||||
rightSectionProps={{
|
||||
style: {
|
||||
display: "flex",
|
||||
@@ -187,12 +190,14 @@ const CardServicesTable: FC<Props> = ({
|
||||
},
|
||||
}}
|
||||
rightSection={
|
||||
<LockCheckbox
|
||||
label={"Зафиксировать цену"}
|
||||
variant={"default"}
|
||||
value={service.isFixedPrice}
|
||||
onChange={value => onLockChange(service, value)}
|
||||
/>
|
||||
!authState.isDealsViewer && (
|
||||
<LockCheckbox
|
||||
label={"Зафиксировать цену"}
|
||||
variant={"default"}
|
||||
value={service.isFixedPrice}
|
||||
onChange={value => onLockChange(service, value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -211,25 +216,27 @@ const CardServicesTable: FC<Props> = ({
|
||||
₽
|
||||
</Title>
|
||||
</Flex>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
mt={"auto"}>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onCreateClick}
|
||||
fullWidth
|
||||
variant={"default"}>
|
||||
Добавить услугу
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onAddKitClick}
|
||||
fullWidth
|
||||
variant={"default"}>
|
||||
Добавить набор услуг
|
||||
</Button>
|
||||
</Flex>
|
||||
{!authState.isDealsViewer && (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
mt={"auto"}>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onCreateClick}
|
||||
fullWidth
|
||||
variant={"default"}>
|
||||
Добавить услугу
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onAddKitClick}
|
||||
fullWidth
|
||||
variant={"default"}>
|
||||
Добавить набор услуг
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<Modal
|
||||
title={"Добавление сотрудника к услуге"}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Button, Checkbox, Stack } from "@mantine/core";
|
||||
import { notifications } from "../../../../../../shared/lib/notifications.ts";
|
||||
import { useEffect, useState } from "react";
|
||||
import { isEqual } from "lodash";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../../../redux/store.ts";
|
||||
|
||||
|
||||
type GeneralDataFormType = {
|
||||
@@ -16,7 +18,8 @@ type GeneralDataFormType = {
|
||||
|
||||
const GeneralDataForm = () => {
|
||||
const { selectedCard: card, refetchCard } = useCardPageContext();
|
||||
if (!card) return;
|
||||
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
|
||||
if (!card) return <></>;
|
||||
|
||||
const [initialValues, setInitialValues] = useState<GeneralDataFormType>(card);
|
||||
|
||||
@@ -67,7 +70,7 @@ const GeneralDataForm = () => {
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<Stack>
|
||||
<ShippingWarehouseAutocomplete
|
||||
placeholder={"Введите склад отгрузки"}
|
||||
placeholder={isDealsViewer ? "" : "Введите склад отгрузки"}
|
||||
label={"Склад отгрузки"}
|
||||
value={
|
||||
isShippingWarehouse(
|
||||
@@ -87,18 +90,23 @@ const GeneralDataForm = () => {
|
||||
"shippingWarehouse",
|
||||
).onChange(event);
|
||||
}}
|
||||
readOnly={isDealsViewer}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Учет выручки в статистике"}
|
||||
{...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
|
||||
/>
|
||||
<Button
|
||||
type={"submit"}
|
||||
variant={"default"}
|
||||
disabled={isEqual(initialValues, form.values)}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
{!isDealsViewer && (
|
||||
<>
|
||||
<Checkbox
|
||||
label={"Учет выручки в статистике"}
|
||||
{...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
|
||||
/>
|
||||
<Button
|
||||
type={"submit"}
|
||||
variant={"default"}
|
||||
disabled={isEqual(initialValues, form.values)}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -3,19 +3,40 @@ import ButtonCopy from "../../../../../../components/ButtonCopy/ButtonCopy.tsx";
|
||||
import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
|
||||
import { getCurrentDateTimeForFilename } from "../../../../../../shared/lib/date.ts";
|
||||
import FileSaver from "file-saver";
|
||||
import { Button, Popover, Stack } from "@mantine/core";
|
||||
|
||||
type Props = {
|
||||
card: CardSchema;
|
||||
}
|
||||
|
||||
const PaymentLinkButton = ({ card }: Props) => {
|
||||
const billRequestPdfUrl = card?.billRequest?.pdfUrl || card?.group?.billRequest?.pdfUrl;
|
||||
if ((!card.billRequests || card.billRequests.length === 0) && (!card?.group?.billRequests || card?.group?.billRequests.length === 0)) {
|
||||
return (
|
||||
<ButtonCopyControlled
|
||||
onCopyClick={() => {
|
||||
const date =
|
||||
getCurrentDateTimeForFilename();
|
||||
FileSaver.saveAs(
|
||||
`${import.meta.env.VITE_API_URL}/card/billing-document/${card.id}`,
|
||||
`bill_${card.id}_${date}.pdf`,
|
||||
);
|
||||
}}
|
||||
copied={false}
|
||||
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
||||
>
|
||||
Ссылка на оплату (PDF)
|
||||
</ButtonCopyControlled>
|
||||
);
|
||||
}
|
||||
|
||||
if (billRequestPdfUrl) {
|
||||
const requests = (card?.group ? card?.group?.billRequests : card.billRequests) ?? [];
|
||||
const urls = requests.map(request => request.pdfUrl).filter(url => url !== null);
|
||||
|
||||
if (urls.length === 1) {
|
||||
return (
|
||||
<ButtonCopy
|
||||
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
||||
value={billRequestPdfUrl}
|
||||
value={urls[0]}
|
||||
>
|
||||
Ссылка на оплату
|
||||
</ButtonCopy>
|
||||
@@ -23,21 +44,25 @@ const PaymentLinkButton = ({ card }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonCopyControlled
|
||||
onCopyClick={() => {
|
||||
const date =
|
||||
getCurrentDateTimeForFilename();
|
||||
FileSaver.saveAs(
|
||||
`${import.meta.env.VITE_API_URL}/card/billing-document/${card.id}`,
|
||||
`bill_${card.id}_${date}.pdf`,
|
||||
);
|
||||
}}
|
||||
copied={false}
|
||||
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
||||
>
|
||||
Ссылка на оплату (PDF)
|
||||
</ButtonCopyControlled>
|
||||
<Popover width={380} position="bottom" withArrow shadow="md">
|
||||
<Popover.Target>
|
||||
<Button variant={"default"}>Ссылки на оплату</Button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Stack gap={"md"}>
|
||||
{urls.map((url, i) => (
|
||||
<ButtonCopy
|
||||
key={i}
|
||||
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
||||
value={url}
|
||||
>
|
||||
{`Ссылка на оплату (часть ${String(i + 1)})`}
|
||||
</ButtonCopy>
|
||||
))}
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default PaymentLinkButton;
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleUsersTable from "../../../../../../pages/CardsPage/components/Simpl
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../../../redux/store.ts";
|
||||
import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
|
||||
import { isDealLocked } from "../../../../../../pages/CardsPage/utils/isDealPaid.ts";
|
||||
|
||||
type RestProps = {
|
||||
quantity: number;
|
||||
@@ -20,16 +21,16 @@ type RestProps = {
|
||||
|
||||
type Props = CRUDTableProps<CardProductServiceSchema> & RestProps;
|
||||
const ProductServicesTable: FC<Props> = ({
|
||||
items,
|
||||
quantity,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onChange,
|
||||
onCopyServices,
|
||||
onKitAdd,
|
||||
}) => {
|
||||
items,
|
||||
quantity,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onChange,
|
||||
onCopyServices,
|
||||
onKitAdd,
|
||||
}) => {
|
||||
const { cardState } = useCardProductAndServiceTabState();
|
||||
const isLocked = Boolean(cardState.card?.billRequest);
|
||||
const isLocked = isDealLocked(cardState.card);
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const columns = useProductServicesTableColumns({ data: items, quantity });
|
||||
@@ -78,7 +79,7 @@ const ProductServicesTable: FC<Props> = ({
|
||||
const getCurrentEmployees = (): UserSchema[] => {
|
||||
if (!currentService) return [];
|
||||
const item = items.find(
|
||||
i => i.service.id === currentService.service.id
|
||||
i => i.service.id === currentService.service.id,
|
||||
);
|
||||
if (!item) return [];
|
||||
return item.employees;
|
||||
@@ -102,8 +103,8 @@ const ProductServicesTable: FC<Props> = ({
|
||||
{
|
||||
enableColumnActions: false,
|
||||
enableSorting: false,
|
||||
enableRowActions: true,
|
||||
enableBottomToolbar: true,
|
||||
enableRowActions: !authState.isDealsViewer,
|
||||
enableBottomToolbar: !authState.isDealsViewer,
|
||||
renderBottomToolbar: (
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
@@ -158,7 +159,7 @@ const ProductServicesTable: FC<Props> = ({
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onEmployeeClick(
|
||||
row.original
|
||||
row.original,
|
||||
)
|
||||
}
|
||||
variant={"default"}>
|
||||
|
||||
@@ -14,6 +14,9 @@ import { modals } from "@mantine/modals";
|
||||
import { ServiceType } from "../../../../../../shared/enums/ServiceType.ts";
|
||||
import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
|
||||
import { useDebouncedCallback } from "@mantine/hooks";
|
||||
import { isDealLocked } from "../../../../../../pages/CardsPage/utils/isDealPaid.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../../../redux/store.ts";
|
||||
|
||||
type Props = {
|
||||
product: CardProductSchema;
|
||||
@@ -47,12 +50,14 @@ const ProductView: FC<Props> = ({
|
||||
if (!onChange) return;
|
||||
onChange(item);
|
||||
}, 200);
|
||||
const isLocked = Boolean(cardState.card?.billRequest);
|
||||
const isLocked = isDealLocked(cardState.card);
|
||||
const onDeleteClick = () => {
|
||||
if (!onDelete) return;
|
||||
onDelete(product);
|
||||
};
|
||||
|
||||
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const onServiceDelete = (item: CardProductServiceSchema) => {
|
||||
if (!onChange) return;
|
||||
onChange({
|
||||
@@ -161,25 +166,29 @@ const ProductView: FC<Props> = ({
|
||||
isNumber(event) && onQuantityChange(event)
|
||||
}
|
||||
placeholder={"Введите количество товара"}
|
||||
readOnly={isDealsViewer}
|
||||
/>
|
||||
<Textarea
|
||||
mih={rem(140)}
|
||||
styles={{
|
||||
wrapper: { height: "90%" },
|
||||
input: { height: "90%" },
|
||||
}}
|
||||
my={rem(10)}
|
||||
disabled={isLocked}
|
||||
defaultValue={product.comment}
|
||||
onChange={event => {
|
||||
if (!onChange) return;
|
||||
debouncedOnChange({
|
||||
...product,
|
||||
comment: event.currentTarget.value,
|
||||
});
|
||||
}}
|
||||
placeholder={"Введите комментарий для товара"}
|
||||
/>
|
||||
{!(isDealsViewer && product.comment?.length === 0) && (
|
||||
<Textarea
|
||||
mih={rem(140)}
|
||||
styles={{
|
||||
wrapper: { height: "90%" },
|
||||
input: { height: "90%" },
|
||||
}}
|
||||
my={rem(10)}
|
||||
disabled={isLocked}
|
||||
defaultValue={product.comment}
|
||||
onChange={event => {
|
||||
if (!onChange) return;
|
||||
debouncedOnChange({
|
||||
...product,
|
||||
comment: event.currentTarget.value,
|
||||
});
|
||||
}}
|
||||
placeholder={isDealsViewer ? "" : "Введите комментарий для товара"}
|
||||
readOnly={isDealsViewer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles["services-container"]}>
|
||||
@@ -194,34 +203,36 @@ const ProductView: FC<Props> = ({
|
||||
onDelete={onServiceDelete}
|
||||
onChange={onServiceChange}
|
||||
/>
|
||||
<Flex
|
||||
mt={"auto"}
|
||||
ml={"auto"}
|
||||
gap={rem(10)}>
|
||||
<Tooltip
|
||||
onClick={onPrintBarcodeClick}
|
||||
label="Печать штрихкода">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconBarcode />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
onClick={onProductEditClick}
|
||||
label="Редактировать товар">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
onClick={onDeleteClick}
|
||||
label="Удалить товар">
|
||||
<ActionIcon
|
||||
disabled={isLocked}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
{!isDealsViewer && (
|
||||
<Flex
|
||||
mt={"auto"}
|
||||
ml={"auto"}
|
||||
gap={rem(10)}>
|
||||
<Tooltip
|
||||
onClick={onPrintBarcodeClick}
|
||||
label="Печать штрихкода">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconBarcode />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
onClick={onProductEditClick}
|
||||
label="Редактировать товар">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
onClick={onDeleteClick}
|
||||
label="Удалить товар">
|
||||
<ActionIcon
|
||||
disabled={isLocked}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { createContext, FC, useContext, useEffect, useState } from "react";
|
||||
import { useProjectsContext } from "../../contexts/ProjectsContext.tsx";
|
||||
import { MODULES } from "../modules.tsx";
|
||||
import { ModuleNames, MODULES } from "../modules.tsx";
|
||||
import { Module } from "../types.tsx";
|
||||
import { RootState } from "../../redux/store.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
type ModulesContextState = {
|
||||
modules: Module[];
|
||||
@@ -13,15 +15,24 @@ const ModulesContext = createContext<ModulesContextState | undefined>(
|
||||
|
||||
const useModulesContextState = () => {
|
||||
const { selectedProject } = useProjectsContext();
|
||||
|
||||
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
|
||||
const [modules, setModules] = useState<Module[]>([]);
|
||||
|
||||
const filterModules = (modulesToFilter: Module[]): Module[] => {
|
||||
if (isDealsViewer) {
|
||||
const modulesForDealsViewer: string[] = [ModuleNames.SERVICES_AND_PRODUCTS];
|
||||
return modulesToFilter.filter(module => modulesForDealsViewer.includes(module.info.key));
|
||||
}
|
||||
|
||||
return modulesToFilter;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const modules = selectedProject?.modules ?? [];
|
||||
const projectModules = modules.map(module => {
|
||||
return MODULES[module.key];
|
||||
}) ?? [];
|
||||
setModules(projectModules);
|
||||
setModules(filterModules(projectModules));
|
||||
}, [selectedProject?.id]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -22,42 +22,42 @@ const modules: ModulesType = {
|
||||
[ModuleNames.CLIENTS]: {
|
||||
info: {
|
||||
label: "Клиенты",
|
||||
key: "clients",
|
||||
key: ModuleNames.CLIENTS,
|
||||
icon: <IconUser />,
|
||||
}
|
||||
},
|
||||
[ModuleNames.SERVICES_AND_PRODUCTS]: {
|
||||
info: {
|
||||
label: "Товары и услуги",
|
||||
key: "servicesAndProducts",
|
||||
key: ModuleNames.SERVICES_AND_PRODUCTS,
|
||||
icon: <IconBox />,
|
||||
}
|
||||
},
|
||||
[ModuleNames.SHIPMENT]: {
|
||||
info: {
|
||||
label: "Отгрузка",
|
||||
key: "shipment",
|
||||
key: ModuleNames.SHIPMENT,
|
||||
icon: <IconCubeSend />,
|
||||
}
|
||||
},
|
||||
[ModuleNames.EMPLOYEES]: {
|
||||
info: {
|
||||
label: "Сотрудники",
|
||||
key: "employees",
|
||||
key: ModuleNames.EMPLOYEES,
|
||||
icon: <IconUsersGroup />,
|
||||
}
|
||||
},
|
||||
[ModuleNames.MANAGERS]: {
|
||||
info: {
|
||||
label: "Менеджер",
|
||||
key: "managers",
|
||||
key: ModuleNames.MANAGERS,
|
||||
icon: <IconUserCog />,
|
||||
}
|
||||
},
|
||||
[ModuleNames.CHAT]: {
|
||||
info: {
|
||||
label: "Чат",
|
||||
key: "chat",
|
||||
key: ModuleNames.CHAT,
|
||||
icon: <IconMessage />,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IconQrcode, IconSubtask,
|
||||
IconTopologyStar3,
|
||||
IconUser,
|
||||
IconBuildingWarehouse,
|
||||
} from "@tabler/icons-react";
|
||||
import UsersTab from "./tabs/Users/UsersTab.tsx";
|
||||
import { motion } from "framer-motion";
|
||||
@@ -22,6 +23,7 @@ import OrganizationalStructureTab from "./tabs/OrganizationalStructureTab/Organi
|
||||
import { ReactNode } from "react";
|
||||
import WorkShiftsPlanning from "./tabs/WorkShiftsPlanning/WorkShiftsPlanning.tsx";
|
||||
import Attributes from "./tabs/Attributes/Attributes.tsx";
|
||||
import WarehouseManagementTab from "./tabs/WarehouseManagement/WarehouseManagementTab.tsx";
|
||||
|
||||
const AdminPage = () => {
|
||||
const userRole = useSelector((state: RootState) => state.auth.role);
|
||||
@@ -96,6 +98,11 @@ const AdminPage = () => {
|
||||
Атрибуты карточек
|
||||
</Tabs.Tab>
|
||||
)}
|
||||
<Tabs.Tab
|
||||
value={"warehouseManagement"}
|
||||
leftSection={<IconBuildingWarehouse />}>
|
||||
Управление складом
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
{getTabPanel("users", <UsersTab />)}
|
||||
{getTabPanel("rolesAndPositions", <OrganizationalStructureTab />)}
|
||||
@@ -105,6 +112,7 @@ const AdminPage = () => {
|
||||
{getTabPanel("workShifts", <WorkShiftsTab />)}
|
||||
{getTabPanel("transactions", <TransactionsTab />)}
|
||||
{getTabPanel("attributes", <Attributes />)}
|
||||
{getTabPanel("warehouseManagement", <WarehouseManagementTab />)}
|
||||
</Tabs>
|
||||
</PageBlock>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { WarehouseManagementTabContextProvider } from "./placeType/contexts/WarehouseManagementTabContext.tsx";
|
||||
import { WmsPage, WmsSegmentedControl } from "./placeType/components/WmsSegmentedControl.tsx";
|
||||
import { useState } from "react";
|
||||
import PlaceTypesEditor from "./placeType/components/PlaceTypesEditor.tsx";
|
||||
import PlacesEditor from "./place/components/PlacesEditor.tsx";
|
||||
|
||||
const WarehouseManagementTab = () => {
|
||||
const [page, setPage] = useState(WmsPage.PLACE);
|
||||
|
||||
return (
|
||||
<WarehouseManagementTabContextProvider>
|
||||
<WmsSegmentedControl
|
||||
w={"100%"}
|
||||
value={page.toString()}
|
||||
onChange={event => {
|
||||
setPage(parseInt(event));
|
||||
}}
|
||||
/>
|
||||
{page === WmsPage.PLACE_TYPE ? (
|
||||
<PlaceTypesEditor />
|
||||
) : (
|
||||
<PlacesEditor />
|
||||
)}
|
||||
</WarehouseManagementTabContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarehouseManagementTab;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { DataTable } from "mantine-datatable";
|
||||
import { IconChevronRight, IconSpace } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import classes from "../../../OrganizationalStructureTab/components/DepartmentsTree/DepartmentsTree.module.css";
|
||||
import { useState } from "react";
|
||||
import { PlaceSchema } from "../../../../../../client";
|
||||
import PlaceActions from "./PlaceActions.tsx";
|
||||
|
||||
type Props = {
|
||||
place: PlaceSchema;
|
||||
}
|
||||
|
||||
const Place = ({ place }: Props) => {
|
||||
const [placeTypeIds, setPlaceTypeIds] = useState<number[]>([]);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
noHeader
|
||||
columns={[
|
||||
{
|
||||
accessor: "name",
|
||||
title: "Место",
|
||||
noWrap: true,
|
||||
render: ({ id, number, placeType }) => (
|
||||
<>
|
||||
<IconChevronRight
|
||||
className={clsx(classes.icon, classes.expandIcon, {
|
||||
[classes.expandIconRotated]: placeTypeIds?.includes(id),
|
||||
})}
|
||||
/>
|
||||
<IconSpace className={classes.icon} />
|
||||
<span>{placeType.name} {number}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "actions",
|
||||
title: "",
|
||||
width: "0%",
|
||||
render: (place) => (
|
||||
<PlaceActions place={place} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
records={place.children?.sort((a, b) => a.id - b.id)}
|
||||
rowExpansion={{
|
||||
allowMultiple: true,
|
||||
expanded: { recordIds: placeTypeIds, onRecordIdsChange: setPlaceTypeIds },
|
||||
content: ({ record }) => (
|
||||
<Place place={record} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Place;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { PlaceSchema } from "../../../../../../client";
|
||||
import { IconEdit, IconPlaylistAdd, IconQrcode, IconTrash } from "@tabler/icons-react";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useWarehouseManagementTabContext } from "../../placeType/contexts/WarehouseManagementTabContext.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
place: PlaceSchema;
|
||||
}
|
||||
|
||||
const PlaceActions = ({ place }: Props) => {
|
||||
const { placeCrud, onCreatePlace, onEditPlace, generateQrCode } = useWarehouseManagementTabContext();
|
||||
|
||||
const getAction = (
|
||||
label: string,
|
||||
func: () => void,
|
||||
icon: ReactNode,
|
||||
disabled: boolean = false,
|
||||
) => {
|
||||
return (
|
||||
<Tooltip label={label} key={label}>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
func();
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{icon}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const actions = [
|
||||
getAction(
|
||||
"Распечатать QR-код",
|
||||
() => generateQrCode(place),
|
||||
<IconQrcode />,
|
||||
),
|
||||
getAction(
|
||||
"Добавить",
|
||||
() => onCreatePlace(place),
|
||||
<IconPlaylistAdd />,
|
||||
place.placeType.childCount === 0,
|
||||
),
|
||||
getAction(
|
||||
"Редактировать",
|
||||
() => onEditPlace(place),
|
||||
<IconEdit />,
|
||||
),
|
||||
getAction(
|
||||
"Удалить",
|
||||
() => placeCrud.onDelete && placeCrud.onDelete(place),
|
||||
<IconTrash />,
|
||||
place.children?.length !== 0,
|
||||
),
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex gap={"md"} mx={"md"} direction={"row"}>
|
||||
{...actions}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceActions;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FC } from "react";
|
||||
import { FlatPlaceTypeSchema, PlaceTypeSchema } from "../../../../../../client";
|
||||
import ObjectSelect, { ObjectSelectProps } from "../../../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<FlatPlaceTypeSchema | PlaceTypeSchema | null>,
|
||||
"getValueFn" | "getLabelFn"
|
||||
>;
|
||||
|
||||
const PlaceTypeSelect: FC<Props> = (props) => {
|
||||
return (
|
||||
<ObjectSelect
|
||||
getLabelFn={(placeType: FlatPlaceTypeSchema) => placeType.name}
|
||||
getValueFn={(placeType: FlatPlaceTypeSchema) => placeType.id.toString()}
|
||||
clearable
|
||||
searchable
|
||||
{...props}
|
||||
onClear={() => props.onChange(null)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceTypeSelect;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Button, Group, Stack } from "@mantine/core";
|
||||
import { IconChevronRight, IconSpace } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import classes from "../../../OrganizationalStructureTab/components/DepartmentsTree/DepartmentsTree.module.css";
|
||||
import { DataTable } from "mantine-datatable";
|
||||
import { useState } from "react";
|
||||
import { useWarehouseManagementTabContext } from "../../placeType/contexts/WarehouseManagementTabContext.tsx";
|
||||
import PlaceActions from "./PlaceActions.tsx";
|
||||
import Place from "./Place.tsx";
|
||||
|
||||
const PlacesEditor = () => {
|
||||
const { places, onCreatePlace } = useWarehouseManagementTabContext();
|
||||
const [placeIds, setPlaceIds] = useState<number[]>([]);
|
||||
|
||||
return (
|
||||
<Stack w={"100%"} mt={"md"}>
|
||||
<Group>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreatePlace()}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</Group>
|
||||
<DataTable
|
||||
noHeader
|
||||
withTableBorder
|
||||
columns={[
|
||||
{
|
||||
accessor: "name",
|
||||
title: "Место",
|
||||
noWrap: true,
|
||||
render: ({ id, number, placeType }) => (
|
||||
<>
|
||||
<IconChevronRight
|
||||
className={clsx(classes.icon, classes.expandIcon, {
|
||||
[classes.expandIconRotated]: placeIds?.includes(id),
|
||||
})}
|
||||
/>
|
||||
<IconSpace className={classes.icon} />
|
||||
<span>{placeType.name} {number}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "actions",
|
||||
title: "",
|
||||
width: "0%",
|
||||
render: (place) => (
|
||||
<PlaceActions place={place} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
records={places.sort((a, b) => a.id - b.id)}
|
||||
rowExpansion={{
|
||||
allowMultiple: true,
|
||||
expanded: { recordIds: placeIds, onRecordIdsChange: setPlaceIds },
|
||||
content: ({ record }) => (
|
||||
<Place place={record} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlacesEditor;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useCRUD } from "../../../../../../hooks/useCRUD.tsx";
|
||||
import { BasePlaceSchema, EditPlaceSchema, PlaceSchema, WmsService } from "../../../../../../client";
|
||||
import { notifications } from "../../../../../../shared/lib/notifications.ts";
|
||||
|
||||
|
||||
export type PlaceCrud = {
|
||||
onCreate: (element: BasePlaceSchema) => void,
|
||||
onDelete: (element: PlaceSchema) => void,
|
||||
onChange: (element: EditPlaceSchema) => void
|
||||
}
|
||||
|
||||
type Props = {
|
||||
fetchPlaces: () => void;
|
||||
}
|
||||
|
||||
const usePlacesCrud = ({ fetchPlaces }: Props): PlaceCrud => {
|
||||
return useCRUD<EditPlaceSchema, BasePlaceSchema>({
|
||||
onChange: (place: EditPlaceSchema) => {
|
||||
WmsService.editPlace({
|
||||
requestBody: { place },
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
fetchPlaces();
|
||||
if (ok) return;
|
||||
notifications.error({ message });
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
},
|
||||
onDelete: (place: EditPlaceSchema) => {
|
||||
WmsService.deletePlace({
|
||||
placeId: place.id,
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
fetchPlaces();
|
||||
if (ok) return;
|
||||
notifications.error({ message });
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
},
|
||||
onCreate: (place: BasePlaceSchema) => {
|
||||
WmsService.createPlace({
|
||||
requestBody: {
|
||||
place,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
fetchPlaces();
|
||||
if (ok) return;
|
||||
notifications.error({ message });
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default usePlacesCrud;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { WmsService } from "../../../../../../client";
|
||||
import ObjectList from "../../../../../../hooks/objectList.tsx";
|
||||
|
||||
const usePlacesList = () =>
|
||||
ObjectList({
|
||||
queryFn: WmsService.getPlaces,
|
||||
getObjectsFn: response => response.places,
|
||||
queryKey: "getAllPlaces",
|
||||
});
|
||||
|
||||
export default usePlacesList;
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { EditPlaceSchema, FlatPlaceTypeSchema, PlaceSchema, PlaceTypeSchema } from "../../../../../../client";
|
||||
import { Button, NumberInput, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { PlaceCrud } from "../hooks/usePlacesCrud.tsx";
|
||||
import PlaceTypeSelect from "../components/PlaceTypeSelect.tsx";
|
||||
|
||||
type Props = {
|
||||
placeCrud: PlaceCrud;
|
||||
parent?: PlaceSchema;
|
||||
placeTypes?: FlatPlaceTypeSchema[];
|
||||
element?: EditPlaceSchema;
|
||||
}
|
||||
|
||||
type PlaceModalForm = {
|
||||
placeType: PlaceTypeSchema | null;
|
||||
placeNumber: number | null;
|
||||
}
|
||||
|
||||
const PlaceModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const { parent, placeCrud, placeTypes } = innerProps;
|
||||
|
||||
const closeModal = () => {
|
||||
context.closeContextModal(id);
|
||||
};
|
||||
|
||||
const initialValues: PlaceModalForm = {
|
||||
placeType: null,
|
||||
placeNumber: innerProps.element?.number ?? null,
|
||||
};
|
||||
const form = useForm<PlaceModalForm>({
|
||||
initialValues,
|
||||
validate: {
|
||||
placeType: placeType => !isEditing && !placeType && "Необходимо указать тип",
|
||||
placeNumber: placeNumber => isEditing && !placeNumber && "Необходимо ввести номер",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: PlaceModalForm) => {
|
||||
if (isEditing) {
|
||||
const place = innerProps.element!;
|
||||
placeCrud.onChange({
|
||||
id: place.id,
|
||||
placeTypeId: place.placeTypeId,
|
||||
number: values.placeNumber!,
|
||||
parentId: place.parentId,
|
||||
});
|
||||
} else {
|
||||
if (!values.placeType) return;
|
||||
placeCrud.onCreate({
|
||||
placeTypeId: values.placeType.id,
|
||||
parentId: parent?.id || null,
|
||||
});
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<Stack>
|
||||
{!isEditing && (
|
||||
<PlaceTypeSelect
|
||||
label={"Тип места на складе"}
|
||||
{...form.getInputProps("placeType")}
|
||||
data={placeTypes ?? []}
|
||||
defaultValue={(form.getValues().placeType ?? "") as FlatPlaceTypeSchema & string}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isEditing && (
|
||||
<NumberInput
|
||||
label={"Номер места на складе"}
|
||||
{...form.getInputProps("placeNumber")}
|
||||
defaultValue={form.getValues().placeNumber ?? ""}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button variant={"default"} type={"submit"}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceModal;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { PlaceSchema } from "../../../../../../client";
|
||||
import { Stack } from "@mantine/core";
|
||||
import InlineButton from "../../../../../../components/InlineButton/InlineButton.tsx";
|
||||
import { IconBoxAlignBottom, IconQrcode } from "@tabler/icons-react";
|
||||
|
||||
type Props = {
|
||||
place: PlaceSchema;
|
||||
}
|
||||
|
||||
|
||||
const SelectPlaceQrType = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const { place } = innerProps;
|
||||
|
||||
const closeModal = () => {
|
||||
context.closeContextModal(id);
|
||||
};
|
||||
|
||||
const generateQrCode = (isShort: boolean) => {
|
||||
const pdfWindow = window.open(
|
||||
`${import.meta.env.VITE_API_URL}/wms/place/pdf/${place.id}/${isShort}`,
|
||||
);
|
||||
if (!pdfWindow) return;
|
||||
pdfWindow.print();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<InlineButton onClick={() => generateQrCode(false)}>
|
||||
<IconQrcode />
|
||||
Целая наклейка
|
||||
</InlineButton>
|
||||
<InlineButton onClick={() => generateQrCode(true)}>
|
||||
<IconBoxAlignBottom />
|
||||
Половина наклейки
|
||||
</InlineButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectPlaceQrType;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { PlaceTypeSchema } from "../../../../../../client";
|
||||
import { DataTable } from "mantine-datatable";
|
||||
import { IconChevronRight, IconSpace } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import classes from "../../../OrganizationalStructureTab/components/DepartmentsTree/DepartmentsTree.module.css";
|
||||
import { useState } from "react";
|
||||
import PlaceTypeActions from "./PlaceTypeActions.tsx";
|
||||
|
||||
type Props = {
|
||||
placeType: PlaceTypeSchema;
|
||||
}
|
||||
|
||||
const PlaceType = ({ placeType }: Props) => {
|
||||
const [placeTypeIds, setPlaceTypeIds] = useState<number[]>([]);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
noHeader
|
||||
columns={[
|
||||
{
|
||||
accessor: "name",
|
||||
title: "Место",
|
||||
noWrap: true,
|
||||
render: ({ id, name }) => (
|
||||
<>
|
||||
<IconChevronRight
|
||||
className={clsx(classes.icon, classes.expandIcon, {
|
||||
[classes.expandIconRotated]: placeTypeIds?.includes(id),
|
||||
})}
|
||||
/>
|
||||
<IconSpace className={classes.icon} />
|
||||
<span>{name}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "actions",
|
||||
title: "",
|
||||
width: "0%",
|
||||
render: (placeType) => (
|
||||
<PlaceTypeActions placeType={placeType} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
records={placeType.children?.sort((a, b) => a.id - b.id)}
|
||||
rowExpansion={{
|
||||
allowMultiple: true,
|
||||
expanded: { recordIds: placeTypeIds, onRecordIdsChange: setPlaceTypeIds },
|
||||
content: ({ record }) => (
|
||||
<PlaceType placeType={record} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceType;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { PlaceTypeSchema } from "../../../../../../client";
|
||||
import { IconEdit, IconPlaylistAdd, IconTrash } from "@tabler/icons-react";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useWarehouseManagementTabContext } from "../contexts/WarehouseManagementTabContext.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
|
||||
type Props = {
|
||||
placeType: PlaceTypeSchema;
|
||||
}
|
||||
|
||||
const PlaceTypeActions = ({ placeType }: Props) => {
|
||||
const { placeTypeCrud } = useWarehouseManagementTabContext();
|
||||
|
||||
const getAction = (
|
||||
label: string,
|
||||
func: () => void,
|
||||
icon: ReactNode,
|
||||
disabled: boolean = false,
|
||||
) => {
|
||||
return (
|
||||
<Tooltip label={label} key={label}>
|
||||
<ActionIcon
|
||||
disabled={disabled}
|
||||
variant={"default"}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
func();
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const actions = [
|
||||
getAction(
|
||||
"Добавить",
|
||||
() => {
|
||||
modals.openContextModal({
|
||||
modal: "placeTypeModal",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
placeTypeCrud,
|
||||
parent: placeType,
|
||||
},
|
||||
});
|
||||
},
|
||||
<IconPlaylistAdd />,
|
||||
),
|
||||
getAction(
|
||||
"Редактировать",
|
||||
() => {
|
||||
modals.openContextModal({
|
||||
modal: "placeTypeModal",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
placeTypeCrud,
|
||||
element: placeType,
|
||||
},
|
||||
});
|
||||
},
|
||||
<IconEdit />,
|
||||
),
|
||||
getAction(
|
||||
"Удалить",
|
||||
() => placeTypeCrud.onDelete && placeTypeCrud.onDelete(placeType),
|
||||
<IconTrash />,
|
||||
placeType.placesCount !== 0 || placeType.children?.length !== 0,
|
||||
),
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex gap={"md"} mx={"md"} direction={"row"}>
|
||||
{...actions}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceTypeActions;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Button, Group, Stack } from "@mantine/core";
|
||||
import { useWarehouseManagementTabContext } from "../contexts/WarehouseManagementTabContext.tsx";
|
||||
import { IconChevronRight, IconSpace } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import classes from "../../../OrganizationalStructureTab/components/DepartmentsTree/DepartmentsTree.module.css";
|
||||
import { DataTable } from "mantine-datatable";
|
||||
import { useState } from "react";
|
||||
import PlaceType from "./PlaceType.tsx";
|
||||
import PlaceTypeActions from "./PlaceTypeActions.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
const PlaceTypesEditor = () => {
|
||||
const { placeTypes, placeTypeCrud } = useWarehouseManagementTabContext();
|
||||
const [placeTypeIds, setPlaceTypeIds] = useState<number[]>([]);
|
||||
|
||||
return (
|
||||
<Stack w={"100%"} mt={"md"}>
|
||||
<Group>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => {
|
||||
modals.openContextModal({
|
||||
modal: "placeTypeModal",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
placeTypeCrud,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</Group>
|
||||
<DataTable
|
||||
noHeader
|
||||
withTableBorder
|
||||
columns={[
|
||||
{
|
||||
accessor: "name",
|
||||
title: "Место",
|
||||
noWrap: true,
|
||||
render: ({ id, name }) => (
|
||||
<>
|
||||
<IconChevronRight
|
||||
className={clsx(classes.icon, classes.expandIcon, {
|
||||
[classes.expandIconRotated]: placeTypeIds?.includes(id),
|
||||
})}
|
||||
/>
|
||||
<IconSpace className={classes.icon} />
|
||||
<span>{name}</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "actions",
|
||||
title: "",
|
||||
width: "0%",
|
||||
render: (placeType) => (
|
||||
<PlaceTypeActions placeType={placeType} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
records={placeTypes.sort((a, b) => a.id - b.id)}
|
||||
rowExpansion={{
|
||||
allowMultiple: true,
|
||||
expanded: { recordIds: placeTypeIds, onRecordIdsChange: setPlaceTypeIds },
|
||||
content: ({ record }) => (
|
||||
<PlaceType placeType={record} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceTypesEditor;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SegmentedControl, SegmentedControlProps } from "@mantine/core";
|
||||
import { FC } from "react";
|
||||
|
||||
export enum WmsPage {
|
||||
PLACE,
|
||||
PLACE_TYPE,
|
||||
}
|
||||
|
||||
type Props = Omit<SegmentedControlProps, "data">;
|
||||
const data = [
|
||||
{
|
||||
label: "Места на складе",
|
||||
value: WmsPage.PLACE.toString(),
|
||||
},
|
||||
{
|
||||
label: "Типы мест на складе",
|
||||
value: WmsPage.PLACE_TYPE.toString(),
|
||||
},
|
||||
];
|
||||
|
||||
export const WmsSegmentedControl: FC<Props> = props => {
|
||||
return (
|
||||
<SegmentedControl
|
||||
data={data}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { createContext, FC, useContext } from "react";
|
||||
import { EditPlaceSchema, PlaceSchema, PlaceTypeSchema, WmsService } from "../../../../../../client";
|
||||
import usePlaceTypesList from "../hooks/usePlaceTypesList.tsx";
|
||||
import usePlaceTypesCrud, { PlaceTypeCrud } from "../hooks/usePlaceTypesCrud.tsx";
|
||||
import usePlacesList from "../../place/hooks/usePlacesList.tsx";
|
||||
import usePlacesCrud, { PlaceCrud } from "../../place/hooks/usePlacesCrud.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
type WarehouseManagementTabContextState = {
|
||||
refetchPlaceTypes: () => void;
|
||||
placeTypeCrud: PlaceTypeCrud;
|
||||
placeTypes: PlaceTypeSchema[];
|
||||
refetchPlaces: () => void;
|
||||
placeCrud: PlaceCrud;
|
||||
places: PlaceSchema[];
|
||||
onCreatePlace: (place?: PlaceSchema) => void;
|
||||
onEditPlace: (place?: EditPlaceSchema) => void;
|
||||
generateQrCode: (place: PlaceSchema) => void;
|
||||
};
|
||||
|
||||
const WarehouseManagementTabContext = createContext<WarehouseManagementTabContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const useWarehouseManagementTabContextState = () => {
|
||||
const { objects: placeTypes, refetch: refetchPlaceTypes } = usePlaceTypesList();
|
||||
const { objects: places, refetch: refetchPlaces } = usePlacesList();
|
||||
|
||||
const placeTypeCrud: PlaceTypeCrud = usePlaceTypesCrud({
|
||||
fetchPlaceTypes: refetchPlaceTypes,
|
||||
});
|
||||
|
||||
const placeCrud: PlaceCrud = usePlacesCrud({
|
||||
fetchPlaces: refetchPlaces,
|
||||
});
|
||||
|
||||
const onCreatePlace = (place?: PlaceSchema) => {
|
||||
WmsService.getFlatPlaceTypes({
|
||||
parentPlaceTypeId: place?.placeTypeId || -1,
|
||||
})
|
||||
.then(({ placeTypes }) => {
|
||||
if (placeTypes.length === 1) {
|
||||
placeCrud.onCreate({
|
||||
parentId: place?.id || null,
|
||||
placeTypeId: placeTypes[0].id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modals.openContextModal({
|
||||
modal: "placeModal",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
placeCrud,
|
||||
parent: place,
|
||||
placeTypes,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onEditPlace = (place?: EditPlaceSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "placeModal",
|
||||
withCloseButton: false,
|
||||
title: "Редактирование",
|
||||
innerProps: {
|
||||
placeCrud,
|
||||
element: place,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const generateQrCode = (place: PlaceSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "selectPlaceQrType",
|
||||
withCloseButton: false,
|
||||
centered: true,
|
||||
innerProps: {
|
||||
place,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
placeTypes,
|
||||
placeTypeCrud,
|
||||
refetchPlaceTypes,
|
||||
places,
|
||||
placeCrud,
|
||||
refetchPlaces,
|
||||
onCreatePlace,
|
||||
onEditPlace,
|
||||
generateQrCode,
|
||||
};
|
||||
};
|
||||
|
||||
type WarehouseManagementTabContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const WarehouseManagementTabContextProvider: FC<WarehouseManagementTabContextProviderProps> = ({ children }) => {
|
||||
const state = useWarehouseManagementTabContextState();
|
||||
return (
|
||||
<WarehouseManagementTabContext.Provider value={state}>
|
||||
{children}
|
||||
</WarehouseManagementTabContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useWarehouseManagementTabContext = () => {
|
||||
const context = useContext(WarehouseManagementTabContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useWarehouseManagementTabContext must be used within a WarehouseManagementTabContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { WmsService } from "../../../../../../client";
|
||||
import ObjectList from "../../../../../../hooks/objectList.tsx";
|
||||
|
||||
const useFlatPlaceTypesList = (parentPlaceTypeId: number = -1) =>
|
||||
ObjectList({
|
||||
queryFn: () => WmsService.getFlatPlaceTypes({ parentPlaceTypeId }),
|
||||
getObjectsFn: response => response.placeTypes,
|
||||
queryKey: "getFlatPlaceTypes",
|
||||
});
|
||||
|
||||
export default useFlatPlaceTypesList;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useCRUD } from "../../../../../../hooks/useCRUD.tsx";
|
||||
import { BasePlaceTypeSchema, FlatPlaceTypeSchema, WmsService } from "../../../../../../client";
|
||||
import { notifications } from "../../../../../../shared/lib/notifications.ts";
|
||||
|
||||
|
||||
export type PlaceTypeCrud = {
|
||||
onCreate: (element: BasePlaceTypeSchema) => void,
|
||||
onDelete: (element: FlatPlaceTypeSchema) => void,
|
||||
onChange: (element: FlatPlaceTypeSchema) => void
|
||||
}
|
||||
|
||||
type Props = {
|
||||
fetchPlaceTypes: () => void;
|
||||
}
|
||||
|
||||
const usePlaceTypesCrud = ({ fetchPlaceTypes }: Props): PlaceTypeCrud => {
|
||||
return useCRUD<FlatPlaceTypeSchema, BasePlaceTypeSchema>({
|
||||
onChange: (placeType: FlatPlaceTypeSchema) => {
|
||||
WmsService.editPlaceType({
|
||||
requestBody: { placeType },
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
fetchPlaceTypes();
|
||||
if (ok) return;
|
||||
notifications.error({ message });
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
},
|
||||
onDelete: (placeType: FlatPlaceTypeSchema) => {
|
||||
WmsService.deletePlaceType({
|
||||
placeTypeId: placeType.id,
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
fetchPlaceTypes();
|
||||
if (ok) return;
|
||||
notifications.error({ message });
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
},
|
||||
onCreate: (placeType: BasePlaceTypeSchema) => {
|
||||
WmsService.createPlaceType({
|
||||
requestBody: {
|
||||
placeType,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
fetchPlaceTypes();
|
||||
if (ok) return;
|
||||
notifications.error({ message });
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default usePlaceTypesCrud;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { WmsService } from "../../../../../../client";
|
||||
import ObjectList from "../../../../../../hooks/objectList.tsx";
|
||||
|
||||
const usePlaceTypesList = () =>
|
||||
ObjectList({
|
||||
queryFn: WmsService.getPlaceTypes,
|
||||
getObjectsFn: response => response.placeTypes,
|
||||
queryKey: "getAllPlaceTypes",
|
||||
});
|
||||
|
||||
export default usePlaceTypesList;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { BasePlaceTypeSchema, PlaceTypeSchema } from "../../../../../../client";
|
||||
import { PlaceTypeCrud } from "../hooks/usePlaceTypesCrud.tsx";
|
||||
import { Button, Stack, TextInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
|
||||
type Props = {
|
||||
placeTypeCrud: PlaceTypeCrud;
|
||||
element?: PlaceTypeSchema & BasePlaceTypeSchema;
|
||||
parent?: PlaceTypeSchema;
|
||||
}
|
||||
|
||||
type PlaceTypeModalForm = {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const PlaceTypeModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const { element, parent, placeTypeCrud } = innerProps;
|
||||
|
||||
const closeModal = () => {
|
||||
context.closeContextModal(id);
|
||||
};
|
||||
|
||||
const initialValues: PlaceTypeModalForm = {
|
||||
name: innerProps.element?.name ?? "",
|
||||
};
|
||||
const form = useForm<PlaceTypeModalForm>({
|
||||
initialValues,
|
||||
validate: {
|
||||
name: name => !name && "Необходимо указать название",
|
||||
},
|
||||
});
|
||||
|
||||
const isChanged = (): boolean => {
|
||||
return initialValues.name !== form.values.name;
|
||||
};
|
||||
|
||||
const onSubmit = (values: PlaceTypeModalForm) => {
|
||||
if (element) {
|
||||
if (isChanged()) {
|
||||
placeTypeCrud.onChange({
|
||||
name: values.name ?? "",
|
||||
id: element?.id,
|
||||
parentId: element?.parentId ?? null,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
placeTypeCrud.onCreate({
|
||||
name: values.name ?? "",
|
||||
parentId: parent?.id ?? null,
|
||||
});
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
|
||||
<Button variant={"default"} type={"submit"}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceTypeModal;
|
||||
@@ -7,7 +7,7 @@ import { processSelectedCells } from "../../../../../shared/lib/interpolateCells
|
||||
import { TimeTrackingData } from "../../../../../client";
|
||||
import dayjs from "dayjs";
|
||||
import { IMaskInput } from "react-imask";
|
||||
import { floatHoursToHoursAndMinutes } from "../../../../../types/utils.ts";
|
||||
import { dateToString, floatHoursToHoursAndMinutes } from "../../../../../types/utils.ts";
|
||||
|
||||
export type EmployeeData = {
|
||||
name: string;
|
||||
@@ -20,21 +20,17 @@ export type EmployeeData = {
|
||||
[key: string]: number | string;
|
||||
};
|
||||
type Props = {
|
||||
month: Date;
|
||||
data: EmployeeData[];
|
||||
onUpdate: (date: Date, userId: number, value: string) => void;
|
||||
selectedCells: string[];
|
||||
setSelectedCells: (cells: string[]) => void;
|
||||
selectedBoundaries: [Date | null, Date | null];
|
||||
range: dayjs.Dayjs[];
|
||||
};
|
||||
const useWorkTableColumns = ({
|
||||
month,
|
||||
onUpdate,
|
||||
data,
|
||||
selectedCells,
|
||||
setSelectedCells,
|
||||
selectedBoundaries,
|
||||
range,
|
||||
}: Props) => {
|
||||
const totalAmount = useMemo(() => {
|
||||
@@ -48,7 +44,7 @@ const useWorkTableColumns = ({
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
}, [data, month, selectedCells, selectedBoundaries]);
|
||||
}, [data, selectedCells, range]);
|
||||
const getBorderStyles = (cellId: string) => {
|
||||
if (selectedCells.length <= 1) return {};
|
||||
if (selectedCells[0] === cellId)
|
||||
@@ -86,8 +82,8 @@ const useWorkTableColumns = ({
|
||||
|
||||
...range.map(date => ({
|
||||
size: 100,
|
||||
accessorKey: date.date().toString(),
|
||||
header: date.date().toString(),
|
||||
accessorKey: dateToString(date.toDate()) ?? "",
|
||||
header: dateToString(date.toDate()) ?? "",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
Header: (
|
||||
@@ -130,7 +126,7 @@ const useWorkTableColumns = ({
|
||||
row.original.userId,
|
||||
value,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction={"column"}>
|
||||
@@ -178,7 +174,7 @@ const useWorkTableColumns = ({
|
||||
),
|
||||
},
|
||||
],
|
||||
[month, selectedCells, selectedBoundaries, totalAmount],
|
||||
[selectedCells, range, totalAmount],
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,49 +1,45 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { TimeTrackingRecord, TimeTrackingService } from "../../../../../client";
|
||||
import {
|
||||
dateWithoutTimezone,
|
||||
getDatesInMonth,
|
||||
} from "../../../../../shared/lib/date.ts";
|
||||
import { last } from "lodash";
|
||||
import { getDefaultEndDate } from "../../WorkShiftsPlanning/utils/utils.tsx";
|
||||
import { dateToString } from "../../../../../types/utils.ts";
|
||||
|
||||
const getDateBoundaries = (month: Date) => {
|
||||
return [
|
||||
getDatesInMonth(month)[0].toDate(),
|
||||
last(getDatesInMonth(month))?.toDate(),
|
||||
];
|
||||
};
|
||||
const useWorkTableState = () => {
|
||||
const [month, setMonth] = useState<Date>(
|
||||
new Date(new Date().getFullYear(), new Date().getMonth(), 1)
|
||||
);
|
||||
const [dateRange, setDateRange] = useState<
|
||||
[Date | null, Date | null]
|
||||
>([new Date(), getDefaultEndDate()]);
|
||||
const [trackingRecords, setTrackingRecords] = useState<
|
||||
TimeTrackingRecord[]
|
||||
>([]);
|
||||
const [dateBoundaries, setDateBoundaries] = useState(
|
||||
getDateBoundaries(month)
|
||||
);
|
||||
|
||||
const refetch = async () => {
|
||||
return TimeTrackingService.getTimeTrackingRecords({
|
||||
requestBody: {
|
||||
date: dateWithoutTimezone(month),
|
||||
userIds: [],
|
||||
},
|
||||
}).then(response => setTrackingRecords(response.records));
|
||||
const refetch = () => {
|
||||
const ending = "T00:00:00";
|
||||
const dateFrom = dateToString(dateRange[0]);
|
||||
const dateTo = dateToString(dateRange[1]);
|
||||
if (!(dateFrom && dateTo)) return;
|
||||
|
||||
TimeTrackingService
|
||||
.getTimeTrackingRecords({
|
||||
requestBody: {
|
||||
dateFrom: dateFrom + ending,
|
||||
dateTo: dateTo + ending,
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
console.log(response.records);
|
||||
setTrackingRecords(response.records);
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
refetch().then(_ => {
|
||||
setDateBoundaries(getDateBoundaries(month));
|
||||
});
|
||||
}, [month]);
|
||||
refetch();
|
||||
}, [dateRange]);
|
||||
|
||||
return {
|
||||
month,
|
||||
setMonth,
|
||||
dateRange,
|
||||
setDateRange,
|
||||
refetch,
|
||||
trackingRecords,
|
||||
dateBoundaries,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,56 +1,44 @@
|
||||
import { ActionIcon, Flex, MultiSelect, rem, Tooltip } from "@mantine/core";
|
||||
import { DatePickerInput, MonthPickerInput } from "@mantine/dates";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import useWorkTableState from "../hooks/useWorkTableState.tsx";
|
||||
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import useWorkTableColumns, { EmployeeData } from "../hooks/useWorkTableColumns.tsx";
|
||||
import { TimeTrackingRecord, TimeTrackingService, UserSchema } from "../../../../../client";
|
||||
import { dateWithoutTimezone, getDatesBetween, getDatesInMonth } from "../../../../../shared/lib/date.ts";
|
||||
import { getDatesBetween } from "../../../../../shared/lib/date.ts";
|
||||
import useUsersList from "../../../hooks/useUsersList.tsx";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { PaySchemeType } from "../../../../../shared/enums/PaySchemeType.ts";
|
||||
import { IconEyeOff } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { difference, omit } from "lodash";
|
||||
import { strTimeToFloatHours } from "../../../../../types/utils.ts";
|
||||
import { dateToString, strTimeToFloatHours } from "../../../../../types/utils.ts";
|
||||
import { useListState } from "@mantine/hooks";
|
||||
|
||||
const WorkTimeTable = () => {
|
||||
const [data, setData] = useState<EmployeeData[]>([]);
|
||||
const { dateBoundaries, month, setMonth, trackingRecords, refetch } =
|
||||
useWorkTableState();
|
||||
const { dateRange, setDateRange, trackingRecords, refetch } = useWorkTableState();
|
||||
const [shownUsers, shownUsersHandlers] = useListState<UserSchema>([]);
|
||||
const [selectedBoundaries, setSelectedBoundaries] = useState<
|
||||
[Date | null, Date | null]
|
||||
>([null, null]);
|
||||
|
||||
const users = useUsersList().objects.filter(
|
||||
user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY,
|
||||
);
|
||||
|
||||
const getRange = () => {
|
||||
const startDate = selectedBoundaries[0];
|
||||
const endDate = selectedBoundaries[1];
|
||||
if (startDate && endDate) {
|
||||
return getDatesBetween(startDate, endDate);
|
||||
} else {
|
||||
return getDatesInMonth(month);
|
||||
}
|
||||
if (!(dateRange.length === 2 && dateRange[0] && dateRange[1])) return [];
|
||||
const startDate = dateRange[0];
|
||||
const endDate = dateRange[1];
|
||||
return getDatesBetween(startDate, endDate);
|
||||
};
|
||||
const range = getRange();
|
||||
|
||||
const transformTrackingRecordsToData = (
|
||||
trackingRecords: TimeTrackingRecord[],
|
||||
): EmployeeData[] => {
|
||||
if (!month) return [];
|
||||
const rangeDays = range.map(r => r.date());
|
||||
if (!(dateRange.length === 2 && dateRange[0] && dateRange[1])) return [];
|
||||
const dateFrom = dateRange[0];
|
||||
const dateTo = dateRange[1];
|
||||
const dates = getDatesBetween(dateFrom, dateTo);
|
||||
|
||||
trackingRecords = trackingRecords.map(tr => ({
|
||||
...tr,
|
||||
data: tr.data.filter(d =>
|
||||
rangeDays.includes(new Date(d.date).getDate()),
|
||||
),
|
||||
}));
|
||||
const existingUserIds = trackingRecords.map(tr => tr.user.id);
|
||||
const firstResult = trackingRecords.map(record => ({
|
||||
name: `${record.user.firstName} ${record.user.secondName}`,
|
||||
@@ -59,14 +47,14 @@ const WorkTimeTable = () => {
|
||||
totalAmount: record.totalAmount,
|
||||
data: record.data,
|
||||
...Object.fromEntries(
|
||||
getDatesInMonth(month).reduce((acc, day) => {
|
||||
return acc.set(day.date().toString(), 0);
|
||||
dates.reduce((acc, day) => {
|
||||
return acc.set(dateToString(day.toDate()) ?? "", 0);
|
||||
}, new Map<string, number>()),
|
||||
),
|
||||
...Object.fromEntries(
|
||||
record.data.reduce((acc, recordData) => {
|
||||
return acc.set(
|
||||
new Date(recordData.date).getDate().toString(),
|
||||
recordData.date,
|
||||
recordData.hours,
|
||||
);
|
||||
}, new Map<string, number>()),
|
||||
@@ -80,7 +68,7 @@ const WorkTimeTable = () => {
|
||||
comment: user.comment,
|
||||
totalAmount: 0,
|
||||
...Object.fromEntries(
|
||||
getDatesInMonth(month).reduce((acc, day) => {
|
||||
dates.reduce((acc, day) => {
|
||||
return acc.set(day.date().toString(), 0);
|
||||
}, new Map<string, number>()),
|
||||
),
|
||||
@@ -93,13 +81,12 @@ const WorkTimeTable = () => {
|
||||
// @ts-expect-error
|
||||
.concat(restUsersResult)
|
||||
.filter(r => shownUserIds.includes(r.userId));
|
||||
const firstDate = selectedBoundaries[0];
|
||||
const lastDate = selectedBoundaries[1];
|
||||
if (firstDate && lastDate) {
|
||||
const allDays = getDatesInMonth(month).map(d =>
|
||||
|
||||
if (dateFrom && dateTo) {
|
||||
const allDays = dates.map(d =>
|
||||
d.date().toString(),
|
||||
);
|
||||
const allowedDays = getDatesBetween(firstDate, lastDate).map(d =>
|
||||
const allowedDays = getDatesBetween(dateFrom, dateTo).map(d =>
|
||||
d.date().toString(),
|
||||
);
|
||||
const omitDays = difference(allDays, allowedDays);
|
||||
@@ -117,10 +104,7 @@ const WorkTimeTable = () => {
|
||||
const user = users.find(user => user.id === userId);
|
||||
if (!user) return;
|
||||
|
||||
console.log(value);
|
||||
|
||||
const hours = strTimeToFloatHours(value);
|
||||
console.log(hours);
|
||||
if (hours === -1) return;
|
||||
|
||||
setData(prevState =>
|
||||
@@ -128,12 +112,13 @@ const WorkTimeTable = () => {
|
||||
if (record.userId !== userId) return record;
|
||||
record[date.getDate()] = value;
|
||||
return record;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const ending = "T00:00:00";
|
||||
TimeTrackingService.updateTimeTrackingRecord({
|
||||
requestBody: {
|
||||
date: dateWithoutTimezone(date),
|
||||
date: dateToString(date) + ending,
|
||||
hours,
|
||||
userId: user.id,
|
||||
},
|
||||
@@ -141,13 +126,11 @@ const WorkTimeTable = () => {
|
||||
if (!ok) {
|
||||
notifications.guess(ok, { message });
|
||||
}
|
||||
await refetch();
|
||||
refetch();
|
||||
});
|
||||
};
|
||||
|
||||
const columns = useWorkTableColumns({
|
||||
month,
|
||||
selectedBoundaries,
|
||||
data,
|
||||
onUpdate: optimisticUpdate,
|
||||
selectedCells: [],
|
||||
@@ -158,11 +141,7 @@ const WorkTimeTable = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setData(transformTrackingRecordsToData(trackingRecords));
|
||||
}, [trackingRecords, shownUsers, selectedBoundaries]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedBoundaries([null, null]);
|
||||
}, [month]);
|
||||
}, [trackingRecords, shownUsers, dateRange]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -193,55 +172,45 @@ const WorkTimeTable = () => {
|
||||
/>
|
||||
<Flex gap={rem(10)}>
|
||||
<DatePickerInput
|
||||
styles={{
|
||||
input: {
|
||||
textAlign: "center",
|
||||
},
|
||||
}}
|
||||
miw={rem(80)}
|
||||
valueFormat={"DD"}
|
||||
placeholder={"Выберите временной промежуток"}
|
||||
type={"range"}
|
||||
minDate={dateBoundaries[0]}
|
||||
maxDate={dateBoundaries[1]}
|
||||
value={selectedBoundaries}
|
||||
onChange={setSelectedBoundaries}
|
||||
placeholder={"Даты"}
|
||||
/>
|
||||
<MonthPickerInput
|
||||
allowDeselect={false}
|
||||
onChange={event => event && setMonth(event)}
|
||||
value={month}
|
||||
placeholder={"Выберите месяц"}
|
||||
value={dateRange}
|
||||
onChange={(value) => {
|
||||
setDateRange(value);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<BaseTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
restProps={
|
||||
{
|
||||
enableColumnActions: false,
|
||||
enableSorting: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Скрыть">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
shownUsersHandlers.filter(
|
||||
user => user.id !== row.original.userId,
|
||||
);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconEyeOff />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<EmployeeData>
|
||||
}
|
||||
/>
|
||||
{dateRange[1] !== null && (
|
||||
<BaseTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
restProps={
|
||||
{
|
||||
enableColumnActions: false,
|
||||
enableSorting: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Скрыть">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
shownUsersHandlers.filter(
|
||||
user => user.id !== row.original.userId,
|
||||
);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconEyeOff />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<EmployeeData>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { CardPageContextProvider, useCardPageContext } from "../../CardsPage/contexts/CardPageContext.tsx";
|
||||
import ProductAndServiceTab from "../../../modules/cardModules/cardEditorTabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
|
||||
import ProductAndServiceTab
|
||||
from "../../../modules/cardModules/cardEditorTabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
|
||||
import React, { FC, useEffect } from "react";
|
||||
import { CardService } from "../../../client";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../redux/store.ts";
|
||||
|
||||
export type Props = {
|
||||
cardId: number;
|
||||
};
|
||||
const CardPageContent: FC<Props> = ({ cardId }) => {
|
||||
const { setSelectedCard } = useCardPageContext();
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
useEffect(() => {
|
||||
CardService.getCardById({ cardId }).then(card => {
|
||||
setSelectedCard(card);
|
||||
});
|
||||
}, []);
|
||||
if (authState.isAuthorized) {
|
||||
CardService.getCardById({ cardId }).then(card => {
|
||||
setSelectedCard(card);
|
||||
});
|
||||
}
|
||||
}, [authState.isAuthorized]);
|
||||
|
||||
return <ProductAndServiceTab />;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { ActionIcon, Flex, rem, Text } from "@mantine/core";
|
||||
import { IconEdit, IconMenu2, IconMenuDeep, IconPlus } from "@tabler/icons-react";
|
||||
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { IconEdit, IconLogout, IconMenu2, IconMenuDeep, IconPlus } from "@tabler/icons-react";
|
||||
import { motion } from "framer-motion";
|
||||
import styles from "../../ui/CardsPage.module.css";
|
||||
import PageBlock from "../../../../components/PageBlock/PageBlock.tsx";
|
||||
import DisplayMode from "../../enums/DisplayMode.ts";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { CardsPageState } from "../../hooks/useCardsPageState.tsx";
|
||||
import { CardsPageState } from "../../hooks/useCardsTableForm.tsx";
|
||||
import React from "react";
|
||||
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";
|
||||
import { RootState, useAppDispatch } from "../../../../redux/store.ts";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
import classes from "../../../../components/Navbar/Navbar.module.css";
|
||||
import { logout } from "../../../../features/authSlice.ts";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
type Props = {
|
||||
displayMode: DisplayMode;
|
||||
@@ -28,7 +31,9 @@ const CardsPageHeader = ({
|
||||
}: Props) => {
|
||||
const { openProjectsEditor } = useProjectsEditorContext();
|
||||
const { selectedProject, setSelectedProject, projects, refetchProjects } = useProjectsContext();
|
||||
const userRole = useSelector((state: RootState) => state.auth.role);
|
||||
const { role: userRole, isDealsViewer } = useSelector((state: RootState) => state.auth);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const isAdmin = userRole === "admin";
|
||||
|
||||
const handleCreateClick = () => {
|
||||
@@ -42,6 +47,31 @@ const CardsPageHeader = ({
|
||||
});
|
||||
};
|
||||
|
||||
const onLogoutClick = () => {
|
||||
dispatch(logout());
|
||||
navigate({ to: "/login" });
|
||||
};
|
||||
|
||||
const getLogoutButton = () => {
|
||||
return (
|
||||
<Tooltip
|
||||
display={"flex"}
|
||||
label={"Выйти"}
|
||||
position="right"
|
||||
transitionProps={{ duration: 0 }}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onLogoutClick()}
|
||||
className={classes.link}>
|
||||
<IconLogout
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const getHeaderInputsBoard = () => {
|
||||
return (
|
||||
<div
|
||||
@@ -66,12 +96,16 @@ const CardsPageHeader = ({
|
||||
</ActionIcon>
|
||||
</>
|
||||
)}
|
||||
<ObjectSelect
|
||||
placeholder={"Выберите проект"}
|
||||
data={projects}
|
||||
value={selectedProject}
|
||||
onChange={setSelectedProject}
|
||||
/>
|
||||
{isDealsViewer ? (
|
||||
getLogoutButton()
|
||||
) : (
|
||||
<ObjectSelect
|
||||
placeholder={"Выберите проект"}
|
||||
data={projects}
|
||||
value={selectedProject}
|
||||
onChange={setSelectedProject}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { CardService, CardSummary } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import useCardsTableColumns from "./columns.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import { ActionIcon, Flex, Group, Pagination, Stack, Tooltip } from "@mantine/core";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { useCardPageContext } from "../../contexts/CardPageContext.tsx";
|
||||
import useCardSummaries from "../../hooks/useCardSummaries.tsx";
|
||||
import { CardsPageState } from "../../hooks/useCardsTableForm.tsx";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
|
||||
type RestProps = {
|
||||
viewOnly?: boolean;
|
||||
form: UseFormReturnType<CardsPageState>;
|
||||
};
|
||||
type Props = CRUDTableProps<CardSummary> & RestProps;
|
||||
type Props = Omit<CRUDTableProps<CardSummary>, "items"> & RestProps;
|
||||
|
||||
const CardsTable: FC<Props> = ({ form, onSelectionChange, viewOnly = false }) => {
|
||||
const [debouncedForm] = useDebouncedValue(form, 300);
|
||||
const prevDebouncedValues = useRef(debouncedForm?.values);
|
||||
|
||||
const CardsTable: FC<Props> = ({ items, onSelectionChange, viewOnly = false }) => {
|
||||
const columns = useCardsTableColumns();
|
||||
const { setSelectedCard } = useCardPageContext();
|
||||
const onEditClick = (cardSummary: CardSummary) => {
|
||||
@@ -22,35 +30,65 @@ const CardsTable: FC<Props> = ({ items, onSelectionChange, viewOnly = false }) =
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
summaries,
|
||||
fetchSummaries,
|
||||
totalPages,
|
||||
page,
|
||||
setPage,
|
||||
} = useCardSummaries({
|
||||
full: true,
|
||||
values: form?.values,
|
||||
withPagination: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (prevDebouncedValues.current !== debouncedForm.values) {
|
||||
prevDebouncedValues.current = debouncedForm.values;
|
||||
setPage(1);
|
||||
}
|
||||
fetchSummaries();
|
||||
}, [debouncedForm.values, page]);
|
||||
|
||||
return (
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
onSelectionChange={onSelectionChange}
|
||||
restProps={
|
||||
{
|
||||
enableSorting: true,
|
||||
enableColumnActions: false,
|
||||
enablePagination: !viewOnly,
|
||||
enableBottomToolbar: !viewOnly,
|
||||
paginationDisplayMode: "pages",
|
||||
enableRowActions: true,
|
||||
enableRowSelection: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
disabled={viewOnly}
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<CardSummary>
|
||||
}
|
||||
/>
|
||||
<Stack>
|
||||
<BaseTable
|
||||
data={summaries}
|
||||
columns={columns}
|
||||
onSelectionChange={onSelectionChange}
|
||||
restProps={
|
||||
{
|
||||
enableSorting: true,
|
||||
enableColumnActions: false,
|
||||
paginationDisplayMode: "pages",
|
||||
enableRowActions: true,
|
||||
enableRowSelection: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
disabled={viewOnly}
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<CardSummary>
|
||||
}
|
||||
/>
|
||||
{!viewOnly && (
|
||||
<Group justify={"flex-end"}>
|
||||
<Pagination
|
||||
withEdges
|
||||
total={totalPages}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CardSchema, CardService } from "../../../client";
|
||||
type CardPageContextState = {
|
||||
selectedCard?: CardSchema;
|
||||
setSelectedCard: (card: CardSchema | undefined) => void;
|
||||
refetchCards: () => Promise<void>;
|
||||
refetchCards: () => void;
|
||||
refetchCard: () => void;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ const CardPageContext = createContext<CardPageContextState | undefined>(
|
||||
);
|
||||
|
||||
type CardPageContextStateProps = {
|
||||
refetchCards: () => Promise<void>;
|
||||
refetchCards: () => void;
|
||||
defaultCardId?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,26 +11,26 @@ type DndContextState = {
|
||||
dragState: DragState,
|
||||
onDragStart: (start: DragStart) => void,
|
||||
onDragEnd: (result: DropResult) => Promise<void>,
|
||||
refetchSummaries: () => void;
|
||||
};
|
||||
|
||||
const DndContext = createContext<DndContextState | undefined>(undefined);
|
||||
|
||||
type DndContextProps = {
|
||||
summariesRaw: CardSummary[];
|
||||
summaries: CardSummary[];
|
||||
refetchSummaries: () => void;
|
||||
}
|
||||
|
||||
const useDndContextState = ({
|
||||
summariesRaw,
|
||||
summaries,
|
||||
refetchSummaries,
|
||||
}: DndContextProps) => {
|
||||
const [dragState, setDragState] = useState<DragState>(DragState.DRAG_ENDED);
|
||||
|
||||
const {
|
||||
summaries,
|
||||
onCardDragEnd,
|
||||
} = useCardsDnd({
|
||||
summariesRaw,
|
||||
summaries,
|
||||
refetchSummaries,
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ const useDndContextState = ({
|
||||
dragState,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
refetchSummaries,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user