@metron-labs/quote-react 0.1.68 → 0.1.70

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.
@@ -62,7 +62,9 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
62
62
  catch { }
63
63
  // Older than the TTL (or saved before there was a stamp): start fresh. The save effect
64
64
  // below then removes it, as scheduleResult stays null.
65
- if (saved && !(Date.now() - (saved.savedAt ?? 0) < SAVED_QUOTE_TTL_MS))
65
+ if (process.env.NODE_ENV === "development")
66
+ saved = null;
67
+ else if (saved && !(Date.now() - (saved.savedAt ?? 0) < SAVED_QUOTE_TTL_MS))
66
68
  saved = null;
67
69
  if (saved?.scheduleResult) {
68
70
  setEntryMode("schedule");
@@ -17,5 +17,6 @@ export declare function candidateFromCart(item: CartItem): MatchCandidate;
17
17
  export declare function refreshedCartItem(refreshed: CartItem, existing: CartItem): CartItem;
18
18
  export declare function displayLineLabel(item: LineItem, allItems: LineItem[]): string;
19
19
  export declare function formatResultsTotal(totalValue: number | null, needsPricingCount: number): string | null;
20
+ export declare function curatedCandidates(candidates: MatchCandidate[], result: MatchResult | undefined, selectedProductId: string | null): MatchCandidate[];
20
21
  export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart, onAddToCart, onRemoveFromCart, onGoToBasket, cartSeeded, onCartSeeded, reviewGateComplete, onReviewGateComplete, internal, }: ResultsStageProps): import("react").JSX.Element;
21
22
  export {};
@@ -121,11 +121,6 @@ export function refreshedCartItem(refreshed, existing) {
121
121
  conversionNotes: existing.conversionNotes,
122
122
  };
123
123
  }
124
- function classificationLabel(value) {
125
- if (!value)
126
- return "not stated";
127
- return value.replace(/_/g, " ");
128
- }
129
124
  function ConfChip({ band }) {
130
125
  return (_jsx("span", { className: `status-dot status-dot--${band}`, title: band, children: _jsx("span", { className: "status-dot__mark" }) }));
131
126
  }
@@ -194,23 +189,14 @@ function LinkedGroupPill({ meta }) {
194
189
  }
195
190
  return (_jsx("span", { className: "results-linked-pill", style: meta.bubbleStyle, children: `Link ${meta.token}` }));
196
191
  }
