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,783 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
let react = require("react");
|
|
25
|
+
react = __toESM(react);
|
|
26
|
+
|
|
27
|
+
//#region src/core/request.ts
|
|
28
|
+
function resolveFetch(options) {
|
|
29
|
+
const f = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
30
|
+
if (!f) throw new Error("No fetch implementation available. Pass `options.fetch` (e.g. node-fetch / undici) when running outside the browser.");
|
|
31
|
+
return f;
|
|
32
|
+
}
|
|
33
|
+
function buildExtras(extras, options) {
|
|
34
|
+
const merged = {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
...extras
|
|
37
|
+
};
|
|
38
|
+
if (options.token != null && merged.token == null) merged.token = options.token;
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns a JSON string of API errors if the response carries any, otherwise
|
|
43
|
+
* `null`. Mirrors the original `!responseJson.api.message.errors` checks.
|
|
44
|
+
*/
|
|
45
|
+
function getApiErrors(api) {
|
|
46
|
+
const errors = api?.message?.errors;
|
|
47
|
+
if (errors && Object.keys(errors).length > 0) return JSON.stringify(errors);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* GET helper. Sends request metadata via the `extras` header exactly like the
|
|
52
|
+
* original hooks did, parses JSON, and returns the typed payload.
|
|
53
|
+
*/
|
|
54
|
+
async function apiGet(path, extras, options = {}) {
|
|
55
|
+
const doFetch = resolveFetch(options);
|
|
56
|
+
const url = (options.baseUrl ?? "") + path;
|
|
57
|
+
const headers = { extras: JSON.stringify(buildExtras(extras, options)) };
|
|
58
|
+
console.log("[API GET]", {
|
|
59
|
+
url,
|
|
60
|
+
headers
|
|
61
|
+
});
|
|
62
|
+
const response = await doFetch(url, {
|
|
63
|
+
method: "GET",
|
|
64
|
+
headers,
|
|
65
|
+
signal: options.signal
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
|
|
68
|
+
return await response.json();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* JSON body helper for POST/PUT mutations.
|
|
72
|
+
*/
|
|
73
|
+
async function apiSend(path, method, payload, options = {}) {
|
|
74
|
+
const doFetch = resolveFetch(options);
|
|
75
|
+
const url = (options.baseUrl ?? "") + path;
|
|
76
|
+
const headers = { "Content-Type": "application/json" };
|
|
77
|
+
if (options.token != null) headers.token = options.token;
|
|
78
|
+
console.log(`[API ${method}]`, {
|
|
79
|
+
url,
|
|
80
|
+
headers,
|
|
81
|
+
payload
|
|
82
|
+
});
|
|
83
|
+
const response = await doFetch(url, {
|
|
84
|
+
method,
|
|
85
|
+
headers,
|
|
86
|
+
body: JSON.stringify(payload),
|
|
87
|
+
signal: options.signal
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) throw new Error(`Failed to ${method.toLowerCase()}: ${response.statusText}`);
|
|
90
|
+
return await response.json();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* DELETE helper.
|
|
94
|
+
*/
|
|
95
|
+
async function apiDelete(path, options = {}) {
|
|
96
|
+
const doFetch = resolveFetch(options);
|
|
97
|
+
const url = (options.baseUrl ?? "") + path;
|
|
98
|
+
const headers = {};
|
|
99
|
+
if (options.token != null) headers.token = options.token;
|
|
100
|
+
console.log("[API DELETE]", {
|
|
101
|
+
url,
|
|
102
|
+
headers
|
|
103
|
+
});
|
|
104
|
+
const response = await doFetch(url, {
|
|
105
|
+
method: "DELETE",
|
|
106
|
+
headers,
|
|
107
|
+
signal: options.signal
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) throw new Error(`Failed to delete: ${response.statusText}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/core/category.ts
|
|
114
|
+
/**
|
|
115
|
+
* Fetches categories. Framework-agnostic: returns the flattened `Category[]`
|
|
116
|
+
* or throws on API errors.
|
|
117
|
+
*/
|
|
118
|
+
async function fetchCategories(params = {}, options = {}) {
|
|
119
|
+
const extras = { deleted: params.deleted ?? false };
|
|
120
|
+
if (params.categoryIds) extras.categoryIds = params.categoryIds;
|
|
121
|
+
if (params.token) extras.token = params.token;
|
|
122
|
+
const json = await apiGet("/api/categories", extras, options);
|
|
123
|
+
const error = getApiErrors(json.api);
|
|
124
|
+
if (error) throw new Error(error);
|
|
125
|
+
return Object.values(json.categories ?? {});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/core/consumer.ts
|
|
130
|
+
/**
|
|
131
|
+
* Fetches consumers by id. Returns an empty array when no ids are supplied
|
|
132
|
+
* (the original hook short-circuited in that case).
|
|
133
|
+
*/
|
|
134
|
+
async function fetchConsumers(consumerIds, options = {}) {
|
|
135
|
+
if (!consumerIds || consumerIds.length === 0) return [];
|
|
136
|
+
const json = await apiGet("/api/consumer", { ids: consumerIds.join(",") }, options);
|
|
137
|
+
const error = getApiErrors(json.api);
|
|
138
|
+
if (error) throw new Error(error);
|
|
139
|
+
return Object.values(json.consumers ?? {});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/core/employee.ts
|
|
144
|
+
/**
|
|
145
|
+
* Fetches employees. Returns the flattened `Employee[]`.
|
|
146
|
+
*/
|
|
147
|
+
async function fetchEmployees(params = {}, options = {}) {
|
|
148
|
+
const extras = { deleted: params.deleted ?? false };
|
|
149
|
+
if (params.employeeIds) extras.employeeIds = params.employeeIds;
|
|
150
|
+
const json = await apiGet("/api/employees", extras, options);
|
|
151
|
+
const error = getApiErrors(json.api);
|
|
152
|
+
if (error) throw new Error(error);
|
|
153
|
+
return Object.values(json.employees ?? {});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/core/product.ts
|
|
158
|
+
/**
|
|
159
|
+
* Fetches products. Returns the flattened `Product[]`.
|
|
160
|
+
*/
|
|
161
|
+
async function fetchProducts(params = {}, options = {}) {
|
|
162
|
+
const extras = { deleted: params.deleted ?? false };
|
|
163
|
+
if (params.productIds) extras.productIds = params.productIds;
|
|
164
|
+
if (params.storeId) extras.storeId = params.storeId;
|
|
165
|
+
if (params.token) extras.token = params.token;
|
|
166
|
+
const json = await apiGet("/api/products", extras, options);
|
|
167
|
+
const error = getApiErrors(json.api);
|
|
168
|
+
if (error) throw new Error(error);
|
|
169
|
+
return Object.values(json.products ?? {});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Fetches products by id and returns them keyed by id, transparently batching
|
|
173
|
+
* the request (default 200 ids per call) to stay within API limits. Extracted
|
|
174
|
+
* from the original `useBasketReport` inline batching loop so it can be reused
|
|
175
|
+
* outside React.
|
|
176
|
+
*/
|
|
177
|
+
async function fetchProductsMap(productIds, options = {}, batchSize = 200) {
|
|
178
|
+
const result = {};
|
|
179
|
+
if (!productIds || productIds.length === 0) return result;
|
|
180
|
+
for (let i = 0; i < productIds.length; i += batchSize) {
|
|
181
|
+
const products = await fetchProducts({ productIds: productIds.slice(i, i + batchSize) }, options);
|
|
182
|
+
for (const product of products) result[product.id] = product;
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/core/promotion.ts
|
|
189
|
+
/**
|
|
190
|
+
* Fetches promotions. Returns the flattened `Promotion[]`.
|
|
191
|
+
*/
|
|
192
|
+
async function fetchPromotions(params = {}, options = {}) {
|
|
193
|
+
const extras = { deleted: params.deleted ?? false };
|
|
194
|
+
if (params.promotionIds) extras.promotionIds = params.promotionIds;
|
|
195
|
+
if (params.token) extras.token = params.token;
|
|
196
|
+
const json = await apiGet("/api/promotions", extras, options);
|
|
197
|
+
const error = getApiErrors(json.api);
|
|
198
|
+
if (error) throw new Error(error);
|
|
199
|
+
return Object.values(json.promotions ?? {});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/core/store.ts
|
|
204
|
+
/**
|
|
205
|
+
* Fetches stores. Returns the flattened `Store[]`.
|
|
206
|
+
*/
|
|
207
|
+
async function fetchStores(params = {}, options = {}) {
|
|
208
|
+
const json = await apiGet("/api/stores", { deleted: params.deleted ?? false }, options);
|
|
209
|
+
const error = getApiErrors(json.api);
|
|
210
|
+
if (error) throw new Error(error);
|
|
211
|
+
return Object.values(json.stores ?? {});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/core/report.ts
|
|
216
|
+
/**
|
|
217
|
+
* Runs one or more report queries and returns the `reports` map keyed by query
|
|
218
|
+
* name. The query is JSON-encoded into the `extras.query` header, matching the
|
|
219
|
+
* original endpoint contract.
|
|
220
|
+
*/
|
|
221
|
+
async function fetchReport(query, options = {}) {
|
|
222
|
+
const json = await apiGet("/api/report", { query: JSON.stringify(query) }, options);
|
|
223
|
+
const error = getApiErrors(json.api);
|
|
224
|
+
if (error) throw new Error(error);
|
|
225
|
+
return json.reports ?? {};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/core/session.ts
|
|
230
|
+
/**
|
|
231
|
+
* Ends the current server session.
|
|
232
|
+
*
|
|
233
|
+
* This is the framework-agnostic half of the original `useLogout` hook. The
|
|
234
|
+
* Next.js-specific redirect logic (router.push to the auth server logout URL,
|
|
235
|
+
* built from server actions) is intentionally NOT included here because it is
|
|
236
|
+
* tied to `next/navigation` and `'use server'` actions — see the README.
|
|
237
|
+
*/
|
|
238
|
+
async function logout(options = {}) {
|
|
239
|
+
await apiDelete("/api/session?action=logout", options);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/core/entity.ts
|
|
244
|
+
/**
|
|
245
|
+
* Generic CRUD-style mutations extracted from `useHandleDelete` /
|
|
246
|
+
* `useHandleSubmit`. The original hooks also surfaced notistack snackbars; that
|
|
247
|
+
* UI concern is deliberately left to the hooks/host layer so core stays
|
|
248
|
+
* dependency-free.
|
|
249
|
+
*/
|
|
250
|
+
/**
|
|
251
|
+
* Deletes `/api/{resourcePath}/{entityId}`.
|
|
252
|
+
*/
|
|
253
|
+
async function deleteEntity(resourcePath, entityId, options = {}) {
|
|
254
|
+
if (entityId == null || entityId === "") throw new Error("entityId is missing");
|
|
255
|
+
await apiDelete(`/api/${resourcePath}/${entityId}`, options);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Sends a create/update mutation and returns the parsed `result` field
|
|
259
|
+
* (matching the original `result.result` access).
|
|
260
|
+
*/
|
|
261
|
+
async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
|
|
262
|
+
return (await apiSend(endpoint, method, payload, options)).result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/core/basket.ts
|
|
267
|
+
const DEFAULT_BASKET_FILTERS = {
|
|
268
|
+
search: "",
|
|
269
|
+
dateRange: {
|
|
270
|
+
start: null,
|
|
271
|
+
end: null
|
|
272
|
+
},
|
|
273
|
+
consumerId: null,
|
|
274
|
+
status: "all"
|
|
275
|
+
};
|
|
276
|
+
const DEFAULT_PAGINATION = {
|
|
277
|
+
page: 0,
|
|
278
|
+
pageSize: 25,
|
|
279
|
+
total: 0
|
|
280
|
+
};
|
|
281
|
+
/** Formats a date as `YYYY-MM-DD HH:mm:ss` for the report query. */
|
|
282
|
+
function formatDateForQuery(date, isEndOfDay = false) {
|
|
283
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${isEndOfDay ? "23:59:59" : "00:00:00"}`;
|
|
284
|
+
}
|
|
285
|
+
/** Builds the `Basket` report query from a set of filters. */
|
|
286
|
+
function buildBasketReportQuery(filters) {
|
|
287
|
+
const whereConditions = [];
|
|
288
|
+
const today = /* @__PURE__ */ new Date();
|
|
289
|
+
const startDate = filters.dateRange.start ?? today;
|
|
290
|
+
const endDate = filters.dateRange.end ?? today;
|
|
291
|
+
whereConditions.push(`Basket.completeDate > '${formatDateForQuery(startDate, false)}'`);
|
|
292
|
+
whereConditions.push(`Basket.completeDate < '${formatDateForQuery(endDate, true)}'`);
|
|
293
|
+
if (filters.consumerId) whereConditions.push(`Basket.consumerId = ${filters.consumerId}`);
|
|
294
|
+
if (filters.status !== "all") whereConditions.push(`Basket.status = '${filters.status}'`);
|
|
295
|
+
return { baskets: {
|
|
296
|
+
FROM: "Basket",
|
|
297
|
+
WHERE: whereConditions.length > 0 ? { AND: whereConditions } : void 0
|
|
298
|
+
} };
|
|
299
|
+
}
|
|
300
|
+
/** Flattens the raw report rows/keys into structured baskets. */
|
|
301
|
+
function parseBasketReportData(reports) {
|
|
302
|
+
if (!reports?.baskets?.data || !reports?.baskets?.keys) return [];
|
|
303
|
+
const { keys, data } = reports.baskets;
|
|
304
|
+
const keyIndices = {};
|
|
305
|
+
keys.forEach((key, index) => {
|
|
306
|
+
keyIndices[key.name] = index;
|
|
307
|
+
});
|
|
308
|
+
const productKey = keys.find((k) => k.name === "Basket.Product");
|
|
309
|
+
const productSubKeys = {};
|
|
310
|
+
if (productKey?.item) productKey.item.forEach((subKey, index) => {
|
|
311
|
+
productSubKeys[subKey.name] = index;
|
|
312
|
+
});
|
|
313
|
+
return data.map((row) => {
|
|
314
|
+
const products = (row[keyIndices["Basket.Product"]] || []).map((productRow) => ({
|
|
315
|
+
productId: productRow[productSubKeys["Basket.Product.productId"]],
|
|
316
|
+
productName: `Product #${productRow[productSubKeys["Basket.Product.productId"]]}`,
|
|
317
|
+
quantity: productRow[productSubKeys["Basket.Product.quantity"]] || 1,
|
|
318
|
+
price: productRow[productSubKeys["Basket.Product.productPrice"]] || 0,
|
|
319
|
+
tax: productRow[productSubKeys["Basket.Product.productTaxPercentage"]] || 0
|
|
320
|
+
}));
|
|
321
|
+
const total = products.reduce((sum, p) => sum + p.price * p.quantity, 0);
|
|
322
|
+
const taxTotal = products.reduce((sum, p) => sum + p.price * p.quantity * p.tax, 0);
|
|
323
|
+
const completeDateTimestamp = row[keyIndices["Basket.completeDate"]];
|
|
324
|
+
const completeDate = completeDateTimestamp ? new Date(completeDateTimestamp).toISOString() : "";
|
|
325
|
+
return {
|
|
326
|
+
id: row[keyIndices["Basket.id"]],
|
|
327
|
+
name: row[keyIndices["Basket.name"]],
|
|
328
|
+
consumerId: row[keyIndices["Basket.consumerId"]],
|
|
329
|
+
products,
|
|
330
|
+
total,
|
|
331
|
+
taxTotal,
|
|
332
|
+
status: "completed",
|
|
333
|
+
completeDate
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/** Unique non-null consumer ids referenced by a set of baskets. */
|
|
338
|
+
function collectConsumerIds(baskets) {
|
|
339
|
+
if (!baskets.length) return null;
|
|
340
|
+
const ids = baskets.map((b) => b.consumerId).filter((id) => id != null);
|
|
341
|
+
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
342
|
+
}
|
|
343
|
+
/** Unique product ids referenced by a set of baskets. */
|
|
344
|
+
function collectProductIds(baskets) {
|
|
345
|
+
if (!baskets.length) return null;
|
|
346
|
+
const ids = /* @__PURE__ */ new Set();
|
|
347
|
+
baskets.forEach((basket) => {
|
|
348
|
+
basket.products.forEach((product) => {
|
|
349
|
+
if (product.productId) ids.add(product.productId);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
return ids.size > 0 ? [...ids] : null;
|
|
353
|
+
}
|
|
354
|
+
/** Joins baskets with consumer + product lookups, applies search, sorts. */
|
|
355
|
+
function combineBasketRows({ baskets, consumerMap, productMap, search }) {
|
|
356
|
+
if (!baskets.length) return [];
|
|
357
|
+
let combinedRows = baskets.map((basket) => {
|
|
358
|
+
const consumer = basket.consumerId ? consumerMap[basket.consumerId] : null;
|
|
359
|
+
const productsWithNames = basket.products.map((p) => {
|
|
360
|
+
const product = productMap[p.productId];
|
|
361
|
+
return {
|
|
362
|
+
...p,
|
|
363
|
+
productName: product?.name || `Product #${p.productId}`
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
id: basket.id,
|
|
368
|
+
basketId: basket.id,
|
|
369
|
+
basketName: basket.name || `Basket #${basket.id}`,
|
|
370
|
+
consumerId: basket.consumerId,
|
|
371
|
+
consumerName: consumer ? `${consumer.firstName} ${consumer.lastName}` : "Unknown",
|
|
372
|
+
consumerEmail: consumer?.email || "-",
|
|
373
|
+
products: productsWithNames,
|
|
374
|
+
basketTotal: (basket.total || 0) / 100,
|
|
375
|
+
tax: basket.taxTotal || 0,
|
|
376
|
+
dateTime: basket.completeDate,
|
|
377
|
+
status: basket.status || "pending"
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
if (search) {
|
|
381
|
+
const searchLower = search.toLowerCase();
|
|
382
|
+
combinedRows = combinedRows.filter((row) => row.basketId.toString().includes(searchLower) || row.basketName.toLowerCase().includes(searchLower) || row.consumerName.toLowerCase().includes(searchLower) || row.consumerEmail.toLowerCase().includes(searchLower));
|
|
383
|
+
}
|
|
384
|
+
combinedRows.sort((a, b) => {
|
|
385
|
+
const dateA = new Date(a.dateTime).getTime();
|
|
386
|
+
return new Date(b.dateTime).getTime() - dateA;
|
|
387
|
+
});
|
|
388
|
+
return combinedRows;
|
|
389
|
+
}
|
|
390
|
+
/** Aggregate totals for a set of report rows. */
|
|
391
|
+
function computeBasketSummary(rows) {
|
|
392
|
+
const uniqueConsumerIds = new Set(rows.map((row) => row.consumerId));
|
|
393
|
+
return {
|
|
394
|
+
totalBaskets: rows.length,
|
|
395
|
+
totalRevenue: rows.reduce((sum, row) => sum + row.basketTotal, 0),
|
|
396
|
+
totalTax: rows.reduce((sum, row) => sum + row.tax, 0),
|
|
397
|
+
uniqueConsumers: uniqueConsumerIds.size
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/** Builds a consumer id → consumer lookup. */
|
|
401
|
+
function buildConsumerMap(consumers) {
|
|
402
|
+
const map = {};
|
|
403
|
+
if (consumers) consumers.forEach((consumer) => {
|
|
404
|
+
map[consumer.id] = consumer;
|
|
405
|
+
});
|
|
406
|
+
return map;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/hooks/shared.ts
|
|
411
|
+
function toMessage(e) {
|
|
412
|
+
if (e instanceof Error) return e.message;
|
|
413
|
+
return String(e);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Internal primitive powering the auto-fetch hooks. Runs `fetcher` on mount and
|
|
417
|
+
* whenever `deps` change, tracks loading/error/data, cancels in-flight requests
|
|
418
|
+
* on unmount or dep change, and exposes `refetch`.
|
|
419
|
+
*
|
|
420
|
+
* When `enabled` is false the fetch is skipped (used for hooks that need
|
|
421
|
+
* arguments before they can run, e.g. consumers-by-id).
|
|
422
|
+
*/
|
|
423
|
+
function useQuery(fetcher, deps, enabled = true) {
|
|
424
|
+
const [data, setData] = (0, react.useState)(null);
|
|
425
|
+
const [loading, setLoading] = (0, react.useState)(enabled);
|
|
426
|
+
const [error, setError] = (0, react.useState)(null);
|
|
427
|
+
const [tick, setTick] = (0, react.useState)(0);
|
|
428
|
+
const refetch = (0, react.useCallback)(() => setTick((t) => t + 1), []);
|
|
429
|
+
(0, react.useEffect)(() => {
|
|
430
|
+
if (!enabled) {
|
|
431
|
+
setLoading(false);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const controller = new AbortController();
|
|
435
|
+
let active = true;
|
|
436
|
+
setData(null);
|
|
437
|
+
setError(null);
|
|
438
|
+
setLoading(true);
|
|
439
|
+
fetcher(controller.signal).then((result) => {
|
|
440
|
+
if (active) setData(result);
|
|
441
|
+
}).catch((e) => {
|
|
442
|
+
if (active && e?.name !== "AbortError") setError(toMessage(e));
|
|
443
|
+
}).finally(() => {
|
|
444
|
+
if (active) setLoading(false);
|
|
445
|
+
});
|
|
446
|
+
return () => {
|
|
447
|
+
active = false;
|
|
448
|
+
controller.abort();
|
|
449
|
+
};
|
|
450
|
+
}, [
|
|
451
|
+
...deps,
|
|
452
|
+
tick,
|
|
453
|
+
enabled
|
|
454
|
+
]);
|
|
455
|
+
return {
|
|
456
|
+
data,
|
|
457
|
+
loading,
|
|
458
|
+
error,
|
|
459
|
+
refetch
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Internal primitive powering the manually-triggered hooks.
|
|
464
|
+
*/
|
|
465
|
+
function useMutation(action, deps) {
|
|
466
|
+
const [data, setData] = (0, react.useState)(null);
|
|
467
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
468
|
+
const [error, setError] = (0, react.useState)(null);
|
|
469
|
+
return {
|
|
470
|
+
mutate: (0, react.useCallback)(async (...args) => {
|
|
471
|
+
setLoading(true);
|
|
472
|
+
setError(null);
|
|
473
|
+
try {
|
|
474
|
+
const result = await action(...args);
|
|
475
|
+
setData(result);
|
|
476
|
+
return result;
|
|
477
|
+
} catch (e) {
|
|
478
|
+
setError(toMessage(e));
|
|
479
|
+
return null;
|
|
480
|
+
} finally {
|
|
481
|
+
setLoading(false);
|
|
482
|
+
}
|
|
483
|
+
}, deps),
|
|
484
|
+
data,
|
|
485
|
+
loading,
|
|
486
|
+
error
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/hooks/useCategory.ts
|
|
492
|
+
/**
|
|
493
|
+
* Auto-fetches categories on mount and whenever the params change.
|
|
494
|
+
* Manual reloads are available via `refetch` (replaces the old `reloadFlag`).
|
|
495
|
+
*/
|
|
496
|
+
function useCategory(params = {}, options) {
|
|
497
|
+
return useQuery((signal) => fetchCategories(params, {
|
|
498
|
+
...options,
|
|
499
|
+
signal
|
|
500
|
+
}), [
|
|
501
|
+
JSON.stringify(params),
|
|
502
|
+
options?.baseUrl,
|
|
503
|
+
options?.token
|
|
504
|
+
]);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/hooks/useConsumer.ts
|
|
509
|
+
/**
|
|
510
|
+
* Auto-fetches consumers by id. Disabled (no request) until at least one id is
|
|
511
|
+
* supplied, mirroring the original early-return behaviour.
|
|
512
|
+
*/
|
|
513
|
+
function useConsumer(consumerIds, options) {
|
|
514
|
+
const enabled = !!consumerIds && consumerIds.length > 0;
|
|
515
|
+
return useQuery((signal) => fetchConsumers(consumerIds, {
|
|
516
|
+
...options,
|
|
517
|
+
signal
|
|
518
|
+
}), [
|
|
519
|
+
JSON.stringify(consumerIds),
|
|
520
|
+
options?.baseUrl,
|
|
521
|
+
options?.token
|
|
522
|
+
], enabled);
|
|
523
|
+
}
|
|
524
|
+
var useConsumer_default = useConsumer;
|
|
525
|
+
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/hooks/useEmployee.ts
|
|
528
|
+
/**
|
|
529
|
+
* Auto-fetches employees on mount and whenever the params change.
|
|
530
|
+
*/
|
|
531
|
+
function useEmployee(params = {}, options) {
|
|
532
|
+
return useQuery((signal) => fetchEmployees(params, {
|
|
533
|
+
...options,
|
|
534
|
+
signal
|
|
535
|
+
}), [
|
|
536
|
+
JSON.stringify(params),
|
|
537
|
+
options?.baseUrl,
|
|
538
|
+
options?.token
|
|
539
|
+
]);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/hooks/useProduct.ts
|
|
544
|
+
/**
|
|
545
|
+
* Auto-fetches products on mount and whenever the params change.
|
|
546
|
+
*/
|
|
547
|
+
function useProduct(params = {}, options) {
|
|
548
|
+
return useQuery((signal) => fetchProducts(params, {
|
|
549
|
+
...options,
|
|
550
|
+
signal
|
|
551
|
+
}), [
|
|
552
|
+
JSON.stringify(params),
|
|
553
|
+
options?.baseUrl,
|
|
554
|
+
options?.token
|
|
555
|
+
]);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
//#endregion
|
|
559
|
+
//#region src/hooks/usePromotion.ts
|
|
560
|
+
/**
|
|
561
|
+
* Auto-fetches promotions on mount and whenever the params change.
|
|
562
|
+
* Manual reloads are available via `refetch`.
|
|
563
|
+
*/
|
|
564
|
+
function usePromotion(params = {}, options) {
|
|
565
|
+
return useQuery((signal) => fetchPromotions(params, {
|
|
566
|
+
...options,
|
|
567
|
+
signal
|
|
568
|
+
}), [
|
|
569
|
+
JSON.stringify(params),
|
|
570
|
+
options?.baseUrl,
|
|
571
|
+
options?.token
|
|
572
|
+
]);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/hooks/useStore.ts
|
|
577
|
+
/**
|
|
578
|
+
* Auto-fetches stores on mount. Manual reloads via `refetch`
|
|
579
|
+
* (replaces the old `reloadFlag` argument).
|
|
580
|
+
*/
|
|
581
|
+
function useStore(params = {}, options) {
|
|
582
|
+
return useQuery((signal) => fetchStores(params, {
|
|
583
|
+
...options,
|
|
584
|
+
signal
|
|
585
|
+
}), [
|
|
586
|
+
JSON.stringify(params),
|
|
587
|
+
options?.baseUrl,
|
|
588
|
+
options?.token
|
|
589
|
+
]);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/hooks/useReport.ts
|
|
594
|
+
/**
|
|
595
|
+
* Auto-fetches one or more reports. Disabled (no request) until a non-null
|
|
596
|
+
* query is supplied, mirroring the original early-return behaviour.
|
|
597
|
+
*/
|
|
598
|
+
function useReport(query, options) {
|
|
599
|
+
const enabled = !!query;
|
|
600
|
+
return useQuery((signal) => fetchReport(query, {
|
|
601
|
+
...options,
|
|
602
|
+
signal
|
|
603
|
+
}), [
|
|
604
|
+
JSON.stringify(query),
|
|
605
|
+
options?.baseUrl,
|
|
606
|
+
options?.token
|
|
607
|
+
], enabled);
|
|
608
|
+
}
|
|
609
|
+
var useReport_default = useReport;
|
|
610
|
+
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/hooks/useDelete.ts
|
|
613
|
+
/**
|
|
614
|
+
* Manually-triggered delete. Returns `{ mutate, loading, error, data }`.
|
|
615
|
+
*
|
|
616
|
+
* Note: the original `useHandleDelete` surfaced notistack toasts. That UI
|
|
617
|
+
* concern is left to the host app — handle success/error from the returned
|
|
618
|
+
* state or the promise resolved by `mutate`.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* const { mutate, loading } = useDelete();
|
|
622
|
+
* await mutate("products", 42);
|
|
623
|
+
*/
|
|
624
|
+
function useDelete(options) {
|
|
625
|
+
return useMutation((resourcePath, entityId) => deleteEntity(resourcePath, entityId, options), [options?.baseUrl, options?.token]);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/hooks/useSubmit.ts
|
|
630
|
+
/**
|
|
631
|
+
* Manually-triggered create/update. Returns `{ mutate, loading, error, data }`,
|
|
632
|
+
* where `data` is the API `result` payload.
|
|
633
|
+
*
|
|
634
|
+
* The original `useHandleSubmit` bundled validation, payload transformation and
|
|
635
|
+
* notistack toasts. Those are app concerns: resolve the endpoint/method/payload
|
|
636
|
+
* in your component and pass them in, then react to the returned state.
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* const { mutate, loading } = useSubmit<Product>();
|
|
640
|
+
* const saved = await mutate({
|
|
641
|
+
* endpoint: id ? `/api/products/${id}` : "/api/products",
|
|
642
|
+
* method: id ? "PUT" : "POST",
|
|
643
|
+
* payload,
|
|
644
|
+
* });
|
|
645
|
+
*/
|
|
646
|
+
function useSubmit(options) {
|
|
647
|
+
return useMutation((args) => submitEntity(args, options), [options?.baseUrl, options?.token]);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/hooks/useLogout.ts
|
|
652
|
+
/**
|
|
653
|
+
* Manually-triggered logout (ends the server session).
|
|
654
|
+
*
|
|
655
|
+
* This is framework-agnostic and does NOT redirect. The original `useLogout`
|
|
656
|
+
* also pushed the browser to the auth-server logout URL via `next/navigation`
|
|
657
|
+
* and `'use server'` actions; that part is Next-specific and stays in your app.
|
|
658
|
+
*
|
|
659
|
+
* @example
|
|
660
|
+
* const { mutate: doLogout } = useLogout();
|
|
661
|
+
* await doLogout();
|
|
662
|
+
* router.push(logoutUrl); // app-specific redirect
|
|
663
|
+
*/
|
|
664
|
+
function useLogout(options) {
|
|
665
|
+
return useMutation(() => logout(options), [options?.baseUrl, options?.token]);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/hooks/useBasketReport.ts
|
|
670
|
+
/**
|
|
671
|
+
* Composite hook that orchestrates the report → consumers → products pipeline
|
|
672
|
+
* for the basket report screen. All the data-shaping logic now lives in the
|
|
673
|
+
* framework-agnostic `core/basket` helpers; this hook is the thin React glue
|
|
674
|
+
* (state, memoisation, effects) on top of them.
|
|
675
|
+
*/
|
|
676
|
+
function useBasketReport(options) {
|
|
677
|
+
const [filters, setFiltersState] = (0, react.useState)(DEFAULT_BASKET_FILTERS);
|
|
678
|
+
const [pagination, setPaginationState] = (0, react.useState)(DEFAULT_PAGINATION);
|
|
679
|
+
const [refreshFlag, setRefreshFlag] = (0, react.useState)(0);
|
|
680
|
+
const { data: reports, loading: basketLoading, error: basketError } = useReport_default((0, react.useMemo)(() => buildBasketReportQuery(filters), [filters, refreshFlag]), options);
|
|
681
|
+
const basketData = (0, react.useMemo)(() => parseBasketReportData(reports), [reports]);
|
|
682
|
+
const consumerIds = (0, react.useMemo)(() => collectConsumerIds(basketData), [basketData]);
|
|
683
|
+
const productIds = (0, react.useMemo)(() => collectProductIds(basketData), [basketData]);
|
|
684
|
+
const { data: consumers, loading: consumerLoading, error: consumerError } = useConsumer_default(consumerIds, options);
|
|
685
|
+
const [productMap, setProductMap] = (0, react.useState)({});
|
|
686
|
+
const [productLoading, setProductLoading] = (0, react.useState)(false);
|
|
687
|
+
const [productError, setProductError] = (0, react.useState)(null);
|
|
688
|
+
(0, react.useEffect)(() => {
|
|
689
|
+
if (!productIds || productIds.length === 0) {
|
|
690
|
+
setProductMap({});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
let active = true;
|
|
694
|
+
setProductLoading(true);
|
|
695
|
+
setProductError(null);
|
|
696
|
+
fetchProductsMap(productIds, options).then((map) => {
|
|
697
|
+
if (active) setProductMap(map);
|
|
698
|
+
}).catch((e) => {
|
|
699
|
+
if (active) setProductError(e instanceof Error ? e.message : "Failed to fetch products");
|
|
700
|
+
}).finally(() => {
|
|
701
|
+
if (active) setProductLoading(false);
|
|
702
|
+
});
|
|
703
|
+
return () => {
|
|
704
|
+
active = false;
|
|
705
|
+
};
|
|
706
|
+
}, [
|
|
707
|
+
JSON.stringify(productIds),
|
|
708
|
+
options?.baseUrl,
|
|
709
|
+
options?.token
|
|
710
|
+
]);
|
|
711
|
+
const consumerMap = (0, react.useMemo)(() => buildConsumerMap(consumers), [consumers]);
|
|
712
|
+
const rows = (0, react.useMemo)(() => combineBasketRows({
|
|
713
|
+
baskets: basketData,
|
|
714
|
+
consumerMap,
|
|
715
|
+
productMap,
|
|
716
|
+
search: filters.search
|
|
717
|
+
}), [
|
|
718
|
+
basketData,
|
|
719
|
+
consumerMap,
|
|
720
|
+
productMap,
|
|
721
|
+
filters.search
|
|
722
|
+
]);
|
|
723
|
+
const summary = (0, react.useMemo)(() => computeBasketSummary(rows), [rows]);
|
|
724
|
+
(0, react.useEffect)(() => {
|
|
725
|
+
if (rows.length > 0 && rows.length === pagination.pageSize) setPaginationState((prev) => ({
|
|
726
|
+
...prev,
|
|
727
|
+
total: (pagination.page + 2) * pagination.pageSize
|
|
728
|
+
}));
|
|
729
|
+
else if (rows.length > 0) setPaginationState((prev) => ({
|
|
730
|
+
...prev,
|
|
731
|
+
total: pagination.page * pagination.pageSize + rows.length
|
|
732
|
+
}));
|
|
733
|
+
}, [
|
|
734
|
+
rows.length,
|
|
735
|
+
pagination.page,
|
|
736
|
+
pagination.pageSize
|
|
737
|
+
]);
|
|
738
|
+
const setFilters = (0, react.useCallback)((newFilters) => {
|
|
739
|
+
setFiltersState((prev) => ({
|
|
740
|
+
...prev,
|
|
741
|
+
...newFilters
|
|
742
|
+
}));
|
|
743
|
+
setPaginationState((prev) => ({
|
|
744
|
+
...prev,
|
|
745
|
+
page: 0
|
|
746
|
+
}));
|
|
747
|
+
}, []);
|
|
748
|
+
const setPagination = (0, react.useCallback)((page, pageSize) => {
|
|
749
|
+
setPaginationState((prev) => ({
|
|
750
|
+
...prev,
|
|
751
|
+
page,
|
|
752
|
+
pageSize
|
|
753
|
+
}));
|
|
754
|
+
}, []);
|
|
755
|
+
const refresh = (0, react.useCallback)(() => {
|
|
756
|
+
setRefreshFlag((prev) => prev + 1);
|
|
757
|
+
}, []);
|
|
758
|
+
return {
|
|
759
|
+
rows,
|
|
760
|
+
summary,
|
|
761
|
+
loading: basketLoading || consumerLoading || productLoading,
|
|
762
|
+
error: basketError || consumerError || productError,
|
|
763
|
+
filters,
|
|
764
|
+
setFilters,
|
|
765
|
+
pagination,
|
|
766
|
+
setPagination,
|
|
767
|
+
refresh
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
//#endregion
|
|
772
|
+
exports.useBasketReport = useBasketReport;
|
|
773
|
+
exports.useCategory = useCategory;
|
|
774
|
+
exports.useConsumer = useConsumer;
|
|
775
|
+
exports.useDelete = useDelete;
|
|
776
|
+
exports.useEmployee = useEmployee;
|
|
777
|
+
exports.useLogout = useLogout;
|
|
778
|
+
exports.useProduct = useProduct;
|
|
779
|
+
exports.usePromotion = usePromotion;
|
|
780
|
+
exports.useReport = useReport;
|
|
781
|
+
exports.useStore = useStore;
|
|
782
|
+
exports.useSubmit = useSubmit;
|
|
783
|
+
//# sourceMappingURL=index.cjs.map
|