react-shopwave-connect 0.4.2 → 0.4.4
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 +19 -5
- package/dist/core/index.cjs +630 -510
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +109 -5
- package/dist/core/index.d.ts +109 -5
- package/dist/core/index.js +623 -511
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +287 -188
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +40 -1
- package/dist/hooks/index.d.ts +40 -1
- package/dist/hooks/index.js +286 -189
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +716 -567
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -5
- package/dist/index.d.ts +129 -5
- package/dist/index.js +707 -568
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +87 -45
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +65 -7
- package/dist/next/index.d.ts +65 -7
- package/dist/next/index.js +86 -46
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +84 -45
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +67 -8
- package/dist/server/index.d.ts +67 -8
- package/dist/server/index.js +83 -46
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/hooks/index.cjs
CHANGED
|
@@ -283,6 +283,118 @@ async function fetchProductsMap(productIds, options = {}, batchSize = 200) {
|
|
|
283
283
|
return result;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/core/resources.ts
|
|
288
|
+
/**
|
|
289
|
+
* - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
|
|
290
|
+
* updates it (without an `id` the API would create a second merchant, so the
|
|
291
|
+
* route requires one).
|
|
292
|
+
* - user: `GET /user` (read-only here; the API's `POST /user` invites users and
|
|
293
|
+
* needs the admin scope).
|
|
294
|
+
*/
|
|
295
|
+
const SHOPWAVE_RESOURCES = Object.freeze({
|
|
296
|
+
merchant: {
|
|
297
|
+
route: "merchant",
|
|
298
|
+
key: "merchant",
|
|
299
|
+
upstream: "merchant",
|
|
300
|
+
writable: true
|
|
301
|
+
},
|
|
302
|
+
user: {
|
|
303
|
+
route: "user",
|
|
304
|
+
key: "user",
|
|
305
|
+
upstream: "user",
|
|
306
|
+
writable: false
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
/** `UploadKind` → the API's `contentType` value. */
|
|
310
|
+
const UPLOAD_KINDS = Object.freeze({
|
|
311
|
+
merchant: "merchant",
|
|
312
|
+
merchantFeature: "merchantFeature",
|
|
313
|
+
product: "product",
|
|
314
|
+
user: "user",
|
|
315
|
+
applicationLogo: "applicaionLogo",
|
|
316
|
+
applicationImages: "applicaionImages"
|
|
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
|
+
]);
|
|
325
|
+
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
326
|
+
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
327
|
+
route: "upload",
|
|
328
|
+
upstream: "uploader",
|
|
329
|
+
kindHeader: "contentType",
|
|
330
|
+
prefixHeader: "prefix",
|
|
331
|
+
fileField: "file",
|
|
332
|
+
kindField: "kind",
|
|
333
|
+
prefixField: "prefix"
|
|
334
|
+
});
|
|
335
|
+
function isUploadKind(value) {
|
|
336
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
337
|
+
}
|
|
338
|
+
function isUploadPrefix(value) {
|
|
339
|
+
return typeof value === "string" && UPLOAD_PREFIXES.includes(value);
|
|
340
|
+
}
|
|
341
|
+
const SHOPWAVE_LOOKUPS = Object.freeze({
|
|
342
|
+
productTags: {
|
|
343
|
+
route: "products/tags",
|
|
344
|
+
key: "tags",
|
|
345
|
+
upstream: "product/tags"
|
|
346
|
+
},
|
|
347
|
+
productInstanceTags: {
|
|
348
|
+
route: "products/instance-tags",
|
|
349
|
+
key: "tags",
|
|
350
|
+
upstream: "product/instance/tags"
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/core/productTags.ts
|
|
356
|
+
/**
|
|
357
|
+
* Turns a tag list from the API into clean strings: trims each tag, drops
|
|
358
|
+
* blanks and anything that isn't text, and removes exact duplicates (first one
|
|
359
|
+
* wins). The API's own order is kept. A map is accepted too (its values are
|
|
360
|
+
* used), in case the API ever answers keyed by id.
|
|
361
|
+
*/
|
|
362
|
+
function normalizeTagList(value) {
|
|
363
|
+
const list = Array.isArray(value) ? value : value && typeof value === "object" ? Object.values(value) : [];
|
|
364
|
+
const seen = /* @__PURE__ */ new Set();
|
|
365
|
+
const out = [];
|
|
366
|
+
for (const item of list) {
|
|
367
|
+
if (typeof item !== "string" && typeof item !== "number") continue;
|
|
368
|
+
const tag = String(item).trim();
|
|
369
|
+
if (!tag || seen.has(tag)) continue;
|
|
370
|
+
seen.add(tag);
|
|
371
|
+
out.push(tag);
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
async function fetchLookup(kind, options) {
|
|
376
|
+
const def = SHOPWAVE_LOOKUPS[kind];
|
|
377
|
+
if (!def) throw new Error(`Unknown Shopwave lookup "${kind}"`);
|
|
378
|
+
const json = await apiGet(`/api/${def.route}`, {}, options);
|
|
379
|
+
assertNoApiErrors(json, 200);
|
|
380
|
+
return normalizeTagList(json?.[def.key]);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Every distinct tag used on the merchant's products (`GET /product/tags`),
|
|
384
|
+
* for a tag field's suggestions. Returns `[]` when there are none.
|
|
385
|
+
*/
|
|
386
|
+
function fetchProductTags(options = {}) {
|
|
387
|
+
return fetchLookup("productTags", options);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Every distinct tag used on the merchant's price instances
|
|
391
|
+
* (`GET /product/instance/tags`): the channel names (Eat In, Takeout, …).
|
|
392
|
+
* Returns `[]` when there are none.
|
|
393
|
+
*/
|
|
394
|
+
function fetchProductInstanceTags(options = {}) {
|
|
395
|
+
return fetchLookup("productInstanceTags", options);
|
|
396
|
+
}
|
|
397
|
+
|
|
286
398
|
//#endregion
|
|
287
399
|
//#region src/core/promotion.ts
|
|
288
400
|
/**
|
|
@@ -392,194 +504,6 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
|
|
|
392
504
|
return json ?? void 0;
|
|
393
505
|
}
|
|
394
506
|
|
|
395
|
-
//#endregion
|
|
396
|
-
//#region src/core/basket.ts
|
|
397
|
-
const DEFAULT_BASKET_FILTERS = {
|
|
398
|
-
search: "",
|
|
399
|
-
dateRange: {
|
|
400
|
-
start: null,
|
|
401
|
-
end: null
|
|
402
|
-
},
|
|
403
|
-
consumerId: null,
|
|
404
|
-
status: "all"
|
|
405
|
-
};
|
|
406
|
-
const DEFAULT_PAGINATION = {
|
|
407
|
-
page: 0,
|
|
408
|
-
pageSize: 25,
|
|
409
|
-
total: 0
|
|
410
|
-
};
|
|
411
|
-
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
412
|
-
function formatDateForQuery(date, isEndOfDay = false) {
|
|
413
|
-
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
414
|
-
}
|
|
415
|
-
/** Builds the `Basket` report query from a set of filters. */
|
|
416
|
-
function buildBasketReportQuery(filters) {
|
|
417
|
-
const whereConditions = [];
|
|
418
|
-
const today = /* @__PURE__ */ new Date();
|
|
419
|
-
const startDate = filters.dateRange.start ?? today;
|
|
420
|
-
const endDate = filters.dateRange.end ?? today;
|
|
421
|
-
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
422
|
-
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
423
|
-
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
424
|
-
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
425
|
-
return { baskets: {
|
|
426
|
-
FROM: "Basket",
|
|
427
|
-
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
428
|
-
} };
|
|
429
|
-
}
|
|
430
|
-
/** Flattens the raw report rows/keys into structured baskets. */
|
|
431
|
-
function parseBasketReportData(reports) {
|
|
432
|
-
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
433
|
-
const { keys, data } = reports.baskets;
|
|
434
|
-
const keyIndices = {};
|
|
435
|
-
keys.forEach((key, index) => {
|
|
436
|
-
keyIndices[key.name] = index;
|
|
437
|
-
});
|
|
438
|
-
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
439
|
-
const productSubKeys = {};
|
|
440
|
-
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
441
|
-
productSubKeys[subKey.name] = index;
|
|
442
|
-
});
|
|
443
|
-
return data.map((row) => {
|
|
444
|
-
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
445
|
-
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
446
|
-
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
447
|
-
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
448
|
-
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
449
|
-
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
450
|
-
}));
|
|
451
|
-
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
452
|
-
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
453
|
-
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
454
|
-
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
455
|
-
return {
|
|
456
|
-
id: row[keyIndices["Basket.id"]],
|
|
457
|
-
name: row[keyIndices["Basket.name"]],
|
|
458
|
-
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
459
|
-
products,
|
|
460
|
-
total,
|
|
461
|
-
taxTotal,
|
|
462
|
-
status: "completed",
|
|
463
|
-
completeDate
|
|
464
|
-
};
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
468
|
-
function collectConsumerIds(baskets) {
|
|
469
|
-
if (!baskets.length) return null;
|
|
470
|
-
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
471
|
-
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
472
|
-
}
|
|
473
|
-
/** Unique product ids referenced by a set of baskets. */
|
|
474
|
-
function collectProductIds(baskets) {
|
|
475
|
-
if (!baskets.length) return null;
|
|
476
|
-
const ids = /* @__PURE__ */ new Set();
|
|
477
|
-
baskets.forEach((basket) => {
|
|
478
|
-
basket.products.forEach((product) => {
|
|
479
|
-
if (product.productId) ids.add(product.productId);
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
return ids.size > 0 ? [...ids] : null;
|
|
483
|
-
}
|
|
484
|
-
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
485
|
-
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
486
|
-
if (!baskets.length) return [];
|
|
487
|
-
let combinedRows = baskets.map((basket) => {
|
|
488
|
-
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
489
|
-
const productsWithNames = basket.products.map((p) => {
|
|
490
|
-
const product = productMap[p.productId];
|
|
491
|
-
return {
|
|
492
|
-
...p,
|
|
493
|
-
productName: product?.name || `Product #${p.productId}`
|
|
494
|
-
};
|
|
495
|
-
});
|
|
496
|
-
return {
|
|
497
|
-
id: basket.id,
|
|
498
|
-
basketId: basket.id,
|
|
499
|
-
basketName: basket.name || `Basket #${basket.id}`,
|
|
500
|
-
consumerId: basket.consumerId,
|
|
501
|
-
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
502
|
-
consumerEmail: consumer?.email || "-",
|
|
503
|
-
products: productsWithNames,
|
|
504
|
-
basketTotal: (basket.total || 0) / 100,
|
|
505
|
-
tax: basket.taxTotal || 0,
|
|
506
|
-
dateTime: basket.completeDate,
|
|
507
|
-
status: basket.status || "pending"
|
|
508
|
-
};
|
|
509
|
-
});
|
|
510
|
-
if (search) {
|
|
511
|
-
const searchLower = search.toLowerCase();
|
|
512
|
-
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
513
|
-
}
|
|
514
|
-
combinedRows.sort((a, b) => {
|
|
515
|
-
const dateA = new Date(a.dateTime).getTime();
|
|
516
|
-
return new Date(b.dateTime).getTime() - dateA;
|
|
517
|
-
});
|
|
518
|
-
return combinedRows;
|
|
519
|
-
}
|
|
520
|
-
/** Aggregate totals for a set of report rows. */
|
|
521
|
-
function computeBasketSummary(rows) {
|
|
522
|
-
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
523
|
-
return {
|
|
524
|
-
totalBaskets: rows.length,
|
|
525
|
-
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
526
|
-
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
527
|
-
uniqueConsumers: uniqueConsumerIds.size
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
/** Builds a consumer id → consumer lookup. */
|
|
531
|
-
function buildConsumerMap(consumers) {
|
|
532
|
-
const map = {};
|
|
533
|
-
if (consumers) consumers.forEach((consumer) => {
|
|
534
|
-
map[consumer.id] = consumer;
|
|
535
|
-
});
|
|
536
|
-
return map;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
//#endregion
|
|
540
|
-
//#region src/core/resources.ts
|
|
541
|
-
/**
|
|
542
|
-
* - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
|
|
543
|
-
* updates it (without an `id` the API would create a second merchant, so the
|
|
544
|
-
* route requires one).
|
|
545
|
-
* - user: `GET /user` (read-only here; the API's `POST /user` invites users and
|
|
546
|
-
* needs the admin scope).
|
|
547
|
-
*/
|
|
548
|
-
const SHOPWAVE_RESOURCES = Object.freeze({
|
|
549
|
-
merchant: {
|
|
550
|
-
route: "merchant",
|
|
551
|
-
key: "merchant",
|
|
552
|
-
upstream: "merchant",
|
|
553
|
-
writable: true
|
|
554
|
-
},
|
|
555
|
-
user: {
|
|
556
|
-
route: "user",
|
|
557
|
-
key: "user",
|
|
558
|
-
upstream: "user",
|
|
559
|
-
writable: false
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
/** `UploadKind` → the API's `contentType` value. */
|
|
563
|
-
const UPLOAD_KINDS = Object.freeze({
|
|
564
|
-
merchant: "merchant",
|
|
565
|
-
merchantFeature: "merchantFeature",
|
|
566
|
-
product: "product",
|
|
567
|
-
user: "user",
|
|
568
|
-
applicationLogo: "applicaionLogo",
|
|
569
|
-
applicationImages: "applicaionImages"
|
|
570
|
-
});
|
|
571
|
-
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
572
|
-
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
573
|
-
route: "upload",
|
|
574
|
-
upstream: "uploader",
|
|
575
|
-
kindHeader: "contentType",
|
|
576
|
-
fileField: "file",
|
|
577
|
-
kindField: "kind"
|
|
578
|
-
});
|
|
579
|
-
function isUploadKind(value) {
|
|
580
|
-
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
507
|
//#endregion
|
|
584
508
|
//#region src/core/imageFit.ts
|
|
585
509
|
/**
|
|
@@ -690,10 +614,12 @@ function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
|
|
|
690
614
|
async function uploadImage(file, params, options = {}) {
|
|
691
615
|
if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
|
|
692
616
|
if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
|
|
617
|
+
if (params.prefix != null && !isUploadPrefix(params.prefix)) throw new Error(`uploadImage: unknown prefix "${params.prefix}"`);
|
|
693
618
|
const name = params.fileName ?? file.name ?? "upload";
|
|
694
619
|
const form = new FormData();
|
|
695
620
|
form.append(SHOPWAVE_UPLOAD.fileField, file, name);
|
|
696
621
|
form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
|
|
622
|
+
if (params.prefix != null) form.append(SHOPWAVE_UPLOAD.prefixField, params.prefix);
|
|
697
623
|
const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
|
|
698
624
|
method: "POST",
|
|
699
625
|
body: form
|
|
@@ -781,6 +707,150 @@ async function uploadMerchantImage(file, slot, options = {}) {
|
|
|
781
707
|
return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
|
|
782
708
|
}
|
|
783
709
|
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/core/basket.ts
|
|
712
|
+
const DEFAULT_BASKET_FILTERS = {
|
|
713
|
+
search: "",
|
|
714
|
+
dateRange: {
|
|
715
|
+
start: null,
|
|
716
|
+
end: null
|
|
717
|
+
},
|
|
718
|
+
consumerId: null,
|
|
719
|
+
status: "all"
|
|
720
|
+
};
|
|
721
|
+
const DEFAULT_PAGINATION = {
|
|
722
|
+
page: 0,
|
|
723
|
+
pageSize: 25,
|
|
724
|
+
total: 0
|
|
725
|
+
};
|
|
726
|
+
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
727
|
+
function formatDateForQuery(date, isEndOfDay = false) {
|
|
728
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
729
|
+
}
|
|
730
|
+
/** Builds the `Basket` report query from a set of filters. */
|
|
731
|
+
function buildBasketReportQuery(filters) {
|
|
732
|
+
const whereConditions = [];
|
|
733
|
+
const today = /* @__PURE__ */ new Date();
|
|
734
|
+
const startDate = filters.dateRange.start ?? today;
|
|
735
|
+
const endDate = filters.dateRange.end ?? today;
|
|
736
|
+
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
737
|
+
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
738
|
+
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
739
|
+
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
740
|
+
return { baskets: {
|
|
741
|
+
FROM: "Basket",
|
|
742
|
+
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
743
|
+
} };
|
|
744
|
+
}
|
|
745
|
+
/** Flattens the raw report rows/keys into structured baskets. */
|
|
746
|
+
function parseBasketReportData(reports) {
|
|
747
|
+
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
748
|
+
const { keys, data } = reports.baskets;
|
|
749
|
+
const keyIndices = {};
|
|
750
|
+
keys.forEach((key, index) => {
|
|
751
|
+
keyIndices[key.name] = index;
|
|
752
|
+
});
|
|
753
|
+
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
754
|
+
const productSubKeys = {};
|
|
755
|
+
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
756
|
+
productSubKeys[subKey.name] = index;
|
|
757
|
+
});
|
|
758
|
+
return data.map((row) => {
|
|
759
|
+
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
760
|
+
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
761
|
+
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
762
|
+
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
763
|
+
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
764
|
+
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
765
|
+
}));
|
|
766
|
+
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
767
|
+
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
768
|
+
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
769
|
+
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
770
|
+
return {
|
|
771
|
+
id: row[keyIndices["Basket.id"]],
|
|
772
|
+
name: row[keyIndices["Basket.name"]],
|
|
773
|
+
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
774
|
+
products,
|
|
775
|
+
total,
|
|
776
|
+
taxTotal,
|
|
777
|
+
status: "completed",
|
|
778
|
+
completeDate
|
|
779
|
+
};
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
783
|
+
function collectConsumerIds(baskets) {
|
|
784
|
+
if (!baskets.length) return null;
|
|
785
|
+
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
786
|
+
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
787
|
+
}
|
|
788
|
+
/** Unique product ids referenced by a set of baskets. */
|
|
789
|
+
function collectProductIds(baskets) {
|
|
790
|
+
if (!baskets.length) return null;
|
|
791
|
+
const ids = /* @__PURE__ */ new Set();
|
|
792
|
+
baskets.forEach((basket) => {
|
|
793
|
+
basket.products.forEach((product) => {
|
|
794
|
+
if (product.productId) ids.add(product.productId);
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
return ids.size > 0 ? [...ids] : null;
|
|
798
|
+
}
|
|
799
|
+
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
800
|
+
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
801
|
+
if (!baskets.length) return [];
|
|
802
|
+
let combinedRows = baskets.map((basket) => {
|
|
803
|
+
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
804
|
+
const productsWithNames = basket.products.map((p) => {
|
|
805
|
+
const product = productMap[p.productId];
|
|
806
|
+
return {
|
|
807
|
+
...p,
|
|
808
|
+
productName: product?.name || `Product #${p.productId}`
|
|
809
|
+
};
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
id: basket.id,
|
|
813
|
+
basketId: basket.id,
|
|
814
|
+
basketName: basket.name || `Basket #${basket.id}`,
|
|
815
|
+
consumerId: basket.consumerId,
|
|
816
|
+
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
817
|
+
consumerEmail: consumer?.email || "-",
|
|
818
|
+
products: productsWithNames,
|
|
819
|
+
basketTotal: (basket.total || 0) / 100,
|
|
820
|
+
tax: basket.taxTotal || 0,
|
|
821
|
+
dateTime: basket.completeDate,
|
|
822
|
+
status: basket.status || "pending"
|
|
823
|
+
};
|
|
824
|
+
});
|
|
825
|
+
if (search) {
|
|
826
|
+
const searchLower = search.toLowerCase();
|
|
827
|
+
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
828
|
+
}
|
|
829
|
+
combinedRows.sort((a, b) => {
|
|
830
|
+
const dateA = new Date(a.dateTime).getTime();
|
|
831
|
+
return new Date(b.dateTime).getTime() - dateA;
|
|
832
|
+
});
|
|
833
|
+
return combinedRows;
|
|
834
|
+
}
|
|
835
|
+
/** Aggregate totals for a set of report rows. */
|
|
836
|
+
function computeBasketSummary(rows) {
|
|
837
|
+
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
838
|
+
return {
|
|
839
|
+
totalBaskets: rows.length,
|
|
840
|
+
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
841
|
+
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
842
|
+
uniqueConsumers: uniqueConsumerIds.size
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
/** Builds a consumer id → consumer lookup. */
|
|
846
|
+
function buildConsumerMap(consumers) {
|
|
847
|
+
const map = {};
|
|
848
|
+
if (consumers) consumers.forEach((consumer) => {
|
|
849
|
+
map[consumer.id] = consumer;
|
|
850
|
+
});
|
|
851
|
+
return map;
|
|
852
|
+
}
|
|
853
|
+
|
|
784
854
|
//#endregion
|
|
785
855
|
//#region src/core/merchant.ts
|
|
786
856
|
const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;
|
|
@@ -1124,6 +1194,33 @@ function useProduct(params = {}, options) {
|
|
|
1124
1194
|
]);
|
|
1125
1195
|
}
|
|
1126
1196
|
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/hooks/useProductTags.ts
|
|
1199
|
+
/**
|
|
1200
|
+
* The merchant's distinct product tags, for a tag field's suggestions.
|
|
1201
|
+
* `data` is `string[]` once loaded.
|
|
1202
|
+
*
|
|
1203
|
+
* @example
|
|
1204
|
+
* const [opened, setOpened] = useState(false);
|
|
1205
|
+
* const { data: tags, loading } = useProductTags({ enabled: opened });
|
|
1206
|
+
*/
|
|
1207
|
+
function useProductTags(options) {
|
|
1208
|
+
return useQuery((signal) => fetchProductTags({
|
|
1209
|
+
...options,
|
|
1210
|
+
signal
|
|
1211
|
+
}), [options?.baseUrl, options?.token], options?.enabled ?? true);
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* The merchant's distinct price-instance (channel) tags: Eat In, Takeout, …
|
|
1215
|
+
* `data` is `string[]` once loaded.
|
|
1216
|
+
*/
|
|
1217
|
+
function useProductInstanceTags(options) {
|
|
1218
|
+
return useQuery((signal) => fetchProductInstanceTags({
|
|
1219
|
+
...options,
|
|
1220
|
+
signal
|
|
1221
|
+
}), [options?.baseUrl, options?.token], options?.enabled ?? true);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1127
1224
|
//#endregion
|
|
1128
1225
|
//#region src/hooks/usePromotion.ts
|
|
1129
1226
|
/**
|
|
@@ -1442,6 +1539,8 @@ exports.useEmployee = useEmployee;
|
|
|
1442
1539
|
exports.useLogout = useLogout;
|
|
1443
1540
|
exports.useMerchant = useMerchant;
|
|
1444
1541
|
exports.useProduct = useProduct;
|
|
1542
|
+
exports.useProductInstanceTags = useProductInstanceTags;
|
|
1543
|
+
exports.useProductTags = useProductTags;
|
|
1445
1544
|
exports.usePromotion = usePromotion;
|
|
1446
1545
|
exports.useReport = useReport;
|
|
1447
1546
|
exports.useSession = useSession;
|