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