@ironcode/vas-lib 3.0.0-alpha.2 → 3.0.0-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/cjs/lib/control-value/vas-camera-control-value.model.d.ts +4 -2
  2. package/cjs/lib/control-value/vas-camera-control-value.model.d.ts.map +1 -1
  3. package/cjs/lib/control-value/vas-camera-control-value.model.js.map +1 -1
  4. package/cjs/lib/entity/vas-base.model.d.ts +1 -1
  5. package/cjs/lib/entity/vas-base.model.d.ts.map +1 -1
  6. package/cjs/lib/entity/vas-base.model.js.map +1 -1
  7. package/cjs/lib/entity/vas-field.dto.d.ts +3 -4
  8. package/cjs/lib/entity/vas-field.dto.d.ts.map +1 -1
  9. package/cjs/lib/entity/vas-field.dto.js.map +1 -1
  10. package/cjs/lib/entity/vas-field.model.js +1 -1
  11. package/cjs/lib/entity/vas-field.model.js.map +1 -1
  12. package/cjs/lib/entity/vas-form.model.d.ts +1 -1
  13. package/cjs/lib/entity/vas-form.model.d.ts.map +1 -1
  14. package/cjs/lib/entity/vas-form.model.js +1 -1
  15. package/cjs/lib/entity/vas-form.model.js.map +1 -1
  16. package/cjs/lib/entity/vas-job-copy.d.ts +12 -12
  17. package/cjs/lib/entity/vas-job-copy.d.ts.map +1 -1
  18. package/cjs/lib/entity/vas-job-copy.js.map +1 -1
  19. package/cjs/lib/entity/vas-job.dto.d.ts +12 -3
  20. package/cjs/lib/entity/vas-job.dto.d.ts.map +1 -1
  21. package/cjs/lib/entity/vas-job.dto.js.map +1 -1
  22. package/cjs/lib/entity/vas-job.model.d.ts +30 -13
  23. package/cjs/lib/entity/vas-job.model.d.ts.map +1 -1
  24. package/cjs/lib/entity/vas-job.model.js +273 -95
  25. package/cjs/lib/entity/vas-job.model.js.map +1 -1
  26. package/cjs/lib/model/estimate-control/estimate-control-value.dto.d.ts +14 -0
  27. package/cjs/lib/model/estimate-control/estimate-control-value.dto.d.ts.map +1 -0
  28. package/cjs/lib/model/estimate-control/estimate-control-value.dto.js +3 -0
  29. package/cjs/lib/model/estimate-control/estimate-control-value.dto.js.map +1 -0
  30. package/cjs/lib/model/estimate-control/estimate-control-value.model.d.ts +11 -0
  31. package/cjs/lib/model/estimate-control/estimate-control-value.model.d.ts.map +1 -0
  32. package/cjs/lib/model/estimate-control/estimate-control-value.model.js +84 -0
  33. package/cjs/lib/model/estimate-control/estimate-control-value.model.js.map +1 -0
  34. package/cjs/lib/model/estimate-control/estimate-item.dto.d.ts +17 -0
  35. package/cjs/lib/model/estimate-control/estimate-item.dto.d.ts.map +1 -0
  36. package/cjs/lib/model/estimate-control/estimate-item.dto.js +3 -0
  37. package/cjs/lib/model/estimate-control/estimate-item.dto.js.map +1 -0
  38. package/cjs/lib/model/estimate-control/estimate-item.model.d.ts +25 -0
  39. package/cjs/lib/model/estimate-control/estimate-item.model.d.ts.map +1 -0
  40. package/cjs/lib/model/estimate-control/estimate-item.model.js +126 -0
  41. package/cjs/lib/model/estimate-control/estimate-item.model.js.map +1 -0
  42. package/cjs/lib/model/estimate-control/estimate-source.d.ts +2 -0
  43. package/cjs/lib/model/estimate-control/estimate-source.d.ts.map +1 -0
  44. package/cjs/lib/model/estimate-control/estimate-source.js +3 -0
  45. package/cjs/lib/model/estimate-control/estimate-source.js.map +1 -0
  46. package/cjs/lib/model/estimate-control/estimate.dto.d.ts +38 -0
  47. package/cjs/lib/model/estimate-control/estimate.dto.d.ts.map +1 -0
  48. package/cjs/lib/model/estimate-control/estimate.dto.js +3 -0
  49. package/cjs/lib/model/estimate-control/estimate.dto.js.map +1 -0
  50. package/cjs/lib/model/estimate-control/estimate.model.d.ts +55 -0
  51. package/cjs/lib/model/estimate-control/estimate.model.d.ts.map +1 -0
  52. package/cjs/lib/model/estimate-control/estimate.model.js +279 -0
  53. package/cjs/lib/model/estimate-control/estimate.model.js.map +1 -0
  54. package/cjs/lib/model/estimate-control/estimate.types.d.ts +3 -0
  55. package/cjs/lib/model/estimate-control/estimate.types.d.ts.map +1 -0
  56. package/cjs/lib/model/estimate-control/estimate.types.js +3 -0
  57. package/cjs/lib/model/estimate-control/estimate.types.js.map +1 -0
  58. package/cjs/lib/model/estimate-control/index.d.ts +9 -0
  59. package/cjs/lib/model/estimate-control/index.d.ts.map +1 -0
  60. package/cjs/lib/model/estimate-control/index.js +11 -0
  61. package/cjs/lib/model/estimate-control/index.js.map +1 -0
  62. package/cjs/lib/model/index.d.ts +2 -0
  63. package/cjs/lib/model/index.d.ts.map +1 -1
  64. package/cjs/lib/model/index.js +2 -0
  65. package/cjs/lib/model/index.js.map +1 -1
  66. package/cjs/lib/model/report-invocation.d.ts +40 -0
  67. package/cjs/lib/model/report-invocation.d.ts.map +1 -0
  68. package/cjs/lib/model/report-invocation.js +3 -0
  69. package/cjs/lib/model/report-invocation.js.map +1 -0
  70. package/cjs/lib/utils/get-value-by-path.d.ts +5 -15
  71. package/cjs/lib/utils/get-value-by-path.d.ts.map +1 -1
  72. package/cjs/lib/utils/get-value-by-path.js +48 -59
  73. package/cjs/lib/utils/get-value-by-path.js.map +1 -1
  74. package/cjs/lib/utils/index.d.ts +8 -0
  75. package/cjs/lib/utils/index.d.ts.map +1 -0
  76. package/cjs/lib/utils/index.js +11 -0
  77. package/cjs/lib/utils/index.js.map +1 -0
  78. package/cjs/lib/utils/is-array-string.d.ts +3 -0
  79. package/cjs/lib/utils/is-array-string.d.ts.map +1 -1
  80. package/cjs/lib/utils/is-array-string.js +3 -0
  81. package/cjs/lib/utils/is-array-string.js.map +1 -1
  82. package/cjs/lib/utils/is-numeric.d.ts +2 -0
  83. package/cjs/lib/utils/is-numeric.d.ts.map +1 -0
  84. package/cjs/lib/utils/is-numeric.js +10 -0
  85. package/cjs/lib/utils/is-numeric.js.map +1 -0
  86. package/cjs/lib/utils/is-plain-object.d.ts +3 -0
  87. package/cjs/lib/utils/is-plain-object.d.ts.map +1 -1
  88. package/cjs/lib/utils/is-plain-object.js +3 -0
  89. package/cjs/lib/utils/is-plain-object.js.map +1 -1
  90. package/cjs/lib/utils/try-json.d.ts +2 -2
  91. package/cjs/lib/utils/try-json.js +2 -2
  92. package/cjs/lib/utils/try-json.js.map +1 -1
  93. package/cjs/public-api.d.ts +1 -0
  94. package/cjs/public-api.d.ts.map +1 -1
  95. package/cjs/public-api.js +1 -0
  96. package/cjs/public-api.js.map +1 -1
  97. package/fesm2022/ironcode-vas-lib.mjs +842 -149
  98. package/fesm2022/ironcode-vas-lib.mjs.map +1 -1
  99. package/lib/control-value/vas-camera-control-value.model.d.ts +4 -2
  100. package/lib/entity/vas-base.model.d.ts +1 -1
  101. package/lib/entity/vas-field.dto.d.ts +3 -4
  102. package/lib/entity/vas-form.model.d.ts +1 -1
  103. package/lib/entity/vas-job-copy.d.ts +12 -12
  104. package/lib/entity/vas-job.dto.d.ts +12 -3
  105. package/lib/entity/vas-job.model.d.ts +30 -13
  106. package/lib/model/estimate-control/estimate-control-value.dto.d.ts +13 -0
  107. package/lib/model/estimate-control/estimate-control-value.model.d.ts +10 -0
  108. package/lib/model/estimate-control/estimate-item.dto.d.ts +16 -0
  109. package/lib/model/estimate-control/estimate-item.model.d.ts +24 -0
  110. package/lib/model/estimate-control/estimate-source.d.ts +1 -0
  111. package/lib/model/estimate-control/estimate.dto.d.ts +37 -0
  112. package/lib/model/estimate-control/estimate.model.d.ts +54 -0
  113. package/lib/model/estimate-control/estimate.types.d.ts +2 -0
  114. package/lib/model/estimate-control/index.d.ts +8 -0
  115. package/lib/model/index.d.ts +2 -0
  116. package/lib/model/report-invocation.d.ts +39 -0
  117. package/lib/utils/get-value-by-path.d.ts +5 -15
  118. package/lib/utils/index.d.ts +7 -0
  119. package/lib/utils/is-array-string.d.ts +4 -0
  120. package/lib/utils/is-numeric.d.ts +1 -0
  121. package/lib/utils/is-plain-object.d.ts +3 -0
  122. package/lib/utils/omit.d.ts +7 -0
  123. package/lib/utils/try-json.d.ts +2 -2
  124. package/package.json +3 -2
  125. package/public-api.d.ts +1 -0
