tin-spa 20.3.1 → 20.3.2

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.
@@ -683,8 +683,17 @@ class DetailsDialogProcessor {
683
683
  }
684
684
  function transformStepUrl(stepConfig) {
685
685
  if (stepConfig.loadAction && stepConfig.loadIDField) {
686
- // let heroId = updatedDetailsConfig.details[updatedDetailsConfig.heroField];
687
- stepConfig.loadAction.url = `${stepConfig.loadAction.url.replace('{id}', updatedDetailsConfig.details[stepConfig.loadIDField])}`;
686
+ // Changed: Support generic {propertyName} placeholders
687
+ if (stepConfig.loadAction.url.includes('{')) {
688
+ stepConfig.loadAction.url = stepConfig.loadAction.url.replace(/\{(\w+)\}/g, (match, propName) => {
689
+ const value = updatedDetailsConfig.details?.[propName];
690
+ return value !== undefined && value !== null ? String(value) : match;
691
+ });
692
+ }
693
+ else {
694
+ // Fallback to legacy {id} replacement for backward compatibility
695
+ stepConfig.loadAction.url = `${stepConfig.loadAction.url.replace('{id}', updatedDetailsConfig.details[stepConfig.loadIDField])}`;
696
+ }
688
697
  // console.log("Transformed step URL", stepConfig.loadAction.url);
689
698
  }
690
699
  }
