@campfire-interactive/volume-intelligence-ui 0.9.1 → 0.11.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.d.ts CHANGED
@@ -90,8 +90,12 @@ declare function VolumeImport(props: VolumeImportProps): react.JSX.Element;
90
90
 
91
91
  type SourceLayout = 'wide' | 'tall';
92
92
  /** `base` = IHS/AFS historical training signal (one active per tenant);
93
- * `overlay` = EDI-style additive customer-stated demand. */
94
- type VolumeSourceKind = 'base' | 'overlay';
93
+ * `overlay` = EDI-style additive customer-stated demand;
94
+ * `reference` = a second market vendor's curve, drawn beside the active base
95
+ * rather than summed into it. A reference source never drives prediction,
96
+ * never supplies level labels, and is never what an overlay links against —
97
+ * which is why it is a kind of its own rather than a second `base`. */
98
+ type VolumeSourceKind = 'base' | 'overlay' | 'reference';
95
99
  interface VolumeSource {
96
100
  id: string;
97
101
  code: string;
@@ -272,7 +276,18 @@ interface DetectSegmentsResult {
272
276
  interface ClearSourceDataResult {
273
277
  uploads: number;
274
278
  criteria: number;
275
- cleared: true;
279
+ /** Whether the source is now EMPTY. False when the call hit its time budget
280
+ * with rows still to delete — call again to continue.
281
+ *
282
+ * BREAKING in 2026-09: this was the literal `true`, returned regardless of
283
+ * whether anything remained. Batched clearing introduced a third outcome
284
+ * (partial), and a consumer branching on `cleared` would have read it as an
285
+ * empty source. */
286
+ cleared: boolean;
287
+ /** False when the call hit its time budget with rows still to delete. */
288
+ done?: boolean;
289
+ /** Criteria still to delete when `done` is false. */
290
+ remainingCriteria?: number;
276
291
  }
277
292
  /** Whether a source satisfies every required identity field. The dual-
278
293
  * satisfaction rule (explicit automotive_field mapping OR the covering
package/dist/index.js CHANGED
@@ -901,7 +901,8 @@ function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras,
901
901
  /* @__PURE__ */ jsxs4("span", { className: "cfi-vi-card-title", children: [
902
902
  /* @__PURE__ */ jsx4("button", { type: "button", className: "cfi-vi-link", onClick: () => onOpenSource(src.id), children: src.name }),
903
903
  isActive && /* @__PURE__ */ jsx4(Badge, { variant: "success", children: "Active" }),
904
- src.kind === "overlay" && /* @__PURE__ */ jsx4(Badge, { variant: "purple", title: "Overlay sources are active whenever data is uploaded \u2014 no Set Active step.", children: "Overlay" })
904
+ src.kind === "overlay" && /* @__PURE__ */ jsx4(Badge, { variant: "purple", title: "Overlay sources are active whenever data is uploaded \u2014 no Set Active step.", children: "Overlay" }),
905
+ src.kind === "reference" && /* @__PURE__ */ jsx4(Badge, { variant: "outline", title: "A second vendor's curve, drawn beside the active base rather than summed into it. Never drives prediction and has no Set Active step.", children: "Reference" })
905
906
  ] }),
906
907
  /* @__PURE__ */ jsxs4("span", { className: "cfi-vi-row", children: [
907
908
  /* @__PURE__ */ jsx4(Badge, { variant: "outline", children: src.code }),
@@ -1664,6 +1665,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, readOnl
1664
1665
  const [savingLayout, setSavingLayout] = useState7(false);
1665
1666
  const [editLayout, setEditLayout] = useState7("wide");
1666
1667
  const [editValueDateFormat, setEditValueDateFormat] = useState7("yyyy-MM-dd");
1668
+ const [editKind, setEditKind] = useState7("base");
1667
1669
  const [newSegmentLabel, setNewSegmentLabel] = useState7("");
1668
1670
  const [newSegmentDataType, setNewSegmentDataType] = useState7("string");
1669
1671
  const [addingSegment, setAddingSegment] = useState7(false);
@@ -1701,6 +1703,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, readOnl
1701
1703
  setSource(src);
1702
1704
  setSelectedKeyId(src.uniqueKeySegmentId ?? "__none__");
1703
1705
  setEditLayout(src.layout);
1706
+ setEditKind(src.kind);
1704
1707
  setEditValueDateFormat(src.valueDateFormat);
1705
1708
  setHasData(uploads.items.some((u) => u.status === "done" || u.status === "done_with_errors"));
1706
1709
  setMappingKeys(keys);
@@ -1769,8 +1772,31 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, readOnl
1769
1772
  setError(null);
1770
1773
  setClearResult(null);
1771
1774
  try {
1772
- const result = await transport.clearSourceData(id, clearConfirm);
1773
- setClearResult(`Cleared ${result.criteria.toLocaleString()} criteria and ${result.uploads.toLocaleString()} uploads. Configuration (segments, mappings) preserved.`);
1775
+ let criteria = 0;
1776
+ let uploads = 0;
1777
+ let passes = 0;
1778
+ const MAX_PASSES = 200;
1779
+ for (; ; ) {
1780
+ const result = await transport.clearSourceData(id, clearConfirm);
1781
+ criteria += result.criteria;
1782
+ uploads += result.uploads;
1783
+ passes += 1;
1784
+ if (result.done !== false || passes >= MAX_PASSES) {
1785
+ if (result.done === false) {
1786
+ setClearResult(
1787
+ `Stopped after ${passes} passes with ${(result.remainingCriteria ?? 0).toLocaleString()} criteria still to delete. ${criteria.toLocaleString()} criteria and ${uploads.toLocaleString()} uploads were removed. Run Clear again to continue.`
1788
+ );
1789
+ } else {
1790
+ setClearResult(
1791
+ `Cleared ${criteria.toLocaleString()} criteria and ${uploads.toLocaleString()} uploads${passes > 1 ? ` over ${passes} passes` : ""}. Configuration (segments, mappings) preserved.`
1792
+ );
1793
+ }
1794
+ break;
1795
+ }
1796
+ setClearResult(
1797
+ `Clearing\u2026 ${criteria.toLocaleString()} criteria removed so far, ${(result.remainingCriteria ?? 0).toLocaleString()} to go.`
1798
+ );
1799
+ }
1774
1800
  setClearConfirm("");
1775
1801
  await load();
1776
1802
  } catch (e) {
@@ -1809,12 +1835,17 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, readOnl
1809
1835
  setSavingKey(false);
1810
1836
  }
1811
1837
  }
1838
+ const crossesMeasure = !!source && editKind !== source.kind && (editKind === "overlay" || source.kind === "overlay");
1812
1839
  async function saveLayoutAndRouting() {
1813
1840
  if (!id) return;
1814
1841
  setSavingLayout(true);
1815
1842
  setError(null);
1816
1843
  try {
1817
- await transport.updateSource(id, { layout: editLayout, valueDateFormat: editValueDateFormat });
1844
+ await transport.updateSource(id, {
1845
+ layout: editLayout,
1846
+ valueDateFormat: editValueDateFormat,
1847
+ kind: editKind
1848
+ });
1818
1849
  await load();
1819
1850
  } catch (e) {
1820
1851
  setError(e instanceof Error ? e.message : "Failed to save layout");
@@ -1946,9 +1977,41 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, readOnl
1946
1977
  onChange: (e) => setEditValueDateFormat(e.target.value)
1947
1978
  }
1948
1979
  )
1980
+ ] }),
1981
+ /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-field", style: { flex: 1 }, children: [
1982
+ /* @__PURE__ */ jsx8("label", { className: "cfi-vi-label", children: "Role" }),
1983
+ /* @__PURE__ */ jsxs8(
1984
+ "select",
1985
+ {
1986
+ className: "cfi-vi-input",
1987
+ value: editKind,
1988
+ disabled: savingLayout || readOnly,
1989
+ onChange: (e) => setEditKind(e.target.value),
1990
+ children: [
1991
+ /* @__PURE__ */ jsx8("option", { value: "base", children: "base \u2014 the tenant's own volume" }),
1992
+ /* @__PURE__ */ jsx8("option", { value: "overlay", children: "overlay \u2014 customer-stated demand" }),
1993
+ /* @__PURE__ */ jsx8("option", { value: "reference", children: "reference \u2014 another vendor's curve" })
1994
+ ]
1995
+ }
1996
+ )
1949
1997
  ] })
