Compare commits

...

29 Commits

Author SHA1 Message Date
27f27f4c38 feat: update CardAttributeField to handle date values without timezone 2025-09-18 16:24:39 +03:00
6e19d98f81 feat: update ProductSelect to use server-side search value for improved filtering 2025-09-07 00:02:29 +03:00
7a052f7422 feat: enhance product filtering in ProductSelect component 2025-09-05 04:42:08 +03:00
b08f921c1c add .dockerignore to exclude node_modules from Docker builds 2025-09-02 06:13:41 +03:00
d0da5155a1 add .dockerignore to exclude node_modules from Docker builds 2025-09-02 06:12:09 +03:00
bdbdd10231 feat: refactor ClientSelect to use ObjectSelect for improved client selection 2025-09-02 06:11:24 +03:00
9dde9041b1 add build script for Docker and update .gitignore to exclude PDF files 2025-09-02 05:49:02 +03:00
01260d0768 feat: add Dockerfile and Nginx configuration for deployment 2025-09-02 03:38:24 +03:00
15a085721e fix: client delete fix 2025-07-18 19:28:27 +04:00
655ae077ca feat: logout button in the deals viewer mode 2025-07-14 22:31:56 +04:00
50590fa41c feat: no project selecting for clients 2025-07-14 09:52:36 +04:00
c866231730 fix: data loading after login fixed 2025-07-04 21:12:01 +04:00
c3d135eba9 feat: deals viewer mode and links for viewers 2025-07-04 15:12:37 +04:00
82e2ef6db2 fix: client phone number setting fixed 2025-06-28 13:12:09 +04:00
4787151b69 fix: client empty phone number enabled 2025-06-28 12:54:28 +04:00
eea03bd70a fix: cards reorder fixed 2025-06-23 16:57:31 +04:00
f7d514df4b feat: clients search 2025-05-16 12:19:53 +04:00
b09479302b feat: warehouse place number editing 2025-05-13 15:54:33 +04:00
74665e123b fix: product quantity edit fix 2025-05-12 14:57:39 +04:00
4fee2da42c fix: removing from group fixed 2025-05-12 01:06:51 +04:00
cc39d13b1c fix: modal for place qr code cards, deal services editing 2025-05-12 00:42:49 +04:00
2a7a50886c fix: wms modals without titles 2025-05-11 22:17:46 +04:00
3b082d7836 fix: worktime table columns order fix 2025-05-09 16:24:24 +04:00
37136b83bc feat: worktime table with dates range 2025-05-09 16:19:29 +04:00
d03ba66ebb feat: split bills for one deal 2025-05-09 13:33:17 +04:00
e4f8e90ae6 feat: warehouse places accounting 2025-05-07 09:53:02 +04:00
d347c09199 feat: chats list on the left of the client chat 2025-04-16 13:58:02 +04:00
f28df5a074 Merge remote-tracking branch 'origin/master' 2025-04-15 11:00:27 +04:00
9ce112fd63 fix: cards fetch optimization 2025-04-15 10:59:56 +04:00
127 changed files with 4406 additions and 1147 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
node_modules

26
Dockerfile Normal file
View 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
View File

@@ -0,0 +1,2 @@
docker build -t git.denco.store/fakz9/fulfillment-frontend:latest .
docker push git.denco.store/fakz9/fulfillment-frontend:latest

1188
bun.lock Normal file

File diff suppressed because it is too large Load Diff

BIN
bun.lockb

Binary file not shown.

12
nginx.conf Normal file
View 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;
}
}

View File

@@ -32,6 +32,9 @@ export type { BaseEnumListSchema } from './models/BaseEnumListSchema';
export type { BaseEnumSchema } from './models/BaseEnumSchema'; export type { BaseEnumSchema } from './models/BaseEnumSchema';
export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema'; export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema';
export type { BaseMessageSchema } from './models/BaseMessageSchema'; 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 { BaseProjectSchema } from './models/BaseProjectSchema';
export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouseSchema'; export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouseSchema';
export type { BaseStatusSchema } from './models/BaseStatusSchema'; export type { BaseStatusSchema } from './models/BaseStatusSchema';
@@ -117,6 +120,7 @@ export type { CardUpdateServiceQuantityResponse } from './models/CardUpdateServi
export type { CardUpdateServiceRequest } from './models/CardUpdateServiceRequest'; export type { CardUpdateServiceRequest } from './models/CardUpdateServiceRequest';
export type { CardUpdateServiceResponse } from './models/CardUpdateServiceResponse'; export type { CardUpdateServiceResponse } from './models/CardUpdateServiceResponse';
export type { ChatSchema } from './models/ChatSchema'; export type { ChatSchema } from './models/ChatSchema';
export type { ChatsListItemSchema } from './models/ChatsListItemSchema';
export type { CityBreakdownFromExcelSchema } from './models/CityBreakdownFromExcelSchema'; export type { CityBreakdownFromExcelSchema } from './models/CityBreakdownFromExcelSchema';
export type { ClientCreateRequest } from './models/ClientCreateRequest'; export type { ClientCreateRequest } from './models/ClientCreateRequest';
export type { ClientCreateResponse } from './models/ClientCreateResponse'; export type { ClientCreateResponse } from './models/ClientCreateResponse';
@@ -151,6 +155,7 @@ export type { CreateDepartmentRequest } from './models/CreateDepartmentRequest';
export type { CreateDepartmentResponse } from './models/CreateDepartmentResponse'; export type { CreateDepartmentResponse } from './models/CreateDepartmentResponse';
export type { CreateDepartmentSectionRequest } from './models/CreateDepartmentSectionRequest'; export type { CreateDepartmentSectionRequest } from './models/CreateDepartmentSectionRequest';
export type { CreateDepartmentSectionResponse } from './models/CreateDepartmentSectionResponse'; export type { CreateDepartmentSectionResponse } from './models/CreateDepartmentSectionResponse';
export type { CreateGuestUrlResponse } from './models/CreateGuestUrlResponse';
export type { CreateMarketplaceRequest } from './models/CreateMarketplaceRequest'; export type { CreateMarketplaceRequest } from './models/CreateMarketplaceRequest';
export type { CreateMarketplaceResponse } from './models/CreateMarketplaceResponse'; export type { CreateMarketplaceResponse } from './models/CreateMarketplaceResponse';
export type { CreatePalletResponse } from './models/CreatePalletResponse'; export type { CreatePalletResponse } from './models/CreatePalletResponse';
@@ -158,6 +163,10 @@ export type { CreatePaymentRecordRequest } from './models/CreatePaymentRecordReq
export type { CreatePaymentRecordResponse } from './models/CreatePaymentRecordResponse'; export type { CreatePaymentRecordResponse } from './models/CreatePaymentRecordResponse';
export type { CreatePayRateRequest } from './models/CreatePayRateRequest'; export type { CreatePayRateRequest } from './models/CreatePayRateRequest';
export type { CreatePayRateResponse } from './models/CreatePayRateResponse'; 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 { CreatePositionRequest } from './models/CreatePositionRequest';
export type { CreatePositionResponse } from './models/CreatePositionResponse'; export type { CreatePositionResponse } from './models/CreatePositionResponse';
export type { CreateProjectRequest } from './models/CreateProjectRequest'; export type { CreateProjectRequest } from './models/CreateProjectRequest';
@@ -196,6 +205,8 @@ export type { DeletePaymentRecordRequest } from './models/DeletePaymentRecordReq
export type { DeletePaymentRecordResponse } from './models/DeletePaymentRecordResponse'; export type { DeletePaymentRecordResponse } from './models/DeletePaymentRecordResponse';
export type { DeletePayRateRequest } from './models/DeletePayRateRequest'; export type { DeletePayRateRequest } from './models/DeletePayRateRequest';
export type { DeletePayRateResponse } from './models/DeletePayRateResponse'; 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 { DeletePositionRequest } from './models/DeletePositionRequest';
export type { DeletePositionResponse } from './models/DeletePositionResponse'; export type { DeletePositionResponse } from './models/DeletePositionResponse';
export type { DeleteProjectResponse } from './models/DeleteProjectResponse'; export type { DeleteProjectResponse } from './models/DeleteProjectResponse';
@@ -220,10 +231,16 @@ export type { DepartmentSectionSchema } from './models/DepartmentSectionSchema';
export type { EditMessageRequest } from './models/EditMessageRequest'; export type { EditMessageRequest } from './models/EditMessageRequest';
export type { EditMessageResponse } from './models/EditMessageResponse'; export type { EditMessageResponse } from './models/EditMessageResponse';
export type { EditMessageSchema } from './models/EditMessageSchema'; 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 { FinishPauseByShiftIdResponse } from './models/FinishPauseByShiftIdResponse';
export type { FinishPauseByUserIdResponse } from './models/FinishPauseByUserIdResponse'; export type { FinishPauseByUserIdResponse } from './models/FinishPauseByUserIdResponse';
export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse'; export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse';
export type { FinishShiftResponse } from './models/FinishShiftResponse'; export type { FinishShiftResponse } from './models/FinishShiftResponse';
export type { FlatPlaceTypeSchema } from './models/FlatPlaceTypeSchema';
export type { FullProjectSchema } from './models/FullProjectSchema'; export type { FullProjectSchema } from './models/FullProjectSchema';
export type { GenerateInviteCodeRequest } from './models/GenerateInviteCodeRequest'; export type { GenerateInviteCodeRequest } from './models/GenerateInviteCodeRequest';
export type { GenerateInviteCodeResponse } from './models/GenerateInviteCodeResponse'; export type { GenerateInviteCodeResponse } from './models/GenerateInviteCodeResponse';
@@ -252,16 +269,21 @@ export type { GetBoardsResponse } from './models/GetBoardsResponse';
export type { GetCardBillById } from './models/GetCardBillById'; export type { GetCardBillById } from './models/GetCardBillById';
export type { GetCardProductsBarcodesPdfRequest } from './models/GetCardProductsBarcodesPdfRequest'; export type { GetCardProductsBarcodesPdfRequest } from './models/GetCardProductsBarcodesPdfRequest';
export type { GetCardProductsBarcodesPdfResponse } from './models/GetCardProductsBarcodesPdfResponse'; export type { GetCardProductsBarcodesPdfResponse } from './models/GetCardProductsBarcodesPdfResponse';
export type { GetCardSummariesRequest } from './models/GetCardSummariesRequest';
export type { GetChatRequest } from './models/GetChatRequest'; export type { GetChatRequest } from './models/GetChatRequest';
export type { GetChatResponse } from './models/GetChatResponse'; export type { GetChatResponse } from './models/GetChatResponse';
export type { GetChatsListResponse } from './models/GetChatsListResponse';
export type { GetClientMarketplacesRequest } from './models/GetClientMarketplacesRequest'; export type { GetClientMarketplacesRequest } from './models/GetClientMarketplacesRequest';
export type { GetClientMarketplacesResponse } from './models/GetClientMarketplacesResponse'; export type { GetClientMarketplacesResponse } from './models/GetClientMarketplacesResponse';
export type { GetDepartmentSectionsResponse } from './models/GetDepartmentSectionsResponse'; export type { GetDepartmentSectionsResponse } from './models/GetDepartmentSectionsResponse';
export type { GetDepartmentsResponse } from './models/GetDepartmentsResponse'; export type { GetDepartmentsResponse } from './models/GetDepartmentsResponse';
export type { GetFlatPlaceTypesResponse } from './models/GetFlatPlaceTypesResponse';
export type { GetManagersResponse } from './models/GetManagersResponse'; export type { GetManagersResponse } from './models/GetManagersResponse';
export type { GetMessagesRequest } from './models/GetMessagesRequest'; export type { GetMessagesRequest } from './models/GetMessagesRequest';
export type { GetMessagesResponse } from './models/GetMessagesResponse'; export type { GetMessagesResponse } from './models/GetMessagesResponse';
export type { GetPaymentRecordsResponse } from './models/GetPaymentRecordsResponse'; 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 { GetPlannedWorkShiftsResponse } from './models/GetPlannedWorkShiftsResponse';
export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfRequest'; export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfRequest';
export type { GetProductBarcodePdfResponse } from './models/GetProductBarcodePdfResponse'; export type { GetProductBarcodePdfResponse } from './models/GetProductBarcodePdfResponse';
@@ -309,6 +331,8 @@ export type { PayRateSchema } from './models/PayRateSchema';
export type { PayRateSchemaBase } from './models/PayRateSchemaBase'; export type { PayRateSchemaBase } from './models/PayRateSchemaBase';
export type { PayrollSchemeSchema } from './models/PayrollSchemeSchema'; export type { PayrollSchemeSchema } from './models/PayrollSchemeSchema';
export type { PermissionSchema } from './models/PermissionSchema'; 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 { PlannedWorkShiftSchema } from './models/PlannedWorkShiftSchema';
export type { PlanningTableRow } from './models/PlanningTableRow'; export type { PlanningTableRow } from './models/PlanningTableRow';
export type { PositionSchema } from './models/PositionSchema'; export type { PositionSchema } from './models/PositionSchema';
@@ -342,6 +366,7 @@ export type { ProjectGeneralInfoSchema } from './models/ProjectGeneralInfoSchema
export type { ProjectSchema } from './models/ProjectSchema'; export type { ProjectSchema } from './models/ProjectSchema';
export type { ReceiptBoxSchema } from './models/ReceiptBoxSchema'; export type { ReceiptBoxSchema } from './models/ReceiptBoxSchema';
export type { ReceiptPalletSchema } from './models/ReceiptPalletSchema'; export type { ReceiptPalletSchema } from './models/ReceiptPalletSchema';
export type { ReorderResponse } from './models/ReorderResponse';
export type { RepeatSendingMessageSchema } from './models/RepeatSendingMessageSchema'; export type { RepeatSendingMessageSchema } from './models/RepeatSendingMessageSchema';
export type { RepeatSendingTextMessageRequest } from './models/RepeatSendingTextMessageRequest'; export type { RepeatSendingTextMessageRequest } from './models/RepeatSendingTextMessageRequest';
export type { RepeatSendingTextMessageResponse } from './models/RepeatSendingTextMessageResponse'; export type { RepeatSendingTextMessageResponse } from './models/RepeatSendingTextMessageResponse';
@@ -480,5 +505,6 @@ export { TaskService } from './services/TaskService';
export { TimeTrackingService } from './services/TimeTrackingService'; export { TimeTrackingService } from './services/TimeTrackingService';
export { TransactionService } from './services/TransactionService'; export { TransactionService } from './services/TransactionService';
export { UserService } from './services/UserService'; export { UserService } from './services/UserService';
export { WmsService } from './services/WmsService';
export { WorkShiftsService } from './services/WorkShiftsService'; export { WorkShiftsService } from './services/WorkShiftsService';
export { WorkShiftsPlanningService } from './services/WorkShiftsPlanningService'; export { WorkShiftsPlanningService } from './services/WorkShiftsPlanningService';

