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.d.cts
CHANGED
|
@@ -354,6 +354,19 @@ interface BasketPagination {
|
|
|
354
354
|
* the correctly spelt names here and the server maps them.
|
|
355
355
|
*/
|
|
356
356
|
type UploadKind = "merchant" | "merchantFeature" | "product" | "user" | "applicationLogo" | "applicationImages";
|
|
357
|
+
/**
|
|
358
|
+
* The role an image plays, sent to Shopwave as the uploader's `prefix` header.
|
|
359
|
+
* The uploader names the file `<prefix>_<hash>.<ext>`, so the role travels with
|
|
360
|
+
* the file wherever a record stores it (a product's `images`). Live, merchant
|
|
361
|
+
* 5644, 4 Nov 2026: all four values answered 201.
|
|
362
|
+
* - `product`: a product gallery image;
|
|
363
|
+
* - `product_thumbnail`: the product's lead image, shown in lists;
|
|
364
|
+
* - `variant` / `variant_thumbnail`: a variant's own image and its lead image
|
|
365
|
+
* (used by the Connect integrations).
|
|
366
|
+
*
|
|
367
|
+
* Values are the ones Connect's `uploadFileToShopwave` sends.
|
|
368
|
+
*/
|
|
369
|
+
type UploadPrefix = "product" | "product_thumbnail" | "variant" | "variant_thumbnail";
|
|
357
370
|
//#endregion
|
|
358
371
|
//#region src/core/imageFit.d.ts
|
|
359
372
|
interface FitImageOptions {
|
|
@@ -369,6 +382,12 @@ interface FitImageOptions {
|
|
|
369
382
|
interface UploadImageParams {
|
|
370
383
|
/** What the image is for (sent to Shopwave as the uploader's `contentType`). */
|
|
371
384
|
kind: UploadKind;
|
|
385
|
+
/**
|
|
386
|
+
* The image's role (sent to Shopwave as the uploader's `prefix` header), e.g.
|
|
387
|
+
* `product_thumbnail` for a product's lead image. Leave it out for images
|
|
388
|
+
* without a role (merchant logos).
|
|
389
|
+
*/
|
|
390
|
+
prefix?: UploadPrefix;
|
|
372
391
|
/** File name to send when `file` is a Blob without one. Defaults to `File.name`, else `"upload"`. */
|
|
373
392
|
fileName?: string;
|
|
374
393
|
}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -354,6 +354,19 @@ interface BasketPagination {
|
|
|
354
354
|
* the correctly spelt names here and the server maps them.
|
|
355
355
|
*/
|
|
356
356
|
type UploadKind = "merchant" | "merchantFeature" | "product" | "user" | "applicationLogo" | "applicationImages";
|
|
357
|
+
/**
|
|
358
|
+
* The role an image plays, sent to Shopwave as the uploader's `prefix` header.
|
|
359
|
+
* The uploader names the file `<prefix>_<hash>.<ext>`, so the role travels with
|
|
360
|
+
* the file wherever a record stores it (a product's `images`). Live, merchant
|
|
361
|
+
* 5644, 4 Nov 2026: all four values answered 201.
|
|
362
|
+
* - `product`: a product gallery image;
|
|
363
|
+
* - `product_thumbnail`: the product's lead image, shown in lists;
|
|
364
|
+
* - `variant` / `variant_thumbnail`: a variant's own image and its lead image
|
|
365
|
+
* (used by the Connect integrations).
|
|
366
|
+
*
|
|
367
|
+
* Values are the ones Connect's `uploadFileToShopwave` sends.
|
|
368
|
+
*/
|
|
369
|
+
type UploadPrefix = "product" | "product_thumbnail" | "variant" | "variant_thumbnail";
|
|
357
370
|
//#endregion
|
|
358
371
|
//#region src/core/imageFit.d.ts
|
|
359
372
|
interface FitImageOptions {
|
|
@@ -369,6 +382,12 @@ interface FitImageOptions {
|
|
|
369
382
|
interface UploadImageParams {
|
|
370
383
|
/** What the image is for (sent to Shopwave as the uploader's `contentType`). */
|
|
371
384
|
kind: UploadKind;
|
|
385
|
+
/**
|
|
386
|
+
* The image's role (sent to Shopwave as the uploader's `prefix` header), e.g.
|
|
387
|
+
* `product_thumbnail` for a product's lead image. Leave it out for images
|
|
388
|
+
* without a role (merchant logos).
|
|
389
|
+
*/
|
|
390
|
+
prefix?: UploadPrefix;
|
|
372
391
|
/** File name to send when `file` is a Blob without one. Defaults to `File.name`, else `"upload"`. */
|
|
373
392
|
fileName?: string;
|
|
374
393
|
}
|
package/dist/hooks/index.js
CHANGED
|
@@ -291,17 +291,29 @@ const UPLOAD_KINDS = Object.freeze({
|
|
|
291
291
|
applicationLogo: "applicaionLogo",
|
|
292
292
|
applicationImages: "applicaionImages"
|
|
293
293
|
});
|
|
294
|
+
/** Every `UploadPrefix`, longest first so a name check never takes `product` for `product_thumbnail`. */
|
|
295
|
+
const UPLOAD_PREFIXES = Object.freeze([
|
|
296
|
+
"product_thumbnail",
|
|
297
|
+
"variant_thumbnail",
|
|
298
|
+
"product",
|
|
299
|
+
"variant"
|
|
300
|
+
]);
|
|
294
301
|
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
295
302
|
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
296
303
|
route: "upload",
|
|
297
304
|
upstream: "uploader",
|
|
298
305
|
kindHeader: "contentType",
|
|
306
|
+
prefixHeader: "prefix",
|
|
299
307
|
fileField: "file",
|
|
300
|
-
kindField: "kind"
|
|
308
|
+
kindField: "kind",
|
|
309
|
+
prefixField: "prefix"
|
|
301
310
|
});
|
|
302
311
|
function isUploadKind(value) {
|
|
303
312
|
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
304
313
|
}
|
|
314
|
+
function isUploadPrefix(value) {
|
|
315
|
+
return typeof value === "string" && UPLOAD_PREFIXES.includes(value);
|
|
316
|
+
}
|
|
305
317
|
const SHOPWAVE_LOOKUPS = Object.freeze({
|
|
306
318
|
productTags: {
|
|
307
319
|
route: "products/tags",
|
|
@@ -468,150 +480,6 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
|
|
|
468
480
|
return json ?? void 0;
|
|
469
481
|
}
|
|
470
482
|
|
|
471
|
-
//#endregion
|
|
472
|
-
//#region src/core/basket.ts
|
|
473
|
-
const DEFAULT_BASKET_FILTERS = {
|
|
474
|
-
search: "",
|
|
475
|
-
dateRange: {
|
|
476
|
-
start: null,
|
|
477
|
-
end: null
|
|
478
|
-
},
|
|
479
|
-
consumerId: null,
|
|
480
|
-
status: "all"
|
|
481
|
-
};
|
|
482
|
-
const DEFAULT_PAGINATION = {
|
|
483
|
-
page: 0,
|
|
484
|
-
pageSize: 25,
|
|
485
|
-
total: 0
|
|
486
|
-
};
|
|
487
|
-
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
488
|
-
function formatDateForQuery(date, isEndOfDay = false) {
|
|
489
|
-
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
490
|
-
}
|
|
491
|
-
/** Builds the `Basket` report query from a set of filters. */
|
|
492
|
-
function buildBasketReportQuery(filters) {
|
|
493
|
-
const whereConditions = [];
|
|
494
|
-
const today = /* @__PURE__ */ new Date();
|
|
495
|
-
const startDate = filters.dateRange.start ?? today;
|
|
496
|
-
const endDate = filters.dateRange.end ?? today;
|
|
497
|
-
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
498
|
-
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
499
|
-
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
500
|
-
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
501
|
-
return { baskets: {
|
|
502
|
-
FROM: "Basket",
|
|
503
|
-
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
504
|
-
} };
|
|
505
|
-
}
|
|
506
|
-
/** Flattens the raw report rows/keys into structured baskets. */
|
|
507
|
-
function parseBasketReportData(reports) {
|
|
508
|
-
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
509
|
-
const { keys, data } = reports.baskets;
|
|
510
|
-
const keyIndices = {};
|
|
511
|
-
keys.forEach((key, index) => {
|
|
512
|
-
keyIndices[key.name] = index;
|
|
513
|
-
});
|
|
514
|
-
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
515
|
-
const productSubKeys = {};
|
|
516
|
-
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
517
|
-
productSubKeys[subKey.name] = index;
|
|
518
|
-
});
|
|
519
|
-
return data.map((row) => {
|
|
520
|
-
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
521
|
-
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
522
|
-
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
523
|
-
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
524
|
-
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
525
|
-
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
526
|
-
}));
|
|
527
|
-
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
528
|
-
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
529
|
-
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
530
|
-
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
531
|
-
return {
|
|
532
|
-
id: row[keyIndices["Basket.id"]],
|
|
533
|
-
name: row[keyIndices["Basket.name"]],
|
|
534
|
-
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
535
|
-
products,
|
|
536
|
-
total,
|
|
537
|
-
taxTotal,
|
|
538
|
-
status: "completed",
|
|
539
|
-
completeDate
|
|
540
|
-
};
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
544
|
-
function collectConsumerIds(baskets) {
|
|
545
|
-
if (!baskets.length) return null;
|
|
546
|
-
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
547
|
-
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
548
|
-
}
|
|
549
|
-
/** Unique product ids referenced by a set of baskets. */
|
|
550
|
-
function collectProductIds(baskets) {
|
|
551
|
-
if (!baskets.length) return null;
|
|
552
|
-
const ids = /* @__PURE__ */ new Set();
|
|
553
|
-
baskets.forEach((basket) => {
|
|
554
|
-
basket.products.forEach((product) => {
|
|
555
|
-
if (product.productId) ids.add(product.productId);
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
return ids.size > 0 ? [...ids] : null;
|
|
559
|
-
}
|
|
560
|
-
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
561
|
-
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
562
|
-
if (!baskets.length) return [];
|
|
563
|
-
let combinedRows = baskets.map((basket) => {
|
|
564
|
-
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
565
|
-
const productsWithNames = basket.products.map((p) => {
|
|
566
|
-
const product = productMap[p.productId];
|
|
567
|
-
return {
|
|
568
|
-
...p,
|
|
569
|
-
productName: product?.name || `Product #${p.productId}`
|
|
570
|
-
};
|
|
571
|
-
});
|
|
572
|
-
return {
|
|
573
|
-
id: basket.id,
|
|
574
|
-
basketId: basket.id,
|
|
575
|
-
basketName: basket.name || `Basket #${basket.id}`,
|
|
576
|
-
consumerId: basket.consumerId,
|
|
577
|
-
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
578
|
-
consumerEmail: consumer?.email || "-",
|
|
579
|
-
products: productsWithNames,
|
|
580
|
-
basketTotal: (basket.total || 0) / 100,
|
|
581
|
-
tax: basket.taxTotal || 0,
|
|
582
|
-
dateTime: basket.completeDate,
|
|
583
|
-
status: basket.status || "pending"
|
|
584
|
-
};
|
|
585
|
-
});
|
|
586
|
-
if (search) {
|
|
587
|
-
const searchLower = search.toLowerCase();
|
|
588
|
-
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
589
|
-
}
|
|
590
|
-
combinedRows.sort((a, b) => {
|
|
591
|
-
const dateA = new Date(a.dateTime).getTime();
|
|
592
|
-
return new Date(b.dateTime).getTime() - dateA;
|
|
593
|
-
});
|
|
594
|
-
return combinedRows;
|
|
595
|
-
}
|
|
596
|
-
/** Aggregate totals for a set of report rows. */
|
|
597
|
-
function computeBasketSummary(rows) {
|
|
598
|
-
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
599
|
-
return {
|
|
600
|
-
totalBaskets: rows.length,
|
|
601
|
-
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
602
|
-
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
603
|
-
uniqueConsumers: uniqueConsumerIds.size
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
/** Builds a consumer id → consumer lookup. */
|
|
607
|
-
function buildConsumerMap(consumers) {
|
|
608
|
-
const map = {};
|
|
609
|
-
if (consumers) consumers.forEach((consumer) => {
|
|
610
|
-
map[consumer.id] = consumer;
|
|
611
|
-
});
|
|
612
|
-
return map;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
483
|
//#endregion
|
|
616
484
|
//#region src/core/imageFit.ts
|
|
617
485
|
/**
|
|
@@ -647,6 +515,35 @@ function coverCrop(srcWidth, srcHeight, dstWidth, dstHeight) {
|
|
|
647
515
|
function requireBrowserImages(fn) {
|
|
648
516
|
if (typeof createImageBitmap !== "function") throw new Error(`${fn} needs a browser (createImageBitmap is not available)`);
|
|
649
517
|
}
|
|
518
|
+
/** Draws `crop` of the bitmap at `width × height` and encodes it (one resample). */
|
|
519
|
+
async function drawCrop(bitmap, crop, width, height, type, quality, fn) {
|
|
520
|
+
const { sx, sy, sw, sh } = crop;
|
|
521
|
+
if (typeof OffscreenCanvas === "function") {
|
|
522
|
+
const canvas$1 = new OffscreenCanvas(width, height);
|
|
523
|
+
const ctx$1 = canvas$1.getContext("2d");
|
|
524
|
+
if (!ctx$1) throw new Error(`${fn}: no 2D canvas context`);
|
|
525
|
+
ctx$1.imageSmoothingQuality = "high";
|
|
526
|
+
ctx$1.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
527
|
+
return canvas$1.convertToBlob({
|
|
528
|
+
type,
|
|
529
|
+
quality
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const canvas = document.createElement("canvas");
|
|
533
|
+
canvas.width = width;
|
|
534
|
+
canvas.height = height;
|
|
535
|
+
const ctx = canvas.getContext("2d");
|
|
536
|
+
if (!ctx) throw new Error(`${fn}: no 2D canvas context`);
|
|
537
|
+
ctx.imageSmoothingQuality = "high";
|
|
538
|
+
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
539
|
+
return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error(`${fn}: encoding failed`)), type, quality));
|
|
540
|
+
}
|
|
541
|
+
/** The input's name with the output type's extension. */
|
|
542
|
+
function outputName(file, type) {
|
|
543
|
+
const name = file.name || "image";
|
|
544
|
+
const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
|
|
545
|
+
return `${name.replace(/\.[^.]+$/, "")}.${ext}`;
|
|
546
|
+
}
|
|
650
547
|
/**
|
|
651
548
|
* Returns the image at exactly `width × height`: scaled and centre-cropped
|
|
652
549
|
* (never stretched). Browser only. The result is a `File` named like the
|
|
@@ -655,34 +552,11 @@ function requireBrowserImages(fn) {
|
|
|
655
552
|
async function fitImageToSize(file, width, height, options = {}) {
|
|
656
553
|
requireBrowserImages("fitImageToSize");
|
|
657
554
|
const { type = "image/png", quality, keepIfExact = true } = options;
|
|
658
|
-
const
|
|
659
|
-
const ext = type === "image/jpeg" ? "jpg" : type.split("/")[1];
|
|
660
|
-
const outName = `${name.replace(/\.[^.]+$/, "")}.${ext}`;
|
|
555
|
+
const outName = outputName(file, type);
|
|
661
556
|
const bitmap = await createImageBitmap(file);
|
|
662
557
|
try {
|
|
663
558
|
if (keepIfExact && bitmap.width === width && bitmap.height === height && file.type === type) return file instanceof File ? file : new File([file], outName, { type });
|
|
664
|
-
const
|
|
665
|
-
let blob;
|
|
666
|
-
if (typeof OffscreenCanvas === "function") {
|
|
667
|
-
const canvas = new OffscreenCanvas(width, height);
|
|
668
|
-
const ctx = canvas.getContext("2d");
|
|
669
|
-
if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
|
|
670
|
-
ctx.imageSmoothingQuality = "high";
|
|
671
|
-
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
672
|
-
blob = await canvas.convertToBlob({
|
|
673
|
-
type,
|
|
674
|
-
quality
|
|
675
|
-
});
|
|
676
|
-
} else {
|
|
677
|
-
const canvas = document.createElement("canvas");
|
|
678
|
-
canvas.width = width;
|
|
679
|
-
canvas.height = height;
|
|
680
|
-
const ctx = canvas.getContext("2d");
|
|
681
|
-
if (!ctx) throw new Error("fitImageToSize: no 2D canvas context");
|
|
682
|
-
ctx.imageSmoothingQuality = "high";
|
|
683
|
-
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, width, height);
|
|
684
|
-
blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(/* @__PURE__ */ new Error("fitImageToSize: encoding failed")), type, quality));
|
|
685
|
-
}
|
|
559
|
+
const blob = await drawCrop(bitmap, coverCrop(bitmap.width, bitmap.height, width, height), width, height, type, quality, "fitImageToSize");
|
|
686
560
|
return new File([blob], outName, { type });
|
|
687
561
|
} finally {
|
|
688
562
|
bitmap.close?.();
|
|
@@ -722,10 +596,12 @@ function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
|
|
|
722
596
|
async function uploadImage(file, params, options = {}) {
|
|
723
597
|
if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
|
|
724
598
|
if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
|
|
599
|
+
if (params.prefix != null && !isUploadPrefix(params.prefix)) throw new Error(`uploadImage: unknown prefix "${params.prefix}"`);
|
|
725
600
|
const name = params.fileName ?? file.name ?? "upload";
|
|
726
601
|
const form = new FormData();
|
|
727
602
|
form.append(SHOPWAVE_UPLOAD.fileField, file, name);
|
|
728
603
|
form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
|
|
604
|
+
if (params.prefix != null) form.append(SHOPWAVE_UPLOAD.prefixField, params.prefix);
|
|
729
605
|
const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
|
|
730
606
|
method: "POST",
|
|
731
607
|
body: form
|
|
@@ -813,6 +689,150 @@ async function uploadMerchantImage(file, slot, options = {}) {
|
|
|
813
689
|
return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
|
|
814
690
|
}
|
|
815
691
|
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/core/basket.ts
|
|
694
|
+
const DEFAULT_BASKET_FILTERS = {
|
|
695
|
+
search: "",
|
|
696
|
+
dateRange: {
|
|
697
|
+
start: null,
|
|
698
|
+
end: null
|
|
699
|
+
},
|
|
700
|
+
consumerId: null,
|
|
701
|
+
status: "all"
|
|
702
|
+
};
|
|
703
|
+
const DEFAULT_PAGINATION = {
|
|
704
|
+
page: 0,
|
|
705
|
+
pageSize: 25,
|
|
706
|
+
total: 0
|
|
707
|
+
};
|
|
708
|
+
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
709
|
+
function formatDateForQuery(date, isEndOfDay = false) {
|
|
710
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
711
|
+
}
|
|
712
|
+
/** Builds the `Basket` report query from a set of filters. */
|
|
713
|
+
function buildBasketReportQuery(filters) {
|
|
714
|
+
const whereConditions = [];
|
|
715
|
+
const today = /* @__PURE__ */ new Date();
|
|
716
|
+
const startDate = filters.dateRange.start ?? today;
|
|
717
|
+
const endDate = filters.dateRange.end ?? today;
|
|
718
|
+
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
719
|
+
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
720
|
+
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
721
|
+
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
722
|
+
return { baskets: {
|
|
723
|
+
FROM: "Basket",
|
|
724
|
+
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
725
|
+
} };
|
|
726
|
+
}
|
|
727
|
+
/** Flattens the raw report rows/keys into structured baskets. */
|
|
728
|
+
function parseBasketReportData(reports) {
|
|
729
|
+
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
730
|
+
const { keys, data } = reports.baskets;
|
|
731
|
+
const keyIndices = {};
|
|
732
|
+
keys.forEach((key, index) => {
|
|
733
|
+
keyIndices[key.name] = index;
|
|
734
|
+
});
|
|
735
|
+
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
736
|
+
const productSubKeys = {};
|
|
737
|
+
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
738
|
+
productSubKeys[subKey.name] = index;
|
|
739
|
+
});
|
|
740
|
+
return data.map((row) => {
|
|
741
|
+
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
742
|
+
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
743
|
+
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
744
|
+
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
745
|
+
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
746
|
+
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
747
|
+
}));
|
|
748
|
+
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
749
|
+
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
750
|
+
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
751
|
+
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
752
|
+
return {
|
|
753
|
+
id: row[keyIndices["Basket.id"]],
|
|
754
|
+
name: row[keyIndices["Basket.name"]],
|
|
755
|
+
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
756
|
+
products,
|
|
757
|
+
total,
|
|
758
|
+
taxTotal,
|
|
759
|
+
status: "completed",
|
|
760
|
+
completeDate
|
|
761
|
+
};
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
765
|
+
function collectConsumerIds(baskets) {
|
|
766
|
+
if (!baskets.length) return null;
|
|
767
|
+
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
768
|
+
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
769
|
+
}
|
|
770
|
+
/** Unique product ids referenced by a set of baskets. */
|
|
771
|
+
function collectProductIds(baskets) {
|
|
772
|
+
if (!baskets.length) return null;
|
|
773
|
+
const ids = /* @__PURE__ */ new Set();
|
|
774
|
+
baskets.forEach((basket) => {
|
|
775
|
+
basket.products.forEach((product) => {
|
|
776
|
+
if (product.productId) ids.add(product.productId);
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
return ids.size > 0 ? [...ids] : null;
|
|
780
|
+
}
|
|
781
|
+
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
782
|
+
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
783
|
+
if (!baskets.length) return [];
|
|
784
|
+
let combinedRows = baskets.map((basket) => {
|
|
785
|
+
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
786
|
+
const productsWithNames = basket.products.map((p) => {
|
|
787
|
+
const product = productMap[p.productId];
|
|
788
|
+
return {
|
|
789
|
+
...p,
|
|
790
|
+
productName: product?.name || `Product #${p.productId}`
|
|
791
|
+
};
|
|
792
|
+
});
|
|
793
|
+
return {
|
|
794
|
+
id: basket.id,
|
|
795
|
+
basketId: basket.id,
|
|
796
|
+
basketName: basket.name || `Basket #${basket.id}`,
|
|
797
|
+
consumerId: basket.consumerId,
|
|
798
|
+
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
799
|
+
consumerEmail: consumer?.email || "-",
|
|
800
|
+
products: productsWithNames,
|
|
801
|
+
basketTotal: (basket.total || 0) / 100,
|
|
802
|
+
tax: basket.taxTotal || 0,
|
|
803
|
+
dateTime: basket.completeDate,
|
|
804
|
+
status: basket.status || "pending"
|
|
805
|
+
};
|
|
806
|
+
});
|
|
807
|
+
if (search) {
|
|
808
|
+
const searchLower = search.toLowerCase();
|
|
809
|
+
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
810
|
+
}
|
|
811
|
+
combinedRows.sort((a, b) => {
|
|
812
|
+
const dateA = new Date(a.dateTime).getTime();
|
|
813
|
+
return new Date(b.dateTime).getTime() - dateA;
|
|
814
|
+
});
|
|
815
|
+
return combinedRows;
|
|
816
|
+
}
|
|
817
|
+
/** Aggregate totals for a set of report rows. */
|
|
818
|
+
function computeBasketSummary(rows) {
|
|
819
|
+
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
820
|
+
return {
|
|
821
|
+
totalBaskets: rows.length,
|
|
822
|
+
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
823
|
+
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
824
|
+
uniqueConsumers: uniqueConsumerIds.size
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/** Builds a consumer id → consumer lookup. */
|
|
828
|
+
function buildConsumerMap(consumers) {
|
|
829
|
+
const map = {};
|
|
830
|
+
if (consumers) consumers.forEach((consumer) => {
|
|
831
|
+
map[consumer.id] = consumer;
|
|
832
|
+
});
|
|
833
|
+
return map;
|
|
834
|
+
}
|
|
835
|
+
|
|
816
836
|
//#endregion
|
|
817
837
|
//#region src/core/merchant.ts
|
|
818
838
|
const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;
|