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

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 +278 -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 +841 -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,546 @@ 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
+ this.summaryAdjusted = this.getSummary(this.summaryNet, this.sundryRate, this.sundryMax, this.sundryFixed, this.sundryMethod, this.partsDiscount, this.paintDiscount, true);
3572
+ }
3573
+ this.summary = this.getSummary(this.summaryNet, this.sundryRate, this.sundryMax, this.sundryFixed, this.sundryMethod, this.partsDiscount, this.paintDiscount, false);
3574
+ this.net = this.summary.net;
3575
+ this.vat = this.summary.vat;
3576
+ this.gross = this.summary.gross;
3577
+ }
3578
+ /**
3579
+ * Returns the net summary (before sundries and discounts) of the estimate.
3580
+ * By summary, we mean grouping the costs by cost category.
3581
+ *
3582
+ * @param adjusted if the values should be calculated using the original or
3583
+ * adjusted values
3584
+ */
3585
+ getSummaryNet(adjusted = false) {
3586
+ const summary = this.items.reduce((map, item) => {
3587
+ map[item.costCategory] = b$1(map[item.costCategory] || '0')
3588
+ .add(item.getLineValue(this.labourRate, this.timeBasisSafe, adjusted, this.source === 'gtmotive' ? false : undefined))
3589
+ .toFixed(2);
3590
+ return map;
3591
+ }, {
3592
+ labour: '0.00',
3593
+ none: '0.00',
3594
+ parts: '0.00',
3595
+ pm: '0.00',
3596
+ pl: '0.00',
3597
+ specialist: '0.00',
3598
+ other: '0.00',
3599
+ net: '0.00',
3600
+ vat: '0.00',
3601
+ gross: '0.00',
3602
+ hours: '0.00',
3603
+ labourRate: '0',
3604
+ });
3605
+ if (this.source === 'audatex') {
3606
+ const mechHours = this.items
3607
+ .reduce((h, item) => {
3608
+ return isLabourWorkCategory(item.workCategory) ?
3609
+ h.add(b$1(item.quantity || '0').mul(item.units || '0')) :
3610
+ h;
3611
+ }, b$1())
3612
+ .div(this.timeBasisSafe || '1')
3613
+ .round(2);
3614
+ const paintHours = this.items
3615
+ .reduce((h, item) => {
3616
+ return isPaintWorkCategory(item.workCategory) ?
3617
+ h.add(b$1(item.quantity || '0').mul(item.units || '0')) :
3618
+ h;
3619
+ }, b$1())
3620
+ .div(this.timeBasisSafe || '1')
3621
+ .round(1);
3622
+ const mechCost = mechHours.mul(this.labourRate || '0');
3623
+ const paintCost = paintHours.mul(this.labourRate || '0');
3624
+ summary.labour = mechCost.add(paintCost).toFixed(2);
3625
+ }
3626
+ return summary;
3627
+ }
3628
+ /**
3629
+ * Applies the sundry and discount values to the net summary.
3630
+ */
3631
+ getSummary(summaryNet, sundryRate, sundryMax, sundryFixed, sundryMethod, partsDiscount, paintDiscount, adjusted) {
3632
+ const partsDisc = b$1('1').minus(partsDiscount || '0');
3633
+ const paintDisc = b$1('1').minus(paintDiscount || '0');
3634
+ let partsSundry = '0.00';
3635
+ if (sundryMethod === 'percentage') {
3636
+ partsSundry = b$1(summaryNet.parts).mul(sundryRate || '1').toFixed(2);
3637
+ }
3638
+ else if (sundryMethod === 'percentage-with-max') {
3639
+ partsSundry = b$1(summaryNet.parts).mul(sundryRate || '1').toFixed(2);
3640
+ partsSundry = Math.min(parseFloat(sundryMax || '0'), parseFloat(partsSundry)).toFixed(2);
3641
+ }
3642
+ else if (sundryMethod === 'fixed') {
3643
+ partsSundry = b$1(sundryFixed).toFixed(2);
3644
+ }
3645
+ const parts = b$1(summaryNet.parts).add(partsSundry);
3646
+ const net = b$1(0)
3647
+ .add(summaryNet.labour)
3648
+ .add(summaryNet.none)
3649
+ .add(parts)
3650
+ .add(summaryNet.pm)
3651
+ .add(summaryNet.specialist)
3652
+ .toFixed(2);
3653
+ const vat = b$1(net || '0')
3654
+ .mul(this.vatRate || '0')
3655
+ .toFixed(2);
3656
+ let gross = b$1(net || '0').add(vat || '0').toFixed(2);
3657
+ // calculate the total value of lines that are VAT excluded
3658
+ const vatExcludeTotal = this.items.reduce((p, c) => c.vatExcluded ?
3659
+ p.add(c.getLineValue(this.labourRate, this.timeBasisSafe, adjusted, true)) :
3660
+ p, b$1());
3661
+ gross = b$1(gross).add(vatExcludeTotal).toFixed(2);
3662
+ return {
3663
+ labour: summaryNet.labour,
3664
+ none: summaryNet.none,
3665
+ parts: b$1(parts).mul(partsDisc).toFixed(2),
3666
+ pm: b$1(summaryNet.pm).mul(paintDisc).toFixed(2),
3667
+ pl: summaryNet.pl,
3668
+ specialist: summaryNet.specialist,
3669
+ other: summaryNet.other,
3670
+ net: net,
3671
+ vat: vat,
3672
+ gross: gross,
3673
+ labourRate: this.labourRate,
3674
+ hours: this.getTotalHours(adjusted)
3675
+ };
3676
+ }
3677
+ toDto() {
3678
+ return {
3679
+ id: this.id,
3680
+ source: this.source,
3681
+ label: this.label,
3682
+ comment: this.comment,
3683
+ timestamp: this.timestamp,
3684
+ version: this.version,
3685
+ net: this.net,
3686
+ vat: this.vat,
3687
+ gross: this.gross,
3688
+ labourRate: this.labourRate,
3689
+ vatRate: this.vatRate,
3690
+ sundryRate: this.sundryRate,
3691
+ sundryMax: this.sundryMax,
3692
+ sundryFixed: this.sundryFixed,
3693
+ sundryMethod: this.sundryMethod,
3694
+ partsDiscount: this.partsDiscount,
3695
+ paintDiscount: this.paintDiscount,
3696
+ timeBasis: this.timeBasisSafe,
3697
+ items: this.items.map(i => i.toDto()),
3698
+ adjusted: this.adjusted,
3699
+ summary: this.summary,
3700
+ summaryAdjusted: this.summaryAdjusted
3701
+ };
3702
+ }
3703
+ getTotalHours(adjusted = false) {
3704
+ if (this.source === 'audatex') {
3705
+ const mechUnitsHours = this.items
3706
+ .filter(item => item.workCategory !== 'paint')
3707
+ .reduce((h, item) => h.add(b$1(item.getTotalUnits(adjusted))), b$1())
3708
+ .round(2)
3709
+ .div(this.timeBasisSafe);
3710
+ const paintHours = this.items
3711
+ .filter(item => item.workCategory === 'paint')
3712
+ .reduce((h, item) => h.add(b$1(item.getTotalUnits(adjusted))), b$1())
3713
+ .round(1)
3714
+ .div(this.timeBasisSafe);
3715
+ return mechUnitsHours.add(paintHours).toFixed(2);
3716
+ }
3717
+ const units = this.items
3718
+ .filter(i => i.costCategory === 'labour')
3719
+ .reduce((p, c) => p.add(b$1(c.getTotalUnits(adjusted))), b$1('0'))
3720
+ .toFixed(2);
3721
+ return b$1(units).div(this.timeBasisSafe).toFixed(2);
3722
+ }
3723
+ }
3724
+
3725
+ const b = (value = '0') => new Big(value);
3726
+ /**
3727
+ * Utility class for computing the summary value for an estimate control given
3728
+ * the first and last estimates
3729
+ */
3730
+ class EstimateControlValueModel {
3731
+ static calculateSummary(original, latest) {
3732
+ const o = original.summary;
3733
+ const l = latest.adjusted ?
3734
+ latest.summaryAdjusted :
3735
+ latest.summary;
3736
+ const s = {
3737
+ labour: b(o.labour).minus(l.labour).toFixed(2),
3738
+ none: b(o.none).minus(l.none).toFixed(2),
3739
+ parts: b(o.parts).minus(l.parts).toFixed(2),
3740
+ pm: b(o.pm).minus(l.pm).toFixed(2),
3741
+ pl: b(o.pm).minus(l.pl).toFixed(2),
3742
+ specialist: b(o.specialist).minus(l.specialist).toFixed(2),
3743
+ other: b(o.other).minus(l.other).toFixed(2),
3744
+ net: b(o.net).minus(l.net).toFixed(2),
3745
+ vat: b(o.vat).minus(l.vat).toFixed(2),
3746
+ gross: b(o.gross).minus(l.gross).toFixed(2),
3747
+ hours: b(o.hours).minus(l.hours).toFixed(0),
3748
+ labourRate: b(o.labourRate).minus(l.labourRate).toFixed(0)
3749
+ };
3750
+ return {
3751
+ original: o,
3752
+ latest: l,
3753
+ savings: s
3754
+ };
3755
+ }
3756
+ static getEmptySummary() {
3757
+ return {
3758
+ original: {
3759
+ labour: '0.00',
3760
+ none: '0.00',
3761
+ parts: '0.00',
3762
+ pm: '0.00',
3763
+ pl: '0.00',
3764
+ specialist: '0.00',
3765
+ other: '0.00',
3766
+ net: '0.00',
3767
+ vat: '0.00',
3768
+ gross: '0.00',
3769
+ hours: '0',
3770
+ labourRate: '0.00'
3771
+ },
3772
+ latest: {
3773
+ labour: '0.00',
3774
+ none: '0.00',
3775
+ parts: '0.00',
3776
+ pm: '0.00',
3777
+ pl: '0.00',
3778
+ specialist: '0.00',
3779
+ other: '0.00',
3780
+ net: '0.00',
3781
+ vat: '0.00',
3782
+ gross: '0.00',
3783
+ hours: '0',
3784
+ labourRate: '0.00'
3785
+ },
3786
+ savings: {
3787
+ labour: '0.00',
3788
+ none: '0.00',
3789
+ parts: '0.00',
3790
+ pm: '0.00',
3791
+ pl: '0.00',
3792
+ specialist: '0.00',
3793
+ other: '0.00',
3794
+ net: '0.00',
3795
+ vat: '0.00',
3796
+ gross: '0.00',
3797
+ hours: '0',
3798
+ labourRate: '0.00'
3799
+ }
3800
+ };
3801
+ }
3802
+ }
3324
3803
 
