@metron-labs/quote-react 0.1.53 → 0.1.55

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.
@@ -1,11 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { assetUrl } from "../../lib/assets";
4
- import { apiUrl } from "../../lib/config";
5
4
  import { useTrack } from "../../lib/analytics";
6
5
  import ForwardArrow from "../../components/ForwardArrow";
7
6
  import CloseIcon from "../../components/CloseIcon";
8
- import { clientHeaders, readJson } from "../../lib/api/boqClient";
9
7
  import { FILE_EXTENSIONS, isPhotoFile, shrinkPhoto, validateLines, validateUploadFile } from "../../lib/boq/inputLimits";
10
8
  import { ProcessingContent } from "./ProcessingStage";
11
9
  import { useEscapeKey } from "../../lib/useEscapeKey";
@@ -156,42 +154,25 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
156
154
  setMatching(false);
157
155
  }
158
156
  };
159
- const matchPhoto = async (file) => {
160
- const id = matchRun.current;
161
- try {
162
- const photo = await shrinkPhoto(file);
163
- const problem = validateUploadFile(photo);
164
- if (problem)
165
- throw new Error(problem);
166
- const formData = new FormData();
167
- formData.append("file", photo);
168
- const response = await fetch(apiUrl("/api/schedule/extract-photo"), { method: "POST", body: formData, headers: clientHeaders() });
169
- const payload = await readJson(response);
170
- if (!response.ok)
171
- throw new Error(payload?.error || payload?.message || "Failed to extract line items from image");
172
- const text = typeof payload?.text === "string" ? payload.text.trim() : "";
173
- if (!text)
174
- throw new Error("No line items were extracted from this image.");
175
- const lines = Array.isArray(payload?.lines)
176
- ? payload.lines.filter((line) => Boolean(line && typeof line === "object" && typeof line.text === "string"))
177
- : undefined;
178
- // Cancelled while the photo was being read - don't start matching it.
179
- if (id !== matchRun.current)
180
- return;
181
- await onPasteSchedule(text, lines, false);
182
- }
183
- catch (error) {
184
- if (id !== matchRun.current)
185
- return;
186
- track("error", { stage: "photo" });
187
- setUploadError(error?.message || "Failed to extract line items from image");
188
- }
189
- };
190
157
  const matchUpload = () => {
191
158
  if (!uploadFile)
192
159
  return;
193
160
  track("list_submitted", { method: isPhotoFile(uploadFile) ? "photo" : "upload" });
194
- return startMatching(() => (isPhotoFile(uploadFile) ? matchPhoto(uploadFile) : onUploadSchedule(uploadFile)));
161
+ return startMatching(async () => {
162
+ if (!isPhotoFile(uploadFile))
163
+ return onUploadSchedule(uploadFile);
164
+ try {
165
+ const photo = await shrinkPhoto(uploadFile);
166
+ const problem = validateUploadFile(photo);
167
+ if (problem)
168
+ throw new Error(problem);
169
+ await onUploadSchedule(photo);
170
+ }
171
+ catch (error) {
172
+ track("error", { stage: "photo" });
173
+ setUploadError(error?.message || "Failed to process image upload");
174
+ }
175
+ });
195
176
  };
196
177
  const isFileDrag = (event) => event.dataTransfer.types.includes("Files");
197
178
  const onPageDragEnter = (event) => {
@@ -689,7 +689,7 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
689
689
  })();
690
690
  const activeReviewCartMatches = !!activeReviewRow?.best && cart[activeReviewRow.item.line_id]?.productId === activeReviewRow.best.product_id;
691
691
  const activeReviewHasPendingUpdate = !!activeReviewRow?.best && !!cart[activeReviewRow.item.line_id] && !activeReviewCartMatches;
