@zeniai/client-epic-state 5.2.13-beta0ND → 5.2.13-betaAK0

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 (47) hide show
  1. package/lib/epic.d.ts +1 -2
  2. package/lib/epic.js +0 -2
  3. package/lib/esm/epic.js +0 -2
  4. package/lib/esm/index.js +7 -6
  5. package/lib/esm/view/expenseAutomationView/epics/accountRecon/confirmStatementMappingEpic.js +23 -7
  6. package/lib/esm/view/expenseAutomationView/epics/accountRecon/updateCombinedStatementInfoEpic.js +1 -1
  7. package/lib/esm/view/expenseAutomationView/payload/reconciliationPayload.js +1 -1
  8. package/lib/esm/view/expenseAutomationView/reducers/reconciliationViewReducer.js +5 -0
  9. package/lib/esm/view/expenseAutomationView/selectors/reconciliationViewSelector.js +33 -2
  10. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/catalogDetailEditPayload.js +24 -7
  11. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +1 -1
  12. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +6 -8
  13. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogSubFamilyEpics.js +8 -2
  14. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +15 -10
  15. package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +0 -1
  16. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +15 -36
  17. package/lib/esm/view/recommendation/recommendationReducer.js +1 -13
  18. package/lib/index.d.ts +9 -8
  19. package/lib/index.js +32 -30
  20. package/lib/view/expenseAutomationView/epics/accountRecon/confirmStatementMappingEpic.js +23 -7
  21. package/lib/view/expenseAutomationView/epics/accountRecon/updateCombinedStatementInfoEpic.js +1 -1
  22. package/lib/view/expenseAutomationView/payload/reconciliationPayload.d.ts +8 -3
  23. package/lib/view/expenseAutomationView/payload/reconciliationPayload.js +1 -1
  24. package/lib/view/expenseAutomationView/reducers/reconciliationViewReducer.js +5 -0
  25. package/lib/view/expenseAutomationView/selectors/reconciliationViewSelector.d.ts +3 -1
  26. package/lib/view/expenseAutomationView/selectors/reconciliationViewSelector.js +34 -2
  27. package/lib/view/expenseAutomationView/types/reconciliationViewState.d.ts +6 -0
  28. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/catalogDetailEditPayload.d.ts +11 -2
  29. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/catalogDetailEditPayload.js +24 -7
  30. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.d.ts +7 -0
  31. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +4 -3
  32. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.d.ts +19 -4
  33. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +5 -4
  34. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +5 -7
  35. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogSubFamilyEpics.d.ts +1 -1
  36. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogSubFamilyEpics.js +8 -2
  37. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +14 -9
  38. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +0 -1
  39. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +12 -13
  40. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +16 -38
  41. package/lib/view/recommendation/recommendationPayload.d.ts +0 -34
  42. package/lib/view/recommendation/recommendationReducer.d.ts +1 -2
  43. package/lib/view/recommendation/recommendationReducer.js +2 -14
  44. package/package.json +1 -1
  45. package/lib/esm/view/recommendation/submitRecommendationFeedbackEpic.js +0 -52
  46. package/lib/view/recommendation/submitRecommendationFeedbackEpic.d.ts +0 -7
  47. package/lib/view/recommendation/submitRecommendationFeedbackEpic.js +0 -56
@@ -1,6 +1,7 @@
1
1
  import { Amount } from '../../../commonStateTypes/amount';
2
2
  import type { InvoicingPlan } from '../../../entity/invoicing/plan/planState';
3
3
  import type { InvoicingProduct } from '../../../entity/invoicing/product/productState';
4
+ import { type InvoicingCatalogAccountingLookup } from './editInvoicingCatalogItemDetailViewPayload';
4
5
  import type { InvoicingCatalogItemFormSubFamily, InvoicingCatalogItemFormTier } from './editInvoicingCatalogItemDetailViewState';
5
6
  export type CatalogDetailSubFamilyEditValues = InvoicingCatalogItemFormSubFamily;
