feat: residues accounting

This commit is contained in:
2025-01-14 21:35:06 +04:00
parent fec6b13972
commit c45d2ac20a
74 changed files with 2994 additions and 28 deletions

View File

@@ -42,8 +42,10 @@ export type { ClientCreateRequest } from './models/ClientCreateRequest';
export type { ClientCreateResponse } from './models/ClientCreateResponse'; export type { ClientCreateResponse } from './models/ClientCreateResponse';
export type { ClientDeleteRequest } from './models/ClientDeleteRequest'; export type { ClientDeleteRequest } from './models/ClientDeleteRequest';
export type { ClientDeleteResponse } from './models/ClientDeleteResponse'; export type { ClientDeleteResponse } from './models/ClientDeleteResponse';
export type { ClientDetailedSchema } from './models/ClientDetailedSchema';
export type { ClientDetailsSchema } from './models/ClientDetailsSchema'; export type { ClientDetailsSchema } from './models/ClientDetailsSchema';
export type { ClientGetAllResponse } from './models/ClientGetAllResponse'; export type { ClientGetAllResponse } from './models/ClientGetAllResponse';
export type { ClientGetResponse } from './models/ClientGetResponse';
export type { ClientSchema } from './models/ClientSchema'; export type { ClientSchema } from './models/ClientSchema';
export type { ClientUpdateDetailsRequest } from './models/ClientUpdateDetailsRequest'; export type { ClientUpdateDetailsRequest } from './models/ClientUpdateDetailsRequest';
export type { ClientUpdateRequest } from './models/ClientUpdateRequest'; export type { ClientUpdateRequest } from './models/ClientUpdateRequest';
@@ -71,6 +73,13 @@ export type { CreatePositionRequest } from './models/CreatePositionRequest';
export type { CreatePositionResponse } from './models/CreatePositionResponse'; export type { CreatePositionResponse } from './models/CreatePositionResponse';
export type { CreatePriceCategoryRequest } from './models/CreatePriceCategoryRequest'; export type { CreatePriceCategoryRequest } from './models/CreatePriceCategoryRequest';
export type { CreatePriceCategoryResponse } from './models/CreatePriceCategoryResponse'; export type { CreatePriceCategoryResponse } from './models/CreatePriceCategoryResponse';
export type { CreateResidualBoxRequest } from './models/CreateResidualBoxRequest';
export type { CreateResidualBoxResponse } from './models/CreateResidualBoxResponse';
export type { CreateResidualPalletRequest } from './models/CreateResidualPalletRequest';
export type { CreateResidualPalletResponse } from './models/CreateResidualPalletResponse';
export type { CreateResidualProductRequest } from './models/CreateResidualProductRequest';
export type { CreateResidualProductResponse } from './models/CreateResidualProductResponse';
export type { CreateResidualProductSchema } from './models/CreateResidualProductSchema';
export type { CreateServiceKitSchema } from './models/CreateServiceKitSchema'; export type { CreateServiceKitSchema } from './models/CreateServiceKitSchema';
export type { CreateServicesKitRequest } from './models/CreateServicesKitRequest'; export type { CreateServicesKitRequest } from './models/CreateServicesKitRequest';
export type { CreateServicesKitResponse } from './models/CreateServicesKitResponse'; export type { CreateServicesKitResponse } from './models/CreateServicesKitResponse';
@@ -163,6 +172,9 @@ export type { DeletePositionRequest } from './models/DeletePositionRequest';
export type { DeletePositionResponse } from './models/DeletePositionResponse'; export type { DeletePositionResponse } from './models/DeletePositionResponse';
export type { DeletePriceCategoryRequest } from './models/DeletePriceCategoryRequest'; export type { DeletePriceCategoryRequest } from './models/DeletePriceCategoryRequest';
export type { DeletePriceCategoryResponse } from './models/DeletePriceCategoryResponse'; export type { DeletePriceCategoryResponse } from './models/DeletePriceCategoryResponse';
export type { DeleteResidualBoxResponse } from './models/DeleteResidualBoxResponse';
export type { DeleteResidualPalletResponse } from './models/DeleteResidualPalletResponse';
export type { DeleteResidualProductResponse } from './models/DeleteResidualProductResponse';
export type { DeleteShiftResponse } from './models/DeleteShiftResponse'; export type { DeleteShiftResponse } from './models/DeleteShiftResponse';
export type { DeleteShippingProductResponse } from './models/DeleteShippingProductResponse'; export type { DeleteShippingProductResponse } from './models/DeleteShippingProductResponse';
export type { DeleteShippingWarehouseRequest } from './models/DeleteShippingWarehouseRequest'; export type { DeleteShippingWarehouseRequest } from './models/DeleteShippingWarehouseRequest';
@@ -214,6 +226,8 @@ export type { GetProfitChartDataRequest } from './models/GetProfitChartDataReque
export type { GetProfitChartDataResponse } from './models/GetProfitChartDataResponse'; export type { GetProfitChartDataResponse } from './models/GetProfitChartDataResponse';
export type { GetProfitTableDataRequest } from './models/GetProfitTableDataRequest'; export type { GetProfitTableDataRequest } from './models/GetProfitTableDataRequest';
export type { GetProfitTableDataResponse } from './models/GetProfitTableDataResponse'; export type { GetProfitTableDataResponse } from './models/GetProfitTableDataResponse';
export type { GetResidualBoxResponse } from './models/GetResidualBoxResponse';
export type { GetResidualPalletResponse } from './models/GetResidualPalletResponse';
export type { GetServiceKitSchema } from './models/GetServiceKitSchema'; export type { GetServiceKitSchema } from './models/GetServiceKitSchema';
export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest'; export type { GetTimeTrackingRecordsRequest } from './models/GetTimeTrackingRecordsRequest';
export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse'; export type { GetTimeTrackingRecordsResponse } from './models/GetTimeTrackingRecordsResponse';
@@ -221,6 +235,8 @@ export type { GetTransactionTagsResponse } from './models/GetTransactionTagsResp
export type { GetWorkShiftsResponse } from './models/GetWorkShiftsResponse'; export type { GetWorkShiftsResponse } from './models/GetWorkShiftsResponse';
export type { GroupBillRequestSchema } from './models/GroupBillRequestSchema'; export type { GroupBillRequestSchema } from './models/GroupBillRequestSchema';
export type { HTTPValidationError } from './models/HTTPValidationError'; export type { HTTPValidationError } from './models/HTTPValidationError';
export type { LoadReceiptRequest } from './models/LoadReceiptRequest';
export type { LoadReceiptResponse } from './models/LoadReceiptResponse';
export type { ManageEmployeeRequest } from './models/ManageEmployeeRequest'; export type { ManageEmployeeRequest } from './models/ManageEmployeeRequest';
export type { ManageEmployeeResponse } from './models/ManageEmployeeResponse'; export type { ManageEmployeeResponse } from './models/ManageEmployeeResponse';
export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema'; export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema';
@@ -242,6 +258,7 @@ export type { PermissionSchema } from './models/PermissionSchema';
export type { PositionSchema } from './models/PositionSchema'; export type { PositionSchema } from './models/PositionSchema';
export type { ProductAddBarcodeRequest } from './models/ProductAddBarcodeRequest'; export type { ProductAddBarcodeRequest } from './models/ProductAddBarcodeRequest';
export type { ProductAddBarcodeResponse } from './models/ProductAddBarcodeResponse'; export type { ProductAddBarcodeResponse } from './models/ProductAddBarcodeResponse';
export type { ProductAndQuantitySchema } from './models/ProductAndQuantitySchema';
export type { ProductCreateRequest } from './models/ProductCreateRequest'; export type { ProductCreateRequest } from './models/ProductCreateRequest';
export type { ProductCreateResponse } from './models/ProductCreateResponse'; export type { ProductCreateResponse } from './models/ProductCreateResponse';
export type { ProductDeleteBarcodeImageResponse } from './models/ProductDeleteBarcodeImageResponse'; export type { ProductDeleteBarcodeImageResponse } from './models/ProductDeleteBarcodeImageResponse';
@@ -262,6 +279,11 @@ export type { ProductUploadImageResponse } from './models/ProductUploadImageResp
export type { ProfitChartDataItem } from './models/ProfitChartDataItem'; export type { ProfitChartDataItem } from './models/ProfitChartDataItem';
export type { ProfitTableDataItem } from './models/ProfitTableDataItem'; export type { ProfitTableDataItem } from './models/ProfitTableDataItem';
export type { ProfitTableGroupBy } from './models/ProfitTableGroupBy'; export type { ProfitTableGroupBy } from './models/ProfitTableGroupBy';
export type { ReceiptBoxSchema } from './models/ReceiptBoxSchema';
export type { ReceiptPalletSchema } from './models/ReceiptPalletSchema';
export type { ResidualBoxSchema } from './models/ResidualBoxSchema';
export type { ResidualPalletSchema } from './models/ResidualPalletSchema';
export type { ResidualProductSchema } from './models/ResidualProductSchema';
export type { RoleSchema } from './models/RoleSchema'; export type { RoleSchema } from './models/RoleSchema';
export type { ServiceCategoryPriceSchema } from './models/ServiceCategoryPriceSchema'; export type { ServiceCategoryPriceSchema } from './models/ServiceCategoryPriceSchema';
export type { ServiceCategoryReorderRequest } from './models/ServiceCategoryReorderRequest'; export type { ServiceCategoryReorderRequest } from './models/ServiceCategoryReorderRequest';
@@ -310,6 +332,9 @@ export type { UpdatePayRateRequest } from './models/UpdatePayRateRequest';
export type { UpdatePayRateResponse } from './models/UpdatePayRateResponse'; export type { UpdatePayRateResponse } from './models/UpdatePayRateResponse';
export type { UpdatePriceCategoryRequest } from './models/UpdatePriceCategoryRequest'; export type { UpdatePriceCategoryRequest } from './models/UpdatePriceCategoryRequest';
export type { UpdatePriceCategoryResponse } from './models/UpdatePriceCategoryResponse'; export type { UpdatePriceCategoryResponse } from './models/UpdatePriceCategoryResponse';
export type { UpdateResidualProductRequest } from './models/UpdateResidualProductRequest';
export type { UpdateResidualProductResponse } from './models/UpdateResidualProductResponse';
export type { UpdateResidualProductSchema } from './models/UpdateResidualProductSchema';
export type { UpdateServiceKitSchema } from './models/UpdateServiceKitSchema'; export type { UpdateServiceKitSchema } from './models/UpdateServiceKitSchema';
export type { UpdateServicesKitRequest } from './models/UpdateServicesKitRequest'; export type { UpdateServicesKitRequest } from './models/UpdateServicesKitRequest';
export type { UpdateServicesKitResponse } from './models/UpdateServicesKitResponse'; export type { UpdateServicesKitResponse } from './models/UpdateServicesKitResponse';
@@ -345,6 +370,7 @@ export { MarketplaceService } from './services/MarketplaceService';
export { PayrollService } from './services/PayrollService'; export { PayrollService } from './services/PayrollService';
export { PositionService } from './services/PositionService'; export { PositionService } from './services/PositionService';
export { ProductService } from './services/ProductService'; export { ProductService } from './services/ProductService';
export { ResiduesService } from './services/ResiduesService';
export { RoleService } from './services/RoleService'; export { RoleService } from './services/RoleService';
export { ServiceService } from './services/ServiceService'; export { ServiceService } from './services/ServiceService';
export { ShippingService } from './services/ShippingService'; export { ShippingService } from './services/ShippingService';