View 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;
};

View 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);
};

View 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;
};

View File

@@ -7,6 +7,6 @@ export type CardGroupSchema = {
id: number; id: number;
name?: (string | null); name?: (string | null);
lexorank: string; lexorank: string;
billRequest?: (GroupBillRequestSchema | null); billRequests?: Array<GroupBillRequestSchema>;
}; };

View File

@@ -35,7 +35,7 @@ export type CardSchema = {
clientId: (number | null); clientId: (number | null);
client: (ClientSchema | null); client: (ClientSchema | null);
shippingWarehouse?: (ShippingWarehouseSchema | string | null); shippingWarehouse?: (ShippingWarehouseSchema | string | null);
billRequest?: (CardBillRequestSchema | null); billRequests?: Array<CardBillRequestSchema>;
group?: (CardGroupSchema | null); group?: (CardGroupSchema | null);
manager?: (UserSchema | null); manager?: (UserSchema | null);
pallets?: Array<PalletSchema>; pallets?: Array<PalletSchema>;

View File

@@ -24,7 +24,7 @@ export type CardSummary = {
attributes: Array<CardAttributeSchema>; attributes: Array<CardAttributeSchema>;
shipmentWarehouseId: (number | null); shipmentWarehouseId: (number | null);
shipmentWarehouseName: (string | null); shipmentWarehouseName: (string | null);
billRequest?: (CardBillRequestSchema | null); billRequests: Array<CardBillRequestSchema>;
group?: (CardGroupSchema | null); group?: (CardGroupSchema | null);
}; };

View File

@@ -3,7 +3,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { CardSummary } from './CardSummary'; import type { CardSummary } from './CardSummary';
import type { PaginationInfoSchema } from './PaginationInfoSchema';
export type CardSummaryResponse = { export type CardSummaryResponse = {
summaries: Array<CardSummary>; summaries: Array<CardSummary>;
paginationInfo: PaginationInfoSchema;
}; };

View 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;
};

View File

@@ -15,6 +15,7 @@ export type ClientDetailedSchema = {
comment?: (string | null); comment?: (string | null);
details?: (ClientDetailsSchema | null); details?: (ClientDetailsSchema | null);
chat?: (ChatSchema | null); chat?: (ChatSchema | null);
isDeleted?: (boolean | null);
pallets?: Array<ResidualPalletSchema>; pallets?: Array<ResidualPalletSchema>;
boxes?: Array<ResidualBoxSchema>; boxes?: Array<ResidualBoxSchema>;
}; };

View File

@@ -13,5 +13,6 @@ export type ClientSchema = {
comment?: (string | null); comment?: (string | null);
details?: (ClientDetailsSchema | null); details?: (ClientDetailsSchema | null);
chat?: (ChatSchema | null); chat?: (ChatSchema | null);
isDeleted?: (boolean | null);
}; };

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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);
};

View 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>;
};

View 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>;
};

View 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>;
};

View 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>;
};

View File

@@ -3,7 +3,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetTimeTrackingRecordsRequest = { export type GetTimeTrackingRecordsRequest = {
date: string; dateFrom: string;
userIds: Array<number>; dateTo: string;
}; };

View 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;
};

View 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;
};

View 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;
};

View File

