From 43e9c1c91e22afa5ded3cd186adea73961344d9d Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 19 May 2026 18:13:17 +0100 Subject: [PATCH 1/4] feat: update transfer document action labels --- grails-app/i18n/messages.properties | 8 ++++---- grails-app/i18n/messages_ru.properties | 8 ++++---- grails-app/i18n/messages_tg.properties | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 4b8333fd77..0f1d2eb466 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -760,8 +760,8 @@ enum.ActivityCode.ENABLE_FULFILLER_APPROVAL_NOTIFICATIONS=Enable fulfiller appro enum.ActivityCode.PACK_SHIPMENT=Pack shipment enum.ActivityCode.PARTIAL_RECEIVING=Partial receiving enum.ActivityCode.REQUIRE_ACCOUNTING=Require accounting -enum.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=Require document on outbound stock transfer -enum.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=Require document on inbound stock transfer +enum.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=Require document on outbound +enum.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=Require document on inbound enum.ActivityCode.ENABLE_CENTRAL_PURCHASING=Enable central purchasing enum.ActivityCode.HOLD_STOCK=Hold stock enum.ActivityCode.SUBMIT_REQUEST=Submit request @@ -4330,8 +4330,8 @@ react.locationsConfiguration.ActivityCode.ENABLE_NOTIFICATIONS=Enable Notificati react.locationsConfiguration.ActivityCode.PACK_SHIPMENT=Pack shipment react.locationsConfiguration.ActivityCode.PARTIAL_RECEIVING=Partial receiving react.locationsConfiguration.ActivityCode.REQUIRE_ACCOUNTING=Require accounting -react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=Require document on outbound stock transfer -react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=Require document on inbound stock transfer +react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=Require document on outbound +react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=Require document on inbound react.locationsConfiguration.ActivityCode.ENABLE_CENTRAL_PURCHASING=Enable central purchasing react.locationsConfiguration.ActivityCode.HOLD_STOCK=Hold stock react.locationsConfiguration.ActivityCode.SUBMIT_REQUEST=Submit request diff --git a/grails-app/i18n/messages_ru.properties b/grails-app/i18n/messages_ru.properties index 50569ed5ad..70ab239887 100644 --- a/grails-app/i18n/messages_ru.properties +++ b/grails-app/i18n/messages_ru.properties @@ -760,8 +760,8 @@ enum.ActivityCode.ENABLE_FULFILLER_APPROVAL_NOTIFICATIONS=\u0412\u043a\u043b\u04 enum.ActivityCode.PACK_SHIPMENT=\u041f\u0430\u043a\u0435\u0442\u043d\u0430\u044f \u043e\u0442\u0433\u0440\u0443\u0437\u043a\u0430 enum.ActivityCode.PARTIAL_RECEIVING=\u0427\u0430\u0441\u0442\u0438\u0447\u043d\u044b\u0439 \u043f\u0440\u0438\u0435\u043c enum.ActivityCode.REQUIRE_ACCOUNTING=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0443\u0447\u0435\u0442 -enum.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u043f\u0440\u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0435\u043c \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0438 \u0442\u043e\u0432\u0430\u0440\u0430 -enum.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u043f\u0440\u0438 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u043c \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0438 \u0442\u043e\u0432\u0430\u0440\u0430 +enum.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 +enum.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 enum.ActivityCode.ENABLE_CENTRAL_PURCHASING=\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u043a\u0443\u043f\u043a\u0438 enum.ActivityCode.HOLD_STOCK=\u0414\u0435\u0440\u0436\u0438\u0442\u0435 \u0437\u0430\u043f\u0430\u0441 enum.ActivityCode.SUBMIT_REQUEST=\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441 @@ -4297,8 +4297,8 @@ react.locationsConfiguration.ActivityCode.ENABLE_NOTIFICATIONS=\u0412\u043a\u043 react.locationsConfiguration.ActivityCode.PACK_SHIPMENT=\u041f\u0430\u043a\u0435\u0442\u043d\u0430\u044f \u043e\u0442\u0433\u0440\u0443\u0437\u043a\u0430 react.locationsConfiguration.ActivityCode.PARTIAL_RECEIVING=\u0427\u0430\u0441\u0442\u0438\u0447\u043d\u044b\u0439 \u043f\u0440\u0438\u0435\u043c react.locationsConfiguration.ActivityCode.REQUIRE_ACCOUNTING=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0443\u0447\u0435\u0442 -react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u043f\u0440\u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0435\u043c \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0438 \u0442\u043e\u0432\u0430\u0440\u0430 -react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u043f\u0440\u0438 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u043c \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0438 \u0442\u043e\u0432\u0430\u0440\u0430 +react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 +react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 react.locationsConfiguration.ActivityCode.ENABLE_CENTRAL_PURCHASING=\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u043a\u0443\u043f\u043a\u0438 react.locationsConfiguration.ActivityCode.HOLD_STOCK=\u0414\u0435\u0440\u0436\u0438\u0442\u0435 \u0437\u0430\u043f\u0430\u0441 react.locationsConfiguration.ActivityCode.SUBMIT_REQUEST=\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441 diff --git a/grails-app/i18n/messages_tg.properties b/grails-app/i18n/messages_tg.properties index 1c507f79f4..9b64425019 100644 --- a/grails-app/i18n/messages_tg.properties +++ b/grails-app/i18n/messages_tg.properties @@ -760,8 +760,8 @@ enum.ActivityCode.ENABLE_FULFILLER_APPROVAL_NOTIFICATIONS=\u041e\u0433\u043e\u04 enum.ActivityCode.PACK_SHIPMENT=\u0418\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0431\u0430\u0441\u0442\u0430\u0431\u0430\u043d\u0434\u04e3 enum.ActivityCode.PARTIAL_RECEIVING=\u049a\u0438\u0441\u043c\u0430\u043d \u049b\u0430\u0431\u0443\u043b enum.ActivityCode.REQUIRE_ACCOUNTING=\u0411\u0430\u04b3\u0438\u0441\u043e\u0431\u0433\u0438\u0440\u0438\u0438 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0440\u043e \u0442\u0430\u043b\u0430\u0431 \u043a\u0443\u043d\u0435\u0434 -enum.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=\u0411\u0430\u0440\u043e\u0438 \u0438\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0437\u0430\u0445\u0438\u0440\u0430\u0438 \u0431\u0435\u0440\u0443\u043d\u04e3 \u04b3\u0443\u04b7\u04b7\u0430\u0442 \u0442\u0430\u043b\u0430\u0431 \u043a\u0430\u0440\u0434\u0430 \u0448\u0430\u0432\u0430\u0434 -enum.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=\u0411\u0430\u0440\u043e\u0438 \u0438\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0437\u0430\u0445\u0438\u0440\u0430\u0438 \u0434\u0430\u0440\u0443\u043d\u04e3 \u04b3\u0443\u04b7\u04b7\u0430\u0442 \u0442\u0430\u043b\u0430\u0431 \u043a\u0430\u0440\u0434\u0430 \u0448\u0430\u0432\u0430\u0434 +enum.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=\u0411\u0430\u0440\u043e\u0438 \u0438\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0431\u0435\u0440\u0443\u043d\u04e3 \u04b3\u0443\u04b7\u04b7\u0430\u0442 \u0442\u0430\u043b\u0430\u0431 \u043a\u0430\u0440\u0434\u0430 \u0448\u0430\u0432\u0430\u0434 +enum.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=\u0411\u0430\u0440\u043e\u0438 \u0438\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0434\u0430\u0440\u0443\u043d\u04e3 \u04b3\u0443\u04b7\u04b7\u0430\u0442 \u0442\u0430\u043b\u0430\u0431 \u043a\u0430\u0440\u0434\u0430 \u0448\u0430\u0432\u0430\u0434 enum.ActivityCode.ENABLE_CENTRAL_PURCHASING=\u0425\u0430\u0440\u0438\u0434\u0438 \u043c\u0430\u0440\u043a\u0430\u0437\u0438\u0440\u043e \u0444\u0430\u044a\u043e\u043b \u0441\u043e\u0437\u0435\u0434 enum.ActivityCode.HOLD_STOCK=\u0417\u0430\u0445\u0438\u0440\u0430 \u043d\u0438\u0433\u043e\u04b3 \u0434\u043e\u0440\u0435\u0434 enum.ActivityCode.SUBMIT_REQUEST=\u041f\u0435\u0448\u043d\u0438\u04b3\u043e\u0434\u0438 \u0434\u0430\u0440\u0445\u043e\u0441\u0442 @@ -4297,8 +4297,8 @@ react.locationsConfiguration.ActivityCode.ENABLE_NOTIFICATIONS=\u041e\u0433\u043 react.locationsConfiguration.ActivityCode.PACK_SHIPMENT=\u0418\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0431\u0430\u0441\u0442\u0430\u0431\u0430\u043d\u0434\u04e3 react.locationsConfiguration.ActivityCode.PARTIAL_RECEIVING=\u049a\u0438\u0441\u043c\u0430\u043d \u049b\u0430\u0431\u0443\u043b react.locationsConfiguration.ActivityCode.REQUIRE_ACCOUNTING=\u0411\u0430\u04b3\u0438\u0441\u043e\u0431\u0433\u0438\u0440\u0438\u0438 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0440\u043e \u0442\u0430\u043b\u0430\u0431 \u043a\u0443\u043d\u0435\u0434 -react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=\u0411\u0430\u0440\u043e\u0438 \u0438\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0437\u0430\u0445\u0438\u0440\u0430\u0438 \u0431\u0435\u0440\u0443\u043d\u04e3 \u04b3\u0443\u04b7\u04b7\u0430\u0442 \u0442\u0430\u043b\u0430\u0431 \u043a\u0430\u0440\u0434\u0430 \u0448\u0430\u0432\u0430\u0434 -react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=\u0411\u0430\u0440\u043e\u0438 \u0438\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0437\u0430\u0445\u0438\u0440\u0430\u0438 \u0434\u0430\u0440\u0443\u043d\u04e3 \u04b3\u0443\u04b7\u04b7\u0430\u0442 \u0442\u0430\u043b\u0430\u0431 \u043a\u0430\u0440\u0434\u0430 \u0448\u0430\u0432\u0430\u0434 +react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_OUT_DOCUMENT=\u0411\u0430\u0440\u043e\u0438 \u0438\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0431\u0435\u0440\u0443\u043d\u04e3 \u04b3\u0443\u04b7\u04b7\u0430\u0442 \u0442\u0430\u043b\u0430\u0431 \u043a\u0430\u0440\u0434\u0430 \u0448\u0430\u0432\u0430\u0434 +react.locationsConfiguration.ActivityCode.REQUIRE_TRANSFER_IN_DOCUMENT=\u0411\u0430\u0440\u043e\u0438 \u0438\u043d\u0442\u0438\u049b\u043e\u043b\u0438 \u0434\u0430\u0440\u0443\u043d\u04e3 \u04b3\u0443\u04b7\u04b7\u0430\u0442 \u0442\u0430\u043b\u0430\u0431 \u043a\u0430\u0440\u0434\u0430 \u0448\u0430\u0432\u0430\u0434 react.locationsConfiguration.ActivityCode.ENABLE_CENTRAL_PURCHASING=\u0425\u0430\u0440\u0438\u0434\u0438 \u043c\u0430\u0440\u043a\u0430\u0437\u0438\u0440\u043e \u0444\u0430\u044a\u043e\u043b \u0441\u043e\u0437\u0435\u0434 react.locationsConfiguration.ActivityCode.HOLD_STOCK=\u0417\u0430\u0445\u0438\u0440\u0430 \u043d\u0438\u0433\u043e\u04b3 \u0434\u043e\u0440\u0435\u0434 react.locationsConfiguration.ActivityCode.SUBMIT_REQUEST=\u041f\u0435\u0448\u043d\u0438\u04b3\u043e\u0434\u0438 \u0434\u0430\u0440\u0445\u043e\u0441\u0442 From 0c28a53ebb788d9f7b22f31c21835a8dbad0ebab Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 19 May 2026 18:19:18 +0100 Subject: [PATCH 2/4] feat: add custom putaway document upload functionality - Implemented a new controller `CustomPutawayDocumentController` to handle document listing and uploading for putaways. - Added routes in `UrlMappings.groovy` for document upload and retrieval. - Created `CustomPutawayDocumentService` to manage document-related business logic. - Updated `PutawayService` to validate document requirements during putaway completion. - Introduced a new React component `SupportingDocumentsPanel` for handling document uploads in the UI. - Added tests for the `SupportingDocumentsPanel` to ensure proper functionality and error handling. - Updated internationalization files to include messages related to document requirements and errors. - Refactored existing API utility functions to support the new document handling logic. --- .../org/pih/warehouse/UrlMappings.groovy | 6 ++ .../CustomPutawayDocumentController.groovy | 72 +++++++++++++++++++ grails-app/i18n/messages.properties | 1 + grails-app/i18n/messages_ru.properties | 1 + grails-app/i18n/messages_tg.properties | 1 + .../CustomPutawayDocumentService.groovy | 40 +++++++++++ .../warehouse/putaway/PutawayService.groovy | 2 + .../components/put-away/PutAwayCheckPage.jsx | 17 +++++ .../stock-transfer/StockTransferCheckPage.jsx | 7 +- ....jsx => SupportingDocumentsPanel.test.jsx} | 72 ++++++++++++------- ...Panel.jsx => SupportingDocumentsPanel.jsx} | 57 +++++++++------ ...nel.scss => SupportingDocumentsPanel.scss} | 2 +- .../stockTransferDocuments/utils/api.js | 12 ++-- 13 files changed, 235 insertions(+), 55 deletions(-) create mode 100644 grails-app/controllers/org/pih/warehouse/custom/putawayDocuments/CustomPutawayDocumentController.groovy create mode 100644 grails-app/services/org/pih/warehouse/custom/putawayDocuments/CustomPutawayDocumentService.groovy rename src/js/custom/stockTransferDocuments/__tests__/{StockTransferDocumentsPanel.test.jsx => SupportingDocumentsPanel.test.jsx} (82%) rename src/js/custom/stockTransferDocuments/components/{StockTransferDocumentsPanel.jsx => SupportingDocumentsPanel.jsx} (87%) rename src/js/custom/stockTransferDocuments/components/{StockTransferDocumentsPanel.scss => SupportingDocumentsPanel.scss} (98%) diff --git a/grails-app/controllers/org/pih/warehouse/UrlMappings.groovy b/grails-app/controllers/org/pih/warehouse/UrlMappings.groovy index dbd7159a40..75f8951c28 100644 --- a/grails-app/controllers/org/pih/warehouse/UrlMappings.groovy +++ b/grails-app/controllers/org/pih/warehouse/UrlMappings.groovy @@ -619,6 +619,12 @@ class UrlMappings { action = [GET: "refreshFilteredBinLocations"] } + // Custom: putaway-document-upload + "/api/custom/putaways/$id/documents"(parseRequest: true) { + controller = "customPutawayDocument" + action = [GET: "list", POST: "upload"] + } + // Requirement API "/api/requirements"(parseRequest: true) { diff --git a/grails-app/controllers/org/pih/warehouse/custom/putawayDocuments/CustomPutawayDocumentController.groovy b/grails-app/controllers/org/pih/warehouse/custom/putawayDocuments/CustomPutawayDocumentController.groovy new file mode 100644 index 0000000000..a69218d04b --- /dev/null +++ b/grails-app/controllers/org/pih/warehouse/custom/putawayDocuments/CustomPutawayDocumentController.groovy @@ -0,0 +1,72 @@ +package org.pih.warehouse.custom.putawayDocuments + +import grails.converters.JSON +import org.pih.warehouse.custom.stockTransferDocuments.UploadValidationException +import org.pih.warehouse.order.Order +import org.springframework.context.i18n.LocaleContextHolder +import org.springframework.web.multipart.MultipartFile +import org.springframework.web.multipart.MultipartHttpServletRequest + +class CustomPutawayDocumentController { + + def customPutawayDocumentService + def messageSource + + def list() { + Order order = Order.get(params.id) + if (!order) { + response.status = 404 + render([errorMessage: "No putaway found for order ID ${params.id}"] as JSON) + return + } + + render([ + data: [ + documentRequired: customPutawayDocumentService.isDocumentRequired(params.id), + documents : customPutawayDocumentService.listDocuments(params.id), + ], + ] as JSON) + } + + def upload() { + MultipartFile fileContents = (request instanceof MultipartHttpServletRequest) + ? (request as MultipartHttpServletRequest).getFile("fileContents") + : null + if (!fileContents || fileContents.empty) { + response.status = 400 + render([errorMessage: "fileContents is required"] as JSON) + return + } + + try { + customPutawayDocumentService.uploadDocument(params.id, fileContents) + render([data: "Document was uploaded successfully"] as JSON) + } catch (UploadValidationException ex) { + log.warn "custom_putaway_document_upload_rejected putawayId=${params.id}" + + " code=${ex.messageCode} originalFilename=${fileContents.originalFilename}" + + " contentType=${fileContents.contentType} size=${fileContents.size}" + response.status = 400 + render([errorMessage: resolveMessage(ex)] as JSON) + } catch (IllegalArgumentException ex) { + log.warn "custom_putaway_document_upload_bad_request putawayId=${params.id} message=${ex.message}" + response.status = 404 + render([errorMessage: ex.message] as JSON) + } catch (Exception ex) { + log.error "custom_putaway_document_upload_failed putawayId=${params.id}", ex + response.status = 500 + render([errorMessage: 'Document upload failed. Please try again.'] as JSON) + } + } + + private String resolveMessage(UploadValidationException ex) { + try { + return messageSource.getMessage( + ex.messageCode, + ex.messageArgs, + ex.message, + LocaleContextHolder.locale) + } catch (Exception ignore) { + return ex.message + } + } +} diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 0f1d2eb466..f310691e5f 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -4961,3 +4961,4 @@ customStockTransferDocument.upload.invalidFilename.error=File name is missing or react.custom.stockTransferDocuments.upload.invalidType.error=Unsupported file type. Allowed: PDF, image, Word, Excel, CSV, ZIP. react.custom.stockTransferDocuments.upload.tooLarge.error=File is too large. react.custom.stockTransferDocuments.upload.invalidFilename.error=File name is missing or invalid. +react.custom.putawayDocuments.required.warning=A document must be attached before this putaway can be completed diff --git a/grails-app/i18n/messages_ru.properties b/grails-app/i18n/messages_ru.properties index 70ab239887..d45d95be0e 100644 --- a/grails-app/i18n/messages_ru.properties +++ b/grails-app/i18n/messages_ru.properties @@ -4919,3 +4919,4 @@ customStockTransferDocument.upload.invalidFilename.error=\u0418\u043c\u044f \u04 react.custom.stockTransferDocuments.upload.invalidType.error=\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0439 \u0442\u0438\u043f \u0444\u0430\u0439\u043b\u0430. \u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u044b: PDF, \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435, Word, Excel, CSV, ZIP. react.custom.stockTransferDocuments.upload.tooLarge.error=\u0424\u0430\u0439\u043b \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0431\u043e\u043b\u044c\u0448\u043e\u0439. react.custom.stockTransferDocuments.upload.invalidFilename.error=\u0418\u043c\u044f \u0444\u0430\u0439\u043b\u0430 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u043b\u0438 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e. +react.custom.putawayDocuments.required.warning=\u041f\u0435\u0440\u0435\u0434 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435\u043c \u0440\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0440\u0438\u043a\u0440\u0435\u043f\u0438\u0442\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 diff --git a/grails-app/i18n/messages_tg.properties b/grails-app/i18n/messages_tg.properties index 9b64425019..7f1b32b013 100644 --- a/grails-app/i18n/messages_tg.properties +++ b/grails-app/i18n/messages_tg.properties @@ -4919,3 +4919,4 @@ customStockTransferDocument.upload.invalidFilename.error=\u041d\u043e\u043c\u043 react.custom.stockTransferDocuments.upload.invalidType.error=\u041d\u0430\u0432\u044a\u0438 \u0444\u0430\u0439\u043b\u0438 \u0434\u0430\u0441\u0442\u0433\u0438\u0440\u043d\u0430\u0448\u0430\u0432\u0430\u043d\u0434\u0430. \u0418\u04b7\u043e\u0437\u0430\u0442 \u0434\u043e\u0434\u0430 \u0448\u0443\u0434\u0430\u0430\u0441\u0442: PDF, \u0442\u0430\u0441\u0432\u0438\u0440, Word, Excel, CSV, ZIP. react.custom.stockTransferDocuments.upload.tooLarge.error=\u0424\u0430\u0439\u043b \u0430\u0437 \u04b3\u0430\u0434 \u0437\u0438\u0451\u0434 \u043a\u0430\u043b\u043e\u043d \u0430\u0441\u0442. react.custom.stockTransferDocuments.upload.invalidFilename.error=\u041d\u043e\u043c\u0438 \u0444\u0430\u0439\u043b \u043d\u0435\u0441\u0442 \u0451 \u043d\u043e\u0434\u0443\u0440\u0443\u0441\u0442 \u0430\u0441\u0442. +react.custom.putawayDocuments.required.warning=\u041f\u0435\u0448 \u0430\u0437 \u043e\u043d \u043a\u0438 \u0438\u043d \u04b7\u043e\u0431\u0430\u04b7\u043e\u0433\u0443\u0437\u043e\u0440\u04e3 \u0430\u043d\u04b7\u043e\u043c \u0434\u043e\u0434\u0430 \u0448\u0430\u0432\u0430\u0434, \u0431\u043e\u044f\u0434 \u04b3\u0443\u04b7\u04b7\u0430\u0442 \u0437\u0430\u043c\u0438\u043c\u0430 \u043a\u0430\u0440\u0434\u0430 \u0448\u0430\u0432\u0430\u0434 diff --git a/grails-app/services/org/pih/warehouse/custom/putawayDocuments/CustomPutawayDocumentService.groovy b/grails-app/services/org/pih/warehouse/custom/putawayDocuments/CustomPutawayDocumentService.groovy new file mode 100644 index 0000000000..9b52023d67 --- /dev/null +++ b/grails-app/services/org/pih/warehouse/custom/putawayDocuments/CustomPutawayDocumentService.groovy @@ -0,0 +1,40 @@ +package org.pih.warehouse.custom.putawayDocuments + +import grails.gorm.transactions.Transactional +import org.pih.warehouse.api.Putaway +import org.pih.warehouse.custom.stockTransferDocuments.CustomStockTransferDocumentService +import org.pih.warehouse.order.Order +import org.springframework.web.multipart.MultipartFile + +@Transactional +class CustomPutawayDocumentService { + + CustomStockTransferDocumentService customStockTransferDocumentService + + Boolean isDocumentRequired(String putawayId) { + Order order = Order.get(putawayId) + return order ? customStockTransferDocumentService.isDocumentRequired(order) : false + } + + List listDocuments(String putawayId) { + Order order = Order.get(putawayId) + return order ? customStockTransferDocumentService.listDocuments(order) : [] + } + + Order uploadDocument(String putawayId, MultipartFile fileContents) { + return customStockTransferDocumentService.uploadDocument(putawayId, fileContents) + } + + // Skips validation when the underlying Order doesn't exist yet — supporting docs can only be + // attached to a saved putaway, so a missing Order can't have a missing-document state. + void validateForCompletion(Putaway putaway) { + if (!putaway?.id) { + return + } + Order order = Order.get(putaway.id) + if (!order) { + return + } + customStockTransferDocumentService.validateForCompletion(order) + } +} diff --git a/grails-app/services/org/pih/warehouse/putaway/PutawayService.groovy b/grails-app/services/org/pih/warehouse/putaway/PutawayService.groovy index af9c97d8fe..31aa218792 100644 --- a/grails-app/services/org/pih/warehouse/putaway/PutawayService.groovy +++ b/grails-app/services/org/pih/warehouse/putaway/PutawayService.groovy @@ -37,6 +37,7 @@ class PutawayService { LocationService locationService InventoryService inventoryService def productAvailabilityService + def customPutawayDocumentService GrailsApplication grailsApplication def getPutawayCandidates(Location location) { @@ -136,6 +137,7 @@ class PutawayService { Order completePutaway(Putaway putaway) { + customPutawayDocumentService.validateForCompletion(putaway) validatePutaway(putaway) // Save the putaway as a transfer order diff --git a/src/js/components/put-away/PutAwayCheckPage.jsx b/src/js/components/put-away/PutAwayCheckPage.jsx index a43adaf53e..faf5310e83 100644 --- a/src/js/components/put-away/PutAwayCheckPage.jsx +++ b/src/js/components/put-away/PutAwayCheckPage.jsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; +import SupportingDocumentsPanel from 'custom/stockTransferDocuments/components/SupportingDocumentsPanel'; import _ from 'lodash'; import PropTypes from 'prop-types'; import { confirmAlert } from 'react-confirm-alert'; @@ -73,11 +74,15 @@ class PutAwayCheckPage extends Component { pivotBy, expanded, location: this.props.location, + customCanComplete: false, }; this.confirmEmptyBin = this.confirmEmptyBin.bind(this); this.confirmLowerQuantity = this.confirmLowerQuantity.bind(this); this.save = this.save.bind(this); + this.handleCanCompleteChange = (canComplete) => { + this.setState({ customCanComplete: canComplete }); + }; } componentWillReceiveProps(nextProps) { @@ -425,6 +430,7 @@ class PutAwayCheckPage extends Component { onClick={() => this.completePutAway()} className="btn btn-outline-secondary btn-xs mr-3" data-testid="complete-putaway-button" + disabled={this.state.customCanComplete === false} > @@ -453,6 +459,16 @@ class PutAwayCheckPage extends Component { ) : null } +
{ this.state.completed @@ -472,6 +488,7 @@ class PutAwayCheckPage extends Component { onClick={() => this.completePutAway()} className="btn btn-outline-primary btn-form float-right btn-xs" data-testid="complete-putaway-button" + disabled={this.state.customCanComplete === false} > diff --git a/src/js/components/stock-transfer/StockTransferCheckPage.jsx b/src/js/components/stock-transfer/StockTransferCheckPage.jsx index 388ab9b1ee..b4300b8c80 100644 --- a/src/js/components/stock-transfer/StockTransferCheckPage.jsx +++ b/src/js/components/stock-transfer/StockTransferCheckPage.jsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; -import StockTransferDocumentsPanel from 'custom/stockTransferDocuments/components/StockTransferDocumentsPanel'; +import SupportingDocumentsPanel from 'custom/stockTransferDocuments/components/SupportingDocumentsPanel'; import _ from 'lodash'; import PropTypes from 'prop-types'; import { confirmAlert } from 'react-confirm-alert'; @@ -309,8 +309,9 @@ class StockTransferSecondPage extends Component { ) : null } - diff --git a/src/js/custom/stockTransferDocuments/__tests__/StockTransferDocumentsPanel.test.jsx b/src/js/custom/stockTransferDocuments/__tests__/SupportingDocumentsPanel.test.jsx similarity index 82% rename from src/js/custom/stockTransferDocuments/__tests__/StockTransferDocumentsPanel.test.jsx rename to src/js/custom/stockTransferDocuments/__tests__/SupportingDocumentsPanel.test.jsx index 76552a1aed..645b363c0b 100644 --- a/src/js/custom/stockTransferDocuments/__tests__/StockTransferDocumentsPanel.test.jsx +++ b/src/js/custom/stockTransferDocuments/__tests__/SupportingDocumentsPanel.test.jsx @@ -4,10 +4,10 @@ import React from 'react'; import { fireEvent, render, screen, waitFor, } from '@testing-library/react'; -import StockTransferDocumentsPanel from 'custom/stockTransferDocuments/components/StockTransferDocumentsPanel'; +import SupportingDocumentsPanel from 'custom/stockTransferDocuments/components/SupportingDocumentsPanel'; import { - fetchStockTransferDocuments, - uploadStockTransferDocument, + fetchDocuments, + uploadDocument, } from 'custom/stockTransferDocuments/utils/api'; import '@testing-library/jest-dom'; @@ -19,6 +19,7 @@ jest.mock('utils/Translate', () => ({ default: ({ defaultMessage }) => {defaultMessage}, })); +const STOCK_TRANSFER_API_BASE = '/api/custom/stockTransfers'; const STOCK_TRANSFER_ID = 'st-123'; const SAMPLE_DOCUMENT = { id: 'doc-1', @@ -42,18 +43,19 @@ const LABELS = { }; const mockFetchResolved = (payload) => { - fetchStockTransferDocuments.mockResolvedValueOnce({ data: { data: payload } }); + fetchDocuments.mockResolvedValueOnce({ data: { data: payload } }); }; const mockFetchRejected = () => { - fetchStockTransferDocuments.mockRejectedValueOnce(new Error('network')); + fetchDocuments.mockRejectedValueOnce(new Error('network')); }; const renderPanel = (overrides = {}) => { const onCanCompleteChange = jest.fn(); const utils = render( - , @@ -92,7 +94,7 @@ beforeEach(() => { jest.clearAllMocks(); }); -describe('StockTransferDocumentsPanel', () => { +describe('SupportingDocumentsPanel', () => { describe('initial load', () => { it('shows the empty state and reports canComplete=true when not required', async () => { mockFetchResolved({ documentRequired: false, documents: [] }); @@ -110,7 +112,7 @@ describe('StockTransferDocumentsPanel', () => { expect(screen.queryByText(LABELS.requiredWarning)).not.toBeInTheDocument(); }); - it('shows the required warning and reports canComplete=false when required with no documents', async () => { + it('shows the default required warning when no override is provided', async () => { mockFetchResolved({ documentRequired: true, documents: [] }); const { onCanCompleteChange } = renderPanel(); @@ -121,6 +123,21 @@ describe('StockTransferDocumentsPanel', () => { expect(onCanCompleteChange).toHaveBeenLastCalledWith(false); }); + it('shows the requiredWarning prop override when provided', async () => { + const customWarning = { + id: 'react.custom.putawayDocuments.required.warning', + defaultMessage: 'A document must be attached before this putaway can be completed', + }; + mockFetchResolved({ documentRequired: true, documents: [] }); + + renderPanel({ requiredWarning: customWarning }); + + await waitFor(() => { + expect(screen.getByText(customWarning.defaultMessage)).toBeInTheDocument(); + }); + expect(screen.queryByText(LABELS.requiredWarning)).not.toBeInTheDocument(); + }); + it('lists loaded documents and reports canComplete=true when required with documents', async () => { mockFetchResolved({ documentRequired: true, documents: [SAMPLE_DOCUMENT] }); @@ -148,7 +165,7 @@ describe('StockTransferDocumentsPanel', () => { describe('uploading', () => { it('uploads pending files and refreshes the list on success', async () => { mockFetchResolved({ documentRequired: true, documents: [] }); - uploadStockTransferDocument.mockResolvedValueOnce({ + uploadDocument.mockResolvedValueOnce({ data: { data: 'Document was uploaded successfully' }, }); mockFetchResolved({ documentRequired: true, documents: [SAMPLE_DOCUMENT] }); @@ -164,14 +181,18 @@ describe('StockTransferDocumentsPanel', () => { fireEvent.click(screen.getByRole('button', { name: LABELS.uploadButton })); await waitFor(() => { - expect(uploadStockTransferDocument).toHaveBeenCalledWith(STOCK_TRANSFER_ID, file); + expect(uploadDocument).toHaveBeenCalledWith( + STOCK_TRANSFER_API_BASE, + STOCK_TRANSFER_ID, + file, + ); }); expect(await screen.findByText(SAMPLE_DOCUMENT.name)).toBeInTheDocument(); }); it('keeps only the failed files pending and shows partialError on partial failure', async () => { mockFetchResolved({ documentRequired: true, documents: [] }); - uploadStockTransferDocument + uploadDocument .mockResolvedValueOnce({ data: { data: 'ok' } }) .mockRejectedValueOnce(new Error('boom')) .mockResolvedValueOnce({ data: { data: 'ok' } }); @@ -191,12 +212,15 @@ describe('StockTransferDocumentsPanel', () => { fireEvent.click(screen.getByRole('button', { name: LABELS.uploadButton })); await waitFor(() => { - expect(uploadStockTransferDocument).toHaveBeenCalledTimes(3); + expect(uploadDocument).toHaveBeenCalledTimes(3); }); - expect(uploadStockTransferDocument).toHaveBeenNthCalledWith(1, STOCK_TRANSFER_ID, fileA); - expect(uploadStockTransferDocument).toHaveBeenNthCalledWith(2, STOCK_TRANSFER_ID, fileB); - expect(uploadStockTransferDocument).toHaveBeenNthCalledWith(3, STOCK_TRANSFER_ID, fileC); + expect(uploadDocument) + .toHaveBeenNthCalledWith(1, STOCK_TRANSFER_API_BASE, STOCK_TRANSFER_ID, fileA); + expect(uploadDocument) + .toHaveBeenNthCalledWith(2, STOCK_TRANSFER_API_BASE, STOCK_TRANSFER_ID, fileB); + expect(uploadDocument) + .toHaveBeenNthCalledWith(3, STOCK_TRANSFER_API_BASE, STOCK_TRANSFER_ID, fileC); await waitFor(() => { expect(screen.getByText(LABELS.partialUploadError)).toBeInTheDocument(); @@ -208,11 +232,11 @@ describe('StockTransferDocumentsPanel', () => { it('does not re-send already-uploaded files when the user retries after a partial failure', async () => { mockFetchResolved({ documentRequired: true, documents: [] }); - uploadStockTransferDocument + uploadDocument .mockResolvedValueOnce({ data: { data: 'ok' } }) // a.pdf - .mockRejectedValueOnce(new Error('boom')); // b.pdf + .mockRejectedValueOnce(new Error('boom')); // b.pdf mockFetchResolved({ documentRequired: true, documents: [] }); - uploadStockTransferDocument + uploadDocument .mockResolvedValueOnce({ data: { data: 'ok' } }); // b.pdf retry mockFetchResolved({ documentRequired: true, documents: [SAMPLE_DOCUMENT] }); @@ -232,15 +256,15 @@ describe('StockTransferDocumentsPanel', () => { fireEvent.click(screen.getByRole('button', { name: LABELS.uploadButton })); await waitFor(() => { - expect(uploadStockTransferDocument).toHaveBeenCalledTimes(3); + expect(uploadDocument).toHaveBeenCalledTimes(3); }); - const calledFiles = uploadStockTransferDocument.mock.calls.map(([, file]) => file.name); + const calledFiles = uploadDocument.mock.calls.map(([, , file]) => file.name); expect(calledFiles).toEqual(['a.pdf', 'b.pdf', 'b.pdf']); }); it('shows an upload error and keeps pending files when upload fails', async () => { mockFetchResolved({ documentRequired: true, documents: [] }); - uploadStockTransferDocument.mockRejectedValueOnce(new Error('boom')); + uploadDocument.mockRejectedValueOnce(new Error('boom')); const { container } = renderPanel(); @@ -279,7 +303,7 @@ describe('StockTransferDocumentsPanel', () => { await waitFor(() => { expect(screen.getByText(LABELS.invalidTypeError)).toBeInTheDocument(); }); - expect(uploadStockTransferDocument).not.toHaveBeenCalled(); + expect(uploadDocument).not.toHaveBeenCalled(); }); it('rejects an oversize file with an inline warning', async () => { @@ -304,7 +328,7 @@ describe('StockTransferDocumentsPanel', () => { await waitFor(() => { expect(screen.getByText(LABELS.tooLargeError)).toBeInTheDocument(); }); - expect(uploadStockTransferDocument).not.toHaveBeenCalled(); + expect(uploadDocument).not.toHaveBeenCalled(); }); it('removes a pending file when the remove button is clicked', async () => { diff --git a/src/js/custom/stockTransferDocuments/components/StockTransferDocumentsPanel.jsx b/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.jsx similarity index 87% rename from src/js/custom/stockTransferDocuments/components/StockTransferDocumentsPanel.jsx rename to src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.jsx index 9c192a3828..8301b7e21f 100644 --- a/src/js/custom/stockTransferDocuments/components/StockTransferDocumentsPanel.jsx +++ b/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.jsx @@ -1,10 +1,11 @@ import React, { - useCallback, useEffect, useRef, useState, + useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { - fetchStockTransferDocuments, - uploadStockTransferDocument, + buildDocumentsUrl, + fetchDocuments, + uploadDocument, } from 'custom/stockTransferDocuments/utils/api'; import M from 'custom/stockTransferDocuments/utils/messages'; import PropTypes from 'prop-types'; @@ -12,9 +13,9 @@ import Dropzone from 'react-dropzone'; import Translate from 'utils/Translate'; -import 'custom/stockTransferDocuments/components/StockTransferDocumentsPanel.scss'; +import 'custom/stockTransferDocuments/components/SupportingDocumentsPanel.scss'; -const BLOCK = 'custom-stock-transfer-documents'; +const BLOCK = 'custom-supporting-documents'; const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; @@ -44,8 +45,10 @@ Warning.propTypes = { defaultMessage: PropTypes.string.isRequired, }; -const StockTransferDocumentsPanel = ({ - stockTransferId, +const SupportingDocumentsPanel = ({ + entityId, + apiBasePath, + requiredWarning, disabled, onCanCompleteChange, }) => { @@ -63,6 +66,11 @@ const StockTransferDocumentsPanel = ({ isMountedRef.current = false; }, []); + const effectiveRequiredWarning = useMemo( + () => requiredWarning || M.requiredWarning, + [requiredWarning], + ); + const reportCanComplete = useCallback((required, docs) => { if (onCanCompleteChange) { onCanCompleteChange(!required || docs.length > 0); @@ -70,8 +78,8 @@ const StockTransferDocumentsPanel = ({ }, [onCanCompleteChange]); const loadDocuments = useCallback(() => { - if (!stockTransferId) return; - fetchStockTransferDocuments(stockTransferId) + if (!entityId) return; + fetchDocuments(apiBasePath, entityId) .then((response) => { if (!isMountedRef.current) return; const payload = response?.data?.data ?? {}; @@ -87,7 +95,7 @@ const StockTransferDocumentsPanel = ({ setFetchError(true); if (onCanCompleteChange) onCanCompleteChange(false); }); - }, [stockTransferId, reportCanComplete, onCanCompleteChange]); + }, [apiBasePath, entityId, reportCanComplete, onCanCompleteChange]); useEffect(() => { loadDocuments(); @@ -125,7 +133,7 @@ const StockTransferDocumentsPanel = ({ }, []); const uploadPendingFiles = useCallback(async () => { - if (pendingFiles.length === 0 || !stockTransferId) return; + if (pendingFiles.length === 0 || !entityId) return; const attempted = pendingFiles; setUploading(true); setUploadErrorMessage(null); @@ -135,7 +143,7 @@ const StockTransferDocumentsPanel = ({ const failed = await attempted.reduce( (chain, file) => chain.then(async (acc) => { try { - await uploadStockTransferDocument(stockTransferId, file); + await uploadDocument(apiBasePath, entityId, file); return acc; } catch { return [...acc, file]; @@ -158,7 +166,7 @@ const StockTransferDocumentsPanel = ({ if (failed.length < attempted.length) { loadDocuments(); } - }, [pendingFiles, stockTransferId, loadDocuments]); + }, [apiBasePath, pendingFiles, entityId, loadDocuments]); const showRequiredWarning = documentRequired && documents.length === 0; @@ -175,7 +183,7 @@ const StockTransferDocumentsPanel = ({ >

- {collapsed ? '\u25B6' : '\u25BC'} + {collapsed ? '▶' : '▼'} {documentRequired && ( @@ -190,8 +198,8 @@ const StockTransferDocumentsPanel = ({ <> {showRequiredWarning && ( )} @@ -295,16 +303,23 @@ const StockTransferDocumentsPanel = ({ ); }; -StockTransferDocumentsPanel.propTypes = { - stockTransferId: PropTypes.string, +SupportingDocumentsPanel.propTypes = { + entityId: PropTypes.string, + apiBasePath: PropTypes.string.isRequired, + requiredWarning: PropTypes.shape({ + id: PropTypes.string.isRequired, + defaultMessage: PropTypes.string.isRequired, + }), disabled: PropTypes.bool, onCanCompleteChange: PropTypes.func, }; -StockTransferDocumentsPanel.defaultProps = { - stockTransferId: null, +SupportingDocumentsPanel.defaultProps = { + entityId: null, + requiredWarning: null, disabled: false, onCanCompleteChange: null, }; -export default StockTransferDocumentsPanel; +export { buildDocumentsUrl }; +export default SupportingDocumentsPanel; diff --git a/src/js/custom/stockTransferDocuments/components/StockTransferDocumentsPanel.scss b/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.scss similarity index 98% rename from src/js/custom/stockTransferDocuments/components/StockTransferDocumentsPanel.scss rename to src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.scss index 3a309dbfdb..48007abf50 100644 --- a/src/js/custom/stockTransferDocuments/components/StockTransferDocumentsPanel.scss +++ b/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.scss @@ -1,4 +1,4 @@ -.custom-stock-transfer-documents { +.custom-supporting-documents { border: 1px solid #dee2e6; border-radius: 4px; padding: 12px 16px; diff --git a/src/js/custom/stockTransferDocuments/utils/api.js b/src/js/custom/stockTransferDocuments/utils/api.js index 93caa79799..fbec7ba5df 100644 --- a/src/js/custom/stockTransferDocuments/utils/api.js +++ b/src/js/custom/stockTransferDocuments/utils/api.js @@ -1,15 +1,15 @@ import apiClient from 'utils/apiClient'; -export const stockTransferDocumentsUrl = (stockTransferId) => - `/api/custom/stockTransfers/${stockTransferId}/documents`; +export const buildDocumentsUrl = (apiBasePath, entityId) => + `${apiBasePath}/${entityId}/documents`; -export const fetchStockTransferDocuments = (stockTransferId) => - apiClient.get(stockTransferDocumentsUrl(stockTransferId)); +export const fetchDocuments = (apiBasePath, entityId) => + apiClient.get(buildDocumentsUrl(apiBasePath, entityId)); -export const uploadStockTransferDocument = (stockTransferId, file) => { +export const uploadDocument = (apiBasePath, entityId, file) => { const formData = new FormData(); formData.append('fileContents', file); - return apiClient.post(stockTransferDocumentsUrl(stockTransferId), formData, { + return apiClient.post(buildDocumentsUrl(apiBasePath, entityId), formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); }; From 2c51305b579df3169025a9d8fb4eb4baa0c8a988 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 19 May 2026 19:11:53 +0100 Subject: [PATCH 3/4] feat: add required document warning for stock transfer completion --- .../stock-transfer/StockTransferCheckPage.jsx | 5 +++++ .../__tests__/SupportingDocumentsPanel.test.jsx | 14 ++++++++++---- .../components/SupportingDocumentsPanel.jsx | 14 ++++---------- .../stockTransferDocuments/utils/messages.js | 5 ----- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/js/components/stock-transfer/StockTransferCheckPage.jsx b/src/js/components/stock-transfer/StockTransferCheckPage.jsx index b4300b8c80..7d0a0b053b 100644 --- a/src/js/components/stock-transfer/StockTransferCheckPage.jsx +++ b/src/js/components/stock-transfer/StockTransferCheckPage.jsx @@ -312,6 +312,11 @@ class StockTransferSecondPage extends Component { diff --git a/src/js/custom/stockTransferDocuments/__tests__/SupportingDocumentsPanel.test.jsx b/src/js/custom/stockTransferDocuments/__tests__/SupportingDocumentsPanel.test.jsx index 645b363c0b..4c1e456825 100644 --- a/src/js/custom/stockTransferDocuments/__tests__/SupportingDocumentsPanel.test.jsx +++ b/src/js/custom/stockTransferDocuments/__tests__/SupportingDocumentsPanel.test.jsx @@ -28,11 +28,16 @@ const SAMPLE_DOCUMENT = { uri: '/openboxes/document/download/doc-1', }; +const REQUIRED_WARNING = { + id: 'react.custom.stockTransferDocuments.required.warning', + defaultMessage: + 'A document must be attached before this stock transfer can be completed', +}; + const LABELS = { panelTitle: 'Supporting documents', empty: 'No documents attached yet', - requiredWarning: - 'A document must be attached before this stock transfer can be completed', + requiredWarning: REQUIRED_WARNING.defaultMessage, fetchError: 'Unable to load documents. Please refresh to try again.', uploadError: 'Document upload failed', partialUploadError: 'Some documents failed to upload. The remaining files above can be retried.', @@ -56,6 +61,7 @@ const renderPanel = (overrides = {}) => { , @@ -112,7 +118,7 @@ describe('SupportingDocumentsPanel', () => { expect(screen.queryByText(LABELS.requiredWarning)).not.toBeInTheDocument(); }); - it('shows the default required warning when no override is provided', async () => { + it('shows the requiredWarning passed via props and reports canComplete=false', async () => { mockFetchResolved({ documentRequired: true, documents: [] }); const { onCanCompleteChange } = renderPanel(); @@ -123,7 +129,7 @@ describe('SupportingDocumentsPanel', () => { expect(onCanCompleteChange).toHaveBeenLastCalledWith(false); }); - it('shows the requiredWarning prop override when provided', async () => { + it('renders a different requiredWarning when the caller supplies one', async () => { const customWarning = { id: 'react.custom.putawayDocuments.required.warning', defaultMessage: 'A document must be attached before this putaway can be completed', diff --git a/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.jsx b/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.jsx index 8301b7e21f..387c8ecb95 100644 --- a/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.jsx +++ b/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.jsx @@ -1,5 +1,5 @@ import React, { - useCallback, useEffect, useMemo, useRef, useState, + useCallback, useEffect, useRef, useState, } from 'react'; import { @@ -66,11 +66,6 @@ const SupportingDocumentsPanel = ({ isMountedRef.current = false; }, []); - const effectiveRequiredWarning = useMemo( - () => requiredWarning || M.requiredWarning, - [requiredWarning], - ); - const reportCanComplete = useCallback((required, docs) => { if (onCanCompleteChange) { onCanCompleteChange(!required || docs.length > 0); @@ -198,8 +193,8 @@ const SupportingDocumentsPanel = ({ <> {showRequiredWarning && ( )} @@ -309,14 +304,13 @@ SupportingDocumentsPanel.propTypes = { requiredWarning: PropTypes.shape({ id: PropTypes.string.isRequired, defaultMessage: PropTypes.string.isRequired, - }), + }).isRequired, disabled: PropTypes.bool, onCanCompleteChange: PropTypes.func, }; SupportingDocumentsPanel.defaultProps = { entityId: null, - requiredWarning: null, disabled: false, onCanCompleteChange: null, }; diff --git a/src/js/custom/stockTransferDocuments/utils/messages.js b/src/js/custom/stockTransferDocuments/utils/messages.js index 936a487dbf..c77a9919be 100644 --- a/src/js/custom/stockTransferDocuments/utils/messages.js +++ b/src/js/custom/stockTransferDocuments/utils/messages.js @@ -35,11 +35,6 @@ const STOCK_TRANSFER_DOCUMENTS_MESSAGES = { id: 'react.custom.stockTransferDocuments.fetch.error', defaultMessage: 'Unable to load documents. Please refresh to try again.', }, - requiredWarning: { - id: 'react.custom.stockTransferDocuments.required.warning', - defaultMessage: - 'A document must be attached before this stock transfer can be completed', - }, invalidTypeError: { id: 'react.custom.stockTransferDocuments.upload.invalidType.error', defaultMessage: 'Unsupported file type. Allowed: PDF, image, Word, Excel, CSV, ZIP.', From d319553ca6e9b8f7583c9c336ce72420ab5d1e49 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Thu, 21 May 2026 08:21:56 +0100 Subject: [PATCH 4/4] refactor: rename folder --- .../__tests__/SupportingDocumentsPanel.test.jsx | 0 .../components/SupportingDocumentsPanel.jsx | 0 .../components/SupportingDocumentsPanel.scss | 0 .../{stockTransferDocuments => supportingDocuments}/utils/api.js | 0 .../utils/messages.js | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename src/js/custom/{stockTransferDocuments => supportingDocuments}/__tests__/SupportingDocumentsPanel.test.jsx (100%) rename src/js/custom/{stockTransferDocuments => supportingDocuments}/components/SupportingDocumentsPanel.jsx (100%) rename src/js/custom/{stockTransferDocuments => supportingDocuments}/components/SupportingDocumentsPanel.scss (100%) rename src/js/custom/{stockTransferDocuments => supportingDocuments}/utils/api.js (100%) rename src/js/custom/{stockTransferDocuments => supportingDocuments}/utils/messages.js (100%) diff --git a/src/js/custom/stockTransferDocuments/__tests__/SupportingDocumentsPanel.test.jsx b/src/js/custom/supportingDocuments/__tests__/SupportingDocumentsPanel.test.jsx similarity index 100% rename from src/js/custom/stockTransferDocuments/__tests__/SupportingDocumentsPanel.test.jsx rename to src/js/custom/supportingDocuments/__tests__/SupportingDocumentsPanel.test.jsx diff --git a/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.jsx b/src/js/custom/supportingDocuments/components/SupportingDocumentsPanel.jsx similarity index 100% rename from src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.jsx rename to src/js/custom/supportingDocuments/components/SupportingDocumentsPanel.jsx diff --git a/src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.scss b/src/js/custom/supportingDocuments/components/SupportingDocumentsPanel.scss similarity index 100% rename from src/js/custom/stockTransferDocuments/components/SupportingDocumentsPanel.scss rename to src/js/custom/supportingDocuments/components/SupportingDocumentsPanel.scss diff --git a/src/js/custom/stockTransferDocuments/utils/api.js b/src/js/custom/supportingDocuments/utils/api.js similarity index 100% rename from src/js/custom/stockTransferDocuments/utils/api.js rename to src/js/custom/supportingDocuments/utils/api.js diff --git a/src/js/custom/stockTransferDocuments/utils/messages.js b/src/js/custom/supportingDocuments/utils/messages.js similarity index 100% rename from src/js/custom/stockTransferDocuments/utils/messages.js rename to src/js/custom/supportingDocuments/utils/messages.js