@metron-labs/quote-react 0.1.71 → 0.1.72

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.
@@ -24,7 +24,6 @@ export default function QuickQuote(props) {
24
24
  function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = false }) {
25
25
  const [stage, setStage] = useState("entry");
26
26
  const [entryMode, setEntryMode] = useState(null);
27
- const [error, setError] = useState("");
28
27
  const schedulePollCancelRef = useRef(null);
29
28
  const matchRunRef = useRef(0);
30
29
  const [processingStep, setProcessingStep] = useState(0);
@@ -161,7 +160,6 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
161
160
  const resetToEntry = () => {
162
161
  setStage("entry");
163
162
  setEntryMode(null);
164
- setError("");
165
163
  setProcessingStep(0);
166
164
  setScheduleResult(null);
167
165
  setScheduleReviewGateComplete(false);
@@ -175,7 +173,6 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
175
173
  // don't hit a stale-closure read.
176
174
  const runScheduleMatch = useCallback(async (file, providedLines, archiveSource = true) => {
177
175
  const run = ++matchRunRef.current;
178
- setError("");
179
176
  setProcessingStep(0);
180
177
  let step = 0;
181
178
  const advanceFakeStep = () => {
@@ -210,8 +207,8 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
210
207
  if (run !== matchRunRef.current)
211
208
  return;
212
209
  track("error", { stage: "matching" });
213
- setError(err?.message || "Failed to process schedule");
214
- setStage("entry");
210
+ // Every match starts from EntryStage's dialog, which shows the error next to the input.
211
+ throw err;
215
212
  }
216
213
  finally {
217
214
  if (run === matchRunRef.current) {
@@ -243,7 +240,7 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
243
240
  setEntryMode("schedule");
244
241
  await runScheduleMatch(file);
245
242
  }, [runScheduleMatch]);
246
- return (_jsx(TrackContext.Provider, { value: track, children: _jsx("div", { ref: rootRef, className: `app-wrapper${fullPage ? "" : " app-wrapper--embedded"}${hideHeader ? " app-wrapper--no-header" : ""}${!fullPage && stage === "processing" ? " app-wrapper--loading" : ""}`, style: getQuoteConfig().theme, children: _jsxs("main", { className: "main-content", children: [error && (_jsxs("div", { className: "error-box", role: "alert", children: [_jsx("strong", { children: "Error:" }), " ", error] })), stage === "entry" && (_jsx(EntryStage, { onPasteSchedule: handlePasteSchedule, onUploadSchedule: handleUploadSchedule, onEntryModeChange: setEntryMode, draft: entryDraft, onSubmitDraft: setEntryDraft, onCancelMatch: cancelMatch })), stage === "results" && entryMode === "schedule" && scheduleResult && (_jsx(ScheduleResultsStage, { scheduleResult: scheduleResult, onStartOver: resetToEntry, cart: cart, onAddToCart: addToCart, onRemoveFromCart: removeFromCart, onGoToBasket: goToBasket, cartSeeded: cartSeeded, onCartSeeded: () => setCartSeeded(true), reviewGateComplete: scheduleReviewGateComplete, onReviewGateComplete: () => setScheduleReviewGateComplete(true), internal: internal })), stage === "basket" && (_jsx(BasketStage, { cart: cart, onUpdateQty: updateCartQty, onUpdateSellUnits: updateCartSellUnits, onResolveConversions: updateCartConversions, onRemove: removeFromCart, onBack: backToResults, onStartNewQuote: resetToEntry, onQuoteRequested: () => {
243
+ return (_jsx(TrackContext.Provider, { value: track, children: _jsx("div", { ref: rootRef, className: `app-wrapper${fullPage ? "" : " app-wrapper--embedded"}${hideHeader ? " app-wrapper--no-header" : ""}${!fullPage && stage === "processing" ? " app-wrapper--loading" : ""}`, style: getQuoteConfig().theme, children: _jsxs("main", { className: "main-content", children: [stage === "entry" && (_jsx(EntryStage, { onPasteSchedule: handlePasteSchedule, onUploadSchedule: handleUploadSchedule, onEntryModeChange: setEntryMode, draft: entryDraft, onSubmitDraft: setEntryDraft, onCancelMatch: cancelMatch })), stage === "results" && entryMode === "schedule" && scheduleResult && (_jsx(ScheduleResultsStage, { scheduleResult: scheduleResult, onStartOver: resetToEntry, cart: cart, onAddToCart: addToCart, onRemoveFromCart: removeFromCart, onGoToBasket: goToBasket, cartSeeded: cartSeeded, onCartSeeded: () => setCartSeeded(true), reviewGateComplete: scheduleReviewGateComplete, onReviewGateComplete: () => setScheduleReviewGateComplete(true), internal: internal })), stage === "basket" && (_jsx(BasketStage, { cart: cart, onUpdateQty: updateCartQty, onUpdateSellUnits: updateCartSellUnits, onResolveConversions: updateCartConversions, onRemove: removeFromCart, onBack: backToResults, onStartNewQuote: resetToEntry, onQuoteRequested: () => {
247
244
  setScheduleResult(null);
248
245
  setScheduleReviewGateComplete(false);
249
246
  setCartSeeded(false);
@@ -35,7 +35,7 @@ const orderSummary = (item, pricing) => {
35
35
  const reviewMessage = (pricing) => {
36
36
  if (["box", "pack"].includes(pricing.requestedUnit))
37
37
  return "Pack size needed to calculate the order quantity.";
38
- return `This product is sold by ${unitDisplayText(pricing.sellUnit, 1)}.`;
38
+ return `This product is sold by the ${unitDisplayText(pricing.sellUnit, 1)}.`;
39
39
  };
40
40
  export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, onQuoteRequested, internal = false, }) {
41
41
  // Both widgets can render a basket on the same page, so element ids are made unique.
@@ -114,9 +114,7 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
114
114
  updates[resolved.line_id] = {
115
115
  conversionConfidence: conversion.conversion_confidence,
116
116
  conversionNotes: conversion.conversion_notes,
117
- ...(Number.isFinite(conversion.suggested_order_qty) && conversion.suggested_order_qty > 0
118
- ? { sellUnitsOverride: conversion.suggested_order_qty }
119
- : {}),
117
+ ...(Number.isFinite(conversion.suggested_order_qty) && conversion.suggested_order_qty > 0 ? { sellUnitsOverride: conversion.suggested_order_qty } : {}),
120
118
  };
121
119
  }
122
120
  if (Object.keys(updates).length > 0)
@@ -32,7 +32,7 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
32
32
  const [activeTab, setActiveTab] = useState(() => (draft && typeof window !== "undefined" && window.location.hash === DIALOG_HASH ? draft.tab : null));
33
33
  const [pastedText, setPastedText] = useState(draft?.text ?? "");
34
34
  const [uploadFile, setUploadFile] = useState(draft?.file ?? null);
35
- const [uploadError, setUploadError] = useState("");
35
+ const [dialogError, setDialogError] = useState("");
36
36
  const [matching, setMatching] = useState(false);
37
37
  const [pageDragOver, setPageDragOver] = useState(false);
38
38
  const [photoPreviewUrl, setPhotoPreviewUrl] = useState(null);
@@ -101,6 +101,7 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
101
101
  const openWorkflow = (tab) => {
102
102
  track("method_selected", { method: tab });
103
103
  setActiveTab(tab);
104
+ setDialogError("");
104
105
  onEntryModeChange(null);
105
106
  const hash = window.location.hash;
106
107
  if (hash === DIALOG_HASH)
@@ -126,13 +127,13 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
126
127
  if (!files?.length)
127
128
  return;
128
129
  if (files.length > 1)
129
- return setUploadError("Please choose one file at a time.");
130
+ return setDialogError("Please choose one file at a time.");
130
131
  const file = files[0];
131
132
  // Photos can be shrunk before their final size check.
132
133
  const problem = validateUploadFile(file, isPhotoFile(file) ? Infinity : undefined);
133
134
  if (problem)
134
- return setUploadError(problem);
135
- setUploadError("");
135
+ return setDialogError(problem);
136
+ setDialogError("");
136
137
  setUploadFile(file);
137
138
  };
138
139
  const startMatching = async (run) => {
@@ -145,10 +146,15 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
145
146
  if (activeTab)
146
147
  onSubmitDraft?.({ tab: activeTab, text: activeTab === "paste" ? pastedText : "", file: activeTab === "upload" ? uploadFile : null });
147
148
  const id = ++matchRun.current;
149
+ setDialogError("");
148
150
  setMatching(true);
149
151
  try {
150
152
  await run();
151
153
  }
154
+ catch (error) {
155
+ if (id === matchRun.current)
156
+ setDialogError(error?.message || "Failed to process your list");
157
+ }
152
158
  finally {
153
159
  if (id === matchRun.current)
154
160
  setMatching(false);
@@ -161,17 +167,18 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
161
167
  return startMatching(async () => {
162
168
  if (!isPhotoFile(uploadFile))
163
169
  return onUploadSchedule(uploadFile);
170
+ let photo;
164
171
  try {
165
- const photo = await shrinkPhoto(uploadFile);
172
+ photo = await shrinkPhoto(uploadFile);
166
173
  const problem = validateUploadFile(photo);
167
174
  if (problem)
168
175
  throw new Error(problem);
169
- await onUploadSchedule(photo);
170
176
  }
171
177
  catch (error) {
172
178
  track("error", { stage: "photo" });
173
- setUploadError(error?.message || "Failed to process image upload");
179
+ throw error;
174
180
  }
181
+ await onUploadSchedule(photo);
175
182
  });
176
183
  };
177
184
  const isFileDrag = (event) => event.dataTransfer.types.includes("Files");
@@ -200,13 +207,13 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
200
207
  applyUploadFile(event.dataTransfer.files);
201
208
  openWorkflow("upload");
202
209
  };
203
- return (_jsxs("div", { className: "wizard-step entry-stage", onDragEnter: onPageDragEnter, onDragOver: (event) => isFileDrag(event) && event.preventDefault(), onDragLeave: onPageDragLeave, onDrop: onPageDrop, children: [pageDragOver && (_jsx("div", { className: "entry-drop-overlay", "aria-hidden": "true", children: "Drop your file to upload" })), _jsxs("div", { className: "quick-order-card", children: [_jsxs("div", { className: "quick-order-alt", children: [_jsxs("div", { className: "quick-order-entry-copy", children: [_jsx("div", { className: "quick-order-alt-title", children: "How would you like to get started?" }), _jsx("p", { className: "quick-order-alt-intro", children: "Upload a file or type a list of items and quantities. We'll match them to our products and help you build a quote." })] }), _jsxs("div", { className: "entry-options entry-options--workflow entry-options--two", children: [_jsxs("button", { type: "button", className: `entry-option-card entry-option-card--paste${activeTab === "paste" ? " entry-option-card--active" : ""}`, onClick: () => openWorkflow("paste"), children: [_jsxs("span", { className: "entry-option-content", children: [_jsx("span", { className: "entry-option-title", children: "Type or paste a list" }), _jsx("span", { className: "entry-option-desc", children: "Enter your materials and quantities" })] }), _jsx("span", { className: "entry-option-media entry-option-media--paste", "aria-hidden": "true", children: _jsx("img", { className: "fill-img", src: assetUrl("/quick-order/paste-list.webp"), alt: "" }) })] }), _jsxs("button", { type: "button", className: `entry-option-card entry-option-card--upload${activeTab === "upload" ? " entry-option-card--active" : ""}`, onClick: () => openWorkflow("upload"), children: [_jsxs("span", { className: "entry-option-content", children: [_jsx("span", { className: "entry-option-title", children: "Upload a file" }), _jsx("span", { className: "entry-option-desc", children: "A photo, spreadsheet, Word document or PDF" })] }), _jsx("span", { className: "entry-option-media entry-option-media--upload", "aria-hidden": "true", children: _jsx("img", { className: "fill-img", src: assetUrl("/quick-order/photo-list.webp"), alt: "" }) })] })] })] }), activeTab && (_jsx("div", { className: "modal-backdrop quick-order-modal-backdrop", role: "dialog", "aria-modal": "true", "aria-labelledby": "quick-order-workflow-title", onClick: () => !hasWorkflowInput && closeWorkflow(), children: _jsxs("section", { className: `quick-order-workflow-modal${matching ? " quick-order-workflow-modal--matching" : ""}`, onClick: (event) => event.stopPropagation(), children: [_jsxs("header", { className: "quick-order-workflow-modal-header", children: [_jsx("h2", { id: "quick-order-workflow-title", children: activeTab === "paste" ? "Type or paste a list" : "Upload a file" }), _jsx("button", { type: "button", className: "quick-order-workflow-close", onClick: closeWorkflow, "aria-label": "Close", children: _jsx(CloseIcon, {}) })] }), _jsxs("div", { className: "quick-order-panel", children: [_jsxs("div", { className: "quick-order-body", "aria-hidden": matching, children: [_jsx("div", { className: "quick-order-body-main", children: activeTab === "paste" ? (_jsxs(_Fragment, { children: [_jsx("textarea", { className: "paste-textarea", placeholder: "Paste your line items here, one per line, e.g.\ninsulated plasterboard 50mm\nC24 timber 4.8m", value: pastedText, onChange: (event) => setPastedText(event.target.value), "aria-label": "Paste schedule line items" }), pasteLineProblem && (_jsxs("div", { className: "error-box", role: "alert", children: [_jsx("strong", { children: "Error:" }), " ", pasteLineProblem] })), _jsxs("div", { className: "quick-order-hint", children: [_jsx("strong", { children: "Tip:" }), "\u00A0Paste one line per item. Include a count, e.g. \"x10\", to help us find the right stock and pricing."] }), _jsx("div", { className: "quick-order-actions", children: _jsxs("button", { type: "button", className: "cta-btn", disabled: !lineCount || Boolean(pasteLineProblem), onClick: () => {
210
+ return (_jsxs("div", { className: "wizard-step entry-stage", onDragEnter: onPageDragEnter, onDragOver: (event) => isFileDrag(event) && event.preventDefault(), onDragLeave: onPageDragLeave, onDrop: onPageDrop, children: [pageDragOver && (_jsx("div", { className: "entry-drop-overlay", "aria-hidden": "true", children: "Drop your file to upload" })), _jsxs("div", { className: "quick-order-card", children: [_jsxs("div", { className: "quick-order-alt", children: [_jsxs("div", { className: "quick-order-entry-copy", children: [_jsx("div", { className: "quick-order-alt-title", children: "How would you like to get started?" }), _jsx("p", { className: "quick-order-alt-intro", children: "Upload a file or type a list of items and quantities. We'll match them to our products and help you build a quote." })] }), _jsxs("div", { className: "entry-options entry-options--workflow entry-options--two", children: [_jsxs("button", { type: "button", className: `entry-option-card entry-option-card--paste${activeTab === "paste" ? " entry-option-card--active" : ""}`, onClick: () => openWorkflow("paste"), children: [_jsxs("span", { className: "entry-option-content", children: [_jsx("span", { className: "entry-option-title", children: "Type or paste a list" }), _jsx("span", { className: "entry-option-desc", children: "Enter your materials and quantities" })] }), _jsx("span", { className: "entry-option-media entry-option-media--paste", "aria-hidden": "true", children: _jsx("img", { className: "fill-img", src: assetUrl("/quick-order/paste-list.webp"), alt: "" }) })] }), _jsxs("button", { type: "button", className: `entry-option-card entry-option-card--upload${activeTab === "upload" ? " entry-option-card--active" : ""}`, onClick: () => openWorkflow("upload"), children: [_jsxs("span", { className: "entry-option-content", children: [_jsx("span", { className: "entry-option-title", children: "Upload a file" }), _jsx("span", { className: "entry-option-desc", children: "A photo, spreadsheet, Word document or PDF" })] }), _jsx("span", { className: "entry-option-media entry-option-media--upload", "aria-hidden": "true", children: _jsx("img", { className: "fill-img", src: assetUrl("/quick-order/photo-list.webp"), alt: "" }) })] })] })] }), activeTab && (_jsx("div", { className: "modal-backdrop quick-order-modal-backdrop", role: "dialog", "aria-modal": "true", "aria-labelledby": "quick-order-workflow-title", onClick: () => !hasWorkflowInput && closeWorkflow(), children: _jsxs("section", { className: `quick-order-workflow-modal${matching ? " quick-order-workflow-modal--matching" : ""}`, onClick: (event) => event.stopPropagation(), children: [_jsxs("header", { className: "quick-order-workflow-modal-header", children: [_jsx("h2", { id: "quick-order-workflow-title", children: activeTab === "paste" ? "Type or paste a list" : "Upload a file" }), _jsx("button", { type: "button", className: "quick-order-workflow-close", onClick: closeWorkflow, "aria-label": "Close", children: _jsx(CloseIcon, {}) })] }), _jsxs("div", { className: "quick-order-panel", children: [_jsxs("div", { className: "quick-order-body", "aria-hidden": matching, children: [_jsx("div", { className: "quick-order-body-main", children: activeTab === "paste" ? (_jsxs(_Fragment, { children: [_jsx("textarea", { className: "paste-textarea", placeholder: "Paste your line items here, one per line, e.g.\ninsulated plasterboard 50mm\nC24 timber 4.8m", value: pastedText, onChange: (event) => setPastedText(event.target.value), "aria-label": "Paste schedule line items" }), (pasteLineProblem || dialogError) && (_jsxs("div", { className: "error-box", role: "alert", children: [_jsx("strong", { children: "Error:" }), " ", pasteLineProblem || dialogError] })), _jsxs("div", { className: "quick-order-hint", children: [_jsx("strong", { children: "Tip:" }), "\u00A0Paste one line per item. Include a count, e.g. \"x10\", to help us find the right stock and pricing."] }), _jsx("div", { className: "quick-order-actions", children: _jsxs("button", { type: "button", className: "cta-btn", disabled: !lineCount || Boolean(pasteLineProblem), onClick: () => {
204
211
  track("list_submitted", { method: "paste", lines: lineCount });
205
212
  void startMatching(() => onPasteSchedule(pastedText));
206
- }, "aria-label": "Match pasted lines", children: [_jsx("span", { children: "Match line items" }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] }) })] })) : (_jsxs(_Fragment, { children: [uploadError && (_jsxs("div", { className: "error-box", role: "alert", children: [_jsx("strong", { children: "Error:" }), " ", uploadError] })), _jsxs("div", { className: `upload-zone${uploadFile ? " has-file" : ""}`, onClick: () => fileInputRef.current?.click(), onDragOver: (event) => event.preventDefault(), onDrop: onPageDrop, role: "button", tabIndex: 0, onKeyDown: (event) => (event.key === "Enter" || event.key === " ") && fileInputRef.current?.click(), "aria-label": "Upload zone: click or drag a list or image file here", children: [photoPreviewUrl ? (_jsx("img", { src: photoPreviewUrl, alt: uploadFile?.name ?? "Selected photo", className: "upload-zone-photo-preview" })) : (_jsx("div", { className: "upload-zone-icon", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), _jsx("polyline", { points: "14,2 14,8 20,8" }), _jsx("polyline", { points: "9,15 11,17 15,13" })] }) })), uploadFile ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "upload-zone-subtext", children: uploadFile.name }), _jsx("button", { type: "button", className: "link-btn link-btn--underline", onClick: (event) => {
213
+ }, "aria-label": "Match pasted lines", children: [_jsx("span", { children: "Match line items" }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] }) })] })) : (_jsxs(_Fragment, { children: [dialogError && (_jsxs("div", { className: "error-box", role: "alert", children: [_jsx("strong", { children: "Error:" }), " ", dialogError] })), _jsxs("div", { className: `upload-zone${uploadFile ? " has-file" : ""}`, onClick: () => fileInputRef.current?.click(), onDragOver: (event) => event.preventDefault(), onDrop: onPageDrop, role: "button", tabIndex: 0, onKeyDown: (event) => (event.key === "Enter" || event.key === " ") && fileInputRef.current?.click(), "aria-label": "Upload zone: click or drag a list or image file here", children: [photoPreviewUrl ? (_jsx("img", { src: photoPreviewUrl, alt: uploadFile?.name ?? "Selected photo", className: "upload-zone-photo-preview" })) : (_jsx("div", { className: "upload-zone-icon", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), _jsx("polyline", { points: "14,2 14,8 20,8" }), _jsx("polyline", { points: "9,15 11,17 15,13" })] }) })), uploadFile ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "upload-zone-subtext", children: uploadFile.name }), _jsx("button", { type: "button", className: "link-btn link-btn--underline", onClick: (event) => {
207
214
  event.stopPropagation();
208
215
  setUploadFile(null);
209
- setUploadError("");
216
+ setDialogError("");
210
217
  if (fileInputRef.current)
211
218
  fileInputRef.current.value = "";
212
219
  }, children: "Remove" })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "upload-zone-text", children: "Drop a photo, spreadsheet, Word document or PDF" }), _jsx("div", { className: "upload-zone-subtext", children: "or click to choose a file" })] })), _jsx("input", { ref: fileInputRef, type: "file", accept: UPLOAD_ACCEPT, className: "visually-hidden", onChange: (event) => applyUploadFile(event.target.files), tabIndex: -1, "aria-hidden": "true" })] }), _jsx("div", { className: "quick-order-actions", children: _jsxs("button", { type: "button", className: "cta-btn", disabled: !uploadFile, onClick: () => void matchUpload(), "aria-label": "Match uploaded items", children: [_jsx("span", { children: "Match items" }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] }) })] })) }), _jsx("aside", { className: "quick-order-sidebar", children: STEPS[activeTab].map((step, index) => (_jsxs("div", { className: "quick-order-step quick-order-step--rail", children: [_jsx("span", { className: "quick-order-step-dot", children: index + 1 }), _jsxs("div", { children: [_jsx("div", { className: "quick-order-step-title", children: step.title }), _jsx("div", { className: "quick-order-step-desc", children: step.desc })] })] }, step.title))) })] }), matching && (_jsx("div", { className: "quick-order-matching-overlay", children: _jsx(ProcessingContent, {}) }))] })] }) }))] })] }));