@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.
package/dist/app/QuickQuote.js
CHANGED
|
@@ -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
|
-
|
|
214
|
-
|
|
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: [
|
|
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 [
|
|
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
|
|
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
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
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, {}) }))] })] }) }))] })] }));
|