jupiter-dynamic-forms 1.22.3 → 1.22.4

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
@@ -1857,6 +1857,16 @@ const typedDimensionWarning$1 = {
1857
1857
  "continue": "Continue to validate",
1858
1858
  cancel: "Cancel"
1859
1859
  };
1860
+ const validateWarningPopup$1 = {
1861
+ title: "Validation warnings"
1862
+ };
1863
+ const duplicateFactWarning$1 = {
1864
+ title: "Duplicate facts with different values",
1865
+ message: "The same concept, period and dimensions were found with different values entered in different roles. Only one value can be correct — click a fact below to review and fix it.",
1866
+ role: "Role:",
1867
+ value: "Value:",
1868
+ clickToFocus: "Click to go to this fact"
1869
+ };
1860
1870
  const calculationWarning$1 = {
1861
1871
  title: "Calculation warning",
1862
1872
  summaryLine: 'Total "{{label}}" should equal {{calculated}}, but you entered {{entered}}.',
@@ -1877,7 +1887,8 @@ const formulaValidation$1 = {
1877
1887
  summaryCountBreakdown: "{{warnings}} warnings, {{errors}} errors",
1878
1888
  severityWarning: "Warning",
1879
1889
  severityError: "Error",
1880
- close: "Close"
1890
+ close: "Close",
1891
+ breakdownLabel: "Values checked:"
1881
1892
  };
1882
1893
  const scrollToConcept$1 = {
1883
1894
  revealRole: {
@@ -1899,6 +1910,8 @@ const enTranslations = {
1899
1910
  xbrlValidation: xbrlValidation$1,
1900
1911
  error: error$1,
1901
1912
  typedDimensionWarning: typedDimensionWarning$1,
1913
+ validateWarningPopup: validateWarningPopup$1,
1914
+ duplicateFactWarning: duplicateFactWarning$1,
1902
1915
  calculationWarning: calculationWarning$1,
1903
1916
  balanceCheck: balanceCheck$1,
1904
1917
  formulaValidation: formulaValidation$1,
@@ -2104,6 +2117,16 @@ const typedDimensionWarning = {
2104
2117
  "continue": "Doorgaan met valideren",
2105
2118
  cancel: "Annuleren"
2106
2119
  };
2120
+ const validateWarningPopup = {
2121
+ title: "Validatiewaarschuwingen"
2122
+ };
2123
+ const duplicateFactWarning = {
2124
+ title: "Dubbele feiten met verschillende waarden",
2125
+ message: "Hetzelfde concept, dezelfde periode en dimensies zijn gevonden met verschillende waarden in verschillende rollen. Slechts één waarde kan juist zijn — klik op een feit hieronder om het te controleren en te corrigeren.",
2126
+ role: "Rol:",
2127
+ value: "Waarde:",
2128
+ clickToFocus: "Klik om naar dit feit te gaan"
2129
+ };
2107
2130
  const calculationWarning = {
2108
2131
  title: "Berekeningswaarschuwing",
2109
2132
  summaryLine: 'Totaal "{{label}}" zou {{calculated}} moeten zijn, maar u heeft {{entered}} ingevoerd.',
@@ -2124,7 +2147,8 @@ const formulaValidation = {
2124
2147
  summaryCountBreakdown: "{{warnings}} waarschuwingen, {{errors}} fouten",
2125
2148
  severityWarning: "Waarschuwing",
2126
2149
  severityError: "Fout",
2127
- close: "Sluiten"
2150
+ close: "Sluiten",
2151
+ breakdownLabel: "Gecontroleerde waarden:"
2128
2152
  };
2129
2153
  const scrollToConcept = {
2130
2154
  revealRole: {
@@ -2146,6 +2170,8 @@ const nlTranslations = {
2146
2170
  xbrlValidation,
2147
2171
  error,
2148
2172
  typedDimensionWarning,
2173
+ validateWarningPopup,
2174
+ duplicateFactWarning,
2149
2175
  calculationWarning,
2150
2176
  balanceCheck,
2151
2177
  formulaValidation,
@@ -2528,6 +2554,19 @@ class Parser {
2528
2554
  }
2529
2555
  parseBoolTerm() {
2530
2556
  const token = this.peek();
2557
+ if (token.type === "LPAREN") {
2558
+ const savedPos = this.pos;
2559
+ try {
2560
+ this.next();
2561
+ const inner = this.parseOrExpr();
2562
+ this.expect("RPAREN");
2563
+ return inner;
2564
+ } catch (error2) {
2565
+ if (!(error2 instanceof FormulaExpressionError))
2566
+ throw error2;
2567
+ this.pos = savedPos;
2568
+ }
2569
+ }
2531
2570
  if (token.type === "IDENT" && token.value === "not") {
2532
2571
  this.next();
2533
2572
  this.expect("LPAREN");
@@ -2988,8 +3027,8 @@ function collectDescendantConceptIds(childrenIndex, rootConceptId) {
2988
3027
  function candidateKey(candidate) {
2989
3028
  return `${candidate.conceptId}::${candidate.columnId}`;
2990
3029
  }
2991
- function resolveMatchedCandidates(variable, ctx) {
2992
- return intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, ctx.allCandidates, ctx, variable.filters)));
3030
+ function resolveMatchedCandidates(variable, ctx, roleURI) {
3031
+ return intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, ctx.allCandidates, ctx, variable.filters, roleURI)));
2993
3032
  }
2994
3033
  function getColumn(ctx, candidate) {
2995
3034
  var _a;
@@ -3103,7 +3142,7 @@ function resolveCrossRoleChildren(anchorQName, ownLinkrole, ctx) {
3103
3142
  const otherRoles = Array.from(candidatesByRole.entries()).filter(([roleURI]) => roleURI !== ownLinkrole);
3104
3143
  return otherRoles.length === 1 ? otherRoles[0][1] : [];
3105
3144
  }
3106
- function matchFilter(filter2, universe, ctx, siblingFilters) {
3145
+ function matchFilter(filter2, universe, ctx, siblingFilters, roleURI) {
3107
3146
  var _a, _b;
3108
3147
  switch (filter2.type) {
3109
3148
  case "CONCEPT_NAME": {
@@ -3111,7 +3150,9 @@ function matchFilter(filter2, universe, ctx, siblingFilters) {
3111
3150
  const infos = qname ? ctx.conceptByQName.get(qname) : void 0;
3112
3151
  if (!infos || infos.length === 0)
3113
3152
  return [];
3114
- const conceptIds = new Set(infos.map((info) => info.conceptId));
3153
+ const ownRoleInfos = roleURI ? infos.filter((info) => ctx.roleURIByConceptId.get(info.conceptId) === roleURI) : [];
3154
+ const scopedInfos = ownRoleInfos.length > 0 ? ownRoleInfos : infos;
3155
+ const conceptIds = new Set(scopedInfos.map((info) => info.conceptId));
3115
3156
  return universe.filter((candidate) => conceptIds.has(candidate.conceptId));
3116
3157
  }
3117
3158
  case "CONCEPT_BALANCE": {
@@ -3171,7 +3212,7 @@ function matchFilter(filter2, universe, ctx, siblingFilters) {
3171
3212
  return universe.filter((candidate) => columnMatchesPeriod(getColumn(ctx, candidate), dateAttribute, ctx));
3172
3213
  }
3173
3214
  case "OR_FILTER":
3174
- return unionCandidates(filter2.children.map((child) => resolveFilter(child, universe, ctx, siblingFilters)));
3215
+ return unionCandidates(filter2.children.map((child) => resolveFilter(child, universe, ctx, siblingFilters, roleURI)));
3175
3216
  case "ASPECT_COVER":
3176
3217
  return universe;
3177
3218
  case "UNKNOWN": {
@@ -3189,8 +3230,8 @@ function matchFilter(filter2, universe, ctx, siblingFilters) {
3189
3230
  return [];
3190
3231
  }
3191
3232
  }
3192
- function resolveFilter(filter2, universe, ctx, siblingFilters) {
3193
- const matched = matchFilter(filter2, universe, ctx, siblingFilters);
3233
+ function resolveFilter(filter2, universe, ctx, siblingFilters, roleURI) {
3234
+ const matched = matchFilter(filter2, universe, ctx, siblingFilters, roleURI);
3194
3235
  if (!filter2.complement)
3195
3236
  return matched;
3196
3237
  const matchedKeys = new Set(matched.map(candidateKey));
@@ -3220,12 +3261,9 @@ function hasExplicitPeriodFilter(filters) {
3220
3261
  });
3221
3262
  }
3222
3263
  function narrowToCurrentPeriod(candidates, variable, ctx) {
3223
- if (candidates.length <= 1 || hasExplicitPeriodFilter(variable.filters))
3264
+ if (candidates.length === 0 || hasExplicitPeriodFilter(variable.filters))
3224
3265
  return candidates;
3225
- const currentPeriodOnly = candidates.filter(
3226
- (candidate) => columnEffectivePeriod(getColumn(ctx, candidate), ctx).end === ctx.periodEndDate
3227
- );
3228
- return currentPeriodOnly.length > 0 ? currentPeriodOnly : candidates;
3266
+ return candidates.filter((candidate) => columnEffectivePeriod(getColumn(ctx, candidate), ctx).end === ctx.periodEndDate);
3229
3267
  }
3230
3268
  function narrowByPreferredOccurrence(candidates, ctx) {
3231
3269
  const groups = /* @__PURE__ */ new Map();
@@ -3272,9 +3310,13 @@ function narrowToDirectChildOfDescendantAnchor(candidates, variable, ctx) {
3272
3310
  return candidates;
3273
3311
  return candidates.filter((candidate) => directChildren.has(candidate.conceptId));
3274
3312
  }
3275
- function resolveNarrowedCandidates(variable, ctx) {
3313
+ function resolveNarrowedCandidates(variable, ctx, roleURI) {
3276
3314
  return narrowByPreferredOccurrence(
3277
- narrowToDirectChildOfDescendantAnchor(narrowToCurrentPeriod(resolveMatchedCandidates(variable, ctx), variable, ctx), variable, ctx),
3315
+ narrowToDirectChildOfDescendantAnchor(
3316
+ narrowToCurrentPeriod(resolveMatchedCandidates(variable, ctx, roleURI), variable, ctx),
3317
+ variable,
3318
+ ctx
3319
+ ),
3278
3320
  ctx
3279
3321
  );
3280
3322
  }
@@ -3339,9 +3381,9 @@ function findUncoveredAxisAnchor(variables, axisId, candidatesByName, ctx) {
3339
3381
  }
3340
3382
  return void 0;
3341
3383
  }
3342
- function resolveAssertionVariableBindings(variables, ctx, implicitFiltering) {
3384
+ function computeAssertionNarrowedCandidatesByName(variables, ctx, implicitFiltering, roleURI) {
3343
3385
  const candidatesByName = /* @__PURE__ */ new Map();
3344
- variables.forEach((variable) => candidatesByName.set(variable.name, resolveNarrowedCandidates(variable, ctx)));
3386
+ variables.forEach((variable) => candidatesByName.set(variable.name, resolveNarrowedCandidates(variable, ctx, roleURI)));
3345
3387
  if (implicitFiltering) {
3346
3388
  const axisIds = /* @__PURE__ */ new Set();
3347
3389
  variables.forEach((variable) => uncoveredExplicitDimensionAxisIds(variable, ctx).forEach((axisId) => axisIds.add(axisId)));
@@ -3362,10 +3404,35 @@ function resolveAssertionVariableBindings(variables, ctx, implicitFiltering) {
3362
3404
  });
3363
3405
  });
3364
3406
  }
3407
+ return candidatesByName;
3408
+ }
3409
+ function resolveAssertionVariableBindings(variables, ctx, implicitFiltering, roleURI) {
3410
+ const candidatesByName = computeAssertionNarrowedCandidatesByName(variables, ctx, implicitFiltering, roleURI);
3365
3411
  const bindings = /* @__PURE__ */ new Map();
3366
3412
  variables.forEach((variable) => bindings.set(variable.name, valuesFromCandidates(variable, candidatesByName.get(variable.name) || [], ctx)));
3367
3413
  return bindings;
3368
3414
  }
3415
+ function buildFormulaVariableBreakdown(variables, ctx, implicitFiltering, roleURI) {
3416
+ const sequenceVariables = variables.filter((variable) => variable.bindAsSequence);
3417
+ if (sequenceVariables.length === 0)
3418
+ return void 0;
3419
+ const candidatesByName = computeAssertionNarrowedCandidatesByName(variables, ctx, implicitFiltering, roleURI);
3420
+ const breakdown = [];
3421
+ for (const variable of sequenceVariables) {
3422
+ const candidates = (candidatesByName.get(variable.name) || []).map((candidate) => {
3423
+ var _a, _b;
3424
+ return {
3425
+ conceptId: candidate.conceptId,
3426
+ conceptQName: (_a = ctx.conceptById.get(candidate.conceptId)) == null ? void 0 : _a.qname,
3427
+ columnId: candidate.columnId,
3428
+ value: resolveRawValue((_b = ctx.formData[candidate.conceptId]) == null ? void 0 : _b[candidate.columnId])
3429
+ };
3430
+ }).filter((candidate) => candidate.value !== void 0 && candidate.conceptQName !== void 0);
3431
+ if (candidates.length > 0)
3432
+ breakdown.push({ variableName: variable.name, candidates });
3433
+ }
3434
+ return breakdown.length > 0 ? breakdown : void 0;
3435
+ }
3369
3436
  function filterSignature(filter2) {
3370
3437
  const attrs = Object.entries(filter2.attributes).filter(([key]) => key !== "id").sort(([a2], [b2]) => a2.localeCompare(b2));
3371
3438
  return JSON.stringify({ type: filter2.type, complement: filter2.complement, cover: filter2.cover, attrs });
@@ -3520,8 +3587,8 @@ function resolveVariableColumnsForConcept(variable, targetConceptId, ctx) {
3520
3587
  const resolved = intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, universe, ctx, variable.filters)));
3521
3588
  return resolved.map((candidate) => candidate.columnId);
3522
3589
  }
3523
- function buildAssertionBindings(assertion, ctx) {
3524
- return resolveAssertionVariableBindings(assertion.variables, ctx, assertion.implicitFiltering);
3590
+ function buildAssertionBindings(assertion, ctx, roleURI) {
3591
+ return resolveAssertionVariableBindings(assertion.variables, ctx, assertion.implicitFiltering, roleURI);
3525
3592
  }
3526
3593
  function buildAssertionFallbackUsage(assertion, ctx) {
3527
3594
  var _a;
@@ -3613,7 +3680,7 @@ class FormulaValidationService {
3613
3680
  }
3614
3681
  _evaluateAssertion(assertion, role, ctx, language) {
3615
3682
  try {
3616
- const bindings = buildAssertionBindings(assertion, ctx);
3683
+ const bindings = buildAssertionBindings(assertion, ctx, role.roleURI);
3617
3684
  const sumOfMembersShape = detectSumOfMembersOnAbstractShape(assertion);
3618
3685
  const sumOfMembersResult = sumOfMembersShape ? resolveSumOfMembersOnAbstractSatisfaction(sumOfMembersShape, ctx) : void 0;
3619
3686
  const usesSumOfMembersPath = sumOfMembersResult !== void 0 && sumOfMembersResult !== "not-applicable";
@@ -3640,7 +3707,8 @@ class FormulaValidationService {
3640
3707
  return this._evaluatedResult(assertion, role, ctx, sumOfMembersResult, language);
3641
3708
  }
3642
3709
  const satisfied = evaluateFormulaTest(assertion.test, bindings, fallbackUsage);
3643
- return this._evaluatedResult(assertion, role, ctx, satisfied, language);
3710
+ const variableBreakdown = satisfied ? void 0 : buildFormulaVariableBreakdown(assertion.variables, ctx, assertion.implicitFiltering, role.roleURI);
3711
+ return this._evaluatedResult(assertion, role, ctx, satisfied, language, void 0, variableBreakdown);
3644
3712
  } catch (error2) {
3645
3713
  if (error2 instanceof FormulaExpressionError) {
3646
3714
  console.warn(`[FormulaValidationService] Assertion "${assertion.id}" could not be evaluated (${error2.reason}): ${error2.message}`);
@@ -3661,7 +3729,7 @@ class FormulaValidationService {
3661
3729
  skipReason
3662
3730
  };
3663
3731
  }
3664
- _evaluatedResult(assertion, role, ctx, satisfied, language, matchedFactCount) {
3732
+ _evaluatedResult(assertion, role, ctx, satisfied, language, matchedFactCount, variableBreakdown) {
3665
3733
  const message = satisfied ? void 0 : this._pickMessage(assertion, language);
3666
3734
  return {
3667
3735
  assertionId: assertion.id,
@@ -3673,7 +3741,8 @@ class FormulaValidationService {
3673
3741
  skipped: false,
3674
3742
  message,
3675
3743
  matchedFactCount,
3676
- conceptLinks: message ? this._buildConceptLinks(assertion, role.roleURI, ctx, message) : void 0
3744
+ conceptLinks: message ? this._buildConceptLinks(assertion, role.roleURI, ctx, message) : void 0,
3745
+ variableBreakdown
3677
3746
  };
3678
3747
  }
3679
3748
  _pickMessage(assertion, language) {
@@ -12233,6 +12302,44 @@ let JupiterFormulaValidationDialog = class extends LitElement {
12233
12302
  composed: true
12234
12303
  }));
12235
12304
  }
12305
+ // JDF-129: same navigate-and-close flow as `_handleConceptLinkClick`, for a breakdown line
12306
+ // rather than a message's backtick-quoted concept mention.
12307
+ _handleBreakdownCandidateClick(candidate) {
12308
+ this.dispatchEvent(new CustomEvent("formula-concept-click", {
12309
+ detail: { conceptId: candidate.conceptId, conceptQName: candidate.conceptQName, columnId: candidate.columnId },
12310
+ bubbles: true,
12311
+ composed: true
12312
+ }));
12313
+ }
12314
+ // JDF-129: local (non-prefixed) name for a breakdown candidate's concept — same derivation as
12315
+ // `_buildConceptLinks`'s own label in FormulaValidationService.
12316
+ _breakdownCandidateLabel(candidate) {
12317
+ return candidate.conceptQName.includes(":") ? candidate.conceptQName.split(":").pop() : candidate.conceptQName;
12318
+ }
12319
+ // JDF-129: plain, unflagged per-candidate breakdown for a bindAsSequence:true variable — the
12320
+ // real facts an aggregate sum/comparison summed, so a user can see one corrected line reflected
12321
+ // even though the assertion as a whole (a pure aggregate check) is still failing. Deliberately no
12322
+ // per-line pass/fail — see the ticket's own "Diff behavior" scoping decision.
12323
+ _renderBreakdown(result) {
12324
+ const breakdown = result.variableBreakdown;
12325
+ if (!breakdown || breakdown.length === 0)
12326
+ return "";
12327
+ return html`
12328
+ <div class="assertion-breakdown">
12329
+ <span class="assertion-breakdown-label">${I18n.t("formulaValidation.breakdownLabel")}</span>
12330
+ <ul class="assertion-breakdown-list">
12331
+ ${breakdown.flatMap((variable) => variable.candidates.map((candidate) => html`
12332
+ <li class="assertion-breakdown-item">
12333
+ <button type="button" class="concept-link" @click="${() => this._handleBreakdownCandidateClick(candidate)}">
12334
+ ${this._breakdownCandidateLabel(candidate)}
12335
+ </button>
12336
+ <span class="assertion-breakdown-value">${candidate.value}</span>
12337
+ </li>
12338
+ `))}
12339
+ </ul>
12340
+ </div>
12341
+ `;
12342
+ }
12236
12343
  // Splits a message on its backtick-quoted concept mentions and renders the ones this
12237
12344
  // assertion resolved a fact location for (`result.conceptLinks`) as click-to-focus buttons;
12238
12345
  // any other backtick mention (e.g. an enumerated value like `Na`, not a concept) renders as
@@ -12299,7 +12406,10 @@ let JupiterFormulaValidationDialog = class extends LitElement {
12299
12406
  <span class="severity-badge ${result.severity === "ERROR" ? "error" : "warning"}">
12300
12407
  ${result.severity === "ERROR" ? I18n.t("formulaValidation.severityError") : I18n.t("formulaValidation.severityWarning")}
12301
12408
  </span>
12302
- <span class="assertion-message">${this._renderMessage(result)}</span>
12409
+ <div class="assertion-row-body">
12410
+ <span class="assertion-message">${this._renderMessage(result)}</span>
12411
+ ${this._renderBreakdown(result)}
12412
+ </div>
12303
12413
  </div>
12304
12414
  `)}
12305
12415
  </div>
@@ -12488,12 +12598,53 @@ JupiterFormulaValidationDialog.styles = css`
12488
12598
  background: var(--jupiter-error-color, #d32f2f);
12489
12599
  }
12490
12600
 
12601
+ .assertion-row-body {
12602
+ display: flex;
12603
+ flex-direction: column;
12604
+ gap: 4px;
12605
+ min-width: 0;
12606
+ }
12607
+
12491
12608
  .assertion-message {
12492
12609
  font-size: 13px;
12493
12610
  color: var(--primaryTextColor, var(--jupiter-text-primary, #333));
12494
12611
  line-height: 1.4;
12495
12612
  }
12496
12613
 
12614
+ .assertion-breakdown {
12615
+ display: flex;
12616
+ flex-direction: column;
12617
+ gap: 2px;
12618
+ }
12619
+
12620
+ .assertion-breakdown-label {
12621
+ font-size: 11px;
12622
+ font-weight: 600;
12623
+ color: var(--jupiter-text-secondary, #666);
12624
+ }
12625
+
12626
+ .assertion-breakdown-list {
12627
+ list-style: none;
12628
+ margin: 0;
12629
+ padding: 0;
12630
+ display: flex;
12631
+ flex-direction: column;
12632
+ gap: 2px;
12633
+ }
12634
+
12635
+ .assertion-breakdown-item {
12636
+ display: flex;
12637
+ align-items: baseline;
12638
+ justify-content: space-between;
12639
+ gap: 8px;
12640
+ font-size: 12px;
12641
+ }
12642
+
12643
+ .assertion-breakdown-value {
12644
+ color: var(--jupiter-text-secondary, #666);
12645
+ white-space: nowrap;
12646
+ }
12647
+
12497
12648
  .concept-link {
12498
12649
  font: inherit;
12499
12650
  font-weight: 600;
@@ -12785,8 +12936,9 @@ let JupiterDynamicForm = class extends LitElement {
12785
12936
  this._xbrlFormErrors = [];
12786
12937
  this._calculationWarnings = /* @__PURE__ */ new Map();
12787
12938
  this._showErrorPopup = false;
12788
- this._showTypedDimensionWarning = false;
12939
+ this._showValidateWarningPopup = false;
12789
12940
  this._typedDimensionWarningFacts = [];
12941
+ this._duplicateFactMismatches = [];
12790
12942
  this._submitDisabled = false;
12791
12943
  this._allSections = [];
12792
12944
  this._selectedRoleIds = [];
@@ -14354,7 +14506,12 @@ let JupiterDynamicForm = class extends LitElement {
14354
14506
  await this._focusFormField(conceptId, columnId, sectionId);
14355
14507
  }
14356
14508
  async _handleTypedDimensionFactClick(conceptId, columnId, sectionId) {
14357
- this._showTypedDimensionWarning = false;
14509
+ this._showValidateWarningPopup = false;
14510
+ this.requestUpdate();
14511
+ await this._focusFormField(conceptId, columnId, sectionId);
14512
+ }
14513
+ async _handleDuplicateFactClick(conceptId, columnId, sectionId) {
14514
+ this._showValidateWarningPopup = false;
14358
14515
  this.requestUpdate();
14359
14516
  await this._focusFormField(conceptId, columnId, sectionId);
14360
14517
  }
@@ -15047,6 +15204,137 @@ let JupiterDynamicForm = class extends LitElement {
15047
15204
  }
15048
15205
  return groups;
15049
15206
  }
15207
+ /**
15208
+ * Validate pre-flight: finds entered facts that share their concept, period and dimensions
15209
+ * (i.e. would resolve to the same XBRL context) with at least one other entered fact — typically
15210
+ * because the same concept is presented in more than one disclosure role — but were given
15211
+ * different values. E.g. `IncomeTaxExpense` entered as 992 in one note and -992 in another for
15212
+ * the same year: only one can be correct, so this is a genuine conflict rather than a benign
15213
+ * duplicate (an identical value entered twice in two roles is left alone — see
15214
+ * `_normalizeFactValueForCompare` below). Walks every concept's own `fields` (not every
15215
+ * column x every concept) to stay cheap on large taxonomies.
15216
+ */
15217
+ _findDuplicateFactMismatches() {
15218
+ const mismatches = [];
15219
+ if (!this._currentSchema)
15220
+ return mismatches;
15221
+ const groups = /* @__PURE__ */ new Map();
15222
+ for (const section2 of this._currentSchema.sections) {
15223
+ const columnsById = new Map((section2.columns || []).map((col) => [col.id, col]));
15224
+ this._collectDuplicateFactCandidates(section2.concepts, columnsById, section2, groups);
15225
+ }
15226
+ groups.forEach((group) => {
15227
+ if (group.entries.length < 2)
15228
+ return;
15229
+ const distinctValues = new Set(group.entries.map((e2) => this._normalizeFactValueForCompare(e2.value)));
15230
+ if (distinctValues.size < 2)
15231
+ return;
15232
+ mismatches.push({
15233
+ conceptId: group.conceptId,
15234
+ conceptLabel: group.conceptLabel,
15235
+ facts: group.entries
15236
+ });
15237
+ });
15238
+ return mismatches;
15239
+ }
15240
+ _collectDuplicateFactCandidates(concepts, columnsById, section2, groups) {
15241
+ var _a;
15242
+ for (const concept of concepts) {
15243
+ const conceptValues = this._formData[concept.id];
15244
+ if (conceptValues) {
15245
+ for (const field2 of concept.fields) {
15246
+ const value = conceptValues[field2.columnId];
15247
+ if (value === void 0 || value === null || String(value).trim() === "")
15248
+ continue;
15249
+ const column2 = columnsById.get(field2.columnId);
15250
+ if (!column2)
15251
+ continue;
15252
+ const contextKey = this._buildFactContextKey(concept, field2, column2, section2);
15253
+ if (!contextKey)
15254
+ continue;
15255
+ const entry = {
15256
+ conceptId: concept.id,
15257
+ conceptLabel: concept.label,
15258
+ columnId: column2.id,
15259
+ columnTitle: column2.title,
15260
+ sectionId: section2.id,
15261
+ sectionTitle: section2.title,
15262
+ value
15263
+ };
15264
+ let group = groups.get(contextKey);
15265
+ if (!group) {
15266
+ group = { conceptId: concept.originalConceptId || concept.id, conceptLabel: concept.label, entries: [] };
15267
+ groups.set(contextKey, group);
15268
+ }
15269
+ group.entries.push(entry);
15270
+ }
15271
+ }
15272
+ if ((_a = concept.children) == null ? void 0 : _a.length) {
15273
+ this._collectDuplicateFactCandidates(concept.children, columnsById, section2, groups);
15274
+ }
15275
+ }
15276
+ }
15277
+ /**
15278
+ * Canonical key for the XBRL context (concept + period + dimensions) `concept`'s value in
15279
+ * `column` would resolve to, independent of which role produced it. Returns null when a typed
15280
+ * dimension on this column has no value entered yet, since its true context can't be
15281
+ * determined without one (that gap is already surfaced by the typed-dimension warning).
15282
+ */
15283
+ _buildFactContextKey(concept, field2, column2, section2) {
15284
+ var _a, _b, _c;
15285
+ const conceptKey = concept.originalConceptId || concept.id;
15286
+ const isInstant = concept.periodType === "instant";
15287
+ const fieldPeriodData = (_a = this._periodData[concept.id]) == null ? void 0 : _a[column2.id];
15288
+ const periodStartDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.startDate) || column2.periodStartDate || field2.periodStartDate || this.periodStartDate;
15289
+ const periodEndDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.endDate) || column2.periodEndDate || field2.periodEndDate || this.periodEndDate;
15290
+ const periodInstantDate = (fieldPeriodData == null ? void 0 : fieldPeriodData.instantDate) || field2.periodInstantDate || periodEndDate || periodStartDate;
15291
+ const periodKey = isInstant ? `instant:${periodInstantDate}` : `duration:${periodStartDate}_${periodEndDate}`;
15292
+ const dimensionData = column2.dimensionData;
15293
+ let dimensionKey = "base";
15294
+ if (dimensionData) {
15295
+ const typedMembers = dimensionData.typedMembers;
15296
+ if (typedMembers && typedMembers.length > 0) {
15297
+ const scoped = this._typedMemberData[this._scopedColumnKey(section2.id, column2.id)];
15298
+ const legacy = this._typedMemberData[column2.id];
15299
+ const typedParts = [];
15300
+ for (const tm of typedMembers) {
15301
+ const typedValue = (_b = (scoped == null ? void 0 : scoped[tm.axisId]) ?? (legacy == null ? void 0 : legacy[tm.axisId])) == null ? void 0 : _b.trim();
15302
+ if (!typedValue)
15303
+ return null;
15304
+ typedParts.push(`${tm.axisId}=${typedValue}`);
15305
+ }
15306
+ dimensionKey = typedParts.sort().join("|");
15307
+ } else if ((_c = dimensionData.combinations) == null ? void 0 : _c.length) {
15308
+ dimensionKey = dimensionData.combinations.map((c2) => `${c2.axisId}=${c2.memberId}`).sort().join("|");
15309
+ } else if (dimensionData.dimensionIdKey) {
15310
+ dimensionKey = dimensionData.dimensionIdKey;
15311
+ } else if (dimensionData.axisId && dimensionData.memberId) {
15312
+ dimensionKey = `${dimensionData.axisId}=${dimensionData.memberId}`;
15313
+ }
15314
+ }
15315
+ return `${conceptKey}||${periodKey}||${dimensionKey}`;
15316
+ }
15317
+ /** Numeric-aware value comparison for the duplicate-fact check — "992" and 992 (or "992.0") count as equal. */
15318
+ _normalizeFactValueForCompare(value) {
15319
+ if (typeof value === "number")
15320
+ return String(value);
15321
+ if (typeof value === "string") {
15322
+ const trimmed = value.trim();
15323
+ const asNumber = Number(trimmed);
15324
+ return trimmed !== "" && !isNaN(asNumber) ? String(asNumber) : trimmed;
15325
+ }
15326
+ return String(value);
15327
+ }
15328
+ /** Popup header for the Validate pre-flight warning: specific when only one check found something, generic when both did. */
15329
+ _validateWarningPopupTitle() {
15330
+ const hasTypedDimensionWarning = this._typedDimensionWarningFacts.length > 0;
15331
+ const hasDuplicateFactWarning = this._duplicateFactMismatches.length > 0;
15332
+ if (hasTypedDimensionWarning && hasDuplicateFactWarning)
15333
+ return I18n.t("validateWarningPopup.title");
15334
+ if (hasDuplicateFactWarning)
15335
+ return I18n.t("duplicateFactWarning.title");
15336
+ return I18n.t("typedDimensionWarning.title");
15337
+ }
15050
15338
  _handleSubmit() {
15051
15339
  console.log("📝 Form submission started...");
15052
15340
  this._submitted = true;
@@ -15058,10 +15346,13 @@ let JupiterDynamicForm = class extends LitElement {
15058
15346
  return;
15059
15347
  }
15060
15348
  const missingTypedDimensionFacts = this._findMissingTypedDimensionFacts();
15061
- if (missingTypedDimensionFacts.length > 0) {
15349
+ const duplicateFactMismatches = this._findDuplicateFactMismatches();
15350
+ if (missingTypedDimensionFacts.length > 0 || duplicateFactMismatches.length > 0) {
15062
15351
  console.log("⚠️ [Submit] Typed dimension values missing for entered facts:", missingTypedDimensionFacts);
15352
+ console.log("⚠️ [Submit] Duplicate facts with mismatched values:", duplicateFactMismatches);
15063
15353
  this._typedDimensionWarningFacts = missingTypedDimensionFacts;
15064
- this._showTypedDimensionWarning = true;
15354
+ this._duplicateFactMismatches = duplicateFactMismatches;
15355
+ this._showValidateWarningPopup = true;
15065
15356
  this._submitDisabled = false;
15066
15357
  this.requestUpdate();
15067
15358
  return;
@@ -15070,8 +15361,8 @@ let JupiterDynamicForm = class extends LitElement {
15070
15361
  }
15071
15362
  /**
15072
15363
  * The original Validate/submit logic (error checks + submission), extracted so "Continue to
15073
- * validate" on the typed-dimension warning popup can re-enter it directly without re-running
15074
- * (and re-triggering) the typed-dimension pre-flight in _handleSubmit() above.
15364
+ * validate" on the Validate pre-flight warning popup can re-enter it directly without
15365
+ * re-running (and re-triggering) the checks in _handleSubmit() above.
15075
15366
  */
15076
15367
  _continueSubmit() {
15077
15368
  this._submitted = true;
@@ -18098,53 +18389,87 @@ let JupiterDynamicForm = class extends LitElement {
18098
18389
  </div>
18099
18390
  ` : ""}
18100
18391
 
18101
- <!-- Typed Dimension Warning Popup -->
18102
- ${this._showTypedDimensionWarning ? html`
18392
+ <!-- Validate Pre-flight Warning Popup: typed-dimension-missing facts and/or duplicate-fact value mismatches -->
18393
+ ${this._showValidateWarningPopup ? html`
18103
18394
  <div class="error-popup-overlay" @click="${() => {
18104
- this._showTypedDimensionWarning = false;
18395
+ this._showValidateWarningPopup = false;
18105
18396
  this.requestUpdate();
18106
18397
  }}">
18107
18398
  <div class="error-popup" @click="${(e2) => e2.stopPropagation()}">
18108
18399
  <div class="error-popup-header">
18109
- <h3>⚠️ ${I18n.t("typedDimensionWarning.title")}</h3>
18400
+ <h3>⚠️ ${this._validateWarningPopupTitle()}</h3>
18110
18401
  <button class="error-popup-close" @click="${() => {
18111
- this._showTypedDimensionWarning = false;
18402
+ this._showValidateWarningPopup = false;
18112
18403
  this.requestUpdate();
18113
18404
  }}">×</button>
18114
18405
  </div>
18115
18406
  <div class="error-popup-content">
18116
- <p>${I18n.t("typedDimensionWarning.message")}</p>
18117
- <ul class="error-list">
18118
- ${this._groupTypedDimensionWarningFactsByRole().map((group) => html`
18119
- <li>
18120
- <strong>${I18n.t("typedDimensionWarning.role")}</strong> ${group.sectionTitle}
18121
- <ul>
18122
- ${group.facts.map((fact) => html`
18123
- <li>
18124
- <strong>${I18n.t("typedDimensionWarning.field")}</strong>
18125
- <a
18126
- href="javascript:void(0)"
18127
- class="error-field-link"
18128
- @click="${() => this._handleTypedDimensionFactClick(fact.conceptId, fact.columnId, fact.sectionId)}"
18129
- title="${I18n.t("typedDimensionWarning.clickToFocus")}"
18130
- >
18131
- ${fact.conceptLabel} (${fact.columnTitle})
18132
- </a>
18133
- — <strong>${I18n.t("typedDimensionWarning.value")}</strong> ${fact.value}
18134
- </li>
18135
- `)}
18136
- </ul>
18137
- </li>
18138
- `)}
18139
- </ul>
18407
+ ${this._typedDimensionWarningFacts.length > 0 ? html`
18408
+ <div class="validate-warning-section">
18409
+ ${this._duplicateFactMismatches.length > 0 ? html`<h4>${I18n.t("typedDimensionWarning.title")}</h4>` : ""}
18410
+ <p>${I18n.t("typedDimensionWarning.message")}</p>
18411
+ <ul class="error-list">
18412
+ ${this._groupTypedDimensionWarningFactsByRole().map((group) => html`
18413
+ <li>
18414
+ <strong>${I18n.t("typedDimensionWarning.role")}</strong> ${group.sectionTitle}
18415
+ <ul>
18416
+ ${group.facts.map((fact) => html`
18417
+ <li>
18418
+ <strong>${I18n.t("typedDimensionWarning.field")}</strong>
18419
+ <a
18420
+ href="javascript:void(0)"
18421
+ class="error-field-link"
18422
+ @click="${() => this._handleTypedDimensionFactClick(fact.conceptId, fact.columnId, fact.sectionId)}"
18423
+ title="${I18n.t("typedDimensionWarning.clickToFocus")}"
18424
+ >
18425
+ ${fact.conceptLabel} (${fact.columnTitle})
18426
+ </a>
18427
+ — <strong>${I18n.t("typedDimensionWarning.value")}</strong> ${fact.value}
18428
+ </li>
18429
+ `)}
18430
+ </ul>
18431
+ </li>
18432
+ `)}
18433
+ </ul>
18434
+ </div>
18435
+ ` : ""}
18436
+ ${this._duplicateFactMismatches.length > 0 ? html`
18437
+ <div class="validate-warning-section">
18438
+ ${this._typedDimensionWarningFacts.length > 0 ? html`<h4>${I18n.t("duplicateFactWarning.title")}</h4>` : ""}
18439
+ <p>${I18n.t("duplicateFactWarning.message")}</p>
18440
+ <ul class="error-list">
18441
+ ${this._duplicateFactMismatches.map((mismatch) => html`
18442
+ <li>
18443
+ <strong>${mismatch.conceptLabel}</strong>
18444
+ <ul>
18445
+ ${mismatch.facts.map((fact) => html`
18446
+ <li>
18447
+ <strong>${I18n.t("duplicateFactWarning.role")}</strong> ${fact.sectionTitle} —
18448
+ <a
18449
+ href="javascript:void(0)"
18450
+ class="error-field-link"
18451
+ @click="${() => this._handleDuplicateFactClick(fact.conceptId, fact.columnId, fact.sectionId)}"
18452
+ title="${I18n.t("duplicateFactWarning.clickToFocus")}"
18453
+ >
18454
+ ${fact.columnTitle}
18455
+ </a>
18456
+ — <strong>${I18n.t("duplicateFactWarning.value")}</strong> ${fact.value}
18457
+ </li>
18458
+ `)}
18459
+ </ul>
18460
+ </li>
18461
+ `)}
18462
+ </ul>
18463
+ </div>
18464
+ ` : ""}
18140
18465
  </div>
18141
18466
  <div class="error-popup-footer">
18142
18467
  <button class="btn-secondary" @click="${() => {
18143
- this._showTypedDimensionWarning = false;
18468
+ this._showValidateWarningPopup = false;
18144
18469
  this.requestUpdate();
18145
18470
  }}">${I18n.t("typedDimensionWarning.cancel")}</button>
18146
18471
  <button class="btn-primary" @click="${() => {
18147
- this._showTypedDimensionWarning = false;
18472
+ this._showValidateWarningPopup = false;
18148
18473
  this._continueSubmit();
18149
18474
  }}">${I18n.t("typedDimensionWarning.continue")}</button>
18150
18475
  </div>
@@ -19023,6 +19348,17 @@ JupiterDynamicForm.styles = css`
19023
19348
  color: var(--jupiter-text-primary, #333);
19024
19349
  }
19025
19350
 
19351
+ .validate-warning-section + .validate-warning-section {
19352
+ margin-top: 24px;
19353
+ padding-top: 20px;
19354
+ border-top: 1px solid var(--jupiter-border-color, #ddd);
19355
+ }
19356
+
19357
+ .validate-warning-section h4 {
19358
+ margin: 0 0 8px 0;
19359
+ color: var(--jupiter-text-primary, #333);
19360
+ }
19361
+
19026
19362
  .error-list {
19027
19363
  list-style: none;
19028
19364
  padding: 0;
@@ -19262,10 +19598,13 @@ __decorateClass([
19262
19598
  ], JupiterDynamicForm.prototype, "_showErrorPopup", 2);
19263
19599
  __decorateClass([
19264
19600
  r()
19265
- ], JupiterDynamicForm.prototype, "_showTypedDimensionWarning", 2);
19601
+ ], JupiterDynamicForm.prototype, "_showValidateWarningPopup", 2);
19266
19602
  __decorateClass([
19267
19603
  r()
19268
19604
  ], JupiterDynamicForm.prototype, "_typedDimensionWarningFacts", 2);
19605
+ __decorateClass([
19606
+ r()
19607
+ ], JupiterDynamicForm.prototype, "_duplicateFactMismatches", 2);
19269
19608
  __decorateClass([
19270
19609
  r()
19271
19610
  ], JupiterDynamicForm.prototype, "_submitDisabled", 2);