@@ -755,7 +764,16 @@ class DetailsDialogProcessor {
755
764
  static loadDetailsFromUrl(detailsConfig, formConfig) {
756
765
  let action;
757
766
  if (formConfig.loadAction) {
758
- if (detailsConfig.heroField && detailsConfig.details) {
767
+ // Changed: Check if URL contains {propertyName} placeholders first - takes priority over hero field
768
+ if (formConfig.loadAction.url.includes('{') && detailsConfig.details) {
769
+ const transformedUrl = formConfig.loadAction.url.replace(/\{(\w+)\}/g, (match, propName) => {
770
+ const value = detailsConfig.details?.[propName];
771
+ return value !== undefined && value !== null ? String(value) : match;
772
+ });
773
+ action = { url: transformedUrl };
774
+ console.log("Transformed form URL (placeholder)", transformedUrl);
775
+ }
776
+ else if (detailsConfig.heroField && detailsConfig.details) {
759
777
  // console.log("DetailsConfig Hero ID Field " + detailsConfig.heroField);
760
778
  action = { url: `${formConfig.loadAction.url}/${detailsConfig.details[detailsConfig.heroField]}` };
761
779
  }
@@ -802,6 +820,19 @@ var InvoiceStatus;
802
820
  InvoiceStatus[InvoiceStatus["Discarded"] = 3] = "Discarded";
803
821
  InvoiceStatus[InvoiceStatus["Paying"] = 4] = "Paying"; // Changed: Added Paying status for partially paid invoices
804
822
  })(InvoiceStatus || (InvoiceStatus = {}));
823
+ // Added: Invoice item type enum - mirrors backend InvoiceItemType
824
+ var InvoiceItemType;
825
+ (function (InvoiceItemType) {
826
+ InvoiceItemType[InvoiceItemType["General"] = 0] = "General";
827
+ InvoiceItemType[InvoiceItemType["Product"] = 1] = "Product";
828
+ InvoiceItemType[InvoiceItemType["Service"] = 2] = "Service";
829
+ })(InvoiceItemType || (InvoiceItemType = {}));
830
+ // Transaction timing enum - separates WHEN payment occurs from HOW
831
+ var TransactionTiming;
832
+ (function (TransactionTiming) {
833
+ TransactionTiming[TransactionTiming["Immediate"] = 0] = "Immediate";
834
+ TransactionTiming[TransactionTiming["Deferred"] = 1] = "Deferred"; // Payment occurs later - creates AR/AP
835
+ })(TransactionTiming || (TransactionTiming = {}));
805
836
  // Inventory receipt status tracking enum - mirrors backend
806
837
  var InventoryReceiptStatus;
807
838
  (function (InventoryReceiptStatus) {
@@ -1509,6 +1540,7 @@ class DataServiceLib {
1509
1540
  this.capInventoryDashboard = new CapItem;
1510
1541
  this.capInventoryStock = new CapItem; // Changed: Added new cap item for inventory stock view
1511
1542
  this.capProducts = new CapItem;
1543
+ this.capServiceItems = new CapItem; // Added: Capability for service items
1512
1544
  this.capBundleProducts = new CapItem; // Added: Capability for bundle products
1513
1545
  this.capInventoryItems = new CapItem;
1514
1546
  this.capPurchaseOrders = new CapItem;
@@ -2239,9 +2271,9 @@ class DataServiceLib {
2239
2271
  this.capInventory.name = "cap34";
2240
2272
  this.capInventory.display = "Inventory";
2241
2273
  this.capInventory.icon = "inventory";
2242
- this.capInventory.capSubItems = [this.capSales, this.capInventoryReceipts, this.capProducts, this.capBundleProducts, this.capInventoryStock, this.capInventoryTransactions, this.capInventoryItems,
2274
+ this.capInventory.capSubItems = [this.capSales, this.capInventoryReceipts, this.capProducts, this.capServiceItems, this.capBundleProducts, this.capInventoryStock, this.capInventoryTransactions, this.capInventoryItems,
2243
2275
  this.capPurchaseOrders, this.capSalesOrders, this.capRequisitions, this.capInventoryAdjustments,
2244
- this.capInventoryReturns, this.capRequisitionReturns, this.capProductionRecipes, this.capProductionOrders, this.capInventoryDashboard];
2276
+ this.capInventoryReturns, this.capRequisitionReturns, this.capProductionRecipes, this.capProductionOrders, this.capInventoryDashboard]; // Changed: Added capServiceItems to inventory menu
2245
2277
  this.capInventoryDashboard.name = "cap35";
2246
2278
  this.capInventoryDashboard.display = "Dashboard";
2247
2279
  this.capInventoryDashboard.link = "home/admin/inventory-dashboard";
@@ -2254,6 +2286,10 @@ class DataServiceLib {
2254
2286
  this.capProducts.display = "Products";
2255
2287
  this.capProducts.link = "home/admin/inventory-products";
2256
2288
  this.capProducts.icon = "category";
2289
+ this.capServiceItems.name = "cap59"; // Added: Service items capability
2290
+ this.capServiceItems.display = "Services";
2291
+ this.capServiceItems.link = "home/admin/inventory-service-items";
2292
+ this.capServiceItems.icon = "build";
2257
2293
  this.capBundleProducts.name = "cap57"; // Added: Bundle products capability
2258
2294
  this.capBundleProducts.display = "Bundle Products";
2259
2295
  this.capBundleProducts.link = "home/admin/bundle-products";
@@ -2681,7 +2717,7 @@ class AccountingService {
2681
2717
  loadAction: { url: 'accounts/id' },
2682
2718
  heroField: 'accountID',
2683
2719
  };
2684
- this.accountCreateButton = { name: 'create', display: 'Create', dialog: true, action: { url: 'accounts?action=create', method: 'post' } };
2720
+ this.accountCreateButton = { name: 'create', display: 'Create Account', dialog: true, action: { url: 'accounts?action=create', method: 'post' } };
2685
2721
  this.finAccounEditButton = { name: 'edit', dialog: true, action: { url: 'accounts?action=edit', method: 'post' } };
2686
2722
  this.accountBaseDetailsConfig = {
2687
2723
  formConfig: this.accountFormConfig,
@@ -2719,7 +2755,6 @@ class AccountingService {
2719
2755
  showFilter: false,
2720
2756
  minColumns: ['paymentDate', 'amount', 'methodName'],
2721
2757
  columns: [
2722
- { name: 'invoicePaymentID', type: 'number', alias: 'ID', hiddenCondition: () => true },
2723
2758
  { name: 'paymentDate', type: 'date', alias: 'Date' },
2724
2759
  { name: 'amount', type: 'money', alias: 'Amount' },
2725
2760
  { name: 'methodName', type: 'text', alias: 'Method' },
@@ -2730,9 +2765,21 @@ class AccountingService {
2730
2765
  action: { url: 'invoicepayments/migrate', method: 'post',
2731
2766
  successMessage: 'Migration completed successfully' },
2732
2767
  confirm: { message: 'This will migrate existing invoice payments from the legacy PaidAmount field to the new InvoicePayments table. Continue?' }
2733
- }
2768
+ },
2769
+ { name: 'edit', dialog: true, action: { url: 'invoicepayments?action=edit', method: 'post' }, visible: x => x.status != InvoiceStatus.Paid }, // Changed: Added edit button, hide on Paid invoices
2770
+ { name: 'delete', inDialog: true, icon: { name: 'delete', color: 'red' }, action: { url: 'invoicepayments?action=delete', method: 'post', successMessage: 'Payment deleted' }, confirm: { message: 'Delete this payment? The linked financial transaction will be reversed.' }, visible: x => x.status != InvoiceStatus.Paid } // Changed: Added delete button with reversal warning, hide on Paid invoices
2734
2771
  ],
2735
- loadAction: { url: 'invoicepayments/x/x' }, loadCriteria: 'invoice', loadIDField: 'invoiceID'
2772
+ loadAction: { url: 'invoicepayments/x/x' }, loadCriteria: 'invoice', loadIDField: 'invoiceID',
2773
+ formConfig: {
2774
+ title: 'Edit Payment',
2775
+ fields: [
2776
+ { name: 'paymentDate', type: 'date', alias: 'Payment Date', required: true },
2777
+ { name: 'method', type: 'select', alias: 'Payment Method', required: true, loadAction: { url: 'invoicepayments/list/methods' } },
2778
+ { name: 'amount', type: 'money', alias: 'Amount', required: true },
2779
+ { name: 'reference', type: 'text', alias: 'Reference' }
2780
+ ],
2781
+ loadAction: { url: 'invoicepayments/id' }
2782
+ }
2736
2783
  };
2737
2784
  // Invoice form configuration with customer and status fields
2738
2785
  this.invoiceFormConfig = {
@@ -2753,6 +2800,36 @@ class AccountingService {
2753
2800
  loadAction: { url: 'invoices/id' },
2754
2801
  heroField: 'invoiceID',
2755
2802
  };
2803
+ // Invoice items form configuration
2804
+ this.invoiceItemsFormConfig = {
2805
+ title: 'Invoice Item',
2806
+ fields: [
2807
+ { name: 'itemType', type: 'select', required: true, alias: 'Item Type', loadAction: { url: 'invoiceitems/list/item-types' }, defaultValue: 0 }, // Added: Item type selector
2808
+ { name: 'productID', type: 'select', alias: 'Product', loadAction: { url: 'products/list/invoice-items' },
2809
+ hiddenCondition: x => x.itemType !== 1, // Added: Product selector - visible only when itemType = Product (1)
2810
+ onSelectChange: (selectedId, formData, option) => {
2811
+ if (option) {
2812
+ formData.description = option.description || option.name;
2813
+ formData.unitPrice = option.unitPrice;
2814
+ }
2815
+ }
2816
+ },
2817
+ { name: 'serviceItemID', type: 'select', alias: 'Service', loadAction: { url: 'serviceitems/list/active' },
2818
+ hiddenCondition: x => x.itemType !== 2, // Added: Service selector - visible only when itemType = Service (2)
2819
+ onSelectChange: (selectedId, formData, option) => {
2820
+ if (option) {
2821
+ formData.description = option.description || option.name;
2822
+ formData.unitPrice = option.unitPrice;
2823
+ }
2824
+ }
2825
+ },
2826
+ { name: 'description', type: 'text', required: true, alias: 'Description', span: true }, // Changed: Description field (backend will populate for Product/Service)
2827
+ { name: 'quantity', type: 'number', required: true, defaultValue: 1 },
2828
+ { name: 'unitPrice', type: 'money', alias: 'Unit Price', required: true }, // Changed: Unit price field (backend will populate for Product/Service)
2829
+ ],
2830
+ loadAction: { url: 'invoiceitems/id/{invoiceItemID}' },
2831
+ heroField: 'invoiceItemID',
2832
+ };
2756
2833
  // Invoice items table for manual line item entry
2757
2834
  this.invoiceItemsTableConfig = {
2758
2835
  tabTitle: 'Invoice Items',
@@ -2770,20 +2847,17 @@ class AccountingService {
2770
2847
  { name: 'create', display: 'Add Item', dialog: true, action: { url: 'invoiceitems?action=create', method: 'post' },
2771
2848
  disabled: x => x.status != InvoiceStatus.Draft
2772
2849
  },
2773
- { name: 'edit', dialog: true, action: { url: 'invoiceitems?action=edit', method: 'post' }, disabled: x => x.status != InvoiceStatus.Draft },
2774
- { name: 'delete', inDialog: true, icon: { name: 'delete', color: 'red' }, action: { url: 'invoiceitems?action=delete', method: 'post', successMessage: 'Deleted' }, confirm: { message: 'Delete this item?' }, disabled: x => x.status != InvoiceStatus.Draft }
2850
+ { name: 'edit', dialog: true, action: { url: 'invoiceitems?action=edit', method: 'post' },
2851
+ disabled: x => x.status != InvoiceStatus.Draft
2852
+ },
2853
+ { name: 'delete', inDialog: true, icon: { name: 'delete', color: 'red' }, action: { url: 'invoiceitems?action=delete', method: 'post',
2854
+ successMessage: 'Deleted' },
2855
+ confirm: { message: 'Delete this item?' },
2856
+ disabled: x => x.status != InvoiceStatus.Draft
2857
+ },
2775
2858
  ],
2776
2859
  loadAction: { url: 'invoiceitems/x/x' }, loadCriteria: 'invoice', loadIDField: 'invoiceID',
2777
- formConfig: {
2778
- title: 'Invoice Item',
2779
- fields: [
2780
- { name: 'description', type: 'text', required: true, span: true },
2781
- { name: 'quantity', type: 'number', required: true, defaultValue: 1 },
2782
- { name: 'unitPrice', type: 'number', alias: 'Unit Price', required: true },
2783
- { name: 'amount', type: 'number', readonly: true, hideOnCreate: true }
2784
- ],
2785
- loadAction: { url: 'invoiceitems/id' }
2786
- }
2860
+ formConfig: this.invoiceItemsFormConfig // Changed: Reference extracted form config
2787
2861
  };
2788
2862
  // Invoice action buttons
2789
2863
  this.invoiceRecordPaymentButton = { name: 'record-payment', display: 'Record Payment', dialog: true, icon: { name: 'payment', color: 'blue' },
@@ -2791,7 +2865,8 @@ class AccountingService {
2791
2865
  visible: x => (x.status == InvoiceStatus.Submitted || x.status == InvoiceStatus.Paying) && x.outstandingAmount > 0 // Changed: Allow on both Submitted and Paying
2792
2866
  }; // Changed: Use detailsConfig to open payment form dialog
2793
2867
  this.invoiceDiscardButton = { name: 'discard', inDialog: true, display: 'Discard', icon: { name: 'close', color: 'red' },
2794
- action: { url: 'invoices?action=discard', method: 'post', successMessage: 'Discarded' }, confirm: { message: 'Invoice will be marked as cancelled?' },
2868
+ action: { url: 'invoices?action=discard', method: 'post', successMessage: 'Discarded' },
2869
+ confirm: { message: 'Invoice will be marked as cancelled?' },
2795
2870
  visible: x => x.status == InvoiceStatus.Draft || x.status == InvoiceStatus.Submitted // Changed: Only allow on Draft and Submitted
2796
2871
  };
2797
2872
  this.invoiceSubmitButton = { name: 'submit', inDialog: true, display: 'Submit', icon: { name: 'send', },
@@ -2800,8 +2875,9 @@ class AccountingService {
2800
2875
  visible: x => x.status == InvoiceStatus.Draft,
2801
2876
  disabled: x => x.totalAmount == 0
2802
2877
  };
2803
- this.invoiceEditButton = { name: 'edit', dialog: true, action: { url: 'invoices?action=edit', method: 'post', }, confirm: { message: 'Proceed ?' },
2804
- visible: x => x.status == InvoiceStatus.Draft || x.status == InvoiceStatus.Submitted // Changed: Disable on Paying and Paid
2878
+ this.invoiceEditButton = { name: 'edit', dialog: true, action: { url: 'invoices?action=edit', method: 'post', },
2879
+ confirm: { message: 'Proceed ?' },
2880
+ visible: x => x.status == InvoiceStatus.Draft // Changed: Draft only
2805
2881
  };
2806
2882
  this.invoiceDownloadButton = { name: 'pdf', display: 'Download PDF', inDialog: true, icon: { name: 'picture_as_pdf', color: 'red' } };
2807
2883
  // Invoice details dialog with items, payments history, and action buttons
@@ -2831,7 +2907,7 @@ class AccountingService {
2831
2907
  collapseButtons: true,
2832
2908
  greyOut: x => x.status == InvoiceStatus.Paid,
2833
2909
  columns: [
2834
- { name: 'invoiceNumber', type: 'text', alias: 'Invoice #' },
2910
+ { name: 'invoiceNumber', type: 'button', alias: 'Invoice #', detailsConfig: this.invoiceDetailsDialogConfig },
2835
2911
  { name: 'customerName', type: 'text', alias: 'Customer' },
2836
2912
  { name: 'invoiceDate', type: 'date' },
2837
2913
  { name: 'dueDate', type: 'date', alias: 'Due Date' },
@@ -2851,14 +2927,16 @@ class AccountingService {
2851
2927
  { name: 'outstandingAmount', type: 'money', alias: 'Outstanding' }
2852
2928
  ],
2853
2929
  buttons: [
2854
- { name: 'create', display: 'Create', dialog: true, action: { url: 'invoices?action=create', method: 'post' }, onSuccessButton: this.invoiceViewButton },
2855
- { name: 'view', dialog: true, detailsConfig: this.invoiceDetailsDialogConfig },
2856
- this.invoiceRecordPaymentButton,
2930
+ { name: 'create', display: 'Create', dialog: true, onSuccessButton: this.invoiceViewButton,
2931
+ action: { url: 'invoices?action=create', method: 'post' }
2932
+ },
2933
+ // { name: 'view', dialog: true, detailsConfig: this.invoiceDetailsDialogConfig },
2857
2934
  this.invoiceSubmitButton,
2935
+ this.invoiceRecordPaymentButton,
2858
2936
  // this.invoicePayButton,
2859
2937
  this.invoiceDiscardButton,
2860
- this.invoiceEditButton,
2861
- this.invoiceDownloadButton,
2938
+ // this.invoiceEditButton,
2939
+ // this.invoiceDownloadButton,
2862
2940
  ],
2863
2941
  loadAction: { url: 'invoices/all/x' },
2864
2942
  formConfig: this.invoiceFormConfig,
@@ -2881,9 +2959,10 @@ class AccountingService {
2881
2959
  // Added: Base aging table configuration with common properties
2882
2960
  this.agingBaseTableConfig = {
2883
2961
  showFilter: true,
2962
+ flatButtons: true,
2884
2963
  minColumns: ['invoiceNumber', 'customerName', 'outstandingAmount'],
2885
2964
  columns: [
2886
- { name: 'invoiceNumber', type: 'text', alias: 'Invoice #' },
2965
+ { name: 'invoiceNumber', type: 'text', alias: 'Invoice #', detailsConfig: this.invoiceDetailsDialogConfig },
2887
2966
  { name: 'customerName', type: 'text', alias: 'Customer' },
2888
2967
  { name: 'invoiceDate', type: 'date', alias: 'Invoice Date' },
2889
2968
  { name: 'dueDate', type: 'date', alias: 'Due Date' },
@@ -2892,7 +2971,10 @@ class AccountingService {
2892
2971
  { name: 'paidAmount', type: 'money', alias: 'Paid' },
2893
2972
  { name: 'outstandingAmount', type: 'money', alias: 'Outstanding' }
2894
2973
  ],
2895
- buttons: [{ name: 'view', dialog: true, detailsConfig: this.invoiceDetailsDialogConfig }]
2974
+ buttons: [
2975
+ { name: 'view', dialog: true, detailsConfig: this.invoiceDetailsDialogConfig },
2976
+ this.invoiceRecordPaymentButton,
2977
+ ]
2896
2978
  };
2897
2979
  //--------------------------Customer Invoices-------------------------
2898
2980
  // Customer invoice form without customerID field
@@ -3072,15 +3154,15 @@ class AccountingService {
3072
3154
  { name: 'red', condition: x => x.status == 1 },
3073
3155
  ],
3074
3156
  },
3075
- { name: 'amount', type: 'money',
3076
- color: { name: 'red', condition: x => x.reducesBalance && !x.isAggregate },
3077
- },
3078
3157
  { name: 'typeName', type: 'text', alias: 'Type' },
3079
3158
  { name: 'debitAccountName', type: 'text', alias: 'Debit Account' },
3080
3159
  { name: 'creditAccountName', type: 'text', alias: 'Credit Account' },
3160
+ { name: 'amount', type: 'money',
3161
+ color: { name: 'red', condition: x => x.reducesBalance && !x.isAggregate },
3162
+ },
3081
3163
  ],
3082
3164
  buttons: [
3083
- { name: 'create', display: 'Create', dialog: true, action: { url: 'transactions/dto?action=create', method: 'post' } },
3165
+ { name: 'create', display: 'Create Transaction', dialog: true, action: { url: 'transactions/dto?action=create', method: 'post' } },
3084
3166
  { name: 'view', dialog: true, detailsConfig: this.transactionDetailsConfig },
3085
3167
  this.transactionEditButton,
3086
3168
  this.transactionVoidButton,
@@ -3091,12 +3173,91 @@ class AccountingService {
3091
3173
  this.accountTransactionsTableConfig = {
3092
3174
  ...this.transactionsTableConfig,
3093
3175
  causeFormRefresh: true,
3176
+ minColumns: ['date', 'description', 'amount', 'runningBalance'],
3094
3177
  tabTitle: 'Account Transactions',
3095
3178
  loadAction: { url: 'transactions/account/x' }, loadCriteria: 'account', loadIDField: 'accountID',
3096
3179
  columns: [
3097
3180
  ...this.transactionsTableConfig.columns,
3098
- { name: 'runningBalance', type: 'money', alias: 'Balance' } // Added running balance column
3181
+ { name: 'runningBalance', type: 'money', alias: 'Balance' }
3182
+ ],
3183
+ };
3184
+ //--------------------------Transaction Templates-------------------------
3185
+ // Template form configuration (accountID hidden - auto-populated from context)
3186
+ this.transactionTemplateFormConfig = {
3187
+ security: { allow: [this.dataService.capTransactions] },
3188
+ title: 'Transaction Template',
3189
+ includeAudit: true,
3190
+ fields: [
3191
+ { name: 'name', type: 'text', required: true, span: true },
3192
+ { name: 'accountID', type: 'number', hidden: true }, // Auto-populated from Account Details context
3193
+ { name: 'transactionTypeID', type: 'select', alias: 'Transaction Type', required: true, detailsConfig: this.transactionTypeDetailsConfig, loadAction: { url: 'transactionTypes/list/x' } },
3194
+ { name: 'blank', type: 'blank' },
3195
+ { name: 'debitAccountID', type: 'select', alias: 'Default Debit Account', nullable: true, masterField: 'transactionTypeID', masterValueField: 'debitAccountType', masterDefaultValueField: 'defaultDebitAccountID', masterOptionValue: 'type', loadAction: { url: 'accounts/list/x' }, detailsConfig: this.accountBaseDetailsConfig },
3196
+ { name: 'creditAccountID', type: 'select', alias: 'Default Credit Account', nullable: true, masterField: 'transactionTypeID', masterValueField: 'creditAccountType', masterDefaultValueField: 'defaultCreditAccountID', masterOptionValue: 'type', loadAction: { url: 'accounts/list/x' }, detailsConfig: this.accountBaseDetailsConfig },
3197
+ { name: 'description', type: 'text', alias: 'Default Description', span: true },
3198
+ { name: 'defaultAmount', type: 'money', alias: 'Default Amount', span: true },
3199
+ ],
3200
+ loadAction: { url: 'transactiontemplates/id' },
3201
+ heroField: 'transactionTemplateID',
3202
+ };
3203
+ // Process form configuration (convert template to transaction)
3204
+ this.transactionTemplateProcessFormConfig = {
3205
+ title: 'Create Transaction from Template',
3206
+ fixedTitle: true,
3207
+ fields: [
3208
+ { name: 'transactionTemplateID', type: 'number', hidden: true },
3209
+ { name: 'name', type: 'label', alias: 'Template', readonly: true, span: true },
3210
+ { name: 'transactionTypeID', type: 'select', alias: 'Transaction Type', required: true, detailsConfig: this.transactionTypeDetailsConfig, loadAction: { url: 'transactionTypes/list/x' } },
3211
+ { name: 'date', type: 'date', required: true, defaultValue: 'now' },
3212
+ { name: 'debitAccountID', type: 'select', alias: 'Debit Account', required: true, masterField: 'transactionTypeID', masterValueField: 'debitAccountType', masterOptionValue: 'type', loadAction: { url: 'accounts/list/x' }, detailsConfig: this.accountBaseDetailsConfig },
3213
+ { name: 'creditAccountID', type: 'select', alias: 'Credit Account', required: true, masterField: 'transactionTypeID', masterValueField: 'creditAccountType', masterOptionValue: 'type', loadAction: { url: 'accounts/list/x' }, detailsConfig: this.accountBaseDetailsConfig },
3214
+ { name: 'description', type: 'text', required: true, span: true },
3215
+ { name: 'amount', type: 'money', required: true, span: true },
3216
+ ],
3217
+ };
3218
+ // Template action buttons
3219
+ this.transactionTemplateCreateButton = { name: 'create', display: 'Create Template', dialog: true, action: { url: 'transactiontemplates?action=create', method: 'post' } };
3220
+ this.transactionTemplateEditButton = { name: 'edit', dialog: true, action: { url: 'transactiontemplates?action=edit', method: 'post' } };
3221
+ // Template details config (for view/edit)
3222
+ this.transactionTemplateDetailsConfig = {
3223
+ formConfig: this.transactionTemplateFormConfig,
3224
+ heroField: 'transactionTemplateID',
3225
+ buttons: [this.transactionTemplateCreateButton, this.transactionTemplateEditButton]
3226
+ };
3227
+ // Process details config (for processing template)
3228
+ this.transactionTemplateProcessDetailsConfig = {
3229
+ formConfig: this.transactionTemplateProcessFormConfig,
3230
+ heroField: 'transactionTemplateID',
3231
+ mode: 'edit',
3232
+ buttons: [
3233
+ { name: 'process', display: 'Create Transaction', icon: { name: 'play_arrow', color: 'green' }, inDialog: true,
3234
+ action: { url: 'transactiontemplates/dto?action=process', method: 'post',
3235
+ successMessage: 'Transaction created from template' }
3236
+ }
3237
+ ]
3238
+ };
3239
+ // Templates table configuration (3rd tab in Account Details)
3240
+ this.transactionTemplatesTableConfig = {
3241
+ tabTitle: 'Transaction Templates',
3242
+ showFilter: true,
3243
+ minColumns: ['name', 'transactionTypeName', 'defaultAmountDisplay'],
3244
+ flatButtons: true,
3245
+ columns: [
3246
+ { name: 'name', type: 'button', detailsConfig: this.transactionTemplateProcessDetailsConfig },
3247
+ { name: 'transactionTypeName', type: 'text', alias: 'Type' },
3248
+ { name: 'debitAccountName', type: 'text', alias: 'Debit Account' },
3249
+ { name: 'creditAccountName', type: 'text', alias: 'Credit Account' },
3250
+ { name: 'defaultAmountDisplay', type: 'text', alias: 'Default Amount' },
3099
3251
  ],
3252
+ buttons: [
3253
+ this.transactionTemplateCreateButton,
3254
+ { name: 'process', display: 'Create Transaction', icon: { name: 'play_arrow', color: 'green' }, dialog: true, detailsConfig: this.transactionTemplateProcessDetailsConfig },
3255
+ { name: 'view', dialog: true, detailsConfig: this.transactionTemplateDetailsConfig },
3256
+ { name: 'edit', dialog: true, action: { url: 'transactiontemplates?action=edit', method: 'post' }, detailsConfig: this.transactionTemplateDetailsConfig }, // Changed: Inline edit button with both action and detailsConfig
3257
+ { name: 'delete', inDialog: true, icon: { name: 'delete', color: 'red' }, action: { url: 'transactiontemplates?action=delete', method: 'post', successMessage: 'Template deleted' }, confirm: { message: 'Delete this template?' } },
3258
+ ],
3259
+ loadAction: { url: 'transactiontemplates/account/x' }, loadCriteria: 'account', loadIDField: 'accountID',
3260
+ formConfig: this.transactionTemplateFormConfig
3100
3261
  };
3101
3262
  //--------------------------Accounts-------------------------
3102
3263
  this.accountDetailsConfig = {
@@ -3109,6 +3270,7 @@ class AccountingService {
3109
3270
  buttons: [],
3110
3271
  loadAction: { url: 'transactionTypes/account/x' }, loadCriteria: 'account', loadIDField: 'accountID'
3111
3272
  },
3273
+ { ...this.transactionTemplatesTableConfig }, // Changed: Added 3rd tab for transaction templates
3112
3274
  ]
3113
3275
  };
3114
3276
  this.accountsTableConfig = {
@@ -3727,6 +3889,47 @@ class InventoryService {
3727
3889
  loadAction: { url: 'products/bundles/x' },
3728
3890
  formConfig: this.bundleProductFormConfig
3729
3891
  };
3892
+ //--------------------------ServiceItems-------------------------
3893
+ // ServiceItem form configuration - follows Product pattern
3894
+ this.serviceItemFormConfig = {
3895
+ title: 'Service',
3896
+ includeAudit: true,
3897
+ fields: [
3898
+ { name: 'name', type: 'text', required: true, alias: 'Service Name' },
3899
+ { name: 'description', type: 'text', alias: 'Description' },
3900
+ { name: 'unitPrice', type: 'money', required: true, alias: 'Unit Price' },
3901
+ { name: 'isActive', type: 'checkbox', alias: 'Active', defaultValue: true }
3902
+ ],
3903
+ loadAction: { url: 'serviceitems/id' },
3904
+ heroField: 'serviceItemID'
3905
+ };
3906
+ this.serviceItemCreateButton = { name: 'create', display: 'Create Service', dialog: true, action: { url: 'serviceitems?action=create', method: 'post' } };
3907
+ this.serviceItemEditButton = { name: 'edit', dialog: true, action: { url: 'serviceitems?action=edit', method: 'post' } };
3908
+ this.serviceItemDeleteButton = { name: 'delete', dialog: true, action: { url: 'serviceitems?action=delete', method: 'post' } };
3909
+ this.serviceItemDetailsConfig = {
3910
+ formConfig: this.serviceItemFormConfig,
3911
+ heroField: 'serviceItemID',
3912
+ buttons: [this.serviceItemCreateButton, this.serviceItemEditButton, this.serviceItemDeleteButton]
3913
+ };
3914
+ this.serviceItemsTableConfig = {
3915
+ showFilter: true,
3916
+ flatButtons: true,
3917
+ minColumns: ['name', 'unitPrice', 'statusName'],
3918
+ columns: [
3919
+ { name: 'name', type: 'text', alias: 'Service Name' },
3920
+ { name: 'description', type: 'text', alias: 'Description' },
3921
+ { name: 'unitPrice', type: 'money', alias: 'Unit Price' },
3922
+ { name: 'statusName', type: 'text', alias: 'Status' }
3923
+ ],
3924
+ buttons: [
3925
+ this.serviceItemCreateButton,
3926
+ { name: 'view', dialog: true, detailsConfig: this.serviceItemDetailsConfig },
3927
+ this.serviceItemEditButton,
3928
+ this.serviceItemDeleteButton
3929
+ ],
3930
+ loadAction: { url: 'serviceitems/all/x' },
3931
+ formConfig: this.serviceItemFormConfig
3932
+ };
3730
3933
  //--------------------------Requisitions-------------------------
3731
3934
  this.requisitionItemFormConfig = {
3732
3935
  title: 'Requisition Item',
@@ -3818,10 +4021,10 @@ class InventoryService {
3818
4021
  };
3819
4022
  //--------------------------Purchase Orders-------------------------
3820
4023
  // Payment type options (used across forms)
4024
+ // Changed: Removed Credit option - timing now determines credit vs cash purchase
3821
4025
  this.paymentTypeOptions = [
3822
4026
  { value: 0, name: 'Cash' },
3823
- { value: 1, name: 'Bank' },
3824
- { value: 2, name: 'Credit' }
4027
+ { value: 1, name: 'Bank' }
3825
4028
  ];
3826
4029
  // PO Item form configuration
3827
4030
  this.purchaseOrderItemFormConfig = {
@@ -4005,9 +4208,15 @@ class InventoryService {
4005
4208
  { name: 'supplierID', type: 'select', required: true, alias: 'Supplier', section: 'receiptInfo',
4006
4209
  loadAction: { url: 'suppliers/list/x' }, detailsConfig: this.dataService.supplierDetailsConfig, infoMessage: 'Supplier providing the goods'
4007
4210
  },
4008
- { name: 'paymentType', type: 'select', required: true, alias: 'Payment Type', section: 'receiptInfo',
4009
- options: this.paymentTypeOptions, defaultFirstValue: true, infoMessage: 'Payment method used for this purchase' },
4010
- { name: 'supplierInvoiceNumber', type: 'text', alias: 'Supplier Invoice #', section: 'receiptInfo', infoMessage: 'Invoice number from supplier' },
4211
+ { name: 'timing', type: 'select', required: true, alias: 'Transaction Type', section: 'receiptInfo',
4212
+ loadAction: { url: 'inventoryreceipts/list/timing' }, defaultFirstValue: true,
4213
+ infoMessage: 'Cash Purchase (pay now) or Credit Purchase (pay later)'
4214
+ },
4215
+ { name: 'paymentType', type: 'select', required: true, alias: 'Payment Method', section: 'receiptInfo', defaultFirstValue: true,
4216
+ options: this.paymentTypeOptions, infoMessage: 'Payment method used for this purchase',
4217
+ hiddenCondition: x => x.timing === 1,
4218
+ requiredCondition: x => x.timing === 0
4219
+ },
4011
4220
  { name: 'receiptNumber', type: 'text', alias: 'Receipt Number', readonly: true, hideOnCreate: true, section: 'receiptInfo', infoMessage: 'Auto-generated unique receipt identifier' },
4012
4221
  { name: 'poNumber', type: 'text', alias: 'PO Number', hideOnCreate: true, section: 'receiptInfo', infoMessage: 'Linked purchase order if receiving against a PO',
4013
4222
  hiddenCondition: (row) => !row.purchaseOrderID,
@@ -4034,12 +4243,16 @@ class InventoryService {
4034
4243
  { name: 'totals', type: 'section', alias: 'Totals', hideOnCreate: true, collapsed: true },
4035
4244
  { name: 'totalAmount', type: 'money', alias: 'Total Amount', readonly: true, section: 'totals', infoMessage: 'Total value of all items' },
4036
4245
  { name: 'additionalInfo', type: 'section', alias: 'Additional Information', collapsed: true },
4037
- { name: 'notes', type: 'text', alias: 'Notes', span: true, section: 'additionalInfo', infoMessage: 'Additional notes about this receipt' }
4246
+ { name: 'notes', type: 'text', alias: 'Notes', span: true, section: 'additionalInfo', infoMessage: 'Additional notes about this receipt' },
4247
+ { name: 'supplierInvoiceNumber', type: 'text', alias: 'Supplier Invoice #', section: 'additionalInfo', infoMessage: 'Invoice number from supplier' },
4038
4248
  ],
4039
4249
  loadAction: { url: 'inventoryreceipts/id' },
4040
4250
  heroField: 'inventoryReceiptID'
4041
4251
  };
4042
- this.inventoryReceiptEditButton = { name: 'edit', dialog: true, action: { url: 'inventoryreceipts?action=edit', method: 'post' }, visible: (x) => x.status !== InventoryReceiptStatus.Completed }; // Changed: Use status enum instead of statusName
4252
+ this.inventoryReceiptEditButton = { name: 'edit', dialog: true,
4253
+ action: { url: 'inventoryreceipts?action=edit', method: 'post' },
4254
+ visible: (x) => x.status !== InventoryReceiptStatus.Completed
4255
+ };
4043
4256
  this.inventoryReceiptCompleteButton = { name: 'complete', display: 'Complete Receipt', inDialog: true, // Changed: Updated display text and action name
4044
4257
  icon: { name: 'check_circle', color: 'green' },
4045
4258
  action: { url: 'inventoryreceipts?action=complete', method: 'post', successMessage: 'Receipt Completed' }, // Changed: Updated endpoint and message
@@ -4072,7 +4285,7 @@ class InventoryService {
4072
4285
  { name: 'receiptDate', type: 'date', alias: 'Date' },
4073
4286
  { name: 'supplierName', type: 'text', alias: 'Supplier' },
4074
4287
  { name: 'productsDisplay', type: 'text', alias: 'Products' }, // Added: Display product names or count
4075
- { name: 'paymentTypeName', type: 'text', alias: 'Payment' },
4288
+ { name: 'timingName', type: 'text', alias: 'Payment' },
4076
4289
  { name: 'status', type: 'icon', alias: 'Status', detailsConfig: this.inventoryReceiptDetailsConfig, // Changed: Use status instead of statusName
4077
4290
  icons: [
4078
4291
  { name: 'article', color: '#9E9E9E', condition: (x) => x.status === InventoryReceiptStatus.Draft, tip: 'Draft' }, // Changed: Use status enum
@@ -4373,7 +4586,23 @@ class InventoryService {
4373
4586
  fields: [
4374
4587
  { name: 'saleInfo', type: 'section', alias: 'Sale Information' }, // Changed: Hide when single product mode or when editing existing sale
4375
4588
  { name: 'saleDate', type: 'date', required: true, alias: 'Sale Date', section: 'saleInfo', infoMessage: 'Date when the sale was completed' },
4376
- { name: 'customerID', type: 'select', required: true, alias: 'Customer', section: 'saleInfo', loadAction: { url: 'customers/list/x' }, detailsConfig: this.dataService.customerDetailsConfig },
4589
+ { name: 'customerID', type: 'select', required: true, alias: 'Customer', section: 'saleInfo',
4590
+ loadAction: { url: 'customers/list/x' }, detailsConfig: this.dataService.customerDetailsConfig
4591
+ },
4592
+ { name: 'timing', type: 'select', required: true, alias: 'Transaction Type', section: 'saleInfo',
4593
+ loadAction: { url: 'sales/list/timing' }, defaultFirstValue: true, infoMessage: 'Cash Sale (pay now) or Credit Sale (pay later)'
4594
+ },
4595
+ { name: 'paymentMethod', type: 'select', required: true, alias: 'Payment Method', section: 'saleInfo', defaultFirstValue: true, infoMessage: 'Method of payment used for this sale',
4596
+ requiredCondition: x => x.timing === 0,
4597
+ hiddenCondition: x => x.timing === 1,
4598
+ options: [
4599
+ { name: 'Cash', value: 0 },
4600
+ { name: 'Bank Transfer', value: 1 },
4601
+ { name: 'Mobile Money', value: 2 },
4602
+ { name: 'Card', value: 3 },
4603
+ { name: 'Complementary', value: 5 }
4604
+ ],
4605
+ },
4377
4606
  { name: 'multipleProducts', type: 'checkbox', alias: 'Multiple Products', defaultValue: false, hideOnExists: true, infoMessage: 'Check this box if you want to add multiple products to this sale' }, // Changed: Added checkbox for multiple products mode
4378
4607
  { name: 'saleType', type: 'select', required: true, alias: 'Sale Type', section: 'saleInfo', options: [{ name: 'Quick Sale', value: 0 }, { name: 'From Order', value: 1 }], defaultFirstValue: true, hideOnCreate: true, infoMessage: 'Select if this is a quick sale or created from an order' },
4379
4608
  { name: 'quickSaleItem', type: 'section', alias: 'Quick Sale Item', hiddenCondition: x => x.multipleProducts === true || x.saleID }, // Changed: Section for single product entry
@@ -4386,17 +4615,16 @@ class InventoryService {
4386
4615
  },
4387
4616
  { name: 'quantity', type: 'number', required: true, alias: 'Quantity', section: 'quickSaleItem', defaultValue: 1, hiddenCondition: x => x.multipleProducts === true, infoMessage: 'Quantity of the product' }, // Changed: Quantity field for quick sale
4388
4617
  { name: 'unitPrice', type: 'money', required: true, alias: 'Unit Price', section: 'quickSaleItem', hiddenCondition: x => x.multipleProducts === true, infoMessage: 'Price per unit' }, // Changed: Unit price field for quick sale
4389
- { name: 'paymentInfo', type: 'section', alias: 'Payment Information', collapsedCondition: x => x.saleID },
4390
- { name: 'paymentMethod', type: 'select', required: true, alias: 'Payment Method', section: 'paymentInfo', options: [{ name: 'Cash', value: 0 }, { name: 'Bank Transfer', value: 1 }, { name: 'Mobile Money', value: 2 }, { name: 'Card', value: 3 }, { name: 'Credit', value: 4 }, { name: 'Complementary', value: 5 }], defaultFirstValue: true, infoMessage: 'Method of payment used for this sale' },
4391
- { name: 'paymentReference', type: 'text', alias: 'Payment Reference', section: 'paymentInfo', infoMessage: 'Transaction reference number or payment receipt' },
4392
- { name: 'paymentStatus', type: 'select', alias: 'Payment Status', readonly: true, section: 'paymentInfo', loadAction: { url: 'sales/list/payment-status' }, hideOnCreate: true, infoMessage: 'Current payment status of the sale' },
4393
4618
  { name: 'totals', type: 'section', alias: 'Totals', collapsed: true, hideOnCreate: true },
4394
4619
  { name: 'subTotal', type: 'money', alias: 'Sub Total', readonly: true, section: 'totals', infoMessage: 'Total before tax and discounts' },
4395
4620
  { name: 'taxAmount', type: 'money', alias: 'Tax Amount', section: 'totals', infoMessage: 'Tax amount applied to the sale' },
4396
4621
  { name: 'discount', type: 'money', alias: 'Discount', section: 'totals', infoMessage: 'Discount amount applied to the sale' },
4397
4622
  { name: 'totalAmount', type: 'money', alias: 'Total Amount', readonly: true, section: 'totals', infoMessage: 'Final amount after tax and discounts' },
4398
4623
  { name: 'additionalInfo', type: 'section', alias: 'Additional Information', collapsed: true },
4399
- { name: 'notes', type: 'text', alias: 'Notes', span: true, section: 'additionalInfo', infoMessage: 'Additional notes or comments about this sale' }
4624
+ { name: 'notes', type: 'text', alias: 'Notes', span: true, section: 'additionalInfo', infoMessage: 'Additional notes or comments about this sale' },
4625
+ { name: 'paymentReference', type: 'text', alias: 'Payment Reference', section: 'additionalInfo', infoMessage: 'Transaction reference number or payment receipt',
4626
+ hiddenCondition: x => x.timing === 1
4627
+ },
4400
4628
  ],
4401
4629
  loadAction: { url: 'sales/id' },
4402
4630
  heroField: 'saleID'
@@ -4410,26 +4638,72 @@ class InventoryService {
4410
4638
  { name: 'Partial', value: 2, icon: 'payment' }
4411
4639
  ]
4412
4640
  };
4641
+ // Added: Sale payment form configuration (matches invoice payment form)
4642
+ this.salePaymentFormConfig = {
4643
+ security: { allow: [this.dataService.capSales] },
4644
+ title: 'Record Payment',
4645
+ fixedTitle: true,
4646
+ fields: [
4647
+ { name: 'saleID', type: 'number', hidden: true },
4648
+ { name: 'paymentDate', type: 'date', alias: 'Payment Date', required: true },
4649
+ { name: 'method', type: 'select', alias: 'Payment Method', required: true, defaultValue: 1, loadAction: { url: 'invoicepayments/list/methods' } }, // Changed: Default to 1 (Bank)
4650
+ { name: 'totalAmount', type: 'label', alias: 'Total Amount', readonly: true },
4651
+ { name: 'amount', type: 'money', alias: 'Amount', required: true, span: true },
4652
+ { name: 'paymentReferenceTemp', type: 'text', alias: 'Reference', span: true }
4653
+ ],
4654
+ };
4655
+ // Added: Sale payment button (matches invoice payment button pattern)
4656
+ this.salePaymentCreateButton = {
4657
+ name: 'edit', // Changed: Use standard 'edit' button name for framework recognition
4658
+ action: { url: 'sales?action=record-payment', method: 'post', successMessage: 'Payment recorded successfully' }
4659
+ };
4660
+ // Added: Sale payment details dialog config
4661
+ this.salePaymentDetailsConfig = {
4662
+ formConfig: this.salePaymentFormConfig,
4663
+ heroField: 'saleID',
4664
+ mode: 'edit',
4665
+ buttons: [
4666
+ this.salePaymentCreateButton // Changed: Use separate button definition
4667
+ ]
4668
+ };
4669
+ // Added: Sale payment record button
4670
+ this.saleRecordPaymentButton = {
4671
+ name: 'record-payment', display: 'Record Payment', inDialog: true, dialog: true, icon: { name: 'payment', color: 'blue' },
4672
+ detailsConfig: this.salePaymentDetailsConfig,
4673
+ visible: x => x.timing === 1 && x.paymentStatus !== 1
4674
+ };
4675
+ // Added: Sale payments table for payment history (from linked invoice)
4676
+ this.salePaymentsTableConfig = {
4677
+ tabTitle: 'Payment History',
4678
+ showFilter: false,
4679
+ minColumns: ['paymentDate', 'amount', 'methodName'],
4680
+ columns: [
4681
+ { name: 'paymentDate', type: 'date', alias: 'Date' },
4682
+ { name: 'amount', type: 'money', alias: 'Amount' },
4683
+ { name: 'methodName', type: 'text', alias: 'Method' },
4684
+ { name: 'reference', type: 'text', alias: 'Reference' }
4685
+ ],
4686
+ loadAction: { url: 'invoicepayments/x/x' }, loadCriteria: 'invoice', loadIDField: 'invoiceID',
4687
+ hideTabCondition: (x) => x.timing !== 1 // Changed: Only show for deferred payments (credit sales)
4688
+ };
4413
4689
  // Details dialog config for sale with complete button (only visible if not yet paid)
4414
4690
  this.saleDetailsConfig = {
4415
4691
  formConfig: this.saleFormConfig,
4416
- tableConfigs: [this.saleItemsTableConfig],
4692
+ tableConfigs: [this.saleItemsTableConfig, this.salePaymentsTableConfig], // Added: Include payment history table
4417
4693
  heroField: 'saleID',
4418
4694
  stepConfig: this.saleStepConfig, // Changed: Added step config for payment status tracking
4419
4695
  buttons: [
4420
4696
  { name: 'complete', display: 'Complete Sale', color: 'primary', inDialog: true,
4421
4697
  action: { url: 'sales?action=complete', method: 'post' },
4422
4698
  confirm: { message: 'Complete this sale? This will reduce inventory and create accounting entries.' },
4423
- visible: x => x.paymentStatus !== 1
4424
- }
4699
+ visible: x => x.timing === 0 && x.paymentStatus !== 1 // Changed: Only for immediate sales that aren't paid (deferred sales are auto-completed)
4700
+ },
4701
+ this.saleRecordPaymentButton // Added: Record payment button for credit sales
4425
4702
  ]
4426
4703
  };
4427
4704
  this.saleViewButton = { name: 'view', dialog: true, detailsConfig: this.saleDetailsConfig };
4428
4705
  this.saleCreateButton = {
4429
- name: 'create',
4430
- display: 'Quick Sale',
4431
- dialog: true,
4432
- action: { url: 'sales?action=create', method: 'post' },
4706
+ name: 'create', display: 'Quick Sale', dialog: true, action: { url: 'sales?action=create', method: 'post' },
4433
4707
  };
4434
4708
  this.saleVoidButton = { name: 'void', display: 'Void', icon: { name: 'undo', color: 'red' },
4435
4709
  confirm: { message: 'Caution: Voiding this sale will reverse all inventory transactions and void accounting entries. This action cannot be undone.' },
@@ -4444,7 +4718,7 @@ class InventoryService {
4444
4718
  { name: 'saleDate', type: 'date', alias: 'Date' },
4445
4719
  { name: 'displayCustomerName', type: 'text', alias: 'Customer' },
4446
4720
  { name: 'productsDisplay', type: 'text', alias: 'Products' }, // Added: Display product names or count
4447
- { name: 'paymentMethodName', type: 'text', alias: 'Payment' },
4721
+ { name: 'timingName', type: 'text', alias: 'Payment' },
4448
4722
  { name: 'paymentStatus', type: 'icon', alias: 'Status', detailsConfig: this.inventoryReceiptDetailsConfig, // Changed: Use status instead of statusName
4449
4723
  icons: [
4450
4724
  { name: 'article', color: '#9E9E9E', condition: (x) => x.paymentStatus === 0, tip: 'Unpaid' }, // Changed: Use status enum
@@ -4455,7 +4729,10 @@ class InventoryService {
4455
4729
  { name: 'totalAmount', type: 'money', alias: 'Total' }
4456
4730
  ],
4457
4731
  buttons: [
4458
- this.saleViewButton, this.saleCreateButton, this.saleVoidButton
4732
+ this.saleViewButton,
4733
+ this.saleRecordPaymentButton,
4734
+ this.saleCreateButton,
4735
+ this.saleVoidButton
4459
4736
  ],
4460
4737
  loadAction: { url: 'sales/all/x' },
4461
4738
  formConfig: this.saleFormConfig
@@ -6000,6 +6277,7 @@ class SelectCommonComponent {
6000
6277
  this.subscription = this.field.optionsSubject.subscribe(newOptions => {
6001
6278
  if (newOptions) {
6002
6279
  this.options = newOptions;
6280
+ this.setDefaultValue(); // Changed: Apply default after options update via subscription
6003
6281
  this.updateSelectedOptionHint();
6004
6282
  }
6005
6283
  });
@@ -6016,15 +6294,54 @@ class SelectCommonComponent {
6016
6294
  this.updateSelectedOptionHint();
6017
6295
  }
6018
6296
  setDefaultValue() {
6019
- if (this.defaultFirstValue && this.options && this.options.length > 0 &&
6020
- (this.value == null || this.value == undefined || (typeof (this.value) == 'string' && this.value == '') || (typeof (this.value) == 'number' && this.value == 0))) {
6297
+ // Guard: Need options to set any default
6298
+ if (!this.options || this.options.length === 0) {
6299
+ return;
6300
+ }
6301
+ // Guard: Check if value is already meaningfully set
6302
+ const hasValue = this.value != null &&
6303
+ this.value !== undefined &&
6304
+ !(typeof this.value === 'string' && this.value === '') &&
6305
+ !(typeof this.value === 'number' && this.value === 0 && !this.nullable);
6306
+ if (hasValue) {
6307
+ // Value already set - verify it exists in options
6308
+ const valueExists = this.options.some(opt => this.compareValues(opt[this.optionValue], this.value));
6309
+ if (valueExists) {
6310
+ return; // Valid value already selected
6311
+ }
6312
+ }
6313
+ // Priority 1: Use field.defaultValue if configured
6314
+ const defaultVal = this.field?.defaultValue;
6315
+ if (defaultVal != null && defaultVal !== undefined) {
6316
+ const matchingOption = this.options.find(opt => this.compareValues(opt[this.optionValue], defaultVal));
6317
+ if (matchingOption) {
6318
+ this.value = matchingOption[this.optionValue];
6319
+ this.changed();
6320
+ return;
6321
+ }
6322
+ }
6323
+ // Priority 2: Use defaultFirstValue if configured
6324
+ if (this.defaultFirstValue) {
6021
6325
  this.value = this.options[0][this.optionValue];
6022
6326
  this.changed();
6023
6327
  }
6024
6328
  }
6329
+ // Changed: Added helper method for type-coerced value comparison (handles number vs string)
6330
+ compareValues(optionValue, targetValue) {
6331
+ if (optionValue === targetValue)
6332
+ return true;
6333
+ if (optionValue == null || targetValue == null)
6334
+ return false;
6335
+ return String(optionValue) === String(targetValue);
6336
+ }
6025
6337
  changed() {
6026
6338
  this.valueChange.emit(this.value);
6027
6339
  this.updateSelectedOptionHint();
6340
+ // Added: Call onSelectChange callback if provided
6341
+ if (this.field?.onSelectChange && this.data) {
6342
+ const selectedOption = this.options?.find(opt => opt[this.optionValue] === this.value);
6343
+ this.field.onSelectChange(this.value, this.data, selectedOption);
6344
+ }
6028
6345
  }
6029
6346
  updateSelectedOptionHint() {
6030
6347
  if (!this.options || this.options.length === 0 || this.value == null || this.value == undefined) {
@@ -6063,7 +6380,18 @@ class SelectCommonComponent {
6063
6380
  this.getData(refreshAction);
6064
6381
  }
6065
6382
  transformLoadUrl(action) {
6066
- if (!action?.url || !this.loadIDField || !this.data)
6383
+ if (!action?.url || !this.data)
6384
+ return action;
6385
+ // Changed: Check if URL contains {propertyName} placeholders first
6386
+ if (action.url.includes('{')) {
6387
+ const transformedUrl = action.url.replace(/\{(\w+)\}/g, (match, propName) => {
6388
+ const value = this.data?.[propName];
6389
+ return value !== undefined && value !== null ? String(value) : match;
6390
+ });
6391
+ return { ...action, url: transformedUrl };
6392
+ }
6393
+ // Changed: Fallback to existing loadIDField pattern for backward compatibility
6394
+ if (!this.loadIDField)
6067
6395
  return action;
6068
6396
  const idValue = this.data[this.loadIDField];
6069
6397
  if (!idValue)
@@ -6981,7 +7309,18 @@ class ViewerComponent {
6981
7309
  // fileList: string[];
6982
7310
  loadData() {
6983
7311
  console.log("Calling files");
6984
- let url = this.fileAction.url.replace("/x", '/' + this.folderName);
7312
+ let url = this.fileAction.url;
7313
+ // Changed: Support generic {propertyName} placeholders first
7314
+ if (url.includes('{')) {
7315
+ url = url.replace(/\{(\w+)\}/g, (match, propName) => {
7316
+ const value = { folderName: this.folderName }[propName];
7317
+ return value !== undefined && value !== null ? String(value) : match;
7318
+ });
7319
+ }
7320
+ else {
7321
+ // Fallback: Legacy /x replacement for backward compatibility
7322
+ url = url.replace("/x", '/' + this.folderName);
7323
+ }
6985
7324
  this.dataService.CallApi({ url: url }, "").subscribe((apiResponse) => {
6986
7325
  this.fileNames = apiResponse.data;
6987
7326
  // console.log(this.fileNames)
@@ -7557,12 +7896,22 @@ class NotesComponent {
7557
7896
  return;
7558
7897
  }
7559
7898
  // Otherwise, load from the URL if configured
7560
- if (this.loadAction && this.data && this.loadIDField) {
7561
- const idValue = this.data[this.loadIDField];
7562
- if (!idValue) {
7563
- return;
7899
+ if (this.loadAction && this.data) {
7900
+ let url = this.loadAction.url;
7901
+ // Changed: Support generic {propertyName} placeholders first
7902
+ if (url.includes('{')) {
7903
+ url = url.replace(/\{(\w+)\}/g, (match, propName) => {
7904
+ const value = this.data?.[propName];
7905
+ return value !== undefined && value !== null ? String(value) : match;
7906
+ });
7907
+ }
7908
+ else if (this.loadIDField) {
7909
+ // Fallback: Use loadIDField for backward compatibility
7910
+ const idValue = this.data[this.loadIDField];
7911
+ if (!idValue)
7912
+ return;
7913
+ url = url.replace('/x', '/' + idValue);
7564
7914
  }
7565
- let url = this.loadAction.url.replace('{x}', idValue);
7566
7915
  this.dataService.CallApi({ ...this.loadAction, url }).subscribe((apiResponse) => {
7567
7916
  if (apiResponse.success && apiResponse.data) {
7568
7917
  this.notes = apiResponse.data;
@@ -14852,11 +15201,11 @@ class RolesComponent {
14852
15201
  });
14853
15202
  }
14854
15203
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RolesComponent, deps: [{ token: HttpService }, { token: i1$2.Router }, { token: AuthService }, { token: DataServiceLib }, { token: DialogService }, { token: i5.MatDialog }, { token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
14855
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: RolesComponent, isStandalone: false, selector: "spa-roles", ngImport: i0, template: "<h4> Roles </h4>\r\n<hr />\r\n\r\n<div class=\"container-fluid mb-5\">\r\n\r\n <div class=\"d-flex justify-content-between mb-2\">\r\n\r\n <div >\r\n <button id=\"btnNewRole\" mat-raised-button color=\"primary\" (click)=\"addRole()\">New Role</button>\r\n </div>\r\n\r\n <div class=\"d-flex justify-content-end\">\r\n <button id=\"btnRefresh\" mat-icon-button color=\"primary\" (click)=\"refresh()\" matTooltip=\"refresh data\" matTooltipPosition=\"right\"><mat-icon >refresh</mat-icon></button>\r\n </div>\r\n\r\n </div>\r\n\r\n\r\n <div class=\"row mt-2 mb-1\" *ngFor=\"let role of roles\">\r\n\r\n <mat-card class=\"mat-elevation-z8\" style=\"width:100%\">\r\n\r\n <div class=\"d-flex justify-content-between align-items-center\">\r\n\r\n <label style=\"font-size: 16px;\">{{role.roleName}}</label>\r\n\r\n <button mat-icon-button color=\"primary\" matTooltip=\"Rename Role\" (click)=\"renameRole(role)\">\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <hr style=\"margin-top: 0px;\">\r\n\r\n <div class=\"tin-row\" style=\" font-size:12px;\">\r\n\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capItem of appConfig.capItems\">\r\n\r\n <!-- Main item-->\r\n <mat-checkbox *ngIf=\"capItem.isBool || capItem.capSubItems\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capItem.name]\" (ngModelChange)=\"onCapItemChange(capItem, $event, role)\">\r\n {{capItem.display}}\r\n <span *ngIf=\"!role[capItem.name] && hasSubItemsAccess(capItem, role)\" class=\"asterisk\" style=\"color: red;\">*</span>\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capItem.isBool && !capItem.capSubItems\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capItem.display\"\r\n [(value)]=\"role[capItem.name]\"\r\n width=\"150px\" \r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n\r\n <ng-container *ngIf=\"capItem.capSubItems && role[capItem.name]\">\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capSubItem of capItem.capSubItems\">\r\n\r\n\r\n <!-- Sub Item -->\r\n <mat-checkbox *ngIf=\"capSubItem.isBool\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubItem.name]\">\r\n {{capSubItem.display}}\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capSubItem.isBool\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capSubItem.display\"\r\n [(value)]=\"role[capSubItem.name]\"\r\n width=\"150px\"\r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n <ng-container *ngIf=\"capSubItem.capSubItems\">\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capSubSubItem of capSubItem.capSubItems\">\r\n\r\n <!-- Sub Sub Items -->\r\n <mat-checkbox *ngIf=\"capSubSubItem.isBool\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubSubItem.name]\">\r\n {{capSubSubItem.display}}\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capSubSubItem.isBool\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capSubSubItem.display\"\r\n [(value)]=\"role[capSubSubItem.name]\"\r\n width=\"150px\" \r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n </div>\r\n\r\n </ng-container>\r\n\r\n\r\n\r\n </div>\r\n\r\n </ng-container>\r\n\r\n </div>\r\n\r\n </div>\r\n\r\n\r\n <mat-card-actions>\r\n\r\n <button mat-raised-button color=\"primary\" (click)=\"updateRole(role)\" style=\"margin-right:10px;\">\r\n <mat-icon>done_all</mat-icon>\r\n Update\r\n </button>\r\n\r\n <button mat-raised-button (click)=\"deleteRole(role)\" style=\"margin-right:10px\">\r\n <mat-icon>delete</mat-icon>\r\n Delete\r\n </button>\r\n\r\n </mat-card-actions>\r\n\r\n </mat-card>\r\n\r\n </div>\r\n\r\n\r\n</div>\r\n\r\n", styles: [""], dependencies: [{ kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "component", type: i5$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i5$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i6$2.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i6$2.MatCardActions, selector: "mat-card-actions", inputs: ["align"], exportAs: ["matCardActions"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }] }); }
15204
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: RolesComponent, isStandalone: false, selector: "spa-roles", ngImport: i0, template: "<h4> Roles </h4>\r\n<hr />\r\n\r\n<div class=\"container-fluid mb-5\">\r\n\r\n <div class=\"d-flex justify-content-between mb-2\">\r\n\r\n <div >\r\n <button id=\"btnNewRole\" mat-raised-button color=\"primary\" (click)=\"addRole()\">New Role</button>\r\n </div>\r\n\r\n <div class=\"d-flex justify-content-end\">\r\n <button id=\"btnRefresh\" mat-icon-button color=\"primary\" (click)=\"refresh()\" matTooltip=\"refresh data\" matTooltipPosition=\"right\"><mat-icon >refresh</mat-icon></button>\r\n </div>\r\n\r\n </div>\r\n\r\n\r\n <div class=\"row mt-2 mb-1\" *ngFor=\"let role of roles\">\r\n\r\n <mat-card class=\"mat-elevation-z8\" style=\"width:100%\">\r\n\r\n <div class=\"d-flex justify-content-between align-items-center\">\r\n\r\n <label style=\"font-size: 16px;\">{{role.roleName}}</label>\r\n\r\n <button mat-icon-button color=\"primary\" matTooltip=\"Rename Role\" (click)=\"renameRole(role)\">\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <hr style=\"margin-top: 0px;\">\r\n\r\n <div class=\"tin-row\" style=\" font-size:12px;\">\r\n\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capItem of appConfig.capItems\">\r\n\r\n <!-- Main item-->\r\n <mat-checkbox *ngIf=\"capItem.isBool || capItem.capSubItems\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capItem.name]\" (ngModelChange)=\"onCapItemChange(capItem, $event, role)\">\r\n {{capItem.display}}\r\n <span *ngIf=\"!role[capItem.name] && hasSubItemsAccess(capItem, role)\" class=\"asterisk\" style=\"color: red;\">*</span>\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capItem.isBool && !capItem.capSubItems\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capItem.display\"\r\n [(value)]=\"role[capItem.name]\"\r\n width=\"150px\" \r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n\r\n <ng-container *ngIf=\"capItem.capSubItems && role[capItem.name]\">\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capSubItem of capItem.capSubItems\">\r\n\r\n\r\n <!-- Sub Item -->\r\n <mat-checkbox *ngIf=\"capSubItem.isBool\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubItem.name]\">\r\n {{capSubItem.display}}\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capSubItem.isBool\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capSubItem.display\"\r\n [(value)]=\"role[capSubItem.name]\"\r\n width=\"150px\"\r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n <ng-container *ngIf=\"capSubItem.capSubItems\">\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capSubSubItem of capSubItem.capSubItems\">\r\n\r\n <!-- Sub Sub Items -->\r\n <mat-checkbox *ngIf=\"capSubSubItem.isBool\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubSubItem.name]\">\r\n {{capSubSubItem.display}}\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capSubSubItem.isBool\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capSubSubItem.display\"\r\n [(value)]=\"role[capSubSubItem.name]\"\r\n width=\"150px\" \r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n </div>\r\n\r\n </ng-container>\r\n\r\n\r\n\r\n </div>\r\n\r\n </ng-container>\r\n\r\n </div>\r\n\r\n </div>\r\n\r\n\r\n <mat-card-actions>\r\n\r\n <button mat-raised-button color=\"primary\" (click)=\"updateRole(role)\" style=\"margin-right:10px;\">\r\n <mat-icon>done_all</mat-icon>\r\n Update\r\n </button>\r\n\r\n <button mat-raised-button (click)=\"deleteRole(role)\" style=\"margin-right:10px\">\r\n <mat-icon>delete</mat-icon>\r\n Delete\r\n </button>\r\n\r\n </mat-card-actions>\r\n\r\n </mat-card>\r\n\r\n </div>\r\n\r\n <hr style=\"margin-top: 50px;\" />\r\n\r\n\r\n</div>\r\n\r\n", styles: [""], dependencies: [{ kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "component", type: i5$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i5$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i6$2.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i6$2.MatCardActions, selector: "mat-card-actions", inputs: ["align"], exportAs: ["matCardActions"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }] }); }
14856
15205
  }
14857
15206
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RolesComponent, decorators: [{
14858
15207
  type: Component,
14859
- args: [{ selector: "spa-roles", standalone: false, template: "<h4> Roles </h4>\r\n<hr />\r\n\r\n<div class=\"container-fluid mb-5\">\r\n\r\n <div class=\"d-flex justify-content-between mb-2\">\r\n\r\n <div >\r\n <button id=\"btnNewRole\" mat-raised-button color=\"primary\" (click)=\"addRole()\">New Role</button>\r\n </div>\r\n\r\n <div class=\"d-flex justify-content-end\">\r\n <button id=\"btnRefresh\" mat-icon-button color=\"primary\" (click)=\"refresh()\" matTooltip=\"refresh data\" matTooltipPosition=\"right\"><mat-icon >refresh</mat-icon></button>\r\n </div>\r\n\r\n </div>\r\n\r\n\r\n <div class=\"row mt-2 mb-1\" *ngFor=\"let role of roles\">\r\n\r\n <mat-card class=\"mat-elevation-z8\" style=\"width:100%\">\r\n\r\n <div class=\"d-flex justify-content-between align-items-center\">\r\n\r\n <label style=\"font-size: 16px;\">{{role.roleName}}</label>\r\n\r\n <button mat-icon-button color=\"primary\" matTooltip=\"Rename Role\" (click)=\"renameRole(role)\">\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <hr style=\"margin-top: 0px;\">\r\n\r\n <div class=\"tin-row\" style=\" font-size:12px;\">\r\n\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capItem of appConfig.capItems\">\r\n\r\n <!-- Main item-->\r\n <mat-checkbox *ngIf=\"capItem.isBool || capItem.capSubItems\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capItem.name]\" (ngModelChange)=\"onCapItemChange(capItem, $event, role)\">\r\n {{capItem.display}}\r\n <span *ngIf=\"!role[capItem.name] && hasSubItemsAccess(capItem, role)\" class=\"asterisk\" style=\"color: red;\">*</span>\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capItem.isBool && !capItem.capSubItems\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capItem.display\"\r\n [(value)]=\"role[capItem.name]\"\r\n width=\"150px\" \r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n\r\n <ng-container *ngIf=\"capItem.capSubItems && role[capItem.name]\">\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capSubItem of capItem.capSubItems\">\r\n\r\n\r\n <!-- Sub Item -->\r\n <mat-checkbox *ngIf=\"capSubItem.isBool\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubItem.name]\">\r\n {{capSubItem.display}}\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capSubItem.isBool\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capSubItem.display\"\r\n [(value)]=\"role[capSubItem.name]\"\r\n width=\"150px\"\r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n <ng-container *ngIf=\"capSubItem.capSubItems\">\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capSubSubItem of capSubItem.capSubItems\">\r\n\r\n <!-- Sub Sub Items -->\r\n <mat-checkbox *ngIf=\"capSubSubItem.isBool\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubSubItem.name]\">\r\n {{capSubSubItem.display}}\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capSubSubItem.isBool\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capSubSubItem.display\"\r\n [(value)]=\"role[capSubSubItem.name]\"\r\n width=\"150px\" \r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n </div>\r\n\r\n </ng-container>\r\n\r\n\r\n\r\n </div>\r\n\r\n </ng-container>\r\n\r\n </div>\r\n\r\n </div>\r\n\r\n\r\n <mat-card-actions>\r\n\r\n <button mat-raised-button color=\"primary\" (click)=\"updateRole(role)\" style=\"margin-right:10px;\">\r\n <mat-icon>done_all</mat-icon>\r\n Update\r\n </button>\r\n\r\n <button mat-raised-button (click)=\"deleteRole(role)\" style=\"margin-right:10px\">\r\n <mat-icon>delete</mat-icon>\r\n Delete\r\n </button>\r\n\r\n </mat-card-actions>\r\n\r\n </mat-card>\r\n\r\n </div>\r\n\r\n\r\n</div>\r\n\r\n" }]
15208
+ args: [{ selector: "spa-roles", standalone: false, template: "<h4> Roles </h4>\r\n<hr />\r\n\r\n<div class=\"container-fluid mb-5\">\r\n\r\n <div class=\"d-flex justify-content-between mb-2\">\r\n\r\n <div >\r\n <button id=\"btnNewRole\" mat-raised-button color=\"primary\" (click)=\"addRole()\">New Role</button>\r\n </div>\r\n\r\n <div class=\"d-flex justify-content-end\">\r\n <button id=\"btnRefresh\" mat-icon-button color=\"primary\" (click)=\"refresh()\" matTooltip=\"refresh data\" matTooltipPosition=\"right\"><mat-icon >refresh</mat-icon></button>\r\n </div>\r\n\r\n </div>\r\n\r\n\r\n <div class=\"row mt-2 mb-1\" *ngFor=\"let role of roles\">\r\n\r\n <mat-card class=\"mat-elevation-z8\" style=\"width:100%\">\r\n\r\n <div class=\"d-flex justify-content-between align-items-center\">\r\n\r\n <label style=\"font-size: 16px;\">{{role.roleName}}</label>\r\n\r\n <button mat-icon-button color=\"primary\" matTooltip=\"Rename Role\" (click)=\"renameRole(role)\">\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <hr style=\"margin-top: 0px;\">\r\n\r\n <div class=\"tin-row\" style=\" font-size:12px;\">\r\n\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capItem of appConfig.capItems\">\r\n\r\n <!-- Main item-->\r\n <mat-checkbox *ngIf=\"capItem.isBool || capItem.capSubItems\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capItem.name]\" (ngModelChange)=\"onCapItemChange(capItem, $event, role)\">\r\n {{capItem.display}}\r\n <span *ngIf=\"!role[capItem.name] && hasSubItemsAccess(capItem, role)\" class=\"asterisk\" style=\"color: red;\">*</span>\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capItem.isBool && !capItem.capSubItems\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capItem.display\"\r\n [(value)]=\"role[capItem.name]\"\r\n width=\"150px\" \r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n\r\n <ng-container *ngIf=\"capItem.capSubItems && role[capItem.name]\">\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capSubItem of capItem.capSubItems\">\r\n\r\n\r\n <!-- Sub Item -->\r\n <mat-checkbox *ngIf=\"capSubItem.isBool\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubItem.name]\">\r\n {{capSubItem.display}}\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capSubItem.isBool\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capSubItem.display\"\r\n [(value)]=\"role[capSubItem.name]\"\r\n width=\"150px\"\r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n <ng-container *ngIf=\"capSubItem.capSubItems\">\r\n\r\n <div class=\"tin-row\" *ngFor=\"let capSubSubItem of capSubItem.capSubItems\">\r\n\r\n <!-- Sub Sub Items -->\r\n <mat-checkbox *ngIf=\"capSubSubItem.isBool\"\r\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubSubItem.name]\">\r\n {{capSubSubItem.display}}\r\n </mat-checkbox>\r\n\r\n <spa-select\r\n *ngIf=\"!capSubSubItem.isBool\"\r\n [options]=\"roleAccessOptions\"\r\n optionDisplay=\"name\"\r\n optionValue=\"value\"\r\n [display]=\"capSubSubItem.display\"\r\n [(value)]=\"role[capSubSubItem.name]\"\r\n width=\"150px\" \r\n style=\"font-size: 12px;\">\r\n </spa-select>\r\n\r\n </div>\r\n\r\n </ng-container>\r\n\r\n\r\n\r\n </div>\r\n\r\n </ng-container>\r\n\r\n </div>\r\n\r\n </div>\r\n\r\n\r\n <mat-card-actions>\r\n\r\n <button mat-raised-button color=\"primary\" (click)=\"updateRole(role)\" style=\"margin-right:10px;\">\r\n <mat-icon>done_all</mat-icon>\r\n Update\r\n </button>\r\n\r\n <button mat-raised-button (click)=\"deleteRole(role)\" style=\"margin-right:10px\">\r\n <mat-icon>delete</mat-icon>\r\n Delete\r\n </button>\r\n\r\n </mat-card-actions>\r\n\r\n </mat-card>\r\n\r\n </div>\r\n\r\n <hr style=\"margin-top: 50px;\" />\r\n\r\n\r\n</div>\r\n\r\n" }]
14860
15209
  }], ctorParameters: () => [{ type: HttpService }, { type: i1$2.Router }, { type: AuthService }, { type: DataServiceLib }, { type: DialogService }, { type: i5.MatDialog }, { type: MessageService }] });
14861
15210
 
14862
15211
  class CreateAccountComponent {
@@ -16064,6 +16413,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
16064
16413
  }]
16065
16414
  }] });
16066
16415
 
16416
+ // ServiceItems component for managing service items
16417
+ class ServiceItemsComponent {
16418
+ constructor() {
16419
+ this.inventoryService = inject(InventoryService);
16420
+ this.pageConfig = {
16421
+ title: 'Services',
16422
+ tableConfig: this.inventoryService.serviceItemsTableConfig
16423
+ };
16424
+ }
16425
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ServiceItemsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
16426
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ServiceItemsComponent, isStandalone: false, selector: "spa-service-items", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
16427
+ }
16428
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ServiceItemsComponent, decorators: [{
16429
+ type: Component,
16430
+ args: [{
16431
+ selector: 'spa-service-items',
16432
+ standalone: false,
16433
+ template: '<spa-page [config]="pageConfig"></spa-page>'
16434
+ }]
16435
+ }] });
16436
+
16067
16437
  class GptCachesComponent {
16068
16438
  constructor() {
16069
16439
  this.dataService = inject(DataServiceLib);
@@ -16124,6 +16494,7 @@ const routes$1 = [
16124
16494
  { path: "loans", component: LoansComponent },
16125
16495
  { path: "loans-payments", component: LoanPaymentsComponent },
16126
16496
  { path: "inventory-products", component: ProductsComponent },
16497
+ { path: "inventory-service-items", component: ServiceItemsComponent }, // Added: Service items route
16127
16498
  { path: "inventory-items", component: InventoryItemsComponent },
16128
16499
  { path: "purchase-orders", component: PurchaseOrdersComponent },
16129
16500
  { path: "inventory-receipts", component: InventoryReceiptsComponent },
@@ -16180,7 +16551,8 @@ class AdminModule {
16180
16551
  InventoryStockComponent,
16181
16552
  ProductionRecipesComponent,
16182
16553
  ProductionOrdersComponent,
16183
- BundleProductsComponent // Added: Bundle products component
16554
+ BundleProductsComponent, // Added: Bundle products component
16555
+ ServiceItemsComponent // Added: Service items component
16184
16556
  ], imports: [CommonModule,
16185
16557
  AdminRoutingModule,
16186
16558
  SpaAdminModule] }); }
@@ -16216,7 +16588,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
16216
16588
  InventoryStockComponent,
16217
16589
  ProductionRecipesComponent,
16218
16590
  ProductionOrdersComponent,
16219
- BundleProductsComponent // Added: Bundle products component
16591
+ BundleProductsComponent, // Added: Bundle products component
16592
+ ServiceItemsComponent // Added: Service items component
16220
16593
  ],
16221
16594
  imports: [
16222
16595
  CommonModule,
@@ -16273,5 +16646,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
16273
16646
  * Generated bundle index. Do not edit.
16274
16647
  */
16275
16648
 
16276
- export { Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, InvoicesComponent as AccountingInvoicesComponent, OutstandingInvoicesComponent as AccountingOutstandingInvoicesComponent, AccountingService, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, Action, ActivityComponent, AdminModule, AlertComponent, AlertConfig, AlertMessage, ApiResponse, AppConfig, AppModelsComponent, AttachComponent, AuthService, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, CheckComponent, ChipsComponent, Constants, Core, CreateAccountComponent, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EmailComponent, EmployeesComponent, ExportService, FilterComponent, FormComponent, FormConfig, GeneralService, GradesComponent, GroupsComponent, HtmlComponent, HttpService, IndexModule, InventoryReceiptStatus, InventoryService, InvoiceStatus, LabelComponent, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, MembershipComponent, MessageService, MoneyComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NumberComponent, OnboardingComponent, OptionComponent, PageComponent, PageConfig, PlansComponent, PositionsComponent, Profile, ProfileComponent, RecoverAccountComponent, Register, Role, RoleAccess, RolesComponent, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectMultiComponent, SettingsComponent, SignupComponent, SpaAdminModule, SpaIndexModule, SpaMatModule, SpaUserModule, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SuppliersComponent, TabService, TableComponent, TableConfig, TabsComponent, TabsInternalComponent, TabsLiteComponent, TasksComponent, TenantsComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, UnitOfMeasure, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, authGuard, dialogOptions, loginConfig, messageDialog, viewerDialog };
16649
+ export { Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, InvoicesComponent as AccountingInvoicesComponent, OutstandingInvoicesComponent as AccountingOutstandingInvoicesComponent, AccountingService, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, Action, ActivityComponent, AdminModule, AlertComponent, AlertConfig, AlertMessage, ApiResponse, AppConfig, AppModelsComponent, AttachComponent, AuthService, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, CheckComponent, ChipsComponent, Constants, Core, CreateAccountComponent, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EmailComponent, EmployeesComponent, ExportService, FilterComponent, FormComponent, FormConfig, GeneralService, GradesComponent, GroupsComponent, HtmlComponent, HttpService, IndexModule, InventoryReceiptStatus, InventoryService, InvoiceItemType, InvoiceStatus, LabelComponent, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, MembershipComponent, MessageService, MoneyComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NumberComponent, OnboardingComponent, OptionComponent, PageComponent, PageConfig, PlansComponent, PositionsComponent, Profile, ProfileComponent, RecoverAccountComponent, Register, Role, RoleAccess, RolesComponent, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectMultiComponent, SettingsComponent, SignupComponent, SpaAdminModule, SpaIndexModule, SpaMatModule, SpaUserModule, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SuppliersComponent, TabService, TableComponent, TableConfig, TabsComponent, TabsInternalComponent, TabsLiteComponent, TasksComponent, TenantsComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, TransactionTiming, UnitOfMeasure, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, authGuard, dialogOptions, loginConfig, messageDialog, viewerDialog };
16277
16650
  //# sourceMappingURL=tin-spa.mjs.map