1950
1998
  ] }),
1951
- /* @__PURE__ */ jsx8(Btn, { size: "sm", onClick: () => void saveLayoutAndRouting(), disabled: hasData || savingLayout || readOnly, children: savingLayout ? "Saving\u2026" : "Save layout" })
1999
+ hasData && crossesMeasure && /* @__PURE__ */ jsxs8("p", { className: "cfi-vi-hint", style: { color: "#b45309" }, children: [
2000
+ "This source has data. Changing between ",
2001
+ source.kind,
2002
+ " and ",
2003
+ editKind,
2004
+ " would reinterpret parts as vehicles or the reverse \u2014 clear its data first."
2005
+ ] }),
2006
+ /* @__PURE__ */ jsx8(
2007
+ Btn,
2008
+ {
2009
+ size: "sm",
2010
+ onClick: () => void saveLayoutAndRouting(),
2011
+ disabled: savingLayout || readOnly || hasData && crossesMeasure || hasData && editKind === source.kind,
2012
+ children: savingLayout ? "Saving\u2026" : "Save"
2013
+ }
2014
+ )
1952
2015
  ] }),
1953
2016
  /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-panel", children: [
1954
2017
  /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-row", children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campfire-interactive/volume-intelligence-ui",
3
- "version": "0.9.1",
3
+ "version": "0.11.1",
4
4
  "description": "Shared Volume Intelligence UI components (volume import) for the Campfire Suite — embedded by the VI webapp and by consuming apps (OMSF) so there is one import surface, not two that drift.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",