OBPIH-5932 Add validation for expiration date in inbound returns#4454
Conversation
…g the items in inbound return - remove the updateFormValues() which was manually updating state of our form - fetch inventoryItem of a given product and lot to check to validate expiration date
| return this.saveStockTransfer(returnItems, null) | ||
| .catch(() => this.props.hideSpinner()); | ||
| async nextPage(formValues) { | ||
| this.saveStockTransferInCurrentStep(formValues, this.state.inboundReturn.status !== 'PLACED' ? 'PLACED' : null) |
There was a problem hiding this comment.
I think it will be worth creating an enum with statuses including this PLACED instead of using magic strings
| for (const it of itemsWithLotAndExpirationDate) { | ||
| const { data } = await ProductApi.getInventoryItem(it.product?.id, it.lotNumber); | ||
| if (data.inventoryItem && data.inventoryItem.expirationDate !== it.expirationDate) { | ||
| isSomeItemsDontMatchExpirationDate = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // After find at least a single instance where expiration date we are trying to save | ||
| // does not match the existing inventoryItem expiration date, we want to inform the user | ||
| // that certain updates to th expiration date in the system will be performed | ||
| if (isSomeItemsDontMatchExpirationDate) { | ||
| const isConfirmed = await this.confirmExpirationDateSave(); | ||
| if (!isConfirmed) { | ||
| return Promise.reject(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Wouldn't it be better to place this code from if (isSomeItemsDontMatchExpirationDate) { ... } directly in if (data.inventoryItem && data.inventoryItem.expirationDate !== it.expirationDate)?
Basically, you are trying to find at least one item that doesn't match the expiration date, so I think there is no need to use an additional let declaration, and then check if we change the value of this particular variable because within this loop we already know if we found something that doesn't match expiration date.
So I think it should be something similar to (not tested):
for (const it of itemsWithLotAndExpirationDate) {
const { data } = await ProductApi.getInventoryItem(it.product?.id, it.lotNumber);
if (data.inventoryItem && data.inventoryItem.expirationDate !== it.expirationDate) {
const isConfirmed = await this.confirmExpirationDateSave();
if (!isConfirmed) {
return Promise.reject();
}
}
}
There was a problem hiding this comment.
I agree with Alan's comment here
| if (hasErrors) { | ||
| const isConfirmed = await this.confirmValidationErrorOnExit(); | ||
| if (!isConfirmed) { | ||
| return; | ||
| } | ||
| } else { | ||
| try { | ||
| await this.saveStockTransferInCurrentStep(formValues); | ||
| } catch (error) { | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
maybe it would be better to check if (!hasErrors) { ... return } to avoid nesting ifs, and because of that you can avoid using else 😆
| async previousPage(values, invalid) { | ||
| if (!invalid) { | ||
| this.saveStockTransferInCurrentStep(values.returnItems) | ||
| .then(() => this.props.previousPage(this.state.inboundReturn)); | ||
| await this.saveStockTransfer(values.returnItems, null); | ||
| } else { | ||
| confirmAlert({ | ||
| title: this.props.translate('react.inboundReturns.confirmPreviousPage.label', 'Validation error'), | ||
| message: this.props.translate('react.inboundReturns.confirmPreviousPage.message', 'Cannot save due to validation error on page'), | ||
| buttons: [ | ||
| { | ||
| label: this.props.translate('react.inboundReturns.confirmPreviousPage.correctError.label', 'Correct error'), | ||
| }, | ||
| { | ||
| label: this.props.translate('react.inboundReturns.confirmPreviousPage.continue.label', 'Continue (lose unsaved work)'), | ||
| onClick: () => this.props.previousPage(this.state.inboundReturn), | ||
| }, | ||
| ], | ||
| }); | ||
| const correctErrors = await this.confirmValidationErrorOnPreviousPage(); | ||
| if (correctErrors) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
maybe return without else will be more readable?
+ with that change if (correctErrors) won't be nested in the else.
| label: this.props.translate('react.default.no.label', 'No'), | ||
| }, | ||
| ], | ||
| confirmValidationErrorOnExit() { |
There was a problem hiding this comment.
I regret that this component is not a functional one 😢
When it is functional we will be able to move all of those confirmation alerts to a hook, and make this component a little bit shorter (those alerts have close to 100 lines). Maybe other developers may have any idea how we can make this part of the code a little bit cleaner.
There was a problem hiding this comment.
Maybe other developers may have any idea how we can make this part of the code a little bit cleaner.
I can see that the only difference between those are either button labels or title/message.
We wouldn't achieve much by creating a generic function that would accept the title, message, buttons, because the amount of the props we'd have to provide would probably be the same, but I would have to see it after the refactor.
A second approach might be to have one alert method, and we could potentially have a map that would store the labels and we could access it depending on an alert key, e.g.:
const Alerts = {
VALIDATION_ON_EXIT: 'VALIDATION_ON_EXIT',
...
};
const alertsMap = {
[Alerts.VALIDATION_ON_EXIT]: {
title: '...'
message: '...',
resolveButtonLabel: '...',
rejectButtonLabel: '...',
}
}
runConfirmAlert(key) {
const labels = alertsMap[key];
return new Promise((resolve) => {
confirmAlert({
title: labels?.title,
message: labels?.message,
willUnmount: () => resolve(false),
buttons: [
{
label: labels?.resolveButtonLabel
onClick: () => resolve(true),
},
{
label: labels?.rejectButtonLabel
onClick: () => resolve(false),
},
],
});
});
}There was a problem hiding this comment.
I decided not to proceed with it, I feel like generalizing it will make the code a bit less maintainable.
I know these methods are very similar but I think we will benefit more if we leave it separated as we have it now.
There was a problem hiding this comment.
@drodzewicz OK. I've just wanted to show @alannadolny that there is a possibility to make it generic if we would want to. I'm fine with leaving it, as this component anyway needs to be rewritten anytime soon hopefully, but on the other hand I would not agree that my version makes it difficult to maintain.
| label: this.props.translate('react.default.no.label', 'No'), | ||
| }, | ||
| ], | ||
| confirmValidationErrorOnExit() { |
There was a problem hiding this comment.
Maybe other developers may have any idea how we can make this part of the code a little bit cleaner.
I can see that the only difference between those are either button labels or title/message.
We wouldn't achieve much by creating a generic function that would accept the title, message, buttons, because the amount of the props we'd have to provide would probably be the same, but I would have to see it after the refactor.
A second approach might be to have one alert method, and we could potentially have a map that would store the labels and we could access it depending on an alert key, e.g.:
const Alerts = {
VALIDATION_ON_EXIT: 'VALIDATION_ON_EXIT',
...
};
const alertsMap = {
[Alerts.VALIDATION_ON_EXIT]: {
title: '...'
message: '...',
resolveButtonLabel: '...',
rejectButtonLabel: '...',
}
}
runConfirmAlert(key) {
const labels = alertsMap[key];
return new Promise((resolve) => {
confirmAlert({
title: labels?.title,
message: labels?.message,
willUnmount: () => resolve(false),
buttons: [
{
label: labels?.resolveButtonLabel
onClick: () => resolve(true),
},
{
label: labels?.rejectButtonLabel
onClick: () => resolve(false),
},
],
});
});
}| for (const it of itemsWithLotAndExpirationDate) { | ||
| const { data } = await ProductApi.getInventoryItem(it.product?.id, it.lotNumber); | ||
| if (data.inventoryItem && data.inventoryItem.expirationDate !== it.expirationDate) { | ||
| isSomeItemsDontMatchExpirationDate = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // After find at least a single instance where expiration date we are trying to save | ||
| // does not match the existing inventoryItem expiration date, we want to inform the user | ||
| // that certain updates to th expiration date in the system will be performed | ||
| if (isSomeItemsDontMatchExpirationDate) { | ||
| const isConfirmed = await this.confirmExpirationDateSave(); | ||
| if (!isConfirmed) { | ||
| return Promise.reject(); | ||
| } | ||
| } |
There was a problem hiding this comment.
I agree with Alan's comment here
| async save(formValues) { | ||
| this.saveStockTransferInCurrentStep(formValues) | ||
| .then(() => { | ||
| Alert.success( |
There was a problem hiding this comment.
let's use our notification factory.
| label: this.props.translate('react.default.no.label', 'No'), | ||
| }, | ||
| ], | ||
| async save(formValues) { |
There was a problem hiding this comment.
if it's made async how about using await instead of .then(() => ...?
| try { | ||
| await this.saveStockTransferInCurrentStep(formValues); | ||
| } catch (error) { | ||
| return; |
There was a problem hiding this comment.
why do we actually want to do nothing when an error happens? Shouldn't we let the error handler handle the error and display it to a user?
There was a problem hiding this comment.
the error will be displayed to the user, because when using our custom apiClient we have a response interceptor that displays an alert when we get a server error.
I am returning here to prevent the function going further, because the last step is redirecting to stock movement show page and if we have an error we don;t want to do that.
There was a problem hiding this comment.
@drodzewicz ok. I thought it would work like in Java (if we use the catch, we'd override the error handler and not let it handle the error).
There was a problem hiding this comment.
The difference between our grails error handler and frontend axios error handler is the order when they are called.
In case of grails, the global error handler is called as the last method, so when we catch a method manually it does not propagate to the global error handler.
For our custom apiClient on frontend we set the error handler as a response interceptor which is always called first. It handles the error and then returns a rejected promise.
src/js/api/urls.js
Outdated
| export const STOCK_TRANSFER_API = `${API}/stockTransfers`; | ||
| export const STOCK_TRANSFER_DELETE = id => `${STOCK_TRANSFER_API}/${id}`; | ||
| export const STOCK_TRANSFER_UPDATE = id => `${STOCK_TRANSFER_API}/${id}`; | ||
| export const STOCK_TRANSFER_GET = id => `${STOCK_TRANSFER_API}/${id}`; |
There was a problem hiding this comment.
I apologize not for catching it earlier, but I think we used to name such urls as ..._BY_ID, not to repeat the urls for update, delete, get, read, so I think it should be:
export const STOCK_TRANSFER_BY_ID = id => `${STOCK_TRANSFER_API}/${id}`;There was a problem hiding this comment.
Someone only did it for stock movements, I think it could be just STOCK_TRANSFER, and then you just have:
delete(STOCK_TRANSFER), update(STOCK_TRANSFER) etc.
There was a problem hiding this comment.
It was done only for stock movements, because it was the moment in which we decided to avoid using DELETE, GET, POST, and PUT. I remember we created a ticket for the refactor of the rest of the namings (but I could be wrong).
src/js/api/urls.js
Outdated
| @@ -30,6 +30,9 @@ export const STOCK_MOVEMENT_ITEM_REMOVE = id => `${STOCK_MOVEMENT_ITEM_API}/${id | |||
| // STOCK TRANSFER | |||
| export const STOCK_TRANSFER_API = `${API}/stockTransfers`; | |||
| export const STOCK_TRANSFER_DELETE = id => `${STOCK_TRANSFER_API}/${id}`; | |||
There was a problem hiding this comment.
yeah, I think you've followed what was there, but I think this should also be _BY_ID. (Like for STOCK_MOVEMENT)
There was a problem hiding this comment.
or perhaps just STOCK_TRANSFER without _BY_ID :)
There was a problem hiding this comment.
basically, it would be better to name it as it was done for stock movements
export const STOCK_TRANSFER_API = `${API}/stockTransfers`;
export const STOCK_TRANSFER_BY_ID = id => `${STOCK_TRANSFER_API}/${id}`;
it couldn't be just STOCK_TRANSFER, because we have to separate the URLs for sending requests and redirects.
There was a problem hiding this comment.
Oh, that's why it was done like that, forgot about redirects :/
There was a problem hiding this comment.
but wait, should not be used for this applicationUrls?
4b4a9cd to
8df0177
Compare
- added StockTransferStatus consts - added StockTrasnferApi and replaced regular apiClient on AddItemsPage - fixed some order of method calls
8df0177 to
87d5941
Compare
Some changes that are worth pointing out
Removed updateFormValues
After inspecting it closes I noticed that this function does not serve any purpose than to make the page probably slower and confuse the developer.
Before,
updateFormValues(values)would have been called on each table field onBlurr callback to update the state of formValues. We do not need to manually update the formValues, the ReactForm does it for us and keeps track of the state. When we perform any kind of a save on this addItemsPage, the state that we are providing as a payload for update request is values prop which is provided by renderProps of thereact-final-formcomponent, we never used formVlaues anywhere on the page but we were updating it for some reason.Separated confirm modals into separate methods and made ten return promises instead
I had a little bit of a hard time trying to figure out how to handle two consecutive confirmation modals and got and idea.
So in javascript, in the browser, we have a
window.confirm("Some question)which upon calling stalls the code in the browser and awaits users response, then returns a boolean value for which option the user clicked (ok or cancel).I thought that I could mimic this behavior by making this confirmation return a promise and resolve to
truewhen user clicks ok andfalsewhen user clicks cancelClean up save stock movement method
in all cases be it
nextPage,save,save and exit, we are performing the same save actions with the same validation so I moved common code to thesaveStockTransferInCurrentStepand call it in all of the mentioned methods. The difference in these methods are what happens after, for nextPage we trigger next page method for save we display a success alert and for save and exit we are redirecting to the show page.For more information on the approach I took to validating expiration date please check out the comment under the ticket