3325
3804
  const getEmptyGeoLocation = () => ({
3326
3805
  accuracy: 0,
@@ -3335,8 +3814,44 @@ const getEmptyGeoLocation = () => ({
3335
3814
  });
3336
3815
 
3337
3816
  /**
3338
- * Try to parse the value as JSON if it looks like a JSON encoded string
3339
- * @param value
3817
+ * Return true IFF the array contains only strings
3818
+ */
3819
+ const isArrayString = (value) => {
3820
+ return Array.isArray(value) && value.every(i => typeof i === 'string');
3821
+ };
3822
+
3823
+ /**
3824
+ * Return true if the argument is a plain object {}, i.e. not an array, null.
3825
+ */
3826
+ function isPlainObject(value) {
3827
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3828
+ }
3829
+
3830
+ /**
3831
+ * Return an object omitting properties specified by key(s)
3832
+ *
3833
+ * @param obj object to be processed
3834
+ * @param omitKey key or array of keys to be omitted
3835
+ */
3836
+ const omit = (obj, omitKey) => {
3837
+ return Object.keys(obj).reduce((result, key) => {
3838
+ if (Array.isArray(omitKey)) {
3839
+ if (!omitKey.includes(key)) {
3840
+ result[key] = obj[key];
3841
+ }
3842
+ }
3843
+ else {
3844
+ if (omitKey !== key) {
3845
+ result[key] = obj[key];
3846
+ }
3847
+ }
3848
+ return result;
3849
+ }, {});
3850
+ };
3851
+
3852
+ /**
3853
+ * Try to parse the value as JSON if it looks like a JSON encoded string, return
3854
+ * the original value if it's not a JSON string or parsing fails.
3340
3855
  */
3341
3856
  const tryJson = (value) => {
3342
3857
  if (typeof value === 'string' && ['{', '['].includes(value.substring(0, 1))) {
@@ -3350,21 +3865,35 @@ const tryJson = (value) => {
3350
3865
  return value;
3351
3866
  };
3352
3867
 
3353
- function isPlainObject(value) {
3354
- return typeof value === 'object' && value !== null && !Array.isArray(value);
3355
- }
3356
-
3868
+ const nullables = new Set([
3869
+ 'created',
3870
+ 'modified',
3871
+ 'reference',
3872
+ 'jobDate',
3873
+ 'followUpDate',
3874
+ 'followUpStatus',
3875
+ 'followUpNote',
3876
+ ]);
3357
3877
  let jobStaticProps;
3358
- function getJobStaticProperties() {
3878
+ function getStaticProps() {
3359
3879
  if (jobStaticProps) {
3360
3880
  return jobStaticProps;
3361
3881
  }
3362
- return jobStaticProps = new Set(Object.getOwnPropertyNames(VasJobModel.empty()));
3882
+ return jobStaticProps = new Set([
3883
+ ...Object.getOwnPropertyNames(VasJobDocumentModel.empty()),
3884
+ 'assignee'
3885
+ ]);
3886
+ }
3887
+ function isArrayOfFields(value) {
3888
+ return !!value && Array.isArray(value);
3889
+ }
3890
+ function isDictionaryOfFields(value) {
3891
+ return !!value && isPlainObject(value);
3363
3892
  }
3364
3893
  /**
3365
- * JobModel
3894
+ * VasJobBaseModel
3366
3895
  */
3367
- class VasJobModel extends VasRestrictedAccountObjectModel {
3896
+ class VasJobBaseModel extends VasRestrictedAccountObjectModel {
3368
3897
  id;
3369
3898
  created;
3370
3899
  serverCreated;
@@ -3379,6 +3908,7 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3379
3908
  reference;
3380
3909
  jobDate;
3381
3910
  jobStatus;
3911
+ jobStatusLastUpdate;
3382
3912
  jobType;
3383
3913
  assigneeId;
3384
3914
  formId;
@@ -3386,6 +3916,9 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3386
3916
  pendingFields;
3387
3917
  childModified;
3388
3918
  version;
3919
+ followUpDate;
3920
+ followUpStatus;
3921
+ followUpNote;
3389
3922
  geoLocation;
3390
3923
  files;
3391
3924
  fields;
@@ -3394,7 +3927,7 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3394
3927
  reports;
3395
3928
  tasks;
3396
3929
  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) {
3930
+ 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
3931
  super(id, created, serverCreated, createdBy, modified, serverModified, modifiedBy, createdByName, modifiedByName, account, accessGroup);
3399
3932
  this.id = id;
3400
3933
  this.created = created;
@@ -3410,6 +3943,7 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3410
3943
  this.reference = reference;
3411
3944
  this.jobDate = jobDate;
3412
3945
  this.jobStatus = jobStatus;
3946
+ this.jobStatusLastUpdate = jobStatusLastUpdate;
3413
3947
  this.jobType = jobType;
3414
3948
  this.assigneeId = assigneeId;
3415
3949
  this.formId = formId;
@@ -3417,6 +3951,9 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3417
3951
  this.pendingFields = pendingFields;
3418
3952
  this.childModified = childModified;
3419
3953
  this.version = version;
3954
+ this.followUpDate = followUpDate;
3955
+ this.followUpStatus = followUpStatus;
3956
+ this.followUpNote = followUpNote;
3420
3957
  this.geoLocation = geoLocation;
3421
3958
  this.files = files;
3422
3959
  this.fields = fields;
@@ -3436,52 +3973,7 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3436
3973
  * Return the list of Job properties
3437
3974
  */
3438
3975
  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);
3976
+ return getStaticProps();
3485
3977
  }
3486
3978
  /**
3487
3979
  * Returns an object describing how many attachments were added to this job
@@ -3522,39 +4014,6 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3522
4014
  getValueByPath(path = []) {
3523
4015
  return getValueByPath(Array.isArray(path) ? path : path.split('.'), this);
3524
4016
  }
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
4017
  /**
3559
4018
  * This method will parse a syntax that allows us to build strings from the
3560
4019
  * values of from the job model. For example if the job model was:
@@ -3587,7 +4046,6 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3587
4046
  if (!value || typeof value !== 'string') {
3588
4047
  return '';
3589
4048
  }
3590
- const fields = this.fields;
3591
4049
  return value.replace(/({[^}]*})/g, (match, token) => {
3592
4050
  const syntax = token.substring(1, token.length - 1);
3593
4051
  let [key, filter] = syntax.split('|');
@@ -3613,8 +4071,8 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3613
4071
  if (objectKey === 'job') {
3614
4072
  result = (this.getValueByPath(path) || '').toString();
3615
4073
  }
3616
- else if (objectKey === 'fields' || this.fields[path[0]] !== undefined) {
3617
- result = (getValueByPath(path, fields) || '').toString();
4074
+ else if (objectKey === 'fields') {
4075
+ result = (this.getValueByPath(['fields', ...path]) || '').toString();
3618
4076
  }
3619
4077
  else if (objectKey.length && options.objects && options.objects[objectKey] !== undefined) {
3620
4078
  result = (getValueByPath(path, options.objects[objectKey]) || '').toString();
@@ -3672,8 +4130,242 @@ class VasJobModel extends VasRestrictedAccountObjectModel {
3672
4130
  'assigneeId',
3673
4131
  'timeZoneOffset',
3674
4132
  'pendingFields',
3675
- 'formId'
3676
- ]);
4133
+ 'formId',
4134
+ 'followUpDate',
4135
+ 'followUpStatus',
4136
+ 'followUpNote',
4137
+ ], (value, key) => value[key] === null && nullables.has(key) ? '~' : value[key]);
4138
+ }
4139
+ }
4140
+ class VasJobRelationalModel extends VasJobBaseModel {
4141
+ getValueByPath(path = []) {
4142
+ const parts = Array.isArray(path) ? path : path.split('.');
4143
+ if (parts[0] === 'fields') {
4144
+ return this.getFieldValueByPath(parts[1], parts.slice(2));
4145
+ }
4146
+ return getValueByPath(parts, this);
4147
+ }
4148
+ getFieldsMap() {
4149
+ return this.fields.reduce((p, c) => {
4150
+ p[c.controlName] = c.value;
4151
+ return p;
4152
+ }, {});
4153
+ }
4154
+ getFieldValueByPath(controlName, path) {
4155
+ for (const field of this.fields) {
4156
+ if (field.controlName === controlName) {
4157
+ if (path.length === 0) {
4158
+ return field.value;
4159
+ }
4160
+ if (field.value === null) {
4161
+ return path.length >= 1 ? undefined : null;
4162
+ }
4163
+ if (isPlainObject(field.value)) {
4164
+ return getValueByPath(path, field.value);
4165
+ }
4166
+ return undefined;
4167
+ }
4168
+ }
4169
+ return undefined;
4170
+ }
4171
+ static fromDto(dto) {
4172
+ if (dto.fields !== undefined && !Array.isArray(dto.fields)) {
4173
+ throw Error('fields must be an array');
4174
+ }
4175
+ 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);
4176
+ }
4177
+ static empty() {
4178
+ return new VasJobRelationalModel('', // id
4179
+ '', // created
4180
+ '', // serverCreated
4181
+ '', // createdBy
4182
+ '', // modified
4183
+ '', // serverModified
4184
+ '', // modifiedBy
4185
+ '', // createdByName
4186
+ '', // modifiedByName
4187
+ '', // account
4188
+ '', // accessGroup
4189
+ '', // reference
4190
+ '', // jobDate
4191
+ '', // jobStatus
4192
+ '', // jobStatusLastUpdate
4193
+ '', // jobType
4194
+ '', // assigneeId
4195
+ '', // formId
4196
+ moment().utcOffset(), // timeZoneOffset
4197
+ 0, // pendingFields: number,
4198
+ '', // childModified: string,
4199
+ 0, // version: number,
4200
+ '', // followUpDate: string,
4201
+ '', // followUpStatus: string,
4202
+ '', // followUpNote: string,
4203
+ getEmptyGeoLocation(), // geoLocation: GeoLocation,
4204
+ [], // files: Array<VasFileDto>,
4205
+ [], // fields: FType,
4206
+ [], // jobEmails: Array<VasJobEmailDto>,
4207
+ [], // notes: Array<VasNoteDto>,
4208
+ [], // reports: Array<VasReportDto>,
4209
+ [], // tasks: Array<VasTaskDto>,
4210
+ null // instruction: VasInstructionDto | null
4211
+ );
4212
+ }
4213
+ toDto() {
4214
+ return {
4215
+ id: this.id,
4216
+ created: this.created,
4217
+ createdBy: this.createdBy,
4218
+ modified: this.modified,
4219
+ modifiedBy: this.modifiedBy,
4220
+ serverCreated: this.serverCreated,
4221
+ serverModified: this.serverModified,
4222
+ createdByName: this.createdByName,
4223
+ modifiedByName: this.modifiedByName,
4224
+ account: this.account,
4225
+ accessGroup: this.accessGroup,
4226
+ reference: this.reference,
4227
+ jobDate: this.jobDate,
4228
+ jobStatus: this.jobStatus,
4229
+ jobStatusLastUpdate: this.jobStatusLastUpdate,
4230
+ jobType: this.jobType,
4231
+ assigneeId: this.assigneeId,
4232
+ formId: this.formId,
4233
+ timeZoneOffset: this.timeZoneOffset,
4234
+ pendingFields: this.pendingFields,
4235
+ childModified: this.childModified,
4236
+ version: this.version,
4237
+ followUpDate: this.followUpDate,
4238
+ followUpStatus: this.followUpStatus,
4239
+ followUpNote: this.followUpNote,
4240
+ geoLocation: this.geoLocation,
4241
+ files: this.files,
4242
+ fields: this.fields.map(f => f.toDto()),
4243
+ jobEmails: this.jobEmails,
4244
+ notes: this.notes,
4245
+ reports: this.reports,
4246
+ tasks: this.tasks,
4247
+ instruction: this.instruction
4248
+ };
4249
+ }
4250
+ }
4251
+ class VasJobDocumentModel extends VasJobBaseModel {
4252
+ static fromRelationalDto(dto) {
4253
+ 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) => {
4254
+ p[c.controlName.replace('fields.', '')] = c.value;
4255
+ return p;
4256
+ }, {}), dto.jobEmails || [], dto.notes || [], dto.reports || [], dto.tasks || [], dto.instruction || null);
4257
+ }
4258
+ static fromDto(dto, options = { disableMigrateCheck: false }) {
4259
+ let fields = {};
4260
+ if (dto.fields !== undefined) {
4261
+ if (isArrayOfFields(dto.fields)) {
4262
+ for (const field of dto.fields) {
4263
+ fields[field.controlName.replace('fields.', '')] =
4264
+ tryJson(field.value);
4265
+ }
4266
+ }
4267
+ else if (isDictionaryOfFields(dto.fields)) {
4268
+ fields = dto.fields;
4269
+ }
4270
+ }
4271
+ // this code is checking if a have a document in the old dynamic job style,
4272
+ // i.e. where there are keys in the job object that come from the form-group
4273
+ // names. I did consider comparing the number of keys in the dto vs the
4274
+ // number of expected keys, but this will fail if we try to read old objects
4275
+ // that don't have new properties added. So we always check by default. We
4276
+ // can skip this check by setting disableMigrateCheck in the options.
4277
+ if (!options || !options.disableMigrateCheck) {
4278
+ const ddto = dto;
4279
+ const actualKeys = Object.keys(ddto);
4280
+ actualKeys.reduce((f, groupName) => {
4281
+ if (getStaticProps().has(groupName)) {
4282
+ return f;
4283
+ }
4284
+ Object
4285
+ .keys(ddto[groupName])
4286
+ .forEach(controlName => {
4287
+ if (!controlName.startsWith('fields.')) {
4288
+ return;
4289
+ }
4290
+ f[controlName.slice(7)] = ddto[groupName][controlName];
4291
+ });
4292
+ return f;
4293
+ }, fields);
4294
+ }
4295
+ 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);
4296
+ }
4297
+ static empty() {
4298
+ return new VasJobDocumentModel('', // id
4299
+ '', // created
4300
+ '', // serverCreated
4301
+ '', // createdBy
4302
+ '', // modified
4303
+ '', // serverModified
4304
+ '', // modifiedBy
4305
+ '', // createdByName
4306
+ '', // modifiedByName
4307
+ '', // account
4308
+ '', // accessGroup
4309
+ '', // reference
4310
+ '', // jobDate
4311
+ '', // jobStatus
4312
+ '', // jobStatusLastUpdate
4313
+ '', // jobType
4314
+ '', // assigneeId
4315
+ '', // formId
4316
+ moment().utcOffset(), // timeZoneOffset
4317
+ 0, // pendingFields: number,
4318
+ '', // childModified: string,
4319
+ 0, // version: number,
4320
+ '', // followUpDate: string,
4321
+ '', // followUpStatus: string,
4322
+ '', // followUpNote: string,
4323
+ getEmptyGeoLocation(), // geoLocation: GeoLocation,
4324
+ [], // files: Array<VasFileDto>,
4325
+ {}, // fields: FType,
4326
+ [], // jobEmails: Array<VasJobEmailDto>,
4327
+ [], // notes: Array<VasNoteDto>,
4328
+ [], // reports: Array<VasReportDto>,
4329
+ [], // tasks: Array<VasTaskDto>,
4330
+ null // instruction: VasInstructionDto | null
4331
+ );
4332
+ }
4333
+ toDto() {
4334
+ return {
4335
+ id: this.id,
4336
+ created: this.created,
4337
+ createdBy: this.createdBy,
4338
+ modified: this.modified,
4339
+ modifiedBy: this.modifiedBy,
4340
+ serverCreated: this.serverCreated,
4341
+ serverModified: this.serverModified,
4342
+ createdByName: this.createdByName,
4343
+ modifiedByName: this.modifiedByName,
4344
+ account: this.account,
4345
+ accessGroup: this.accessGroup,
4346
+ reference: this.reference,
4347
+ jobDate: this.jobDate,
4348
+ jobStatus: this.jobStatus,
4349
+ jobStatusLastUpdate: this.jobStatusLastUpdate,
4350
+ jobType: this.jobType,
4351
+ assigneeId: this.assigneeId,
4352
+ formId: this.formId,
4353
+ timeZoneOffset: this.timeZoneOffset,
4354
+ pendingFields: this.pendingFields,
4355
+ childModified: this.childModified,
4356
+ version: this.version,
4357
+ followUpDate: this.followUpDate,
4358
+ followUpStatus: this.followUpStatus,
4359
+ followUpNote: this.followUpNote,
4360
+ geoLocation: this.geoLocation,
4361
+ files: this.files,
4362
+ fields: this.fields,
4363
+ jobEmails: this.jobEmails,
4364
+ notes: this.notes,
4365
+ reports: this.reports,
4366
+ tasks: this.tasks,
4367
+ instruction: this.instruction
4368
+ };
3677
4369
  }
3678
4370
  }
3679
4371
 
@@ -4284,5 +4976,5 @@ class VasVehicleModel extends VasAccountObjectModel {
4284
4976
  * Generated bundle index. Do not edit.
4285
4977
  */
4286
4978
 
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 };
4979
+ 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
4980
  //# sourceMappingURL=ironcode-vas-lib.mjs.map