jupiter-dynamic-forms 1.19.9 → 1.20.1

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.
package/dist/index.mjs CHANGED
@@ -683,6 +683,7 @@ class XBRLFormBuilder {
683
683
  var _a, _b;
684
684
  const label = this.getPreferredLabel(concept.labels, language, concept.preferredLabel);
685
685
  const childPeriodHint = ((_a = concept.preferredLabel) == null ? void 0 : _a.includes("periodStartLabel")) ? "start" : ((_b = concept.preferredLabel) == null ? void 0 : _b.includes("periodEndLabel")) ? "end" : ancestorPeriodHint;
686
+ const effectivePeriodRole = childPeriodHint === "start" ? "periodStartLabel" : childPeriodHint === "end" ? "periodEndLabel" : void 0;
686
687
  const fields = [];
687
688
  if (!concept.elementAbstract) {
688
689
  let columnIds = [];
@@ -751,8 +752,10 @@ class XBRLFormBuilder {
751
752
  periodType: concept.periodType,
752
753
  balance: concept.balance,
753
754
  // Pass through balance attribute from XBRL data
754
- preferredLabel: concept.preferredLabel
755
+ preferredLabel: concept.preferredLabel,
755
756
  // Pass through for period date calculation in manual columns
757
+ effectivePeriodRole
758
+ // JDF-039: own-or-inherited periodStartLabel/periodEndLabel role
756
759
  };
757
760
  }
758
761
  /**
@@ -6432,50 +6435,87 @@ let JupiterFormSection = class extends LitElement {
6432
6435
  this._totalChildConceptsMap.clear();
6433
6436
  this._traverseTotalGroups(concepts);
6434
6437
  }
6438
+ /**
6439
+ * JDF-038: XBRL uses more than one preferredLabel role to mark a "total" row — the plain
6440
+ * `.../role/totalLabel` and the sign-flipped `.../role/negatedTotalLabel` (used for subtotals of
6441
+ * negated/expense items, e.g. "Total of sum of expenses"). A case-sensitive `includes('totalLabel')`
6442
+ * misses the latter, because "negatedTotalLabel" contains "TotalLabel" with a capital T. Case-insensitive
6443
+ * matching is the single source of truth for "is this row a total" everywhere in this file.
6444
+ */
6445
+ static _isTotalLabel(preferredLabel) {
6446
+ return !!(preferredLabel == null ? void 0 : preferredLabel.toLowerCase().includes("totallabel"));
6447
+ }
6448
+ /**
6449
+ * A member of a total's sibling/child group can itself be a presentation-only abstract wrapper
6450
+ * (e.g. "Group equity" wrapping Equity + Third party share + "Total of group equity") rather than
6451
+ * a value-bearing concept. Abstract concepts never have a form field, so resolve them to the nested
6452
+ * totalLabel concept that actually carries their aggregate value — otherwise the outer total's
6453
+ * calculation silently drops that whole subgroup (formData lookup on the abstract's id is always empty).
6454
+ */
6455
+ static _resolveValueConcept(node, depth = 0) {
6456
+ var _a;
6457
+ if (!node.abstract)
6458
+ return node;
6459
+ if (depth > 5 || !((_a = node.children) == null ? void 0 : _a.length))
6460
+ return null;
6461
+ const nestedTotal = node.children.find((c2) => JupiterFormSection._isTotalLabel(c2.preferredLabel));
6462
+ if (!nestedTotal)
6463
+ return null;
6464
+ return JupiterFormSection._resolveValueConcept(nestedTotal, depth + 1);
6465
+ }
6466
+ _registerTotalGroup(totalConcept, members) {
6467
+ const resolvedMembers = members.map((m) => JupiterFormSection._resolveValueConcept(m)).filter((m) => m !== null);
6468
+ if (resolvedMembers.length === 0)
6469
+ return;
6470
+ const memberIds = resolvedMembers.map((m) => m.id);
6471
+ this._totalChildrenMap.set(totalConcept.id, memberIds);
6472
+ this._totalConceptMap.set(totalConcept.id, totalConcept);
6473
+ this._totalChildConceptsMap.set(totalConcept.id, resolvedMembers);
6474
+ for (const memberId of memberIds) {
6475
+ this._memberParentMap.set(memberId, totalConcept.id);
6476
+ }
6477
+ }
6435
6478
  _traverseTotalGroups(concepts) {
6436
- var _a, _b, _c, _d;
6479
+ var _a, _b;
6437
6480
  for (let i2 = 0; i2 < concepts.length; i2++) {
6438
6481
  const concept = concepts[i2];
6439
- if ((_a = concept.preferredLabel) == null ? void 0 : _a.includes("totalLabel")) {
6440
- if ((_b = concept.children) == null ? void 0 : _b.length) {
6441
- const childIds = concept.children.map((c2) => c2.id);
6442
- this._totalChildrenMap.set(concept.id, childIds);
6443
- this._totalConceptMap.set(concept.id, concept);
6444
- this._totalChildConceptsMap.set(concept.id, concept.children);
6445
- for (const childId of childIds) {
6446
- this._memberParentMap.set(childId, concept.id);
6447
- }
6482
+ if (JupiterFormSection._isTotalLabel(concept.preferredLabel)) {
6483
+ if ((_a = concept.children) == null ? void 0 : _a.length) {
6484
+ this._registerTotalGroup(concept, concept.children);
6448
6485
  } else {
6449
6486
  let lastTotalIdx = -1;
6450
6487
  for (let j = i2 - 1; j >= 0; j--) {
6451
- if ((_c = concepts[j].preferredLabel) == null ? void 0 : _c.includes("totalLabel")) {
6488
+ if (JupiterFormSection._isTotalLabel(concepts[j].preferredLabel)) {
6452
6489
  lastTotalIdx = j;
6453
6490
  break;
6454
6491
  }
6455
6492
  }
6456
6493
  const siblings = concepts.slice(lastTotalIdx + 1, i2);
6457
6494
  if (siblings.length > 0) {
6458
- const siblingIds = siblings.map((s2) => s2.id);
6459
- this._totalChildrenMap.set(concept.id, siblingIds);
6460
- this._totalConceptMap.set(concept.id, concept);
6461
- this._totalChildConceptsMap.set(concept.id, siblings);
6462
- for (const siblingId of siblingIds) {
6463
- this._memberParentMap.set(siblingId, concept.id);
6464
- }
6495
+ this._registerTotalGroup(concept, siblings);
6496
+ } else if (lastTotalIdx >= 0) {
6497
+ this._registerTotalGroup(concept, [concepts[lastTotalIdx]]);
6465
6498
  }
6466
6499
  }
6467
6500
  }
6468
- if ((_d = concept.children) == null ? void 0 : _d.length) {
6501
+ if ((_b = concept.children) == null ? void 0 : _b.length) {
6469
6502
  this._traverseTotalGroups(concept.children);
6470
6503
  }
6471
6504
  }
6472
6505
  }
6473
6506
  _runCalculationCheck(conceptId, columnId) {
6507
+ const totalIds = /* @__PURE__ */ new Set();
6508
+ if (this._totalConceptMap.has(conceptId))
6509
+ totalIds.add(conceptId);
6510
+ const parentTotalId = this._memberParentMap.get(conceptId);
6511
+ if (parentTotalId)
6512
+ totalIds.add(parentTotalId);
6513
+ for (const totalConceptId of totalIds) {
6514
+ this._evaluateTotal(totalConceptId, columnId);
6515
+ }
6516
+ }
6517
+ _evaluateTotal(totalConceptId, columnId) {
6474
6518
  var _a;
6475
- const isSelfTotal = this._totalConceptMap.has(conceptId);
6476
- const totalConceptId = isSelfTotal ? conceptId : this._memberParentMap.get(conceptId);
6477
- if (!totalConceptId)
6478
- return;
6479
6519
  const totalConcept = this._totalConceptMap.get(totalConceptId);
6480
6520
  const totalRaw = (_a = this.formData[totalConceptId]) == null ? void 0 : _a[columnId];
6481
6521
  const key = `${totalConceptId}__${columnId}`;
@@ -10744,6 +10784,38 @@ let JupiterDynamicForm = class extends LitElement {
10744
10784
  }
10745
10785
  return `${startDate} / ${endDate}`;
10746
10786
  }
10787
+ /**
10788
+ * JDF-040: Resolves the instant date for a submission entry (Save Draft/Submit).
10789
+ * periodStartLabel-effective concepts (own-labelled or inherited, JDF-039) always
10790
+ * resolve via _resolveInstantDate — the same call the self-heal itself makes — instead
10791
+ * of trusting whatever happens to be cached in _periodData/field.periodInstantDate.
10792
+ * This matters because self-heal only ever corrects an EXISTING _periodData override
10793
+ * (it never fabricates one), so a cell that was never manually period-edited would
10794
+ * otherwise fall through to column.periodEndDate here and silently re-emit the wrong
10795
+ * date on every Save Draft/Submit even though field.periodInstantDate was healed.
10796
+ * Everything else keeps the previous fallback chain, but with field.periodInstantDate
10797
+ * now outranking column.periodEndDate, matching the already-correct sibling
10798
+ * implementations in _findFactValueForField and _addConceptDataToSubmission.
10799
+ */
10800
+ _resolveSubmissionInstantDate(concept, field2, fieldPeriodData, column2) {
10801
+ const effectiveRole = this._getEffectivePeriodRole(concept);
10802
+ if (effectiveRole == null ? void 0 : effectiveRole.includes("periodStartLabel")) {
10803
+ const periodStartDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.startDate) || (column2 == null ? void 0 : column2.periodStartDate) || field2.periodStartDate || this.periodStartDate;
10804
+ const periodEndDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.endDate) || (column2 == null ? void 0 : column2.periodEndDate) || field2.periodEndDate || this.periodEndDate;
10805
+ return this._resolveInstantDate(effectiveRole, void 0, periodStartDate, periodEndDate);
10806
+ }
10807
+ return (fieldPeriodData == null ? void 0 : fieldPeriodData.instantDate) || (fieldPeriodData == null ? void 0 : fieldPeriodData.endDate) || field2.periodInstantDate || (column2 == null ? void 0 : column2.periodEndDate) || field2.periodEndDate || field2.periodStartDate || this.periodStartDate;
10808
+ }
10809
+ /**
10810
+ * JDF-039: The label value to use for periodStartLabel/periodEndLabel checks — the
10811
+ * concept's own preferredLabel if it carries one of those roles, otherwise the role
10812
+ * inherited from the nearest ancestor that does (stamped by XBRLFormBuilder as
10813
+ * effectivePeriodRole). Falls back to preferredLabel for any concept built before this
10814
+ * fix (or otherwise missing the field).
10815
+ */
10816
+ _getEffectivePeriodRole(concept) {
10817
+ return concept.effectivePeriodRole ?? concept.preferredLabel;
10818
+ }
10747
10819
  _resolveInstantDate(preferredLabel, instantDate, startDate, endDate) {
10748
10820
  if (preferredLabel == null ? void 0 : preferredLabel.includes("periodStartLabel")) {
10749
10821
  const base = startDate || "2025-01-01";
@@ -10773,7 +10845,7 @@ let JupiterDynamicForm = class extends LitElement {
10773
10845
  _correctPeriodStartInstantDatesInConcepts(concepts, section2) {
10774
10846
  concepts.forEach((concept) => {
10775
10847
  var _a, _b;
10776
- if (((_a = concept.preferredLabel) == null ? void 0 : _a.includes("periodStartLabel")) && concept.periodType === "instant" && ((_b = concept.fields) == null ? void 0 : _b.length)) {
10848
+ if (((_a = this._getEffectivePeriodRole(concept)) == null ? void 0 : _a.includes("periodStartLabel")) && concept.periodType === "instant" && ((_b = concept.fields) == null ? void 0 : _b.length)) {
10777
10849
  concept.fields.forEach((field2) => {
10778
10850
  this._correctFieldPeriodStartInstantDate(concept, field2, concept.id, section2);
10779
10851
  const repeatCount = this._repeatCounts[concept.id] || 0;
@@ -10793,7 +10865,7 @@ let JupiterDynamicForm = class extends LitElement {
10793
10865
  const fieldPeriodData = (_a = this._periodData[periodDataKey]) == null ? void 0 : _a[field2.columnId];
10794
10866
  const periodStartDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.startDate) || (column2 == null ? void 0 : column2.periodStartDate) || field2.periodStartDate || this.periodStartDate;
10795
10867
  const periodEndDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.endDate) || (column2 == null ? void 0 : column2.periodEndDate) || field2.periodEndDate || this.periodEndDate;
10796
- const correctedInstantDate = this._resolveInstantDate(concept.preferredLabel, void 0, periodStartDate, periodEndDate);
10868
+ const correctedInstantDate = this._resolveInstantDate(this._getEffectivePeriodRole(concept), void 0, periodStartDate, periodEndDate);
10797
10869
  if (fieldPeriodData && fieldPeriodData.instantDate !== correctedInstantDate) {
10798
10870
  console.log(`🩹 [Legacy Period Correction] ${periodDataKey}/${field2.columnId}: instantDate ${fieldPeriodData.instantDate} → ${correctedInstantDate}`);
10799
10871
  fieldPeriodData.instantDate = correctedInstantDate;
@@ -11003,7 +11075,7 @@ let JupiterDynamicForm = class extends LitElement {
11003
11075
  let field2;
11004
11076
  const colStartDate = request.periodType === "instant" ? request.instantDate : request.startDate;
11005
11077
  const colEndDate = request.periodType === "instant" ? request.instantDate : request.endDate;
11006
- const instantDate = this._resolveInstantDate(concept.preferredLabel, request.instantDate, colStartDate, colEndDate);
11078
+ const instantDate = this._resolveInstantDate(this._getEffectivePeriodRole(concept), request.instantDate, colStartDate, colEndDate);
11007
11079
  if (existingField) {
11008
11080
  field2 = {
11009
11081
  id: `${concept.id}_${columnId}`,
@@ -11719,7 +11791,7 @@ let JupiterDynamicForm = class extends LitElement {
11719
11791
  // CRITICAL: Include conceptType for XBRL validation
11720
11792
  periodStartDate: column2.periodStartDate,
11721
11793
  periodEndDate: column2.periodEndDate,
11722
- periodInstantDate: conceptPeriodType === "instant" ? column2.periodEndDate || column2.periodStartDate : void 0
11794
+ periodInstantDate: conceptPeriodType === "instant" ? this._resolveInstantDate(this._getEffectivePeriodRole(concept), void 0, column2.periodStartDate, column2.periodEndDate) : void 0
11723
11795
  };
11724
11796
  if ((_a = this._periodData[concept.id]) == null ? void 0 : _a[column2.id]) {
11725
11797
  const customPeriod = this._periodData[concept.id][column2.id];
@@ -12051,7 +12123,7 @@ let JupiterDynamicForm = class extends LitElement {
12051
12123
  }
12052
12124
  };
12053
12125
  if (concept.periodType === "instant") {
12054
- const instantDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.instantDate) || (fieldPeriodData == null ? void 0 : fieldPeriodData.endDate) || (column2 == null ? void 0 : column2.periodEndDate) || field2.periodInstantDate || field2.periodEndDate || field2.periodStartDate || this.periodStartDate;
12126
+ const instantDate = this._resolveSubmissionInstantDate(concept, field2, fieldPeriodData, column2);
12055
12127
  submissionEntry.period.date = instantDate;
12056
12128
  } else {
12057
12129
  const startDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.startDate) || (column2 == null ? void 0 : column2.periodStartDate) || field2.periodStartDate || this.periodStartDate;
@@ -12167,7 +12239,7 @@ let JupiterDynamicForm = class extends LitElement {
12167
12239
  }
12168
12240
  };
12169
12241
  if (concept.periodType === "instant") {
12170
- instanceEntry.period.date = (fieldPeriodData == null ? void 0 : fieldPeriodData.instantDate) || (fieldPeriodData == null ? void 0 : fieldPeriodData.endDate) || (column2 == null ? void 0 : column2.periodEndDate) || field2.periodInstantDate || field2.periodEndDate || field2.periodStartDate || this.periodStartDate;
12242
+ instanceEntry.period.date = this._resolveSubmissionInstantDate(concept, field2, fieldPeriodData, column2);
12171
12243
  } else {
12172
12244
  instanceEntry.period.startDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.startDate) || (column2 == null ? void 0 : column2.periodStartDate) || field2.periodStartDate || this.periodStartDate;
12173
12245
  instanceEntry.period.endDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.endDate) || (column2 == null ? void 0 : column2.periodEndDate) || field2.periodEndDate || this.periodEndDate;