@metron-labs/quote-react 0.1.74 → 0.1.76
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/LICENSE +31 -31
- package/README.md +253 -253
- package/dist/app/stages/BasketStage.js +4 -3
- package/dist/embed/quote.js +6971 -6971
- package/dist/styles.css +6961 -6961
- package/dist/util/pricing.d.ts +1 -0
- package/dist/util/pricing.js +87 -4
- package/package.json +53 -53
package/dist/util/pricing.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface PricingBreakdown {
|
|
|
11
11
|
calculation: string;
|
|
12
12
|
costingMode: "direct" | "derived" | "review";
|
|
13
13
|
}
|
|
14
|
+
export declare const needsQuantityConfirmation: (item: CartItem, pricing: PricingBreakdown) => boolean;
|
|
14
15
|
export declare function getCartSubtotal(items: CartItem[], options?: Parameters<typeof getPricingBreakdown>[1]): number | null;
|
|
15
16
|
export declare function getPricingBreakdown(item: CartItem, options?: Parameters<typeof calculateBreakdown>[1]): PricingBreakdown;
|
|
16
17
|
declare function calculateBreakdown(item: CartItem, options?: {
|
package/dist/util/pricing.js
CHANGED
|
@@ -5,10 +5,25 @@ const unitAliases = {
|
|
|
5
5
|
"m²": "m2",
|
|
6
6
|
cbm: "m3",
|
|
7
7
|
"cu m": "m3",
|
|
8
|
+
"m³": "m3",
|
|
8
9
|
lm: "m",
|
|
9
10
|
"lin m": "m",
|
|
11
|
+
"linear metre": "m",
|
|
12
|
+
"linear metres": "m",
|
|
13
|
+
"linear meter": "m",
|
|
14
|
+
"linear meters": "m",
|
|
10
15
|
metre: "m",
|
|
16
|
+
metres: "m",
|
|
11
17
|
meter: "m",
|
|
18
|
+
meters: "m",
|
|
19
|
+
litre: "l",
|
|
20
|
+
litres: "l",
|
|
21
|
+
liter: "l",
|
|
22
|
+
liters: "l",
|
|
23
|
+
ltr: "l",
|
|
24
|
+
ltrs: "l",
|
|
25
|
+
rolls: "roll",
|
|
26
|
+
coils: "coil",
|
|
12
27
|
each: "each",
|
|
13
28
|
ea: "each",
|
|
14
29
|
nr: "each",
|
|
@@ -29,6 +44,7 @@ const unitAliases = {
|
|
|
29
44
|
};
|
|
30
45
|
const discreteSellUnits = new Set(["sheet", "length", "box", "bag", "pack", "roll", "set", "tub", "each"]);
|
|
31
46
|
const packUnits = new Set(["box", "bag", "pack", "pallet", "roll", "set", "tub"]);
|
|
47
|
+
const rollCountUnits = new Set(["roll", "coil"]);
|
|
32
48
|
const volumeInMl = (amount, unit) => {
|
|
33
49
|
if (unit === "ml")
|
|
34
50
|
return amount;
|
|
@@ -49,6 +65,39 @@ const canonicalUnit = (value) => {
|
|
|
49
65
|
const normalized = normalize(value);
|
|
50
66
|
return unitAliases[normalized] ?? normalized;
|
|
51
67
|
};
|
|
68
|
+
const unitScale = (value) => {
|
|
69
|
+
const unit = canonicalUnit(value);
|
|
70
|
+
if (unit === "ml")
|
|
71
|
+
return { dimension: "volume", toBase: 1 };
|
|
72
|
+
if (unit === "l")
|
|
73
|
+
return { dimension: "volume", toBase: 1000 };
|
|
74
|
+
if (unit === "m3")
|
|
75
|
+
return { dimension: "volume", toBase: 1000000 };
|
|
76
|
+
if (unit === "g")
|
|
77
|
+
return { dimension: "mass", toBase: 1 };
|
|
78
|
+
if (unit === "kg")
|
|
79
|
+
return { dimension: "mass", toBase: 1000 };
|
|
80
|
+
if (unit === "t" || unit === "tonne" || unit === "tonnes")
|
|
81
|
+
return { dimension: "mass", toBase: 1000000 };
|
|
82
|
+
if (unit === "mm")
|
|
83
|
+
return { dimension: "length", toBase: 0.001 };
|
|
84
|
+
if (unit === "cm")
|
|
85
|
+
return { dimension: "length", toBase: 0.01 };
|
|
86
|
+
if (unit === "m")
|
|
87
|
+
return { dimension: "length", toBase: 1 };
|
|
88
|
+
if (unit === "ft")
|
|
89
|
+
return { dimension: "length", toBase: 0.3048 };
|
|
90
|
+
if (unit === "in" || unit === "inch" || unit === "inches")
|
|
91
|
+
return { dimension: "length", toBase: 0.0254 };
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
const convertUnitAmount = (amount, from, to) => {
|
|
95
|
+
if (sameUnit(from, to))
|
|
96
|
+
return amount;
|
|
97
|
+
const source = unitScale(from);
|
|
98
|
+
const target = unitScale(to);
|
|
99
|
+
return source && target && source.dimension === target.dimension ? (amount * source.toBase) / target.toBase : null;
|
|
100
|
+
};
|
|
52
101
|
const sameUnit = (left, right) => {
|
|
53
102
|
const a = canonicalUnit(left);
|
|
54
103
|
const b = canonicalUnit(right);
|
|
@@ -79,6 +128,8 @@ const supportsRequestedUnit = (item, requestedUnit) => {
|
|
|
79
128
|
if (supported.length === 0 || supported.includes(requestedUnit)) {
|
|
80
129
|
return true;
|
|
81
130
|
}
|
|
131
|
+
if (item.unitConversions?.some((entry) => convertUnitAmount(1, requestedUnit, entry.requestUom) != null))
|
|
132
|
+
return true;
|
|
82
133
|
if (sameUnit(requestedUnit, "pallet") && item.itemsPerPallet != null && item.itemsPerPallet > 0) {
|
|
83
134
|
return true;
|
|
84
135
|
}
|
|
@@ -131,6 +182,8 @@ const coverageFromDescription = (item, requestedUnit, sellUnit) => {
|
|
|
131
182
|
const coverage = Number.parseFloat(match[1].replace(",", "."));
|
|
132
183
|
return Number.isFinite(coverage) && coverage > 0 ? coverage : null;
|
|
133
184
|
};
|
|
185
|
+
export const needsQuantityConfirmation = (item, pricing) => pricing.costingMode === "review" &&
|
|
186
|
+
!(item.conversionConfidence === "llm_basket_line" && item.sellUnitsOverride != null && !item.conversionNotes?.trim());
|
|
134
187
|
export function getCartSubtotal(items, options) {
|
|
135
188
|
const totals = items.map((item) => getPricingBreakdown(item, options).lineTotal);
|
|
136
189
|
return totals.some((total) => total == null) ? null : totals.reduce((sum, total) => sum + (total ?? 0), 0);
|
|
@@ -219,6 +272,32 @@ function calculateBreakdown(item, options) {
|
|
|
219
272
|
costingMode: "direct",
|
|
220
273
|
};
|
|
221
274
|
}
|
|
275
|
+
const singleFormRequest = rollCountUnits.has(requestedUnit);
|
|
276
|
+
const explicitMultiFormPack = /\b(?:pack|box)\s+of\s+(\d+)\s+(?:rolls?|coils?)\b|\b(\d+)\s+(?:rolls?|coils?)\s+(?:per|in each)\s+(?:pack|box)\b/i.test(item.description);
|
|
277
|
+
const itemsInSalePack = sellUnit === "pack" ? item.itemsPerPack : null;
|
|
278
|
+
const hasAreaOrLengthCoverage = (item.derivedLengthM ?? 0) > 0 ||
|
|
279
|
+
(item.derivedCoverageM2 ?? 0) > 0 ||
|
|
280
|
+
/^(?:m2|m²|sqm|m)\s*(?:per|\/)/i.test(item.coverageUom ?? "");
|
|
281
|
+
if (singleFormRequest &&
|
|
282
|
+
["each", "pack"].includes(sellUnit) &&
|
|
283
|
+
salesUnitSize === 1 &&
|
|
284
|
+
!(itemsInSalePack != null && itemsInSalePack > 1) &&
|
|
285
|
+
!explicitMultiFormPack &&
|
|
286
|
+
hasAreaOrLengthCoverage) {
|
|
287
|
+
const required = hasOverride ? overriddenSellUnits : roundQuantity(requestedAmount * (1 + wasteFactor), item.roundingRule ?? "round_up_each");
|
|
288
|
+
return {
|
|
289
|
+
requestedAmount,
|
|
290
|
+
requestedUnit,
|
|
291
|
+
salesUnitSize,
|
|
292
|
+
sellUnit,
|
|
293
|
+
calculatedSellUnits: required,
|
|
294
|
+
requiredSellUnits: required,
|
|
295
|
+
unitPrice: item.price,
|
|
296
|
+
lineTotal: item.price != null ? item.price * required : null,
|
|
297
|
+
calculation: `${requestedAmount} ${requestedUnit}${wasteFactor > 0 ? ` x ${1 + wasteFactor}` : ""} = ${required} ${sellUnit} (one product per sale unit)`,
|
|
298
|
+
costingMode: "derived",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
222
301
|
if (!supportsRequestedUnit(item, requestedUnit)) {
|
|
223
302
|
return review("Requested unit is not supported by this catalogue conversion.");
|
|
224
303
|
}
|
|
@@ -228,7 +307,8 @@ function calculateBreakdown(item, options) {
|
|
|
228
307
|
let calculation = `Quantity review required, initial request needed ${requestedAmount} ${requestedUnit}.`;
|
|
229
308
|
const wasteMultiplier = 1 + wasteFactor;
|
|
230
309
|
const descriptionCoverage = coverageFromDescription(item, requestedUnit, sellUnit);
|
|
231
|
-
const conversion = item.unitConversions?.find((entry) =>
|
|
310
|
+
const conversion = item.unitConversions?.find((entry) => convertUnitAmount(1, requestedUnit, entry.requestUom) != null);
|
|
311
|
+
const conversionRequestAmount = conversion ? convertUnitAmount(requestedAmount, requestedUnit, conversion.requestUom) : null;
|
|
232
312
|
const hasStructuredRoute = !!item.saleUnit && (sameUnit(requestedUnit, sellUnit) || !!conversion || (sameUnit(requestedUnit, "each") && !packUnits.has(sellUnit)));
|
|
233
313
|
// A partial structured conversion must not hide a valid legacy/LLM conversion.
|
|
234
314
|
if (hasStructuredRoute) {
|
|
@@ -237,8 +317,8 @@ function calculateBreakdown(item, options) {
|
|
|
237
317
|
costingMode = "direct";
|
|
238
318
|
calculation = `${requestedAmount} ${requestedUnit}${wasteFactor > 0 ? ` x ${wasteMultiplier}` : ""}`;
|
|
239
319
|
}
|
|
240
|
-
else if (conversion) {
|
|
241
|
-
rawSellUnits = (
|
|
320
|
+
else if (conversion && conversionRequestAmount != null) {
|
|
321
|
+
rawSellUnits = (conversionRequestAmount * wasteMultiplier) / conversion.quantityPerSaleUnit;
|
|
242
322
|
costingMode = "derived";
|
|
243
323
|
// A product sold individually can legitimately be requested by pallet. The
|
|
244
324
|
// canonical conversion is pallets per each (for example 1/72), but present
|
|
@@ -246,9 +326,12 @@ function calculateBreakdown(item, options) {
|
|
|
246
326
|
const itemsPerRequestedUnit = 1 / conversion.quantityPerSaleUnit;
|
|
247
327
|
const roundedItemsPerRequestedUnit = Math.round(itemsPerRequestedUnit);
|
|
248
328
|
const isWholePackRelationship = sameUnit(sellUnit, "each") && conversion.quantityPerSaleUnit < 1 && Math.abs(itemsPerRequestedUnit - roundedItemsPerRequestedUnit) < 1e-9;
|
|
329
|
+
const conversionBasis = sameUnit(requestedUnit, conversion.requestUom)
|
|
330
|
+
? `${requestedAmount} ${requestedUnit}`
|
|
331
|
+
: `${requestedAmount} ${requestedUnit} (${conversionRequestAmount} ${conversion.requestUom})`;
|
|
249
332
|
calculation = isWholePackRelationship
|
|
250
333
|
? `${requestedAmount} ${requestedUnit}${wasteFactor > 0 ? ` x ${wasteMultiplier}` : ""} x ${roundedItemsPerRequestedUnit} per ${requestedUnit}`
|
|
251
|
-
: `${
|
|
334
|
+
: `${conversionBasis}${wasteFactor > 0 ? ` x ${wasteMultiplier}` : ""} / ${conversion.quantityPerSaleUnit} ${conversion.requestUom} per ${sellUnit}`;
|
|
252
335
|
}
|
|
253
336
|
else if (sameUnit(requestedUnit, "each") && !packUnits.has(sellUnit)) {
|
|
254
337
|
rawSellUnits = (requestedAmount * wasteMultiplier) / salesUnitSize;
|
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@metron-labs/quote-react",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Metron Labs quote and estimating calculator components for React",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"sideEffects": [
|
|
7
|
-
"*.css"
|
|
8
|
-
],
|
|
9
|
-
"main": "./dist/index.js",
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": {
|
|
13
|
-
"types": "./dist/index.d.ts",
|
|
14
|
-
"default": "./dist/index.js"
|
|
15
|
-
},
|
|
16
|
-
"./styles.css": "./dist/styles.css",
|
|
17
|
-
"./*": {
|
|
18
|
-
"types": "./dist/*.d.ts",
|
|
19
|
-
"default": "./dist/*.js"
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"files": [
|
|
23
|
-
"dist"
|
|
24
|
-
],
|
|
25
|
-
"publishConfig": {
|
|
26
|
-
"access": "restricted"
|
|
27
|
-
},
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node scripts/copyStatic.mjs && node scripts/buildEmbed.mjs",
|
|
30
|
-
"typecheck": "tsc --noEmit",
|
|
31
|
-
"test": "vitest run",
|
|
32
|
-
"prepublishOnly": "npm run typecheck && npm run build",
|
|
33
|
-
"check:embed": "node scripts/checkEmbed.mjs"
|
|
34
|
-
},
|
|
35
|
-
"peerDependencies": {
|
|
36
|
-
"react": "^18.3.0 || ^19.0.0",
|
|
37
|
-
"react-dom": "^18.3.0 || ^19.0.0"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"xlsx": "^0.18.5"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"@types/node": "^20",
|
|
44
|
-
"@types/react": "^19",
|
|
45
|
-
"@types/react-dom": "^19",
|
|
46
|
-
"esbuild": "^0.28.2",
|
|
47
|
-
"react": "19.2.3",
|
|
48
|
-
"react-dom": "19.2.3",
|
|
49
|
-
"tsx": "^4",
|
|
50
|
-
"typescript": "^5",
|
|
51
|
-
"vitest": "^3"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@metron-labs/quote-react",
|
|
3
|
+
"version": "0.1.76",
|
|
4
|
+
"description": "Metron Labs quote and estimating calculator components for React",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"*.css"
|
|
8
|
+
],
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./styles.css": "./dist/styles.css",
|
|
17
|
+
"./*": {
|
|
18
|
+
"types": "./dist/*.d.ts",
|
|
19
|
+
"default": "./dist/*.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "restricted"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node scripts/copyStatic.mjs && node scripts/buildEmbed.mjs",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"prepublishOnly": "npm run typecheck && npm run build",
|
|
33
|
+
"check:embed": "node scripts/checkEmbed.mjs"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": "^18.3.0 || ^19.0.0",
|
|
37
|
+
"react-dom": "^18.3.0 || ^19.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"xlsx": "^0.18.5"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20",
|
|
44
|
+
"@types/react": "^19",
|
|
45
|
+
"@types/react-dom": "^19",
|
|
46
|
+
"esbuild": "^0.28.2",
|
|
47
|
+
"react": "19.2.3",
|
|
48
|
+
"react-dom": "19.2.3",
|
|
49
|
+
"tsx": "^4",
|
|
50
|
+
"typescript": "^5",
|
|
51
|
+
"vitest": "^3"
|
|
52
|
+
}
|
|
53
|
+
}
|