src/Oro/Bundle/SaleBundle/Resources/public/js/app/views/line-item-view.js   C
last analyzed

Complexity

Total Complexity 60
Complexity/F 1.5

Size

Lines of Code 601
Function Count 40

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
nc 393216
dl 0
loc 601
rs 5.0946
c 0
b 0
f 0
wmc 60
mnd 2
bc 56
fnc 40
bpm 1.4
cpm 1.5
noi 3

How to fix   Complexity   

Complexity

Complex classes like src/Oro/Bundle/SaleBundle/Resources/public/js/app/views/line-item-view.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
define(function(require) {
2
    'use strict';
3
4
    var LineItemView;
5
    var $ = require('jquery');
6
    var _ = require('underscore');
7
    var __ = require('orotranslation/js/translator');
8
    var BaseView = require('oroui/js/app/views/base/view');
9
    var UnitsUtil = require('oroproduct/js/app/units-util');
10
    var BaseModel = require('oroui/js/app/models/base/model');
11
    var LoadingMaskView = require('oroui/js/app/views/loading-mask-view');
12
    var mediator = require('oroui/js/mediator');
13
    var routing = require('routing');
14
    require('jquery.validate');
15
16
    /**
17
     * @export orosale/js/app/views/line-item-view
18
     * @extends oroui.app.views.base.View
19
     * @class orosale.app.views.LineItemView
20
     */
21
    LineItemView = BaseView.extend({
22
        /**
23
         * @property {Object}
24
         */
25
        options: {
26
            classNotesSellerActive: 'quote-lineitem-notes-seller-active',
27
            productSelect: '.quote-lineitem-product-select input[type="hidden"]',
28
            productSkuLabel: '.quote-lineitem-product-sku-label',
29
            productSkuInput: '.quote-lineitem-product-free-form input[data-name="field__product-sku"]',
30
            productReplacementSelect: '.quote-lineitem-product-replacement-select input[type="hidden"]',
31
            offersQuantitySelector: '.quote-lineitem-offers-quantity input',
32
            offersPriceValueSelector: '.quote-lineitem-offers-price input:first',
33
            typeSelect: '.quote-lineitem-product-type-select',
34
            unitsSelect: '.quote-lineitem-offer-unit-select',
35
            productFreeFormInput: '.quote-lineitem-product-freeform-input',
36
            productReplacementFreeFormInput: '.quote-lineitem-productreplacement-freeform-input',
37
            unitsRoute: 'oro_product_unit_product_units',
38
            compactUnits: false,
39
            addItemButton: '.add-list-item',
40
            productSelectLink: '.quote-lineitem-product-select-link',
41
            freeFormLink: '.quote-lineitem-free-form-link',
42
            allowEditFreeForm: true,
43
            productFormContainer: '.quote-lineitem-product-form',
44
            freeFormContainer: '.quote-lineitem-product-free-form',
45
            fieldsRowContainer: '.fields-row',
46
            notesContainer: '.quote-lineitem-notes',
47
            addNotesButton: '.quote-lineitem-notes-add-btn',
48
            removeNotesButton: '.quote-lineitem-notes-remove-btn',
49
            itemsCollectionContainer: '.quote-lineitem-collection',
50
            itemsContainer: '.quote-lineitem-offers-items',
51
            itemWidget: '.quote-lineitem-offers-item',
52
            syncClass: 'synchronized',
53
            productReplacementContainer: '.quote-lineitem-product-replacement-row',
54
            sellerNotesContainer: '.quote-lineitem-notes-seller',
55
            requestsOnlyContainer: '.sale-quoteproductrequest-only',
56
            errorMessage: 'Sorry, an unexpected error has occurred.',
57
            submitButton: 'button#save-and-transit',
58
            allUnits: {},
59
            units: {},
60
            events: {
61
                before: 'entry-point:quote:load:before',
62
                after: 'entry-point:quote:load:after',
63
                trigger: 'entry-point:quote:trigger'
64
            }
65
        },
66
67
        /**
68
         * @property {int}
69
         */
70
        typeOffer: null,
71
72
        /**
73
         * @property {int}
74
         */
75
        typeReplacement: null,
76
77
        /**
78
         * @property {Boolean}
79
         */
80
        isFreeForm: false,
81
82
        /**
83
         * @property {Object}
84
         */
85
        units: {},
86
87
        /**
88
         * @property {Array}
89
         */
90
        allUnits: {},
91
92
        /**
93
         * @property {Object}
94
         */
95
        $el: null,
96
97
        /**
98
         * @property {Object}
99
         */
100
        $productSelect: null,
101
102
        /**
103
         * @property {Object}
104
         */
105
        $productReplacementSelect: null,
106
107
        /**
108
         * @property {Object}
109
         */
110
        $typeSelect: null,
111
112
        /**
113
         * @property {Object}
114
         */
115
        $addItemButton: null,
116
117
        /**
118
         * @property {Object}
119
         */
120
        $itemsContainer: null,
121
122
        /**
123
         * @property {Object}
124
         */
125
        $requestsOnlyContainer: null,
126
127
        /**
128
         * @property {Object}
129
         */
130
        $notesContainer: null,
131
132
        /**
133
         * @property {Object}
134
         */
135
        $sellerNotesContainer: null,
136
137
        /**
138
         * @property {Object}
139
         */
140
        $productReplacementContainer: null,
141
142
        /**
143
         * @property {LoadingMaskView|null}
144
         */
145
        loadingMask: null,
146
147
        /**
148
         * @inheritDoc
149
         */
150
        constructor: function LineItemView() {
151
            LineItemView.__super__.constructor.apply(this, arguments);
152
        },
153
154
        /**
155
         * @inheritDoc
156
         */
157
        initialize: function(options) {
158
            if (!this.model) {
159
                this.model = new BaseModel();
160
            }
161
162
            this.options = _.defaults(options || {}, this.options);
163
            this.units = _.defaults(this.units, options.units);
164
            this.allUnits = _.defaults(this.allUnits, options.allUnits);
165
166
            this.typeOffer = options.typeOffer;
167
            this.typeReplacement = options.typeReplacement;
168
169
            this.isFreeForm = options.isFreeForm || false;
170
171
            this.loadingMask = new LoadingMaskView({container: this.$el});
172
173
            this.delegate('click', '.removeLineItem', this.removeRow);
174
            this.delegate('click', '.removeRow', this.removeOfferRow);
175
176
            this.$productSelect = this.$el.find(this.options.productSelect);
177
            this.$productReplacementSelect = this.$el.find(this.options.productReplacementSelect);
178
            this.$typeSelect = this.$el.find(this.options.typeSelect);
179
            this.$addItemButton = this.$el.find(this.options.addItemButton);
180
            this.$itemsContainer = this.$el.find(this.options.itemsContainer);
181
            this.$productReplacementContainer = this.$el.find(this.options.productReplacementContainer);
182
            this.$notesContainer = this.$el.find(this.options.notesContainer);
183
            this.$sellerNotesContainer = this.$el.find(this.options.sellerNotesContainer);
184
            this.$requestsOnlyContainer = this.$el.find(this.options.requestsOnlyContainer);
185
186
            this.$el
187
                .on('change', this.options.productSelect, _.bind(this.onProductChanged, this))
188
                .on('change', this.options.productReplacementSelect, _.bind(this.onProductChanged, this))
189
                .on('change', this.options.typeSelect, _.bind(this.onTypeChanged, this))
190
                .on('click', this.options.addNotesButton, _.bind(this.onAddNotesClick, this))
191
                .on('click', this.options.removeNotesButton, _.bind(this.onRemoveNotesClick, this))
192
                .on('click', this.options.freeFormLink, _.bind(this.onFreeFormLinkClick, this))
193
                .on('click', this.options.productSelectLink, _.bind(this.onProductSelectLinkClick, this))
194
                .on('content:changed', _.bind(this.onContentChanged, this))
195
            ;
196
197
            this.listenTo(mediator, this.options.events.before, this.disableSubmit);
198
            this.listenTo(mediator, this.options.events.after, this.enableSubmit);
199
200
            this.$typeSelect.trigger('change');
201
202
            this.$form = this.$el.closest('form');
203
            this.$fields = this.$el.find(':input[name]');
204
205
            this.fieldsByName = {};
206
            this.$fields.each(_.bind(function(i, field) {
207
                if (!this.fieldsByName[this.formFieldName(field)]) {
208
                    this.fieldsByName[this.formFieldName(field)] = [];
209
                }
210
211
                this.fieldsByName[this.formFieldName(field)].push($(field));
212
            }, this));
213
214
            this.entryPointTriggers([
215
                this.fieldsByName.quantity,
216
                this.fieldsByName.productUnit,
217
                this.fieldsByName.priceValue,
218
                this.fieldsByName.priceType
219
            ]);
220
221
            this.checkAddButton();
222
            this.checkAddNotes();
223
            if (this.isFreeForm && !this.options.allowEditFreeForm) {
224
                this.setReadonlyState();
225
            }
226
227
            this.updateValidation();
228
        },
229
230
        disableSubmit: function() {
231
            $(this.options.submitButton).prop('disabled', true);
232
        },
233
234
        enableSubmit: function() {
235
            $(this.options.submitButton).prop('disabled', false);
236
        },
237
238
        /**
239
         * @param {Object} field
240
         * @returns {String}
241
         */
242
        formFieldName: function(field) {
243
            var name = '';
244
            var nameParts = field.name.replace(/.*\[[0-9]+\]/, '').replace(/[\[\]]/g, '_').split('_');
245
            var namePart;
246
247
            for (var i = 0, iMax = nameParts.length; i < iMax; i++) {
248
                namePart = nameParts[i];
249
                if (!namePart.length) {
250
                    continue;
251
                }
252
                if (name.length === 0) {
253
                    name += namePart;
254
                } else {
255
                    name += namePart[0].toUpperCase() + namePart.substr(1);
256
                }
257
            }
258
            return name;
259
        },
260
261
        checkAddButton: function() {
262
            var enabled = Boolean(this.getProductId()) || (this.isFreeForm && this.options.allowEditFreeForm);
263
            this.$addItemButton.toggle(enabled);
264
        },
265
266
        removeOfferRow: function() {
267
            mediator.trigger(this.options.events.trigger);
268
        },
269
270
        removeRow: function() {
271
            this.$el.trigger('content:remove');
272
            this.remove();
273
274
            mediator.trigger(this.options.events.trigger);
275
        },
276
277
        /**
278
         * @param {jQuery|Array} fields
279
         */
280
        entryPointTriggers: function(fields) {
281
            _.each(fields, function(fields) {
282
                _.each(fields, function(field) {
283
                    $(field).attr('data-entry-point-trigger', true);
284
                });
285
            });
286
287
            mediator.trigger('entry-point:quote:init');
288
        },
289
290
        /**
291
         * Handle Product change
292
         *
293
         * @param {jQuery.Event} e
294
         */
295
        onProductChanged: function(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
296
            this.checkAddButton();
297
298
            if (this.getProductId() && !this.$itemsContainer.children().length) {
299
                this.$addItemButton.click();
300
            }
301
302
            if (this.$itemsContainer.children().length) {
303
                this.updateContent(true);
304
            }
305
306
            this.updateSkuLabel();
307
308
            var $quantitySelector = this.$el.find(this.options.offersQuantitySelector);
309
            $quantitySelector.trigger('change');
310
311
            mediator.trigger(this.options.events.trigger);
312
        },
313
314
        /**
315
         * Handle Type change
316
         *
317
         * @param {jQuery.Event} e
318
         */
319
        onTypeChanged: function(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
320
            var typeValue = parseInt(this.$typeSelect.val());
321
322
            this.$productReplacementContainer.toggle(this.typeReplacement === typeValue);
323
            this.$requestsOnlyContainer.toggle(this.typeOffer !== typeValue);
324
325
            this.$productSelect.trigger('change');
326
        },
327
328
        /**
329
         * Handle Content change
330
         *
331
         * @param {jQuery.Event} e
332
         */
333
        onContentChanged: function(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
334
            this.updateContent(false);
335
        },
336
337
        /**
338
         * @param {Boolean} force
339
         */
340
        updateContent: function(force) {
341
            this.updateValidation();
342
343
            var productId = this.getProductId();
344
            var productUnits = productId ? this.units[productId] : this.allUnits;
345
346
            if (!productId || productUnits) {
347
                this.updateProductUnits(productUnits, force || false);
348
            } else {
349
                var self = this;
350
                var routeParams = {id: productId};
351
352
                if (this.options.compactUnits) {
353
                    routeParams.short = true;
354
                }
355
356
                $.ajax({
357
                    url: routing.generate(this.options.unitsRoute, routeParams),
358
                    type: 'GET',
359
                    beforeSend: function() {
360
                        self.loadingMask.show();
361
                    },
362
                    success: function(response) {
363
                        self.units[productId] = response.units;
364
                        self.updateProductUnits(response.units, force || false);
365
                    },
366
                    complete: function() {
367
                        self.loadingMask.hide();
368
                    },
369
                    errorHandlerMessage: __(this.options.errorMessage)
370
                });
371
            }
372
        },
373
374
        /**
375
         * Update available ProductUnit select
376
         *
377
         * @param {Object} data
378
         * @param {Boolean} force
379
         */
380
        updateProductUnits: function(data, force) {
381
            var self = this;
382
383
            self.model.set('product_units', data || {});
384
385
            var widgets = self.$el.find(self.options.itemWidget);
386
387
            $.each(widgets, function(index, widget) {
388
                var $select = $(widget).find(self.options.unitsSelect);
389
390
                if (!force && $select.hasClass(self.options.syncClass)) {
391
                    return;
392
                }
393
394
                UnitsUtil.updateSelect(self.model, $select);
395
                $select.addClass(self.options.syncClass);
396
            });
397
398
            if (force) {
399
                this.$el.find('select').inputWidget('refresh');
400
            }
401
        },
402
403
        /**
404
         * Check Add Notes
405
         */
406
        checkAddNotes: function() {
407
            if (this.$sellerNotesContainer.find('textarea').val()) {
408
                this.$notesContainer.addClass(this.options.classNotesSellerActive);
409
                this.$sellerNotesContainer.find('textarea').focus();
410
            }
411
        },
412
413
        /**
414
         * Handle Add Notes click
415
         *
416
         * @param {jQuery.Event} e
417
         */
418
        onAddNotesClick: function(e) {
419
            e.preventDefault();
420
421
            this.$notesContainer.addClass(this.options.classNotesSellerActive);
422
            this.$sellerNotesContainer.find('textarea').focus();
423
        },
424
425
        /**
426
         * Handle Remove Notes click
427
         *
428
         * @param {jQuery.Event} e
429
         */
430
        onRemoveNotesClick: function(e) {
431
            e.preventDefault();
432
433
            this.$notesContainer.removeClass(this.options.classNotesSellerActive);
434
            this.$sellerNotesContainer.find('textarea').val('');
435
        },
436
437
        /**
438
         * Handle Free Form for Product click
439
         *
440
         * @param {jQuery.Event} e
441
         */
442
        onFreeFormLinkClick: function(e) {
443
            e.preventDefault();
444
445
            this.clearInputs();
446
447
            this.$el.find(this.options.productFormContainer).hide();
448
            this.$el.find(this.options.freeFormContainer).show();
449
450
            this.isFreeForm = true;
451
452
            this.checkAddButton();
453
        },
454
455
        /**
456
         * Handle Product Form click
457
         *
458
         * @param {jQuery.Event} e
459
         */
460
        onProductSelectLinkClick: function(e) {
461
            e.preventDefault();
462
463
            this.clearInputs();
464
465
            this.$el.find(this.options.productFormContainer).show();
466
            this.$el.find(this.options.freeFormContainer).hide();
467
468
            this.isFreeForm = false;
469
470
            this.checkAddButton();
471
        },
472
473
        clearInputs: function() {
474
            this.$el.find(this.options.productFormContainer)
475
                .find('input').val('').change()
476
            ;
477
            this.$el.find(this.options.freeFormContainer)
478
                .find('input').val('')
479
            ;
480
481
            var self = this;
482
483
            var widgets = this.$el.find(this.options.itemWidget);
484
485
            $.each(widgets, function(index, widget) {
486
                var $priceValue = $(widget).find(self.options.offersPriceValueSelector);
487
488
                $priceValue.addClass('matched-price');
489
            });
490
491
            this.updateSkuLabel();
492
        },
493
494
        updateSkuLabel: function() {
495
            var productData = this.$el.find(this.options.productSelect).inputWidget('data') || {};
496
497
            this.$el.find(this.options.productSkuLabel).text(productData.sku || '');
498
        },
499
500
        /**
501
         * Get Product Id
502
         */
503
        getProductId: function() {
504
            return this.isProductReplacement() ? this.$productReplacementSelect.val() : this.$productSelect.val();
505
        },
506
507
        /**
508
         * Check that Product is Replacement
509
         */
510
        isProductReplacement: function() {
511
            return this.typeReplacement === parseInt(this.$typeSelect.val());
512
        },
513
514
        /**
515
         * Validation for products
516
         */
517
        updateValidation: function() {
518
            var self = this;
519
520
            self.$el.find(self.options.productFreeFormInput).rules('add', {
521
                required: {
522
                    param: true,
523
                    depends: function() {
524
                        return !self.isProductReplacement() && self.isFreeForm;
525
                    }
526
                },
527
                messages: {
528
                    required: __('oro.sale.quoteproduct.free_form_product.blank')
529
                }
530
            });
531
532
            self.$el.find(self.options.productReplacementFreeFormInput).rules('add', {
533
                required: {
534
                    param: true,
535
                    depends: function() {
536
                        return self.isProductReplacement() && self.isFreeForm;
537
                    }
538
                },
539
                messages: {
540
                    required: __('oro.sale.quoteproduct.free_form_product.blank')
541
                }
542
            });
543
544
            self.$productSelect.rules('add', {
545
                required: {
546
                    param: true,
547
                    depends: function() {
548
                        return !self.isProductReplacement() && !self.isFreeForm;
549
                    }
550
                },
551
                messages: {
552
                    required: __('oro.sale.quoteproduct.product.blank')
553
                }
554
            });
555
556
            self.$productReplacementSelect.rules('add', {
557
                required: {
558
                    param: true,
559
                    depends: function() {
560
                        return self.isProductReplacement() && !self.isFreeForm;
561
                    }
562
                },
563
                messages: {
564
                    required: __('oro.sale.quoteproduct.product.blank')
565
                }
566
            });
567
        },
568
569
        /**
570
         * Disable items update
571
         */
572
        setReadonlyState: function() {
573
            var self = this;
574
575
            self.$el.find(self.options.productFreeFormInput).prop('readonly', true);
576
            self.$el.find(self.options.productSkuInput).prop('readonly', true);
577
            self.$el.find(self.options.productReplacementFreeFormInput).prop('readonly', true);
578
            self.$el.find('.removeLineItem').prop('disabled', true);
579
            self.$el.find('.removeRow').prop('disabled', true);
580
581
            var widgets = this.$el.find(self.options.itemWidget);
582
            $.each(widgets, function(index, widget) {
583
                $(widget).find(self.options.unitsSelect).prop('readonly', true);
584
                $(widget).find(self.options.offersPriceValueSelector).prop('readonly', true);
585
                $(widget).find(self.options.offersQuantitySelector).prop('readonly', true);
586
            });
587
        },
588
589
        dispose: function() {
590
            if (this.disposed) {
591
                return;
592
            }
593
594
            this.$el.off();
595
596
            LineItemView.__super__.dispose.call(this);
597
        }
598
    });
599
600
    return LineItemView;
601
});
602