react-shopwave-connect 0.4.3 → 0.4.5
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/README.md +31 -4
- package/dist/core/index.cjs +526 -276
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +157 -6
- package/dist/core/index.d.ts +157 -6
- package/dist/core/index.js +517 -277
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +190 -170
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +19 -0
- package/dist/hooks/index.d.ts +19 -0
- package/dist/hooks/index.js +190 -170
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +526 -276
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +157 -6
- package/dist/index.d.ts +157 -6
- package/dist/index.js +517 -277
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +75 -57
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +21 -4
- package/dist/next/index.d.ts +21 -4
- package/dist/next/index.js +75 -58
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +75 -57
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +26 -5
- package/dist/server/index.d.ts +26 -5
- package/dist/server/index.js +75 -58
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/hooks/index.cjs
CHANGED
|
@@ -315,17 +315,29 @@ const UPLOAD_KINDS = Object.freeze({
|
|
|
315
315
|
applicationLogo: "applicaionLogo",
|
|
316
316
|
applicationImages: "applicaionImages"
|
|
317
317
|
});
|
|
318
|
+
/** Every `UploadPrefix`, longest first so a name check never takes `product` for `product_thumbnail`. */
|
|
319
|
+
const UPLOAD_PREFIXES = Object.freeze([
|
|
320
|
+
"product_thumbnail",
|
|
321
|
+
"variant_thumbnail",
|
|
322
|
+
"product",
|
|
323
|
+
"variant"
|
|
324
|
+
]);
|
|
318
325
|
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
319
326
|
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
320
327
|
route: "upload",
|
|
321
328
|
upstream: "uploader",
|
|
322
329
|
kindHeader: "contentType",
|
|
330
|
+
prefixHeader: "prefix",
|
|
323
331
|
fileField: "file",
|
|
324
|
-
kindField: "kind"
|
|
332
|
+
kindField: "kind",
|
|
333
|
+
prefixField: "prefix"
|
|
325
334
|
});
|
|
326
335
|
function isUploadKind(value) {
|
|
327
336
|
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
328
337
|
}
|
|
338
|
+
function isUploadPrefix(value) {
|
|
339
|
+
return typeof value === "string" && UPLOAD_PREFIXES.includes(value);
|
|
340
|
+
}
|
|
329
341
|
const SHOPWAVE_LOOKUPS = Object.freeze({
|
|
330
342
|
productTags: {
|
|
331
343
|
route: "products/tags",
|
|
@@ -492,150 +504,6 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
|
|
|
492
504
|
return json ?? void 0;
|
|
493
505
|
}
|
|
494
506
|
|
|
495
|
-
//#endregion
|
|
496
|
-
//#region src/core/basket.ts
|
|
497
|
-
const DEFAULT_BASKET_FILTERS = {
|
|
498
|
-
search: "",
|
|
499
|
-
dateRange: {
|
|
500
|
-
start: null,
|
|
501
|
-
end: null
|
|
502
|
-
},
|
|
503
|
-
consumerId: null,
|
|
504
|
-
status: "all"
|
|
505
|
-
};
|
|
506
|
-
const DEFAULT_PAGINATION = {
|
|
507
|
-
page: 0,
|
|
508
|
-
pageSize: 25,
|
|
509
|
-
total: 0
|
|
510
|
-
};
|
|
511
|
-
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
512
|
-
function formatDateForQuery(date, isEndOfDay = false) {
|
|
513
|
-
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
514
|
-
}
|
|
515
|
-
/** Builds the `Basket` report query from a set of filters. */
|
|
516
|
-
function buildBasketReportQuery(filters) {
|
|
517
|
-
const whereConditions = [];
|
|
518
|
-
const today = /* @__PURE__ */ new Date();
|
|
519
|
-
const startDate = filters.dateRange.start ?? today;
|
|
520
|
-
const endDate = filters.dateRange.end ?? today;
|
|
521
|
-
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
522
|
-
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
523
|
-
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
524
|
-
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
525
|
-
return { baskets: {
|
|
526
|
-
FROM: "Basket",
|
|
527
|
-
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
528
|
-
} };
|
|
529
|
-
}
|
|
530
|
-
/** Flattens the raw report rows/keys into structured baskets. */
|
|
531
|
-
function parseBasketReportData(reports) {
|
|
532
|
-
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
533
|
-
const { keys, data } = reports.baskets;
|
|
534
|
-
const keyIndices = {};
|
|
535
|
-
keys.forEach((key, index) => {
|
|
536
|
-
keyIndices[key.name] = index;
|
|
537
|
-
});
|
|
538
|
-
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
539
|
-
const productSubKeys = {};
|
|
540
|
-
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
541
|
-
productSubKeys[subKey.name] = index;
|
|
542
|
-
});
|
|
543
|
-
return data.map((row) => {
|
|
544
|
-
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
545
|
-
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
546
|
-
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
547
|
-
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
548
|
-
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
549
|
-
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
550
|
-
}));
|
|
551
|
-
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
552
|
-
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
553
|
-
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
554
|
-
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
555
|
-
return {
|
|
556
|
-
id: row[keyIndices["Basket.id"]],
|
|
557
|
-
name: row[keyIndices["Basket.name"]],
|
|
558
|
-
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
559
|
-
products,
|
|
560
|
-
total,
|
|
561
|
-
taxTotal,
|
|
562
|
-
status: "completed",
|
|
563
|
-
completeDate
|
|
564
|
-
};
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
568
|
-
function collectConsumerIds(baskets) {
|
|
569
|
-
if (!baskets.length) return null;
|
|
570
|
-
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
571
|
-
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
572
|
-
}
|
|
573
|
-
/** Unique product ids referenced by a set of baskets. */
|
|
574
|
-
function collectProductIds(baskets) {
|
|
575
|
-
if (!baskets.length) return null;
|
|
576
|
-
const ids = /* @__PURE__ */ new Set();
|
|
577
|
-
baskets.forEach((basket) => {
|
|
578
|
-
basket.products.forEach((product) => {
|
|
579
|
-
if (product.productId) ids.add(product.productId);
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
return ids.size > 0 ? [...ids] : null;
|
|
583
|
-
}
|
|
584
|
-
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
585
|
-
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
586
|
-
if (!baskets.length) return [];
|
|
587
|
-
let combinedRows = baskets.map((basket) => {
|
|
588
|
-
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
589
|
-
const productsWithNames = basket.products.map((p) => {
|
|
590
|
-
const product = productMap[p.productId];
|
|
591
|
-
return {
|
|
592
|
-
...p,
|
|
593
|
-
productName: product?.name || `Product #${p.productId}`
|
|
594
|
-
};
|
|
595
|
-
});
|
|
596
|
-
return {
|
|
597
|
-
id: basket.id,
|
|
598
|
-
basketId: basket.id,
|
|
599
|
-
basketName: basket.name || `Basket #${basket.id}`,
|
|
600
|
-
consumerId: basket.consumerId,
|
|
601
|
-
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
602
|
-
consumerEmail: consumer?.email || "-",
|
|
603
|
-
products: productsWithNames,
|
|
604
|
-
basketTotal: (basket.total || 0) / 100,
|
|
605
|
-
tax: basket.taxTotal || 0,
|
|
606
|
-
dateTime: basket.completeDate,
|
|
607
|
-
status: basket.status || "pending"
|
|
608
|
-
};
|
|
609
|
-
});
|
|
610
|
-
if (search) {
|
|
611
|
-
const searchLower = search.toLowerCase();
|
|
612
|
-
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
613
|
-
}
|
|
614
|
-
combinedRows.sort((a, b) => {
|
|
615
|
-
const dateA = new Date(a.dateTime).getTime();
|
|
616
|
-
return new Date(b.dateTime).getTime() - dateA;
|
|
617
|
-
});
|
|
618
|
-
return combinedRows;
|
|
619
|
-
}
|
|
620
|
-
/** Aggregate totals for a set of report rows. */
|
|
621
|
-
function computeBasketSummary(rows) {
|
|
622
|
-
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
623
|
-
return {
|
|
624
|
-
totalBaskets: rows.length,
|
|
625
|
-
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
626
|
-
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
627
|
-
uniqueConsumers: uniqueConsumerIds.size
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
/** Builds a consumer id → consumer lookup. */
|
|
631
|
-
function buildConsumerMap(consumers) {
|
|
632
|
-
const map = {};
|
|
633
|
-
if (consumers) consumers.forEach((consumer) => {
|
|
634
|
-
map[consumer.id] = consumer;
|
|
635
|
-
});
|
|
636
|
-
return map;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
507
|
//#endregion
|
|
640
508
|
//#region src/core/imageFit.ts
|
|
641
509
|
/**
|
|
@@ -671,6 +539,35 @@ function coverCrop(srcWidth, srcHeight, dstWidth, dstHeight) {
|
|
|
671
539
|
function requireBrowserImages(fn) {
|
|
672
540
|
if (typeof createImageBitmap !== "function") throw new Error(`${fn} needs a browser (createImageBitmap is not available)`);
|
|
673
541
|
}
|
|
542
|
+
/** Draws `crop` of the bitmap at `width × height` and encodes it (one resample). */
|
|
543
|
+
async function drawCrop(bitmap, crop, width, height, type, quality, fn) {
|
|
544
|
+
const { sx, sy, sw, sh } = crop;
|
|
545
|
+
if (typeof OffscreenCanvas === "function") {
|
|
546
|
+
const canvas$1 = new OffscreenCanvas(width, height);
|
|
547
|
+
const ctx$1 = canvas$1.getContext("2d");
|
|
548
|
+
if (!ctx$1) throw new Error(`${fn}: no 2D canvas context`);
|
|
549
|
+
ctx$1.imageSmoothingQuality = "high";
|
|
550
|
+
ctx$1.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
551
|
+
return canvas$1.convertToBlob({
|
|
552
|
+
type,
|
|
553
|
+
quality
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const canvas = document.createElement("canvas");
|
|
557
|
+
canvas.width = width;
|
|
558
|
+
canvas.height = height;
|
|
559
|
+
const ctx = canvas.getContext("2d");
|
|
560
|
+
if (!ctx) throw new Error(`${fn}: no 2D canvas context`);
|
|
561
|
+
ctx.imageSmoothingQuality = "high";
|
|
562
|
+
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
563
|
+
return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error(`${fn}: encoding failed`)), type, quality));
|
|
564
|
+
}
|
|
565
|
+
/** The input's name with the output type's extension. */
|
|
566
|
+
function outputName(file, type) {
|
|
567
|
+
const name = file.name || "image";
|
|
568
|
+
const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
|
|
569
|
+
return `${name.replace(/\.[^.]+$/, "")}.${ext}`;
|
|
570
|
+
}
|
|
674
571
|
/**
|
|
675
572
|
* Returns the image at exactly `width × height`: scaled and centre-cropped
|
|
676
573
|
* (never stretched). Browser only. The result is a `File` named like the
|
|
@@ -679,34 +576,11 @@ function requireBrowserImages(fn) {
|
|
|
679
576
|
async function fitImageToSize(file, width, height, options = {}) {
|
|
680
577
|
requireBrowserImages("fitImageToSize");
|
|
681
578
|
const { type = "image/png", quality, keepIfExact = true } = options;
|
|
682
|
-
const
|
|
683
|
-
const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
|
|
684
|
-
const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
|
|
579
|
+
const outName = outputName(file, type);
|
|
685
580
|
const bitmap = await createImageBitmap(file);
|
|
686
581
|
try {
|
|
687
582
|
if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
|
|
688
|
-
const
|
|
689
|
-
let blob;
|
|
690
|
-
if (typeof OffscreenCanvas === "function") {
|
|
691
|
-
const canvas = new OffscreenCanvas(width, height);
|
|
692
|
-
const ctx = canvas.getContext("2d");
|
|
693
|
-
if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
|
|
694
|
-
ctx.imageSmoothingQuality = "high";
|
|
695
|
-
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
696
|
-
blob = await canvas.convertToBlob({
|
|
697
|
-
type,
|
|
698
|
-
quality
|
|
699
|
-
});
|
|
700
|
-
} else {
|
|
701
|
-
const canvas = document.createElement("canvas");
|
|
702
|
-
canvas.width = width;
|
|
703
|
-
canvas.height = height;
|
|
704
|
-
const ctx = canvas.getContext("2d");
|
|
705
|
-
if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
|
|
706
|
-
ctx.imageSmoothingQuality = "high";
|
|
707
|
-
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
708
|
-
blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
|
|
709
|
-
}
|
|
583
|
+
const blob = await drawCrop(bitmap, coverCrop(bitmap.width, bitmap.height, width, height), width, height, type, quality, "fitImageToSize");
|
|
710
584
|
return new File([blob], outName, { type });
|
|
711
585
|
} finally {
|
|
712
586
|
bitmap.close?.();
|
|
@@ -746,10 +620,12 @@ function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
|
|
|
746
620
|
async function uploadImage(file, params, options = {}) {
|
|
747
621
|
if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
|
|
748
622
|
if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
|
|
623
|
+
if (params.prefix != null && !isUploadPrefix(params.prefix)) throw new Error(`uploadImage: unknown prefix "${params.prefix}"`);
|
|
749
624
|
const name = params.fileName ?? file.name ?? "upload";
|
|
750
625
|
const form = new FormData();
|
|
751
626
|
form.append(SHOPWAVE_UPLOAD.fileField, file, name);
|
|
752
627
|
form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
|
|
628
|
+
if (params.prefix != null) form.append(SHOPWAVE_UPLOAD.prefixField, params.prefix);
|
|
753
629
|
const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
|
|
754
630
|
method: "POST",
|
|
755
631
|
body: form
|
|
@@ -837,6 +713,150 @@ async function uploadMerchantImage(file, slot, options = {}) {
|
|
|
837
713
|
return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
|
|
838
714
|
}
|
|
839
715
|
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/core/basket.ts
|
|
718
|
+
const DEFAULT_BASKET_FILTERS = {
|
|
719
|
+
search: "",
|
|
720
|
+
dateRange: {
|
|
721
|
+
start: null,
|
|
722
|
+
end: null
|
|
723
|
+
},
|
|
724
|
+
consumerId: null,
|
|
725
|
+
status: "all"
|
|
726
|
+
};
|
|
727
|
+
const DEFAULT_PAGINATION = {
|
|
728
|
+
page: 0,
|
|
729
|
+
pageSize: 25,
|
|
730
|
+
total: 0
|
|
731
|
+
};
|
|
732
|
+
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
733
|
+
function formatDateForQuery(date, isEndOfDay = false) {
|
|
734
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
735
|
+
}
|
|
736
|
+
/** Builds the `Basket` report query from a set of filters. */
|
|
737
|
+
function buildBasketReportQuery(filters) {
|
|
738
|
+
const whereConditions = [];
|
|
739
|
+
const today = /* @__PURE__ */ new Date();
|
|
740
|
+
const startDate = filters.dateRange.start ?? today;
|
|
741
|
+
const endDate = filters.dateRange.end ?? today;
|
|
742
|
+
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
743
|
+
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
744
|
+
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
745
|
+
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
746
|
+
return { baskets: {
|
|
747
|
+
FROM: "Basket",
|
|
748
|
+
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
749
|
+
} };
|
|
750
|
+
}
|
|
751
|
+
/** Flattens the raw report rows/keys into structured baskets. */
|
|
752
|
+
function parseBasketReportData(reports) {
|
|
753
|
+
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
754
|
+
const { keys, data } = reports.baskets;
|
|
755
|
+
const keyIndices = {};
|
|
756
|
+
keys.forEach((key, index) => {
|
|
757
|
+
keyIndices[key.name] = index;
|
|
758
|
+
});
|
|
759
|
+
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
760
|
+
const productSubKeys = {};
|
|
761
|
+
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
762
|
+
productSubKeys[subKey.name] = index;
|
|
763
|
+
});
|
|
764
|
+
return data.map((row) => {
|
|
765
|
+
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
766
|
+
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
767
|
+
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
768
|
+
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
769
|
+
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
770
|
+
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
771
|
+
}));
|
|
772
|
+
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
773
|
+
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
774
|
+
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
775
|
+
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
776
|
+
return {
|
|
777
|
+
id: row[keyIndices["Basket.id"]],
|
|
778
|
+
name: row[keyIndices["Basket.name"]],
|
|
779
|
+
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
780
|
+
products,
|
|
781
|
+
total,
|
|
782
|
+
taxTotal,
|
|
783
|
+
status: "completed",
|
|
784
|
+
completeDate
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
789
|
+
function collectConsumerIds(baskets) {
|
|
790
|
+
if (!baskets.length) return null;
|
|
791
|
+
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
792
|
+
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
793
|
+
}
|
|
794
|
+
/** Unique product ids referenced by a set of baskets. */
|
|
795
|
+
function collectProductIds(baskets) {
|
|
796
|
+
if (!baskets.length) return null;
|
|
797
|
+
const ids = /* @__PURE__ */ new Set();
|
|
798
|
+
baskets.forEach((basket) => {
|
|
799
|
+
basket.products.forEach((product) => {
|
|
800
|
+
if (product.productId) ids.add(product.productId);
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
return ids.size > 0 ? [...ids] : null;
|
|
804
|
+
}
|
|
805
|
+
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
806
|
+
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
807
|
+
if (!baskets.length) return [];
|
|
808
|
+
let combinedRows = baskets.map((basket) => {
|
|
809
|
+
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
810
|
+
const productsWithNames = basket.products.map((p) => {
|
|
811
|
+
const product = productMap[p.productId];
|
|
812
|
+
return {
|
|
813
|
+
...p,
|
|
814
|
+
productName: product?.name || `Product #${p.productId}`
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
return {
|
|
818
|
+
id: basket.id,
|
|
819
|
+
basketId: basket.id,
|
|
820
|
+
basketName: basket.name || `Basket #${basket.id}`,
|
|
821
|
+
consumerId: basket.consumerId,
|
|
822
|
+
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
823
|
+
consumerEmail: consumer?.email || "-",
|
|
824
|
+
products: productsWithNames,
|
|
825
|
+
basketTotal: (basket.total || 0) / 100,
|
|
826
|
+
tax: basket.taxTotal || 0,
|
|
827
|
+
dateTime: basket.completeDate,
|
|
828
|
+
status: basket.status || "pending"
|
|
829
|
+
};
|
|
830
|
+
});
|
|
831
|
+
if (search) {
|
|
832
|
+
const searchLower = search.toLowerCase();
|
|
833
|
+
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
834
|
+
}
|
|
835
|
+
combinedRows.sort((a, b) => {
|
|
836
|
+
const dateA = new Date(a.dateTime).getTime();
|
|
837
|
+
return new Date(b.dateTime).getTime() - dateA;
|
|
838
|
+
});
|
|
839
|
+
return combinedRows;
|
|
840
|
+
}
|
|
841
|
+
/** Aggregate totals for a set of report rows. */
|
|
842
|
+
function computeBasketSummary(rows) {
|
|
843
|
+
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
844
|
+
return {
|
|
845
|
+
totalBaskets: rows.length,
|
|
846
|
+
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
847
|
+
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
848
|
+
uniqueConsumers: uniqueConsumerIds.size
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
/** Builds a consumer id → consumer lookup. */
|
|
852
|
+
function buildConsumerMap(consumers) {
|
|
853
|
+
const map = {};
|
|
854
|
+
if (consumers) consumers.forEach((consumer) => {
|
|
855
|
+
map[consumer.id] = consumer;
|
|
856
|
+
});
|
|
857
|
+
return map;
|
|
858
|
+
}
|
|
859
|
+
|
|
840
860
|
//#endregion
|
|
841
861
|
//#region src/core/merchant.ts
|
|
842
862
|
const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;
|