@@ -56,11 +56,13 @@ import type { CreateCardsFromExcelResponse } from '../models/CreateCardsFromExce
import type { GetAvailableEmployeesToAssignResponse } from '../models/GetAvailableEmployeesToAssignResponse'; import type { GetAvailableEmployeesToAssignResponse } from '../models/GetAvailableEmployeesToAssignResponse';
import type { GetCardProductsBarcodesPdfRequest } from '../models/GetCardProductsBarcodesPdfRequest'; import type { GetCardProductsBarcodesPdfRequest } from '../models/GetCardProductsBarcodesPdfRequest';
import type { GetCardProductsBarcodesPdfResponse } from '../models/GetCardProductsBarcodesPdfResponse'; import type { GetCardProductsBarcodesPdfResponse } from '../models/GetCardProductsBarcodesPdfResponse';
import type { GetCardSummariesRequest } from '../models/GetCardSummariesRequest';
import type { ManageEmployeeRequest } from '../models/ManageEmployeeRequest'; import type { ManageEmployeeRequest } from '../models/ManageEmployeeRequest';
import type { ManageEmployeeResponse } from '../models/ManageEmployeeResponse'; import type { ManageEmployeeResponse } from '../models/ManageEmployeeResponse';
import type { ParseCardsExcelResponse } from '../models/ParseCardsExcelResponse'; import type { ParseCardsExcelResponse } from '../models/ParseCardsExcelResponse';
import type { ProductsAndServicesGeneralInfoRequest } from '../models/ProductsAndServicesGeneralInfoRequest'; import type { ProductsAndServicesGeneralInfoRequest } from '../models/ProductsAndServicesGeneralInfoRequest';
import type { ProductsAndServicesGeneralInfoResponse } from '../models/ProductsAndServicesGeneralInfoResponse'; import type { ProductsAndServicesGeneralInfoResponse } from '../models/ProductsAndServicesGeneralInfoResponse';
import type { ReorderResponse } from '../models/ReorderResponse';
import type { UpdateCardClientRequest } from '../models/UpdateCardClientRequest'; import type { UpdateCardClientRequest } from '../models/UpdateCardClientRequest';
import type { UpdateCardClientResponse } from '../models/UpdateCardClientResponse'; import type { UpdateCardClientResponse } from '../models/UpdateCardClientResponse';
import type { UpdateCardManagerRequest } from '../models/UpdateCardManagerRequest'; import type { UpdateCardManagerRequest } from '../models/UpdateCardManagerRequest';
@@ -155,16 +157,23 @@ export class CardService {
* @throws ApiError * @throws ApiError
*/ */
public static getCardSummaries({ public static getCardSummaries({
full, requestBody,
page,
itemsPerPage,
}: { }: {
full: (boolean | null), requestBody: GetCardSummariesRequest,
page?: (number | null),
itemsPerPage?: (number | null),
}): CancelablePromise<CardSummaryResponse> { }): CancelablePromise<CardSummaryResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: 'POST',
url: '/card/summaries', url: '/card/summaries',
query: { query: {
'full': full, 'page': page,
'items_per_page': itemsPerPage,
}, },
body: requestBody,
mediaType: 'application/json',
errors: { errors: {
422: `Validation Error`, 422: `Validation Error`,
}, },
@@ -172,14 +181,14 @@ export class CardService {
} }
/** /**
* Reorder * Reorder
* @returns CardSummaryResponse Successful Response * @returns ReorderResponse Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static reorderCardSummaries({ public static reorderCardSummaries({
requestBody, requestBody,
}: { }: {
requestBody: CardSummaryReorderRequest, requestBody: CardSummaryReorderRequest,
}): CancelablePromise<CardSummaryResponse> { }): CancelablePromise<ReorderResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: 'POST',
url: '/card/summaries/reorder', url: '/card/summaries/reorder',

View File

@@ -10,6 +10,7 @@ import type { EditMessageRequest } from '../models/EditMessageRequest';
import type { EditMessageResponse } from '../models/EditMessageResponse'; import type { EditMessageResponse } from '../models/EditMessageResponse';
import type { GetChatRequest } from '../models/GetChatRequest'; import type { GetChatRequest } from '../models/GetChatRequest';
import type { GetChatResponse } from '../models/GetChatResponse'; import type { GetChatResponse } from '../models/GetChatResponse';
import type { GetChatsListResponse } from '../models/GetChatsListResponse';
import type { GetMessagesRequest } from '../models/GetMessagesRequest'; import type { GetMessagesRequest } from '../models/GetMessagesRequest';
import type { GetMessagesResponse } from '../models/GetMessagesResponse'; import type { GetMessagesResponse } from '../models/GetMessagesResponse';
import type { LoadMessagesResponse } from '../models/LoadMessagesResponse'; 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 * Create Chat
* @returns CreateChatResponse Successful Response * @returns CreateChatResponse Successful Response

View File

@@ -11,6 +11,7 @@ import type { ClientGetResponse } from '../models/ClientGetResponse';
import type { ClientUpdateDetailsRequest } from '../models/ClientUpdateDetailsRequest'; import type { ClientUpdateDetailsRequest } from '../models/ClientUpdateDetailsRequest';
import type { ClientUpdateRequest } from '../models/ClientUpdateRequest'; import type { ClientUpdateRequest } from '../models/ClientUpdateRequest';
import type { ClientUpdateResponse } from '../models/ClientUpdateResponse'; import type { ClientUpdateResponse } from '../models/ClientUpdateResponse';
import type { CreateGuestUrlResponse } from '../models/CreateGuestUrlResponse';
import type { CancelablePromise } from '../core/CancelablePromise'; import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI'; import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request'; 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`,
},
});
}
} }

View 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`,
},
});
}
}

View File

@@ -8,9 +8,10 @@ import { CardGeneralFormType } from "../../pages/CardsPage/drawers/CardEditDrawe
type Props = { type Props = {
project: ProjectSchema; project: ProjectSchema;
form: UseFormReturnType<CardGeneralFormType>; form: UseFormReturnType<CardGeneralFormType>;
readOnly: boolean;
} }
const CardAttributeFields = ({ project, form }: Props) => { const CardAttributeFields = ({ project, form, readOnly }: Props) => {
const attributes: AttributeSchema[] = []; const attributes: AttributeSchema[] = [];
project.attributes.forEach(attribute => { project.attributes.forEach(attribute => {
@@ -27,6 +28,7 @@ const CardAttributeFields = ({ project, form }: Props) => {
key={attribute.id} key={attribute.id}
attribute={attribute} attribute={attribute}
form={form} form={form}
readOnly={readOnly}
/> />
); );
}); });

View File

@@ -4,13 +4,15 @@ import { UseFormReturnType } from "@mantine/form";
import { DatePickerInput, DateTimePicker } from "@mantine/dates"; import { DatePickerInput, DateTimePicker } from "@mantine/dates";
import { CardGeneralFormType } from "../../../pages/CardsPage/drawers/CardEditDrawer/tabs/GeneralTab/GeneralTab.tsx"; import { CardGeneralFormType } from "../../../pages/CardsPage/drawers/CardEditDrawer/tabs/GeneralTab/GeneralTab.tsx";
import { IconInfoCircle } from "@tabler/icons-react"; import { IconInfoCircle } from "@tabler/icons-react";
import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
type Props = { type Props = {
attribute: AttributeSchema; attribute: AttributeSchema;
form: UseFormReturnType<CardGeneralFormType>; form: UseFormReturnType<CardGeneralFormType>;
readOnly: boolean;
} }
const CardAttributeField = ({ attribute, form }: Props) => { const CardAttributeField = ({ attribute, form, readOnly }: Props) => {
const type = attribute.type.type; const type = attribute.type.type;
const getDateValue = (): Date | null => { const getDateValue = (): Date | null => {
@@ -35,6 +37,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
<Checkbox <Checkbox
label={label} label={label}
{...form.getInputProps(attribute.name, { type: "checkbox" })} {...form.getInputProps(attribute.name, { type: "checkbox" })}
readOnly={readOnly}
/> />
); );
} }
@@ -43,10 +46,20 @@ const CardAttributeField = ({ attribute, form }: Props) => {
<DatePickerInput <DatePickerInput
label={label} label={label}
{...form.getInputProps(attribute.name)} {...form.getInputProps(attribute.name)}
onChange={(value) => {
if (!value) {
form.getInputProps(attribute.name).onChange(null);
return
}
form.getInputProps(attribute.name).onChange(
dateWithoutTimezone(value),
);
}}
value={getDateValue()} value={getDateValue()}
clearable clearable
locale={"ru-RU"} locale={"ru-RU"}
valueFormat="DD.MM.YYYY" valueFormat="DD.MM.YYYY"
readOnly={readOnly}
/> />
); );
} }
@@ -55,10 +68,20 @@ const CardAttributeField = ({ attribute, form }: Props) => {
<DateTimePicker <DateTimePicker
label={label} label={label}
{...form.getInputProps(attribute.name)} {...form.getInputProps(attribute.name)}
onChange={(value) => {
if (!value) {
form.getInputProps(attribute.name).onChange(null);
return
}
form.getInputProps(attribute.name).onChange(
dateWithoutTimezone(value),
);
}}
value={getDateValue()} value={getDateValue()}
clearable clearable
locale={"ru-RU"} locale={"ru-RU"}
valueFormat="DD.MM.YYYY HH:mm" valueFormat="DD.MM.YYYY HH:mm"
readOnly={readOnly}
/> />
); );
} }
@@ -68,6 +91,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
label={label} label={label}
{...form.getInputProps(attribute.name)} {...form.getInputProps(attribute.name)}
value={form.getInputProps(attribute.name).value ?? ""} value={form.getInputProps(attribute.name).value ?? ""}
readOnly={readOnly}
/> />
); );
} }
@@ -77,6 +101,7 @@ const CardAttributeField = ({ attribute, form }: Props) => {
allowDecimal={type === "float"} allowDecimal={type === "float"}
label={label} label={label}
{...form.getInputProps(attribute.name)} {...form.getInputProps(attribute.name)}
readOnly={readOnly}
/> />
); );
} }

View File

@@ -7,7 +7,12 @@ import MessageInput from "./components/MessageInput/MessageInput.tsx";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { Stack } from "@mantine/core"; import { Stack } from "@mantine/core";
const Chat = () => { type Props = {
height?: number | string;
padding?: number | string;
}
const Chat = ({ height = "96vh", padding = 0 }: Props) => {
const { const {
messages, messages,
lastMessage, lastMessage,
@@ -45,7 +50,7 @@ const Chat = () => {
); );
} }
return ( return (
<Stack mb={"xs"} mr={"xs"}> <Stack mt={"xs"} mr={"xs"} ml={padding}>
{dateComponent} {dateComponent}
<Message <Message
key={`${sessionData.id}${index}`} key={`${sessionData.id}${index}`}
@@ -59,14 +64,14 @@ const Chat = () => {
if (messages.length === 0) { if (messages.length === 0) {
return ( return (
<Stack h={"96vh"} justify={"flex-end"}> <Stack h={height} justify={"flex-end"}>
<MessageInput /> <MessageInput inputMargin={padding} />
</Stack> </Stack>
); );
} }
return ( return (
<Stack h={"96vh"}> <Stack h={height}>
<Virtuoso <Virtuoso
data={messages} data={messages}
followOutput={onFollowOutputHandler} followOutput={onFollowOutputHandler}
@@ -78,7 +83,7 @@ const Chat = () => {
increaseViewportBy={200} increaseViewportBy={200}
alignToBottom alignToBottom
/> />
<MessageInput /> <MessageInput inputMargin={padding} />
</Stack> </Stack>
); );
}; };

View File

@@ -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);
}
}

View 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;

View File

@@ -6,7 +6,11 @@ import SelectedFile from "../SelectedFile/SelectedFile.tsx";
import React, { useRef } from "react"; import React, { useRef } from "react";
const MessageInput = () => { type Props = {
inputMargin?: number | string;
}
const MessageInput = ({ inputMargin }: Props) => {
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const { const {
@@ -26,7 +30,7 @@ const MessageInput = () => {
} }
}; };
const getFiles = files.map(file => ( const filesList = files.map(file => (
<SelectedFile key={file.name} file={file} /> <SelectedFile key={file.name} file={file} />
)); ));
@@ -34,8 +38,12 @@ const MessageInput = () => {
<form ref={formRef} onSubmit={form.onSubmit(values => submitMessage(values))}> <form ref={formRef} onSubmit={form.onSubmit(values => submitMessage(values))}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Divider /> <Divider />
{getFiles} {filesList.length > 0 && (
<Group wrap={"nowrap"} align={"flex-end"}> <Stack mx={inputMargin}>
{filesList}
</Stack>
)}
<Group wrap={"nowrap"} align={"flex-end"} mx={inputMargin}>
{chat?.tgGroup?.tgInviteLink && ( {chat?.tgGroup?.tgInviteLink && (
<ActionIconCopy <ActionIconCopy
onCopiedLabel={"Ссылка на чат скопирована в буфер обмена"} onCopiedLabel={"Ссылка на чат скопирована в буфер обмена"}

View File

@@ -8,6 +8,8 @@ import useStatus from "../../Statuses/Status/hooks/useStatus.tsx";
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx"; import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
import DragState from "../../../../pages/CardsPage/enums/DragState.ts"; import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx"; import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
type Props = { type Props = {
@@ -22,6 +24,7 @@ const Board = ({ board }: Props) => {
onDeleteBoardClick, onDeleteBoardClick,
} = useBoardsContext(); } = useBoardsContext();
const authState = useSelector((state: RootState) => state.auth);
const { dragState } = useDndContext(); const { dragState } = useDndContext();
const { showContextMenu } = useContextMenu(); const { showContextMenu } = useContextMenu();
@@ -65,6 +68,7 @@ const Board = ({ board }: Props) => {
<Draggable <Draggable
draggableId={"board-" + board.id.toString()} draggableId={"board-" + board.id.toString()}
index={board.ordinalNumber} index={board.ordinalNumber}
isDragDisabled={authState.isGuest}
> >
{(provided) => ( {(provided) => (
<div <div

View File

@@ -8,6 +8,8 @@ import PrefillCardsWithExcelDrawer
from "../../../../pages/CardsPage/drawers/PrefillCardWithExcelDrawer/PrefillCardsWithExcelDrawer.tsx"; from "../../../../pages/CardsPage/drawers/PrefillCardWithExcelDrawer/PrefillCardsWithExcelDrawer.tsx";
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx"; import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx"; import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
const Boards = () => { const Boards = () => {
@@ -16,6 +18,7 @@ const Boards = () => {
selectedBoard, selectedBoard,
onCreateBoardClick, onCreateBoardClick,
} = useBoardsContext(); } = useBoardsContext();
const authState = useSelector((state: RootState) => state.auth);
const { const {
onDragEnd, onDragEnd,
@@ -37,14 +40,16 @@ const Boards = () => {
board={board} board={board}
/> />
))} ))}
<Center {!authState.isGuest && (
px={"md"} <Center
py={"xs"} px={"md"}
style={{ cursor: "pointer", borderBottom: "solid gray 1px" }} py={"xs"}
onClick={onCreateBoardClick} style={{ cursor: "pointer", borderBottom: "solid gray 1px" }}
> onClick={onCreateBoardClick}
<IconPlus /> >
</Center> <IconPlus />
</Center>
)}
<Box w={"100%"} style={{ borderBottom: "solid gray 1px" }}></Box> <Box w={"100%"} style={{ borderBottom: "solid gray 1px" }}></Box>
</Group> </Group>
); );

View File

@@ -56,7 +56,11 @@ const CardAttributesInSummaryItem = ({ cardSummary }: Props) => {
.map(cardAttr => { .map(cardAttr => {
const isHighlight = isHighlightNeeded(cardAttr); const isHighlight = isHighlightNeeded(cardAttr);
return ( 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)} {cardAttr.attribute.label}: {getAttrValueValue(cardAttr)}
</Text> </Text>
); );

View File

@@ -9,6 +9,8 @@ import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts"; import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
import CardTags from "../CardTags/CardTags.tsx"; import CardTags from "../CardTags/CardTags.tsx";
import { ModuleNames } from "../../../../modules/modules.tsx"; import { ModuleNames } from "../../../../modules/modules.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
type Props = { type Props = {
cards: CardSummary[]; cards: CardSummary[];
@@ -21,6 +23,7 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
const [debouncedName] = useDebouncedValue(name, 200); const [debouncedName] = useDebouncedValue(name, 200);
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject); 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 totalPrice = useMemo(() => cards.reduce((acc, card) => acc + card.totalPrice, 0), [cards]);
const totalProducts = useMemo(() => cards.reduce((acc, card) => acc + card.totalProducts, 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> </Flex>
<CardTags groupId={group.id} tags={cards[0].tags}/> {!authState.isGuest && <CardTags groupId={group.id} tags={cards[0].tags}/>}
{isServicesAndProductsIncluded && ( {isServicesAndProductsIncluded && (
<Flex <Flex
p={rem(10)} p={rem(10)}

View File

@@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, useEffect, useState } from "react";
import { CardService, CardSummary } from "../../../../client"; import { CardService, CardSummary } from "../../../../client";
import styles from "./CardSummaryItem.module.css"; import styles from "./CardSummaryItem.module.css";
@@ -14,6 +14,9 @@ import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import CardTags from "../CardTags/CardTags.tsx"; import CardTags from "../CardTags/CardTags.tsx";
import CardAttributesInSummaryItem from "../CardAttributesInSummaryItem/CardAttributesInSummaryItem.tsx"; import CardAttributesInSummaryItem from "../CardAttributesInSummaryItem/CardAttributesInSummaryItem.tsx";
import { ModuleNames } from "../../../../modules/modules.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 = { type Props = {
cardSummary: CardSummary; cardSummary: CardSummary;
@@ -25,6 +28,8 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const { setSelectedCard } = useCardPageContext(); const { setSelectedCard } = useCardPageContext();
const { onDelete, onComplete, onDeleteFromGroup } = useCardSummaryState(); 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 isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
const isClientIncluded = isModuleInProject(ModuleNames.CLIENTS, selectedProject); const isClientIncluded = isModuleInProject(ModuleNames.CLIENTS, selectedProject);
@@ -34,22 +39,23 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
setSelectedCard(card); setSelectedCard(card);
}); });
}; };
const isPaid = () => { const canBeRemovedFromGroup = () => {
return cardSummary.billRequest?.paid || cardSummary.group?.billRequest?.paid; return !!(cardSummary.group && (!cardSummary.group.billRequests || cardSummary.group.billRequests?.length === 0));
};
const isLockedInsideGroup = () => {
return cardSummary.group && !cardSummary.group.billRequest;
}; };
useEffect(() => {
setIsPaid(isDealPaid(cardSummary));
}, []);
return ( return (
<div <div
onContextMenu={showContextMenu([ onContextMenu={!authState.isDealsViewer ? showContextMenu([
...isLockedInsideGroup() ? [{ ...!canBeRemovedFromGroup() ? [] : [{
key: "removeFromGroup", key: "removeFromGroup",
onClick: () => onDeleteFromGroup(cardSummary), onClick: () => onDeleteFromGroup(cardSummary),
title: "Убрать из группы", title: "Убрать из группы",
icon: <IconLayoutGridRemove />, icon: <IconLayoutGridRemove />,
}] : [], }],
{ {
key: "complete", key: "complete",
onClick: () => onComplete(cardSummary), onClick: () => onComplete(cardSummary),
@@ -62,7 +68,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
title: "Удалить", title: "Удалить",
icon: <IconTrash />, icon: <IconTrash />,
}, },
])} ]) : undefined}
onClick={() => onCardSummaryClick()} onClick={() => onCardSummaryClick()}
className={styles["container"]} className={styles["container"]}
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
@@ -105,7 +111,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
</Flex> </Flex>
)} )}
<CardAttributesInSummaryItem cardSummary={cardSummary} /> <CardAttributesInSummaryItem cardSummary={cardSummary} />
{!cardSummary.group?.id && ( {!authState.isGuest && !cardSummary.group?.id && (
<CardTags cardId={cardSummary.id} tags={cardSummary.tags} /> <CardTags cardId={cardSummary.id} tags={cardSummary.tags} />
)} )}
<Flex align={"center"} justify={"space-between"}> <Flex align={"center"} justify={"space-between"}>
@@ -147,7 +153,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
</Popover> </Popover>
)} )}
</CopyButton> </CopyButton>
{isPaid() && ( {isPaid && (
<Tooltip label={"Оплачен"}> <Tooltip label={"Оплачен"}>
<ThemeIcon variant={"transparent"}> <ThemeIcon variant={"transparent"}>
<IconCheck /> <IconCheck />

View File

@@ -12,6 +12,8 @@ import DragState from "../../../../pages/CardsPage/enums/DragState.ts";
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts"; import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx"; import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import { ModuleNames } from "../../../../modules/modules.tsx"; import { ModuleNames } from "../../../../modules/modules.tsx";
import { RootState } from "../../../../redux/store.ts";
import { useSelector } from "react-redux";
type Props = { type Props = {
status: StatusSchema; status: StatusSchema;
@@ -28,6 +30,7 @@ export const CardsDndColumn: FC<Props> = ({
dragState, dragState,
withCreateButton = false, withCreateButton = false,
}) => { }) => {
const authState = useSelector((state: RootState) => state.auth);
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const isCreatingDealFromFileEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject); const isCreatingDealFromFileEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
const isDropDisabled = dragState !== DragState.DRAG_CARD; const isDropDisabled = dragState !== DragState.DRAG_CARD;
@@ -77,7 +80,9 @@ export const CardsDndColumn: FC<Props> = ({
<Draggable <Draggable
draggableId={card.id.toString()} draggableId={card.id.toString()}
index={card.rank} index={card.rank}
key={card.id}> key={card.id}
isDragDisabled={authState.isGuest}
>
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
{...provided.draggableProps} {...provided.draggableProps}
@@ -104,6 +109,7 @@ export const CardsDndColumn: FC<Props> = ({
draggableId={"group-" + group.id} draggableId={"group-" + group.id}
index={cards[0].rank} index={cards[0].rank}
key={"group-" + group.id} key={"group-" + group.id}
isDragDisabled={authState.isGuest}
> >
{(provided) => ( {(provided) => (
<div <div
@@ -135,7 +141,7 @@ export const CardsDndColumn: FC<Props> = ({
styles["items-list-drag-over"], styles["items-list-drag-over"],
)} )}
{...provided.droppableProps}> {...provided.droppableProps}>
{withCreateButton && ( {withCreateButton && !authState.isGuest && (
<> <>
<CreateCardButton status={status} /> <CreateCardButton status={status} />
{ {

View File

@@ -1,27 +1,17 @@
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { Flex } from "@mantine/core"; import { Flex } from "@mantine/core";
import { DropResult } from "@hello-pangea/dnd"; import { DropResult } from "@hello-pangea/dnd";
import { useEffect, useState } from "react";
import { CardGroupService, CardService, CardSummary, CardSummaryReorderRequest } from "../../../../../client"; import { CardGroupService, CardService, CardSummary, CardSummaryReorderRequest } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts"; import { notifications } from "../../../../../shared/lib/notifications.ts";
import { dateWithoutTimezone } from "../../../../../shared/lib/date.ts"; import { dateWithoutTimezone } from "../../../../../shared/lib/date.ts";
type Props = { type Props = {
summariesRaw: CardSummary[]; summaries: CardSummary[];
refetchSummaries: () => void; refetchSummaries: () => void;
} }
const useCardsDnd = ({ const useCardsDnd = ({ summaries, refetchSummaries }: Props) => {
summariesRaw,
refetchSummaries,
}: Props) => {
const [summaries, setSummaries] = useState(summariesRaw);
useEffect(() => {
setSummaries(summariesRaw);
}, [summariesRaw]);
const recalculate = async (cardId: number) => { const recalculate = async (cardId: number) => {
return CardService.recalculateCardPrice({ return CardService.recalculateCardPrice({
requestBody: { requestBody: {
@@ -103,7 +93,6 @@ const useCardsDnd = ({
notifications.error({ message: response.message }); notifications.error({ message: response.message });
return; return;
} }
await refetchSummaries();
await recalculate(sourceId); await recalculate(sourceId);
await refetchSummaries(); await refetchSummaries();
}); });
@@ -120,7 +109,6 @@ const useCardsDnd = ({
notifications.error({ message: response.message }); notifications.error({ message: response.message });
return; return;
} }
await refetchSummaries();
await recalculate(sourceId); await recalculate(sourceId);
await refetchSummaries(); await refetchSummaries();
}); });
@@ -185,8 +173,7 @@ const useCardsDnd = ({
if (statusId == summary.status.id) { if (statusId == summary.status.id) {
CardService.reorderCardSummaries({ CardService.reorderCardSummaries({
requestBody: request as CardSummaryReorderRequest, requestBody: request as CardSummaryReorderRequest,
}).then(async response => { }).then(async () => {
setSummaries(response.summaries);
await refetchSummaries(); await refetchSummaries();
}); });
return; return;
@@ -200,14 +187,12 @@ const useCardsDnd = ({
comment: "", comment: "",
deadline: dateWithoutTimezone(new Date()), deadline: dateWithoutTimezone(new Date()),
}, },
}).then(async response => { }).then(async () => {
setSummaries(response.summaries);
await refetchSummaries(); await refetchSummaries();
}); });
}; };
return { return {
summaries,
onCardDragEnd, onCardDragEnd,
}; };
}; };

View File

@@ -10,6 +10,7 @@ import { usePrefillCardContext } from "../../../../pages/CardsPage/contexts/Pref
import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts"; import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx"; import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import { ModuleNames } from "../../../../modules/modules.tsx"; import { ModuleNames } from "../../../../modules/modules.tsx";
import { useDndContext } from "../../../../pages/CardsPage/contexts/DndContext.tsx";
type Props = { type Props = {
status: StatusSchema; status: StatusSchema;
@@ -20,6 +21,7 @@ const CreateCardButton = ({ status }: Props) => {
const [isTransitionEnded, setIsTransitionEnded] = useState(true); const [isTransitionEnded, setIsTransitionEnded] = useState(true);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { prefillCard, setPrefillCard } = usePrefillCardContext(); const { prefillCard, setPrefillCard } = usePrefillCardContext();
const { refetchSummaries } = useDndContext();
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const isPrefillingDealEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject); const isPrefillingDealEnabled = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedProject);
@@ -57,7 +59,7 @@ const CreateCardButton = ({ status }: Props) => {
}, },
}).then(async (result) => { }).then(async (result) => {
if (isPrefillingDealEnabled && prefillCard) { if (isPrefillingDealEnabled && prefillCard) {
CardService.prefillCard({ await CardService.prefillCard({
requestBody: { requestBody: {
oldCardId: prefillCard.id, oldCardId: prefillCard.id,
newCardId: result.cardId, newCardId: result.cardId,
@@ -67,6 +69,7 @@ const CreateCardButton = ({ status }: Props) => {
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["getCardSummaries"], queryKey: ["getCardSummaries"],
}); });
refetchSummaries();
setIsCreating(false); setIsCreating(false);
setPrefillCard(undefined); setPrefillCard(undefined);
}); });

View File

@@ -13,6 +13,8 @@ import isModuleInProject from "../../../../modules/utils/isModuleInProject.ts";
import { useEqualHeightsContext } from "./contexts/EqualHeightContext.tsx"; import { useEqualHeightsContext } from "./contexts/EqualHeightContext.tsx";
import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx"; import { useBoardsContext } from "../../../../contexts/BoardsContext.tsx";
import { ModuleNames } from "../../../../modules/modules.tsx"; import { ModuleNames } from "../../../../modules/modules.tsx";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store.ts";
type Props = { type Props = {
@@ -26,6 +28,7 @@ const Status = ({ summaries, status, dragState, index }: Props) => {
const { const {
selectedBoard, selectedBoard,
} = useBoardsContext(); } = useBoardsContext();
const authState = useSelector((state: RootState) => state.auth);
const isDropDisabled = dragState !== DragState.DRAG_STATUS; const isDropDisabled = dragState !== DragState.DRAG_STATUS;
const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedBoard?.project); const isServicesAndProductsIncluded = isModuleInProject(ModuleNames.SERVICES_AND_PRODUCTS, selectedBoard?.project);
@@ -107,6 +110,7 @@ const Status = ({ summaries, status, dragState, index }: Props) => {
<Draggable <Draggable
draggableId={"status-" + status.id.toString()} draggableId={"status-" + status.id.toString()}
index={status.ordinalNumber} index={status.ordinalNumber}
isDragDisabled={authState.isGuest}
> >
{(provided) => ( {(provided) => (
<div <div

View File

@@ -10,11 +10,14 @@ import getRenderOptions from "./utils/getRenderOptions.tsx";
type RestProps = { type RestProps = {
clientId: number; clientId: number;
}; };
const MAX_PRODUCTS = 200; const MAX_PRODUCTS = 200;
type Props = Omit<ObjectSelectProps<ProductSchema>, "data"> & RestProps; type Props = Omit<ObjectSelectProps<ProductSchema>, "data"> & RestProps;
const ProductSelect: FC<Props> = (props: Props) => { const ProductSelect: FC<Props> = (props: Props) => {
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const [debounced] = useDebouncedValue(searchValue, 500); const [searchValueServer, setSearchValueServer] = useState("");
const [debounced] = useDebouncedValue(searchValueServer, 500);
const { products, isLoading } = useProductsList({ const { products, isLoading } = useProductsList({
clientId: props.clientId, clientId: props.clientId,
searchInput: debounced, searchInput: debounced,
@@ -22,21 +25,35 @@ const ProductSelect: FC<Props> = (props: Props) => {
itemsPerPage: MAX_PRODUCTS, itemsPerPage: MAX_PRODUCTS,
}); });
const restProps = omit(props, ["clientId"]); const restProps = omit(props, ["clientId"]);
const filterProducts = (searchFilter: string): ProductSchema[] => {
const optionsFilter: OptionsFilter = ({ options }) => options; 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 setSearchValueImpl = (value: string) => {
const names = products.map(product => product.name);
if (names.includes(value)) return;
setSearchValue(value); setSearchValue(value);
const filtered = filterProducts(value);
if (filtered && filtered.length > 0) {
return;
}
setSearchValueServer(value);
}; };
return ( return (
<ObjectSelect <ObjectSelect
rightSection={ rightSection={
isLoading || searchValue !== debounced ? ( isLoading? (
<Loader size={"sm"} /> <Loader size={"sm"} />
) : null ) : null
} }
searchValue={searchValue}
onSearchChange={setSearchValueImpl} onSearchChange={setSearchValueImpl}
renderOption={getRenderOptions(products)} renderOption={getRenderOptions(products)}
searchable searchable

View File

@@ -1,44 +1,23 @@
import { FC, ReactNode } from "react"; import { FC } from "react";
import { Select } from "@mantine/core";
import { ClientSchema } from "../../../client"; import { ClientSchema } from "../../../client";
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx"; 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 ( return (
<Select <ObjectSelect
{...props}
searchable searchable
placeholder={"Выберите клиента"} placeholder={"Выберите клиента"}
value={ data={clients.filter(cl => !cl.isDeleted)}
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}
/> />
); );
}; };

View File

@@ -8,7 +8,7 @@ import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx"
type Props = Omit<ObjectSelectProps<ClientSchema>, "data">; type Props = Omit<ObjectSelectProps<ClientSchema>, "data">;
const ClientSelectNew: FC<Props> = props => { const ClientSelectNew: FC<Props> = props => {
const { clients } = useClientsList(); const { clients } = useClientsList({ all: false });
return ( return (
<ObjectSelect <ObjectSelect
searchable searchable

View File

@@ -1,9 +1,12 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { jwtDecode, JwtPayload as JwtPayloadBase } from "jwt-decode"; import { jwtDecode, JwtPayload as JwtPayloadBase } from "jwt-decode";
import { OpenAPI } from "../client";
interface AuthState { interface AuthState {
isAuthorized: boolean; isAuthorized: boolean;
accessToken: string; accessToken: string;
isDealEditor: boolean;
isDealsViewer: boolean;
isGuest: boolean; isGuest: boolean;
role: string; role: string;
} }
@@ -16,6 +19,8 @@ const initialState = (): AuthState => {
return { return {
accessToken: "", accessToken: "",
isAuthorized: false, isAuthorized: false,
isDealEditor: false,
isDealsViewer: false,
isGuest: false, isGuest: false,
role: "user", role: "user",
}; };
@@ -34,10 +39,13 @@ const authSlice = createSlice({
const { sub, role } = jwtDecode<JwtPayload>( const { sub, role } = jwtDecode<JwtPayload>(
action.payload.accessToken, action.payload.accessToken,
); );
OpenAPI.TOKEN = action.payload.accessToken;
state.accessToken = action.payload.accessToken; state.accessToken = action.payload.accessToken;
state.isAuthorized = true; state.isAuthorized = true;
state.role = role; 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 (_) { } catch (_) {
const url = window.location.href; const url = window.location.href;
const urlObj = new URL(url); const urlObj = new URL(url);

View File

@@ -1,9 +1,12 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { type FullProjectSchema, ProjectService } from "../client"; import { type FullProjectSchema, ProjectService } from "../client";
import { useSelector } from "react-redux";
import { RootState } from "../redux/store.ts";
const useProjects = () => { const useProjects = () => {
const [projects, setProjects] = useState<FullProjectSchema[]>([]); const [projects, setProjects] = useState<FullProjectSchema[]>([]);
const authState = useSelector((state: RootState) => state.auth);
const refetchProjects = () => { const refetchProjects = () => {
ProjectService.getProjects() ProjectService.getProjects()
@@ -14,8 +17,10 @@ const useProjects = () => {
}; };
useEffect(() => { useEffect(() => {
refetchProjects(); if (authState.isAuthorized) {
}, []); refetchProjects();
}
}, [authState.isAuthorized]);
return { return {
projects, projects,

View File

@@ -42,6 +42,9 @@ import AttributeModal from "../pages/AdminPage/tabs/Attributes/modals/AttributeM
import CreateProjectModal import CreateProjectModal
from "../pages/CardsPage/drawers/ProjectEditDrawer/tabs/General/modals/CreateProjectModal.tsx"; from "../pages/CardsPage/drawers/ProjectEditDrawer/tabs/General/modals/CreateProjectModal.tsx";
import CardTagModal from "../pages/CardsPage/drawers/ProjectEditDrawer/tabs/Tags/modals/CardTagModal.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 = { export const modals = {
enterDeadline: EnterDeadlineModal, enterDeadline: EnterDeadlineModal,
@@ -82,4 +85,7 @@ export const modals = {
attributeModal: AttributeModal, attributeModal: AttributeModal,
createProjectModal: CreateProjectModal, createProjectModal: CreateProjectModal,
cardTagModal: CardTagModal, cardTagModal: CardTagModal,
placeTypeModal: PlaceTypeModal,
placeModal: PlaceModal,
selectPlaceQrType: SelectPlaceQrType,
}; };

View File

@@ -22,12 +22,14 @@ const ClientTab = () => {
initialValues, initialValues,
validate: { validate: {
details: { details: {
phoneNumber: value => phoneNumber: value => {
!phone(value || "", { if (!value || value === "+7 ") return false;
return !phone(value || "", {
country: "", country: "",
strictDetection: false, strictDetection: false,
validateMobilePrefix: false, validateMobilePrefix: false,
}).isValid && "Неверно указан номер телефона", }).isValid && "Неверно указан номер телефона";
},
inn: (inn: string | undefined | null) => inn && !isValidInn(inn) ? "Некорректный ИНН" : null, inn: (inn: string | undefined | null) => inn && !isValidInn(inn) ? "Некорректный ИНН" : null,
}, },
}, },
@@ -40,9 +42,10 @@ const ClientTab = () => {
form.setValues(data); form.setValues(data);
}, [card]); }, [card]);
const isEditorDisabled = () => client?.id !== card?.client?.id; const isEditorDisabled = () => client?.id !== card?.client?.id || client?.isDeleted === true;
const handleSubmitClientInfo = (values: ClientSchema) => { const handleSubmitClientInfo = (values: ClientSchema) => {
if (values.details?.phoneNumber === "+7 ") values.details!.phoneNumber = "";
ClientService.updateClient({ ClientService.updateClient({
requestBody: { requestBody: {
data: values, data: values,
@@ -98,6 +101,7 @@ const ClientTab = () => {
mask="+7 000 000-00-00" mask="+7 000 000-00-00"
placeholder={"Введите номер телефона"} placeholder={"Введите номер телефона"}
{...form.getInputProps("details.phoneNumber")} {...form.getInputProps("details.phoneNumber")}
disabled={isEditorDisabled()}
/> />
</Input.Wrapper> </Input.Wrapper>
<TextInput <TextInput
@@ -145,7 +149,6 @@ const ClientTab = () => {
<ClientSelect <ClientSelect
value={client} value={client}
onChange={setClient} onChange={setClient}
withLabel
disabled={!isEqual(initialValues, form.values) || !!card?.chat} disabled={!isEqual(initialValues, form.values) || !!card?.chat}
/> />
{!card?.chat && ( {!card?.chat && (

View File

@@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, useEffect, useState } from "react";
import styles from "./ProductAndServiceTab.module.css"; import styles from "./ProductAndServiceTab.module.css";
import ProductView from "./components/ProductView/ProductView.tsx"; import ProductView from "./components/ProductView/ProductView.tsx";
import { Button, Checkbox, Divider, Flex, Group, rem, ScrollArea, Stack, Text, Title } from "@mantine/core"; 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 PrintDealBarcodesButton from "./components/PrintDealBarcodesButton/PrintDealBarcodesButton.tsx";
import PaymentLinkButton from "./components/PaymentLinkButton/PaymentLinkButton.tsx"; import PaymentLinkButton from "./components/PaymentLinkButton/PaymentLinkButton.tsx";
import isValidInn from "../../../../pages/ClientsPage/utils/isValidInn.ts"; 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 ProductAndServiceTab: FC = () => {
const { cardState, cardServicesState, cardProductsState } = useCardProductAndServiceTabState(); 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 = () => { const onAddProductClick = () => {
if (!cardProductsState.onCreate || !cardState.card || !cardState.card.clientId) return; if (!cardProductsState.onCreate || !cardState.card || !cardState.card.clientId) return;
const productIds = cardState.card.products.map( const productIds = cardState.card.products.map(
@@ -121,7 +133,9 @@ const ProductAndServiceTab: FC = () => {
ProductService.createProduct({ ProductService.createProduct({
requestBody: newProduct, requestBody: newProduct,
}).then(({ ok, message }) => { }).then(({ ok, message }) => {
notifications.guess(ok, { message: message }); notifications.guess(ok, { message });
}).catch(err => {
notifications.error({ message: err.toString() });
}); });
}; };
const onCreateProductClick = () => { const onCreateProductClick = () => {
@@ -222,7 +236,7 @@ const ProductAndServiceTab: FC = () => {
<div <div
className={classNames( className={classNames(
styles["container"], styles["container"],
cardState.card?.billRequest && styles["container-disabled"], cardState.card?.billRequests?.length && styles["container-disabled"],
)}> )}>
<div className={styles["products-list"]}> <div className={styles["products-list"]}>
<ScrollArea offsetScrollbars> <ScrollArea offsetScrollbars>
@@ -249,18 +263,18 @@ const ProductAndServiceTab: FC = () => {
mr={"xs"} mr={"xs"}
> >
<Group wrap={"nowrap"}> <Group wrap={"nowrap"}>
<PrintDealBarcodesButton card={cardState.card} /> {!isDealsViewer && <PrintDealBarcodesButton card={cardState.card} />}
<Checkbox <Checkbox
label={"Оплачен"} label={"Оплачен"}
checked={cardState.card.billRequest?.paid || cardState.card.group?.billRequest?.paid || false} checked={paid}
disabled disabled
/> />
</Group> </Group>
<PaymentLinkButton card={cardState.card} /> {!isDealsViewer && <PaymentLinkButton card={cardState.card} />}
</Group> </Group>
)} )}
<Stack className={styles["card-container-wrapper"]} mr={"xs"}> <Stack className={styles["card-container-wrapper"]} mr={"xs"}>
<GeneralDataForm /> {card && <GeneralDataForm />}
</Stack> </Stack>
<ScrollArea offsetScrollbars> <ScrollArea offsetScrollbars>
<Flex <Flex
@@ -271,41 +285,45 @@ const ProductAndServiceTab: FC = () => {
{...cardServicesState} {...cardServicesState}
/> />
<Divider my={rem(15)} /> {!isDealsViewer && (
<div className={styles["card-container-buttons"]}> <>
<Button <Divider my={rem(15)} />
disabled={isLocked} <div className={styles["card-container-buttons"]}>
variant={"default"} <Button
fullWidth disabled={isLocked}
onClick={onCreateProductClick}> variant={"default"}
Создать товар fullWidth
</Button> onClick={onCreateProductClick}>
<Button Создать товар
disabled={isLocked} </Button>
onClick={onAddProductClick} <Button
variant={"default"} disabled={isLocked}
fullWidth> onClick={onAddProductClick}
Добавить товар variant={"default"}
</Button> fullWidth>
</div> Добавить товар
<Divider my={rem(15)} /> </Button>
<div className={styles["card-container-buttons"]}> </div>
{isLocked ? ( <Divider my={rem(15)} />
<Button <div className={styles["card-container-buttons"]}>
onClick={onCancelBillClick} {isLocked ? (
color={"red"}> <Button
Отозвать счет onClick={onCancelBillClick}
</Button> color={"red"}>
) : ( Отозвать счет
<Button </Button>
disabled={isLocked} ) : (
onClick={onCreateBillClick} <Button
variant={"default"} disabled={isLocked}
fullWidth> onClick={onCreateBillClick}
Выставить счет variant={"default"}
</Button> fullWidth>
)} Выставить счет
</div> </Button>
)}
</div>
</>
)}
</Flex> </Flex>
<Flex <Flex
direction={"column"} direction={"column"}

View File

@@ -12,6 +12,7 @@ import { RootState } from "../../../../../../redux/store.ts";
import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx"; import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
import LockCheckbox from "../../../../../../components/LockCheckbox/LockCheckbox.tsx"; import LockCheckbox from "../../../../../../components/LockCheckbox/LockCheckbox.tsx";
import { useDebouncedCallback } from "@mantine/hooks"; import { useDebouncedCallback } from "@mantine/hooks";
import { isDealLocked } from "../../../../../../pages/CardsPage/utils/isDealPaid.ts";
type RestProps = { type RestProps = {
onKitAdd?: (kit: GetServiceKitSchema) => void; onKitAdd?: (kit: GetServiceKitSchema) => void;
@@ -31,7 +32,7 @@ const CardServicesTable: FC<Props> = ({
const authState = useSelector((state: RootState) => state.auth); const authState = useSelector((state: RootState) => state.auth);
const { cardState } = useCardProductAndServiceTabState(); const { cardState } = useCardProductAndServiceTabState();
const isLocked = Boolean(cardState.card?.billRequest); const isLocked = isDealLocked(cardState.card);
const [currentService, setCurrentService] = useState< const [currentService, setCurrentService] = useState<
CardServiceSchema | undefined CardServiceSchema | undefined
@@ -137,15 +138,17 @@ const CardServicesTable: FC<Props> = ({
w={"100%"} w={"100%"}
gap={rem(10)} gap={rem(10)}
align={"center"}> align={"center"}>
<Tooltip {!authState.isDealsViewer && (
onClick={() => onDeleteClick(service)} <Tooltip
label="Удалить услугу"> onClick={() => onDeleteClick(service)}
<ActionIcon label="Удалить услугу">
disabled={isLocked} <ActionIcon
variant={"default"}> disabled={isLocked}
<IconTrash /> variant={"default"}>
</ActionIcon> <IconTrash />
</Tooltip> </ActionIcon>
</Tooltip>
)}
{!authState.isGuest && ( {!authState.isGuest && (
<Tooltip label="Сотрудники"> <Tooltip label="Сотрудники">
<ActionIcon <ActionIcon
@@ -166,9 +169,8 @@ const CardServicesTable: FC<Props> = ({
isNumber(event) && isNumber(event) &&
onQuantityChange(service, event) onQuantityChange(service, event)
} }
value={service.quantity} value={service.quantity}
readOnly={authState.isDealsViewer}
/> />
<NumberInput <NumberInput
flex={1} flex={1}
@@ -179,6 +181,7 @@ const CardServicesTable: FC<Props> = ({
suffix={"₽"} suffix={"₽"}
value={service.price} value={service.price}
disabled={authState.isGuest || isLocked || service.isFixedPrice} disabled={authState.isGuest || isLocked || service.isFixedPrice}
readOnly={authState.isDealsViewer}
rightSectionProps={{ rightSectionProps={{
style: { style: {
display: "flex", display: "flex",
@@ -187,12 +190,14 @@ const CardServicesTable: FC<Props> = ({
}, },
}} }}
rightSection={ rightSection={
<LockCheckbox !authState.isDealsViewer && (
label={"Зафиксировать цену"} <LockCheckbox
variant={"default"} label={"Зафиксировать цену"}
value={service.isFixedPrice} variant={"default"}
onChange={value => onLockChange(service, value)} value={service.isFixedPrice}
/> onChange={value => onLockChange(service, value)}
/>
)
} }
/> />
</Flex> </Flex>
@@ -211,25 +216,27 @@ const CardServicesTable: FC<Props> = ({
</Title> </Title>
</Flex> </Flex>
<Flex {!authState.isDealsViewer && (
direction={"column"} <Flex
gap={rem(10)} direction={"column"}
mt={"auto"}> gap={rem(10)}
<Button mt={"auto"}>
disabled={isLocked} <Button
onClick={onCreateClick} disabled={isLocked}
fullWidth onClick={onCreateClick}
variant={"default"}> fullWidth
Добавить услугу variant={"default"}>
</Button> Добавить услугу
<Button </Button>
disabled={isLocked} <Button
onClick={onAddKitClick} disabled={isLocked}
fullWidth onClick={onAddKitClick}
variant={"default"}> fullWidth
Добавить набор услуг variant={"default"}>
</Button> Добавить набор услуг
</Flex> </Button>
</Flex>
)}
</Flex> </Flex>
<Modal <Modal
title={"Добавление сотрудника к услуге"} title={"Добавление сотрудника к услуге"}

View File

@@ -7,6 +7,8 @@ import { Button, Checkbox, Stack } from "@mantine/core";
import { notifications } from "../../../../../../shared/lib/notifications.ts"; import { notifications } from "../../../../../../shared/lib/notifications.ts";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts";
type GeneralDataFormType = { type GeneralDataFormType = {
@@ -16,7 +18,8 @@ type GeneralDataFormType = {
const GeneralDataForm = () => { const GeneralDataForm = () => {
const { selectedCard: card, refetchCard } = useCardPageContext(); const { selectedCard: card, refetchCard } = useCardPageContext();
if (!card) return; const { isDealsViewer } = useSelector((state: RootState) => state.auth);
if (!card) return <></>;
const [initialValues, setInitialValues] = useState<GeneralDataFormType>(card); const [initialValues, setInitialValues] = useState<GeneralDataFormType>(card);
@@ -67,7 +70,7 @@ const GeneralDataForm = () => {
<form onSubmit={form.onSubmit(values => onSubmit(values))}> <form onSubmit={form.onSubmit(values => onSubmit(values))}>
<Stack> <Stack>
<ShippingWarehouseAutocomplete <ShippingWarehouseAutocomplete
placeholder={"Введите склад отгрузки"} placeholder={isDealsViewer ? "" : "Введите склад отгрузки"}
label={"Склад отгрузки"} label={"Склад отгрузки"}
value={ value={
isShippingWarehouse( isShippingWarehouse(
@@ -87,18 +90,23 @@ const GeneralDataForm = () => {
"shippingWarehouse", "shippingWarehouse",
).onChange(event); ).onChange(event);
}} }}
readOnly={isDealsViewer}
/> />
<Checkbox {!isDealsViewer && (
label={"Учет выручки в статистике"} <>
{...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })} <Checkbox
/> label={"Учет выручки в статистике"}
<Button {...form.getInputProps("isServicesProfitAccounted", { type: "checkbox" })}
type={"submit"} />
variant={"default"} <Button
disabled={isEqual(initialValues, form.values)} type={"submit"}
> variant={"default"}
Сохранить disabled={isEqual(initialValues, form.values)}
</Button> >
Сохранить
</Button>
</>
)}
</Stack> </Stack>
</form> </form>
); );

View File

@@ -3,19 +3,40 @@ import ButtonCopy from "../../../../../../components/ButtonCopy/ButtonCopy.tsx";
import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx"; import { ButtonCopyControlled } from "../../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
import { getCurrentDateTimeForFilename } from "../../../../../../shared/lib/date.ts"; import { getCurrentDateTimeForFilename } from "../../../../../../shared/lib/date.ts";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import { Button, Popover, Stack } from "@mantine/core";
type Props = { type Props = {
card: CardSchema; card: CardSchema;
} }
const PaymentLinkButton = ({ card }: Props) => { 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 ( return (
<ButtonCopy <ButtonCopy
onCopiedLabel={"Ссылка скопирована в буфер обмена"} onCopiedLabel={"Ссылка скопирована в буфер обмена"}
value={billRequestPdfUrl} value={urls[0]}
> >
Ссылка на оплату Ссылка на оплату
</ButtonCopy> </ButtonCopy>
@@ -23,21 +44,25 @@ const PaymentLinkButton = ({ card }: Props) => {
} }
return ( return (
<ButtonCopyControlled <Popover width={380} position="bottom" withArrow shadow="md">
onCopyClick={() => { <Popover.Target>
const date = <Button variant={"default"}>Ссылки на оплату</Button>
getCurrentDateTimeForFilename(); </Popover.Target>
FileSaver.saveAs( <Popover.Dropdown>
`${import.meta.env.VITE_API_URL}/card/billing-document/${card.id}`, <Stack gap={"md"}>
`bill_${card.id}_${date}.pdf`, {urls.map((url, i) => (
); <ButtonCopy
}} key={i}
copied={false} onCopiedLabel={"Ссылка скопирована в буфер обмена"}
onCopiedLabel={"Ссылка скопирована в буфер обмена"} value={url}
> >
Ссылка на оплату (PDF) {`Ссылка на оплату (часть ${String(i + 1)})`}
</ButtonCopyControlled> </ButtonCopy>
))}
</Stack>
</Popover.Dropdown>
</Popover>
); );
} };
export default PaymentLinkButton; export default PaymentLinkButton;

View File

@@ -11,6 +11,7 @@ import SimpleUsersTable from "../../../../../../pages/CardsPage/components/Simpl
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../../../../../redux/store.ts"; import { RootState } from "../../../../../../redux/store.ts";
import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx"; import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
import { isDealLocked } from "../../../../../../pages/CardsPage/utils/isDealPaid.ts";
type RestProps = { type RestProps = {
quantity: number; quantity: number;
@@ -20,16 +21,16 @@ type RestProps = {
type Props = CRUDTableProps<CardProductServiceSchema> & RestProps; type Props = CRUDTableProps<CardProductServiceSchema> & RestProps;
const ProductServicesTable: FC<Props> = ({ const ProductServicesTable: FC<Props> = ({
items, items,
quantity, quantity,
onCreate, onCreate,
onDelete, onDelete,
onChange, onChange,
onCopyServices, onCopyServices,
onKitAdd, onKitAdd,
}) => { }) => {
const { cardState } = useCardProductAndServiceTabState(); const { cardState } = useCardProductAndServiceTabState();
const isLocked = Boolean(cardState.card?.billRequest); const isLocked = isDealLocked(cardState.card);
const authState = useSelector((state: RootState) => state.auth); const authState = useSelector((state: RootState) => state.auth);
const columns = useProductServicesTableColumns({ data: items, quantity }); const columns = useProductServicesTableColumns({ data: items, quantity });
@@ -78,7 +79,7 @@ const ProductServicesTable: FC<Props> = ({
const getCurrentEmployees = (): UserSchema[] => { const getCurrentEmployees = (): UserSchema[] => {
if (!currentService) return []; if (!currentService) return [];
const item = items.find( const item = items.find(
i => i.service.id === currentService.service.id i => i.service.id === currentService.service.id,
); );
if (!item) return []; if (!item) return [];
return item.employees; return item.employees;
@@ -102,8 +103,8 @@ const ProductServicesTable: FC<Props> = ({
{ {
enableColumnActions: false, enableColumnActions: false,
enableSorting: false, enableSorting: false,
enableRowActions: true, enableRowActions: !authState.isDealsViewer,
enableBottomToolbar: true, enableBottomToolbar: !authState.isDealsViewer,
renderBottomToolbar: ( renderBottomToolbar: (
<Flex <Flex
justify={"flex-end"} justify={"flex-end"}
@@ -158,7 +159,7 @@ const ProductServicesTable: FC<Props> = ({
<ActionIcon <ActionIcon
onClick={() => onClick={() =>
onEmployeeClick( onEmployeeClick(
row.original row.original,
) )
} }
variant={"default"}> variant={"default"}>

View File

@@ -14,6 +14,9 @@ import { modals } from "@mantine/modals";
import { ServiceType } from "../../../../../../shared/enums/ServiceType.ts"; import { ServiceType } from "../../../../../../shared/enums/ServiceType.ts";
import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx"; import useCardProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
import { useDebouncedCallback } from "@mantine/hooks"; 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 = { type Props = {
product: CardProductSchema; product: CardProductSchema;
@@ -47,12 +50,14 @@ const ProductView: FC<Props> = ({
if (!onChange) return; if (!onChange) return;
onChange(item); onChange(item);
}, 200); }, 200);
const isLocked = Boolean(cardState.card?.billRequest); const isLocked = isDealLocked(cardState.card);
const onDeleteClick = () => { const onDeleteClick = () => {
if (!onDelete) return; if (!onDelete) return;
onDelete(product); onDelete(product);
}; };
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
const onServiceDelete = (item: CardProductServiceSchema) => { const onServiceDelete = (item: CardProductServiceSchema) => {
if (!onChange) return; if (!onChange) return;
onChange({ onChange({
@@ -161,25 +166,29 @@ const ProductView: FC<Props> = ({
isNumber(event) && onQuantityChange(event) isNumber(event) && onQuantityChange(event)
} }
placeholder={"Введите количество товара"} placeholder={"Введите количество товара"}
readOnly={isDealsViewer}
/> />
<Textarea {!(isDealsViewer && product.comment?.length === 0) && (
mih={rem(140)} <Textarea
styles={{ mih={rem(140)}
wrapper: { height: "90%" }, styles={{
input: { height: "90%" }, wrapper: { height: "90%" },
}} input: { height: "90%" },
my={rem(10)} }}
disabled={isLocked} my={rem(10)}
defaultValue={product.comment} disabled={isLocked}
onChange={event => { defaultValue={product.comment}
if (!onChange) return; onChange={event => {
debouncedOnChange({ if (!onChange) return;
...product, debouncedOnChange({
comment: event.currentTarget.value, ...product,
}); comment: event.currentTarget.value,
}} });
placeholder={"Введите комментарий для товара"} }}
/> placeholder={isDealsViewer ? "" : "Введите комментарий для товара"}
readOnly={isDealsViewer}
/>
)}
</div> </div>
<div className={styles["services-container"]}> <div className={styles["services-container"]}>
@@ -194,34 +203,36 @@ const ProductView: FC<Props> = ({
onDelete={onServiceDelete} onDelete={onServiceDelete}
onChange={onServiceChange} onChange={onServiceChange}
/> />
<Flex {!isDealsViewer && (
mt={"auto"} <Flex
ml={"auto"} mt={"auto"}
gap={rem(10)}> ml={"auto"}
<Tooltip gap={rem(10)}>
onClick={onPrintBarcodeClick} <Tooltip
label="Печать штрихкода"> onClick={onPrintBarcodeClick}
<ActionIcon variant={"default"}> label="Печать штрихкода">
<IconBarcode /> <ActionIcon variant={"default"}>
</ActionIcon> <IconBarcode />
</Tooltip> </ActionIcon>
<Tooltip </Tooltip>
onClick={onProductEditClick} <Tooltip
label="Редактировать товар"> onClick={onProductEditClick}
<ActionIcon variant={"default"}> label="Редактировать товар">
<IconEdit /> <ActionIcon variant={"default"}>
</ActionIcon> <IconEdit />
</Tooltip> </ActionIcon>
<Tooltip </Tooltip>
onClick={onDeleteClick} <Tooltip
label="Удалить товар"> onClick={onDeleteClick}
<ActionIcon label="Удалить товар">
disabled={isLocked} <ActionIcon
variant={"default"}> disabled={isLocked}
<IconTrash /> variant={"default"}>
</ActionIcon> <IconTrash />
</Tooltip> </ActionIcon>
</Flex> </Tooltip>
</Flex>
)}
</div> </div>
</div> </div>
); );

View File

@@ -1,7 +1,9 @@
import React, { createContext, FC, useContext, useEffect, useState } from "react"; import React, { createContext, FC, useContext, useEffect, useState } from "react";
import { useProjectsContext } from "../../contexts/ProjectsContext.tsx"; import { useProjectsContext } from "../../contexts/ProjectsContext.tsx";
import { MODULES } from "../modules.tsx"; import { ModuleNames, MODULES } from "../modules.tsx";
import { Module } from "../types.tsx"; import { Module } from "../types.tsx";
import { RootState } from "../../redux/store.ts";
import { useSelector } from "react-redux";
type ModulesContextState = { type ModulesContextState = {
modules: Module[]; modules: Module[];
@@ -13,15 +15,24 @@ const ModulesContext = createContext<ModulesContextState | undefined>(
const useModulesContextState = () => { const useModulesContextState = () => {
const { selectedProject } = useProjectsContext(); const { selectedProject } = useProjectsContext();
const { isDealsViewer } = useSelector((state: RootState) => state.auth);
const [modules, setModules] = useState<Module[]>([]); 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(() => { useEffect(() => {
const modules = selectedProject?.modules ?? []; const modules = selectedProject?.modules ?? [];
const projectModules = modules.map(module => { const projectModules = modules.map(module => {
return MODULES[module.key]; return MODULES[module.key];
}) ?? []; }) ?? [];
setModules(projectModules); setModules(filterModules(projectModules));
}, [selectedProject?.id]); }, [selectedProject?.id]);
return { return {

View File

@@ -22,42 +22,42 @@ const modules: ModulesType = {
[ModuleNames.CLIENTS]: { [ModuleNames.CLIENTS]: {
info: { info: {
label: "Клиенты", label: "Клиенты",
key: "clients", key: ModuleNames.CLIENTS,
icon: <IconUser />, icon: <IconUser />,
} }
}, },
[ModuleNames.SERVICES_AND_PRODUCTS]: { [ModuleNames.SERVICES_AND_PRODUCTS]: {
info: { info: {
label: "Товары и услуги", label: "Товары и услуги",
key: "servicesAndProducts", key: ModuleNames.SERVICES_AND_PRODUCTS,
icon: <IconBox />, icon: <IconBox />,
} }
}, },
[ModuleNames.SHIPMENT]: { [ModuleNames.SHIPMENT]: {
info: { info: {
label: "Отгрузка", label: "Отгрузка",
key: "shipment", key: ModuleNames.SHIPMENT,
icon: <IconCubeSend />, icon: <IconCubeSend />,
} }
}, },
[ModuleNames.EMPLOYEES]: { [ModuleNames.EMPLOYEES]: {
info: { info: {
label: "Сотрудники", label: "Сотрудники",
key: "employees", key: ModuleNames.EMPLOYEES,
icon: <IconUsersGroup />, icon: <IconUsersGroup />,
} }
}, },
[ModuleNames.MANAGERS]: { [ModuleNames.MANAGERS]: {
info: { info: {
label: "Менеджер", label: "Менеджер",
key: "managers", key: ModuleNames.MANAGERS,
icon: <IconUserCog />, icon: <IconUserCog />,
} }
}, },
[ModuleNames.CHAT]: { [ModuleNames.CHAT]: {
info: { info: {
label: "Чат", label: "Чат",
key: "chat", key: ModuleNames.CHAT,
icon: <IconMessage />, icon: <IconMessage />,
} }
}, },

View File

@@ -9,6 +9,7 @@ import {
IconQrcode, IconSubtask, IconQrcode, IconSubtask,
IconTopologyStar3, IconTopologyStar3,
IconUser, IconUser,
IconBuildingWarehouse,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import UsersTab from "./tabs/Users/UsersTab.tsx"; import UsersTab from "./tabs/Users/UsersTab.tsx";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
@@ -22,6 +23,7 @@ import OrganizationalStructureTab from "./tabs/OrganizationalStructureTab/Organi
import { ReactNode } from "react"; import { ReactNode } from "react";
import WorkShiftsPlanning from "./tabs/WorkShiftsPlanning/WorkShiftsPlanning.tsx"; import WorkShiftsPlanning from "./tabs/WorkShiftsPlanning/WorkShiftsPlanning.tsx";
import Attributes from "./tabs/Attributes/Attributes.tsx"; import Attributes from "./tabs/Attributes/Attributes.tsx";
import WarehouseManagementTab from "./tabs/WarehouseManagement/WarehouseManagementTab.tsx";
const AdminPage = () => { const AdminPage = () => {
const userRole = useSelector((state: RootState) => state.auth.role); const userRole = useSelector((state: RootState) => state.auth.role);
@@ -96,6 +98,11 @@ const AdminPage = () => {
Атрибуты карточек Атрибуты карточек
</Tabs.Tab> </Tabs.Tab>
)} )}
<Tabs.Tab
value={"warehouseManagement"}
leftSection={<IconBuildingWarehouse />}>
Управление складом
</Tabs.Tab>
</Tabs.List> </Tabs.List>
{getTabPanel("users", <UsersTab />)} {getTabPanel("users", <UsersTab />)}
{getTabPanel("rolesAndPositions", <OrganizationalStructureTab />)} {getTabPanel("rolesAndPositions", <OrganizationalStructureTab />)}
@@ -105,6 +112,7 @@ const AdminPage = () => {
{getTabPanel("workShifts", <WorkShiftsTab />)} {getTabPanel("workShifts", <WorkShiftsTab />)}
{getTabPanel("transactions", <TransactionsTab />)} {getTabPanel("transactions", <TransactionsTab />)}
{getTabPanel("attributes", <Attributes />)} {getTabPanel("attributes", <Attributes />)}
{getTabPanel("warehouseManagement", <WarehouseManagementTab />)}
</Tabs> </Tabs>
</PageBlock> </PageBlock>
</div> </div>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}
/>
);
};

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -7,7 +7,7 @@ import { processSelectedCells } from "../../../../../shared/lib/interpolateCells
import { TimeTrackingData } from "../../../../../client"; import { TimeTrackingData } from "../../../../../client";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { IMaskInput } from "react-imask"; import { IMaskInput } from "react-imask";
import { floatHoursToHoursAndMinutes } from "../../../../../types/utils.ts"; import { dateToString, floatHoursToHoursAndMinutes } from "../../../../../types/utils.ts";
export type EmployeeData = { export type EmployeeData = {
name: string; name: string;
@@ -20,21 +20,17 @@ export type EmployeeData = {
[key: string]: number | string; [key: string]: number | string;
}; };
type Props = { type Props = {
month: Date;
data: EmployeeData[]; data: EmployeeData[];
onUpdate: (date: Date, userId: number, value: string) => void; onUpdate: (date: Date, userId: number, value: string) => void;
selectedCells: string[]; selectedCells: string[];
setSelectedCells: (cells: string[]) => void; setSelectedCells: (cells: string[]) => void;
selectedBoundaries: [Date | null, Date | null];
range: dayjs.Dayjs[]; range: dayjs.Dayjs[];
}; };
const useWorkTableColumns = ({ const useWorkTableColumns = ({
month,
onUpdate, onUpdate,
data, data,
selectedCells, selectedCells,
setSelectedCells, setSelectedCells,
selectedBoundaries,
range, range,
}: Props) => { }: Props) => {
const totalAmount = useMemo(() => { const totalAmount = useMemo(() => {
@@ -48,7 +44,7 @@ const useWorkTableColumns = ({
} }
return acc; return acc;
}, 0); }, 0);
}, [data, month, selectedCells, selectedBoundaries]); }, [data, selectedCells, range]);
const getBorderStyles = (cellId: string) => { const getBorderStyles = (cellId: string) => {
if (selectedCells.length <= 1) return {}; if (selectedCells.length <= 1) return {};
if (selectedCells[0] === cellId) if (selectedCells[0] === cellId)
@@ -86,8 +82,8 @@ const useWorkTableColumns = ({
...range.map(date => ({ ...range.map(date => ({
size: 100, size: 100,
accessorKey: date.date().toString(), accessorKey: dateToString(date.toDate()) ?? "",
header: date.date().toString(), header: dateToString(date.toDate()) ?? "",
enableSorting: false, enableSorting: false,
enableColumnActions: false, enableColumnActions: false,
Header: ( Header: (
@@ -130,7 +126,7 @@ const useWorkTableColumns = ({
row.original.userId, row.original.userId,
value, value,
); );
} };
return ( return (
<Flex direction={"column"}> <Flex direction={"column"}>
@@ -178,7 +174,7 @@ const useWorkTableColumns = ({
), ),
}, },
], ],
[month, selectedCells, selectedBoundaries, totalAmount], [selectedCells, range, totalAmount],
); );
}; };

View File

@@ -1,49 +1,45 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TimeTrackingRecord, TimeTrackingService } from "../../../../../client"; import { TimeTrackingRecord, TimeTrackingService } from "../../../../../client";
import { import { getDefaultEndDate } from "../../WorkShiftsPlanning/utils/utils.tsx";
dateWithoutTimezone, import { dateToString } from "../../../../../types/utils.ts";
getDatesInMonth,
} from "../../../../../shared/lib/date.ts";
import { last } from "lodash";
const getDateBoundaries = (month: Date) => {
return [
getDatesInMonth(month)[0].toDate(),
last(getDatesInMonth(month))?.toDate(),
];
};
const useWorkTableState = () => { const useWorkTableState = () => {
const [month, setMonth] = useState<Date>( const [dateRange, setDateRange] = useState<
new Date(new Date().getFullYear(), new Date().getMonth(), 1) [Date | null, Date | null]
); >([new Date(), getDefaultEndDate()]);
const [trackingRecords, setTrackingRecords] = useState< const [trackingRecords, setTrackingRecords] = useState<
TimeTrackingRecord[] TimeTrackingRecord[]
>([]); >([]);
const [dateBoundaries, setDateBoundaries] = useState(
getDateBoundaries(month)
);
const refetch = async () => { const refetch = () => {
return TimeTrackingService.getTimeTrackingRecords({ const ending = "T00:00:00";
requestBody: { const dateFrom = dateToString(dateRange[0]);
date: dateWithoutTimezone(month), const dateTo = dateToString(dateRange[1]);
userIds: [], if (!(dateFrom && dateTo)) return;
},
}).then(response => setTrackingRecords(response.records)); TimeTrackingService
.getTimeTrackingRecords({
requestBody: {
dateFrom: dateFrom + ending,
dateTo: dateTo + ending,
},
})
.then(response => {
console.log(response.records);
setTrackingRecords(response.records);
})
.catch(err => console.log(err));
}; };
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars refetch();
refetch().then(_ => { }, [dateRange]);
setDateBoundaries(getDateBoundaries(month));
});
}, [month]);
return { return {
month, dateRange,
setMonth, setDateRange,
refetch, refetch,
trackingRecords, trackingRecords,
dateBoundaries,
}; };
}; };

View File

@@ -1,56 +1,44 @@
import { ActionIcon, Flex, MultiSelect, rem, Tooltip } from "@mantine/core"; 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 useWorkTableState from "../hooks/useWorkTableState.tsx";
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx"; import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useWorkTableColumns, { EmployeeData } from "../hooks/useWorkTableColumns.tsx"; import useWorkTableColumns, { EmployeeData } from "../hooks/useWorkTableColumns.tsx";
import { TimeTrackingRecord, TimeTrackingService, UserSchema } from "../../../../../client"; 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 useUsersList from "../../../hooks/useUsersList.tsx";
import { notifications } from "../../../../../shared/lib/notifications.ts"; import { notifications } from "../../../../../shared/lib/notifications.ts";
import { PaySchemeType } from "../../../../../shared/enums/PaySchemeType.ts"; import { PaySchemeType } from "../../../../../shared/enums/PaySchemeType.ts";
import { IconEyeOff } from "@tabler/icons-react"; import { IconEyeOff } from "@tabler/icons-react";
import { MRT_TableOptions } from "mantine-react-table"; import { MRT_TableOptions } from "mantine-react-table";
import { difference, omit } from "lodash"; import { difference, omit } from "lodash";
import { strTimeToFloatHours } from "../../../../../types/utils.ts"; import { dateToString, strTimeToFloatHours } from "../../../../../types/utils.ts";
import { useListState } from "@mantine/hooks"; import { useListState } from "@mantine/hooks";
const WorkTimeTable = () => { const WorkTimeTable = () => {
const [data, setData] = useState<EmployeeData[]>([]); const [data, setData] = useState<EmployeeData[]>([]);
const { dateBoundaries, month, setMonth, trackingRecords, refetch } = const { dateRange, setDateRange, trackingRecords, refetch } = useWorkTableState();
useWorkTableState();
const [shownUsers, shownUsersHandlers] = useListState<UserSchema>([]); const [shownUsers, shownUsersHandlers] = useListState<UserSchema>([]);
const [selectedBoundaries, setSelectedBoundaries] = useState<
[Date | null, Date | null]
>([null, null]);
const users = useUsersList().objects.filter( const users = useUsersList().objects.filter(
user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY, user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY,
); );
const getRange = () => { const getRange = () => {
const startDate = selectedBoundaries[0]; if (!(dateRange.length === 2 && dateRange[0] && dateRange[1])) return [];
const endDate = selectedBoundaries[1]; const startDate = dateRange[0];
if (startDate && endDate) { const endDate = dateRange[1];
return getDatesBetween(startDate, endDate); return getDatesBetween(startDate, endDate);
} else {
return getDatesInMonth(month);
}
}; };
const range = getRange(); const range = getRange();
const transformTrackingRecordsToData = ( const transformTrackingRecordsToData = (
trackingRecords: TimeTrackingRecord[], trackingRecords: TimeTrackingRecord[],
): EmployeeData[] => { ): EmployeeData[] => {
if (!month) return []; if (!(dateRange.length === 2 && dateRange[0] && dateRange[1])) return [];
const rangeDays = range.map(r => r.date()); 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 existingUserIds = trackingRecords.map(tr => tr.user.id);
const firstResult = trackingRecords.map(record => ({ const firstResult = trackingRecords.map(record => ({
name: `${record.user.firstName} ${record.user.secondName}`, name: `${record.user.firstName} ${record.user.secondName}`,
@@ -59,14 +47,14 @@ const WorkTimeTable = () => {
totalAmount: record.totalAmount, totalAmount: record.totalAmount,
data: record.data, data: record.data,
...Object.fromEntries( ...Object.fromEntries(
getDatesInMonth(month).reduce((acc, day) => { dates.reduce((acc, day) => {
return acc.set(day.date().toString(), 0); return acc.set(dateToString(day.toDate()) ?? "", 0);
}, new Map<string, number>()), }, new Map<string, number>()),
), ),
...Object.fromEntries( ...Object.fromEntries(
record.data.reduce((acc, recordData) => { record.data.reduce((acc, recordData) => {
return acc.set( return acc.set(
new Date(recordData.date).getDate().toString(), recordData.date,
recordData.hours, recordData.hours,
); );
}, new Map<string, number>()), }, new Map<string, number>()),
@@ -80,7 +68,7 @@ const WorkTimeTable = () => {
comment: user.comment, comment: user.comment,
totalAmount: 0, totalAmount: 0,
...Object.fromEntries( ...Object.fromEntries(
getDatesInMonth(month).reduce((acc, day) => { dates.reduce((acc, day) => {
return acc.set(day.date().toString(), 0); return acc.set(day.date().toString(), 0);
}, new Map<string, number>()), }, new Map<string, number>()),
), ),
@@ -93,13 +81,12 @@ const WorkTimeTable = () => {
// @ts-expect-error // @ts-expect-error
.concat(restUsersResult) .concat(restUsersResult)
.filter(r => shownUserIds.includes(r.userId)); .filter(r => shownUserIds.includes(r.userId));
const firstDate = selectedBoundaries[0];
const lastDate = selectedBoundaries[1]; if (dateFrom && dateTo) {
if (firstDate && lastDate) { const allDays = dates.map(d =>
const allDays = getDatesInMonth(month).map(d =>
d.date().toString(), d.date().toString(),
); );
const allowedDays = getDatesBetween(firstDate, lastDate).map(d => const allowedDays = getDatesBetween(dateFrom, dateTo).map(d =>
d.date().toString(), d.date().toString(),
); );
const omitDays = difference(allDays, allowedDays); const omitDays = difference(allDays, allowedDays);
@@ -117,10 +104,7 @@ const WorkTimeTable = () => {
const user = users.find(user => user.id === userId); const user = users.find(user => user.id === userId);
if (!user) return; if (!user) return;
console.log(value);
const hours = strTimeToFloatHours(value); const hours = strTimeToFloatHours(value);
console.log(hours);
if (hours === -1) return; if (hours === -1) return;
setData(prevState => setData(prevState =>
@@ -128,12 +112,13 @@ const WorkTimeTable = () => {
if (record.userId !== userId) return record; if (record.userId !== userId) return record;
record[date.getDate()] = value; record[date.getDate()] = value;
return record; return record;
}) }),
); );
const ending = "T00:00:00";
TimeTrackingService.updateTimeTrackingRecord({ TimeTrackingService.updateTimeTrackingRecord({
requestBody: { requestBody: {
date: dateWithoutTimezone(date), date: dateToString(date) + ending,
hours, hours,
userId: user.id, userId: user.id,
}, },
@@ -141,13 +126,11 @@ const WorkTimeTable = () => {
if (!ok) { if (!ok) {
notifications.guess(ok, { message }); notifications.guess(ok, { message });
} }
await refetch(); refetch();
}); });
}; };
const columns = useWorkTableColumns({ const columns = useWorkTableColumns({
month,
selectedBoundaries,
data, data,
onUpdate: optimisticUpdate, onUpdate: optimisticUpdate,
selectedCells: [], selectedCells: [],
@@ -158,11 +141,7 @@ const WorkTimeTable = () => {
useEffect(() => { useEffect(() => {
setData(transformTrackingRecordsToData(trackingRecords)); setData(transformTrackingRecordsToData(trackingRecords));
}, [trackingRecords, shownUsers, selectedBoundaries]); }, [trackingRecords, shownUsers, dateRange]);
useEffect(() => {
setSelectedBoundaries([null, null]);
}, [month]);
return ( return (
<Flex <Flex
@@ -193,55 +172,45 @@ const WorkTimeTable = () => {
/> />
<Flex gap={rem(10)}> <Flex gap={rem(10)}>
<DatePickerInput <DatePickerInput
styles={{ placeholder={"Выберите временной промежуток"}
input: {
textAlign: "center",
},
}}
miw={rem(80)}
valueFormat={"DD"}
type={"range"} type={"range"}
minDate={dateBoundaries[0]} value={dateRange}
maxDate={dateBoundaries[1]} onChange={(value) => {
value={selectedBoundaries} setDateRange(value);
onChange={setSelectedBoundaries} }}
placeholder={"Даты"}
/>
<MonthPickerInput
allowDeselect={false}
onChange={event => event && setMonth(event)}
value={month}
placeholder={"Выберите месяц"}
/> />
</Flex> </Flex>
</Flex> </Flex>
<Flex> <Flex>
<BaseTable {dateRange[1] !== null && (
data={data} <BaseTable
columns={columns} data={data}
restProps={ columns={columns}
{ restProps={
enableColumnActions: false, {
enableSorting: false, enableColumnActions: false,
enableRowActions: true, enableSorting: false,
renderRowActions: ({ row }) => ( enableRowActions: true,
<Flex gap="md"> renderRowActions: ({ row }) => (
<Tooltip label="Скрыть"> <Flex gap="md">
<ActionIcon <Tooltip label="Скрыть">
onClick={() => { <ActionIcon
shownUsersHandlers.filter( onClick={() => {
user => user.id !== row.original.userId, shownUsersHandlers.filter(
); user => user.id !== row.original.userId,
}} );
variant={"default"}> }}
<IconEyeOff /> variant={"default"}>
</ActionIcon> <IconEyeOff />
</Tooltip> </ActionIcon>
</Flex> </Tooltip>
), </Flex>
} as MRT_TableOptions<EmployeeData> ),
} } as MRT_TableOptions<EmployeeData>
/> }
/>
)}
</Flex> </Flex>
</Flex> </Flex>
); );

View File

@@ -1,19 +1,27 @@
import { useParams } from "@tanstack/react-router"; import { useParams } from "@tanstack/react-router";
import { CardPageContextProvider, useCardPageContext } from "../../CardsPage/contexts/CardPageContext.tsx"; 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 React, { FC, useEffect } from "react";
import { CardService } from "../../../client"; import { CardService } from "../../../client";
import { useSelector } from "react-redux";
import { RootState } from "../../../redux/store.ts";
export type Props = { export type Props = {
cardId: number; cardId: number;
}; };
const CardPageContent: FC<Props> = ({ cardId }) => { const CardPageContent: FC<Props> = ({ cardId }) => {
const { setSelectedCard } = useCardPageContext(); const { setSelectedCard } = useCardPageContext();
const authState = useSelector((state: RootState) => state.auth);
useEffect(() => { useEffect(() => {
CardService.getCardById({ cardId }).then(card => { if (authState.isAuthorized) {
setSelectedCard(card); CardService.getCardById({ cardId }).then(card => {
}); setSelectedCard(card);
}, []); });
}
}, [authState.isAuthorized]);
return <ProductAndServiceTab />; return <ProductAndServiceTab />;
}; };

View File

@@ -1,19 +1,22 @@
import { ActionIcon, Flex, rem, Text } from "@mantine/core"; import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
import { IconEdit, IconMenu2, IconMenuDeep, IconPlus } from "@tabler/icons-react"; import { IconEdit, IconLogout, IconMenu2, IconMenuDeep, IconPlus } from "@tabler/icons-react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import styles from "../../ui/CardsPage.module.css"; import styles from "../../ui/CardsPage.module.css";
import PageBlock from "../../../../components/PageBlock/PageBlock.tsx"; import PageBlock from "../../../../components/PageBlock/PageBlock.tsx";
import DisplayMode from "../../enums/DisplayMode.ts"; import DisplayMode from "../../enums/DisplayMode.ts";
import { UseFormReturnType } from "@mantine/form"; import { UseFormReturnType } from "@mantine/form";
import { CardsPageState } from "../../hooks/useCardsPageState.tsx"; import { CardsPageState } from "../../hooks/useCardsTableForm.tsx";
import React from "react"; import React from "react";
import ObjectSelect from "../../../../components/ObjectSelect/ObjectSelect.tsx"; import ObjectSelect from "../../../../components/ObjectSelect/ObjectSelect.tsx";
import CardsTableFiltersModal from "../../modals/CardsTableFiltersModal.tsx"; import CardsTableFiltersModal from "../../modals/CardsTableFiltersModal.tsx";
import { useProjectsEditorContext } from "../../contexts/ProjectsEditorContext.tsx"; import { useProjectsEditorContext } from "../../contexts/ProjectsEditorContext.tsx";
import { useSelector } from "react-redux"; 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 { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
import { modals } from "@mantine/modals"; 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 = { type Props = {
displayMode: DisplayMode; displayMode: DisplayMode;
@@ -28,7 +31,9 @@ const CardsPageHeader = ({
}: Props) => { }: Props) => {
const { openProjectsEditor } = useProjectsEditorContext(); const { openProjectsEditor } = useProjectsEditorContext();
const { selectedProject, setSelectedProject, projects, refetchProjects } = useProjectsContext(); 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 isAdmin = userRole === "admin";
const handleCreateClick = () => { 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 = () => { const getHeaderInputsBoard = () => {
return ( return (
<div <div
@@ -66,12 +96,16 @@ const CardsPageHeader = ({
</ActionIcon> </ActionIcon>
</> </>
)} )}
<ObjectSelect {isDealsViewer ? (
placeholder={"Выберите проект"} getLogoutButton()
data={projects} ) : (
value={selectedProject} <ObjectSelect
onChange={setSelectedProject} placeholder={"Выберите проект"}
/> data={projects}
value={selectedProject}
onChange={setSelectedProject}
/>
)}
</div> </div>
); );
}; };

View File

@@ -1,19 +1,27 @@
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx"; import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
import { CardService, CardSummary } from "../../../../client"; import { CardService, CardSummary } from "../../../../client";
import { FC } from "react"; import { FC, useEffect, useRef } from "react";
import useCardsTableColumns from "./columns.tsx"; import useCardsTableColumns from "./columns.tsx";
import { BaseTable } from "../../../../components/BaseTable/BaseTable.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 { IconEdit } from "@tabler/icons-react";
import { MRT_TableOptions } from "mantine-react-table"; import { MRT_TableOptions } from "mantine-react-table";
import { useCardPageContext } from "../../contexts/CardPageContext.tsx"; 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 = { type RestProps = {
viewOnly?: boolean; 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 columns = useCardsTableColumns();
const { setSelectedCard } = useCardPageContext(); const { setSelectedCard } = useCardPageContext();
const onEditClick = (cardSummary: CardSummary) => { 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 ( return (
<BaseTable <Stack>
data={items} <BaseTable
columns={columns} data={summaries}
onSelectionChange={onSelectionChange} columns={columns}
restProps={ onSelectionChange={onSelectionChange}
{ restProps={
enableSorting: true, {
enableColumnActions: false, enableSorting: true,
enablePagination: !viewOnly, enableColumnActions: false,
enableBottomToolbar: !viewOnly, paginationDisplayMode: "pages",
paginationDisplayMode: "pages", enableRowActions: true,
enableRowActions: true, enableRowSelection: true,
enableRowSelection: true, renderRowActions: ({ row }) => (
renderRowActions: ({ row }) => ( <Flex gap="md">
<Flex gap="md"> <Tooltip label="Редактировать">
<Tooltip label="Редактировать"> <ActionIcon
<ActionIcon disabled={viewOnly}
disabled={viewOnly} onClick={() => onEditClick(row.original)}
onClick={() => onEditClick(row.original)} variant={"default"}>
variant={"default"}> <IconEdit />
<IconEdit /> </ActionIcon>
</ActionIcon> </Tooltip>
</Tooltip> </Flex>
</Flex> ),
), } as MRT_TableOptions<CardSummary>
} as MRT_TableOptions<CardSummary> }
} />
/> {!viewOnly && (
<Group justify={"flex-end"}>
<Pagination
withEdges
total={totalPages}
value={page}
onChange={setPage}
/>
</Group>
)}
</Stack>
); );
}; };

View File

@@ -4,7 +4,7 @@ import { CardSchema, CardService } from "../../../client";
type CardPageContextState = { type CardPageContextState = {
selectedCard?: CardSchema; selectedCard?: CardSchema;
setSelectedCard: (card: CardSchema | undefined) => void; setSelectedCard: (card: CardSchema | undefined) => void;
refetchCards: () => Promise<void>; refetchCards: () => void;
refetchCard: () => void; refetchCard: () => void;
}; };
@@ -13,7 +13,7 @@ const CardPageContext = createContext<CardPageContextState | undefined>(
); );
type CardPageContextStateProps = { type CardPageContextStateProps = {
refetchCards: () => Promise<void>; refetchCards: () => void;
defaultCardId?: number; defaultCardId?: number;
} }

View File

@@ -11,26 +11,26 @@ type DndContextState = {
dragState: DragState, dragState: DragState,
onDragStart: (start: DragStart) => void, onDragStart: (start: DragStart) => void,
onDragEnd: (result: DropResult) => Promise<void>, onDragEnd: (result: DropResult) => Promise<void>,
refetchSummaries: () => void;
}; };
const DndContext = createContext<DndContextState | undefined>(undefined); const DndContext = createContext<DndContextState | undefined>(undefined);
type DndContextProps = { type DndContextProps = {
summariesRaw: CardSummary[]; summaries: CardSummary[];
refetchSummaries: () => void; refetchSummaries: () => void;
} }
const useDndContextState = ({ const useDndContextState = ({
summariesRaw, summaries,
refetchSummaries, refetchSummaries,
}: DndContextProps) => { }: DndContextProps) => {
const [dragState, setDragState] = useState<DragState>(DragState.DRAG_ENDED); const [dragState, setDragState] = useState<DragState>(DragState.DRAG_ENDED);
const { const {
summaries,
onCardDragEnd, onCardDragEnd,
} = useCardsDnd({ } = useCardsDnd({
summariesRaw, summaries,
refetchSummaries, refetchSummaries,
}); });
@@ -64,6 +64,7 @@ const useDndContextState = ({
dragState, dragState,
onDragStart, onDragStart,
onDragEnd, onDragEnd,
refetchSummaries,
}; };
}; };

Some files were not shown because too many files have changed in this diff Show More