View File

@@ -0,0 +1,19 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BarcodeTemplateSchema } from './BarcodeTemplateSchema';
import type { ClientDetailsSchema } from './ClientDetailsSchema';
import type { ResidualBoxSchema } from './ResidualBoxSchema';
import type { ResidualPalletSchema } from './ResidualPalletSchema';
export type ClientDetailedSchema = {
id: number;
name: string;
companyName: string;
barcodeTemplate?: (BarcodeTemplateSchema | null);
comment?: (string | null);
details?: (ClientDetailsSchema | null);
pallets?: Array<ResidualPalletSchema>;
boxes?: Array<ResidualBoxSchema>;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ClientDetailedSchema } from './ClientDetailedSchema';
export type ClientGetResponse = {
client: ClientDetailedSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateResidualBoxRequest = {
clientId: (number | null);
palletId: (number | null);
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateResidualBoxResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateResidualPalletRequest = {
clientId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateResidualPalletResponse = {
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 { CreateResidualProductSchema } from './CreateResidualProductSchema';
export type CreateResidualProductRequest = {
data: CreateResidualProductSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateResidualProductResponse = {
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 CreateResidualProductSchema = {
productId: (number | null);
quantity: (number | null);
palletId: (number | null);
boxId: (number | null);
};

View File

@@ -7,6 +7,7 @@ export type DealGeneralInfoSchema = {
name: string; name: string;
isDeleted: boolean; isDeleted: boolean;
isCompleted: boolean; isCompleted: boolean;
isAccounted: boolean;
comment: string; comment: string;
shippingWarehouse?: (string | null); shippingWarehouse?: (string | null);
deliveryDate?: (string | null); deliveryDate?: (string | null);

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DeleteResidualBoxResponse = {
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 DeleteResidualPalletResponse = {
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 DeleteResidualProductResponse = {
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 */
import type { ResidualBoxSchema } from './ResidualBoxSchema';
export type GetResidualBoxResponse = {
box: ResidualBoxSchema;
clientId: number;
};

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ResidualPalletSchema } from './ResidualPalletSchema';
export type GetResidualPalletResponse = {
pallet: ResidualPalletSchema;
clientId: number;
};

View File

@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ReceiptBoxSchema } from './ReceiptBoxSchema';
import type { ReceiptPalletSchema } from './ReceiptPalletSchema';
export type LoadReceiptRequest = {
pallets: Array<ReceiptPalletSchema>;
boxes: Array<ReceiptBoxSchema>;
clientId: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type LoadReceiptResponse = {
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 ProductAndQuantitySchema = {
productId: (number | null);
quantity: (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 { ProductAndQuantitySchema } from './ProductAndQuantitySchema';
export type ReceiptBoxSchema = {
products: Array<ProductAndQuantitySchema>;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ProductAndQuantitySchema } from './ProductAndQuantitySchema';
import type { ReceiptBoxSchema } from './ReceiptBoxSchema';
export type ReceiptPalletSchema = {
products: Array<ProductAndQuantitySchema>;
boxes: Array<ReceiptBoxSchema>;
};

View File

@@ -0,0 +1,13 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ResidualProductSchema } from './ResidualProductSchema';
export type ResidualBoxSchema = {
id: number;
createdAt: string;
palletId: (number | null);
clientId: (number | null);
residualProducts: Array<ResidualProductSchema>;
};

View File

@@ -0,0 +1,13 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ResidualBoxSchema } from './ResidualBoxSchema';
import type { ResidualProductSchema } from './ResidualProductSchema';
export type ResidualPalletSchema = {
id: number;
createdAt: string;
boxes: Array<ResidualBoxSchema>;
residualProducts: Array<ResidualProductSchema>;
};

View File

@@ -0,0 +1,13 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ProductSchema } from './ProductSchema';
export type ResidualProductSchema = {
id: number;
quantity: number;
product: ProductSchema;
palletId: (number | null);
boxId: (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 { UpdateResidualProductSchema } from './UpdateResidualProductSchema';
export type UpdateResidualProductRequest = {
data: UpdateResidualProductSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateResidualProductResponse = {
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 UpdateResidualProductSchema = {
productId: (number | null);
quantity: (number | null);
};

View File

@@ -7,6 +7,7 @@ import type { ClientCreateResponse } from '../models/ClientCreateResponse';
import type { ClientDeleteRequest } from '../models/ClientDeleteRequest'; import type { ClientDeleteRequest } from '../models/ClientDeleteRequest';
import type { ClientDeleteResponse } from '../models/ClientDeleteResponse'; import type { ClientDeleteResponse } from '../models/ClientDeleteResponse';
import type { ClientGetAllResponse } from '../models/ClientGetAllResponse'; import type { ClientGetAllResponse } from '../models/ClientGetAllResponse';
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';
@@ -66,6 +67,27 @@ export class ClientService {
url: '/client/get-all', url: '/client/get-all',
}); });
} }
/**
* Get Client
* @returns ClientGetResponse Successful Response
* @throws ApiError
*/
public static getClient({
clientId,
}: {
clientId: number,
}): CancelablePromise<ClientGetResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/client/get/{client_id}',
path: {
'client_id': clientId,
},
errors: {
422: `Validation Error`,
},
});
}
/** /**
* Create Client * Create Client
* @returns ClientCreateResponse Successful Response * @returns ClientCreateResponse Successful Response

View File

@@ -0,0 +1,258 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CreateResidualBoxRequest } from '../models/CreateResidualBoxRequest';
import type { CreateResidualBoxResponse } from '../models/CreateResidualBoxResponse';
import type { CreateResidualPalletRequest } from '../models/CreateResidualPalletRequest';
import type { CreateResidualPalletResponse } from '../models/CreateResidualPalletResponse';
import type { CreateResidualProductRequest } from '../models/CreateResidualProductRequest';
import type { CreateResidualProductResponse } from '../models/CreateResidualProductResponse';
import type { DeleteResidualBoxResponse } from '../models/DeleteResidualBoxResponse';
import type { DeleteResidualPalletResponse } from '../models/DeleteResidualPalletResponse';
import type { DeleteResidualProductResponse } from '../models/DeleteResidualProductResponse';
import type { GetResidualBoxResponse } from '../models/GetResidualBoxResponse';
import type { GetResidualPalletResponse } from '../models/GetResidualPalletResponse';
import type { LoadReceiptRequest } from '../models/LoadReceiptRequest';
import type { LoadReceiptResponse } from '../models/LoadReceiptResponse';
import type { UpdateResidualProductRequest } from '../models/UpdateResidualProductRequest';
import type { UpdateResidualProductResponse } from '../models/UpdateResidualProductResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class ResiduesService {
/**
* Get Pallet
* @returns GetResidualPalletResponse Successful Response
* @throws ApiError
*/
public static getResidualPallet({
palletId,
}: {
palletId: number,
}): CancelablePromise<GetResidualPalletResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/residues/pallet/{pallet_id}',
path: {
'pallet_id': palletId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Pallet
* @returns DeleteResidualPalletResponse Successful Response
* @throws ApiError
*/
public static deleteResidualPallet({
palletId,
}: {
palletId: number,
}): CancelablePromise<DeleteResidualPalletResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/residues/pallet/{pallet_id}',
path: {
'pallet_id': palletId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Create Pallet
* @returns CreateResidualPalletResponse Successful Response
* @throws ApiError
*/
public static createResidualPallet({
requestBody,
}: {
requestBody: CreateResidualPalletRequest,
}): CancelablePromise<CreateResidualPalletResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/residues/pallet',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Create Residual Product
* @returns CreateResidualProductResponse Successful Response
* @throws ApiError
*/
public static createResidualProduct({
requestBody,
}: {
requestBody: CreateResidualProductRequest,
}): CancelablePromise<CreateResidualProductResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/residues/product',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Update Residual Product
* @returns UpdateResidualProductResponse Successful Response
* @throws ApiError
*/
public static updateResidualProduct({
residualProductId,
requestBody,
}: {
residualProductId: number,
requestBody: UpdateResidualProductRequest,
}): CancelablePromise<UpdateResidualProductResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/residues/product/{residual_product_id}',
path: {
'residual_product_id': residualProductId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Residual Product
* @returns DeleteResidualProductResponse Successful Response
* @throws ApiError
*/
public static deleteResidualProduct({
residualProductId,
}: {
residualProductId: number,
}): CancelablePromise<DeleteResidualProductResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/residues/product/{residual_product_id}',
path: {
'residual_product_id': residualProductId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Box
* @returns GetResidualBoxResponse Successful Response
* @throws ApiError
*/
public static getResidualBox({
boxId,
}: {
boxId: number,
}): CancelablePromise<GetResidualBoxResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/residues/box/{box_id}',
path: {
'box_id': boxId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Box
* @returns DeleteResidualBoxResponse Successful Response
* @throws ApiError
*/
public static deleteResidualBox({
boxId,
}: {
boxId: number,
}): CancelablePromise<DeleteResidualBoxResponse> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/residues/box/{box_id}',
path: {
'box_id': boxId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Create Box
* @returns CreateResidualBoxResponse Successful Response
* @throws ApiError
*/
public static createResidualBox({
requestBody,
}: {
requestBody: CreateResidualBoxRequest,
}): CancelablePromise<CreateResidualBoxResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/residues/box',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Receipt
* @returns LoadReceiptResponse Successful Response
* @throws ApiError
*/
public static receipt({
requestBody,
}: {
requestBody: LoadReceiptRequest,
}): CancelablePromise<LoadReceiptResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/residues/receipt',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Generate Pdf
* @returns any Successful Response
* @throws ApiError
*/
public static getPdf({
palletIds = '',
boxIds = '',
}: {
palletIds?: string,
boxIds?: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/residues/pdf/',
query: {
'pallet_ids': palletIds,
'box_ids': boxIds,
},
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -0,0 +1,23 @@
import { ReactNode } from "react";
import { Button, ButtonProps, Group } from "@mantine/core";
interface Props extends ButtonProps {
children?: ReactNode;
onClick?: () => void;
}
const InlineButton = ({ children, onClick, ...props }: Props) => {
return (
<Button
variant="default"
onClick={onClick}
{...props}
>
<Group gap="sm">
{children}
</Group>
</Button>
);
};
export default InlineButton;

View File

@@ -7,7 +7,7 @@ import {
IconCash, IconChartDots, IconCash, IconChartDots,
IconDashboard, IconDashboard,
IconFileBarcode, IconFileBarcode,
IconHome2, IconHome2, IconHomeEdit,
IconLogout, IconLogout,
IconMan, IconMan,
IconMoon, IconMoon,
@@ -94,6 +94,11 @@ const mockdata = [
label: "Маркетплейсы", label: "Маркетплейсы",
href: "/marketplaces", href: "/marketplaces",
}, },
{
icon: IconHomeEdit,
label: "Остатки",
href: "/residues",
},
]; ];
const adminMockdata = [ const adminMockdata = [

View File

@@ -27,3 +27,7 @@
.container-full-height-fixed { .container-full-height-fixed {
height: calc(100vh - (rem(20) * 2)); height: calc(100vh - (rem(20) * 2));
} }
.container-no-border-radius {
border-radius: 0 !important;
}

View File

@@ -8,6 +8,7 @@ type Props = {
style?: CSSProperties; style?: CSSProperties;
fullHeight?: boolean; fullHeight?: boolean;
fullHeightFixed?: boolean; fullHeightFixed?: boolean;
noBorderRadius?: boolean;
}; };
export const PageBlock: FC<Props> = ({ export const PageBlock: FC<Props> = ({
children, children,
@@ -15,6 +16,7 @@ export const PageBlock: FC<Props> = ({
fluid = true, fluid = true,
fullHeight = false, fullHeight = false,
fullHeightFixed = false, fullHeightFixed = false,
noBorderRadius = false,
}) => { }) => {
return ( return (
<div <div
@@ -23,7 +25,8 @@ export const PageBlock: FC<Props> = ({
styles["container"], styles["container"],
fluid && styles["container-fluid"], fluid && styles["container-fluid"],
fullHeight && styles["container-full-height"], fullHeight && styles["container-full-height"],
fullHeightFixed && styles["container-full-height-fixed"] fullHeightFixed && styles["container-full-height-fixed"],
noBorderRadius && styles["container-no-border-radius"]
)}> )}>
{children} {children}
</div> </div>

View File

@@ -7,7 +7,8 @@ import AddDealServiceModal from "../pages/LeadsPage/modals/AddDealServiceModal.t
import AddDealProductModal from "../pages/LeadsPage/modals/AddDealProductModal.tsx"; import AddDealProductModal from "../pages/LeadsPage/modals/AddDealProductModal.tsx";
import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx"; import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx";
import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx"; import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
import BarcodeTemplateFormModal from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx"; import BarcodeTemplateFormModal
from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx"; import ProductServiceFormModal from "../pages/LeadsPage/modals/ProductServiceFormModal.tsx";
import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx"; import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx";
import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx"; import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx";
@@ -29,6 +30,9 @@ import DepartmentModal from "../pages/AdminPage/tabs/OrganizationalStructureTab/
import AddUserToDepartmentModal import AddUserToDepartmentModal
from "../pages/AdminPage/tabs/OrganizationalStructureTab/modals/AddUserToDepartmentModal.tsx"; from "../pages/AdminPage/tabs/OrganizationalStructureTab/modals/AddUserToDepartmentModal.tsx";
import AssignUserModal from "../pages/LeadsPage/tabs/EmployeesTab/modals/AssignUserModal.tsx"; import AssignUserModal from "../pages/LeadsPage/tabs/EmployeesTab/modals/AssignUserModal.tsx";
import ResidualProductModal from "../pages/ResiduesPage/modals/ResidualProductModal/ResidualProductModal.tsx";
import NewReceiptModal from "../pages/ReceiptPage/components/NewReceipt/modals/NewReceiptModal.tsx";
import ReceiptModal from "../pages/ReceiptPage/components/ReceiptEditing/modals/ReceiptModal.tsx";
export const modals = { export const modals = {
enterDeadline: EnterDeadlineModal, enterDeadline: EnterDeadlineModal,
@@ -58,6 +62,9 @@ export const modals = {
transactionFormModal: TransactionFormModal, transactionFormModal: TransactionFormModal,
transactionTagsModal: TransactionTagsModal, transactionTagsModal: TransactionTagsModal,
shippingProductModal: ShippingProductModal, shippingProductModal: ShippingProductModal,
residualProductModal: ResidualProductModal,
newReceiptModal: NewReceiptModal,
receiptModal: ReceiptModal,
departmentModal: DepartmentModal, departmentModal: DepartmentModal,
addUserToDepartmentModal: AddUserToDepartmentModal, addUserToDepartmentModal: AddUserToDepartmentModal,
assignUserModal: AssignUserModal, assignUserModal: AssignUserModal,

View File

@@ -1,9 +1,9 @@
import ShippingTree from "./components/ShippingTree.tsx"; import ShippingTree from "./components/ShippingTree.tsx";
import { Button, Group, rem, ScrollArea, Stack } from "@mantine/core"; import { Group, ScrollArea, Stack } from "@mantine/core";
import useShipping from "./hooks/useShipping.tsx"; import useShipping from "./hooks/useShipping.tsx";
import useShippingQrs from "./hooks/useShippingQrs.tsx"; import useShippingQrs from "./hooks/useShippingQrs.tsx";
import { IconPrinter, IconQrcode } from "@tabler/icons-react"; import { IconPrinter, IconQrcode } from "@tabler/icons-react";
import { ReactNode } from "react"; import InlineButton from "../../../../components/InlineButton/InlineButton.tsx";
const ShippingTab = () => { const ShippingTab = () => {
@@ -18,28 +18,27 @@ const ShippingTab = () => {
onGetBoxesPdfClick, onGetBoxesPdfClick,
} = useShippingQrs(); } = useShippingQrs();
const getButton = (label: string, func: () => void, icon?: ReactNode) => {
return (
<Button
variant={"default"}
onClick={func}
>
<Group gap={rem(10)}>
{icon}
{label}
</Group>
</Button>
);
};
return ( return (
<Stack h={"94vh"}> <Stack h={"94vh"}>
<Group> <Group>
{getButton("Добавить паллет", onCreatePalletClick)} <InlineButton onClick={() => onCreatePalletClick()}>
{getButton("Добавить короб", onCreateBoxInDealClick)} Добавить паллет
{getButton("Сделка", onGetDealQrPdfClick, <IconQrcode />)} </InlineButton>
{getButton("Паллеты", onGetPalletsPdfClick, <IconPrinter />)} <InlineButton onClick={() => onCreateBoxInDealClick()}>
{getButton("Короба", onGetBoxesPdfClick, <IconPrinter />)} Добавить короб
</InlineButton>
<InlineButton onClick={() => onGetDealQrPdfClick()}>
<IconQrcode />
Сделка
</InlineButton>
<InlineButton onClick={() => onGetPalletsPdfClick()}>
<IconPrinter />
Паллеты
</InlineButton>
<InlineButton onClick={() => onGetBoxesPdfClick()}>
<IconPrinter />
Короба
</InlineButton>
</Group> </Group>
<ScrollArea> <ScrollArea>
<ShippingTree /> <ShippingTree />

View File

@@ -9,4 +9,8 @@
.container { .container {
padding: rem(20); padding: rem(20);
@media only screen and (max-width: 768px) {
padding: 0 !important;
}
} }

View File

@@ -12,22 +12,26 @@ export type Props = {
const PageWrapper: FC<Props> = ({ children }) => { const PageWrapper: FC<Props> = ({ children }) => {
const authState = useSelector((state: RootState) => state.auth); const authState = useSelector((state: RootState) => state.auth);
const uiState = useSelector((state: RootState) => state.ui); const uiState = useSelector((state: RootState) => state.ui);
const navbarNeeded = authState.isAuthorized && !authState.isGuest;
return ( return (
<AppShell <AppShell
layout={"alt"} layout={"alt"}
withBorder={false} withBorder={false}
navbar={ navbar={
authState.isAuthorized && !authState.isGuest navbarNeeded
? { ? {
width: "130px", width: "130px",
breakpoint: "sm", breakpoint: "sm",
collapsed: { desktop: uiState.hideNavbar }, collapsed: { desktop: uiState.hideNavbar, mobile: true },
} }
: undefined : undefined
}> }>
<AppShell.Header>
</AppShell.Header>
<AppShell.Navbar> <AppShell.Navbar>
{authState.isAuthorized && !authState.isGuest && ( {navbarNeeded && (
<Flex <Flex
className={styles["main-container"]} className={styles["main-container"]}
h={"100%"} h={"100%"}

View File

@@ -0,0 +1,71 @@
import { Accordion, ActionIcon, Center } from "@mantine/core";
import { IconBox, IconPlus, IconTrash } from "@tabler/icons-react";
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
import ReceiptProducts from "./ReceiptProducts.tsx";
type Props = {
pallet?: ReceiptPallet;
palletIdx?: number;
}
const AccordionBoxes = ({ pallet, palletIdx }: Props) => {
const { boxes, boxesHandlers, onObjectEditClick, palletsHandlers, setBoxData } = useReceiptContext();
const deleteBox = (boxId: number) => {
if (palletIdx && pallet) {
palletsHandlers.setItemProp(palletIdx, "boxes", pallet.boxes.filter(box => box.id !== boxId));
} else {
boxesHandlers.filter(item => item.id !== boxId);
}
};
const boxActions = (box: ReceiptBox) => {
return (
<>
<ActionIcon
variant={"default"}
onClick={() => onObjectEditClick(box, true, pallet)}
mr={"sm"}
>
<IconPlus />
</ActionIcon>
<ActionIcon
variant={"default"}
onClick={() => deleteBox(box.id)}
mr={"sm"}
>
<IconTrash />
</ActionIcon>
</>
);
};
const boxesData = pallet ? pallet.boxes : boxes;
if (boxesData.length === 0) return;
return (
<Accordion multiple={true} bd={"solid 1px gray"}>
{boxesData.map(box => (
<Accordion.Item key={box.id} value={box.id.toString()}>
<Center>
<Accordion.Control icon={<IconBox />}>
Короб {box.id}
</Accordion.Control>
{boxActions(box)}
</Center>
<Accordion.Panel>
<ReceiptProducts
products={box.residualProducts}
object={box}
setObjectData={(box: ReceiptBox) => setBoxData(box, pallet)}
/>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
);
};
export default AccordionBoxes;

View File

@@ -0,0 +1,117 @@
import { Accordion, ActionIcon, Box, Button, Center, Flex, Group } from "@mantine/core";
import { IconPlus, IconSpace, IconTrash } from "@tabler/icons-react";
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
import AccordionBoxes from "./AccordionBoxes.tsx";
import ReceiptProducts from "./ReceiptProducts.tsx";
const AccordionPallets = () => {
const { pallets, palletsHandlers, nextId, onObjectEditClick, setPalletData } = useReceiptContext();
const deletePallet = (palletId: number) => {
palletsHandlers.filter(item => item.id !== palletId);
};
const createBox = (pallet: ReceiptPallet) => {
const box = {
id: nextId(),
residualProducts: [],
} as ReceiptBox;
const palletIdx = pallets.findIndex(p => p.id === pallet.id);
palletsHandlers.setItemProp(palletIdx, "boxes", [box, ...pallets[palletIdx].boxes]);
};
const palletActions = (pallet: ReceiptPallet) => {
const isCreateButtonVisible = pallet.boxes.length > 0 || pallet.residualProducts.length > 0;
return (
<>
{isCreateButtonVisible && <ActionIcon
variant={"default"}
onClick={() => {
if (pallet.residualProducts.length > 0) {
onObjectEditClick(pallet, false);
} else {
createBox(pallet);
}
}}
mr={"sm"}
>
<IconPlus />
</ActionIcon>}
<ActionIcon
variant={"default"}
onClick={() => deletePallet(pallet.id)}
mr={"md"}
>
<IconTrash />
</ActionIcon>
</>
);
};
const palletButton = (label: string, onClick: () => void) => {
return (
<Button
variant={"default"}
onClick={onClick}
flex={1}
>
<Group gap={"md"}>
<IconPlus />
{label}
</Group>
</Button>
);
};
const palletButtons = (pallet: ReceiptPallet) => {
if (pallet.boxes.length > 0 || pallet.residualProducts.length > 0) {
return;
}
return (
<Flex gap={"md"}>
{palletButton("Короб", () => createBox(pallet))}
{palletButton("Товар", () => onObjectEditClick(pallet, false))}
</Flex>
);
};
const palletItem = (pallet: ReceiptPallet, palletIdx: number) => {
return (
<Accordion.Item key={pallet.id} value={pallet.id.toString()}>
<Center>
<Accordion.Control icon={<IconSpace />}>
Паллет {pallet.id}
</Accordion.Control>
{palletActions(pallet)}
</Center>
<Accordion.Panel>
{palletButtons(pallet)}
{pallet.boxes &&
<Box pt={"md"}>
<AccordionBoxes pallet={pallet} palletIdx={palletIdx} />
</Box>
}
{pallet.residualProducts && (
<ReceiptProducts
products={pallet.residualProducts}
object={pallet}
setObjectData={object => setPalletData(object as ReceiptPallet)}
/>
)}
</Accordion.Panel>
</Accordion.Item>
);
};
if (pallets.length === 0) return;
return (
<Accordion multiple={true} bd={"solid 1px gray"}>
{pallets.map(((pallet, idx) => palletItem(pallet, idx)))}
</Accordion>
);
};
export default AccordionPallets;

View File

@@ -0,0 +1,64 @@
import { Button, Group } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
import { type ProductAndQuantitySchema, ReceiptBoxSchema, ReceiptPalletSchema, ResiduesService } from "../../../../../client";
import { ReceiptBox, ReceiptPallet, ReceiptProduct } from "../types/types.tsx";
import { notifications } from "../../../../../shared/lib/notifications.ts";
const FinishReceiptButton = () => {
const { pallets, boxes, client, reset } = useReceiptContext();
const finishReceiptDisabled = pallets.length === 0 && boxes.length === 0;
const processProducts = (products: ReceiptProduct[]): ProductAndQuantitySchema[] => {
return products.map(p => ({
productId: p.product.id,
quantity: p.quantity,
}));
};
const processBoxes = (boxes: ReceiptBox[]): ReceiptBoxSchema[] => {
return boxes.map(box => ({
products: processProducts(box.residualProducts),
}));
};
const processPallets = (pallets: ReceiptPallet[]): ReceiptPalletSchema[] => {
return pallets.map(pallet => ({
products: processProducts(pallet.residualProducts),
boxes: processBoxes(pallet.boxes),
}));
};
const finishReceipt = () => {
if (!client) return;
ResiduesService.receipt({
requestBody: {
clientId: client.id,
pallets: processPallets(pallets),
boxes: processBoxes(boxes),
},
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
if (!ok) return;
reset();
})
.catch(err => console.log(err));
};
return (
<Button
variant="default"
onClick={finishReceipt}
disabled={finishReceiptDisabled}
>
<Group gap={"sm"}>
<IconCheck />
Завершить приемку
</Group>
</Button>
);
};
export default FinishReceiptButton;

View File

@@ -0,0 +1,49 @@
import { Flex, Stack } from "@mantine/core";
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
import { ReceiptBox } from "../types/types.tsx";
import AccordionBoxes from "./AccordionBoxes.tsx";
import AccordionPallets from "./AccordionPallets.tsx";
import InlineButton from "../../../../../components/InlineButton/InlineButton.tsx";
import { IconPlus } from "@tabler/icons-react";
const ReceiptEditor = () => {
const { fixed, palletsHandlers, boxesHandlers, nextId } = useReceiptContext();
if (!fixed) return;
const createPallet = () => {
const pallet = {
id: nextId(),
boxes: [],
residualProducts: [],
};
palletsHandlers.prepend(pallet);
};
const createBox = () => {
const box = {
id: nextId(),
residualProducts: [],
} as ReceiptBox;
boxesHandlers.prepend(box);
};
return (
<Stack>
<Flex gap="md">
<InlineButton onClick={() => createPallet()} flex={1}>
<IconPlus />
Паллет
</InlineButton>
<InlineButton onClick={() => createBox()} flex={1}>
<IconPlus />
Короб
</InlineButton>
</Flex>
<AccordionBoxes />
<AccordionPallets />
</Stack>
);
};
export default ReceiptEditor;

View File

@@ -0,0 +1,63 @@
import { ReceiptBox, ReceiptPallet, ReceiptProduct } from "../types/types.tsx";
import { ActionIcon, Divider, Group, NumberInput, rem, Stack, Text } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
type Props = {
products: ReceiptProduct[];
object: ReceiptBox | ReceiptPallet;
setObjectData: (object: ReceiptBox | ReceiptPallet) => void;
}
const ReceiptProducts = ({ products, object, setObjectData }: Props) => {
if (products.length === 0) return;
const deleteProduct = (productId: number) => {
object.residualProducts = object.residualProducts.filter(product => product.id !== productId);
setObjectData({ ...object });
};
const productQuantityInput = (receiptProduct: ReceiptProduct) => {
return (
<Group>
<Text>Количество:</Text>
<NumberInput
value={receiptProduct.quantity}
onChange={value => {
const idx = object.residualProducts.findIndex(p => p.id === receiptProduct.id);
object.residualProducts[idx].quantity = Number(value);
setObjectData({ ...object });
}}
min={1}
allowDecimal={false}
allowNegative={false}
w={rem(100)}
/>
</Group>
);
};
return (
<Stack>
{products.map((receiptProduct: ReceiptProduct) => (
<Stack key={receiptProduct.id}>
<Divider />
<Group wrap={"nowrap"} justify={"space-between"}>
<Stack gap={"sm"} key={`Product-${receiptProduct.id}`}>
<Text>Товар: {receiptProduct.product.name}</Text>
{productQuantityInput(receiptProduct)}
{receiptProduct.product.size && <Text>Размер: {receiptProduct.product.size}</Text>}
</Stack>
<ActionIcon
variant={"default"}
onClick={() => deleteProduct(receiptProduct.id)}
>
<IconTrash />
</ActionIcon>
</Group>
</Stack>
))}
</Stack>
);
};
export default ReceiptProducts;

View File

@@ -0,0 +1,88 @@
import { useReceiptContext } from "../contexts/ReceiptContext.tsx";
import ClientSelectNew from "../../../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
import { Button, Flex, Group, Stack, Text } from "@mantine/core";
import { modals } from "@mantine/modals";
import { IconArrowLeft, IconX } from "@tabler/icons-react";
import FinishReceiptButton from "./FinishReceiptButton.tsx";
const ReceiptUserSelect = () => {
const {
client,
setClient,
fixed,
setFixed,
palletsHandlers,
boxesHandlers,
} = useReceiptContext();
const confirmModalText = (
<Stack>
<Text size="sm">Вы уверены, что хотите отменить приемку?</Text>
<Text size="sm">Все загруженные паллеты, короба и товары удалятся.</Text>
</Stack>
);
const unfix = () => {
modals.openConfirmModal({
title: "Отмена приемки",
children: confirmModalText,
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => {
palletsHandlers.setState([]);
boxesHandlers.setState([]);
setFixed(false);
},
});
};
return (
<Stack>
<Flex gap={"md"} flex={8}>
<Button
variant={"default"}
onClick={() => window.location.reload()}
flex={1}
disabled={fixed}
>
<IconArrowLeft />
</Button>
<ClientSelectNew
placeholder={"Выберите клиента"}
value={client}
onChange={setClient}
disabled={fixed}
flex={7}
/>
</Flex>
{
fixed ? (
<>
<Button
variant={"default"}
onClick={unfix}
>
<Group gap={"sm"}>
<IconX />
Отменить приемку
</Group>
</Button>
<FinishReceiptButton />
</>
) : (
<Button
variant={"default"}
onClick={() => setFixed(true)}
disabled={!client}
>
Выбрать
</Button>
)
}
</Stack>
);
};
export default ReceiptUserSelect;

View File

@@ -0,0 +1,138 @@
import { createContext, Dispatch, FC, SetStateAction, useContext, useState } from "react";
import { ClientSchema } from "../../../../../client";
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
import { modals } from "@mantine/modals";
import nextId from "../utils/nextId.ts";
import { useListState, UseListStateHandlers } from "@mantine/hooks";
type ReceiptContextState = {
client?: ClientSchema;
setClient: Dispatch<SetStateAction<ClientSchema | undefined>>;
fixed: boolean;
setFixed: Dispatch<SetStateAction<boolean>>;
pallets: ReceiptPallet[];
palletsHandlers: UseListStateHandlers<ReceiptPallet>;
boxes: ReceiptBox[];
boxesHandlers: UseListStateHandlers<ReceiptBox>;
nextId: () => number;
onObjectEditClick: (object: ReceiptBox | ReceiptPallet, isBox: boolean, parentPallet?: ReceiptPallet) => void;
reset: () => void;
setBoxData: (box: ReceiptBox, pallet?: ReceiptPallet) => void;
setPalletData: (pallet: ReceiptPallet) => void;
};
const ReceiptContext = createContext<ReceiptContextState | undefined>(
undefined,
);
const useReceiptContextState = () => {
const [client, setClient] = useState<ClientSchema>();
const [fixed, setFixed] = useState<boolean>(false);
const [pallets, palletsHandlers] = useListState<ReceiptPallet>([]);
const [boxes, boxesHandlers] = useListState<ReceiptBox>([]);
const setBoxData = (box: ReceiptBox, pallet?: ReceiptPallet) => {
if (pallet) {
setBoxOnPalletData(box, pallet);
} else {
setBoxWithoutPalletData(box);
}
};
const setBoxWithoutPalletData = (box: ReceiptBox) => {
const boxIdx = boxes.findIndex(item => item.id === box.id);
if (boxIdx === -1) {
boxesHandlers.append(box);
} else {
boxesHandlers.setItem(boxIdx, box);
}
};
const setBoxOnPalletData = (box: ReceiptBox, pallet: ReceiptPallet) => {
const palletIdx = pallets.findIndex(p => p.id === pallet.id);
const boxIdx = pallets[palletIdx].boxes.findIndex(b => b.id === box.id);
if (boxIdx === -1) {
palletsHandlers.setItemProp(palletIdx, "boxes", [box, ...boxes]);
} else {
pallets[palletIdx].boxes[boxIdx] = box;
palletsHandlers.setState(pallets);
}
palletsHandlers.setState([...pallets]);
};
const setPalletData = (pallet: ReceiptPallet) => {
const palletIdx = pallets.findIndex(p => p.id === pallet.id);
if (palletIdx !== -1) {
palletsHandlers.setItem(palletIdx, pallet);
palletsHandlers.setState([...pallets]);
}
};
const onObjectEditClick = (
object: ReceiptBox | ReceiptPallet,
isBox: boolean,
parentPallet?: ReceiptPallet,
) => {
if (!client) return;
modals.openContextModal({
modal: "newReceiptModal",
title: "Добавление товара",
withCloseButton: false,
innerProps: {
clientId: client.id,
object: object,
setObjectData: (object: ReceiptBox | ReceiptPallet) => {
if (isBox) setBoxData(object, parentPallet);
setPalletData(object as ReceiptPallet);
},
},
});
};
const reset = () => {
palletsHandlers.setState([]);
boxesHandlers.setState([]);
setFixed(false);
setClient(undefined);
};
return {
client,
setClient,
fixed,
setFixed,
pallets,
palletsHandlers,
boxes,
boxesHandlers,
nextId,
onObjectEditClick,
reset,
setBoxData,
setPalletData,
};
};
type ReceiptContextProviderProps = {
children: React.ReactNode;
};
export const ReceiptContextProvider: FC<ReceiptContextProviderProps> = ({ children }) => {
const state = useReceiptContextState();
return (
<ReceiptContext.Provider value={state}>
{children}
</ReceiptContext.Provider>
);
};
export const useReceiptContext = () => {
const context = useContext(ReceiptContext);
if (!context) {
throw new Error(
"useReceiptContext must be used within a ReceiptContextProvider",
);
}
return context;
};

View File

@@ -0,0 +1,126 @@
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import { Button, Divider, Flex, NumberInput, rem, Stack } from "@mantine/core";
import ProductSelect from "../../../../../components/ProductSelect/ProductSelect.tsx";
import { ReceiptBox, ReceiptPallet } from "../types/types.tsx";
import { ResidualModalForm } from "../../../../ResiduesPage/types/ResidualProductData.tsx";
import nextId from "../utils/nextId.ts";
import useScanning from "../../../hooks/useScanning.tsx";
import ScanBarcode from "../../ScanBarcode/ScanBarcode.tsx";
import { ProductSchema } from "../../../../../client";
type Props = {
clientId: number;
object: ReceiptBox | ReceiptPallet;
setObjectData: (object: ReceiptBox | ReceiptPallet) => void;
}
const NewReceiptModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const {
clientId,
setObjectData,
object,
} = innerProps;
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
const initialValues = {
product: null,
quantity: 1,
};
const form = useForm<ResidualModalForm>({
initialValues,
validate: {
product: product => !product && "Необходимо выбрать товар",
quantity: quantity => (!quantity || quantity === 0) && "Слишком мало товара",
},
});
const onSubmit = () => {
const productIdx = object.residualProducts.findIndex(p => p.product.id === form.values.product?.id);
if (productIdx !== -1) {
object.residualProducts[productIdx].quantity += form.values.quantity;
} else {
const id = nextId();
const receiptProduct = {
id,
product: form.values.product!,
quantity: form.values.quantity,
};
object.residualProducts.unshift(receiptProduct);
}
setObjectData(object);
form.reset();
context.closeContextModal(id);
};
const onProductAfterScanningSelect = (product?: ProductSchema) => {
if (!product) {
setScannedValue("");
return;
}
const productIdx = object.residualProducts.findIndex(p => p.product.id === product.id);
if (productIdx === -1) {
form.setFieldValue("product", product);
} else {
object.residualProducts[productIdx].quantity++;
setObjectData({ ...object });
context.closeContextModal(id);
}
setScannedValue("");
};
return (
<Flex
direction={"column"}
gap={rem(10)}
>
<form onSubmit={form.onSubmit(() => onSubmit())}>
<Stack gap={rem(10)}>
<Divider />
<ProductSelect
label={"Товар"}
placeholder={"Выберите товар"}
{...form.getInputProps("product")}
clientId={clientId}
disabled={isScanning}
/>
<NumberInput
label={"Количество"}
hideControls
{...form.getInputProps("quantity")}
min={1}
defaultValue={1}
disabled={isScanning}
/>
<Button
variant={"default"}
type={"submit"}
disabled={isScanning}
>
Добавить
</Button>
<Divider />
</Stack>
</form>
<ScanBarcode
clientId={clientId}
isScanning={isScanning}
setIsScanning={setIsScanning}
onProductSelect={onProductAfterScanningSelect}
scannedValue={scannedValue}
object={object}
/>
</Flex>
);
};
export default NewReceiptModal;

View File

@@ -0,0 +1,18 @@
import { ProductSchema } from "../../../../../client";
export type ReceiptProduct = {
id: number;
quantity: number;
product: ProductSchema;
}
export type ReceiptBox = {
id: number;
residualProducts: ReceiptProduct[];
}
export type ReceiptPallet = {
id: number;
residualProducts: ReceiptProduct[];
boxes: ReceiptBox[];
}

View File

@@ -0,0 +1,5 @@
let id = 1;
export default function nextId() {
return id++;
}

View File

@@ -0,0 +1,89 @@
import { Accordion, ActionIcon, Center } from "@mantine/core";
import { IconBox, IconPlus, IconTrash } from "@tabler/icons-react";
import ReceiptProducts from "./ReceiptProducts.tsx";
import { ResidualBoxSchema, ResidualPalletSchema, ResiduesService } from "../../../../../client";
import { modals } from "@mantine/modals";
import { notifications } from "../../../../../shared/lib/notifications.ts";
type Props = {
pallet: ResidualPalletSchema;
clientId: number;
fetchPallet: () => void;
}
const AccordionBoxesOnPallet = ({ pallet, clientId, fetchPallet }: Props) => {
const deleteBox = (boxId: number) => {
ResiduesService.deleteResidualBox({
boxId,
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
fetchPallet();
})
.catch(err => console.log(err));
};
const onCreateProductClick = (box: ResidualBoxSchema) => {
if (!(box && clientId)) return;
modals.openContextModal({
modal: "receiptModal",
title: "Добавление товара",
withCloseButton: false,
innerProps: {
clientId,
isBox: true,
object: box,
fetchObject: fetchPallet,
},
});
};
const boxActions = (box: ResidualBoxSchema) => {
return (
<>
<ActionIcon
variant={"default"}
onClick={() => onCreateProductClick(box)}
mr={"sm"}
>
<IconPlus />
</ActionIcon>
<ActionIcon
variant={"default"}
onClick={() => deleteBox(box.id)}
mr={"sm"}
>
<IconTrash />
</ActionIcon>
</>
);
};
if (pallet.boxes.length === 0) return;
return (
<Accordion multiple={true} bd={"solid 1px gray"}>
{pallet.boxes.map(box => (
<Accordion.Item key={box.id} value={box.id.toString()}>
<Center>
<Accordion.Control icon={<IconBox />}>
Короб {box.id}
</Accordion.Control>
{boxActions(box)}
</Center>
<Accordion.Panel>
<ReceiptProducts
clientId={clientId}
object={box}
updateObject={fetchPallet}
/>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
);
};
export default AccordionBoxesOnPallet;

View File

@@ -0,0 +1,46 @@
import { Group, NumberInput, rem, Text } from "@mantine/core";
import { ResidualProductSchema, ResiduesService } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts";
type Props = {
residualProduct: ResidualProductSchema;
updateObject: () => void;
}
const ProductQuantityField = ({ residualProduct, updateObject }: Props) => {
const updateProduct = (quantity: number) => {
if (residualProduct.quantity === quantity) return;
ResiduesService.updateResidualProduct({
requestBody: {
data: {
quantity,
productId: residualProduct.product.id,
},
},
residualProductId: residualProduct.id,
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
updateObject();
})
.catch(err => console.log(err));
};
return (
<Group>
<Text>Количество: </Text>
<NumberInput
value={residualProduct.quantity}
onChange={(value) => updateProduct(Number(value))}
min={1}
allowDecimal={false}
allowNegative={false}
w={rem(100)}
/>
</Group>
);
};
export default ProductQuantityField;

View File

@@ -0,0 +1,50 @@
import { Button, Flex, Stack, Title } from "@mantine/core";
import useReceiptBox from "../hooks/useReceiptBox.tsx";
import ReceiptProducts from "./ReceiptProducts.tsx";
import InlineButton from "../../../../../components/InlineButton/InlineButton.tsx";
import { IconArrowLeft, IconPlus } from "@tabler/icons-react";
type Props = {
boxId: number;
}
const ReceiptBoxEditor = ({ boxId }: Props) => {
const {
box,
fetchBox,
clientId,
onCreateProductClick,
} = useReceiptBox({ boxId });
const backButton = (
<Button
variant={"default"}
onClick={() => window.location.reload()}
flex={1}
>
<IconArrowLeft />
</Button>
);
return (
<Stack>
<Flex align={"center"} flex={9} gap={"md"}>
{backButton}
<Title flex={8} order={3}>
Короб ID: К{box?.id}
</Title>
</Flex>
<InlineButton onClick={onCreateProductClick}>
<IconPlus />
Товар
</InlineButton>
<ReceiptProducts
object={box}
clientId={clientId}
updateObject={fetchBox}
/>
</Stack>
);
};
export default ReceiptBoxEditor;

View File

@@ -0,0 +1,92 @@
import useReceiptPallet from "../hooks/useReceiptPallet.tsx";
import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core";
import { IconArrowLeft, IconPlus } from "@tabler/icons-react";
import InlineButton from "../../../../../components/InlineButton/InlineButton.tsx";
import ReceiptProducts from "./ReceiptProducts.tsx";
import AccordionBoxesOnPallet from "./AccordionBoxes.tsx";
type Props = {
palletId: number;
}
const ReceiptPalletEditor = ({ palletId }: Props) => {
const {
pallet,
fetchPallet,
clientId,
onCreateProductClick,
onCreateBoxClick,
} = useReceiptPallet({ palletId });
if (!pallet) return <Text>Паллет c ID П{palletId} не найден</Text>;
const createButtons = () => {
const isBoxes = pallet.boxes.length > 0;
const isProducts = pallet.residualProducts.length > 0;
const isBoth = !(isBoxes || isProducts);
return (
<Group>
{(isBoxes || isBoth) && (
<InlineButton onClick={onCreateBoxClick} flex={1}>
<IconPlus />
Короб
</InlineButton>
)}
{(isProducts || isBoth) && (
<InlineButton onClick={onCreateProductClick} flex={1}>
<IconPlus />
Товар
</InlineButton>
)}
</Group>
);
};
const renderPalletData = () => {
if (!clientId) return;
if (pallet?.boxes.length !== 0) {
return (
<AccordionBoxesOnPallet
pallet={pallet}
clientId={clientId}
fetchPallet={fetchPallet}
/>
);
}
if (pallet?.residualProducts.length !== 0) {
return (
<ReceiptProducts
clientId={clientId}
object={pallet}
updateObject={fetchPallet}
/>
);
}
};
const backButton = (
<Button
variant={"default"}
onClick={() => window.location.reload()}
flex={1}
>
<IconArrowLeft />
</Button>
);
return (
<Stack>
<Flex align={"center"} flex={9} gap={"md"}>
{backButton}
<Title flex={8} order={3}>
Паллет ID: П{pallet?.id}
</Title>
</Flex>
{createButtons()}
{renderPalletData()}
</Stack>
);
};
export default ReceiptPalletEditor;

View File

@@ -0,0 +1,64 @@
import { ActionIcon, Divider, Flex, Group, rem, Stack, Text } from "@mantine/core";
import { ResidualBoxSchema, ResidualPalletSchema, ResidualProductSchema, ResiduesService } from "../../../../../client";
import { IconTrash } from "@tabler/icons-react";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import ProductQuantityField from "./ProductQuantityField.tsx";
type Props = {
clientId: number | null;
object: ResidualBoxSchema | ResidualPalletSchema | null;
updateObject: () => void;
}
const ReceiptProducts = ({ clientId, object, updateObject }: Props) => {
if (!object || !clientId) return;
const deleteProduct = (residualProductId: number) => {
ResiduesService.deleteResidualProduct({
residualProductId,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
updateObject();
})
.catch(err => console.log(err));
};
const renderProducts = () => {
return object.residualProducts
.sort((a, b) => a.id - b.id)
.map((residualProduct: ResidualProductSchema) => (
<Stack key={residualProduct.id} gap={"sm"}>
<Divider />
<Group wrap={"nowrap"} justify={"space-between"}>
<Stack>
<Text>Товар: {residualProduct.product.name}</Text>
<ProductQuantityField
residualProduct={residualProduct}
updateObject={updateObject}
/>
</Stack>
<ActionIcon
variant={"default"}
onClick={() => deleteProduct(residualProduct.id)}
>
<IconTrash />
</ActionIcon>
</Group>
</Stack>
));
};
return (
<Flex
direction={"column"}
gap={rem(10)}
>
{renderProducts()}
<Divider />
</Flex>
);
};
export default ReceiptProducts;

View File

@@ -0,0 +1,64 @@
import { Button, Center, Stack, Text } from "@mantine/core";
import useScanning from "../../../hooks/useScanning.tsx";
import { useEffect } from "react";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import ReceiptPalletEditor from "./ReceiptPalletEditor.tsx";
import ReceiptBoxEditor from "./ReceiptBoxEditor.tsx";
import { IconArrowLeft } from "@tabler/icons-react";
const ReceiptQrCodeScan = () => {
const {
setIsScanning,
scannedValue,
setScannedValue,
} = useScanning();
useEffect(() => {
setIsScanning(true);
}, []);
const getId = () => Number.parseInt(scannedValue.substring(1));
const checkQrScannedCorrectly = () => {
if (
scannedValue.length < 2
|| (scannedValue[0] !== "П" && scannedValue[0] !== "К")
|| Number.isNaN(getId())
) {
notifications.error({ message: `Считанный ID ${scannedValue} некорректный` });
setScannedValue("");
setIsScanning(true);
}
};
useEffect(() => {
if (scannedValue) {
checkQrScannedCorrectly();
}
}, [scannedValue]);
if (!scannedValue) {
return (
<Stack>
<Button
variant={"default"}
onClick={() => window.location.reload()}
>
<IconArrowLeft />
</Button>
<Center>
<Text>
Отсканируйте QR код
</Text>
</Center>
</Stack>
);
}
if (scannedValue.length > 0 && scannedValue[0] === "К") {
return <ReceiptBoxEditor boxId={getId()} />;
}
return <ReceiptPalletEditor palletId={getId()} />;
};
export default ReceiptQrCodeScan;

View File

@@ -0,0 +1,53 @@
import { useEffect, useState } from "react";
import { ResidualBoxSchema, ResiduesService } from "../../../../../client";
import { modals } from "@mantine/modals";
type Props = {
boxId: number;
}
const useReceiptBox = ({ boxId }: Props) => {
const [box, setBox] = useState<ResidualBoxSchema | null>(null);
const [clientId, setClientId] = useState<number | null>(null);
useEffect(() => {
fetchBox(boxId);
}, []);
const fetchBox = (boxId?: number) => {
const id = boxId ?? box?.id;
if (!id) return;
ResiduesService.getResidualBox({ boxId: id })
.then(res => {
setBox(res.box);
setClientId(res.clientId);
})
.catch(err => console.log(err));
};
const onCreateProductClick = () => {
if (!(box && clientId)) return;
modals.openContextModal({
modal: "receiptModal",
title: "Добавление товара",
withCloseButton: false,
innerProps: {
clientId,
isBox: true,
object: box,
fetchObject: fetchBox,
},
});
};
return {
box,
fetchBox,
clientId,
onCreateProductClick,
};
};
export default useReceiptBox;

View File

@@ -0,0 +1,70 @@
import { useEffect, useState } from "react";
import { ResidualPalletSchema, ResiduesService } from "../../../../../client";
import { modals } from "@mantine/modals";
import { notifications } from "../../../../../shared/lib/notifications.ts";
type Props = {
palletId: number;
}
const useReceiptPallet = ({ palletId }: Props) => {
const [pallet, setPallet] = useState<ResidualPalletSchema | null>(null);
const [clientId, setClientId] = useState<number | null>(null);
useEffect(() => {
fetchPallet(palletId);
}, []);
const fetchPallet = (palletId?: number) => {
const id = palletId ?? pallet?.id;
if (!id) return;
ResiduesService.getResidualPallet({ palletId: id })
.then(res => {
setPallet(res.pallet);
setClientId(res.clientId);
})
.catch(err => console.log(err));
};
const onCreateProductClick = () => {
if (!(pallet && clientId)) return;
modals.openContextModal({
modal: "receiptModal",
title: "Добавление товара",
withCloseButton: false,
innerProps: {
clientId,
isBox: false,
object: pallet,
fetchObject: fetchPallet,
},
});
};
const onCreateBoxClick = () => {
ResiduesService.createResidualBox({
requestBody: {
palletId,
clientId: null,
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
fetchPallet();
})
.catch(err => console.log(err));
};
return {
pallet,
fetchPallet,
clientId,
onCreateProductClick,
onCreateBoxClick,
};
};
export default useReceiptPallet;

View File

@@ -0,0 +1,129 @@
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import { Button, Divider, Flex, NumberInput, rem, Stack } from "@mantine/core";
import ProductSelect from "../../../../../components/ProductSelect/ProductSelect.tsx";
import useScanning from "../../../hooks/useScanning.tsx";
import ScanBarcode from "../../ScanBarcode/ScanBarcode.tsx";
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema, ResiduesService } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { ResidualModalForm } from "../../../../ResiduesPage/types/ResidualProductData.tsx";
type Props = {
clientId: number;
object: ResidualBoxSchema | ResidualPalletSchema;
fetchObject: () => void;
isBox: boolean;
}
const ReceiptModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const {
clientId,
object,
fetchObject,
} = innerProps;
const { isScanning, setIsScanning, scannedValue, setScannedValue } = useScanning();
const initialValues = {
product: null,
quantity: 1,
};
const form = useForm<ResidualModalForm>({
initialValues,
validate: {
product: product => !product && "Необходимо выбрать товар",
quantity: quantity => (!quantity || quantity === 0) && "Слишком мало товара",
},
});
if (!object || !clientId) return;
const postProduct = (product: ProductSchema, quantity: number) => {
ResiduesService.createResidualProduct({
requestBody: {
data: {
productId: product.id,
quantity,
palletId: innerProps.isBox ? null : object.id,
boxId: innerProps.isBox ? object.id : null,
},
},
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
fetchObject();
if (ok) context.closeContextModal(id);
})
.catch(err => console.log(err));
};
const onProductAfterScanningSelect = (product?: ProductSchema) => {
if (!product) {
setScannedValue("");
return;
}
const productIdx = object.residualProducts.findIndex(p => p.product.id === product.id);
if (productIdx === -1) {
form.setFieldValue("product", product);
} else {
postProduct(product, 1);
}
setScannedValue("");
};
const onSubmit = () => {
postProduct(form.values.product!, form.values.quantity!);
form.reset();
};
return (
<Flex
direction={"column"}
gap={rem(10)}
>
<form onSubmit={form.onSubmit(() => onSubmit())}>
<Stack gap={rem(10)}>
<Divider />
<ProductSelect
label={"Товар"}
placeholder={"Выберите товар"}
{...form.getInputProps("product")}
clientId={clientId}
disabled={isScanning}
/>
<NumberInput
label={"Количество"}
hideControls
{...form.getInputProps("quantity")}
min={1}
defaultValue={1}
disabled={isScanning}
/>
<Button
variant={"default"}
type={"submit"}
disabled={isScanning}
>
Добавить
</Button>
<Divider />
</Stack>
</form>
<ScanBarcode
clientId={clientId}
isScanning={isScanning}
setIsScanning={setIsScanning}
onProductSelect={onProductAfterScanningSelect}
scannedValue={scannedValue}
object={object}
/>
</Flex>
);
};
export default ReceiptModal;

View File

@@ -0,0 +1,135 @@
import { ProductSchema, ResidualBoxSchema, ResidualPalletSchema } from "../../../../client";
import { useEffect, useState } from "react";
import useProductsList from "../../../ProductsPage/hooks/useProductsList.tsx";
import { Button, Group, Radio, Stack, Text } from "@mantine/core";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { ReceiptBox, ReceiptPallet } from "../NewReceipt/types/types.tsx";
type Props = {
onProductSelect: (product?: ProductSchema) => void;
clientId: number;
isScanning: boolean;
setIsScanning: (isScanning: boolean) => void;
scannedValue: string;
object: ReceiptBox | ReceiptPallet | ResidualPalletSchema | ResidualBoxSchema;
}
const ScanBarcode = ({
onProductSelect,
clientId,
isScanning,
setIsScanning,
scannedValue,
object,
}: Props) => {
const productsData = useProductsList({ clientId, searchInput: "" });
const [barcodesProducts, setBarcodesProducts] = useState(new Map<string, ProductSchema[]>());
let productsToSelect: ProductSchema[] = [];
const [selectedProduct, setSelectedProduct] = useState<ProductSchema>();
const toggleScanning = () => {
setIsScanning(!isScanning);
};
const productsToBarcodesProducts = () => {
const data = new Map<string, ProductSchema[]>();
productsData.products.forEach(product => {
product.barcodes.forEach(barcode => {
if (data.has(barcode)) {
data.set(barcode, [...data.get(barcode)!, product]);
} else {
data.set(barcode, [product]);
}
});
});
setBarcodesProducts(data);
};
useEffect(() => {
if (productsData.products.length !== 0) {
productsToBarcodesProducts();
}
}, [productsData.isLoading]);
const findProductInObject = (productsToSelect: ProductSchema[]): ProductSchema | undefined => {
for (let i = 0; i < productsToSelect.length; i++) {
for (let j = 0; j < object.residualProducts.length; j++) {
if (productsToSelect[i].id === object.residualProducts[j].product.id) {
return productsToSelect[i];
}
}
}
};
const renderScanningResults = () => {
productsToSelect = barcodesProducts.get(scannedValue) ?? [];
if (productsToSelect?.length === 0) {
notifications.error({ message: `Товара с штрихкодом ${scannedValue} не найдено` });
onProductSelect();
return;
}
if (productsToSelect?.length === 1) {
onProductSelect(productsToSelect[0]);
return;
}
const product = findProductInObject(productsToSelect);
if (product) {
onProductSelect(product);
return;
}
return renderProductSelect();
};
const renderProductsToSelect = () => {
return productsToSelect.map(product => (
<Group key={product.id} wrap={"nowrap"}>
<Radio
checked={selectedProduct?.id === product.id}
onChange={() => setSelectedProduct(product)}
/>
<Stack>
<Text>{product.name}</Text>
{product.size && <Text>{product.size}</Text>}
</Stack>
</Group>
));
};
const renderProductSelect = () => {
return (
<Stack>
<Text>Выберите товар для данного штрихкода</Text>
{renderProductsToSelect()}
<Button
variant={"default"}
onClick={() => {
if (!selectedProduct) {
notifications.error({ message: "Товар не выбран" });
} else {
onProductSelect(selectedProduct);
setSelectedProduct(undefined);
}
}}
>
Выбрать
</Button>
</Stack>
);
};
return (scannedValue) ? (
renderScanningResults()
) : (
<Stack gap={0}>
<Button
variant={"default"}
type={"button"}
onClick={toggleScanning}
>
{isScanning ? "Отменить сканирование" : "Отсканировать штрихкод"}
</Button>
</Stack>
);
};
export default ScanBarcode;

View File

@@ -0,0 +1,34 @@
import { useState } from "react";
import { useWindowEvent } from "@mantine/hooks";
const useScanning = () => {
const [scanningValue, setScanningValue] = useState<string>("");
const [scannedValue, setScannedValue] = useState<string>("");
const [isScanning, setIsScanning] = useState<boolean>(false);
const setIsScanningValue = (isScanning: boolean) => {
if (!isScanning) {
setScanningValue("");
}
setIsScanning(isScanning);
};
useWindowEvent("keydown", (event) => {
if (!isScanning) return;
event.preventDefault();
setScanningValue(prevState => prevState + event.key);
if (["\n", "\r", "Enter"].includes(event.key)) {
setScannedValue(scanningValue);
setIsScanningValue(false);
}
});
return {
isScanning,
setIsScanning: setIsScanningValue,
scannedValue,
setScannedValue,
};
};
export default useScanning;

View File

View File

@@ -0,0 +1,7 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
gap: rem(10);
}

View File

@@ -0,0 +1,61 @@
import { ReceiptContextProvider } from "../components/NewReceipt/contexts/ReceiptContext.tsx";
import ReceiptUserSelect from "../components/NewReceipt/components/ReceiptUserSelect.tsx";
import ReceiptEditor from "../components/NewReceipt/components/ReceiptEditor.tsx";
import { Button, Stack } from "@mantine/core";
import { useState } from "react";
import ReceiptQrCodeScan from "../components/ReceiptEditing/components/ReceiptQrCodeScan.tsx";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
enum ReceiptType {
NEW_RECEIPT,
RECEIPT_EDITING,
UNDEFINED,
}
const ReceiptPage = () => {
const [receiptType, setReceiptType] = useState<ReceiptType>(ReceiptType.UNDEFINED);
const getContent = () => {
if (receiptType === ReceiptType.UNDEFINED) {
return (
<Stack h="95vh">
<Button
variant="default"
onClick={() => setReceiptType(ReceiptType.NEW_RECEIPT)}
h="100%"
>
Начать приемку
</Button>
<Button
variant="default"
onClick={() => setReceiptType(ReceiptType.RECEIPT_EDITING)}
h="100%"
>
Редактировать паллет/короб
</Button>
</Stack>
);
}
if (receiptType === ReceiptType.NEW_RECEIPT) {
return (
<ReceiptContextProvider>
<Stack>
<ReceiptUserSelect />
<ReceiptEditor />
</Stack>
</ReceiptContextProvider>
);
}
return (
<ReceiptQrCodeScan />
);
};
return (
<PageBlock noBorderRadius style={{ minHeight: "100vh" }}>
{getContent()}
</PageBlock>
);
};
export default ReceiptPage;

View File

@@ -0,0 +1,83 @@
import { ResidualBoxSchema } from "../../../../client";
import { Accordion, ActionIcon, Center, Checkbox, Text, Tooltip } from "@mantine/core";
import { IconBox, IconTrash } from "@tabler/icons-react";
import ResidualProductsTable from "../ResidualProductsTable/ResidualProductsTable.tsx";
import { useResiduesContext } from "../../contexts/ResiduesContext.tsx";
type Props = {
boxes?: ResidualBoxSchema[];
}
const ResidualBoxes = ({ boxes }: Props) => {
const { onDeleteBoxClick, boxIdsToPrint } = useResiduesContext();
if (!boxes || boxes.length == 0) return;
const boxIds = boxes.map((box) => box.id.toString());
const removeBoxButton = (box: ResidualBoxSchema) => {
return (
<Tooltip label="Удалить короб">
<ActionIcon
variant={"default"}
onClick={() => onDeleteBoxClick(box)}
mx={"md"}
>
<IconTrash />
</ActionIcon>
</Tooltip>
);
};
const checkboxToPrint = (box: ResidualBoxSchema) => {
return (
<Checkbox
ml="sm"
checked={boxIdsToPrint.has(box.id)}
onChange={() => {
if (boxIdsToPrint.has(box.id)) {
boxIdsToPrint.delete(box.id);
} else {
boxIdsToPrint.add(box.id);
}
}}
/>
);
};
const renderBox = (box: ResidualBoxSchema) => {
return (
<Accordion.Item key={box.id} value={box.id.toString()}>
<Center>
<Accordion.Control icon={<IconBox />}>
Короб {box.id}
</Accordion.Control>
{checkboxToPrint(box)}
{removeBoxButton(box)}
</Center>
<Accordion.Panel>
{
box.residualProducts.length > 0 ? (
<ResidualProductsTable
items={box.residualProducts.sort((a, b) => a.id - b.id)}
/>
) : (
<Text>Пустой</Text>
)
}
</Accordion.Panel>
</Accordion.Item>
);
};
return (
<Accordion
multiple={true}
defaultValue={boxIds}
bd={"solid 1px gray"}
>
{boxes.sort((a, b) => a.id - b.id).map(box => renderBox(box))}
</Accordion>
);
};
export default ResidualBoxes;

View File

@@ -0,0 +1,84 @@
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
import { ResidualProductSchema, ResiduesService } from "../../../../client";
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { MRT_TableOptions } from "mantine-react-table";
import { notifications } from "../../../../shared/lib/notifications.ts";
import { modals } from "@mantine/modals";
import { useResiduesContext } from "../../contexts/ResiduesContext.tsx";
import useResiduesTableColumns from "../../hooks/residuesTableColumns.tsx";
type Props = {
items: ResidualProductSchema[];
}
const ResidualProductsTable = ({ items }: Props) => {
const columns = useResiduesTableColumns<ResidualProductSchema>();
const { selectedClient, refetchClient } = useResiduesContext();
const onDeleteClick = (residualProduct: ResidualProductSchema) => {
ResiduesService.deleteResidualProduct({
residualProductId: residualProduct.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
refetchClient();
})
.catch(err => console.log(err));
};
const onEditClick = (residualProduct: ResidualProductSchema) => {
if (!selectedClient) return;
modals.openContextModal({
modal: "residualProductModal",
title: "Редактирование товара на паллете",
withCloseButton: false,
innerProps: {
client: selectedClient,
updateOnSubmit: refetchClient,
residuesData: {
residualProductId: residualProduct.id,
product: residualProduct.product,
quantity: residualProduct.quantity,
},
},
});
};
return (
<BaseTable
data={items}
columns={columns}
restProps={
{
enableSorting: false,
enableColumnActions: false,
enableRowActions: true,
enableRowNumbers: true,
positionActionsColumn: "last",
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() => onDeleteClick(row.original)}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onEditClick(row.original)}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<ResidualProductSchema>
}
/>
);
};
export default ResidualProductsTable;

View File

@@ -0,0 +1,34 @@
import { Group } from "@mantine/core";
import { useResiduesContext } from "../../contexts/ResiduesContext.tsx";
import useResiduesPdf from "../../hooks/useResiduesPdf.tsx";
import { IconQrcode } from "@tabler/icons-react";
import InlineButton from "../../../../components/InlineButton/InlineButton.tsx";
const ResiduesHeader = () => {
const {
onCreatePalletClick,
onCreateBoxClick,
} = useResiduesContext();
const {
onGetPalletsPdfClick,
} = useResiduesPdf();
return (
<Group>
<InlineButton onClick={() => onCreatePalletClick()}>
Добавить паллет
</InlineButton>
<InlineButton onClick={() => onCreateBoxClick()}>
Добавить короб
</InlineButton>
<InlineButton onClick={() => onGetPalletsPdfClick()}>
<IconQrcode />
Печать
</InlineButton>
</Group>
);
};
export default ResiduesHeader;

View File

@@ -0,0 +1,21 @@
import { Group, Stack } from "@mantine/core";
import ClientSelect from "../../../../components/Selects/ClientSelect/ClientSelect.tsx";
import ResiduesHeader from "../ResiduesHeader/ResiduesHeader.tsx";
import ResiduesTree from "../ResiduesTree/ResiduesTree.tsx";
import { useResiduesContext } from "../../contexts/ResiduesContext.tsx";
const ResiduesPageContent = () => {
const { selectedClient, selectClient } = useResiduesContext();
return (
<Stack h="92vh">
<Group>
<ClientSelect onChange={selectClient} />
{selectedClient && <ResiduesHeader />}
</Group>
{selectedClient && <ResiduesTree />}
</Stack>
);
};
export default ResiduesPageContent;

View File

@@ -0,0 +1,135 @@
import {
Accordion,
ActionIcon,
Button,
Center, Checkbox,
Group,
rem,
ScrollArea,
Stack,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { IconPlus, IconSpace, IconTrash } from "@tabler/icons-react";
import { useResiduesContext } from "../../contexts/ResiduesContext.tsx";
import { ResidualBoxSchema, ResidualPalletSchema, ResidualProductSchema } from "../../../../client";
import ResidualProductsTable from "../ResidualProductsTable/ResidualProductsTable.tsx";
import ResidualBoxes from "../ResidualBoxes/ResidualBoxes.tsx";
import residualBoxes from "../ResidualBoxes/ResidualBoxes.tsx";
const ResiduesTree = () => {
const {
selectedClient,
onDeletePalletClick,
onCreateBoxClick,
palletIdsToPrint,
} = useResiduesContext();
const palletIds: string[] = [];
const sortById = (data?: ResidualPalletSchema[] | ResidualBoxSchema[] | ResidualProductSchema[]) => {
return data?.sort((a, b) => a.id - b.id);
};
const checkboxToPrint = (pallet: ResidualPalletSchema) => {
return (
<Checkbox
ml="sm"
checked={palletIdsToPrint.has(pallet.id)}
onChange={() => {
if (palletIdsToPrint.has(pallet.id)) {
palletIdsToPrint.delete(pallet.id);
} else {
palletIdsToPrint.add(pallet.id);
}
}}
/>
);
};
const getPallets = () => {
const sortedPallets = sortById(selectedClient?.pallets) as ResidualPalletSchema[];
return sortedPallets?.map((pallet => {
palletIds.push(pallet.id.toString());
return (
<Accordion.Item key={pallet.id} value={pallet.id.toString()}>
<Center>
<Accordion.Control icon={<IconSpace />}>
Паллет - П{pallet.id}
</Accordion.Control>
{checkboxToPrint(pallet)}
{removePalletButton(pallet)}
</Center>
<Accordion.Panel>
{getPalletContent(pallet)}
</Accordion.Panel>
</Accordion.Item>
);
})) ?? [];
};
const removePalletButton = (pallet: ResidualPalletSchema) => {
return (
<Tooltip label="Удалить паллет">
<ActionIcon
variant={"default"}
onClick={() => onDeletePalletClick(pallet)}
mx={"md"}
>
<IconTrash />
</ActionIcon>
</Tooltip>
);
};
const createBoxButton = (palletId: number) => {
return (
<Button
variant={"default"}
onClick={() => onCreateBoxClick(palletId)}
>
<Group gap={"md"}>
<IconPlus />
Добавить короб
</Group>
</Button>
);
};
const getPalletContent = (pallet: ResidualPalletSchema) => {
const isEmpty = pallet.boxes.length === 0 && pallet.residualProducts.length === 0;
const isBox = pallet.residualProducts.length === 0;
const title = isEmpty ? "Пустой" : isBox ? "Короба" : "Товары";
const residualProducts = sortById(pallet.residualProducts) as ResidualProductSchema[];
return (
<Stack gap={rem(5)}>
<Group justify={"space-between"}>
<Stack gap={"xs"}>
<Text>Дата добавления: {new Date(pallet.createdAt).toLocaleString("ru-RU")}</Text>
<Title order={6}>{title}</Title>
</Stack>
{isBox && createBoxButton(pallet.id)}
</Group>
{residualProducts.length > 0 && <ResidualProductsTable items={residualProducts} />}
{residualBoxes.length > 0 && <ResidualBoxes boxes={pallet.boxes} />}
</Stack>
);
};
return (
<ScrollArea>
<Accordion
multiple={true}
defaultValue={palletIds}
bd={"solid 1px gray"}
>
<ResidualBoxes boxes={selectedClient?.boxes} />
{getPallets()}
</Accordion>
</ScrollArea>
);
};
export default ResiduesTree;

View File

@@ -0,0 +1,176 @@
import { createContext, FC, useContext, useState } from "react";
import {
ClientDetailedSchema,
ClientSchema,
ClientService,
ResidualBoxSchema,
ResidualPalletSchema,
ResiduesService,
} from "../../../client";
import { modals } from "@mantine/modals";
import { Text } from "@mantine/core";
import { notifications } from "../../../shared/lib/notifications.ts";
import { useSet } from "@mantine/hooks";
type ResiduesContextState = {
selectedClient?: ClientDetailedSchema;
selectClient: (client?: ClientSchema) => void;
refetchClient: () => void;
onDeletePalletClick: (pallet: ResidualPalletSchema) => void;
onCreatePalletClick: () => void;
onDeleteBoxClick: (box: ResidualBoxSchema) => void;
onCreateBoxClick: (pallet?: number) => void;
boxIdsToPrint: Set<number>;
palletIdsToPrint: Set<number>;
};
const ResiduesContext = createContext<ResiduesContextState | undefined>(
undefined,
);
const useResiduesContextState = () => {
const [selectedClient, setSelectedClient] = useState<ClientDetailedSchema>();
const boxIdsToPrint = useSet<number>();
const palletIdsToPrint = useSet<number>();
const fetchClient = (clientId?: number) => {
if (!clientId) {
setSelectedClient(undefined);
return;
}
ClientService.getClient({
clientId,
}).then(res => {
setSelectedClient(res.client);
}).catch(err => console.log(err));
};
const selectClient = (client?: ClientSchema) => {
fetchClient(client?.id);
};
const refetchClient = () => {
fetchClient(selectedClient?.id);
};
const onDeletePallet = (palletId: number) => {
ResiduesService.deleteResidualPallet({ palletId })
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
palletIdsToPrint.delete(palletId);
refetchClient();
})
.catch(err => console.log(err));
};
const onDeletePalletClick = (pallet: ResidualPalletSchema) => {
if (!selectedClient) return;
if (pallet.boxes.length === 0 && pallet.residualProducts.length === 0) {
onDeletePallet(pallet.id);
return;
}
modals.openConfirmModal({
title: "Удаление паллета",
children: <Text size="sm">Вы уверены что хотите удалить паллет?</Text>,
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onDeletePallet(pallet.id),
});
};
const onDeleteBox = (boxId: number) => {
ResiduesService.deleteResidualBox({ boxId })
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
boxIdsToPrint.delete(boxId);
refetchClient();
})
.catch(err => console.log(err));
};
const onDeleteBoxClick = (box: ResidualBoxSchema) => {
if (!selectedClient) return;
if (box.residualProducts.length === 0) {
onDeleteBox(box.id);
return;
}
modals.openConfirmModal({
title: "Удаление короба",
children: <Text size="sm">Вы уверены что хотите удалить короб?</Text>,
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "red" },
onConfirm: () => onDeleteBox(box.id),
});
};
const onCreatePalletClick = () => {
if (!selectedClient) return;
ResiduesService.createResidualPallet({
requestBody: { clientId: selectedClient.id },
})
.then(({ ok, message }) => {
if (!ok) notifications.error({ message });
refetchClient();
})
.catch(err => console.log(err));
};
const onCreateBoxClick = (palletId?: number) => {
if (!selectedClient) return;
ResiduesService.createResidualBox({
requestBody: {
palletId: palletId ?? null,
clientId: palletId ? null : selectedClient.id,
},
})
.then(({ ok, message }) => {
if (!ok) notifications.error({ message });
refetchClient();
})
.catch(err => console.log(err));
};
return {
selectedClient,
selectClient,
refetchClient,
onDeletePalletClick,
onCreatePalletClick,
onDeleteBoxClick,
onCreateBoxClick,
boxIdsToPrint,
palletIdsToPrint,
};
};
type ResiduesContextProviderProps = {
children: React.ReactNode;
};
export const ResiduesContextProvider: FC<ResiduesContextProviderProps> = ({ children }) => {
const state = useResiduesContextState();
return (
<ResiduesContext.Provider value={state}>
{children}
</ResiduesContext.Provider>
);
};
export const useResiduesContext = () => {
const context = useContext(ResiduesContext);
if (!context) {
throw new Error(
"useResiduesContext must be used within a ResiduesContextProvider",
);
}
return context;
};

View File

@@ -0,0 +1,32 @@
import { useMemo } from "react";
import { MRT_ColumnDef, MRT_RowData } from "mantine-react-table";
const useResiduesTableColumns = <T extends MRT_RowData>() => {
return useMemo<MRT_ColumnDef<T>[]>(
() => [
{
header: "Название",
accessorKey: "product.name",
Cell: ({ row }) => row.original.product?.name ?? "-",
},
{
header: "Артикул",
accessorKey: "product.article",
Cell: ({ row }) => row.original.product?.article ?? "-",
},
{
header: "Размер",
accessorKey: "product.size",
Cell: ({ row }) => row.original.product?.size ?? "-",
},
{
header: "Количество",
accessorKey: "quantity",
},
],
[],
);
};
export default useResiduesTableColumns;

View File

@@ -0,0 +1,31 @@
import { useResiduesContext } from "../contexts/ResiduesContext.tsx";
import { notifications } from "../../../shared/lib/notifications.ts";
const useResiduesPdf = () => {
const { palletIdsToPrint, boxIdsToPrint } = useResiduesContext();
const basePdfUrl = `${import.meta.env.VITE_API_URL}/residues/pdf`;
const getPdf = (url: string) => {
const pdfWindow = window.open(url);
if (!pdfWindow) return;
pdfWindow.print();
};
const onGetPalletsPdfClick = () => {
if (palletIdsToPrint.size === 0 && boxIdsToPrint.size === 0) {
notifications.show({ message: "Не выбран ни один элемент для печати "});
return;
}
const palletIdsStr = palletIdsToPrint.values().toArray().join(",")
const boxIdsStr = boxIdsToPrint.values().toArray().join(",")
getPdf(`${basePdfUrl}/?pallet_ids=${palletIdsStr}&box_ids=${boxIdsStr}`);
};
return {
onGetPalletsPdfClick,
};
};
export default useResiduesPdf;

View File

@@ -0,0 +1,83 @@
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import { Button, Flex, NumberInput, rem } from "@mantine/core";
import { ClientDetailedSchema, ProductSchema, ResiduesService, UpdateResidualProductSchema } from "../../../../client";
import { ResidualModalForm, UpdateResidualProductData } from "../../types/ResidualProductData.tsx";
import { notifications } from "../../../../shared/lib/notifications.ts";
import ProductSelect from "../../../../components/ProductSelect/ProductSelect.tsx";
type Props = {
updateOnSubmit: () => void;
client: ClientDetailedSchema;
residuesData: UpdateResidualProductData;
}
const ResidualProductModal = ({
context,
id,
innerProps,
}: ContextModalProps<Props>) => {
const initialValues: ResidualModalForm = {
quantity: innerProps.residuesData.quantity ?? 0,
product: innerProps.residuesData.product,
};
const form = useForm<ResidualModalForm>({
initialValues,
validate: {
product: product => !product && "Необходимо выбрать товар",
quantity: quantity => quantity === 0 && "Слишком мало товара",
},
});
const updateResidualProduct = () => {
const data = {
...form.values,
productId: form.values.product!.id,
} as UpdateResidualProductSchema;
ResiduesService.updateResidualProduct({
requestBody: { data },
residualProductId: innerProps.residuesData.residualProductId,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message: message });
innerProps.updateOnSubmit();
if (ok) context.closeContextModal(id);
})
.catch(err => console.log(err));
};
return (
<form onSubmit={form.onSubmit(() => updateResidualProduct())}>
<Flex
direction={"column"}
gap={rem(10)}
>
<ProductSelect
label={"Товар"}
placeholder={"Выберите товар"}
{...form.getInputProps("product")}
defaultValue={innerProps.residuesData.product as (ProductSchema & string)}
clientId={innerProps.client.id}
/>
<NumberInput
label={"Количество"}
hideControls
{...form.getInputProps("quantity")}
min={0}
/>
<Button
variant={"default"}
type={"submit"}
>
Сохранить
</Button>
</Flex>
</form>
);
};
export default ResidualProductModal;

View File

@@ -0,0 +1,12 @@
import { ProductSchema } from "../../../client";
export type ResidualModalForm = {
quantity: number;
product: ProductSchema | null;
}
export type UpdateResidualProductData = {
product: ProductSchema | null;
quantity: number;
residualProductId: number;
};

View File

@@ -0,0 +1,19 @@
import styles from "../../ProductsPage/ui/ProductsPage.module.css";
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
import { ResiduesContextProvider } from "../contexts/ResiduesContext.tsx";
import ResiduesPageContent from "../components/ResiduesPageContent/ResiduesPageContent.tsx";
const ResiduesPage = () => {
return (
<div className={styles["container"]}>
<PageBlock fullHeight>
<ResiduesContextProvider>
<ResiduesPageContent />
</ResiduesContextProvider>
</PageBlock>
</div>
);
};
export default ResiduesPage;

View File

@@ -0,0 +1,6 @@
import { createLazyFileRoute } from '@tanstack/react-router'
import ReceiptPage from "../pages/ReceiptPage/ui/ReceiptPage.tsx";
export const Route = createLazyFileRoute('/receipt')({
component: () => <ReceiptPage />,
})

View File

@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import ResiduesPage from "../pages/ResiduesPage/ui/ResiduesPage.tsx";
export const Route = createLazyFileRoute("/residues")({
component: () => <ResiduesPage />,
});