react-shopwave-connect 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/core/index.cjs +426 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +481 -0
- package/dist/core/index.d.ts +481 -0
- package/dist/core/index.js +399 -0
- package/dist/core/index.js.map +1 -0
- package/dist/hooks/index.cjs +783 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.cts +424 -0
- package/dist/hooks/index.d.ts +424 -0
- package/dist/hooks/index.js +749 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.cjs +824 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +618 -0
- package/dist/index.d.ts +618 -0
- package/dist/index.js +763 -0
- package/dist/index.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
//#region src/core/request.ts
|
|
2
|
+
function resolveFetch(options) {
|
|
3
|
+
const f = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
4
|
+
if (!f) throw new Error("No fetch implementation available. Pass `options.fetch` (e.g. node-fetch / undici) when running outside the browser.");
|
|
5
|
+
return f;
|
|
6
|
+
}
|
|
7
|
+
function buildExtras(extras, options) {
|
|
8
|
+
const merged = {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
...extras
|
|
11
|
+
};
|
|
12
|
+
if (options.token != null && merged.token == null) merged.token = options.token;
|
|
13
|
+
return merged;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Returns a JSON string of API errors if the response carries any, otherwise
|
|
17
|
+
* `null`. Mirrors the original `!responseJson.api.message.errors` checks.
|
|
18
|
+
*/
|
|
19
|
+
function getApiErrors(api) {
|
|
20
|
+
const errors = api?.message?.errors;
|
|
21
|
+
if (errors && Object.keys(errors).length > 0) return JSON.stringify(errors);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* GET helper. Sends request metadata via the `extras` header exactly like the
|
|
26
|
+
* original hooks did, parses JSON, and returns the typed payload.
|
|
27
|
+
*/
|
|
28
|
+
async function apiGet(path, extras, options = {}) {
|
|
29
|
+
const doFetch = resolveFetch(options);
|
|
30
|
+
const url = (options.baseUrl ?? "") + path;
|
|
31
|
+
const headers = { extras: JSON.stringify(buildExtras(extras, options)) };
|
|
32
|
+
console.log("[API GET]", {
|
|
33
|
+
url,
|
|
34
|
+
headers
|
|
35
|
+
});
|
|
36
|
+
const response = await doFetch(url, {
|
|
37
|
+
method: "GET",
|
|
38
|
+
headers,
|
|
39
|
+
signal: options.signal
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
|
|
42
|
+
return await response.json();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* JSON body helper for POST/PUT mutations.
|
|
46
|
+
*/
|
|
47
|
+
async function apiSend(path, method, payload, options = {}) {
|
|
48
|
+
const doFetch = resolveFetch(options);
|
|
49
|
+
const url = (options.baseUrl ?? "") + path;
|
|
50
|
+
const headers = { "Content-Type": "application/json" };
|
|
51
|
+
if (options.token != null) headers.token = options.token;
|
|
52
|
+
console.log(`[API ${method}]`, {
|
|
53
|
+
url,
|
|
54
|
+
headers,
|
|
55
|
+
payload
|
|
56
|
+
});
|
|
57
|
+
const response = await doFetch(url, {
|
|
58
|
+
method,
|
|
59
|
+
headers,
|
|
60
|
+
body: JSON.stringify(payload),
|
|
61
|
+
signal: options.signal
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) throw new Error(`Failed to ${method.toLowerCase()}: ${response.statusText}`);
|
|
64
|
+
return await response.json();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* DELETE helper.
|
|
68
|
+
*/
|
|
69
|
+
async function apiDelete(path, options = {}) {
|
|
70
|
+
const doFetch = resolveFetch(options);
|
|
71
|
+
const url = (options.baseUrl ?? "") + path;
|
|
72
|
+
const headers = {};
|
|
73
|
+
if (options.token != null) headers.token = options.token;
|
|
74
|
+
console.log("[API DELETE]", {
|
|
75
|
+
url,
|
|
76
|
+
headers
|
|
77
|
+
});
|
|
78
|
+
const response = await doFetch(url, {
|
|
79
|
+
method: "DELETE",
|
|
80
|
+
headers,
|
|
81
|
+
signal: options.signal
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) throw new Error(`Failed to delete: ${response.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/core/category.ts
|
|
88
|
+
/**
|
|
89
|
+
* Fetches categories. Framework-agnostic: returns the flattened `Category[]`
|
|
90
|
+
* or throws on API errors.
|
|
91
|
+
*/
|
|
92
|
+
async function fetchCategories(params = {}, options = {}) {
|
|
93
|
+
const extras = { deleted: params.deleted ?? false };
|
|
94
|
+
if (params.categoryIds) extras.categoryIds = params.categoryIds;
|
|
95
|
+
if (params.token) extras.token = params.token;
|
|
96
|
+
const json = await apiGet("/api/categories", extras, options);
|
|
97
|
+
const error = getApiErrors(json.api);
|
|
98
|
+
if (error) throw new Error(error);
|
|
99
|
+
return Object.values(json.categories ?? {});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/core/consumer.ts
|
|
104
|
+
/**
|
|
105
|
+
* Fetches consumers by id. Returns an empty array when no ids are supplied
|
|
106
|
+
* (the original hook short-circuited in that case).
|
|
107
|
+
*/
|
|
108
|
+
async function fetchConsumers(consumerIds, options = {}) {
|
|
109
|
+
if (!consumerIds || consumerIds.length === 0) return [];
|
|
110
|
+
const json = await apiGet("/api/consumer", { ids: consumerIds.join(",") }, options);
|
|
111
|
+
const error = getApiErrors(json.api);
|
|
112
|
+
if (error) throw new Error(error);
|
|
113
|
+
return Object.values(json.consumers ?? {});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/core/employee.ts
|
|
118
|
+
const rolesById = {
|
|
119
|
+
"1": "Owner",
|
|
120
|
+
"2": "Manager",
|
|
121
|
+
"3": "Assistant",
|
|
122
|
+
"4": "Guest",
|
|
123
|
+
"5": "Assistant Manager"
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Fetches employees. Returns the flattened `Employee[]`.
|
|
127
|
+
*/
|
|
128
|
+
async function fetchEmployees(params = {}, options = {}) {
|
|
129
|
+
const extras = { deleted: params.deleted ?? false };
|
|
130
|
+
if (params.employeeIds) extras.employeeIds = params.employeeIds;
|
|
131
|
+
const json = await apiGet("/api/employees", extras, options);
|
|
132
|
+
const error = getApiErrors(json.api);
|
|
133
|
+
if (error) throw new Error(error);
|
|
134
|
+
return Object.values(json.employees ?? {});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/core/product.ts
|
|
139
|
+
/**
|
|
140
|
+
* Fetches products. Returns the flattened `Product[]`.
|
|
141
|
+
*/
|
|
142
|
+
async function fetchProducts(params = {}, options = {}) {
|
|
143
|
+
const extras = { deleted: params.deleted ?? false };
|
|
144
|
+
if (params.productIds) extras.productIds = params.productIds;
|
|
145
|
+
if (params.storeId) extras.storeId = params.storeId;
|
|
146
|
+
if (params.token) extras.token = params.token;
|
|
147
|
+
const json = await apiGet("/api/products", extras, options);
|
|
148
|
+
const error = getApiErrors(json.api);
|
|
149
|
+
if (error) throw new Error(error);
|
|
150
|
+
return Object.values(json.products ?? {});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Fetches products by id and returns them keyed by id, transparently batching
|
|
154
|
+
* the request (default 200 ids per call) to stay within API limits. Extracted
|
|
155
|
+
* from the original `useBasketReport` inline batching loop so it can be reused
|
|
156
|
+
* outside React.
|
|
157
|
+
*/
|
|
158
|
+
async function fetchProductsMap(productIds, options = {}, batchSize = 200) {
|
|
159
|
+
const result = {};
|
|
160
|
+
if (!productIds || productIds.length === 0) return result;
|
|
161
|
+
for (let i = 0; i < productIds.length; i += batchSize) {
|
|
162
|
+
const products = await fetchProducts({ productIds: productIds.slice(i, i + batchSize) }, options);
|
|
163
|
+
for (const product of products) result[product.id] = product;
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/core/promotion.ts
|
|
170
|
+
/** Promotion rule identifiers as stored by the Shopwave API. */
|
|
171
|
+
let PromotionRuleSet = /* @__PURE__ */ function(PromotionRuleSet$1) {
|
|
172
|
+
PromotionRuleSet$1["Percentage"] = "|%^/";
|
|
173
|
+
PromotionRuleSet$1["Combo"] = "&%</";
|
|
174
|
+
PromotionRuleSet$1["MealDeal"] = "&=^/";
|
|
175
|
+
return PromotionRuleSet$1;
|
|
176
|
+
}({});
|
|
177
|
+
/**
|
|
178
|
+
* Fetches promotions. Returns the flattened `Promotion[]`.
|
|
179
|
+
*/
|
|
180
|
+
async function fetchPromotions(params = {}, options = {}) {
|
|
181
|
+
const extras = { deleted: params.deleted ?? false };
|
|
182
|
+
if (params.promotionIds) extras.promotionIds = params.promotionIds;
|
|
183
|
+
if (params.token) extras.token = params.token;
|
|
184
|
+
const json = await apiGet("/api/promotions", extras, options);
|
|
185
|
+
const error = getApiErrors(json.api);
|
|
186
|
+
if (error) throw new Error(error);
|
|
187
|
+
return Object.values(json.promotions ?? {});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/core/store.ts
|
|
192
|
+
/**
|
|
193
|
+
* Fetches stores. Returns the flattened `Store[]`.
|
|
194
|
+
*/
|
|
195
|
+
async function fetchStores(params = {}, options = {}) {
|
|
196
|
+
const json = await apiGet("/api/stores", { deleted: params.deleted ?? false }, options);
|
|
197
|
+
const error = getApiErrors(json.api);
|
|
198
|
+
if (error) throw new Error(error);
|
|
199
|
+
return Object.values(json.stores ?? {});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/core/report.ts
|
|
204
|
+
/**
|
|
205
|
+
* Runs one or more report queries and returns the `reports` map keyed by query
|
|
206
|
+
* name. The query is JSON-encoded into the `extras.query` header, matching the
|
|
207
|
+
* original endpoint contract.
|
|
208
|
+
*/
|
|
209
|
+
async function fetchReport(query, options = {}) {
|
|
210
|
+
const json = await apiGet("/api/report", { query: JSON.stringify(query) }, options);
|
|
211
|
+
const error = getApiErrors(json.api);
|
|
212
|
+
if (error) throw new Error(error);
|
|
213
|
+
return json.reports ?? {};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/core/session.ts
|
|
218
|
+
/**
|
|
219
|
+
* Ends the current server session.
|
|
220
|
+
*
|
|
221
|
+
* This is the framework-agnostic half of the original `useLogout` hook. The
|
|
222
|
+
* Next.js-specific redirect logic (router.push to the auth server logout URL,
|
|
223
|
+
* built from server actions) is intentionally NOT included here because it is
|
|
224
|
+
* tied to `next/navigation` and `'use server'` actions — see the README.
|
|
225
|
+
*/
|
|
226
|
+
async function logout(options = {}) {
|
|
227
|
+
await apiDelete("/api/session?action=logout", options);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/core/entity.ts
|
|
232
|
+
/**
|
|
233
|
+
* Generic CRUD-style mutations extracted from `useHandleDelete` /
|
|
234
|
+
* `useHandleSubmit`. The original hooks also surfaced notistack snackbars; that
|
|
235
|
+
* UI concern is deliberately left to the hooks/host layer so core stays
|
|
236
|
+
* dependency-free.
|
|
237
|
+
*/
|
|
238
|
+
/**
|
|
239
|
+
* Deletes `/api/{resourcePath}/{entityId}`.
|
|
240
|
+
*/
|
|
241
|
+
async function deleteEntity(resourcePath, entityId, options = {}) {
|
|
242
|
+
if (entityId == null || entityId === "") throw new Error("entityId is missing");
|
|
243
|
+
await apiDelete(`/api/${resourcePath}/${entityId}`, options);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Sends a create/update mutation and returns the parsed `result` field
|
|
247
|
+
* (matching the original `result.result` access).
|
|
248
|
+
*/
|
|
249
|
+
async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
|
|
250
|
+
return (await apiSend(endpoint, method, payload, options)).result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/core/basket.ts
|
|
255
|
+
const DEFAULT_BASKET_FILTERS = {
|
|
256
|
+
search: "",
|
|
257
|
+
dateRange: {
|
|
258
|
+
start: null,
|
|
259
|
+
end: null
|
|
260
|
+
},
|
|
261
|
+
consumerId: null,
|
|
262
|
+
status: "all"
|
|
263
|
+
};
|
|
264
|
+
const DEFAULT_PAGINATION = {
|
|
265
|
+
page: 0,
|
|
266
|
+
pageSize: 25,
|
|
267
|
+
total: 0
|
|
268
|
+
};
|
|
269
|
+
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
270
|
+
function formatDateForQuery(date, isEndOfDay = false) {
|
|
271
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
272
|
+
}
|
|
273
|
+
/** Builds the `Basket` report query from a set of filters. */
|
|
274
|
+
function buildBasketReportQuery(filters) {
|
|
275
|
+
const whereConditions = [];
|
|
276
|
+
const today = /* @__PURE__ */ new Date();
|
|
277
|
+
const startDate = filters.dateRange.start ?? today;
|
|
278
|
+
const endDate = filters.dateRange.end ?? today;
|
|
279
|
+
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
280
|
+
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
281
|
+
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
282
|
+
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
283
|
+
return { baskets: {
|
|
284
|
+
FROM: "Basket",
|
|
285
|
+
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
286
|
+
} };
|
|
287
|
+
}
|
|
288
|
+
/** Flattens the raw report rows/keys into structured baskets. */
|
|
289
|
+
function parseBasketReportData(reports) {
|
|
290
|
+
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
291
|
+
const { keys, data } = reports.baskets;
|
|
292
|
+
const keyIndices = {};
|
|
293
|
+
keys.forEach((key, index) => {
|
|
294
|
+
keyIndices[key.name] = index;
|
|
295
|
+
});
|
|
296
|
+
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
297
|
+
const productSubKeys = {};
|
|
298
|
+
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
299
|
+
productSubKeys[subKey.name] = index;
|
|
300
|
+
});
|
|
301
|
+
return data.map((row) => {
|
|
302
|
+
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
303
|
+
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
304
|
+
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
305
|
+
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
306
|
+
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
307
|
+
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
308
|
+
}));
|
|
309
|
+
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
310
|
+
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
311
|
+
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
312
|
+
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
313
|
+
return {
|
|
314
|
+
id: row[keyIndices["Basket.id"]],
|
|
315
|
+
name: row[keyIndices["Basket.name"]],
|
|
316
|
+
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
317
|
+
products,
|
|
318
|
+
total,
|
|
319
|
+
taxTotal,
|
|
320
|
+
status: "completed",
|
|
321
|
+
completeDate
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
326
|
+
function collectConsumerIds(baskets) {
|
|
327
|
+
if (!baskets.length) return null;
|
|
328
|
+
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
329
|
+
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
330
|
+
}
|
|
331
|
+
/** Unique product ids referenced by a set of baskets. */
|
|
332
|
+
function collectProductIds(baskets) {
|
|
333
|
+
if (!baskets.length) return null;
|
|
334
|
+
const ids = /* @__PURE__ */ new Set();
|
|
335
|
+
baskets.forEach((basket) => {
|
|
336
|
+
basket.products.forEach((product) => {
|
|
337
|
+
if (product.productId) ids.add(product.productId);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
return ids.size > 0 ? [...ids] : null;
|
|
341
|
+
}
|
|
342
|
+
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
343
|
+
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
344
|
+
if (!baskets.length) return [];
|
|
345
|
+
let combinedRows = baskets.map((basket) => {
|
|
346
|
+
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
347
|
+
const productsWithNames = basket.products.map((p) => {
|
|
348
|
+
const product = productMap[p.productId];
|
|
349
|
+
return {
|
|
350
|
+
...p,
|
|
351
|
+
productName: product?.name || `Product #${p.productId}`
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
id: basket.id,
|
|
356
|
+
basketId: basket.id,
|
|
357
|
+
basketName: basket.name || `Basket #${basket.id}`,
|
|
358
|
+
consumerId: basket.consumerId,
|
|
359
|
+
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
360
|
+
consumerEmail: consumer?.email || "-",
|
|
361
|
+
products: productsWithNames,
|
|
362
|
+
basketTotal: (basket.total || 0) / 100,
|
|
363
|
+
tax: basket.taxTotal || 0,
|
|
364
|
+
dateTime: basket.completeDate,
|
|
365
|
+
status: basket.status || "pending"
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
if (search) {
|
|
369
|
+
const searchLower = search.toLowerCase();
|
|
370
|
+
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
371
|
+
}
|
|
372
|
+
combinedRows.sort((a, b) => {
|
|
373
|
+
const dateA = new Date(a.dateTime).getTime();
|
|
374
|
+
return new Date(b.dateTime).getTime() - dateA;
|
|
375
|
+
});
|
|
376
|
+
return combinedRows;
|
|
377
|
+
}
|
|
378
|
+
/** Aggregate totals for a set of report rows. */
|
|
379
|
+
function computeBasketSummary(rows) {
|
|
380
|
+
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
381
|
+
return {
|
|
382
|
+
totalBaskets: rows.length,
|
|
383
|
+
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
384
|
+
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
385
|
+
uniqueConsumers: uniqueConsumerIds.size
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/** Builds a consumer id → consumer lookup. */
|
|
389
|
+
function buildConsumerMap(consumers) {
|
|
390
|
+
const map = {};
|
|
391
|
+
if (consumers) consumers.forEach((consumer) => {
|
|
392
|
+
map[consumer.id] = consumer;
|
|
393
|
+
});
|
|
394
|
+
return map;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
export { DEFAULT_BASKET_FILTERS, DEFAULT_PAGINATION, PromotionRuleSet, apiDelete, apiGet, apiSend, buildBasketReportQuery, buildConsumerMap, collectConsumerIds, collectProductIds, combineBasketRows, computeBasketSummary, deleteEntity, fetchCategories, fetchConsumers, fetchEmployees, fetchProducts, fetchProductsMap, fetchPromotions, fetchReport, fetchStores, formatDateForQuery, getApiErrors, logout, parseBasketReportData, rolesById, submitEntity };
|
|
399
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["merged: Extras","headers: Record<string, string>","extras: Record<string, unknown>","extras: Record<string, unknown>","extras: Record<string, unknown>","result: { [id: number]: Product }","extras: Record<string, unknown>","DEFAULT_BASKET_FILTERS: BasketFilters","DEFAULT_PAGINATION: BasketPagination","whereConditions: string[]","keyIndices: { [key: string]: number }","productSubKeys: { [key: string]: number }","products: BasketItem[]","combinedRows: BasketReportRow[]","productsWithNames: BasketItem[]","map: { [id: number]: Consumer }"],"sources":["../../src/core/request.ts","../../src/core/category.ts","../../src/core/consumer.ts","../../src/core/employee.ts","../../src/core/product.ts","../../src/core/promotion.ts","../../src/core/store.ts","../../src/core/report.ts","../../src/core/session.ts","../../src/core/entity.ts","../../src/core/basket.ts"],"sourcesContent":["import type { apiResponse } from \"./types\";\n\n/**\n * Per-call configuration shared by every core API function.\n *\n * Nothing here is React-specific. By accepting these options per call, the\n * core layer works in the browser/Next (relative `/api/...` URLs, the default)\n * AND in Node / CLI / Vue / Angular by passing an absolute `baseUrl`, an auth\n * `token`, and/or a custom `fetch` implementation.\n */\nexport interface RequestOptions {\n /**\n * Prefixed in front of every request path. Leave undefined in the browser to\n * use relative URLs (e.g. `/api/products`). In Node/CLI pass something like\n * `https://api.shopwave.com`.\n */\n baseUrl?: string;\n\n /** OAuth/session token forwarded in the `extras.token` header. */\n token?: string;\n\n /**\n * Custom fetch implementation. Defaults to the global `fetch`. Useful for\n * Node < 18, testing, or injecting interceptors.\n */\n fetch?: typeof fetch;\n\n /** Optional abort signal for cancellation (used by the hooks layer). */\n signal?: AbortSignal;\n}\n\n/** The Shopwave API ships request metadata inside a single `extras` header. */\nexport type Extras = Record<string, unknown>;\n\nfunction resolveFetch(options: RequestOptions): typeof fetch {\n const f = options.fetch ?? (typeof fetch !== \"undefined\" ? fetch : undefined);\n if (!f) {\n throw new Error(\n \"No fetch implementation available. Pass `options.fetch` (e.g. node-fetch / undici) when running outside the browser.\"\n );\n }\n return f;\n}\n\nfunction buildExtras(extras: Extras, options: RequestOptions): Extras {\n const merged: Extras = { \"Content-Type\": \"application/json\", ...extras };\n if (options.token != null && merged.token == null) {\n merged.token = options.token;\n }\n return merged;\n}\n\n/**\n * Returns a JSON string of API errors if the response carries any, otherwise\n * `null`. Mirrors the original `!responseJson.api.message.errors` checks.\n */\nexport function getApiErrors(api?: apiResponse): string | null {\n const errors = api?.message?.errors;\n if (errors && Object.keys(errors).length > 0) {\n return JSON.stringify(errors);\n }\n return null;\n}\n\n/**\n * GET helper. Sends request metadata via the `extras` header exactly like the\n * original hooks did, parses JSON, and returns the typed payload.\n */\nexport async function apiGet<T>(\n path: string,\n extras: Extras,\n options: RequestOptions = {}\n): Promise<T> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n const headers = { extras: JSON.stringify(buildExtras(extras, options)) };\n console.log('[API GET]', { url, headers });\n\n const response = await doFetch(url, {\n method: \"GET\",\n headers,\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Network response was not ok (${response.status})`);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * JSON body helper for POST/PUT mutations.\n */\nexport async function apiSend<T>(\n path: string,\n method: \"POST\" | \"PUT\",\n payload: unknown,\n options: RequestOptions = {}\n): Promise<T> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (options.token != null) {\n headers.token = options.token;\n }\n\n console.log(`[API ${method}]`, { url, headers, payload });\n\n const response = await doFetch(url, {\n method,\n headers,\n body: JSON.stringify(payload),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to ${method.toLowerCase()}: ${response.statusText}`);\n }\n\n return (await response.json()) as T;\n}\n\n/**\n * DELETE helper.\n */\nexport async function apiDelete(\n path: string,\n options: RequestOptions = {}\n): Promise<void> {\n const doFetch = resolveFetch(options);\n const url = (options.baseUrl ?? \"\") + path;\n\n const headers: Record<string, string> = {};\n if (options.token != null) {\n headers.token = options.token;\n }\n\n console.log('[API DELETE]', { url, headers });\n\n const response = await doFetch(url, {\n method: \"DELETE\",\n headers,\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to delete: ${response.statusText}`);\n }\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface Category {\n id: number;\n title: string;\n parentId: number | null;\n deleteDate: string;\n activeDate: string;\n type: number;\n}\n\nexport interface CategoryResponse {\n categories: {\n [key: number]: Category;\n };\n api: apiResponse;\n}\n\nexport interface FetchCategoriesParams {\n categoryIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** Token override (also accepted via `options.token`). */\n token?: string;\n}\n\n/**\n * Fetches categories. Framework-agnostic: returns the flattened `Category[]`\n * or throws on API errors.\n */\nexport async function fetchCategories(\n params: FetchCategoriesParams = {},\n options: RequestOptions = {}\n): Promise<Category[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.categoryIds) {\n extras.categoryIds = params.categoryIds;\n }\n if (params.token) {\n extras.token = params.token;\n }\n\n const json = await apiGet<CategoryResponse>(\"/api/categories\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.categories ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface Consumer {\n id: number;\n activeCount: number;\n firstName: string;\n lastName: string;\n email: string;\n}\n\nexport interface ConsumerResponse {\n consumers: {\n [key: number]: Consumer;\n };\n api: apiResponse;\n}\n\n/**\n * Fetches consumers by id. Returns an empty array when no ids are supplied\n * (the original hook short-circuited in that case).\n */\nexport async function fetchConsumers(\n consumerIds: number[] | null | undefined,\n options: RequestOptions = {}\n): Promise<Consumer[]> {\n if (!consumerIds || consumerIds.length === 0) {\n return [];\n }\n\n const extras: Record<string, unknown> = { ids: consumerIds.join(\",\") };\n\n const json = await apiGet<ConsumerResponse>(\"/api/consumer\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.consumers ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport const rolesById = {\n \"1\": \"Owner\",\n \"2\": \"Manager\",\n \"3\": \"Assistant\",\n \"4\": \"Guest\",\n \"5\": \"Assistant Manager\",\n} as const;\n\nexport interface Employee {\n id: number;\n firstName: string;\n lastName: string;\n email: string;\n roleId: number;\n joinedDate: string;\n exitDate: string;\n stores: {\n [key: number]: {\n id: number;\n roleId: number;\n };\n };\n}\n\nexport interface EmployeeResponse {\n employees: {\n [key: number]: Employee;\n };\n api: apiResponse;\n}\n\nexport interface FetchEmployeesParams {\n /** Optional id filter. Forwarded when provided. */\n employeeIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n}\n\n/**\n * Fetches employees. Returns the flattened `Employee[]`.\n */\nexport async function fetchEmployees(\n params: FetchEmployeesParams = {},\n options: RequestOptions = {}\n): Promise<Employee[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.employeeIds) {\n extras.employeeIds = params.employeeIds;\n }\n\n const json = await apiGet<EmployeeResponse>(\"/api/employees\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.employees ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface ProductInstance {\n id: number;\n price: string;\n taxPercentage: string;\n timestamp: string;\n size: string;\n name: string;\n activeDate: string;\n tags: string;\n}\n\nexport interface Product {\n id: number;\n barcode: string;\n name: string;\n details: string;\n tags: string;\n unit: number;\n timestamp: string;\n activeDate: string;\n deleteDate: string;\n images: Array<string>;\n categories: Array<number>;\n instances: {\n [key: number]: ProductInstance;\n };\n}\n\nexport interface ProductResponse {\n products: {\n [key: number]: Product;\n };\n api: apiResponse;\n}\n\nexport interface FetchProductsParams {\n productIds?: Array<number>;\n storeId?: number;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** Token override (also accepted via `options.token`). */\n token?: string;\n}\n\n/**\n * Fetches products. Returns the flattened `Product[]`.\n */\nexport async function fetchProducts(\n params: FetchProductsParams = {},\n options: RequestOptions = {}\n): Promise<Product[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.productIds) {\n extras.productIds = params.productIds;\n }\n if (params.storeId) {\n extras.storeId = params.storeId;\n }\n if (params.token) {\n extras.token = params.token;\n }\n\n const json = await apiGet<ProductResponse>(\"/api/products\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.products ?? {});\n}\n\n/**\n * Fetches products by id and returns them keyed by id, transparently batching\n * the request (default 200 ids per call) to stay within API limits. Extracted\n * from the original `useBasketReport` inline batching loop so it can be reused\n * outside React.\n */\nexport async function fetchProductsMap(\n productIds: number[] | null | undefined,\n options: RequestOptions = {},\n batchSize = 200\n): Promise<{ [id: number]: Product }> {\n const result: { [id: number]: Product } = {};\n if (!productIds || productIds.length === 0) {\n return result;\n }\n\n for (let i = 0; i < productIds.length; i += batchSize) {\n const batch = productIds.slice(i, i + batchSize);\n const products = await fetchProducts({ productIds: batch }, options);\n for (const product of products) {\n result[product.id] = product;\n }\n }\n\n return result;\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\n/** Promotion rule identifiers as stored by the Shopwave API. */\nexport enum PromotionRuleSet {\n Percentage = \"|%^/\",\n Combo = \"&%</\",\n MealDeal = \"&=^/\",\n}\n\nexport interface Promotion {\n id: number;\n merchantId: number;\n storeIds: number[];\n categoryIds: { [key: string]: number };\n productIds: { [key: string]: any };\n title: string;\n details: string | null;\n terms: string | null;\n startDate: string;\n endDate: string;\n daysAvailable: number[];\n automatic: number;\n rule: PromotionRuleSet;\n x: number;\n y: number | null;\n}\n\nexport interface PromotionResponse {\n promotions: {\n [key: number]: Promotion;\n };\n api: apiResponse;\n}\n\nexport interface FetchPromotionsParams {\n /** Optional id filter. Forwarded when provided. */\n promotionIds?: Array<number>;\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n /** Token override (also accepted via `options.token`). */\n token?: string;\n}\n\n/**\n * Fetches promotions. Returns the flattened `Promotion[]`.\n */\nexport async function fetchPromotions(\n params: FetchPromotionsParams = {},\n options: RequestOptions = {}\n): Promise<Promotion[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n if (params.promotionIds) {\n extras.promotionIds = params.promotionIds;\n }\n if (params.token) {\n extras.token = params.token;\n }\n\n const json = await apiGet<PromotionResponse>(\"/api/promotions\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.promotions ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface Store {\n id: number;\n lat: number;\n lng: number;\n addressLine1: string;\n addressLine2: string | null;\n addressLine3: string;\n phoneNumber: string;\n city: string;\n postcode: string;\n countryId: number | null;\n timezoneId: number | null;\n storeDeleteDate: string | null;\n email: string | null;\n sandbox: boolean | null;\n}\n\nexport interface StoreResponse {\n stores: {\n [key: number]: Store;\n };\n api: apiResponse;\n}\n\nexport interface FetchStoresParams {\n /** Whether to include soft-deleted records. Defaults to `false`. */\n deleted?: boolean;\n}\n\n/**\n * Fetches stores. Returns the flattened `Store[]`.\n */\nexport async function fetchStores(\n params: FetchStoresParams = {},\n options: RequestOptions = {}\n): Promise<Store[]> {\n const extras: Record<string, unknown> = { deleted: params.deleted ?? false };\n\n const json = await apiGet<StoreResponse>(\"/api/stores\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return Object.values(json.stores ?? {});\n}\n","import { apiGet, getApiErrors, type RequestOptions } from \"./request\";\nimport type { apiResponse } from \"./types\";\n\nexport interface ReportKey {\n name: string;\n type: string;\n item?: ReportKey[];\n}\n\nexport interface ReportData {\n keys: ReportKey[];\n data: any[][];\n}\n\nexport interface ReportResponse {\n reports: {\n [key: string]: ReportData;\n };\n api: apiResponse;\n}\n\nexport interface ReportQuery {\n FROM: string;\n WHERE?: {\n AND?: string[];\n OR?: string[];\n };\n SELECT?: string[];\n LIMIT?: number;\n OFFSET?: number;\n}\n\nexport interface ReportQueryMap {\n [key: string]: ReportQuery;\n}\n\n/**\n * Runs one or more report queries and returns the `reports` map keyed by query\n * name. The query is JSON-encoded into the `extras.query` header, matching the\n * original endpoint contract.\n */\nexport async function fetchReport(\n query: ReportQueryMap,\n options: RequestOptions = {}\n): Promise<ReportResponse[\"reports\"]> {\n const extras: Record<string, unknown> = { query: JSON.stringify(query) };\n\n const json = await apiGet<ReportResponse>(\"/api/report\", extras, options);\n\n const error = getApiErrors(json.api);\n if (error) {\n throw new Error(error);\n }\n\n return json.reports ?? {};\n}\n","import { apiDelete, type RequestOptions } from \"./request\";\n\n/**\n * Ends the current server session.\n *\n * This is the framework-agnostic half of the original `useLogout` hook. The\n * Next.js-specific redirect logic (router.push to the auth server logout URL,\n * built from server actions) is intentionally NOT included here because it is\n * tied to `next/navigation` and `'use server'` actions — see the README.\n */\nexport async function logout(options: RequestOptions = {}): Promise<void> {\n await apiDelete(\"/api/session?action=logout\", options);\n}\n","import { apiDelete, apiSend, type RequestOptions } from \"./request\";\n\n/**\n * Generic CRUD-style mutations extracted from `useHandleDelete` /\n * `useHandleSubmit`. The original hooks also surfaced notistack snackbars; that\n * UI concern is deliberately left to the hooks/host layer so core stays\n * dependency-free.\n */\n\n/**\n * Deletes `/api/{resourcePath}/{entityId}`.\n */\nexport async function deleteEntity(\n resourcePath: string,\n entityId: number | string,\n options: RequestOptions = {}\n): Promise<void> {\n if (entityId == null || entityId === \"\") {\n throw new Error(\"entityId is missing\");\n }\n await apiDelete(`/api/${resourcePath}/${entityId}`, options);\n}\n\nexport interface SubmitEntityArgs {\n /** Fully-resolved endpoint, e.g. `/api/products` or `/api/products/42`. */\n endpoint: string;\n /** Defaults to PUT when an `entityId` exists, otherwise POST. */\n method?: \"POST\" | \"PUT\";\n /** The request payload (already transformed by the caller). */\n payload: unknown;\n}\n\nexport interface SubmitEntityResponse<R = any> {\n result?: R;\n [key: string]: unknown;\n}\n\n/**\n * Sends a create/update mutation and returns the parsed `result` field\n * (matching the original `result.result` access).\n */\nexport async function submitEntity<R = any>(\n { endpoint, method = \"POST\", payload }: SubmitEntityArgs,\n options: RequestOptions = {}\n): Promise<R | undefined> {\n const json = await apiSend<SubmitEntityResponse<R>>(\n endpoint,\n method,\n payload,\n options\n );\n return json.result;\n}\n","import type { Consumer } from \"./consumer\";\nimport type { Product } from \"./product\";\nimport type { ReportQueryMap, ReportResponse } from \"./report\";\n\n// ── Basket Report Types (preserved from the original `types/basket.ts`) ──────\n\nexport interface BasketItem {\n productId: number;\n productName: string;\n quantity: number;\n price: number;\n tax: number;\n}\n\nexport interface Basket {\n id: number;\n name: string;\n consumerId: number;\n products: BasketItem[];\n total: number;\n taxTotal: number;\n status: \"pending\" | \"completed\" | \"cancelled\";\n completeDate: string;\n}\n\nexport interface BasketReportRow {\n id: number;\n basketId: number;\n basketName: string;\n consumerId: number;\n consumerName: string;\n consumerEmail: string;\n products: BasketItem[];\n basketTotal: number;\n tax: number;\n dateTime: string;\n status: string;\n}\n\nexport interface BasketSummary {\n totalBaskets: number;\n totalRevenue: number;\n totalTax: number;\n uniqueConsumers: number;\n}\n\nexport interface BasketFilters {\n search: string;\n dateRange: {\n start: Date | null;\n end: Date | null;\n };\n consumerId: number | null;\n status: \"all\" | \"pending\" | \"completed\" | \"cancelled\";\n}\n\nexport interface BasketPagination {\n page: number;\n pageSize: number;\n total: number;\n}\n\nexport const DEFAULT_BASKET_FILTERS: BasketFilters = {\n search: \"\",\n dateRange: {\n start: null,\n end: null,\n },\n consumerId: null,\n status: \"all\",\n};\n\nexport const DEFAULT_PAGINATION: BasketPagination = {\n page: 0,\n pageSize: 25,\n total: 0,\n};\n\n/** Intermediate shape produced by {@link parseBasketReportData}. */\nexport interface ParsedBasket {\n id: number;\n name: string;\n consumerId: number;\n products: BasketItem[];\n total: number;\n taxTotal: number;\n status: \"completed\";\n completeDate: string;\n}\n\n// ── Pure transform helpers (extracted from the original `useBasketReport`) ────\n\n/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */\nexport function formatDateForQuery(date: Date, isEndOfDay = false): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n const time = isEndOfDay ? \"23:59:59\" : \"00:00:00\";\n return `${year}-${month}-${day} ${time}`;\n}\n\n/** Builds the `Basket` report query from a set of filters. */\nexport function buildBasketReportQuery(filters: BasketFilters): ReportQueryMap {\n const whereConditions: string[] = [];\n\n const today = new Date();\n const startDate = filters.dateRange.start ?? today;\n const endDate = filters.dateRange.end ?? today;\n\n whereConditions.push(\n `Basket.completeDate > '${formatDateForQuery(startDate, false)}'`\n );\n whereConditions.push(\n `Basket.completeDate < '${formatDateForQuery(endDate, true)}'`\n );\n\n if (filters.consumerId) {\n whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);\n }\n\n if (filters.status !== \"all\") {\n whereConditions.push(`Basket.status = '${filters.status}'`);\n }\n\n return {\n baskets: {\n FROM: \"Basket\",\n WHERE: whereConditions.length > 0 ? { AND: whereConditions } : undefined,\n },\n };\n}\n\n/** Flattens the raw report rows/keys into structured baskets. */\nexport function parseBasketReportData(\n reports: ReportResponse[\"reports\"] | null | undefined\n): ParsedBasket[] {\n if (!reports?.baskets?.data || !reports?.baskets?.keys) {\n return [];\n }\n\n const { keys, data } = reports.baskets;\n\n const keyIndices: { [key: string]: number } = {};\n keys.forEach((key, index) => {\n keyIndices[key.name] = index;\n });\n\n const productKey = keys.find((k) => k.name === \"Basket.Product\");\n const productSubKeys: { [key: string]: number } = {};\n if (productKey?.item) {\n productKey.item.forEach((subKey, index) => {\n productSubKeys[subKey.name] = index;\n });\n }\n\n return data.map((row) => {\n const productsArray = (row[keyIndices[\"Basket.Product\"]] as any[][]) || [];\n const products: BasketItem[] = productsArray.map((productRow) => ({\n productId: productRow[productSubKeys[\"Basket.Product.productId\"]] as number,\n productName: `Product #${productRow[productSubKeys[\"Basket.Product.productId\"]]}`,\n quantity:\n (productRow[productSubKeys[\"Basket.Product.quantity\"]] as number) || 1,\n price:\n (productRow[productSubKeys[\"Basket.Product.productPrice\"]] as number) ||\n 0,\n tax:\n (productRow[\n productSubKeys[\"Basket.Product.productTaxPercentage\"]\n ] as number) || 0,\n }));\n\n const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);\n const taxTotal = products.reduce(\n (sum, p) => sum + p.price * p.quantity * p.tax,\n 0\n );\n\n const completeDateTimestamp = row[keyIndices[\"Basket.completeDate\"]] as number;\n const completeDate = completeDateTimestamp\n ? new Date(completeDateTimestamp).toISOString()\n : \"\";\n\n return {\n id: row[keyIndices[\"Basket.id\"]] as number,\n name: row[keyIndices[\"Basket.name\"]] as string,\n consumerId: row[keyIndices[\"Basket.consumerId\"]] as number,\n products,\n total,\n taxTotal,\n status: \"completed\" as const,\n completeDate,\n };\n });\n}\n\n/** Unique non-null consumer ids referenced by a set of baskets. */\nexport function collectConsumerIds(baskets: ParsedBasket[]): number[] | null {\n if (!baskets.length) return null;\n const ids = baskets\n .map((b) => b.consumerId)\n .filter((id): id is number => id != null);\n return ids.length > 0 ? [...new Set(ids)] : null;\n}\n\n/** Unique product ids referenced by a set of baskets. */\nexport function collectProductIds(baskets: ParsedBasket[]): number[] | null {\n if (!baskets.length) return null;\n const ids = new Set<number>();\n baskets.forEach((basket) => {\n basket.products.forEach((product) => {\n if (product.productId) {\n ids.add(product.productId);\n }\n });\n });\n return ids.size > 0 ? [...ids] : null;\n}\n\nexport interface CombineBasketRowsParams {\n baskets: ParsedBasket[];\n consumerMap: { [id: number]: Consumer };\n productMap: { [id: number]: Product };\n search?: string;\n}\n\n/** Joins baskets with consumer + product lookups, applies search, sorts. */\nexport function combineBasketRows({\n baskets,\n consumerMap,\n productMap,\n search,\n}: CombineBasketRowsParams): BasketReportRow[] {\n if (!baskets.length) return [];\n\n let combinedRows: BasketReportRow[] = baskets.map((basket) => {\n const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;\n\n const productsWithNames: BasketItem[] = basket.products.map((p) => {\n const product = productMap[p.productId];\n return {\n ...p,\n productName: product?.name || `Product #${p.productId}`,\n };\n });\n\n return {\n id: basket.id,\n basketId: basket.id,\n basketName: basket.name || `Basket #${basket.id}`,\n consumerId: basket.consumerId,\n consumerName: consumer\n ? `${consumer.firstName} ${consumer.lastName}`\n : \"Unknown\",\n consumerEmail: consumer?.email || \"-\",\n products: productsWithNames,\n basketTotal: (basket.total || 0) / 100,\n tax: basket.taxTotal || 0,\n dateTime: basket.completeDate,\n status: basket.status || \"pending\",\n };\n });\n\n if (search) {\n const searchLower = search.toLowerCase();\n combinedRows = combinedRows.filter(\n (row) =>\n row.basketId.toString().includes(searchLower) ||\n row.basketName.toLowerCase().includes(searchLower) ||\n row.consumerName.toLowerCase().includes(searchLower) ||\n row.consumerEmail.toLowerCase().includes(searchLower)\n );\n }\n\n combinedRows.sort((a, b) => {\n const dateA = new Date(a.dateTime).getTime();\n const dateB = new Date(b.dateTime).getTime();\n return dateB - dateA;\n });\n\n return combinedRows;\n}\n\n/** Aggregate totals for a set of report rows. */\nexport function computeBasketSummary(rows: BasketReportRow[]): BasketSummary {\n const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));\n return {\n totalBaskets: rows.length,\n totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),\n totalTax: rows.reduce((sum, row) => sum + row.tax, 0),\n uniqueConsumers: uniqueConsumerIds.size,\n };\n}\n\n/** Builds a consumer id → consumer lookup. */\nexport function buildConsumerMap(consumers: Consumer[] | null | undefined): {\n [id: number]: Consumer;\n} {\n const map: { [id: number]: Consumer } = {};\n if (consumers) {\n consumers.forEach((consumer) => {\n map[consumer.id] = consumer;\n });\n }\n return map;\n}\n"],"mappings":"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"}
|