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