692
- return (_jsxs("div", { className: "wizard-step entry-stage", children: [_jsxs("div", { className: `quick-order-card${internal ? "" : " quick-order-card--customer"}`, children: [_jsx("div", { className: `quick-order-hero${internal ? "" : " quick-order-hero--customer"}`, children: internal ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsxs("div", { className: "quick-order-hero-title-row", children: [_jsx("h1", { children: "Your Results" }), _jsxs("span", { className: "summary-chip summary-chip--scope", children: [_jsx("strong", { children: totalLabel ?? "Review quantities" }), " Est. total"] })] }), _jsx("p", { children: "Each line item from your schedule matched against your catalogue, ranked by confidence." })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("h1", { children: "Review your matched products" }), _jsxs("p", { children: ["Check you're happy with these items, then click ", qzaQuoteRequest ? "View Quote" : "View Basket", " to continue."] })] }), _jsxs("button", { type: "button", className: "results-total-action", onClick: onGoToBasket, "aria-label": qzaQuoteRequest ? "View quote" : "View basket", children: [_jsxs("span", { children: [_jsx("small", { children: qzaQuoteRequest ? "View Quote" : "View Basket" }), totalLabel != null && _jsx("strong", { children: totalLabel })] }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] })] })) }), _jsxs("div", { className: "quick-order-panel", style: { paddingTop: 16 }, children: [debugEnabled && (_jsx("div", { className: "results-summary", style: { marginBottom: 12 }, children: _jsx("button", { type: "button", className: "bom-toolbar-btn", "aria-pressed": showTimings, onClick: () => setShowTimings((value) => !value), children: showTimings ? "Hide timings" : "Show timings" }) })), debugEnabled && showTimings && _jsx(TimingBreakdown, { ...timings }), !qzaQuoteRequest && (_jsx("div", { className: "results-toolbar-row", children: !activeReviewRow && (_jsxs("div", { className: "results-summary", style: { marginBottom: 0 }, children: [needsPricingCount > 0 && (_jsxs("span", { className: "summary-chip summary-chip--neutral", children: [_jsx("strong", { children: needsPricingCount }), " need pricing"] })), _jsxs("div", { style: {
692
+ return (_jsxs("div", { className: "wizard-step entry-stage", children: [_jsxs("div", { className: `quick-order-card${internal ? "" : " quick-order-card--customer"}`, children: [_jsx("div", { className: `quick-order-hero${internal ? "" : " quick-order-hero--customer"}`, children: internal ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsxs("div", { className: "quick-order-hero-title-row", children: [_jsx("h1", { children: "Your Results" }), _jsxs("span", { className: "summary-chip summary-chip--scope", children: [_jsx("strong", { children: totalLabel ?? "Review quantities" }), " Est. total"] })] }), _jsx("p", { children: "Each line item from your schedule matched against your catalogue, ranked by confidence." })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("h1", { children: "Review your matched products" }), _jsxs("p", { children: ["Check you're happy with these items, then click ", qzaQuoteRequest ? "View Quote" : "View Basket", " to continue."] })] }), _jsxs("button", { type: "button", className: "results-total-action", onClick: onGoToBasket, disabled: cartCount === 0, "aria-label": qzaQuoteRequest ? "View quote" : "View basket", children: [_jsxs("span", { children: [_jsx("small", { children: qzaQuoteRequest ? "View Quote" : "View Basket" }), totalLabel != null && _jsx("strong", { children: totalLabel })] }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] })] })) }), _jsxs("div", { className: "quick-order-panel", style: { paddingTop: 16 }, children: [debugEnabled && (_jsx("div", { className: "results-summary", style: { marginBottom: 12 }, children: _jsx("button", { type: "button", className: "bom-toolbar-btn", "aria-pressed": showTimings, onClick: () => setShowTimings((value) => !value), children: showTimings ? "Hide timings" : "Show timings" }) })), debugEnabled && showTimings && _jsx(TimingBreakdown, { ...timings }), !qzaQuoteRequest && (_jsx("div", { className: "results-toolbar-row", children: !activeReviewRow && (_jsxs("div", { className: "results-summary", style: { marginBottom: 0 }, children: [needsPricingCount > 0 && (_jsxs("span", { className: "summary-chip summary-chip--neutral", children: [_jsx("strong", { children: needsPricingCount }), " need pricing"] })), _jsxs("div", { style: {
693
693
  display: "flex",
694
694
  gap: 8,
695
695
  flexWrap: "wrap",
@@ -778,10 +778,10 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
778
778
  }
779
779
  }, "aria-label": cart[item.line_id] ? (internal ? "Remove from selection" : `Remove from ${customerCollection}`) : internal ? "Add to selection" : `Add to ${customerCollection}`, children: cart[item.line_id] ? "✓ Added" : internal ? "+ Add" : `Add to ${customerCollection}` }) }))] }, item.line_id));
780
780
  })] })] }) }), unmatchedSection] })), !activeReviewRow && totalPages > 1 && (_jsxs("div", { className: "results-pagination", children: [_jsxs("span", { className: "results-pagination-count", children: ["Showing ", (currentPage - 1) * PAGE_SIZE + 1, "\u2013", Math.min(currentPage * PAGE_SIZE, visibleRows.length), " of ", visibleRows.length] }), _jsxs("div", { className: "results-pagination-controls", children: [_jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage - 1), disabled: currentPage === 1, children: "Previous" }), _jsxs("span", { className: "results-pagination-page", children: ["Page ", currentPage, " of ", totalPages] }), _jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage + 1), disabled: currentPage === totalPages, children: "Next" })] })] })), (parseReport.itemCount != null || parseReport.headingCount != null || parseReport.skippedCount != null) && (_jsxs("div", { className: "results-summary", style: { marginTop: 16 }, children: [parseReport.itemCount != null && (_jsxs("span", { className: "summary-chip", children: [_jsx("strong", { children: parseReport.itemCount }), " items parsed"] })), parseReport.headingCount != null && (_jsxs("span", { className: "summary-chip", children: [_jsx("strong", { children: parseReport.headingCount }), " headings"] })), parseReport.skippedCount != null && (_jsxs("span", { className: "summary-chip", children: [_jsx("strong", { children: parseReport.skippedCount }), " rows skipped"] }))] })), _jsxs("div", { className: "disclaimer-box", children: [_jsx("span", { className: "disclaimer-icon", children: "\u26A0\uFE0F" }), _jsx("p", { className: "disclaimer-text", children: "These matches are generated automatically and should be reviewed before purchase." })] }), !activeReviewRow && (_jsxs("div", { className: "btn-row results-actions", children: [_jsxs("button", { className: "cta-btn cta-btn-secondary", style: {
781
- flex: cartCount > 0 ? "0 1 30%" : "0 1 30%",
781
+ flex: "0 1 30%",
782
782
  gap: 10,
783
783
  justifyContent: "center",
784
- }, onClick: () => setConfirmStartOver(true), "aria-label": "Start over", children: [_jsx(RefreshIcon, {}), _jsx("span", { children: "Start Over" })] }), cartCount > 0 && (_jsxs("button", { className: "cta-btn", style: { flex: "0 1 70%" }, onClick: onGoToBasket, "aria-label": internal ? "View selection" : qzaQuoteRequest ? "View quote" : "View basket", children: [_jsxs("span", { children: [internal ? "View Selection" : qzaQuoteRequest ? "View Quote" : "View Basket", " (", cartCount, ")"] }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] }))] }))] })] }), reviewGateOpen && (_jsx("div", { className: "modal-backdrop quick-order-modal-backdrop review-gate-modal-backdrop", role: "dialog", "aria-modal": "true", "aria-labelledby": "review-gate-title", children: _jsxs("section", { className: "quick-order-workflow-modal review-gate-modal", children: [_jsxs("div", { className: "review-gate-modal__header", children: [_jsx("h2", { id: "review-gate-title", children: "Choose products for these items" }), _jsx("p", { children: "We couldn\u2019t find a clear match for these items, so we need your help. Select an option from the suggestions, search the catalogue, or leave the item out." })] }), _jsx("div", { className: "review-gate-list", children: pendingReviewRows.map((row) => {
784
+ }, onClick: () => setConfirmStartOver(true), "aria-label": "Start over", children: [_jsx(RefreshIcon, {}), _jsx("span", { children: "Start Over" })] }), _jsxs("button", { className: "cta-btn", style: { flex: "0 1 70%" }, onClick: onGoToBasket, disabled: cartCount === 0, "aria-label": internal ? "View selection" : qzaQuoteRequest ? "View quote" : "View basket", children: [_jsxs("span", { children: [internal ? "View Selection" : qzaQuoteRequest ? "View Quote" : "View Basket", " (", cartCount, ")"] }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] })] }))] })] }), reviewGateOpen && (_jsx("div", { className: "modal-backdrop quick-order-modal-backdrop review-gate-modal-backdrop", role: "dialog", "aria-modal": "true", "aria-labelledby": "review-gate-title", children: _jsxs("section", { className: "quick-order-workflow-modal review-gate-modal", children: [_jsxs("div", { className: "review-gate-modal__header", children: [_jsx("h2", { id: "review-gate-title", children: "Choose products for these items" }), _jsx("p", { children: "We couldn\u2019t find a clear match for these items, so we need your help. Select an option from the suggestions, search the catalogue, or leave the item out." })] }), _jsx("div", { className: "review-gate-list", children: pendingReviewRows.map((row) => {
785
785
  const reviewed = reviewedReviewIds.has(row.item.line_id);
786
786
  const partial = hasBestAlternative(row);
787
787
  return (_jsx("div", { className: `review-gate-item${reviewed ? " is-reviewed" : ""}`, children: _jsxs("button", { type: "button", className: "review-gate-item__review", onClick: () => {