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.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
|
}
|
|
@@ -546,6 +565,26 @@ declare function useEmployee(params?: FetchEmployeesParams, options?: RequestOpt
|
|
|
546
565
|
*/
|
|
547
566
|
declare function useProduct(params?: FetchProductsParams, options?: RequestOptions): QueryState<Product[]>;
|
|
548
567
|
//#endregion
|
|
568
|
+
//#region src/hooks/useProductTags.d.ts
|
|
569
|
+
type LookupOptions = RequestOptions & {
|
|
570
|
+
/** Fetch only when true (e.g. once a tag field is first opened). Defaults to true. */
|
|
571
|
+
enabled?: boolean;
|
|
572
|
+
};
|
|
573
|
+
/**
|
|
574
|
+
* The merchant's distinct product tags, for a tag field's suggestions.
|
|
575
|
+
* `data` is `string[]` once loaded.
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* const [opened, setOpened] = useState(false);
|
|
579
|
+
* const { data: tags, loading } = useProductTags({ enabled: opened });
|
|
580
|
+
*/
|
|
581
|
+
declare function useProductTags(options?: LookupOptions): QueryState<string[]>;
|
|
582
|
+
/**
|
|
583
|
+
* The merchant's distinct price-instance (channel) tags: Eat In, Takeout, …
|
|
584
|
+
* `data` is `string[]` once loaded.
|
|
585
|
+
*/
|
|
586
|
+
declare function useProductInstanceTags(options?: LookupOptions): QueryState<string[]>;
|
|
587
|
+
//#endregion
|
|
549
588
|
//#region src/hooks/usePromotion.d.ts
|
|
550
589
|
/**
|
|
551
590
|
* Auto-fetches promotions on mount and whenever the params change.
|
|
@@ -699,5 +738,5 @@ interface UseBasketReportReturn {
|
|
|
699
738
|
*/
|
|
700
739
|
declare function useBasketReport(options?: RequestOptions): UseBasketReportReturn;
|
|
701
740
|
//#endregion
|
|
702
|
-
export { type MutationState, type QueryState, type UseBasketReportReturn, useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser };
|
|
741
|
+
export { type MutationState, type QueryState, type UseBasketReportReturn, useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, useProductInstanceTags, useProductTags, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser };
|
|
703
742
|
//# sourceMappingURL=index.d.cts.map
|
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
|
}
|
|
@@ -546,6 +565,26 @@ declare function useEmployee(params?: FetchEmployeesParams, options?: RequestOpt
|
|
|
546
565
|
*/
|
|
547
566
|
declare function useProduct(params?: FetchProductsParams, options?: RequestOptions): QueryState<Product[]>;
|
|
548
567
|
//#endregion
|
|
568
|
+
//#region src/hooks/useProductTags.d.ts
|
|
569
|
+
type LookupOptions = RequestOptions & {
|
|
570
|
+
/** Fetch only when true (e.g. once a tag field is first opened). Defaults to true. */
|
|
571
|
+
enabled?: boolean;
|
|
572
|
+
};
|
|
573
|
+
/**
|
|
574
|
+
* The merchant's distinct product tags, for a tag field's suggestions.
|
|
575
|
+
* `data` is `string[]` once loaded.
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* const [opened, setOpened] = useState(false);
|
|
579
|
+
* const { data: tags, loading } = useProductTags({ enabled: opened });
|
|
580
|
+
*/
|
|
581
|
+
declare function useProductTags(options?: LookupOptions): QueryState<string[]>;
|
|
582
|
+
/**
|
|
583
|
+
* The merchant's distinct price-instance (channel) tags: Eat In, Takeout, …
|
|
584
|
+
* `data` is `string[]` once loaded.
|
|
585
|
+
*/
|
|
586
|
+
declare function useProductInstanceTags(options?: LookupOptions): QueryState<string[]>;
|
|
587
|
+
//#endregion
|
|
549
588
|
//#region src/hooks/usePromotion.d.ts
|
|
550
589
|
/**
|
|
551
590
|
* Auto-fetches promotions on mount and whenever the params change.
|
|
@@ -699,5 +738,5 @@ interface UseBasketReportReturn {
|
|
|
699
738
|
*/
|
|
700
739
|
declare function useBasketReport(options?: RequestOptions): UseBasketReportReturn;
|
|
701
740
|
//#endregion
|
|
702
|
-
export { type MutationState, type QueryState, type UseBasketReportReturn, useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser };
|
|
741
|
+
export { type MutationState, type QueryState, type UseBasketReportReturn, useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, useProductInstanceTags, useProductTags, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser };
|
|
703
742
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/hooks/index.js
CHANGED
|
@@ -259,6 +259,118 @@ async function fetchProductsMap(productIds, options = {}, batchSize = 200) {
|
|
|
259
259
|
return result;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/core/resources.ts
|
|
264
|
+
/**
|
|
265
|
+
* - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
|
|
266
|
+
* updates it (without an `id` the API would create a second merchant, so the
|
|
267
|
+
* route requires one).
|
|
268
|
+
* - user: `GET /user` (read-only here; the API's `POST /user` invites users and
|
|
269
|
+
* needs the admin scope).
|
|
270
|
+
*/
|
|
271
|
+
const SHOPWAVE_RESOURCES = Object.freeze({
|
|
272
|
+
merchant: {
|
|
273
|
+
route: "merchant",
|
|
274
|
+
key: "merchant",
|
|
275
|
+
upstream: "merchant",
|
|
276
|
+
writable: true
|
|
277
|
+
},
|
|
278
|
+
user: {
|
|
279
|
+
route: "user",
|
|
280
|
+
key: "user",
|
|
281
|
+
upstream: "user",
|
|
282
|
+
writable: false
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
/** `UploadKind` → the API's `contentType` value. */
|
|
286
|
+
const UPLOAD_KINDS = Object.freeze({
|
|
287
|
+
merchant: "merchant",
|
|
288
|
+
merchantFeature: "merchantFeature",
|
|
289
|
+
product: "product",
|
|
290
|
+
user: "user",
|
|
291
|
+
applicationLogo: "applicaionLogo",
|
|
292
|
+
applicationImages: "applicaionImages"
|
|
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
|
+
]);
|
|
301
|
+
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
302
|
+
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
303
|
+
route: "upload",
|
|
304
|
+
upstream: "uploader",
|
|
305
|
+
kindHeader: "contentType",
|
|
306
|
+
prefixHeader: "prefix",
|
|
307
|
+
fileField: "file",
|
|
308
|
+
kindField: "kind",
|
|
309
|
+
prefixField: "prefix"
|
|
310
|
+
});
|
|
311
|
+
function isUploadKind(value) {
|
|
312
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
313
|
+
}
|
|
314
|
+
function isUploadPrefix(value) {
|
|
315
|
+
return typeof value === "string" && UPLOAD_PREFIXES.includes(value);
|
|
316
|
+
}
|
|
317
|
+
const SHOPWAVE_LOOKUPS = Object.freeze({
|
|
318
|
+
productTags: {
|
|
319
|
+
route: "products/tags",
|
|
320
|
+
key: "tags",
|
|
321
|
+
upstream: "product/tags"
|
|
322
|
+
},
|
|
323
|
+
productInstanceTags: {
|
|
324
|
+
route: "products/instance-tags",
|
|
325
|
+
key: "tags",
|
|
326
|
+
upstream: "product/instance/tags"
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/core/productTags.ts
|
|
332
|
+
/**
|
|
333
|
+
* Turns a tag list from the API into clean strings: trims each tag, drops
|
|
334
|
+
* blanks and anything that isn't text, and removes exact duplicates (first one
|
|
335
|
+
* wins). The API's own order is kept. A map is accepted too (its values are
|
|
336
|
+
* used), in case the API ever answers keyed by id.
|
|
337
|
+
*/
|
|
338
|
+
function normalizeTagList(value) {
|
|
339
|
+
const list = Array.isArray(value) ? value : value && typeof value === "object" ? Object.values(value) : [];
|
|
340
|
+
const seen = /* @__PURE__ */ new Set();
|
|
341
|
+
const out = [];
|
|
342
|
+
for (const item of list) {
|
|
343
|
+
if (typeof item !== "string" && typeof item !== "number") continue;
|
|
344
|
+
const tag = String(item).trim();
|
|
345
|
+
if (!tag || seen.has(tag)) continue;
|
|
346
|
+
seen.add(tag);
|
|
347
|
+
out.push(tag);
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
async function fetchLookup(kind, options) {
|
|
352
|
+
const def = SHOPWAVE_LOOKUPS[kind];
|
|
353
|
+
if (!def) throw new Error(`Unknown Shopwave lookup "${kind}"`);
|
|
354
|
+
const json = await apiGet(`/api/${def.route}`, {}, options);
|
|
355
|
+
assertNoApiErrors(json, 200);
|
|
356
|
+
return normalizeTagList(json?.[def.key]);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Every distinct tag used on the merchant's products (`GET /product/tags`),
|
|
360
|
+
* for a tag field's suggestions. Returns `[]` when there are none.
|
|
361
|
+
*/
|
|
362
|
+
function fetchProductTags(options = {}) {
|
|
363
|
+
return fetchLookup("productTags", options);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Every distinct tag used on the merchant's price instances
|
|
367
|
+
* (`GET /product/instance/tags`): the channel names (Eat In, Takeout, …).
|
|
368
|
+
* Returns `[]` when there are none.
|
|
369
|
+
*/
|
|
370
|
+
function fetchProductInstanceTags(options = {}) {
|
|
371
|
+
return fetchLookup("productInstanceTags", options);
|
|
372
|
+
}
|
|
373
|
+
|
|
262
374
|
//#endregion
|
|
263
375
|
//#region src/core/promotion.ts
|
|
264
376
|
/**
|
|
@@ -368,194 +480,6 @@ async function submitEntity({ endpoint, method = "POST", payload }, options = {}
|
|
|
368
480
|
return json ?? void 0;
|
|
369
481
|
}
|
|
370
482
|
|
|
371
|
-
//#endregion
|
|
372
|
-
//#region src/core/basket.ts
|
|
373
|
-
const DEFAULT_BASKET_FILTERS = {
|
|
374
|
-
search: "",
|
|
375
|
-
dateRange: {
|
|
376
|
-
start: null,
|
|
377
|
-
end: null
|
|
378
|
-
},
|
|
379
|
-
consumerId: null,
|
|
380
|
-
status: "all"
|
|
381
|
-
};
|
|
382
|
-
const DEFAULT_PAGINATION = {
|
|
383
|
-
page: 0,
|
|
384
|
-
pageSize: 25,
|
|
385
|
-
total: 0
|
|
386
|
-
};
|
|
387
|
-
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
388
|
-
function formatDateForQuery(date, isEndOfDay = false) {
|
|
389
|
-
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
390
|
-
}
|
|
391
|
-
/** Builds the `Basket` report query from a set of filters. */
|
|
392
|
-
function buildBasketReportQuery(filters) {
|
|
393
|
-
const whereConditions = [];
|
|
394
|
-
const today = /* @__PURE__ */ new Date();
|
|
395
|
-
const startDate = filters.dateRange.start ?? today;
|
|
396
|
-
const endDate = filters.dateRange.end ?? today;
|
|
397
|
-
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
398
|
-
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
399
|
-
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
400
|
-
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
401
|
-
return { baskets: {
|
|
402
|
-
FROM: "Basket",
|
|
403
|
-
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
404
|
-
} };
|
|
405
|
-
}
|
|
406
|
-
/** Flattens the raw report rows/keys into structured baskets. */
|
|
407
|
-
function parseBasketReportData(reports) {
|
|
408
|
-
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
409
|
-
const { keys, data } = reports.baskets;
|
|
410
|
-
const keyIndices = {};
|
|
411
|
-
keys.forEach((key, index) => {
|
|
412
|
-
keyIndices[key.name] = index;
|
|
413
|
-
});
|
|
414
|
-
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
415
|
-
const productSubKeys = {};
|
|
416
|
-
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
417
|
-
productSubKeys[subKey.name] = index;
|
|
418
|
-
});
|
|
419
|
-
return data.map((row) => {
|
|
420
|
-
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
421
|
-
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
422
|
-
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
423
|
-
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
424
|
-
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
425
|
-
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
426
|
-
}));
|
|
427
|
-
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
428
|
-
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
429
|
-
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
430
|
-
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
431
|
-
return {
|
|
432
|
-
id: row[keyIndices["Basket.id"]],
|
|
433
|
-
name: row[keyIndices["Basket.name"]],
|
|
434
|
-
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
435
|
-
products,
|
|
436
|
-
total,
|
|
437
|
-
taxTotal,
|
|
438
|
-
status: "completed",
|
|
439
|
-
completeDate
|
|
440
|
-
};
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
444
|
-
function collectConsumerIds(baskets) {
|
|
445
|
-
if (!baskets.length) return null;
|
|
446
|
-
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
447
|
-
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
448
|
-
}
|
|
449
|
-
/** Unique product ids referenced by a set of baskets. */
|
|
450
|
-
function collectProductIds(baskets) {
|
|
451
|
-
if (!baskets.length) return null;
|
|
452
|
-
const ids = /* @__PURE__ */ new Set();
|
|
453
|
-
baskets.forEach((basket) => {
|
|
454
|
-
basket.products.forEach((product) => {
|
|
455
|
-
if (product.productId) ids.add(product.productId);
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
return ids.size > 0 ? [...ids] : null;
|
|
459
|
-
}
|
|
460
|
-
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
461
|
-
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
462
|
-
if (!baskets.length) return [];
|
|
463
|
-
let combinedRows = baskets.map((basket) => {
|
|
464
|
-
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
465
|
-
const productsWithNames = basket.products.map((p) => {
|
|
466
|
-
const product = productMap[p.productId];
|
|
467
|
-
return {
|
|
468
|
-
...p,
|
|
469
|
-
productName: product?.name || `Product #${p.productId}`
|
|
470
|
-
};
|
|
471
|
-
});
|
|
472
|
-
return {
|
|
473
|
-
id: basket.id,
|
|
474
|
-
basketId: basket.id,
|
|
475
|
-
basketName: basket.name || `Basket #${basket.id}`,
|
|
476
|
-
consumerId: basket.consumerId,
|
|
477
|
-
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
478
|
-
consumerEmail: consumer?.email || "-",
|
|
479
|
-
products: productsWithNames,
|
|
480
|
-
basketTotal: (basket.total || 0) / 100,
|
|
481
|
-
tax: basket.taxTotal || 0,
|
|
482
|
-
dateTime: basket.completeDate,
|
|
483
|
-
status: basket.status || "pending"
|
|
484
|
-
};
|
|
485
|
-
});
|
|
486
|
-
if (search) {
|
|
487
|
-
const searchLower = search.toLowerCase();
|
|
488
|
-
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
489
|
-
}
|
|
490
|
-
combinedRows.sort((a, b) => {
|
|
491
|
-
const dateA = new Date(a.dateTime).getTime();
|
|
492
|
-
return new Date(b.dateTime).getTime() - dateA;
|
|
493
|
-
});
|
|
494
|
-
return combinedRows;
|
|
495
|
-
}
|
|
496
|
-
/** Aggregate totals for a set of report rows. */
|
|
497
|
-
function computeBasketSummary(rows) {
|
|
498
|
-
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
499
|
-
return {
|
|
500
|
-
totalBaskets: rows.length,
|
|
501
|
-
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
502
|
-
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
503
|
-
uniqueConsumers: uniqueConsumerIds.size
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
/** Builds a consumer id → consumer lookup. */
|
|
507
|
-
function buildConsumerMap(consumers) {
|
|
508
|
-
const map = {};
|
|
509
|
-
if (consumers) consumers.forEach((consumer) => {
|
|
510
|
-
map[consumer.id] = consumer;
|
|
511
|
-
});
|
|
512
|
-
return map;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
//#endregion
|
|
516
|
-
//#region src/core/resources.ts
|
|
517
|
-
/**
|
|
518
|
-
* - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`
|
|
519
|
-
* updates it (without an `id` the API would create a second merchant, so the
|
|
520
|
-
* route requires one).
|
|
521
|
-
* - user: `GET /user` (read-only here; the API's `POST /user` invites users and
|
|
522
|
-
* needs the admin scope).
|
|
523
|
-
*/
|
|
524
|
-
const SHOPWAVE_RESOURCES = Object.freeze({
|
|
525
|
-
merchant: {
|
|
526
|
-
route: "merchant",
|
|
527
|
-
key: "merchant",
|
|
528
|
-
upstream: "merchant",
|
|
529
|
-
writable: true
|
|
530
|
-
},
|
|
531
|
-
user: {
|
|
532
|
-
route: "user",
|
|
533
|
-
key: "user",
|
|
534
|
-
upstream: "user",
|
|
535
|
-
writable: false
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
/** `UploadKind` → the API's `contentType` value. */
|
|
539
|
-
const UPLOAD_KINDS = Object.freeze({
|
|
540
|
-
merchant: "merchant",
|
|
541
|
-
merchantFeature: "merchantFeature",
|
|
542
|
-
product: "product",
|
|
543
|
-
user: "user",
|
|
544
|
-
applicationLogo: "applicaionLogo",
|
|
545
|
-
applicationImages: "applicaionImages"
|
|
546
|
-
});
|
|
547
|
-
/** How uploads are addressed (app route and Shopwave uploader). */
|
|
548
|
-
const SHOPWAVE_UPLOAD = Object.freeze({
|
|
549
|
-
route: "upload",
|
|
550
|
-
upstream: "uploader",
|
|
551
|
-
kindHeader: "contentType",
|
|
552
|
-
fileField: "file",
|
|
553
|
-
kindField: "kind"
|
|
554
|
-
});
|
|
555
|
-
function isUploadKind(value) {
|
|
556
|
-
return typeof value === "string" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
483
|
//#endregion
|
|
560
484
|
//#region src/core/imageFit.ts
|
|
561
485
|
/**
|
|
@@ -666,10 +590,12 @@ function getImageUrl(path, base = SHOPWAVE_IMAGE_BASE_URL) {
|
|
|
666
590
|
async function uploadImage(file, params, options = {}) {
|
|
667
591
|
if (!file || typeof file.size !== "number") throw new Error("uploadImage: file must be a File or Blob");
|
|
668
592
|
if (!isUploadKind(params?.kind)) throw new Error(`uploadImage: unknown kind "${params?.kind}"`);
|
|
593
|
+
if (params.prefix != null && !isUploadPrefix(params.prefix)) throw new Error(`uploadImage: unknown prefix "${params.prefix}"`);
|
|
669
594
|
const name = params.fileName ?? file.name ?? "upload";
|
|
670
595
|
const form = new FormData();
|
|
671
596
|
form.append(SHOPWAVE_UPLOAD.fileField, file, name);
|
|
672
597
|
form.append(SHOPWAVE_UPLOAD.kindField, params.kind);
|
|
598
|
+
if (params.prefix != null) form.append(SHOPWAVE_UPLOAD.prefixField, params.prefix);
|
|
673
599
|
const { status, body: raw } = await apiRequest(`/api/${SHOPWAVE_UPLOAD.route}`, {
|
|
674
600
|
method: "POST",
|
|
675
601
|
body: form
|
|
@@ -757,6 +683,150 @@ async function uploadMerchantImage(file, slot, options = {}) {
|
|
|
757
683
|
return uploadImage(fit ? await fitImageToSize(file, def.width, def.height, fitOptions) : file, { kind: def.uploadKind }, requestOptions);
|
|
758
684
|
}
|
|
759
685
|
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/core/basket.ts
|
|
688
|
+
const DEFAULT_BASKET_FILTERS = {
|
|
689
|
+
search: "",
|
|
690
|
+
dateRange: {
|
|
691
|
+
start: null,
|
|
692
|
+
end: null
|
|
693
|
+
},
|
|
694
|
+
consumerId: null,
|
|
695
|
+
status: "all"
|
|
696
|
+
};
|
|
697
|
+
const DEFAULT_PAGINATION = {
|
|
698
|
+
page: 0,
|
|
699
|
+
pageSize: 25,
|
|
700
|
+
total: 0
|
|
701
|
+
};
|
|
702
|
+
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
703
|
+
function formatDateForQuery(date, isEndOfDay = false) {
|
|
704
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
705
|
+
}
|
|
706
|
+
/** Builds the `Basket` report query from a set of filters. */
|
|
707
|
+
function buildBasketReportQuery(filters) {
|
|
708
|
+
const whereConditions = [];
|
|
709
|
+
const today = /* @__PURE__ */ new Date();
|
|
710
|
+
const startDate = filters.dateRange.start ?? today;
|
|
711
|
+
const endDate = filters.dateRange.end ?? today;
|
|
712
|
+
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
713
|
+
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
714
|
+
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
715
|
+
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
716
|
+
return { baskets: {
|
|
717
|
+
FROM: "Basket",
|
|
718
|
+
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
719
|
+
} };
|
|
720
|
+
}
|
|
721
|
+
/** Flattens the raw report rows/keys into structured baskets. */
|
|
722
|
+
function parseBasketReportData(reports) {
|
|
723
|
+
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
724
|
+
const { keys, data } = reports.baskets;
|
|
725
|
+
const keyIndices = {};
|
|
726
|
+
keys.forEach((key, index) => {
|
|
727
|
+
keyIndices[key.name] = index;
|
|
728
|
+
});
|
|
729
|
+
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
730
|
+
const productSubKeys = {};
|
|
731
|
+
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
732
|
+
productSubKeys[subKey.name] = index;
|
|
733
|
+
});
|
|
734
|
+
return data.map((row) => {
|
|
735
|
+
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
736
|
+
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
737
|
+
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
738
|
+
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
739
|
+
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
740
|
+
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
741
|
+
}));
|
|
742
|
+
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
743
|
+
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
744
|
+
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
745
|
+
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
746
|
+
return {
|
|
747
|
+
id: row[keyIndices["Basket.id"]],
|
|
748
|
+
name: row[keyIndices["Basket.name"]],
|
|
749
|
+
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
750
|
+
products,
|
|
751
|
+
total,
|
|
752
|
+
taxTotal,
|
|
753
|
+
status: "completed",
|
|
754
|
+
completeDate
|
|
755
|
+
};
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
759
|
+
function collectConsumerIds(baskets) {
|
|
760
|
+
if (!baskets.length) return null;
|
|
761
|
+
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
762
|
+
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
763
|
+
}
|
|
764
|
+
/** Unique product ids referenced by a set of baskets. */
|
|
765
|
+
function collectProductIds(baskets) {
|
|
766
|
+
if (!baskets.length) return null;
|
|
767
|
+
const ids = /* @__PURE__ */ new Set();
|
|
768
|
+
baskets.forEach((basket) => {
|
|
769
|
+
basket.products.forEach((product) => {
|
|
770
|
+
if (product.productId) ids.add(product.productId);
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
return ids.size > 0 ? [...ids] : null;
|
|
774
|
+
}
|
|
775
|
+
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
776
|
+
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
777
|
+
if (!baskets.length) return [];
|
|
778
|
+
let combinedRows = baskets.map((basket) => {
|
|
779
|
+
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
780
|
+
const productsWithNames = basket.products.map((p) => {
|
|
781
|
+
const product = productMap[p.productId];
|
|
782
|
+
return {
|
|
783
|
+
...p,
|
|
784
|
+
productName: product?.name || `Product #${p.productId}`
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
return {
|
|
788
|
+
id: basket.id,
|
|
789
|
+
basketId: basket.id,
|
|
790
|
+
basketName: basket.name || `Basket #${basket.id}`,
|
|
791
|
+
consumerId: basket.consumerId,
|
|
792
|
+
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
793
|
+
consumerEmail: consumer?.email || "-",
|
|
794
|
+
products: productsWithNames,
|
|
795
|
+
basketTotal: (basket.total || 0) / 100,
|
|
796
|
+
tax: basket.taxTotal || 0,
|
|
797
|
+
dateTime: basket.completeDate,
|
|
798
|
+
status: basket.status || "pending"
|
|
799
|
+
};
|
|
800
|
+
});
|
|
801
|
+
if (search) {
|
|
802
|
+
const searchLower = search.toLowerCase();
|
|
803
|
+
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
804
|
+
}
|
|
805
|
+
combinedRows.sort((a, b) => {
|
|
806
|
+
const dateA = new Date(a.dateTime).getTime();
|
|
807
|
+
return new Date(b.dateTime).getTime() - dateA;
|
|
808
|
+
});
|
|
809
|
+
return combinedRows;
|
|
810
|
+
}
|
|
811
|
+
/** Aggregate totals for a set of report rows. */
|
|
812
|
+
function computeBasketSummary(rows) {
|
|
813
|
+
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
814
|
+
return {
|
|
815
|
+
totalBaskets: rows.length,
|
|
816
|
+
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
817
|
+
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
818
|
+
uniqueConsumers: uniqueConsumerIds.size
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/** Builds a consumer id → consumer lookup. */
|
|
822
|
+
function buildConsumerMap(consumers) {
|
|
823
|
+
const map = {};
|
|
824
|
+
if (consumers) consumers.forEach((consumer) => {
|
|
825
|
+
map[consumer.id] = consumer;
|
|
826
|
+
});
|
|
827
|
+
return map;
|
|
828
|
+
}
|
|
829
|
+
|
|
760
830
|
//#endregion
|
|
761
831
|
//#region src/core/merchant.ts
|
|
762
832
|
const MERCHANT_PATH = `/api/${SHOPWAVE_RESOURCES.merchant.route}`;
|
|
@@ -1100,6 +1170,33 @@ function useProduct(params = {}, options) {
|
|
|
1100
1170
|
]);
|
|
1101
1171
|
}
|
|
1102
1172
|
|
|
1173
|
+
//#endregion
|
|
1174
|
+
//#region src/hooks/useProductTags.ts
|
|
1175
|
+
/**
|
|
1176
|
+
* The merchant's distinct product tags, for a tag field's suggestions.
|
|
1177
|
+
* `data` is `string[]` once loaded.
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* const [opened, setOpened] = useState(false);
|
|
1181
|
+
* const { data: tags, loading } = useProductTags({ enabled: opened });
|
|
1182
|
+
*/
|
|
1183
|
+
function useProductTags(options) {
|
|
1184
|
+
return useQuery((signal) => fetchProductTags({
|
|
1185
|
+
...options,
|
|
1186
|
+
signal
|
|
1187
|
+
}), [options?.baseUrl, options?.token], options?.enabled ?? true);
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* The merchant's distinct price-instance (channel) tags: Eat In, Takeout, …
|
|
1191
|
+
* `data` is `string[]` once loaded.
|
|
1192
|
+
*/
|
|
1193
|
+
function useProductInstanceTags(options) {
|
|
1194
|
+
return useQuery((signal) => fetchProductInstanceTags({
|
|
1195
|
+
...options,
|
|
1196
|
+
signal
|
|
1197
|
+
}), [options?.baseUrl, options?.token], options?.enabled ?? true);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1103
1200
|
//#endregion
|
|
1104
1201
|
//#region src/hooks/usePromotion.ts
|
|
1105
1202
|
/**
|
|
@@ -1410,5 +1507,5 @@ function useBasketReport(options) {
|
|
|
1410
1507
|
}
|
|
1411
1508
|
|
|
1412
1509
|
//#endregion
|
|
1413
|
-
export { useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser };
|
|
1510
|
+
export { useBasketReport, useCategory, useConsumer, useDelete, useEmployee, useLogout, useMerchant, useProduct, useProductInstanceTags, useProductTags, usePromotion, useReport, useSession, useStore, useSubmit, useUpdateMerchant, useUploadImage, useUploadMerchantImage, useUser };
|
|
1414
1511
|
//# sourceMappingURL=index.js.map
|