Skip to content

Commit a02df13

Browse files
feat: maximum order quantity
1 parent d262291 commit a02df13

25 files changed

Lines changed: 681 additions & 19 deletions

app/assets/javascripts/application_legacy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
//= require_self
1919
//= require big
2020
//= require units-converter
21+
//= require validation-utils
2122
//= require migrate-units-form
2223
//= require article-form
2324
//= require unit-conversion-field

app/assets/javascripts/article-form.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class ArticleForm {
1313
this.supplierUnitSelect$ = $(`#${this.unitFieldsIdPrefix}_supplier_order_unit`, this.articleForm$);
1414
this.unitRatiosTable$ = $('#fc_base_price', this.articleForm$);
1515
this.minimumOrderQuantity$ = $(`#${this.unitFieldsIdPrefix}_minimum_order_quantity`, this.articleForm$);
16+
this.maximumOrderQuantity$ = $(`#${this.unitFieldsIdPrefix}_maximum_order_quantity`, this.articleForm$);
1617
this.billingUnit$ = $(`#${this.unitFieldsIdPrefix}_billing_unit`, this.articleForm$);
1718
this.groupOrderGranularity$ = $(`#${this.unitFieldsIdPrefix}_group_order_granularity`, this.articleForm$);
1819
this.groupOrderUnit$ = $(`#${this.unitFieldsIdPrefix}_group_order_unit`, this.articleForm$);
@@ -60,7 +61,7 @@ class ArticleForm {
6061
const tax = parseFloat(this.tax$.val());
6162
const deposit = parseFloat(this.deposit$.val());
6263
const grossPrice = (price + deposit) * (tax / 100 + 1);
63-
const fcPrice = grossPrice * (this.priceMarkup / 100 + 1);
64+
const fcPrice = grossPrice * (this.priceMarkup / 100 + 1);
6465
const priceUnitLabel = this.getUnitLabel(this.priceUnit$.val());
6566
this.fcPrice$.find('.price_value').text(isNaN(fcPrice) ? '?' : I18n.l('currency', fcPrice));
6667
this.fcPrice$.find('.price_per_text').toggle(priceUnitLabel.trim() !== '');
@@ -88,7 +89,7 @@ class ArticleForm {
8889
this.loadRatios();
8990
this.undoPriceConversion();
9091
this.undoOrderAndReceivedUnitsConversion();
91-
} catch(err) {
92+
} catch (err) {
9293
e.preventDefault();
9394
throw err;
9495
}
@@ -191,6 +192,7 @@ class ArticleForm {
191192
initializeRegularFormFields() {
192193
this.unit$.change(() => {
193194
this.setMinimumOrderUnitDisplay();
195+
this.setMaximumOrderUnitDisplay();
194196
this.updateAvailableBillingAndGroupOrderUnits();
195197
this.updateUnitMultiplierLabels();
196198
this.updateCustomUnitWarning();
@@ -204,6 +206,28 @@ class ArticleForm {
204206
this.updateCustomUnitWarning();
205207
});
206208
this.onSupplierUnitChanged();
209+
210+
// Add validation for minimum/maximum order quantity
211+
this.initializeOrderQuantityValidation();
212+
}
213+
214+
initializeOrderQuantityValidation() {
215+
const validateOrderQuantities = () => {
216+
const minValue = parseFloat(this.minimumOrderQuantity$.val()) || 0;
217+
const maxValue = parseFloat(this.maximumOrderQuantity$.val()) || Infinity;
218+
219+
if (this.minimumOrderQuantity$.val() && this.maximumOrderQuantity$.val() && minValue > maxValue) {
220+
const errorMessage = I18n.t('activerecord.errors.models.article_version.attributes.minimum_order_quantity.greater_than_maximum');
221+
this.minimumOrderQuantity$[0].setCustomValidity(errorMessage);
222+
this.maximumOrderQuantity$[0].setCustomValidity(errorMessage);
223+
} else {
224+
this.minimumOrderQuantity$[0].setCustomValidity('');
225+
this.maximumOrderQuantity$[0].setCustomValidity('');
226+
}
227+
};
228+
229+
this.minimumOrderQuantity$.on('input change', validateOrderQuantities);
230+
this.maximumOrderQuantity$.on('input change', validateOrderQuantities);
207231
}
208232

209233
updateCustomUnitWarning() {
@@ -227,6 +251,7 @@ class ArticleForm {
227251
this.unit$.toggle(!valueChosen);
228252
this.filterAvailableRatioUnits();
229253
this.setMinimumOrderUnitDisplay();
254+
this.setMaximumOrderUnitDisplay();
230255
this.updateAvailableBillingAndGroupOrderUnits();
231256
this.updateUnitMultiplierLabels();
232257
}
@@ -245,6 +270,20 @@ class ArticleForm {
245270
this.minimumOrderQuantity$.attr('step', converter.isUnitSiConversible(this.supplierUnitSelect$.val()) ? 'any' : 1);
246271
}
247272

273+
setMaximumOrderUnitDisplay() {
274+
const chosenOptionLabel = this.supplierUnitSelect$.val() !== ''
275+
? $(`option[value="${this.supplierUnitSelect$.val()}"]`, this.supplierUnitSelect$).text()
276+
: undefined;
277+
const unitVal = $(`#${this.unitFieldsIdPrefix}_unit`).val();
278+
this.maximumOrderQuantity$
279+
.parents('.input-group')
280+
.find('.input-group-addon')
281+
.text(chosenOptionLabel !== undefined ? chosenOptionLabel : unitVal);
282+
283+
const converter = this.getUnitsConverter();
284+
this.maximumOrderQuantity$.attr('step', converter.isUnitSiConversible(this.supplierUnitSelect$.val()) ? 'any' : 1);
285+
}
286+
248287
bindAddRatioButton() {
249288
$('*[data-add-ratio]', this.articleForm$).on('click', (e) => {
250289
e.preventDefault();

app/assets/javascripts/group-order-form.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,47 @@ class GroupOrderForm {
1111
this.groupBalance = config.groupBalance;
1212
this.minimumBalance = config.minimumBalance;
1313

14+
ValidationUtils.setupCustomValidation(this.form$);
15+
1416
this.initializeIncreaseDecreaseButtons();
1517
this.submitButton$.removeAttr('disabled');
18+
this.initializeFormValidation();
1619
}
1720

1821
initializeIncreaseDecreaseButtons() {
1922
this.articleRows$.each((_, element) => this.initializeOrderArticleRow($(element)));
2023
}
2124

25+
initializeFormValidation() {
26+
this.form$.on('submit', (e) => {
27+
// Update all validation messages first
28+
this.articleRows$.each((_, row) => {
29+
const row$ = $(row);
30+
this.updateValidationMessages(row$);
31+
});
32+
33+
// Check if form is valid - browser will prevent submission if invalid
34+
if (!this.form$[0].checkValidity()) {
35+
e.preventDefault();
36+
37+
// Show popovers for all invalid inputs
38+
this.form$.find(':invalid').each((_, invalidInput) => {
39+
const input$ = $(invalidInput);
40+
if (input$.hasClass('goa-quantity')) {
41+
// Trigger validation to show popover
42+
ValidationUtils.validateNumericInput(input$);
43+
}
44+
});
45+
46+
// Focus on first invalid input
47+
const firstInvalid = this.form$.find(':invalid').first();
48+
if (firstInvalid.length > 0) {
49+
firstInvalid.focus();
50+
}
51+
}
52+
});
53+
}
54+
2255
initializeOrderArticleRow(row$) {
2356
const quantity$ = row$.find('.goa-quantity');
2457
const tolerance$ = row$.find('.goa-tolerance');
@@ -36,8 +69,18 @@ class GroupOrderForm {
3669
quantityAndTolerance$.change(() => {
3770
this.updateMissingUnits(row$, quantity$);
3871
this.updateBalance();
72+
this.updateValidationMessages(row$);
3973
});
4074
quantityAndTolerance$.keyup(() => quantity$.trigger('change'));
75+
76+
// Handle validation events for quantity field only
77+
quantity$.on('invalid', (e) => {
78+
e.preventDefault();
79+
e.stopPropagation();
80+
this.updateValidationMessages(row$);
81+
});
82+
83+
this.updateValidationMessages(row$);
4184
}
4285

4386
updateBalance() {
@@ -226,5 +269,30 @@ class GroupOrderForm {
226269
var remainder = Big(quantity).mod(packSize).toNumber();
227270
return (remainder > 0 && (Big(remainder).add(tolerance).toNumber() >= packSize));
228271
}
272+
273+
updateValidationMessages(row$) {
274+
const quantity$ = row$.find('.goa-quantity');
275+
276+
if (quantity$.length === 0) return;
277+
278+
const inputElement = quantity$[0];
279+
280+
// Clear any existing validation state
281+
inputElement.setCustomValidity('');
282+
283+
// Use ValidationUtils for validation with Bootstrap popovers
284+
ValidationUtils.validateNumericInput(quantity$);
285+
286+
// Special case: item not available (max = 0)
287+
const rawValue = quantity$.val().trim();
288+
const inputValue = parseFloat(rawValue);
289+
const maxValue = parseFloat(quantity$.attr('max'));
290+
291+
if (maxValue === 0 && inputValue > 0) {
292+
const message = I18n.t('errors.item_not_available');
293+
inputElement.setCustomValidity(message);
294+
ValidationUtils.showValidationPopover(quantity$, message);
295+
}
296+
}
229297
}
230298

app/assets/javascripts/unit-conversion-field.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,22 @@
185185
const unitLabel = this.unitSelectOptions.find(option => option.value === unit).label;
186186
this.conversionResult$.text('= ' + this.getConversionResult() + ' x ' + unitLabel);
187187
this.conversionResult$.parent().find('.numeric-step-error').remove();
188-
if (this.quantityInput$.is(':invalid')) {
189-
this.applyButton$.attr('disabled', 'disabled');
190-
const errorSpan$ = $(`<div class="numeric-step-error">${I18n.t('errors.step_error', {min: 0, granularity: this.quantityInput$.attr('step')})}</div>`);
191-
errorSpan$.show();
192-
this.conversionResult$.after(errorSpan$);
193-
} else {
188+
189+
const errorSpan$ = $('<div class="numeric-step-error"></div>');
190+
191+
// Use ValidationUtils for proper custom error messages
192+
const isValid = ValidationUtils.validateNumericInput(this.quantityInput$, {
193+
errorSpan$: errorSpan$
194+
});
195+
196+
if (isValid) {
194197
this.applyButton$.removeAttr('disabled');
198+
} else {
199+
this.applyButton$.attr('disabled', 'disabled');
200+
if (errorSpan$.text()) {
201+
errorSpan$.show();
202+
this.conversionResult$.after(errorSpan$);
203+
}
195204
}
196205
}
197206

0 commit comments

Comments
 (0)