@teincfood/core 0.7.9 → 0.8.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/README.md +26 -21
- package/dist/adapters/auth.d.ts +8 -0
- package/dist/adapters/auth.js +36 -0
- package/dist/adapters/devices.d.ts +3 -1
- package/dist/adapters/devices.js +187 -3
- package/dist/adapters/filesystem.d.ts +4 -0
- package/dist/adapters/kv.d.ts +10 -0
- package/dist/adapters/kv.js +23 -7
- package/dist/adapters/local-node.d.ts +4 -6
- package/dist/adapters/local-node.js +9 -32
- package/dist/adapters/notify.d.ts +12 -0
- package/dist/adapters/notify.js +33 -0
- package/dist/adapters/query-keys.d.ts +4 -0
- package/dist/adapters/query-keys.js +4 -0
- package/dist/adapters/services.d.ts +96 -15
- package/dist/adapters/services.js +241 -4
- package/dist/auth/session.d.ts +66 -0
- package/dist/auth/session.js +172 -0
- package/dist/catalog/hooks.d.ts +10 -0
- package/dist/catalog/hooks.js +82 -11
- package/dist/db/migrations.js +274 -0
- package/dist/db/operations.d.ts +53 -2
- package/dist/db/operations.js +177 -8
- package/dist/domain/business/index.d.ts +5 -0
- package/dist/domain/business/index.js +5 -0
- package/dist/domain/orders/lifecycle.d.ts +6 -0
- package/dist/domain/orders/lifecycle.js +6 -0
- package/dist/domain/orders/service.d.ts +24 -0
- package/dist/domain/orders/service.js +242 -0
- package/dist/domain/payments/index.d.ts +6 -0
- package/dist/domain/payments/index.js +5 -0
- package/dist/domain/reference/index.d.ts +8 -0
- package/dist/domain/reference/index.js +8 -0
- package/dist/e2e/harness.d.ts +1 -0
- package/dist/e2e/harness.js +61 -0
- package/dist/hooks/analytics.hooks.d.ts +25 -0
- package/dist/hooks/analytics.hooks.js +214 -0
- package/dist/hooks/business-hours.hooks.d.ts +8 -0
- package/dist/hooks/business-hours.hooks.js +45 -0
- package/dist/hooks/dashboard.hooks.d.ts +30 -0
- package/dist/hooks/dashboard.hooks.js +132 -0
- package/dist/hooks/delivery.hooks.d.ts +225 -0
- package/dist/hooks/delivery.hooks.js +80 -0
- package/dist/hooks/fleet.hooks.d.ts +37 -0
- package/dist/hooks/fleet.hooks.js +229 -0
- package/dist/hooks/image.hooks.d.ts +24 -0
- package/dist/hooks/image.hooks.js +66 -0
- package/dist/hooks/kiosk.hooks.d.ts +22 -5
- package/dist/hooks/kiosk.hooks.js +74 -5
- package/dist/hooks/members.hooks.d.ts +48 -0
- package/dist/hooks/members.hooks.js +302 -0
- package/dist/hooks/offline-orders.hooks.d.ts +9 -0
- package/dist/hooks/offline-orders.hooks.js +51 -0
- package/dist/hooks/orders.hooks.d.ts +18 -39
- package/dist/hooks/orders.hooks.js +124 -343
- package/dist/hooks/places.hooks.d.ts +21 -0
- package/dist/hooks/places.hooks.js +112 -0
- package/dist/hooks/realtime-stubs.d.ts +28 -0
- package/dist/hooks/realtime-stubs.js +85 -0
- package/dist/hooks/rider.hooks.d.ts +470 -0
- package/dist/hooks/rider.hooks.js +373 -0
- package/dist/hooks/use-infinite-list.d.ts +243 -0
- package/dist/hooks/use-infinite-list.js +58 -0
- package/dist/hooks/wallet.hooks.d.ts +219 -0
- package/dist/hooks/wallet.hooks.js +56 -0
- package/dist/image-cache/service.d.ts +28 -0
- package/dist/image-cache/service.js +51 -3
- package/dist/index.d.ts +56 -6
- package/dist/index.js +73 -6
- package/dist/local-node/adapters.d.ts +87 -0
- package/dist/local-node/adapters.js +4 -0
- package/dist/local-node/index.d.ts +5 -0
- package/dist/local-node/index.js +5 -0
- package/dist/local-node/security.d.ts +25 -0
- package/dist/local-node/security.js +149 -0
- package/dist/local-node/service.d.ts +83 -0
- package/dist/local-node/service.js +706 -0
- package/dist/local-node/store.d.ts +50 -0
- package/dist/local-node/store.js +284 -0
- package/dist/local-node/types.d.ts +92 -0
- package/dist/local-node/types.js +4 -0
- package/dist/printer/receipt.d.ts +4 -0
- package/dist/printer/receipt.js +75 -0
- package/dist/printer/service.js +1 -1
- package/dist/providers/query-client.d.ts +13 -0
- package/dist/providers/query-client.js +58 -0
- package/dist/realtime/phoenix-socket.js +30 -1
- package/dist/realtime/useEventsConnection.js +11 -2
- package/dist/reference/api.d.ts +2 -0
- package/dist/reference/api.js +2 -0
- package/dist/reference/capabilities.d.ts +12 -7
- package/dist/reference/capabilities.js +27 -13
- package/dist/reference/hooks.d.ts +34 -0
- package/dist/reference/hooks.js +124 -0
- package/dist/reference/menu-apply.d.ts +29 -0
- package/dist/reference/menu-apply.js +289 -0
- package/dist/reference/menu-record.d.ts +69 -0
- package/dist/reference/menu-record.js +168 -0
- package/dist/reference/operations.d.ts +3 -0
- package/dist/reference/operations.js +35 -1
- package/dist/reference/service.js +36 -9
- package/dist/reference/types.d.ts +1 -1
- package/dist/repositories/order.repository.d.ts +7 -0
- package/dist/repositories/order.repository.js +15 -2
- package/dist/repositories/pos.repository.d.ts +4 -0
- package/dist/repositories/pos.repository.js +4 -0
- package/dist/sync/backoff.d.ts +21 -0
- package/dist/sync/backoff.js +42 -0
- package/dist/sync/command-builder.js +10 -2
- package/dist/sync/command-queue.service.d.ts +12 -0
- package/dist/sync/command-queue.service.js +81 -16
- package/dist/sync/device-lease.service.d.ts +3 -3
- package/dist/sync/device-lease.service.js +80 -20
- package/dist/sync/engine.d.ts +3 -1
- package/dist/sync/engine.js +44 -4
- package/dist/sync/full-sync.d.ts +6 -0
- package/dist/sync/full-sync.js +72 -0
- package/dist/sync/health-probe.d.ts +24 -0
- package/dist/sync/health-probe.js +138 -0
- package/dist/sync/offline-capable.js +25 -0
- package/dist/sync/online-gates.d.ts +17 -0
- package/dist/sync/online-gates.js +55 -0
- package/dist/sync/order-number.d.ts +4 -4
- package/dist/sync/order-number.js +33 -13
- package/dist/sync/order-snapshots.service.js +18 -3
- package/dist/sync/pending-orders.service.d.ts +18 -0
- package/dist/sync/pending-orders.service.js +67 -1
- package/dist/sync/reconciliation/index.d.ts +5 -0
- package/dist/sync/reconciliation/index.js +5 -0
- package/dist/sync/snapshots/entity.d.ts +6 -0
- package/dist/sync/snapshots/entity.js +6 -0
- package/dist/sync/sync-jobs.service.d.ts +18 -0
- package/dist/sync/sync-jobs.service.js +539 -0
- package/dist/sync/transition-endpoints.d.ts +11 -0
- package/dist/sync/transition-endpoints.js +30 -0
- package/dist/sync/transport-local-node.js +3 -2
- package/dist/sync/transport-rest.d.ts +7 -0
- package/dist/sync/transport-rest.js +84 -26
- package/dist/sync/use-sync-reconciliation.js +15 -1
- package/dist/test-helpers/memory-driver.js +182 -4
- package/dist/test-helpers/seed-hours.d.ts +1 -0
- package/dist/test-helpers/seed-hours.js +23 -0
- package/dist/types/api/business.api.d.ts +42 -0
- package/dist/types/api/menu.api.d.ts +15 -0
- package/dist/types/api/seller.api.d.ts +20 -0
- package/dist/types/places.d.ts +19 -0
- package/dist/types/places.js +2 -0
- package/dist/types/rider.d.ts +67 -0
- package/dist/types/rider.js +2 -0
- package/dist/utils/attempt-stats.d.ts +23 -0
- package/dist/utils/attempt-stats.js +61 -0
- package/dist/utils/business-hours.d.ts +37 -0
- package/dist/utils/business-hours.js +96 -0
- package/dist/utils/debug-log.d.ts +23 -0
- package/dist/utils/debug-log.js +153 -0
- package/dist/utils/order-filter.d.ts +1 -1
- package/dist/utils/order-filter.js +1 -1
- package/dist/utils/order-transitions.d.ts +30 -0
- package/dist/utils/order-transitions.js +185 -0
- package/dist/utils/sync-logger.js +5 -4
- package/dist/validation/offline.d.ts +46 -0
- package/dist/validation/offline.js +79 -0
- package/package.json +6 -1
- package/dist/pricing/engine.test.d.ts +0 -1
- package/dist/pricing/engine.test.js +0 -142
- package/dist/reference/lww.test.d.ts +0 -1
- package/dist/reference/lww.test.js +0 -23
- package/dist/reference/operations.test.d.ts +0 -1
- package/dist/reference/operations.test.js +0 -66
- package/dist/standalone.integration.test.d.ts +0 -1
- package/dist/standalone.integration.test.js +0 -235
- package/dist/sync/device-lease.test.d.ts +0 -1
- package/dist/sync/device-lease.test.js +0 -38
- package/dist/sync/offline-integration.test.d.ts +0 -1
- package/dist/sync/offline-integration.test.js +0 -52
- package/dist/sync/order-number.test.d.ts +0 -1
- package/dist/sync/order-number.test.js +0 -51
- package/dist/sync/perms.test.d.ts +0 -1
- package/dist/sync/perms.test.js +0 -15
- package/dist/utils/currency.test.d.ts +0 -1
- package/dist/utils/currency.test.js +0 -17
- package/dist/utils/hlc.test.d.ts +0 -1
- package/dist/utils/hlc.test.js +0 -29
- package/dist/utils/phone.test.d.ts +0 -1
- package/dist/utils/phone.test.js +0 -19
- package/dist/utils/responsive.d.ts +0 -13
- package/dist/utils/responsive.js +0 -18
- package/dist/utils/uuid.test.d.ts +0 -1
- package/dist/utils/uuid.test.js +0 -13
- package/dist/utils/validation.test.d.ts +0 -1
- package/dist/utils/validation.test.js +0 -23
|
@@ -1,340 +1,149 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Order
|
|
3
|
-
*
|
|
4
|
-
* Device owns its partition: when offline it serves the durable
|
|
5
|
-
* entity_snapshots + pending overlay immediately; the SyncEngine replays
|
|
6
|
-
* queued commands when back online. No hook ever blocks on a cloud timeout
|
|
7
|
-
* when offline.
|
|
8
|
-
*
|
|
9
|
-
* Host apps just re-export from "@teincfood/core" — no app-local duplication.
|
|
2
|
+
* Order hooks — SQLite is real DB (no pending overlay, no fetchAll).
|
|
3
|
+
* Apps just import from "@teincfood/core".
|
|
10
4
|
*/
|
|
11
|
-
import { useEffect
|
|
5
|
+
import { useEffect } from "react";
|
|
12
6
|
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
|
|
8
|
+
import { useSyncExternalStore } from "react";
|
|
9
|
+
import { listLocalOrders, getLocalOrder } from "../domain/orders/service";
|
|
10
|
+
import { getDeviceNumber, ensureLease } from "../sync/device-lease.service";
|
|
11
|
+
import { assertOnlineFor } from "../sync/online-gates";
|
|
12
|
+
import { SELLER_DASHBOARD_KEYS } from "../adapters/query-keys";
|
|
13
|
+
import { openSyncDatabase } from "../db/connection";
|
|
14
|
+
import { getReferenceData } from "../reference/operations";
|
|
15
|
+
import { calculatePricing } from "../pricing/engine";
|
|
16
|
+
import { generateUUID } from "../utils/uuid";
|
|
17
|
+
import { subscribePendingOrders, getPendingOrdersSnapshot } from "../adapters/pending-orders";
|
|
18
|
+
import { hydratePendingOrders, mergePendingIntoList } from "../sync/pending-orders.service";
|
|
13
19
|
import { posRepository } from "../repositories/pos.repository";
|
|
14
20
|
import { orderRepository } from "../repositories/order.repository";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
18
|
-
import { getPendingOrdersSnapshot, subscribePendingOrders, } from "../adapters/pending-orders";
|
|
19
|
-
import { orderSnapshotsService } from "../sync/order-snapshots.service";
|
|
20
|
-
import { hydratePendingOrders } from "../sync/pending-orders.service";
|
|
21
|
-
import { isLocalOrderNumber } from "../sync/order-number";
|
|
22
|
-
import { filterOrdersOffline, paginateOffline } from "../utils/order-filter";
|
|
23
|
-
// ─── Keys (host apps should import from core, not define their own) ─────
|
|
24
|
-
export const POS_KEYS = {
|
|
25
|
-
all: ["pos"],
|
|
26
|
-
orders: (businessId) => ["pos", "orders", businessId],
|
|
27
|
-
preview: (businessId) => ["pos", "preview", businessId],
|
|
28
|
-
};
|
|
29
|
-
export const SELLER_KEYS = {
|
|
30
|
-
all: ["seller"],
|
|
31
|
-
orders: (businessId, filters) => [...SELLER_KEYS.all, "orders", businessId, filters].filter((x) => x !== undefined),
|
|
32
|
-
deliveries: (businessId, filters) => [...SELLER_KEYS.all, "deliveries", businessId, filters].filter((x) => x !== undefined),
|
|
33
|
-
order: (id, businessId) => [...SELLER_KEYS.all, "order", id, businessId],
|
|
34
|
-
};
|
|
35
|
-
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
21
|
+
export const POS_KEYS = { all: ["pos"], orders: (bid) => ["pos", "orders", bid], preview: (bid) => ["pos", "preview", bid] };
|
|
22
|
+
export const SELLER_KEYS = { all: ["seller"], orders: (bid, f) => [...SELLER_KEYS.all, "orders", bid, f].filter((x) => x !== undefined), deliveries: (bid, f) => [...SELLER_KEYS.all, "deliveries", bid, f].filter((x) => x !== undefined), order: (id, bid) => [...SELLER_KEYS.all, "order", id, bid] };
|
|
36
23
|
function useBusinessId() {
|
|
37
24
|
const id = useSyncExternalStore(subscribeBusiness, () => getActiveBusiness()?.id ?? null, () => null);
|
|
38
25
|
return id ?? undefined;
|
|
39
26
|
}
|
|
40
|
-
function useConnectivity() {
|
|
41
|
-
const state = useSyncExternalStore(subscribeConnectivity, getConnectivityState, getConnectivityState);
|
|
42
|
-
return state;
|
|
43
|
-
}
|
|
44
|
-
function useCorePendingOrders(businessId) {
|
|
45
|
-
const snap = useSyncExternalStore(subscribePendingOrders, getPendingOrdersSnapshot, getPendingOrdersSnapshot);
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (snap.hydrated)
|
|
48
|
-
return;
|
|
49
|
-
hydratePendingOrders().catch(() => { });
|
|
50
|
-
}, [snap.hydrated]);
|
|
51
|
-
return useMemo(() => (businessId ? snap.orders.filter((o) => o.business_id === businessId) : snap.orders), [snap.orders, businessId]);
|
|
52
|
-
}
|
|
53
|
-
function mergePendingOrders(pending, serverOrders) {
|
|
54
|
-
if (pending.length === 0)
|
|
55
|
-
return serverOrders ?? [];
|
|
56
|
-
const seen = new Set();
|
|
57
|
-
const merged = [];
|
|
58
|
-
for (const order of pending) {
|
|
59
|
-
seen.add(order.order_number);
|
|
60
|
-
merged.push(order);
|
|
61
|
-
}
|
|
62
|
-
for (const order of serverOrders ?? []) {
|
|
63
|
-
if (!seen.has(order.order_number))
|
|
64
|
-
merged.push(order);
|
|
65
|
-
}
|
|
66
|
-
return merged.sort((a, b) => (b.inserted_at ?? "").localeCompare(a.inserted_at ?? ""));
|
|
67
|
-
}
|
|
68
|
-
function isOfflineState(s) {
|
|
69
|
-
return !s.isOnline || s.isCloudReachable === "unreachable";
|
|
70
|
-
}
|
|
71
|
-
// ─── POS Orders Query (offline-first) ─────────────────────────────────────
|
|
72
27
|
export function usePOSOrdersQuery(filters, enabled = true) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const offline = isOfflineState(connectivity);
|
|
77
|
-
const query = useInfiniteQuery({
|
|
78
|
-
queryKey: [...POS_KEYS.orders(businessId), filters],
|
|
28
|
+
const bid = useBusinessId();
|
|
29
|
+
return useInfiniteQuery({
|
|
30
|
+
queryKey: [...POS_KEYS.orders(bid), filters],
|
|
79
31
|
queryFn: async ({ pageParam = 1 }) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
params.date_from = filters.date_from;
|
|
89
|
-
if (filters?.date_to)
|
|
90
|
-
params.date_to = filters.date_to;
|
|
91
|
-
if (filters?.q)
|
|
92
|
-
params.q = filters.q;
|
|
93
|
-
if (filters?.search)
|
|
94
|
-
params.search = filters.search;
|
|
95
|
-
if (offline) {
|
|
96
|
-
const offlineList = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
|
|
97
|
-
const base = offlineList ?? [];
|
|
98
|
-
const filtered = filterOrdersOffline(base, filters);
|
|
99
|
-
// date_from/to are handled inside filterSellerOrders via inserted_at; apply if present
|
|
100
|
-
const paged = paginateOffline(filtered, pageParam, 20);
|
|
101
|
-
return paged;
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
const result = (await posRepository.fetchOrders(businessId, params));
|
|
105
|
-
if (pageParam === 1 && businessId && Array.isArray(result.data)) {
|
|
106
|
-
orderSnapshotsService.persistSellerOrders(businessId, businessId, result.data).catch(() => { });
|
|
107
|
-
}
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
catch (e) {
|
|
111
|
-
const offlineList = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
|
|
112
|
-
if (offlineList) {
|
|
113
|
-
const filtered = filterOrdersOffline(offlineList, filters);
|
|
114
|
-
const paged = paginateOffline(filtered, pageParam, 20);
|
|
115
|
-
return paged;
|
|
116
|
-
}
|
|
117
|
-
throw e;
|
|
118
|
-
}
|
|
32
|
+
if (!bid)
|
|
33
|
+
return { data: [], pagination: { page: 1, page_size: 20, total_pages: 1, total_entries: 0 } };
|
|
34
|
+
const { data, total } = await listLocalOrders(bid, filters, pageParam, 20);
|
|
35
|
+
// Offline overlay: pending creations + queued statuses (page 1). Refetch
|
|
36
|
+
// is driven by the pending-orders invalidator on every apply path.
|
|
37
|
+
const merged = await mergePendingIntoList(data, bid, pageParam);
|
|
38
|
+
const totalPages = Math.max(1, Math.ceil(total / 20));
|
|
39
|
+
return { data: merged, pagination: { page: pageParam, page_size: 20, total_pages: totalPages, total_entries: total + (merged.length - data.length) } };
|
|
119
40
|
},
|
|
120
41
|
initialPageParam: 1,
|
|
121
42
|
getNextPageParam: (lastPage) => {
|
|
122
43
|
const p = lastPage.pagination;
|
|
123
|
-
return p
|
|
44
|
+
return p.page < p.total_pages ? p.page + 1 : undefined;
|
|
124
45
|
},
|
|
125
|
-
enabled: enabled && !!
|
|
126
|
-
staleTime:
|
|
127
|
-
retry: false,
|
|
46
|
+
enabled: enabled && !!bid,
|
|
47
|
+
staleTime: 5 * 1000,
|
|
128
48
|
networkMode: "offlineFirst",
|
|
129
49
|
select: (data) => data.pages.flatMap((p) => p.data),
|
|
130
50
|
});
|
|
131
|
-
const data = useMemo(() => mergePendingOrders(pendingOrders, query.data), [pendingOrders, query.data]);
|
|
132
|
-
return { ...query, data };
|
|
133
51
|
}
|
|
134
|
-
// ─── Seller Orders Query (offline-first, shares same snapshot) ────────────
|
|
135
52
|
export function useSellerOrdersQuery(enabled = true, filters) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const offline = isOfflineState(connectivity);
|
|
140
|
-
const query = useInfiniteQuery({
|
|
141
|
-
queryKey: SELLER_KEYS.orders(businessId, filters),
|
|
53
|
+
const bid = useBusinessId();
|
|
54
|
+
return useInfiniteQuery({
|
|
55
|
+
queryKey: SELLER_KEYS.orders(bid, filters),
|
|
142
56
|
queryFn: async ({ pageParam = 1 }) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (filters?.date_from)
|
|
151
|
-
params.date_from = filters.date_from;
|
|
152
|
-
if (filters?.date_to)
|
|
153
|
-
params.date_to = filters.date_to;
|
|
154
|
-
if (offline) {
|
|
155
|
-
const offlineList = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
|
|
156
|
-
const base = offlineList ?? [];
|
|
157
|
-
const filtered = filterOrdersOffline(base, filters);
|
|
158
|
-
const paged = paginateOffline(filtered, pageParam, 20);
|
|
159
|
-
return paged;
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
const result = (await sellerService.fetchBusinessOrders(businessId, params));
|
|
163
|
-
if (pageParam === 1 && businessId && Array.isArray(result.data)) {
|
|
164
|
-
orderSnapshotsService.persistSellerOrders(businessId, businessId, result.data).catch(() => { });
|
|
165
|
-
}
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
|
-
catch (e) {
|
|
169
|
-
const offlineList = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
|
|
170
|
-
if (offlineList) {
|
|
171
|
-
const filtered = filterOrdersOffline(offlineList, filters);
|
|
172
|
-
const paged = paginateOffline(filtered, pageParam, 20);
|
|
173
|
-
return paged;
|
|
174
|
-
}
|
|
175
|
-
throw e;
|
|
176
|
-
}
|
|
57
|
+
if (!bid)
|
|
58
|
+
return { data: [], pagination: { page: 1, page_size: 20, total_pages: 1, total_entries: 0 } };
|
|
59
|
+
const q = filters?.q ?? filters?.search;
|
|
60
|
+
const { data, total } = await listLocalOrders(bid, { status: filters?.status, q }, pageParam, 20);
|
|
61
|
+
const merged = await mergePendingIntoList(data, bid, pageParam);
|
|
62
|
+
const totalPages = Math.max(1, Math.ceil(total / 20));
|
|
63
|
+
return { data: merged, pagination: { page: pageParam, page_size: 20, total_pages: totalPages, total_entries: total + (merged.length - data.length) } };
|
|
177
64
|
},
|
|
178
65
|
initialPageParam: 1,
|
|
179
66
|
getNextPageParam: (lastPage) => {
|
|
180
67
|
const p = lastPage.pagination;
|
|
181
|
-
return p
|
|
68
|
+
return p.page < p.total_pages ? p.page + 1 : undefined;
|
|
182
69
|
},
|
|
183
|
-
enabled: enabled && !!
|
|
184
|
-
staleTime:
|
|
185
|
-
retry: false,
|
|
70
|
+
enabled: enabled && !!bid,
|
|
71
|
+
staleTime: 5 * 1000,
|
|
186
72
|
networkMode: "offlineFirst",
|
|
187
73
|
select: (data) => data.pages.flatMap((p) => p.data),
|
|
188
74
|
});
|
|
189
|
-
const data = useMemo(() => mergePendingOrders(pendingOrders, query.data), [pendingOrders, query.data]);
|
|
190
|
-
return { ...query, data };
|
|
191
75
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return
|
|
76
|
+
export function useSellerOrderQuery(orderId, enabled = true) {
|
|
77
|
+
const bid = useBusinessId();
|
|
78
|
+
return useQuery({
|
|
79
|
+
queryKey: SELLER_KEYS.order(orderId, bid),
|
|
80
|
+
queryFn: async () => {
|
|
81
|
+
if (!bid)
|
|
82
|
+
throw new Error("No business");
|
|
83
|
+
const order = await getLocalOrder(bid, orderId);
|
|
84
|
+
if (!order)
|
|
85
|
+
throw new Error("Order not found");
|
|
86
|
+
return { data: order };
|
|
87
|
+
},
|
|
88
|
+
enabled: enabled && !!orderId && !!bid,
|
|
89
|
+
staleTime: 5 * 1000,
|
|
90
|
+
});
|
|
195
91
|
}
|
|
196
|
-
// ─── Create POS Order (device-lease aware, via SyncEngine) ────────────────
|
|
197
92
|
export function useCreatePOSOrderMutation() {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
93
|
+
const bid = useBusinessId();
|
|
94
|
+
const qc = useQueryClient();
|
|
200
95
|
return useMutation({
|
|
201
96
|
mutationFn: async (data) => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const leased = await m.ensureDeviceLease?.(businessId);
|
|
210
|
-
if (!leased && (!isOnline || isCloudReachable === "unreachable")) {
|
|
211
|
-
throw new Error("No device number leased (99 devices max) — cannot create offline orders");
|
|
212
|
-
}
|
|
213
|
-
}
|
|
97
|
+
if (!bid)
|
|
98
|
+
throw new Error("No business");
|
|
99
|
+
const dn = getDeviceNumber(bid);
|
|
100
|
+
if (!dn) {
|
|
101
|
+
const leased = await ensureLease(bid);
|
|
102
|
+
if (!leased)
|
|
103
|
+
throw new Error("No device number leased — connect online once");
|
|
214
104
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
return posRepository.createOrder(businessId, data);
|
|
220
|
-
},
|
|
221
|
-
onSuccess: (result) => {
|
|
222
|
-
const r = result;
|
|
223
|
-
const orderNumber = r?.order_number ?? "";
|
|
224
|
-
const queued = orderNumber ? isLocalOrderNumber(orderNumber) && r?.status === "pending" : false;
|
|
225
|
-
queryClient.invalidateQueries({ queryKey: POS_KEYS.orders(businessId) });
|
|
226
|
-
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.orders(businessId) });
|
|
227
|
-
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
228
|
-
// Toast is host concern — core just invalidates; host can show queued vs created.
|
|
229
|
-
void queued;
|
|
105
|
+
// Engine route: cloud → LAN node → queue. Never createLocalOrder
|
|
106
|
+
// directly — it bypasses sync and peers never see the order.
|
|
107
|
+
return orderRepository.createOrder({ businessId: bid, data, entityId: generateUUID() });
|
|
230
108
|
},
|
|
109
|
+
onSuccess: () => { qc.invalidateQueries({ queryKey: POS_KEYS.orders(bid) }); qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); },
|
|
231
110
|
});
|
|
232
111
|
}
|
|
233
|
-
|
|
234
|
-
function cartHash(items) {
|
|
235
|
-
if (items.length === 0)
|
|
236
|
-
return "";
|
|
237
|
-
return items
|
|
238
|
-
.map((i) => `${i.menu_item.id}:${i.quantity}:${i.selected_variant?.id || ""}:${(i.selected_addons || []).map((a) => a.addon_id).sort().join(",")}`)
|
|
239
|
-
.join("|");
|
|
240
|
-
}
|
|
112
|
+
function cartHash(items) { return items.map((i) => `${i.menu_item.id}:${i.quantity}:${i.selected_variant?.id || ""}:${(i.selected_addons || []).map((a) => a.addon_id).sort().join(",")}`).join("|"); }
|
|
241
113
|
export function usePOSOrderPreviewQuery(items, orderType, customerName) {
|
|
242
|
-
const
|
|
243
|
-
const hash =
|
|
114
|
+
const bid = useBusinessId();
|
|
115
|
+
const hash = cartHash(items);
|
|
244
116
|
return useQuery({
|
|
245
|
-
queryKey: [...POS_KEYS.preview(
|
|
117
|
+
queryKey: [...POS_KEYS.preview(bid), hash, orderType, customerName],
|
|
246
118
|
queryFn: async () => {
|
|
247
|
-
const orderItems = items.map((i) => ({
|
|
248
|
-
menu_item_id: i.menu_item.id,
|
|
249
|
-
quantity: i.quantity,
|
|
250
|
-
variant_id: i.selected_variant?.id,
|
|
251
|
-
addon_ids: i.selected_addons.length > 0 ? i.selected_addons.map((a) => a.addon_id) : undefined,
|
|
252
|
-
}));
|
|
119
|
+
const orderItems = items.map((i) => ({ menu_item_id: i.menu_item.id, quantity: i.quantity, variant_id: i.selected_variant?.id, addon_ids: i.selected_addons.length > 0 ? i.selected_addons.map((a) => a.addon_id) : undefined }));
|
|
253
120
|
try {
|
|
254
|
-
return await posRepository.fetchPreview({ businessId:
|
|
121
|
+
return await posRepository.fetchPreview({ businessId: bid, items: orderItems, orderType, customerName: customerName || undefined });
|
|
255
122
|
}
|
|
256
|
-
catch
|
|
257
|
-
const s = getConnectivityState();
|
|
258
|
-
const isOffline = !s.isOnline || s.isCloudReachable === "unreachable";
|
|
259
|
-
if (!isOffline)
|
|
260
|
-
throw e;
|
|
261
|
-
const { openSyncDatabase } = await import("../db/connection");
|
|
262
|
-
const { getReferenceData } = await import("../reference/operations");
|
|
263
|
-
const { calculatePricing } = await import("../pricing/engine");
|
|
123
|
+
catch {
|
|
264
124
|
const db = await openSyncDatabase();
|
|
265
|
-
const taxRec = await getReferenceData(db,
|
|
125
|
+
const taxRec = await getReferenceData(db, bid, "tax-rules");
|
|
266
126
|
const taxRules = taxRec?.payload?.data ?? [];
|
|
267
127
|
const pricing = calculatePricing({ items: items, taxRules: taxRules });
|
|
268
128
|
return { data: { subtotal_minor: pricing.subtotal_minor, discount_minor: pricing.discount_minor, delivery_fee_minor: pricing.delivery_fee_minor, tax_minor: pricing.tax_minor, tip_minor: pricing.tip_minor, total_minor: pricing.total_minor, tax_lines: pricing.tax_lines, currency: pricing.currency_code } };
|
|
269
129
|
}
|
|
270
130
|
},
|
|
271
|
-
enabled: !!
|
|
131
|
+
enabled: !!bid && items.length > 0,
|
|
272
132
|
staleTime: 30 * 1000,
|
|
273
133
|
});
|
|
274
134
|
}
|
|
275
|
-
// ─── Seller order detail (offline via snapshot fallback) ───────────────────
|
|
276
|
-
export function useSellerOrderQuery(orderId, enabled = true) {
|
|
277
|
-
const businessId = useBusinessId();
|
|
278
|
-
const connectivity = useConnectivity();
|
|
279
|
-
const offline = isOfflineState(connectivity);
|
|
280
|
-
const pendingOrders = useCorePendingOrders(businessId);
|
|
281
|
-
return useQuery({
|
|
282
|
-
queryKey: SELLER_KEYS.order(orderId, businessId),
|
|
283
|
-
queryFn: async () => {
|
|
284
|
-
const pendingHit = pendingOrders.find((o) => o.id === orderId || o.order_number === orderId);
|
|
285
|
-
if (pendingHit)
|
|
286
|
-
return { data: pendingHit };
|
|
287
|
-
if (offline) {
|
|
288
|
-
const list = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
|
|
289
|
-
const found = list?.find((o) => o.id === orderId || o.order_number === orderId);
|
|
290
|
-
if (found)
|
|
291
|
-
return { data: found };
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
const res = await sellerService.fetchSellerOrder(orderId);
|
|
295
|
-
return res;
|
|
296
|
-
}
|
|
297
|
-
catch (e) {
|
|
298
|
-
const list = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
|
|
299
|
-
const found = list?.find((o) => o.id === orderId || o.order_number === orderId);
|
|
300
|
-
if (found)
|
|
301
|
-
return { data: found };
|
|
302
|
-
const pendingAgain = getPendingOrdersSnapshot().orders.find((o) => o.id === orderId || o.order_number === orderId);
|
|
303
|
-
if (pendingAgain)
|
|
304
|
-
return { data: pendingAgain };
|
|
305
|
-
throw e;
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
enabled: enabled && !!orderId && !!businessId,
|
|
309
|
-
staleTime: 30 * 1000,
|
|
310
|
-
retry: false,
|
|
311
|
-
networkMode: "offlineFirst",
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
// ─── Helpers for queued UI ───────────────────────────────────────────────
|
|
315
|
-
export function isQueuedTransition(result) {
|
|
316
|
-
const r = result;
|
|
317
|
-
if (!r)
|
|
318
|
-
return false;
|
|
319
|
-
return r.status === "pending" && !!r.command_id && !r.id;
|
|
320
|
-
}
|
|
321
|
-
// ─── Order status mutations via SyncEngine (all go through outbox) ────────
|
|
322
135
|
function makeOrderActionMutation(build) {
|
|
323
136
|
return function useOrderActionMutation() {
|
|
324
|
-
const
|
|
325
|
-
const
|
|
137
|
+
const bid = useBusinessId();
|
|
138
|
+
const qc = useQueryClient();
|
|
326
139
|
return useMutation({
|
|
327
140
|
mutationFn: (orderId) => {
|
|
328
|
-
if (!
|
|
141
|
+
if (!bid)
|
|
329
142
|
throw new Error("No active business");
|
|
330
|
-
return build({ orderId, businessId });
|
|
331
|
-
},
|
|
332
|
-
onSettled: (_d, _e, orderId) => {
|
|
333
|
-
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.orders() });
|
|
334
|
-
queryClient.invalidateQueries({ queryKey: POS_KEYS.orders(businessId) });
|
|
335
|
-
if (orderId)
|
|
336
|
-
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.order(orderId) });
|
|
143
|
+
return build({ orderId, businessId: bid });
|
|
337
144
|
},
|
|
145
|
+
onSuccess: (_d, _e, orderId) => { qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: POS_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); if (orderId)
|
|
146
|
+
qc.invalidateQueries({ queryKey: SELLER_KEYS.order(orderId) }); },
|
|
338
147
|
});
|
|
339
148
|
};
|
|
340
149
|
}
|
|
@@ -345,73 +154,45 @@ export const useMarkOrderReadyMutation = makeOrderActionMutation(({ orderId, bus
|
|
|
345
154
|
export const useCompletePickupMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.completePickup({ orderId, businessId }));
|
|
346
155
|
export const useCancelOrderMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.cancelOrder({ orderId, businessId }));
|
|
347
156
|
export function useConfirmHandoverMutation() {
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
return useMutation({
|
|
351
|
-
|
|
352
|
-
if (!businessId)
|
|
353
|
-
throw new Error("No active business");
|
|
354
|
-
return orderRepository.confirmHandover({ deliveryId, businessId });
|
|
355
|
-
},
|
|
356
|
-
onSettled: (_d, _e, deliveryId) => {
|
|
357
|
-
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
358
|
-
queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
|
|
359
|
-
if (deliveryId)
|
|
360
|
-
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.order(deliveryId) });
|
|
361
|
-
},
|
|
362
|
-
});
|
|
157
|
+
const bid = useBusinessId();
|
|
158
|
+
const qc = useQueryClient();
|
|
159
|
+
return useMutation({ mutationFn: (deliveryId) => { if (!bid)
|
|
160
|
+
throw new Error("No business"); assertOnlineFor("order:confirm_handover"); return orderRepository.confirmHandover({ deliveryId, businessId: bid }); }, onSuccess: () => { qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: POS_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); } });
|
|
363
161
|
}
|
|
364
162
|
export function useAssignRiderMutation() {
|
|
365
|
-
const
|
|
366
|
-
const
|
|
367
|
-
return useMutation({
|
|
368
|
-
|
|
369
|
-
if (!businessId)
|
|
370
|
-
throw new Error("No active business");
|
|
371
|
-
return orderRepository.assignRider({ orderId, riderId, businessId });
|
|
372
|
-
},
|
|
373
|
-
onSettled: () => {
|
|
374
|
-
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
375
|
-
queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
|
|
376
|
-
},
|
|
377
|
-
});
|
|
163
|
+
const bid = useBusinessId();
|
|
164
|
+
const qc = useQueryClient();
|
|
165
|
+
return useMutation({ mutationFn: ({ orderId, riderId }) => { if (!bid)
|
|
166
|
+
throw new Error("No business"); assertOnlineFor("order:assign_rider"); return orderRepository.assignRider({ orderId, riderId, businessId: bid }); }, onSuccess: () => { qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); } });
|
|
378
167
|
}
|
|
379
168
|
export function useConfirmCashPaymentMutation() {
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
return useMutation({
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
export function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
case "delivered": return complete.mutateAsync(orderId);
|
|
410
|
-
case "cancelled": return cancel.mutateAsync(orderId);
|
|
411
|
-
case "rejected": return reject.mutateAsync(orderId);
|
|
412
|
-
default: throw new Error(`Unknown status ${status}`);
|
|
413
|
-
}
|
|
414
|
-
},
|
|
415
|
-
isPending: accept.isPending || reject.isPending || preparing.isPending || ready.isPending || complete.isPending || cancel.isPending,
|
|
416
|
-
};
|
|
169
|
+
const bid = useBusinessId();
|
|
170
|
+
const qc = useQueryClient();
|
|
171
|
+
return useMutation({ mutationFn: (orderId) => { if (!bid)
|
|
172
|
+
throw new Error("No business"); return orderRepository.confirmCashPayment({ orderId, businessId: bid }); }, onSuccess: (_d, _e, orderId) => { qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: POS_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); if (orderId)
|
|
173
|
+
qc.invalidateQueries({ queryKey: SELLER_KEYS.order(orderId) }); } });
|
|
174
|
+
}
|
|
175
|
+
export function useUpdateOrderStatusMutation() { const accept = useAcceptOrderMutation(); const reject = useRejectOrderMutation(); const preparing = useStartPreparingOrderMutation(); const ready = useMarkOrderReadyMutation(); const complete = useCompletePickupMutation(); const cancel = useCancelOrderMutation(); return { mutate: (orderId, status) => { const s = String(status); if (!["accepted", "preparing", "ready", "delivered", "cancelled", "rejected"].includes(s))
|
|
176
|
+
throw new Error(`Unknown status ${s}`); switch (s) {
|
|
177
|
+
case "accepted": return accept.mutateAsync(orderId);
|
|
178
|
+
case "preparing": return preparing.mutateAsync(orderId);
|
|
179
|
+
case "ready": return ready.mutateAsync(orderId);
|
|
180
|
+
case "delivered": return complete.mutateAsync(orderId);
|
|
181
|
+
case "cancelled": return cancel.mutateAsync(orderId);
|
|
182
|
+
case "rejected": return reject.mutateAsync(orderId);
|
|
183
|
+
default: throw new Error(`Unknown status ${s}`);
|
|
184
|
+
} }, isPending: accept.isPending || reject.isPending || preparing.isPending || ready.isPending || complete.isPending || cancel.isPending, }; }
|
|
185
|
+
export function isQueuedTransition(result) { const r = result; return !!r && r.status === "pending" && !!r.command_id && !r.id; }
|
|
186
|
+
/**
|
|
187
|
+
* Reactive view of locally queued (unconfirmed) order creations.
|
|
188
|
+
* Hydrates persisted optimistic events once (survives restart offline).
|
|
189
|
+
*/
|
|
190
|
+
export function usePendingOrders() {
|
|
191
|
+
const snap = useSyncExternalStore(subscribePendingOrders, () => getPendingOrdersSnapshot().orders, () => []);
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (!getPendingOrdersSnapshot().hydrated) {
|
|
194
|
+
hydratePendingOrders().catch(() => undefined);
|
|
195
|
+
}
|
|
196
|
+
}, []);
|
|
197
|
+
return snap;
|
|
417
198
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Places search hooks — single source for both apps (previously verbatim
|
|
3
|
+
* copies). Pure React + injected placesService; storage-backed caching lives
|
|
4
|
+
* in the host service implementations.
|
|
5
|
+
*/
|
|
6
|
+
import type { PlaceSuggestion, PlaceDetails } from "../types/places";
|
|
7
|
+
export declare function usePlacesSearch(debounceMs?: number): {
|
|
8
|
+
query: string;
|
|
9
|
+
setQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
10
|
+
suggestions: PlaceSuggestion[];
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
clearSearch: () => void;
|
|
13
|
+
clearAll: () => void;
|
|
14
|
+
};
|
|
15
|
+
export declare function usePlaceDetails(placeId: string | null): {
|
|
16
|
+
details: PlaceDetails | null;
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function clearPlacesCache(): void;
|
|
20
|
+
/** Generic debounced-value hook for search/filter inputs. */
|
|
21
|
+
export declare function useDebouncedValue<T>(value: T, delayMs?: number): T;
|