197
- function curatedCandidates(candidates, result, selectedProductId) {
192
+ export function curatedCandidates(candidates, result, selectedProductId) {
198
193
  const recommendedIds = result?.rerank?.recommendedCandidateIds ?? [];
199
- if (recommendedIds.length === 0) {
200
- const topCandidates = candidates.slice(0, 6);
201
- const pinnedSelected = !topCandidates.some((candidate) => candidate.product_id === selectedProductId)
202
- ? candidates.find((candidate) => candidate.product_id === selectedProductId)
203
- : undefined;
204
- return pinnedSelected ? [...topCandidates, pinnedSelected] : topCandidates;
205
- }
206
194
  const byId = new Map(candidates.map((candidate) => [candidate.product_id, candidate]));
207
- const shown = recommendedIds.map((candidateId) => byId.get(candidateId)).filter((candidate) => candidate != null);
208
- if (selectedProductId && !shown.some((candidate) => candidate.product_id === selectedProductId)) {
209
- const selected = byId.get(selectedProductId);
210
- if (selected) {
211
- shown.unshift(selected);
212
- }
213
- }
195
+ const ordered = recommendedIds.length > 0
196
+ ? recommendedIds.map((candidateId) => byId.get(candidateId)).filter((candidate) => candidate != null)
197
+ : candidates.slice(0, 6);
198
+ const selected = selectedProductId ? byId.get(selectedProductId) : undefined;
199
+ const shown = selected ? [selected, ...ordered.filter(({ product_id }) => product_id !== selected.product_id)] : ordered;
214
200
  return [...new Map(shown.map((candidate) => [candidate.product_id, candidate])).values()].slice(0, 6);
215
201
  }
216
202
  function recommendedCandidates(result) {
@@ -288,10 +274,10 @@ function AdjudicationDebugBlock({ result }) {
288
274
  ];
289
275
  return (_jsxs("div", { className: "results-detail-block", children: [_jsx("h4", { style: { margin: "0 0 12px" }, children: "Adjudication context" }), _jsx("dl", { className: "results-detail-grid", children: details.map(([label, value]) => (_jsxs(Fragment, { children: [_jsx("dt", { children: label }), _jsx("dd", { children: value })] }, label))) }), debug.rerank_line_item_input ? (_jsxs(_Fragment, { children: [_jsx("h5", { style: { margin: "12px 0 8px" }, children: "Rerank input" }), _jsx("pre", { className: "results-pre", children: JSON.stringify(debug.rerank_line_item_input, null, 2) })] })) : null] }));
290
276
  }
291
- function DetailedDecision({ item, decision, result, showRetrievalDebug, reviewOnly = false, linkedGroupMeta, }) {
277
+ function DetailedDecision({ item, decision, selectedProductId, result, showRetrievalDebug, reviewOnly = false, linkedGroupMeta, }) {
292
278
  if (!decision)
293
279
  return null;
294
- const reviewLabel = decision.technical_classification === "no_suitable_candidate" ? "No automatic match:" : `AI review (${decision.confidence ?? "confidence not stated"}):`;
280
+ const reviewLabel = selectedProductId == null ? "Needs review:" : "Selected:";
295
281
  if (reviewOnly) {
296
282
  return decision.reason ? (_jsxs("div", { className: "results-detail-block", children: [_jsxs("p", { className: `basket-calc-text basket-calc-text--review`, children: [_jsx("strong", { children: reviewLabel }), "\u00A0", decision.reason] }), hasVisibleLinkedGroup(item) ? (_jsxs("p", { className: "results-row-meta", style: { marginTop: 8 }, children: ["Linked context: ", linkedGroupMeta ? `Link ${linkedGroupMeta.token} - ` : "", linkedGroupSummary(item)] })) : null] })) : null;
297
283
  }
@@ -305,7 +291,7 @@ function CandidateBreakdown({ item, candidates, result, decision, unmatchable, s
305
291
  // Metron Labs' own tooling — only surfaced under the `debug` config's detail mode.
306
292
  const showBusinessDetail = internal || detailMode === "detailed";
307
293
  const showScoreColumns = debugEnabled && detailMode === "detailed";
308
- const detailBlock = showBusinessDetail || forceReviewNote ? (_jsx(DetailedDecision, { item: item, decision: decision, result: result, showRetrievalDebug: showScoreColumns, reviewOnly: forceReviewNote && !showBusinessDetail, linkedGroupMeta: linkedGroupMeta })) : null;
294
+ const detailBlock = showBusinessDetail || forceReviewNote ? (_jsx(DetailedDecision, { item: item, decision: decision, selectedProductId: selectedProductId, result: result, showRetrievalDebug: showScoreColumns, reviewOnly: forceReviewNote && !showBusinessDetail, linkedGroupMeta: linkedGroupMeta })) : null;
309
295
  if (unmatchable) {
310
296
  return (_jsxs(_Fragment, { children: [detailBlock, _jsx("p", { className: "results-empty-hint", children: "Classified as labour/plant or referencing another line item - skipped by design." })] }));
311
297
  }
@@ -390,7 +376,7 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
390
376
  let totalValue = hasPendingReview ? null : getCartSubtotal(Object.values(cart), { wasteFactorEnabled: false, wasteFactorOverride: 0 });
391
377
  let needsPricingCount = 0;
392
378
  for (const row of rows) {
393
- if (row.unmatchable || row.decision?.include_in_quote !== true || !row.best)
379
+ if (row.unmatchable || !row.best)
394
380
  continue;
395
381
  const cartItem = cart[row.item.line_id];
396
382
  if (!cartItem || getPricingBreakdown(cartItem).lineTotal == null)
@@ -399,7 +385,7 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
399
385
  if (needsPricingCount > 0)
400
386
  totalValue = null;
401
387
  const totalLabel = formatResultsTotal(totalValue, needsPricingCount);
402
- const hasBestAlternative = (row) => !row.unmatchable && !row.best && row.decision?.technical_classification !== "no_suitable_candidate" && recommendedCandidates(row.result).length > 0;
388
+ const hasBestAlternative = (row) => !row.unmatchable && !row.best && recommendedCandidates(row.result).length > 0;
403
389
  const previewCandidate = (row) => row.best ?? recommendedCandidates(row.result)[0];
404
390
  const reviewRequiredRows = rows.filter((row) => !row.unmatchable && !row.best);
405
391
  const pendingReviewRows = reviewRequiredRows.filter((row) => !skippedReviewIds.has(row.item.line_id));
@@ -417,9 +403,8 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
417
403
  const currentPage = Math.min(page, totalPages);
418
404
  const pagedRows = visibleRows.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
419
405
  const matchedTileRows = pagedRows.filter((row) => !row.unmatchable && row.best);
420
- const alternativeTileRows = pagedRows.filter((row) => hasBestAlternative(row));
421
- const matchedDisplayRows = pagedRows.filter((row) => !row.unmatchable && (row.best || hasBestAlternative(row)));
422
- const unmatchedTileRows = pagedRows.filter((row) => row.unmatchable || (!row.best && !hasBestAlternative(row)));
406
+ const matchedDisplayRows = pagedRows.filter((row) => !row.unmatchable && row.best);
407
+ const unmatchedTileRows = pagedRows.filter((row) => row.unmatchable || !row.best);
423
408
  const activeReviewRow = selectedReviewId ? (rows.find((row) => row.item.line_id === selectedReviewId) ?? null) : null;
424
409
  const reviewGateOpen = !internal && !reviewGateComplete && !activeReviewRow && pendingReviewRows.length > 0;
425
410
  const sortMenuRef = useRef(null);
@@ -533,7 +518,7 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
533
518
  else
534
519
  closeReview(true);
535
520
  };
536
- const unmatchedSection = unmatchedTileRows.length > 0 ? (_jsxs("section", { className: "results-tile-section results-tile-section--unmatched", "aria-label": "Items requiring matching", children: [_jsxs("div", { className: "results-tile-section__head", children: [_jsx("h3", { children: "Require Matching" }), _jsx("p", { children: "We couldn\u2019t confidently match these to a product. Open one to pick from the closest options." })] }), _jsx("div", { className: "results-unmatched-list", children: unmatchedTileRows.map((row) => {
521
+ const unmatchedSection = unmatchedTileRows.length > 0 ? (_jsxs("section", { className: "results-tile-section results-tile-section--unmatched", "aria-label": "Items needing review", children: [_jsxs("div", { className: "results-tile-section__head", children: [_jsx("h3", { children: "Needs review" }), _jsx("p", { children: "We couldn\u2019t confidently match these to a product. Open one to pick from the closest options." })] }), _jsx("div", { className: "results-unmatched-list", children: unmatchedTileRows.map((row) => {
537
522
  const { item, qty } = row;
538
523
  return (_jsxs("button", { type: "button", className: "results-unmatched-card", onClick: () => openReview(item.line_id), children: [_jsx("span", { className: "results-unmatched-card__main", children: _jsx("span", { className: "results-unmatched-card__title", children: displayLineLabel(item, lineItems) }) }), _jsx("span", { className: "bom-toolbar-btn results-unmatched-card__prompt", children: "Review" })] }, item.line_id));
539
524
  }) })] })) : null;
@@ -564,7 +549,9 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
564
549
  })));
565
550
  }
566
551
  function toCartItem(row) {
567
- const candidate = row.best ?? recommendedCandidates(row.result)[0];
552
+ const candidate = row.best;
553
+ if (!candidate)
554
+ throw new Error("Cannot add a line item without a selected product.");
568
555
  return candidateToCartItem(candidate, {
569
556
  lineId: row.item.line_id,
570
557
  description: row.item.description_original,
@@ -589,8 +576,8 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
589
576
  : undefined,
590
577
  });
591
578
  }
592
- // All matched items, including review-required ones, land in the basket automatically
593
- // so the common case starts from a complete draft basket. Gated by cartSeeded (lifted
579
+ // All selected items land in the basket automatically so the common case starts from
580
+ // a complete draft basket. Gated by cartSeeded (lifted
594
581
  // to BoQApp) so this fires once per schedule result, not on every remount of this stage.
595
582
  useEffect(() => {
596
583
  if (cartSeeded)
@@ -649,8 +636,8 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
649
636
  const cartCount = Object.keys(cart).length;
650
637
  const reviewQuery = reviewSearch.trim();
651
638
  const llmReviewCandidates = recommendedCandidates(activeReviewRow?.result);
652
- const hasNoSuitableCandidate = activeReviewRow?.decision?.technical_classification === "no_suitable_candidate" || (!!activeReviewRow && !activeReviewRow.best);
653
- const hasAiVerdict = activeReviewRow?.decision?.technical_classification != null;
639
+ const hasNoSuitableCandidate = !!activeReviewRow && !activeReviewRow.best && (activeReviewRow.result?.candidates.length === 0 || activeReviewRow.result?.rerank?.selectedCandidateId === null);
640
+ const hasAiVerdict = activeReviewRow?.result?.rerank != null;
654
641
  const activeReviewReason = activeReviewRow?.decision?.reason ?? (hasNoSuitableCandidate ? "No catalogue candidate found. Refine the request or search the catalogue." : null);
655
642
  const catalogueSearchCandidates = catalogueSearchResults.map((candidate) => ({
656
643
  product_id: candidate.product_id,
@@ -749,22 +736,13 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
749
736
  }, children: _jsx("span", { children: activeReviewCartMatches ? `Remove from ${customerCollection}` : `Add selected product to ${customerCollection}` }) }))) : null, reviewFlowActive ? (_jsx("button", { type: "button", className: "cta-btn cta-btn-secondary customer-review-skip", onClick: () => skipReviewAndReturnToGate(activeReviewRow.item.line_id), children: "Skip this item" })) : null] })) })] }) })) : null, _jsxs("div", { className: "results-tile-layout", children: [visibleRows.length === 0 ? _jsx("div", { className: "results-empty-card", children: "No line items match the current search/filter." }) : null, matchedDisplayRows.length > 0 ? (_jsx("section", { className: "results-tile-section", "aria-label": "Matched and review items", children: _jsx("div", { className: "results-tile-grid", children: matchedDisplayRows.map((row) => {
750
737
  const { item, best, qty } = row;
751
738
  const preview = previewCandidate(row);
752
- const isPartial = !best && hasBestAlternative(row);
753
- return (_jsxs("article", { className: "results-tile-card", children: [_jsxs("button", { type: "button", className: "results-tile-card__surface", onClick: () => openReview(item.line_id), "aria-label": `Review match for ${displayLineLabel(item, lineItems) || "item"}`, children: [_jsxs("div", { className: "results-tile-card__media", children: [isPartial || hasVisibleLinkedGroup(item) ? (_jsxs("div", { className: "results-tile-card__badges", children: [hasVisibleLinkedGroup(item) ? _jsx(LinkedGroupPill, { meta: linkedGroupMetaById.get(item.linked_group_id) }) : _jsx("span", {}), isPartial ? (_jsx("span", { style: {
754
- padding: "6px 10px",
755
- borderRadius: 999,
756
- background: "#fef3c7",
757
- color: "#92400e",
758
- fontSize: 12,
759
- fontWeight: 700,
760
- lineHeight: 1,
761
- }, children: "Partially matched" })) : null] })) : null, _jsx(ProductImage, { className: "fill-img results-tile-card__image", description: preview?.description || item.description_original, photoUrl: preview?.pdp_photo_url })] }), _jsxs("div", { className: "results-tile-card__body", children: [preview ? (_jsx(_Fragment, { children: _jsx("div", { className: "results-tile-card__heading", children: _jsx("div", { className: "results-tile-card__title", children: preview.description }) }) })) : (_jsx("div", { className: "results-empty-hint", children: "No product currently selected for this line item." })), _jsx("div", { className: "results-tile-card__match", children: preview ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "results-tile-card__match-label", children: isPartial ? "↳ partially matched from" : "↳ matched from" }), _jsx("div", { className: "results-tile-card__match-name", children: displayLineLabel(item, lineItems) }), hasVisibleLinkedGroup(item) ? _jsx("div", { className: "results-row-meta", children: linkedGroupSummary(item) }) : null] })) : (_jsx("span", { className: "results-empty-hint", children: "No matched product selected." })) })] }), _jsx("span", { className: "results-tile-card__hover-prompt", "aria-hidden": "true", children: "Click to view options" })] }), _jsxs("div", { className: "results-tile-card__actions", children: [internal && (_jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => openReview(item.line_id), children: "Review" })), _jsx("button", { type: "button", className: `bom-toolbar-btn${cart[item.line_id] ? " bom-toolbar-btn--added" : " bom-toolbar-btn--accent"}`, onClick: () => {
739
+ return (_jsxs("article", { className: "results-tile-card", children: [_jsxs("button", { type: "button", className: "results-tile-card__surface", onClick: () => openReview(item.line_id), "aria-label": `Review match for ${displayLineLabel(item, lineItems) || "item"}`, children: [_jsxs("div", { className: "results-tile-card__media", children: [hasVisibleLinkedGroup(item) ? (_jsx("div", { className: "results-tile-card__badges", children: _jsx(LinkedGroupPill, { meta: linkedGroupMetaById.get(item.linked_group_id) }) })) : null, _jsx(ProductImage, { className: "fill-img results-tile-card__image", description: preview?.description || item.description_original, photoUrl: preview?.pdp_photo_url })] }), _jsxs("div", { className: "results-tile-card__body", children: [preview ? (_jsx(_Fragment, { children: _jsx("div", { className: "results-tile-card__heading", children: _jsx("div", { className: "results-tile-card__title", children: preview.description }) }) })) : (_jsx("div", { className: "results-empty-hint", children: "No product currently selected for this line item." })), _jsx("div", { className: "results-tile-card__match", children: preview ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "results-tile-card__match-label", children: "\u21B3 matched from" }), _jsx("div", { className: "results-tile-card__match-name", children: displayLineLabel(item, lineItems) }), hasVisibleLinkedGroup(item) ? _jsx("div", { className: "results-row-meta", children: linkedGroupSummary(item) }) : null] })) : (_jsx("span", { className: "results-empty-hint", children: "No matched product selected." })) })] }), _jsx("span", { className: "results-tile-card__hover-prompt", "aria-hidden": "true", children: "Click to view options" })] }), _jsxs("div", { className: "results-tile-card__actions", children: [internal && (_jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => openReview(item.line_id), children: "Review" })), _jsx("button", { type: "button", className: `bom-toolbar-btn${cart[item.line_id] ? " bom-toolbar-btn--added" : " bom-toolbar-btn--accent"}`, onClick: () => {
762
740
  if (cart[item.line_id])
763
741
  onRemoveFromCart(item.line_id);
764
742
  else {
765
743
  onAddToCart(toCartItem(row));
766
744
  }
767
- }, disabled: !preview, "aria-label": cart[item.line_id]
745
+ }, disabled: !best, "aria-label": cart[item.line_id]
768
746
  ? internal
769
747
  ? "Remove from selection"
770
748
  : `Remove from ${customerCollection}`
@@ -784,7 +762,7 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
784
762
  alignItems: "center",
785
763
  gap: 8,
786
764
  marginTop: 4,
787
- }, children: [_jsx(LinkedGroupPill, { meta: linkedGroupMetaById.get(item.linked_group_id) }), _jsx("div", { className: "results-row-meta", style: { marginTop: 0 }, children: linkedGroupSummary(item) })] })) : null] }), _jsx("td", { children: best ? (_jsxs("div", { children: [_jsx("div", { className: "results-match-cell-text", title: best.description, children: best.description }), detailMode === "detailed" && decision?.technical_classification && (_jsxs("div", { className: "results-row-meta", children: [classificationLabel(decision.technical_classification), decision.approval_required ? " - approval required" : ""] }))] })) : (_jsx("span", { className: "results-empty-hint" })) }), _jsx("td", { style: { textAlign: "center" }, children: best?.price != null ? fmtPrice(best.price, best.price_basis) : "-" }), detailMode === "simple" && (_jsx("td", { style: { textAlign: "center" }, children: _jsx("button", { type: "button", className: `bom-toolbar-btn${cart[item.line_id] ? " bom-toolbar-btn--added" : " bom-toolbar-btn--accent"}`, style: { width: 84, justifyContent: "center" }, onClick: (event) => {
765
+ }, children: [_jsx(LinkedGroupPill, { meta: linkedGroupMetaById.get(item.linked_group_id) }), _jsx("div", { className: "results-row-meta", style: { marginTop: 0 }, children: linkedGroupSummary(item) })] })) : null] }), _jsx("td", { children: best ? (_jsxs("div", { children: [_jsx("div", { className: "results-match-cell-text", title: best.description, children: best.description }), detailMode === "detailed" && decision && (_jsx("div", { className: "results-row-meta", children: decision.selected_candidate_id === best.product_id ? "Automatically selected" : "Selected" }))] })) : (_jsx("span", { className: "results-empty-hint" })) }), _jsx("td", { style: { textAlign: "center" }, children: best?.price != null ? fmtPrice(best.price, best.price_basis) : "-" }), detailMode === "simple" && (_jsx("td", { style: { textAlign: "center" }, children: _jsx("button", { type: "button", className: `bom-toolbar-btn${cart[item.line_id] ? " bom-toolbar-btn--added" : " bom-toolbar-btn--accent"}`, style: { width: 84, justifyContent: "center" }, onClick: (event) => {
788
766
  event.stopPropagation();
789
767
  if (cart[item.line_id])
790
768
  onRemoveFromCart(item.line_id);