@@ -1,5 +1,7 @@
1
1
  import { UUID } from 'angular2-uuid';
2
2
  import moment from 'moment';
3
+ import { Big } from 'big.js';
4
+ import { v4 } from 'uuid';
3
5
 
4
6
  function isCameraControlValueV1(object) {
5
7
  return object instanceof Object
@@ -1537,7 +1539,7 @@ class VasFieldModel extends VasJobDataModel {
1537
1539
  * @return {VasFieldModel}
1538
1540
  */
1539
1541
  static fromDto(fieldDto) {
1540
- return new VasFieldModel(fieldDto.id || '', fieldDto.created || '', fieldDto.serverCreated || '', fieldDto.createdBy || '', fieldDto.modified || '', fieldDto.serverModified || '', fieldDto.modifiedBy || '', fieldDto.createdByName || '', fieldDto.modifiedByName || '', fieldDto.account || '', fieldDto.job || '', fieldDto.control || '', fieldDto.fieldJobPointers || [], fieldDto.value || '', fieldDto.version || 0, fieldDto.controlName || '', fieldDto.controlTitle || '', fieldDto.controlTypeName || '');
1542
+ return new VasFieldModel(fieldDto.id || '', fieldDto.created || '', fieldDto.serverCreated || '', fieldDto.createdBy || '', fieldDto.modified || '', fieldDto.serverModified || '', fieldDto.modifiedBy || '', fieldDto.createdByName || '', fieldDto.modifiedByName || '', fieldDto.account || '', fieldDto.job || '', fieldDto.control || '', fieldDto.fieldJobPointers || [], fieldDto.value !== undefined ? fieldDto.value : null, fieldDto.version || 0, fieldDto.controlName || '', fieldDto.controlTitle || '', fieldDto.controlTypeName || '');
1541
1543
  }
1542
1544
  /**
1543
1545
  * @return {VasFieldDto}
@@ -2243,7 +2245,7 @@ class VasFormModel extends VasRestrictedAccountObjectModel {
2243
2245
  if (control.controlTypeName !== 'camera') {
2244
2246
  return;
2245
2247
  }
2246
- paths.push([group.name, control.safeName]);
2248
+ paths.push(control.name.split('.'));
2247
2249
  });
2248
2250
  });
2249
2251
  return paths;
@@ -3258,69 +3260,547 @@ class VasJobTypeModel extends VasAccountObjectModel {
3258
3260
  }
3259
3261
  }
3260
3262
 
3263
+ function isNumeric(value) {
3264
+ if (typeof value === 'number') {
3265
+ return !isNaN(value);
3266
+ }
3267
+ return /^-?\d+$/.test(value.trim());
3268
+ }
3269
+
3270
+ function coalescePath(pathOrPaths) {
3271
+ let keys = [];
3272
+ if (typeof pathOrPaths === 'string') {
3273
+ keys = pathOrPaths.split('.');
3274
+ }
3275
+ else if (typeof pathOrPaths === 'number') {
3276
+ keys = [pathOrPaths];
3277
+ }
3278
+ else if (Array.isArray(pathOrPaths)) {
3279
+ keys = pathOrPaths;
3280
+ }
3281
+ else {
3282
+ throw Error('Expected string, number or array of strings or numbers');
3283
+ }
3284
+ const result = [];
3285
+ keys.forEach(key => {
3286
+ if (typeof key === 'number') {
3287
+ result.push(key);
3288
+ return;
3289
+ }
3290
+ if (key.endsWith(']')) {
3291
+ const match = /^([\w_#$-]+)\[([\w_#$-]+)]$/.exec(key.toString());
3292
+ if (match !== null) {
3293
+ result.push(match[1]);
3294
+ result.push(isNumeric(match[2]) ? parseInt(match[2], 10) : match[2]);
3295
+ return;
3296
+ }
3297
+ }
3298
+ result.push(key);
3299
+ });
3300
+ return result;
3301
+ }
3261
3302
  /**
3262
- * A non sophisticated way to get values from the job via paths. For example:
3263
- *
3264
- * getValueByPath(
3265
- * ['foo', 'bar'],
3266
- * {
3267
- * foo: {
3268
- * bar: 'value'
3269
- * }
3270
- * }
3271
- * );
3303
+ * Get a value from an object by path.
3272
3304
  *
3273
- * will return "value"
3274
- * @param {string[]} path
3275
- * @param {Record<string, FieldValueType>} object
3276
- * @return {T | undefined}
3305
+ * Path can be an array of strings or numbers.
3306
+ * Path can be a string using dot notation, which can include square brackets to
3307
+ * access array positions e.g. foo.bar[0].baz
3277
3308
  */
3278
- const getValueByPath = (path, object) => {
3279
- switch (path.length) {
3280
- case 0: {
3281
- return;
3309
+ function getValueByPath(path, object) {
3310
+ const p = coalescePath(path);
3311
+ let scope = object === undefined ? null : object;
3312
+ let index = 0;
3313
+ const length = p.length;
3314
+ while (index < length) {
3315
+ if (scope === undefined || scope === null) {
3316
+ return undefined;
3282
3317
  }
3283
- case 1: {
3284
- return (object)[path[0]];
3318
+ scope = scope[p[index++]];
3319
+ }
3320
+ return scope;
3321
+ }
3322
+
3323
+ const b$2 = (value = '0') => {
3324
+ try {
3325
+ if (typeof value === 'string') {
3326
+ return new Big(value.replace(/[^\d^.]/g, ''));
3285
3327
  }
3286
- case 2: {
3287
- const val0 = object[path[0]];
3288
- if (!val0) {
3289
- return undefined;
3290
- }
3291
- return val0[path[1]];
3328
+ return new Big(value);
3329
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3330
+ }
3331
+ catch (e) {
3332
+ console.error(`failed to construct Big from value '${value}'`);
3333
+ return new Big('0');
3334
+ }
3335
+ };
3336
+ class EstimateItemModel {
3337
+ id;
3338
+ description;
3339
+ type;
3340
+ quantity;
3341
+ quantityAdjusted;
3342
+ units;
3343
+ unitsAdjusted;
3344
+ price;
3345
+ priceAdjusted;
3346
+ workCategory;
3347
+ costCategory;
3348
+ comment;
3349
+ vatExcluded;
3350
+ constructor(id, description, type, quantity, quantityAdjusted, units, unitsAdjusted, price, priceAdjusted, workCategory, costCategory, comment, vatExcluded) {
3351
+ this.id = id;
3352
+ this.description = description;
3353
+ this.type = type;
3354
+ this.quantity = quantity;
3355
+ this.quantityAdjusted = quantityAdjusted;
3356
+ this.units = units;
3357
+ this.unitsAdjusted = unitsAdjusted;
3358
+ this.price = price;
3359
+ this.priceAdjusted = priceAdjusted;
3360
+ this.workCategory = workCategory;
3361
+ this.costCategory = costCategory;
3362
+ this.comment = comment;
3363
+ this.vatExcluded = vatExcluded;
3364
+ }
3365
+ getTotalUnits(adjusted = false) {
3366
+ return adjusted ?
3367
+ b$2(this.quantityAdjusted || '0').mul(this.unitsAdjusted || '0').toFixed(2) :
3368
+ b$2(this.quantity || '0').mul(this.units || '0').toFixed(2);
3369
+ }
3370
+ getLineValue(labourRate = '1', timeBasis = '1', adjusted = false, vatExcluded) {
3371
+ const cat = this.costCategory || 'none';
3372
+ if (this.workCategory === 'removed') {
3373
+ return '0';
3292
3374
  }
3293
- case 3: {
3294
- const val0 = object[path[0]];
3295
- if (!val0) {
3296
- return undefined;
3297
- }
3298
- const val1 = val0[path[1]];
3299
- if (!val1) {
3300
- return undefined;
3301
- }
3302
- return val1[path[2]];
3375
+ if (vatExcluded !== undefined && this.vatExcluded !== vatExcluded) {
3376
+ return '0';
3303
3377
  }
3304
- case 4: {
3305
- const val0 = object[path[0]];
3306
- if (!val0) {
3307
- return undefined;
3308
- }
3309
- const val1 = val0[path[1]];
3310
- if (!val1) {
3311
- return undefined;
3312
- }
3313
- const val2 = val1[path[2]];
3314
- if (!val2) {
3315
- return undefined;
3378
+ const units = (adjusted ? this.unitsAdjusted : this.units) || 0;
3379
+ const quantity = (adjusted ? this.quantityAdjusted : this.quantity) || 0;
3380
+ const price = (adjusted ? this.priceAdjusted : this.price) || '';
3381
+ try {
3382
+ switch (cat) {
3383
+ case 'labour': {
3384
+ return price != '' ?
3385
+ price :
3386
+ b$2(quantity || '0')
3387
+ .mul(units || '0')
3388
+ .mul(labourRate || '0')
3389
+ .div(timeBasis)
3390
+ .toFixed(2);
3391
+ }
3392
+ case 'pm': {
3393
+ return b$2(price || '0')
3394
+ .mul(quantity || '0')
3395
+ .toFixed(2);
3396
+ }
3397
+ case 'parts': {
3398
+ return b$2(price || '0')
3399
+ .mul(quantity || '0')
3400
+ .toFixed(2);
3401
+ }
3402
+ case 'specialist': {
3403
+ return b$2(price || '0')
3404
+ .mul(quantity || '0')
3405
+ .toFixed(2);
3406
+ }
3407
+ default: {
3408
+ return '0';
3409
+ }
3316
3410
  }
3317
- return val2[path[3]];
3318
3411
  }
3319
- default: {
3320
- throw Error('path has too many segments');
3412
+ catch (e) {
3413
+ console.warn(e);
3321
3414
  }
3415
+ return '0';
3416
+ }
3417
+ toDto() {
3418
+ return {
3419
+ id: this.id,
3420
+ description: this.description,
3421
+ type: this.type,
3422
+ quantity: this.quantity,
3423
+ quantityAdjusted: this.quantityAdjusted,
3424
+ units: this.units,
3425
+ unitsAdjusted: this.unitsAdjusted,
3426
+ price: this.price,
3427
+ priceAdjusted: this.priceAdjusted,
3428
+ workCategory: this.workCategory,
3429
+ costCategory: this.costCategory,
3430
+ comment: this.comment,
3431
+ vatExcluded: this.vatExcluded
3432
+ };
3433
+ }
3434
+ static create() {
3435
+ return new EstimateItemModel(v4(), '', '', 1, 1, 0, 0, '', '', 'none', 'none', '', false);
3436
+ }
3437
+ static fromDto(dto) {
3438
+ return new EstimateItemModel(dto.id || v4(), dto.description || '', dto.type || '', dto.quantity || 0, dto.quantityAdjusted || 0, dto.units || 0, dto.unitsAdjusted || 0, dto.price || '', dto.priceAdjusted || '', dto.workCategory || 'none', dto.costCategory || 'none', dto.comment || '', dto.vatExcluded || false);
3439
+ }
3440
+ static equal(a, b) {
3441
+ return (a?.id === b?.id &&
3442
+ a?.description === b?.description &&
3443
+ a?.type === b?.type &&
3444
+ a?.quantity === b?.quantity &&
3445
+ a?.quantityAdjusted === b?.quantityAdjusted &&
3446
+ a?.units === b?.units &&
3447
+ a?.unitsAdjusted === b?.unitsAdjusted &&
3448
+ a?.price === b?.price &&
3449
+ a?.priceAdjusted === b?.priceAdjusted &&
3450
+ a?.workCategory === b?.workCategory &&
3451
+ a?.costCategory === b?.costCategory &&
3452
+ a?.comment === b?.comment);
3453
+ }
3454
+ }
3455
+
3456
+ const b$1 = (value = '0') => {
3457
+ try {
3458
+ if (typeof value === 'string') {
3459
+ return new Big(value.replace(/[^\d^.]/g, ''));
3460
+ }
3461
+ return new Big(value);
3462
+ }
3463
+ catch (e) {
3464
+ console.error(`failed to construct Big from value '${value}'`);
3465
+ return new Big('0');
3322
3466
  }
3323
3467
  };
3468
+ function isLabourWorkCategory(value) {
3469
+ return ['check', 'rr', 'repair', 'removed'].includes(value);
3470
+ }
3471
+ function isPaintWorkCategory(value) {
3472
+ return ['blend', 'paint'].includes(value);
3473
+ }
3474
+ class EstimateModel {
3475
+ id = v4();
3476
+ source = 'manual';
3477
+ label = '';
3478
+ comment = '';
3479
+ timestamp = moment().toISOString();
3480
+ version = 1;
3481
+ net = '0.00';
3482
+ vat = '0.00';
3483
+ gross = '0.00';
3484
+ labourRate = '1';
3485
+ vatRate = '0.00';
3486
+ sundryRate = '0.00';
3487
+ sundryMax = '0.00';
3488
+ sundryFixed = '0.00';
3489
+ sundryMethod = 'fixed';
3490
+ partsDiscount = '0.00';
3491
+ paintDiscount = '0.00';
3492
+ timeBasis = '1';
3493
+ adjusted = false;
3494
+ items = [];
3495
+ summaryNet = {
3496
+ labour: '0.00',
3497
+ none: '0.00',
3498
+ parts: '0.00',
3499
+ pm: '0.00',
3500
+ pl: '0.00',
3501
+ specialist: '0.00',
3502
+ other: '0.00',
3503
+ gross: '0.00',
3504
+ net: '0.00',
3505
+ hours: '0.00',
3506
+ vat: '0.00',
3507
+ labourRate: '0.00',
3508
+ };
3509
+ summary = {
3510
+ labour: '0.00',
3511
+ none: '0.00',
3512
+ parts: '0.00',
3513
+ pm: '0.00',
3514
+ pl: '0.00',
3515
+ specialist: '0.00',
3516
+ other: '0.00',
3517
+ gross: '0.00',
3518
+ net: '0.00',
3519
+ hours: '0.00',
3520
+ vat: '0.00',
3521
+ labourRate: '0.00',
3522
+ };
3523
+ summaryAdjusted = {
3524
+ labour: '0.00',
3525
+ none: '0.00',
3526
+ parts: '0.00',
3527
+ pm: '0.00',
3528
+ pl: '0.00',
3529
+ specialist: '0.00',
3530
+ other: '0.00',
3531
+ gross: '0.00',
3532
+ net: '0.00',
3533
+ hours: '0.00',
3534
+ vat: '0.00',
3535
+ labourRate: '0.00',
3536
+ };
3537
+ get timeBasisSafe() {
3538
+ return (this.timeBasis || '1') === '0' ? '1' : this.timeBasis;
3539
+ }
3540
+ static create(source = 'manual') {
3541
+ const model = new EstimateModel();
3542
+ model.source = source;
3543
+ return model;
3544
+ }
3545
+ static fromDto(dto) {
3546
+ const model = new this();
3547
+ Object.assign(model, {
3548
+ ...dto,
3549
+ items: dto.items?.map(item => EstimateItemModel.fromDto(item)),
3550
+ });
3551
+ model.calculate();
3552
+ return model;
3553
+ }
3554
+ addItem(modelOrDto, opts) {
3555
+ this.items.push(modelOrDto instanceof EstimateItemModel ?
3556
+ modelOrDto :
3557
+ EstimateItemModel.fromDto(modelOrDto));
3558
+ if (!opts || (opts && opts.calculate)) {
3559
+ this.calculate();
3560
+ }
3561
+ }
3562
+ addItems(items, opts) {
3563
+ this.items = this.items.concat(items);
3564
+ if (!opts || (opts && opts.calculate)) {
3565
+ this.calculate();
3566
+ }
3567
+ }
3568
+ calculate() {
3569
+ this.summaryNet = this.getSummaryNet(false);
3570
+ if (this.adjusted) {
3571
+ const summaryNetAdjusted = this.getSummaryNet(true);
3572
+ this.summaryAdjusted = this.getSummary(summaryNetAdjusted, this.sundryRate, this.sundryMax, this.sundryFixed, this.sundryMethod, this.partsDiscount, this.paintDiscount, true);
3573
+ }
3574
+ this.summary = this.getSummary(this.summaryNet, this.sundryRate, this.sundryMax, this.sundryFixed, this.sundryMethod, this.partsDiscount, this.paintDiscount, false);
3575
+ this.net = this.summary.net;
3576
+ this.vat = this.summary.vat;
3577
+ this.gross = this.summary.gross;
3578
+ }
3579
+ /**
3580
+ * Returns the net summary (before sundries and discounts) of the estimate.
3581
+ * By summary, we mean grouping the costs by cost category.
3582
+ *
3583
+ * @param adjusted if the values should be calculated using the original or
3584
+ * adjusted values
3585
+ */
3586
+ getSummaryNet(adjusted = false) {
3587
+ const summary = this.items.reduce((map, item) => {
3588
+ map[item.costCategory] = b$1(map[item.costCategory] || '0')
3589
+ .add(item.getLineValue(this.labourRate, this.timeBasisSafe, adjusted, this.source === 'gtmotive' ? false : undefined))
3590
+ .toFixed(2);
3591
+ return map;
3592
+ }, {
3593
+ labour: '0.00',
3594
+ none: '0.00',
3595
+ parts: '0.00',
3596
+ pm: '0.00',
3597
+ pl: '0.00',
3598
+ specialist: '0.00',
3599
+ other: '0.00',
3600
+ net: '0.00',
3601
+ vat: '0.00',
3602
+ gross: '0.00',
3603
+ hours: '0.00',
3604
+ labourRate: '0',
3605
+ });
3606
+ if (this.source === 'audatex') {
3607
+ const mechHours = this.items
3608
+ .reduce((h, item) => {
3609
+ return isLabourWorkCategory(item.workCategory) ?
3610
+ h.add(b$1(item.getTotalUnits(adjusted))) :
3611
+ h;
3612
+ }, b$1())
3613
+ .div(this.timeBasisSafe || '1')
3614
+ .round(2);
3615
+ const paintHours = this.items
3616
+ .reduce((h, item) => {
3617
+ return isPaintWorkCategory(item.workCategory) ?
3618
+ h.add(b$1(item.getTotalUnits(adjusted))) :
3619
+ h;
3620
+ }, b$1())
3621
+ .div(this.timeBasisSafe || '1')
3622
+ .round(1);
3623
+ const mechCost = mechHours.mul(this.labourRate || '0');
3624
+ const paintCost = paintHours.mul(this.labourRate || '0');
3625
+ summary.labour = mechCost.add(paintCost).toFixed(2);
3626
+ }
3627
+ return summary;
3628
+ }
3629
+ /**
3630
+ * Applies the sundry and discount values to the net summary.
3631
+ */
3632
+ getSummary(summaryNet, sundryRate, sundryMax, sundryFixed, sundryMethod, partsDiscount, paintDiscount, adjusted) {
3633
+ const partsDisc = b$1('1').minus(partsDiscount || '0');
3634
+ const paintDisc = b$1('1').minus(paintDiscount || '0');
3635
+ let partsSundry = '0.00';
3636
+ if (sundryMethod === 'percentage') {
3637
+ partsSundry = b$1(summaryNet.parts).mul(sundryRate || '1').toFixed(2);
3638
+ }
3639
+ else if (sundryMethod === 'percentage-with-max') {
3640
+ partsSundry = b$1(summaryNet.parts).mul(sundryRate || '1').toFixed(2);
3641
+ partsSundry = Math.min(parseFloat(sundryMax || '0'), parseFloat(partsSundry)).toFixed(2);
3642
+ }
3643
+ else if (sundryMethod === 'fixed') {
3644
+ partsSundry = b$1(sundryFixed).toFixed(2);
3645
+ }
3646
+ const parts = b$1(summaryNet.parts).add(partsSundry);
3647
+ const net = b$1(0)
3648
+ .add(summaryNet.labour)
3649
+ .add(summaryNet.none)
3650
+ .add(parts)
3651
+ .add(summaryNet.pm)
3652
+ .add(summaryNet.specialist)
3653
+ .toFixed(2);
3654
+ const vat = b$1(net || '0')
3655
+ .mul(this.vatRate || '0')
3656
+ .toFixed(2);
3657
+ let gross = b$1(net || '0').add(vat || '0').toFixed(2);
3658
+ // calculate the total value of lines that are VAT excluded
3659
+ const vatExcludeTotal = this.items.reduce((p, c) => c.vatExcluded ?
3660
+ p.add(c.getLineValue(this.labourRate, this.timeBasisSafe, adjusted, true)) :
3661
+ p, b$1());
3662
+ gross = b$1(gross).add(vatExcludeTotal).toFixed(2);
3663
+ return {
3664
+ labour: summaryNet.labour,
3665
+ none: summaryNet.none,
3666
+ parts: b$1(parts).mul(partsDisc).toFixed(2),
3667
+ pm: b$1(summaryNet.pm).mul(paintDisc).toFixed(2),
3668
+ pl: summaryNet.pl,
3669
+ specialist: summaryNet.specialist,
3670
+ other: summaryNet.other,
3671
+ net: net,
3672
+ vat: vat,
3673
+ gross: gross,
3674
+ labourRate: this.labourRate,
3675
+ hours: this.getTotalHours(adjusted)
3676
+ };
3677
+ }
3678
+ toDto() {
3679
+ return {
3680
+ id: this.id,
3681
+ source: this.source,
3682
+ label: this.label,
3683
+ comment: this.comment,
3684
+ timestamp: this.timestamp,
3685
+ version: this.version,
3686
+ net: this.net,
3687
+ vat: this.vat,
3688
+ gross: this.gross,
3689
+ labourRate: this.labourRate,
3690
+ vatRate: this.vatRate,
3691
+ sundryRate: this.sundryRate,
3692
+ sundryMax: this.sundryMax,
3693
+ sundryFixed: this.sundryFixed,
3694
+ sundryMethod: this.sundryMethod,
3695
+ partsDiscount: this.partsDiscount,
3696
+ paintDiscount: this.paintDiscount,
3697
+ timeBasis: this.timeBasisSafe,
3698
+ items: this.items.map(i => i.toDto()),
3699
+ adjusted: this.adjusted,
3700
+ summary: this.summary,
3701
+ summaryAdjusted: this.summaryAdjusted
3702
+ };
3703
+ }
3704
+ getTotalHours(adjusted = false) {
3705
+ if (this.source === 'audatex') {
3706
+ const mechUnitsHours = this.items
3707
+ .filter(item => item.workCategory !== 'paint')
3708
+ .reduce((h, item) => h.add(b$1(item.getTotalUnits(adjusted))), b$1())
3709
+ .round(2)
3710
+ .div(this.timeBasisSafe);
3711
+ const paintHours = this.items
3712
+ .filter(item => item.workCategory === 'paint')
3713
+ .reduce((h, item) => h.add(b$1(item.getTotalUnits(adjusted))), b$1())
3714
+ .round(1)
3715
+ .div(this.timeBasisSafe);
3716
+ return mechUnitsHours.add(paintHours).toFixed(2);
3717
+ }
3718
+ const units = this.items
3719
+ .filter(i => i.costCategory === 'labour')
3720
+ .reduce((p, c) => p.add(b$1(c.getTotalUnits(adjusted))), b$1('0'))
3721
+ .toFixed(2);
3722
+ return b$1(units).div(this.timeBasisSafe).toFixed(2);
3723
+ }
3724
+ }
3725
+
3726
+ const b = (value = '0') => new Big(value);
3727
+ /**
3728
+ * Utility class for computing the summary value for an estimate control given
3729
+ * the first and last estimates
3730
+ */
3731
+ class EstimateControlValueModel {
3732
+ static calculateSummary(original, latest) {
3733
+ const o = original.summary;
3734
+ const l = latest.adjusted ?
3735
+ latest.summaryAdjusted :
3736
+ latest.summary;
3737
+ const s = {
3738
+ labour: b(o.labour).minus(l.labour).toFixed(2),
3739
+ none: b(o.none).minus(l.none).toFixed(2),
3740
+ parts: b(o.parts).minus(l.parts).toFixed(2),
3741
+ pm: b(o.pm).minus(l.pm).toFixed(2),
3742
+ pl: b(o.pm).minus(l.pl).toFixed(2),
3743
+ specialist: b(o.specialist).minus(l.specialist).toFixed(2),
3744
+ other: b(o.other).minus(l.other).toFixed(2),
3745
+ net: b(o.net).minus(l.net).toFixed(2),
3746
+ vat: b(o.vat).minus(l.vat).toFixed(2),
3747
+ gross: b(o.gross).minus(l.gross).toFixed(2),
3748
+ hours: b(o.hours).minus(l.hours).toFixed(0),
3749
+ labourRate: b(o.labourRate).minus(l.labourRate).toFixed(0)
3750
+ };
3751
+ return {
3752
+ original: o,
3753
+ latest: l,
3754
+ savings: s
3755
+ };
3756
+ }
3757
+ static getEmptySummary() {
3758
+ return {
3759
+ original: {
3760
+ labour: '0.00',
3761
+ none: '0.00',
3762
+ parts: '0.00',
3763
+ pm: '0.00',
3764
+ pl: '0.00',
3765
+ specialist: '0.00',
3766
+ other: '0.00',
3767
+ net: '0.00',
3768
+ vat: '0.00',
3769
+ gross: '0.00',
3770
+ hours: '0',
3771
+ labourRate: '0.00'
3772
+ },
3773
+ latest: {
3774
+ labour: '0.00',
3775
+ none: '0.00',
3776
+ parts: '0.00',
3777
+ pm: '0.00',
3778
+ pl: '0.00',
3779
+ specialist: '0.00',
3780
+ other: '0.00',
3781
+ net: '0.00',
3782
+ vat: '0.00',
3783
+ gross: '0.00',
3784
+ hours: '0',
3785
+ labourRate: '0.00'
3786
+ },
3787
+ savings: {
3788
+ labour: '0.00',
3789
+ none: '0.00',
3790
+ parts: '0.00',
3791
+ pm: '0.00',
3792
+ pl: '0.00',
3793
+ specialist: '0.00',
3794
+ other: '0.00',
3795
+ net: '0.00',
3796
+ vat: '0.00',
3797
+ gross: '0.00',
3798
+ hours: '0',
3799
+ labourRate: '0.00'
3800
+ }
3801
+ };
3802
+ }
3803
+ }
3324
3804
 
3325
3805
  const getEmptyGeoLocation = () => ({
3326
3806
  accuracy: 0,
@@ -3335,8 +3815,44 @@ const getEmptyGeoLocation = () => ({
3335
3815
  });
3336
3816
 
3337
3817
  /**
3338
- * Try to parse the value as JSON if it looks like a JSON encoded string
3339
- * @param value
3818
+ * Return true IFF the array contains only strings
3819
+ */
3820
+ const isArrayString = (value) => {
3821
+ return Array.isArray(value) && value.every(i => typeof i === 'string');
3822
+ };
3823
+
3824
+ /**
3825
+ * Return true if the argument is a plain object {}, i.e. not an array, null.
3826
+ */
3827
+ function isPlainObject(value) {
3828
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3829
+ }
3830
+
3831
+ /**
3832
+ * Return an object omitting properties specified by key(s)
3833
+ *
3834
+ * @param obj object to be processed
3835
+ * @param omitKey key or array of keys to be omitted
3836
+ */
3837
+ const omit = (obj, omitKey) => {
3838
+ return Object.keys(obj).reduce((result, key) => {
3839
+ if (Array.isArray(omitKey)) {
3840
+ if (!omitKey.includes(key)) {
3841
+ result[key] = obj[key];
3842
+ }
3843
+ }
3844
+ else {
3845
+ if (omitKey !== key) {
3846
+ result[key] = obj[key];
3847
+ }
3848
+ }
3849
+ return result;
3850
+ }, {});
3851
+ };
3852
+
3853
+ /**
3854
+ * Try to parse the value as JSON if it looks like a JSON encoded string, return
3855
+ * the original value if it's not a JSON string or parsing fails.
3340
3856
  */
3341
3857
  const tryJson = (value) => {
3342
3858
  if (typeof value === 'string' && ['{', '['].includes(value.substring(0, 1))) {
@@ -3350,21 +3866,35 @@ const tryJson = (value) => {
3350
3866
  return value;
3351
3867
  };
3352
3868
 
3353
- function isPlainObject(value) {
3354
- return typeof value === 'object' && value !== null && !Array.isArray(value);
3355
- }
3356
-
3869
+ const nullables = new Set([
3870
+ 'created',
3871
+ 'modified',
3872
+ 'reference',
3873
+ 'jobDate',
3874
+ 'followUpDate',
3875
+ 'followUpStatus',
3876
+ 'followUpNote',
3877
+ ]);
3357
3878
  let jobStaticProps;
3358
- function getJobStaticProperties() {
3879
+ function getStaticProps() {
3359
3880
  if (jobStaticProps) {
3360
3881
  return jobStaticProps;
3361
3882
  }
3362
- return jobStaticProps = new Set(Object.getOwnPropertyNames(VasJobModel.empty()));
3883
+ return jobStaticProps = new Set([
3884
+ ...Object.getOwnPropertyNames(VasJobDocumentModel.empty()),
3885
+ 'assignee'
3886
+ ]);
3887
+ }
3888
+ function isArrayOfFields(value) {
3889
+ return !!value && Array.isArray(value);
3890
+ }
3891
+ function isDictionaryOfFields(value) {
3892
+ return !!value && isPlainObject(value);
3363
3893
  }
3364
3894
  /**
3365
- * JobModel
3895
+ * VasJobBaseModel
3366
3896
  */
3367
- class VasJobModel extends VasRestrictedAccountObjectModel {
3897
+ class VasJobBaseModel extends VasRestrictedAccountObjectModel {
3368
3898
  id;
3369
3899
  created;
3370
3900
  serverCreated;
@@ -3379,6 +3909,7 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3379
3909
  reference;
3380
3910
  jobDate;
3381
3911
  jobStatus;
3912
+ jobStatusLastUpdate;
3382
3913
  jobType;
3383
3914
  assigneeId;
3384
3915
  formId;
@@ -3386,6 +3917,9 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3386
3917
  pendingFields;
3387
3918
  childModified;
3388
3919
  version;
3920
+ followUpDate;
3921
+ followUpStatus;
3922
+ followUpNote;
3389
3923
  geoLocation;
3390
3924
  files;
3391
3925
  fields;
@@ -3394,7 +3928,7 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3394
3928
  reports;
3395
3929
  tasks;
3396
3930
  instruction;
3397
- constructor(id, created, serverCreated, createdBy, modified, serverModified, modifiedBy, createdByName, modifiedByName, account, accessGroup, reference, jobDate, jobStatus, jobType, assigneeId, formId, timeZoneOffset, pendingFields, childModified, version, geoLocation, files, fields, jobEmails, notes, reports, tasks, instruction) {
3931
+ constructor(id, created, serverCreated, createdBy, modified, serverModified, modifiedBy, createdByName, modifiedByName, account, accessGroup, reference, jobDate, jobStatus, jobStatusLastUpdate, jobType, assigneeId, formId, timeZoneOffset, pendingFields, childModified, version, followUpDate, followUpStatus, followUpNote, geoLocation, files, fields, jobEmails, notes, reports, tasks, instruction) {
3398
3932
  super(id, created, serverCreated, createdBy, modified, serverModified, modifiedBy, createdByName, modifiedByName, account, accessGroup);
3399
3933
  this.id = id;
3400
3934
  this.created = created;
@@ -3410,6 +3944,7 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3410
3944
  this.reference = reference;
3411
3945
  this.jobDate = jobDate;
3412
3946
  this.jobStatus = jobStatus;
3947
+ this.jobStatusLastUpdate = jobStatusLastUpdate;
3413
3948
  this.jobType = jobType;
3414
3949
  this.assigneeId = assigneeId;
3415
3950
  this.formId = formId;
@@ -3417,6 +3952,9 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3417
3952
  this.pendingFields = pendingFields;
3418
3953
  this.childModified = childModified;
3419
3954
  this.version = version;
3955
+ this.followUpDate = followUpDate;
3956
+ this.followUpStatus = followUpStatus;
3957
+ this.followUpNote = followUpNote;
3420
3958
  this.geoLocation = geoLocation;
3421
3959
  this.files = files;
3422
3960
  this.fields = fields;
@@ -3436,52 +3974,7 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3436
3974
  * Return the list of Job properties
3437
3975
  */
3438
3976
  get staticProperties() {
3439
- if (jobStaticProps) {
3440
- return jobStaticProps;
3441
- }
3442
- return jobStaticProps = new Set(Object.getOwnPropertyNames(VasJobModel.empty()));
3443
- }
3444
- /**
3445
- * @return {VasJobModel}
3446
- */
3447
- static empty() {
3448
- return new VasJobModel('', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', moment().utcOffset(), 0, '', 0, getEmptyGeoLocation(), [], {}, [], [], [], [], null);
3449
- }
3450
- static fromDto(dto) {
3451
- // we have changed the job model and stopped using the dynamic properties
3452
- // from form group names to hold fields. Instead, we store a simple object
3453
- // where the control name (minus the "fields." prefix) is the key, and the
3454
- // field value is the value. However, we have to cater for data that may
3455
- // be persisted in the old structure, or an API response where fields is an
3456
- // array of VasFieldDto.
3457
- // 1: if dto.fields is already an object, we use the existing value
3458
- const fields = isPlainObject(dto.fields) ?
3459
- dto.fields :
3460
- {};
3461
- // 2: if dto.fields is an array, we're handling a vas-rest response, we
3462
- // iterate over the fields and update the fields object.
3463
- Array.isArray(dto.fields) && dto.fields.forEach((field) => {
3464
- fields[field.controlName.replace('fields.', '')] = tryJson(field.value);
3465
- });
3466
- // 3: if `fields` still has no keys, we're restoring a VasJobDto object that
3467
- // was saved in the old "form-group" style, where dynamic keys are
3468
- // derived from the form-group names. We'll iterate over those keys, and
3469
- // update the `fields` object. Once this model is serialized back to a
3470
- // dto, it will have the new shape.
3471
- if (Object.keys(fields).length === 0) {
3472
- Object
3473
- .keys(dto)
3474
- .filter(key => !getJobStaticProperties().has(key))
3475
- .forEach(groupName => {
3476
- Object
3477
- .keys(dto[groupName])
3478
- .forEach(controlName => {
3479
- const cn = controlName.replace('fields.', '');
3480
- fields[cn] = dto[groupName][controlName];
3481
- });
3482
- });
3483
- }
3484
- return new VasJobModel(dto.id || '', dto.created || '', dto.serverCreated || '', dto.createdBy || '', dto.modified || '', dto.serverModified || '', dto.modifiedBy || '', dto.createdByName || '', dto.modifiedByName || '', dto.account || '', dto.accessGroup || '', dto.reference || '', dto.jobDate || '', dto.jobStatus || '', dto.jobType || '', dto.assigneeId || '', dto.formId || dto.lastForm || '', dto.timeZoneOffset || moment().utcOffset(), dto.pendingFields || 0, dto.childModified || '', dto.version || 0, dto.geoLocation || getEmptyGeoLocation(), dto.files || [], fields, dto.jobEmails || [], dto.notes || [], dto.reports || [], dto.tasks || [], dto.instruction || null);
3977
+ return getStaticProps();
3485
3978
  }
3486
3979
  /**
3487
3980
  * Returns an object describing how many attachments were added to this job
@@ -3522,39 +4015,6 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3522
4015
  getValueByPath(path = []) {
3523
4016
  return getValueByPath(Array.isArray(path) ? path : path.split('.'), this);
3524
4017
  }
3525
- toDto() {
3526
- return {
3527
- id: this.id,
3528
- created: this.created,
3529
- createdBy: this.createdBy,
3530
- modified: this.modified,
3531
- modifiedBy: this.modifiedBy,
3532
- serverCreated: this.serverCreated,
3533
- serverModified: this.serverModified,
3534
- createdByName: this.createdByName,
3535
- modifiedByName: this.modifiedByName,
3536
- account: this.account,
3537
- accessGroup: this.accessGroup,
3538
- reference: this.reference,
3539
- jobDate: this.jobDate,
3540
- jobStatus: this.jobStatus,
3541
- jobType: this.jobType,
3542
- assigneeId: this.assigneeId,
3543
- formId: this.formId,
3544
- timeZoneOffset: this.timeZoneOffset,
3545
- pendingFields: this.pendingFields,
3546
- childModified: this.childModified,
3547
- version: this.version,
3548
- geoLocation: this.geoLocation,
3549
- files: this.files,
3550
- fields: this.fields,
3551
- jobEmails: this.jobEmails,
3552
- notes: this.notes,
3553
- reports: this.reports,
3554
- tasks: this.tasks,
3555
- instruction: this.instruction
3556
- };
3557
- }
3558
4018
  /**
3559
4019
  * This method will parse a syntax that allows us to build strings from the
3560
4020
  * values of from the job model. For example if the job model was:
@@ -3587,7 +4047,6 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3587
4047
  if (!value || typeof value !== 'string') {
3588
4048
  return '';
3589
4049
  }
3590
- const fields = this.fields;
3591
4050
  return value.replace(/({[^}]*})/g, (match, token) => {
3592
4051
  const syntax = token.substring(1, token.length - 1);
3593
4052
  let [key, filter] = syntax.split('|');
@@ -3613,8 +4072,8 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3613
4072
  if (objectKey === 'job') {
3614
4073
  result = (this.getValueByPath(path) || '').toString();
3615
4074
  }
3616
- else if (objectKey === 'fields' || this.fields[path[0]] !== undefined) {
3617
- result = (getValueByPath(path, fields) || '').toString();
4075
+ else if (objectKey === 'fields') {
4076
+ result = (this.getValueByPath(['fields', ...path]) || '').toString();
3618
4077
  }
3619
4078
  else if (objectKey.length && options.objects && options.objects[objectKey] !== undefined) {
3620
4079
  result = (getValueByPath(path, options.objects[objectKey]) || '').toString();
@@ -3672,8 +4131,242 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3672
4131
  'assigneeId',
3673
4132
  'timeZoneOffset',
3674
4133
  'pendingFields',
3675
- 'formId'
3676
- ]);
4134
+ 'formId',
4135
+ 'followUpDate',
4136
+ 'followUpStatus',
4137
+ 'followUpNote',
4138
+ ], (value, key) => value[key] === null && nullables.has(key) ? '~' : value[key]);
4139
+ }
4140
+ }
4141
+ class VasJobRelationalModel extends VasJobBaseModel {
4142
+ getValueByPath(path = []) {
4143
+ const parts = Array.isArray(path) ? path : path.split('.');
4144
+ if (parts[0] === 'fields') {
4145
+ return this.getFieldValueByPath(parts[1], parts.slice(2));
4146
+ }
4147
+ return getValueByPath(parts, this);
4148
+ }
4149
+ getFieldsMap() {
4150
+ return this.fields.reduce((p, c) => {
4151
+ p[c.controlName] = c.value;
4152
+ return p;
4153
+ }, {});
4154
+ }
4155
+ getFieldValueByPath(controlName, path) {
4156
+ for (const field of this.fields) {
4157
+ if (field.controlName === controlName) {
4158
+ if (path.length === 0) {
4159
+ return field.value;
4160
+ }
4161
+ if (field.value === null) {
4162
+ return path.length >= 1 ? undefined : null;
4163
+ }
4164
+ if (isPlainObject(field.value)) {
4165
+ return getValueByPath(path, field.value);
4166
+ }
4167
+ return undefined;
4168
+ }
4169
+ }
4170
+ return undefined;
4171
+ }
4172
+ static fromDto(dto) {
4173
+ if (dto.fields !== undefined && !Array.isArray(dto.fields)) {
4174
+ throw Error('fields must be an array');
4175
+ }
4176
+ return new VasJobRelationalModel(dto.id || '', dto.created || '', dto.serverCreated || '', dto.createdBy || '', dto.modified || '', dto.serverModified || '', dto.modifiedBy || '', dto.createdByName || '', dto.modifiedByName || '', dto.account || '', dto.accessGroup || '', dto.reference || '', dto.jobDate || '', dto.jobStatus || '', dto.jobStatusLastUpdate || '', dto.jobType || '', dto.assigneeId || '', dto.formId || dto.lastForm || '', dto.timeZoneOffset || moment().utcOffset(), dto.pendingFields || 0, dto.childModified || '', dto.version || 0, dto.followUpDate || '', dto.followUpStatus || '', dto.followUpNote || '', dto.geoLocation || getEmptyGeoLocation(), dto.files || [], (dto.fields || []).map(f => VasFieldModel.fromDto(f)), dto.jobEmails || [], dto.notes || [], dto.reports || [], dto.tasks || [], dto.instruction || null);
4177
+ }
4178
+ static empty() {
4179
+ return new VasJobRelationalModel('', // id
4180
+ '', // created
4181
+ '', // serverCreated
4182
+ '', // createdBy
4183
+ '', // modified
4184
+ '', // serverModified
4185
+ '', // modifiedBy
4186
+ '', // createdByName
4187
+ '', // modifiedByName
4188
+ '', // account
4189
+ '', // accessGroup
4190
+ '', // reference
4191
+ '', // jobDate
4192
+ '', // jobStatus
4193
+ '', // jobStatusLastUpdate
4194
+ '', // jobType
4195
+ '', // assigneeId
4196
+ '', // formId
4197
+ moment().utcOffset(), // timeZoneOffset
4198
+ 0, // pendingFields: number,
4199
+ '', // childModified: string,
4200
+ 0, // version: number,
4201
+ '', // followUpDate: string,
4202
+ '', // followUpStatus: string,
4203
+ '', // followUpNote: string,
4204
+ getEmptyGeoLocation(), // geoLocation: GeoLocation,
4205
+ [], // files: Array<VasFileDto>,
4206
+ [], // fields: FType,
4207
+ [], // jobEmails: Array<VasJobEmailDto>,
4208
+ [], // notes: Array<VasNoteDto>,
4209
+ [], // reports: Array<VasReportDto>,
4210
+ [], // tasks: Array<VasTaskDto>,
4211
+ null // instruction: VasInstructionDto | null
4212
+ );
4213
+ }
4214
+ toDto() {
4215
+ return {
4216
+ id: this.id,
4217
+ created: this.created,
4218
+ createdBy: this.createdBy,
4219
+ modified: this.modified,
4220
+ modifiedBy: this.modifiedBy,
4221
+ serverCreated: this.serverCreated,
4222
+ serverModified: this.serverModified,
4223
+ createdByName: this.createdByName,
4224
+ modifiedByName: this.modifiedByName,
4225
+ account: this.account,
4226
+ accessGroup: this.accessGroup,
4227
+ reference: this.reference,
4228
+ jobDate: this.jobDate,
4229
+ jobStatus: this.jobStatus,
4230
+ jobStatusLastUpdate: this.jobStatusLastUpdate,
4231
+ jobType: this.jobType,
4232
+ assigneeId: this.assigneeId,
4233
+ formId: this.formId,
4234
+ timeZoneOffset: this.timeZoneOffset,
4235
+ pendingFields: this.pendingFields,
4236
+ childModified: this.childModified,
4237
+ version: this.version,
4238
+ followUpDate: this.followUpDate,
4239
+ followUpStatus: this.followUpStatus,
4240
+ followUpNote: this.followUpNote,
4241
+ geoLocation: this.geoLocation,
4242
+ files: this.files,
4243
+ fields: this.fields.map(f => f.toDto()),
4244
+ jobEmails: this.jobEmails,
4245
+ notes: this.notes,
4246
+ reports: this.reports,
4247
+ tasks: this.tasks,
4248
+ instruction: this.instruction
4249
+ };
4250
+ }
4251
+ }
4252
+ class VasJobDocumentModel extends VasJobBaseModel {
4253
+ static fromRelationalDto(dto) {
4254
+ return new VasJobDocumentModel(dto.id || '', dto.created || '', dto.serverCreated || '', dto.createdBy || '', dto.modified || '', dto.serverModified || '', dto.modifiedBy || '', dto.createdByName || '', dto.modifiedByName || '', dto.account || '', dto.accessGroup || '', dto.reference || '', dto.jobDate || '', dto.jobStatus || '', dto.jobStatusLastUpdate || '', dto.jobType || '', dto.assigneeId || '', dto.formId || dto.lastForm || '', dto.timeZoneOffset || moment().utcOffset(), dto.pendingFields || 0, dto.childModified || '', dto.version || 0, dto.followUpDate || '', dto.followUpStatus || '', dto.followUpNote || '', dto.geoLocation || getEmptyGeoLocation(), dto.files || [], (dto.fields || []).reduce((p, c) => {
4255
+ p[c.controlName.replace('fields.', '')] = c.value;
4256
+ return p;
4257
+ }, {}), dto.jobEmails || [], dto.notes || [], dto.reports || [], dto.tasks || [], dto.instruction || null);
4258
+ }
4259
+ static fromDto(dto, options = { disableMigrateCheck: false }) {
4260
+ let fields = {};
4261
+ if (dto.fields !== undefined) {
4262
+ if (isArrayOfFields(dto.fields)) {
4263
+ for (const field of dto.fields) {
4264
+ fields[field.controlName.replace('fields.', '')] =
4265
+ tryJson(field.value);
4266
+ }
4267
+ }
4268
+ else if (isDictionaryOfFields(dto.fields)) {
4269
+ fields = dto.fields;
4270
+ }
4271
+ }
4272
+ // this code is checking if a have a document in the old dynamic job style,
4273
+ // i.e. where there are keys in the job object that come from the form-group
4274
+ // names. I did consider comparing the number of keys in the dto vs the
4275
+ // number of expected keys, but this will fail if we try to read old objects
4276
+ // that don't have new properties added. So we always check by default. We
4277
+ // can skip this check by setting disableMigrateCheck in the options.
4278
+ if (!options || !options.disableMigrateCheck) {
4279
+ const ddto = dto;
4280
+ const actualKeys = Object.keys(ddto);
4281
+ actualKeys.reduce((f, groupName) => {
4282
+ if (getStaticProps().has(groupName)) {
4283
+ return f;
4284
+ }
4285
+ Object
4286
+ .keys(ddto[groupName])
4287
+ .forEach(controlName => {
4288
+ if (!controlName.startsWith('fields.')) {
4289
+ return;
4290
+ }
4291
+ f[controlName.slice(7)] = ddto[groupName][controlName];
4292
+ });
4293
+ return f;
4294
+ }, fields);
4295
+ }
4296
+ return new VasJobDocumentModel(dto.id || '', dto.created || '', dto.serverCreated || '', dto.createdBy || '', dto.modified || '', dto.serverModified || '', dto.modifiedBy || '', dto.createdByName || '', dto.modifiedByName || '', dto.account || '', dto.accessGroup || '', dto.reference || '', dto.jobDate || '', dto.jobStatus || '', dto.jobStatusLastUpdate || '', dto.jobType || '', dto.assigneeId || '', dto.formId || dto.lastForm || '', dto.timeZoneOffset || moment().utcOffset(), dto.pendingFields || 0, dto.childModified || '', dto.version || 0, dto.followUpDate || '', dto.followUpStatus || '', dto.followUpNote || '', dto.geoLocation || getEmptyGeoLocation(), dto.files || [], fields, dto.jobEmails || [], dto.notes || [], dto.reports || [], dto.tasks || [], dto.instruction || null);
4297
+ }
4298
+ static empty() {
4299
+ return new VasJobDocumentModel('', // id
4300
+ '', // created
4301
+ '', // serverCreated
4302
+ '', // createdBy
4303
+ '', // modified
4304
+ '', // serverModified
4305
+ '', // modifiedBy
4306
+ '', // createdByName
4307
+ '', // modifiedByName
4308
+ '', // account
4309
+ '', // accessGroup
4310
+ '', // reference
4311
+ '', // jobDate
4312
+ '', // jobStatus
4313
+ '', // jobStatusLastUpdate
4314
+ '', // jobType
4315
+ '', // assigneeId
4316
+ '', // formId
4317
+ moment().utcOffset(), // timeZoneOffset
4318
+ 0, // pendingFields: number,
4319
+ '', // childModified: string,
4320
+ 0, // version: number,
4321
+ '', // followUpDate: string,
4322
+ '', // followUpStatus: string,
4323
+ '', // followUpNote: string,
4324
+ getEmptyGeoLocation(), // geoLocation: GeoLocation,
4325
+ [], // files: Array<VasFileDto>,
4326
+ {}, // fields: FType,
4327
+ [], // jobEmails: Array<VasJobEmailDto>,
4328
+ [], // notes: Array<VasNoteDto>,
4329
+ [], // reports: Array<VasReportDto>,
4330
+ [], // tasks: Array<VasTaskDto>,
4331
+ null // instruction: VasInstructionDto | null
4332
+ );
4333
+ }
4334
+ toDto() {
4335
+ return {
4336
+ id: this.id,
4337
+ created: this.created,
4338
+ createdBy: this.createdBy,
4339
+ modified: this.modified,
4340
+ modifiedBy: this.modifiedBy,
4341
+ serverCreated: this.serverCreated,
4342
+ serverModified: this.serverModified,
4343
+ createdByName: this.createdByName,
4344
+ modifiedByName: this.modifiedByName,
4345
+ account: this.account,
4346
+ accessGroup: this.accessGroup,
4347
+ reference: this.reference,
4348
+ jobDate: this.jobDate,
4349
+ jobStatus: this.jobStatus,
4350
+ jobStatusLastUpdate: this.jobStatusLastUpdate,
4351
+ jobType: this.jobType,
4352
+ assigneeId: this.assigneeId,
4353
+ formId: this.formId,
4354
+ timeZoneOffset: this.timeZoneOffset,
4355
+ pendingFields: this.pendingFields,
4356
+ childModified: this.childModified,
4357
+ version: this.version,
4358
+ followUpDate: this.followUpDate,
4359
+ followUpStatus: this.followUpStatus,
4360
+ followUpNote: this.followUpNote,
4361
+ geoLocation: this.geoLocation,
4362
+ files: this.files,
4363
+ fields: this.fields,
4364
+ jobEmails: this.jobEmails,
4365
+ notes: this.notes,
4366
+ reports: this.reports,
4367
+ tasks: this.tasks,
4368
+ instruction: this.instruction
4369
+ };
3677
4370
  }
3678
4371
  }
3679
4372
 
@@ -4284,5 +4977,5 @@ class VasVehicleModel extends VasAccountObjectModel {
4284
4977
  * Generated bundle index. Do not edit.
4285
4978
  */
4286
4979
 
4287
- export { VasAccessGroupModel, VasAccountIndexingMode, VasAccountObjectModel, VasBaseModel, VasBranchModel, VasContactModel, VasContactTypeModel, VasControlConfigDirection, VasControlModel, VasControlTypeModel, VasFieldModel, VasFileModel, VasFireUserModel, VasFormConfigModel, VasFormModel, VasGroupModel, VasInstructionJobFieldModel, VasInstructionJobModel, VasInstructionModel, VasInstructionProviderModel, VasInvitationModel, VasInvitationTypeEnum, VasJobActionControlsStepModel, VasJobActionEmailFormStepModel, VasJobActionInstructionStepModel, VasJobActionModel, VasJobActionNotePromptStepModel, VasJobActionSetDatetimeStepModel, VasJobActionSetJobStatusStepModel, VasJobActionSetValueStepModel, VasJobActionWebhookStepModel, VasJobCopyModel, VasJobDataModel, VasJobEmailModel, VasJobModel, VasJobStatusModel, VasJobTypeModel, VasLookupModel, VasLookupTypeModel, VasMembershipModel, VasNoteModel, VasReportLayoutModel, VasReportModel, VasReportRequestModel, VasRestrictedAccountObjectModel, VasTaskModel, VasTaskTypeModel, VasUserModel, VasVehicleModel, getEmptyGeoLocation, isCameraControlValueV1, isCameraControlValueV2, isFileDto, isJobAutomationStep, isJobControlsStep, isJobEmailFormStep, isJobInstructionStep, isJobNotePromptStep, isJobSetDatetimeStep, isJobSetJobStatusStep, isJobSetValueStep, isJobWebhookStep, prepareFieldApiValue, stepModelFactory };
4980
+ export { EstimateControlValueModel, EstimateItemModel, EstimateModel, VasAccessGroupModel, VasAccountIndexingMode, VasAccountObjectModel, VasBaseModel, VasBranchModel, VasContactModel, VasContactTypeModel, VasControlConfigDirection, VasControlModel, VasControlTypeModel, VasFieldModel, VasFileModel, VasFireUserModel, VasFormConfigModel, VasFormModel, VasGroupModel, VasInstructionJobFieldModel, VasInstructionJobModel, VasInstructionModel, VasInstructionProviderModel, VasInvitationModel, VasInvitationTypeEnum, VasJobActionControlsStepModel, VasJobActionEmailFormStepModel, VasJobActionInstructionStepModel, VasJobActionModel, VasJobActionNotePromptStepModel, VasJobActionSetDatetimeStepModel, VasJobActionSetJobStatusStepModel, VasJobActionSetValueStepModel, VasJobActionWebhookStepModel, VasJobBaseModel, VasJobCopyModel, VasJobDataModel, VasJobDocumentModel, VasJobEmailModel, VasJobRelationalModel, VasJobStatusModel, VasJobTypeModel, VasLookupModel, VasLookupTypeModel, VasMembershipModel, VasNoteModel, VasReportLayoutModel, VasReportModel, VasReportRequestModel, VasRestrictedAccountObjectModel, VasTaskModel, VasTaskTypeModel, VasUserModel, VasVehicleModel, getEmptyGeoLocation, getValueByPath, isArrayString, isCameraControlValueV1, isCameraControlValueV2, isFileDto, isJobAutomationStep, isJobControlsStep, isJobEmailFormStep, isJobInstructionStep, isJobNotePromptStep, isJobSetDatetimeStep, isJobSetJobStatusStep, isJobSetValueStep, isJobWebhookStep, isNumeric, isPlainObject, omit, only, prepareFieldApiValue, stepModelFactory, tryJson };
4288
4981
  //# sourceMappingURL=ironcode-vas-lib.mjs.map