6
7
  export interface CatalogDetailEditValues {
@@ -30,18 +31,26 @@ export interface CatalogDetailSavePayload {
30
31
  }
31
32
  /** Build default inline-edit form values from a loaded catalog item. */
32
33
  export declare const planToCatalogDetailEditValues: (plan: InvoicingPlan) => CatalogDetailEditValues;
33
- export declare const catalogDetailEditValuesToSavePayload: (values: CatalogDetailEditValues, itemType?: string, originalSubFamilies?: readonly InvoicingProduct[]) => CatalogDetailSavePayload;
34
+ export declare const catalogDetailEditValuesToSavePayload: (values: CatalogDetailEditValues, lookup: InvoicingCatalogAccountingLookup, itemType?: string, originalSubFamilies?: readonly InvoicingProduct[]) => CatalogDetailSavePayload;
34
35
  export interface CreateSubFamilyFormValues {
36
+ /** The QuickBooks class id the price point posts to. */
37
+ accountingCategory: string;
38
+ /** The QuickBooks income account id the price point posts to. */
39
+ accountingCode: string;
35
40
  billingCycles: string;
36
41
  description: string;
37
42
  freeQuantity: string;
38
43
  invoiceNotes: string;
44
+ isTaxable: boolean;
39
45
  name: string;
40
46
  period: string;
41
47
  periodUnit: string;
42
48
  priceCode: string;
43
49
  pricingModel: string;
44
50
  prorationType: string;
51
+ showDescriptionInInvoices: boolean;
52
+ showDescriptionInQuotes: boolean;
53
+ sku: string;
45
54
  trialEndAction: string;
46
55
  trialPeriod: string;
47
56
  trialPeriodUnit: string;
@@ -50,5 +59,5 @@ export interface CreateSubFamilyFormValues {
50
59
  tiers?: InvoicingCatalogItemFormTier[];
51
60
  }
52
61
  /** Build POST /catalog/products body for Create sub-family. */
53
- export declare const createSubFamilyFormToPayload: (itemId: string, itemType: string, values: CreateSubFamilyFormValues) => Record<string, unknown>;
62
+ export declare const createSubFamilyFormToPayload: (itemId: string, itemType: string, values: CreateSubFamilyFormValues, lookup: InvoicingCatalogAccountingLookup) => Record<string, unknown>;
54
63
  export declare const productToCreateSubFamilyFormValues: (product?: InvoicingProduct) => CreateSubFamilyFormValues;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.productToCreateSubFamilyFormValues = exports.createSubFamilyFormToPayload = exports.catalogDetailEditValuesToSavePayload = exports.planToCatalogDetailEditValues = void 0;
4
+ const editInvoicingCatalogItemDetailViewPayload_1 = require("./editInvoicingCatalogItemDetailViewPayload");
4
5
  const invoicingCatalogItemFormConfig_1 = require("./invoicingCatalogItemFormConfig");
5
6
  const trimOrUndefined = (value) => value.trim() !== '' ? value.trim() : undefined;
6
7
  const trimOrNull = (value) => value.trim() !== '' ? value.trim() : null;
@@ -39,7 +40,7 @@ const tierToPayload = (tier) => ({
39
40
  pricing_type: tier.pricingType,
40
41
  starting_unit: Number(tier.startingUnit) || 1,
41
42
  });
42
- const subFamilyToProductUpdate = (subFamily, itemType) => {
43
+ const subFamilyToProductUpdate = (subFamily, itemType, lookup) => {
43
44
  const tiered = (0, invoicingCatalogItemFormConfig_1.isTieredCatalogPricingModel)(subFamily.pricingModel);
44
45
  const data = {
45
46
  billing_cycles: hasValue(subFamily.billingCycles)
@@ -66,9 +67,12 @@ const subFamilyToProductUpdate = (subFamily, itemType) => {
66
67
  if (itemType === 'addon') {
67
68
  data.proration_type = subFamily.prorationType;
68
69
  }
70
+ // `null` rather than omitted when empty: an absent key on an update reads as
71
+ // "leave it alone", so a cleared account or class could never be saved.
72
+ data.accounting_detail = (0, editInvoicingCatalogItemDetailViewPayload_1.buildAccountingDetail)(subFamily, lookup) ?? null;
69
73
  return { data, id: subFamily.id };
70
74
  };
71
- const catalogDetailEditValuesToSavePayload = (values, itemType = 'plan', originalSubFamilies) => {
75
+ const catalogDetailEditValuesToSavePayload = (values, lookup, itemType = 'plan', originalSubFamilies) => {
72
76
  const planData = {
73
77
  applicable_items: values.itemApplicability === 'restricted' ? values.applicableItems : [],
74
78
  enabled_for_checkout: values.enabledForCheckout,
@@ -100,12 +104,12 @@ const catalogDetailEditValuesToSavePayload = (values, itemType = 'plan', origina
100
104
  planData.unit = unit;
101
105
  }
102
106
  const productUpdates = values.subFamilies.flatMap((subFamily) => {
103
- const update = subFamilyToProductUpdate(subFamily, itemType);
107
+ const update = subFamilyToProductUpdate(subFamily, itemType, lookup);
104
108
  const original = originalSubFamilies?.find((product) => product.id === subFamily.id);
105
109
  if (original == null) {
106
110
  return [update];
107
111
  }
108
- const originalUpdate = subFamilyToProductUpdate(subFamilyToEditValues(original), itemType);
112
+ const originalUpdate = subFamilyToProductUpdate(subFamilyToEditValues(original), itemType, lookup);
109
113
  const changedData = Object.fromEntries(Object.entries(update.data).filter(([key, value]) => JSON.stringify(value) !== JSON.stringify(originalUpdate.data[key])));
110
114
  return Object.keys(changedData).length > 0
111
115
  ? [{ data: changedData, id: update.id }]
@@ -118,21 +122,28 @@ const catalogDetailEditValuesToSavePayload = (values, itemType = 'plan', origina
118
122
  };
119
123
  exports.catalogDetailEditValuesToSavePayload = catalogDetailEditValuesToSavePayload;
120
124
  /** Build POST /catalog/products body for Create sub-family. */
121
- const createSubFamilyFormToPayload = (itemId, itemType, values) => {
125
+ const createSubFamilyFormToPayload = (itemId, itemType, values, lookup) => {
122
126
  const name = values.name.trim();
123
127
  const priceCode = trimOrUndefined(values.priceCode) ?? toProductCode(name);
124
128
  const tiered = (0, invoicingCatalogItemFormConfig_1.isTieredCatalogPricingModel)(values.pricingModel);
125
129
  const payload = {
126
130
  currency_code: 'USD',
127
131
  free_quantity: Number(values.freeQuantity) || 0,
128
- is_taxable: true,
132
+ is_taxable: values.isTaxable,
129
133
  item_id: itemId,
130
134
  name,
131
135
  price: tiered ? 0 : (values.priceDollars?.amount ?? 0),
132
136
  price_code: priceCode,
133
137
  pricing_model: values.pricingModel || 'flat_fee',
134
- show_description_in_invoices: true,
138
+ show_description_in_invoices: values.showDescriptionInInvoices,
139
+ show_description_in_quotes: values.showDescriptionInQuotes,
135
140
  };
141
+ // The same accounting body the item payload sends, so a price point created
142
+ // from the dialog carries the account and class the form would have given it.
143
+ const accountingDetail = (0, editInvoicingCatalogItemDetailViewPayload_1.buildAccountingDetail)(values, lookup);
144
+ if (accountingDetail != null) {
145
+ payload.accounting_detail = accountingDetail;
146
+ }
136
147
  if (tiered) {
137
148
  payload.tiers = (values.tiers ?? []).map(tierToPayload);
138
149
  }
@@ -163,10 +174,13 @@ const createSubFamilyFormToPayload = (itemId, itemType, values) => {
163
174
  };
164
175
  exports.createSubFamilyFormToPayload = createSubFamilyFormToPayload;
165
176
  const productToCreateSubFamilyFormValues = (product) => ({
177
+ accountingCategory: product?.accountingDetail?.accountingCategory1 ?? '',
178
+ accountingCode: product?.accountingDetail?.accountingCode ?? '',
166
179
  billingCycles: product?.billingCycles != null ? String(product.billingCycles) : '',
167
180
  description: product?.description ?? '',
168
181
  freeQuantity: product?.freeQuantity != null ? String(product.freeQuantity) : '0',
169
182
  invoiceNotes: product?.invoiceNotes ?? '',
183
+ isTaxable: product?.isTaxable ?? true,
170
184
  name: product?.name != null ? `${product.name} (copy)` : '',
171
185
  period: product?.period != null ? String(product.period) : '1',
172
186
  periodUnit: product?.periodUnit ?? 'month',
@@ -174,6 +188,9 @@ const productToCreateSubFamilyFormValues = (product) => ({
174
188
  priceDollars: product?.price,
175
189
  pricingModel: product?.pricingModel ?? 'flat_fee',
176
190
  prorationType: product?.prorationType ?? 'site_default',
191
+ showDescriptionInInvoices: product?.showDescriptionInInvoices ?? true,
192
+ showDescriptionInQuotes: product?.showDescriptionInQuotes ?? false,
193
+ sku: product?.accountingDetail?.sku ?? '',
177
194
  trialEndAction: product?.trialEndAction ?? 'site_default',
178
195
  trialPeriod: product?.trialPeriod != null ? String(product.trialPeriod) : '',
179
196
  trialPeriodUnit: product?.trialPeriodUnit ?? 'day',
@@ -13,6 +13,13 @@ export interface InvoicingCatalogAccountingLookup {
13
13
  accountByQboId: (qboId: string) => AccountBase | undefined;
14
14
  classByQboId: (qboId: string) => ClassBase | undefined;
15
15
  }
16
+ /** The accounting fields any price-point form collects. */
17
+ export interface InvoicingCatalogAccountingFields {
18
+ accountingCategory: string;
19
+ accountingCode: string;
20
+ sku: string;
21
+ }
22
+ export declare const buildAccountingDetail: (subFamily: InvoicingCatalogAccountingFields, lookup: InvoicingCatalogAccountingLookup) => Record<string, unknown> | undefined;
16
23
  /**
17
24
  * Build the parent catalog-item request body from the form draft. Creates omit
18
25
  * both parent and sub-family IDs; edits retain returned sub-family IDs.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.localDataToSaveInvoicingCatalogItemPayload = void 0;
3
+ exports.localDataToSaveInvoicingCatalogItemPayload = exports.buildAccountingDetail = void 0;
4
4
  const accountPayload_1 = require("../../../entity/account/accountPayload");
5
5
  const classPayload_1 = require("../../../entity/class/classPayload");
6
6
  const invoicingCatalogItemFormConfig_1 = require("./invoicingCatalogItemFormConfig");
@@ -31,6 +31,7 @@ const buildAccountingDetail = (subFamily, lookup) => {
31
31
  }
32
32
  return Object.keys(detail).length > 0 ? detail : undefined;
33
33
  };
34
+ exports.buildAccountingDetail = buildAccountingDetail;
34
35
  const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) => {
35
36
  const isAddon = localData.catalogType === 'addon';
36
37
  const tiered = (0, invoicingCatalogItemFormConfig_1.isTieredCatalogPricingModel)(subFamily.pricingModel);
@@ -85,7 +86,7 @@ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) =
85
86
  starting_unit: Number(tier.startingUnit) || 1,
86
87
  }));
87
88
  }
88
- const accountingDetail = buildAccountingDetail(subFamily, lookup);
89
+ const accountingDetail = (0, exports.buildAccountingDetail)(subFamily, lookup);
89
90
  if (accountingDetail != null) {
90
91
  payload.accounting_detail = accountingDetail;
91
92
  }
@@ -142,7 +143,7 @@ const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup)
142
143
  // would make a cleared mapping impossible to save — the same reason
143
144
  // `description` and `invoice_notes` go through `trimOrNull`.
144
145
  detail.accounting_detail =
145
- buildAccountingDetail(subFamily, lookup) ?? null;
146
+ (0, exports.buildAccountingDetail)(subFamily, lookup) ?? null;
146
147
  const id = trimOrUndefined(subFamily.id);
147
148
  return id != null ? { ...detail, id } : detail;
148
149
  });
@@ -1,7 +1,8 @@
1
1
  import { ID } from '../../../commonStateTypes/common';
2
+ import type { InvoicingProduct } from '../../../entity/invoicing/product/productState';
2
3
  import { ZeniAPIStatus } from '../../../responsePayload';
3
4
  import { InvoicingCatalogItemKind } from '../invoicingCatalogItemDetailView/invoicingCatalogItemDetailViewState';
4
- import type { CatalogDetailProductUpdate } from './catalogDetailEditPayload';
5
+ import type { CatalogDetailEditValues, CreateSubFamilyFormValues } from './catalogDetailEditPayload';
5
6
  import { EditInvoicingCatalogItemDetailViewState, InvoicingCatalogItemFormLocalData } from './editInvoicingCatalogItemDetailViewState';
6
7
  /** Push the full form draft into CES (edit tree when `id` is present). */
7
8
  export interface UpdateInvoicingCatalogItemFormDraftPayload {
@@ -18,14 +19,28 @@ export interface SubmitInvoicingCatalogItemFormPayload {
18
19
  * `submitInvoicingCatalogItemForm` instead.
19
20
  */
20
21
  export interface SaveInvoicingCatalogItemPayload {
21
- data: Record<string, unknown>;
22
+ /**
23
+ * The detail page's edit form values. The epic builds the parent body and the
24
+ * per-price-point updates from them, so it can resolve a picked account or
25
+ * class to the record the request carries — a page cannot.
26
+ */
27
+ editValues: CatalogDetailEditValues;
22
28
  kind: InvoicingCatalogItemKind;
23
29
  id?: ID;
24
- productUpdates?: CatalogDetailProductUpdate[];
30
+ /** The catalog item's type, for the price-point bodies. */
31
+ itemType?: string;
32
+ /** The saved price points, so only changed fields are sent. */
33
+ originalSubFamilies?: readonly InvoicingProduct[];
25
34
  }
26
35
  export interface CreateInvoicingCatalogSubFamilyPayload {
27
- data: Record<string, unknown>;
36
+ itemType: string;
28
37
  parentItemId: ID;
38
+ /**
39
+ * The dialog's form values. The request body is built in the epic, which can
40
+ * resolve a picked account id to the record the payload carries — the page
41
+ * holds ids and never needs to know the wire shape.
42
+ */
43
+ values: CreateSubFamilyFormValues;
29
44
  }
30
45
  export interface ArchiveInvoicingCatalogSubFamilyPayload {
31
46
  parentItemId: ID;
@@ -48,12 +48,13 @@ export interface InvoicingCatalogItemFormView extends SelectorView {
48
48
  pricingModelValues: readonly string[];
49
49
  prorationValues: readonly string[];
50
50
  /**
51
- * Account and class trees for the price-point pickers, already narrowed to the
52
- * income accounts a price may post to. Empty until QBO is connected — the form
53
- * must not offer a mapping that cannot be pushed.
51
+ * Account and class trees for the price-point pickers, straight from the
52
+ * shared accounting lists — the pickers drop the nodes they cannot submit.
53
+ * Empty until QBO is connected: the form must not offer a mapping that cannot
54
+ * be pushed.
54
55
  */
56
+ qboAccountHierarchy: NestedAccountHierarchyForReport[];
55
57
  qboClassHierarchy: NestedClassHierarchyForReport[];
56
- qboIncomeAccountHierarchy: NestedAccountHierarchyForReport[];
57
58
  tierTypeValues: readonly string[];
58
59
  trialEndActionOptions: InvoicingNamedOption[];
59
60
  trialUnitOptions: InvoicingNamedOption[];
@@ -55,17 +55,15 @@ const getInvoicingCatalogItemFormView = (state, id) => {
55
55
  const applicableItemOptions = id != null
56
56
  ? allApplicableItemOptions.filter((option) => option.id !== id)
57
57
  : allApplicableItemOptions;
58
- const isQboConnected = (0, invoicingQboViewSelector_1.isInvoicingQboConnected)(state);
58
+ const qboView = (0, invoicingQboViewSelector_1.isInvoicingQboConnected)(state)
59
+ ? (0, invoicingQboViewSelector_1.getInvoicingQboView)(state)
60
+ : undefined;
59
61
  const { billingPeriodUnitOptions, catalogItemTypeOptions, catalogTierTypeOptionsByPricingModel, catalogTrialEndActionOptions, catalogTrialUnitOptions, } = (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state);
60
62
  return {
61
63
  fieldHelp: (0, invoicingConfigViewSelector_1.invoicingFieldHelpForGroup)(state, 'catalog'),
62
64
  isQboPickerDataLoading: (0, invoicingQboViewSelector_1.isInvoicingAccountingPickerDataLoading)(state),
63
- qboClassHierarchy: isQboConnected
64
- ? (0, invoicingQboViewSelector_1.getInvoicingQboView)(state).nestedClassHierarchy
65
- : [],
66
- qboIncomeAccountHierarchy: isQboConnected
67
- ? (0, invoicingQboViewSelector_1.invoicingAccountHierarchyForTypes)(state, invoicingQboViewSelector_1.INVOICING_INCOME_ACCOUNT_TYPES)
68
- : [],
65
+ qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? [],
66
+ qboClassHierarchy: qboView?.nestedClassHierarchy ?? [],
69
67
  applicabilityValues: invoicingCatalogItemFormConfig_1.INVOICING_CATALOG_APPLICABILITY_VALUES,
70
68
  applicableItemOptions,
71
69
  localData,
@@ -5,5 +5,5 @@ import { RootState } from '../../../rootStateTypes';
5
5
  import { ZeniAPI } from '../../../zeniAPI';
6
6
  import { archiveInvoicingCatalogSubFamily, createInvoicingCatalogSubFamily, submitInvoicingCatalogItemFormFailure, submitInvoicingCatalogItemFormSuccess } from './editInvoicingCatalogItemDetailViewReducer';
7
7
  export type ActionType = ReturnType<typeof createInvoicingCatalogSubFamily> | ReturnType<typeof archiveInvoicingCatalogSubFamily> | ReturnType<typeof mergeInvoicingPlanSubFamily> | ReturnType<typeof submitInvoicingCatalogItemFormSuccess> | ReturnType<typeof submitInvoicingCatalogItemFormFailure>;
8
- export declare const createInvoicingCatalogSubFamilyEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
8
+ export declare const createInvoicingCatalogSubFamilyEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
9
9
  export declare const archiveInvoicingCatalogSubFamilyEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
@@ -5,6 +5,8 @@ const rxjs_1 = require("rxjs");
5
5
  const operators_1 = require("rxjs/operators");
6
6
  const planReducer_1 = require("../../../entity/invoicing/plan/planReducer");
7
7
  const responsePayload_1 = require("../../../responsePayload");
8
+ const invoicingQboViewSelector_1 = require("../invoicingQboView/invoicingQboViewSelector");
9
+ const catalogDetailEditPayload_1 = require("./catalogDetailEditPayload");
8
10
  const editInvoicingCatalogItemDetailViewReducer_1 = require("./editInvoicingCatalogItemDetailViewReducer");
9
11
  const subFamilySuccessActions = (parentItemId, subFamilyPayload) => [
10
12
  (0, planReducer_1.mergeInvoicingPlanSubFamily)(parentItemId, subFamilyPayload),
@@ -13,8 +15,12 @@ const subFamilySuccessActions = (parentItemId, subFamilyPayload) => [
13
15
  kind: 'plan',
14
16
  }),
15
17
  ];
16
- const createInvoicingCatalogSubFamilyEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(editInvoicingCatalogItemDetailViewReducer_1.createInvoicingCatalogSubFamily.match), (0, operators_1.mergeMap)((action) => {
17
- const { data, parentItemId } = action.payload;
18
+ const createInvoicingCatalogSubFamilyEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(editInvoicingCatalogItemDetailViewReducer_1.createInvoicingCatalogSubFamily.match), (0, operators_1.mergeMap)((action) => {
19
+ const { itemType, parentItemId, values } = action.payload;
20
+ const state = state$.value;
21
+ // Built here so the dialog can stay in picked ids: resolving one to the
22
+ // account record the payload carries needs the shared account list.
23
+ const data = (0, catalogDetailEditPayload_1.createSubFamilyFormToPayload)(parentItemId, itemType, values, (0, invoicingQboViewSelector_1.getInvoicingCatalogAccountingLookup)(state));
18
24
  const baseUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0`;
19
25
  return zeniAPI
20
26
  .postAndGetJSON(`${baseUrl}/catalog/products`, data)
@@ -10,25 +10,30 @@ const snackbarReducer_1 = require("../../../entity/snackbar/snackbarReducer");
10
10
  const responsePayload_1 = require("../../../responsePayload");
11
11
  const invoicingCatalogListViewReducer_1 = require("../invoicingCatalogListView/invoicingCatalogListViewReducer");
12
12
  const invoicingQboViewSelector_1 = require("../invoicingQboView/invoicingQboViewSelector");
13
+ const catalogDetailEditPayload_1 = require("./catalogDetailEditPayload");
13
14
  const editInvoicingCatalogItemDetailViewPayload_1 = require("./editInvoicingCatalogItemDetailViewPayload");
14
15
  const editInvoicingCatalogItemDetailViewReducer_1 = require("./editInvoicingCatalogItemDetailViewReducer");
15
16
  const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)((action) => editInvoicingCatalogItemDetailViewReducer_1.submitInvoicingCatalogItemForm.match(action) ||
16
17
  editInvoicingCatalogItemDetailViewReducer_1.saveInvoicingCatalogItem.match(action)), (0, operators_1.mergeMap)((action) => {
17
18
  const { id } = action.payload;
18
- // Draft-based create/edit form vs. detail-page inline edit (raw data).
19
+ const state = state$.value;
20
+ const accountingLookup = (0, invoicingQboViewSelector_1.getInvoicingCatalogAccountingLookup)(state);
21
+ // Both sources are form values, turned into request bodies here so the
22
+ // picked account and class become the records the service stores: the
23
+ // detail page's edit form, or the create/edit form's draft.
19
24
  let data;
25
+ let editProductUpdates;
20
26
  if (editInvoicingCatalogItemDetailViewReducer_1.saveInvoicingCatalogItem.match(action)) {
21
- data = action.payload.data;
27
+ const { editValues, itemType, originalSubFamilies } = action.payload;
28
+ const savePayload = (0, catalogDetailEditPayload_1.catalogDetailEditValuesToSavePayload)(editValues, accountingLookup, itemType, originalSubFamilies);
29
+ data = savePayload.planData;
30
+ editProductUpdates = savePayload.productUpdates;
22
31
  }
23
32
  else {
24
- const viewState = state$.value.editInvoicingCatalogItemDetailViewState;
33
+ const viewState = state.editInvoicingCatalogItemDetailViewState;
25
34
  const localData = (id != null ? viewState.editDraftById[id] : viewState.newItemDraft) ??
26
35
  viewState.newItemDraft;
27
- const state = state$.value;
28
- data = (0, editInvoicingCatalogItemDetailViewPayload_1.localDataToSaveInvoicingCatalogItemPayload)(localData, id == null, {
29
- accountByQboId: (qboId) => (0, invoicingQboViewSelector_1.invoicingQboAccountByQboId)(state, qboId),
30
- classByQboId: (qboId) => (0, invoicingQboViewSelector_1.invoicingQboClassByQboId)(state, qboId),
31
- });
36
+ data = (0, editInvoicingCatalogItemDetailViewPayload_1.localDataToSaveInvoicingCatalogItemPayload)(localData, id == null, accountingLookup);
32
37
  }
33
38
  const baseUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0`;
34
39
  const path = id != null
@@ -91,7 +96,7 @@ const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actions$.pip
91
96
  return resultActions;
92
97
  };
93
98
  const productUpdates = editInvoicingCatalogItemDetailViewReducer_1.saveInvoicingCatalogItem.match(action)
94
- ? (action.payload.productUpdates ?? [])
99
+ ? (editProductUpdates ?? [])
95
100
  : [];
96
101
  if (productUpdates.length === 0) {
97
102
  return completeSave([]);
@@ -21,7 +21,6 @@ const fetchInvoicingAccountingPickersEpic = (actions$, state$) => actions$.pipe(
21
21
  accountList.fetchState !== 'In-Progress') {
22
22
  // Same arguments the vendor and categorization screens pass, so this
23
23
  // shares their cached list rather than replacing it with a narrower one.
24
- // Invoicing narrows to income accounts when it builds its options.
25
24
  actions.push((0, accountListReducer_1.fetchAccountList)(invoicingQboViewSelector_1.INVOICING_ACCOUNT_LIST_KEY, true));
26
25
  }
27
26
  const classList = state.classListState;
@@ -1,7 +1,7 @@
1
1
  import { NestedAccountHierarchyForReport } from '../../../commonStateTypes/accountView/nestedAccountID';
2
2
  import { NestedClassHierarchyForReport } from '../../../commonStateTypes/classesView/nestedClassID';
3
3
  import { FetchStateAndError } from '../../../commonStateTypes/common';
4
- import { AccountBase, AccountType } from '../../../entity/account/accountState';
4
+ import { AccountBase } from '../../../entity/account/accountState';
5
5
  import { ClassBase } from '../../../entity/class/classState';
6
6
  import { RootState } from '../../../rootStateTypes';
7
7
  import { InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
@@ -16,11 +16,6 @@ export declare const isInvoicingQboConnected: (state: RootState) => boolean;
16
16
  export declare const isInvoicingAccountingPickerDataLoading: (state: RootState) => boolean;
17
17
  /** The QBO connection fetch on its own, without building the bundled view. */
18
18
  export declare const getInvoicingQboConnectionFetchState: (state: RootState) => FetchStateAndError;
19
- /**
20
- * Account types that can back invoicing revenue. Named here so screens ask for
21
- * "income accounts" rather than repeating the accounting service's vocabulary.
22
- */
23
- export declare const INVOICING_INCOME_ACCOUNT_TYPES: readonly AccountType[];
24
19
  /**
25
20
  * The account behind a picker value. The pickers submit the QuickBooks id, so
26
21
  * that is what callers hold; the payload builders need the whole entity to send
@@ -29,6 +24,17 @@ export declare const INVOICING_INCOME_ACCOUNT_TYPES: readonly AccountType[];
29
24
  export declare const invoicingQboAccountByQboId: (state: RootState, qboId: string | undefined) => AccountBase | undefined;
30
25
  /** The class behind a picker value, for the same reason as the account above. */
31
26
  export declare const invoicingQboClassByQboId: (state: RootState, qboId: string | undefined) => ClassBase | undefined;
27
+ /**
28
+ * The resolver every catalog and settings payload builder needs: a picked
29
+ * QuickBooks id turned back into the account or class record the request body
30
+ * carries. Memoized so callers get one stable object rather than a fresh pair of
31
+ * closures per store change, which in `mapStateToProps` would re-render on every
32
+ * unrelated action.
33
+ */
34
+ export declare const getInvoicingCatalogAccountingLookup: (state: RootState) => {
35
+ accountByQboId: (qboId: string) => AccountBase | undefined;
36
+ classByQboId: (qboId: string) => ClassBase | undefined;
37
+ };
32
38
  /** An account or class as the invoicing pickers render and submit it. */
33
39
  export interface InvoicingAccountingOption {
34
40
  id: string;
@@ -60,13 +66,6 @@ export interface InvoicingQboView {
60
66
  * required would post a mapping QuickBooks rejects.
61
67
  */
62
68
  export declare const invoicingAccountsForQboAccountTypes: (accounts: InvoicingAccountingOption[], accountTypes: string) => InvoicingAccountingOption[];
63
- /**
64
- * The account tree narrowed to the given account types. The tree itself carries
65
- * no account type, so the flat account list supplies that and the matching
66
- * `qboId`s are kept — which is why callers that only need "income accounts, as a
67
- * tree" do not have to hold a second flat list to filter with.
68
- */
69
- export declare const invoicingAccountHierarchyForTypes: (state: RootState, accountTypes: readonly AccountType[]) => NestedAccountHierarchyForReport[];
70
69
  /**
71
70
  * Single bundled view for the QBO integration settings screen — the
72
71
  * connection, the account and class options, sync health, and the fetch/action
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getInvoicingQboView = exports.invoicingAccountHierarchyForTypes = exports.invoicingAccountsForQboAccountTypes = exports.invoicingQboClassByQboId = exports.invoicingQboAccountByQboId = exports.INVOICING_INCOME_ACCOUNT_TYPES = exports.getInvoicingQboConnectionFetchState = exports.isInvoicingAccountingPickerDataLoading = exports.isInvoicingQboConnected = exports.INVOICING_ACCOUNT_LIST_KEY = void 0;
3
+ exports.getInvoicingQboView = exports.invoicingAccountsForQboAccountTypes = exports.getInvoicingCatalogAccountingLookup = exports.invoicingQboClassByQboId = exports.invoicingQboAccountByQboId = exports.getInvoicingQboConnectionFetchState = exports.isInvoicingAccountingPickerDataLoading = exports.isInvoicingQboConnected = exports.INVOICING_ACCOUNT_LIST_KEY = void 0;
4
+ const toolkit_1 = require("@reduxjs/toolkit");
4
5
  const accountListSelector_1 = require("../../accountList/accountListSelector");
5
6
  const classListSelector_1 = require("../../classList/classListSelector");
6
7
  // The shared bucket, fetched with the same arguments as the other consumers so
7
- // whichever screen loads first satisfies the rest. Invoicing narrows to income
8
- // accounts when it builds its options, so it does not need its own copy.
8
+ // whichever screen loads first satisfies the rest. Invoicing renders the same
9
+ // accounts they do, so it does not need its own copy.
9
10
  exports.INVOICING_ACCOUNT_LIST_KEY = 'accountList';
10
11
  /** Whether QBO is connected, without building the option lists to find out. */
11
12
  const isInvoicingQboConnected = (state) => state.invoicingQboViewState.connection?.connected === true;
@@ -35,14 +36,6 @@ exports.isInvoicingAccountingPickerDataLoading = isInvoicingAccountingPickerData
35
36
  /** The QBO connection fetch on its own, without building the bundled view. */
36
37
  const getInvoicingQboConnectionFetchState = (state) => state.invoicingQboViewState.connectionFetchState;
37
38
  exports.getInvoicingQboConnectionFetchState = getInvoicingQboConnectionFetchState;
38
- /**
39
- * Account types that can back invoicing revenue. Named here so screens ask for
40
- * "income accounts" rather than repeating the accounting service's vocabulary.
41
- */
42
- exports.INVOICING_INCOME_ACCOUNT_TYPES = [
43
- 'income',
44
- 'other_income',
45
- ];
46
39
  /**
47
40
  * The account behind a picker value. The pickers submit the QuickBooks id, so
48
41
  * that is what callers hold; the payload builders need the whole entity to send
@@ -85,6 +78,17 @@ const invoicingQboClassByQboId = (state, qboId) => {
85
78
  }
86
79
  };
87
80
  exports.invoicingQboClassByQboId = invoicingQboClassByQboId;
81
+ /**
82
+ * The resolver every catalog and settings payload builder needs: a picked
83
+ * QuickBooks id turned back into the account or class record the request body
84
+ * carries. Memoized so callers get one stable object rather than a fresh pair of
85
+ * closures per store change, which in `mapStateToProps` would re-render on every
86
+ * unrelated action.
87
+ */
88
+ exports.getInvoicingCatalogAccountingLookup = (0, toolkit_1.createSelector)((state) => state, (state) => ({
89
+ accountByQboId: (qboId) => (0, exports.invoicingQboAccountByQboId)(state, qboId),
90
+ classByQboId: (qboId) => (0, exports.invoicingQboClassByQboId)(state, qboId),
91
+ }));
88
92
  /**
89
93
  * Accounts and classes as the invoicing pickers need them. Both come from the
90
94
  * shared accounting-service lists, so whatever another screen already loaded
@@ -92,17 +96,12 @@ exports.invoicingQboClassByQboId = invoicingQboClassByQboId;
92
96
  * API stores; an account without one cannot be posted, so it is dropped rather
93
97
  * than offered.
94
98
  */
95
- const invoicingAccountingOptions = (state, accountTypes) => {
99
+ const invoicingAccountingOptions = (state) => {
96
100
  const { accounts } = (0, accountListSelector_1.getAccountList)(state.accountState, state.accountListState, exports.INVOICING_ACCOUNT_LIST_KEY);
97
101
  return accounts.flatMap((account) => {
98
102
  if (account.qboId == null || account.isDeleted === true) {
99
103
  return [];
100
104
  }
101
- if (accountTypes != null &&
102
- (account.accountType == null ||
103
- !accountTypes.includes(account.accountType))) {
104
- return [];
105
- }
106
105
  return [
107
106
  {
108
107
  id: String(account.qboId),
@@ -160,27 +159,6 @@ const invoicingAccountingClassOptions = (state) => {
160
159
  },
161
160
  ]);
162
161
  };
163
- /**
164
- * The account tree narrowed to the given account types. The tree itself carries
165
- * no account type, so the flat account list supplies that and the matching
166
- * `qboId`s are kept — which is why callers that only need "income accounts, as a
167
- * tree" do not have to hold a second flat list to filter with.
168
- */
169
- const invoicingAccountHierarchyForTypes = (state, accountTypes) => {
170
- const allowedQboIds = new Set(invoicingAccountingOptions(state, accountTypes).map((account) => account.id));
171
- const keepMatching = (nodes) => nodes.flatMap((node) => {
172
- const children = keepMatching(node.children);
173
- const isMatch = node.qboId != null && allowedQboIds.has(String(node.qboId));
174
- // A parent that is not itself a match is still kept when it holds one, so
175
- // the tree's shape survives the filter.
176
- if (!isMatch && children.length === 0) {
177
- return [];
178
- }
179
- return [{ ...node, children }];
180
- });
181
- return keepMatching((0, accountListSelector_1.getNestedAccountListHierarchy)(state.accountListState, exports.INVOICING_ACCOUNT_LIST_KEY));
182
- };
183
- exports.invoicingAccountHierarchyForTypes = invoicingAccountHierarchyForTypes;
184
162
  /**
185
163
  * Single bundled view for the QBO integration settings screen — the
186
164
  * connection, the account and class options, sync health, and the fetch/action
@@ -12,38 +12,4 @@ interface FetchRecommendationPayloadWithCOT {
12
12
  recommendations: RecommendationPayloadWithCOT[];
13
13
  }
14
14
  export type FetchRecommendationResponseWithCOT = ZeniAPIResponse<FetchRecommendationPayloadWithCOT>;
15
- export type RecommendationFeedbackDimension = 'vendor' | 'category' | 'class';
16
- export type RecommendationFeedbackSource = 'primary' | 'alternate' | 'shadow' | 'qbo';
17
- export type RecommendationFeedbackOutcome = 'accepted' | 'override_ranked' | 'override_outside' | 'retracted';
18
- export interface RecommendationFeedbackValue {
19
- name: string;
20
- id?: string | null;
21
- integrationId?: string | null;
22
- }
23
- export interface ShownRecommendationForFeedback {
24
- rank: number;
25
- source: RecommendationFeedbackSource;
26
- value: RecommendationFeedbackValue;
27
- confidence?: number | null;
28
- cot?: string[];
29
- reasoning?: string | null;
30
- }
31
- export interface SubmitRecommendationFeedbackPayload {
32
- dimension: RecommendationFeedbackDimension;
33
- outcome: RecommendationFeedbackOutcome;
34
- selected: RecommendationFeedbackValue & {
35
- isNewEntity?: boolean;
36
- };
37
- transactionId: string;
38
- features?: Record<string, unknown> | null;
39
- lineId?: string | null;
40
- matchedRank?: number | null;
41
- matchedSource?: RecommendationFeedbackSource | null;
42
- reasonText?: string | null;
43
- recommendationSetId?: string | null;
44
- retractsFeedbackId?: string | null;
45
- shownRecommendations?: ShownRecommendationForFeedback[];
46
- skipped?: boolean;
47
- topPickSource?: RecommendationFeedbackSource | null;
48
- }
49
15
  export {};
@@ -5,7 +5,6 @@ import { TransactionID } from '../../entity/transaction/stateTypes/transactionID
5
5
  import { SupportedTransaction } from '../../entity/transaction/transactionState';
6
6
  import { ZeniAPIStatus } from '../../responsePayload';
7
7
  import { TransactionsTab } from '../expenseAutomationView/types/transactionsViewState';
8
- import { SubmitRecommendationFeedbackPayload } from './recommendationPayload';
9
8
  import { EntityRecommendationState } from './recommendationState';
10
9
  export type RecommendationLineUpdateFlowType = 'categorization' | 'detail';
11
10
  type FetchEntityRecommendationsForLineUpdateCommonPayload = {
@@ -58,6 +57,6 @@ export declare const fetchEntityRecommendationsByTransactionId: import("@reduxjs
58
57
  }, "recommendation/updateRecommendationForEntityFailure">, indicateReadyToInitializeRecommendation: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[entityIdOrName: string, entityType: EntityType], {
59
58
  entityIdOrName: string;
60
59
  entityType: EntityType;
61
- }, "recommendation/indicateReadyToInitializeRecommendation", never, never>, submitRecommendationFeedback: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[args: SubmitRecommendationFeedbackPayload], SubmitRecommendationFeedbackPayload, "recommendation/submitRecommendationFeedback", never, never>;
60
+ }, "recommendation/indicateReadyToInitializeRecommendation", never, never>;
62
61
  declare const _default: import("redux").Reducer<EntityRecommendationState>;
63
62
  export default _default;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.submitRecommendationFeedback = exports.indicateReadyToInitializeRecommendation = exports.updateRecommendationForEntityFailure = exports.updateRecommendationForEntity = exports.fetchRecommendationByEntityId = exports.fetchRecommendationByEntityName = exports.fetchEntityRecommendationsForLineUpdate = exports.fetchEntityRecommendationsByTransactionId = exports.initialState = void 0;
4
+ exports.indicateReadyToInitializeRecommendation = exports.updateRecommendationForEntityFailure = exports.updateRecommendationForEntity = exports.fetchRecommendationByEntityId = exports.fetchRecommendationByEntityName = exports.fetchEntityRecommendationsForLineUpdate = exports.fetchEntityRecommendationsByTransactionId = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
6
  const rootActions_1 = require("../../rootActions");
7
7
  const recommendationState_1 = require("./recommendationState");
@@ -135,18 +135,6 @@ const recommendation = (0, toolkit_1.createSlice)({
135
135
  };
136
136
  },
137
137
  },
138
- // Fire-and-forget human feedback on a recommendation (accept, ranked
139
- // override or outside override). Handled by submitRecommendationFeedbackEpic
140
- // which POSTs to the accounting recommendation-feedback endpoint. No state
141
- // mutation: the categorization UX must not depend on the feedback round-trip.
142
- submitRecommendationFeedback: {
143
- reducer() {
144
- // Fire-and-forget: no state mutation; the epic performs the POST.
145
- },
146
- prepare(args) {
147
- return { payload: args };
148
- },
149
- },
150
138
  },
151
139
  extraReducers: (builder) => {
152
140
  builder.addCase(rootActions_1.clearAll, (draft) => {
@@ -154,7 +142,7 @@ const recommendation = (0, toolkit_1.createSlice)({
154
142
  });
155
143
  },
156
144
  });
157
- _a = recommendation.actions, exports.fetchEntityRecommendationsByTransactionId = _a.fetchEntityRecommendationsByTransactionId, exports.fetchEntityRecommendationsForLineUpdate = _a.fetchEntityRecommendationsForLineUpdate, exports.fetchRecommendationByEntityName = _a.fetchRecommendationByEntityName, exports.fetchRecommendationByEntityId = _a.fetchRecommendationByEntityId, exports.updateRecommendationForEntity = _a.updateRecommendationForEntity, exports.updateRecommendationForEntityFailure = _a.updateRecommendationForEntityFailure, exports.indicateReadyToInitializeRecommendation = _a.indicateReadyToInitializeRecommendation, exports.submitRecommendationFeedback = _a.submitRecommendationFeedback;
145
+ _a = recommendation.actions, exports.fetchEntityRecommendationsByTransactionId = _a.fetchEntityRecommendationsByTransactionId, exports.fetchEntityRecommendationsForLineUpdate = _a.fetchEntityRecommendationsForLineUpdate, exports.fetchRecommendationByEntityName = _a.fetchRecommendationByEntityName, exports.fetchRecommendationByEntityId = _a.fetchRecommendationByEntityId, exports.updateRecommendationForEntity = _a.updateRecommendationForEntity, exports.updateRecommendationForEntityFailure = _a.updateRecommendationForEntityFailure, exports.indicateReadyToInitializeRecommendation = _a.indicateReadyToInitializeRecommendation;
158
146
  exports.default = recommendation.reducer;
159
147
  const doUpdateRecommendationForEntity = (draft, entityId, entityType, payload) => {
160
148
  const key = (0, recommendationState_1.getEntityRecommendationKey)(entityType, entityId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.13-beta0ND",
3
+ "version": "5.2.13-betaAK0",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",