@teincfood/core 0.10.13 → 0.10.15
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.
|
@@ -87,8 +87,14 @@ export declare const dashboardService: {
|
|
|
87
87
|
fetchSellerDashboard: (_businessId: string) => Promise<unknown>;
|
|
88
88
|
};
|
|
89
89
|
export declare const analyticsService: {
|
|
90
|
-
fetchSellerAnalytics: (_businessId: string, _period?: string
|
|
91
|
-
|
|
90
|
+
fetchSellerAnalytics: (_businessId: string, _period?: string, _range?: {
|
|
91
|
+
date_from?: string;
|
|
92
|
+
date_to?: string;
|
|
93
|
+
}) => Promise<unknown>;
|
|
94
|
+
fetchRevenueSummary: (_businessId: string, _period?: string, _range?: {
|
|
95
|
+
date_from?: string;
|
|
96
|
+
date_to?: string;
|
|
97
|
+
}) => Promise<unknown>;
|
|
92
98
|
};
|
|
93
99
|
export declare const placesService: {
|
|
94
100
|
searchPlaces: (_query: string, _lat?: number, _lng?: number) => Promise<PlaceSuggestion[]>;
|
|
@@ -172,10 +172,10 @@ export const dashboardService = {
|
|
|
172
172
|
};
|
|
173
173
|
// analytics.service — seller analytics + revenue (hosts inject axios impls)
|
|
174
174
|
export const analyticsService = {
|
|
175
|
-
fetchSellerAnalytics: async (_businessId, _period) => {
|
|
175
|
+
fetchSellerAnalytics: async (_businessId, _period, _range) => {
|
|
176
176
|
notImpl("analyticsService.fetchSellerAnalytics");
|
|
177
177
|
},
|
|
178
|
-
fetchRevenueSummary: async (_businessId, _period) => {
|
|
178
|
+
fetchRevenueSummary: async (_businessId, _period, _range) => {
|
|
179
179
|
notImpl("analyticsService.fetchRevenueSummary");
|
|
180
180
|
},
|
|
181
181
|
};
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
* Offline-first: local SQLite aggregates serve instantly; the network
|
|
4
4
|
* refreshes silently when online.
|
|
5
5
|
*/
|
|
6
|
+
export type AnalyticsPeriod = "today" | "this_week" | "this_month" | "this_year" | "custom";
|
|
7
|
+
/** Inclusive UTC day bounds used by the custom range filter. */
|
|
8
|
+
export interface AnalyticsRange {
|
|
9
|
+
date_from?: string;
|
|
10
|
+
date_to?: string;
|
|
11
|
+
}
|
|
6
12
|
export declare const SELLER_ANALYTICS_KEYS: {
|
|
7
13
|
all: readonly ["seller-analytics"];
|
|
8
|
-
analytics: (businessId?: string, period?:
|
|
14
|
+
analytics: (businessId?: string, period?: AnalyticsPeriod, range?: AnalyticsRange) => readonly ["seller-analytics", string | undefined, AnalyticsPeriod | undefined, string | null, string | null];
|
|
9
15
|
};
|
|
10
16
|
export declare const REVENUE_KEYS: {
|
|
11
17
|
all: readonly ["revenue-summary"];
|
|
12
|
-
summary: (businessId?: string, period?:
|
|
18
|
+
summary: (businessId?: string, period?: AnalyticsPeriod, range?: AnalyticsRange) => readonly ["revenue-summary", string | undefined, AnalyticsPeriod | undefined, string | null, string | null];
|
|
13
19
|
};
|
|
14
20
|
interface LocalOrderRow {
|
|
15
21
|
status: string;
|
|
@@ -31,24 +37,21 @@ interface LocalOrderRow {
|
|
|
31
37
|
interface AggregateOptions {
|
|
32
38
|
forRevenueSummary: boolean;
|
|
33
39
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Backend-shaped aggregates computed from local SQLite rows (offline).
|
|
36
|
-
* Field names mirror orders.ex get_seller_analytics / get_revenue_summary
|
|
37
|
-
* exactly so screens render identically online and offline.
|
|
38
|
-
*/
|
|
39
40
|
/**
|
|
40
41
|
* Filter local rows to the same period boundaries as the backend
|
|
41
|
-
* (`
|
|
42
|
+
* (`period_bounds/2`):
|
|
42
43
|
* - today = current UTC day
|
|
43
44
|
* - this_week = Monday 00:00 UTC → now (backend uses ISO weekday)
|
|
44
45
|
* - this_month = 1st of the current UTC month → now
|
|
46
|
+
* - this_year = 1 January of the current UTC year → now
|
|
47
|
+
* - custom = inclusive `date_from` → `date_to` (YYYY-MM-DD)
|
|
45
48
|
*
|
|
46
49
|
* Exported so the period boundaries can be regression-tested with a fixed
|
|
47
50
|
* clock instead of depending on the day the suite runs.
|
|
48
51
|
*/
|
|
49
|
-
export declare function filterByPeriod(orders: LocalOrderRow[], period:
|
|
50
|
-
export declare function aggregateLocal(orders: LocalOrderRow[], period:
|
|
51
|
-
period:
|
|
52
|
+
export declare function filterByPeriod(orders: LocalOrderRow[], period: AnalyticsPeriod, now?: Date, range?: AnalyticsRange): LocalOrderRow[];
|
|
53
|
+
export declare function aggregateLocal(orders: LocalOrderRow[], period: AnalyticsPeriod, opts: AggregateOptions, range?: AnalyticsRange): {
|
|
54
|
+
period: AnalyticsPeriod;
|
|
52
55
|
total_revenue_minor: number;
|
|
53
56
|
total_orders: number;
|
|
54
57
|
average_order_value_minor: number;
|
|
@@ -77,7 +80,7 @@ export declare function aggregateLocal(orders: LocalOrderRow[], period: "today"
|
|
|
77
80
|
by_payment_method?: undefined;
|
|
78
81
|
by_order_type?: undefined;
|
|
79
82
|
by_day?: undefined;
|
|
80
|
-
period:
|
|
83
|
+
period: AnalyticsPeriod;
|
|
81
84
|
total_revenue_minor: number;
|
|
82
85
|
total_orders: number;
|
|
83
86
|
average_order_value_minor: number;
|
|
@@ -98,6 +101,14 @@ export declare function aggregateLocal(orders: LocalOrderRow[], period: "today"
|
|
|
98
101
|
}[];
|
|
99
102
|
offline: boolean;
|
|
100
103
|
};
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
interface AnalyticsPayload {
|
|
105
|
+
total_orders?: number;
|
|
106
|
+
[key: string]: unknown;
|
|
107
|
+
}
|
|
108
|
+
interface AnalyticsResponse {
|
|
109
|
+
data?: AnalyticsPayload;
|
|
110
|
+
[key: string]: unknown;
|
|
111
|
+
}
|
|
112
|
+
export declare function useSellerAnalyticsQuery(period?: AnalyticsPeriod, enabled?: boolean, range?: AnalyticsRange): import("@tanstack/react-query").UseQueryResult<AnalyticsResponse, Error>;
|
|
113
|
+
export declare function useRevenueSummaryQuery(period?: AnalyticsPeriod, enabled?: boolean, range?: AnalyticsRange): import("@tanstack/react-query").UseQueryResult<AnalyticsResponse, Error>;
|
|
103
114
|
export {};
|
|
@@ -3,45 +3,66 @@
|
|
|
3
3
|
* Offline-first: local SQLite aggregates serve instantly; the network
|
|
4
4
|
* refreshes silently when online.
|
|
5
5
|
*/
|
|
6
|
-
import { useQuery } from "@tanstack/react-query";
|
|
7
|
-
import { useSyncExternalStore } from "react";
|
|
6
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
8
8
|
import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
|
|
9
9
|
import { analyticsService } from "../adapters/services";
|
|
10
10
|
import { listLocalOrders } from "../domain/orders/service";
|
|
11
|
+
import { pullRemoteOrders } from "../sync/sync-jobs.service";
|
|
12
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
11
13
|
import { useIsEffectivelyOnline } from "./use-infinite-list";
|
|
12
14
|
export const SELLER_ANALYTICS_KEYS = {
|
|
13
15
|
all: ["seller-analytics"],
|
|
14
|
-
analytics: (businessId, period) => [
|
|
16
|
+
analytics: (businessId, period, range) => [
|
|
17
|
+
...SELLER_ANALYTICS_KEYS.all,
|
|
18
|
+
businessId,
|
|
19
|
+
period,
|
|
20
|
+
range?.date_from ?? null,
|
|
21
|
+
range?.date_to ?? null,
|
|
22
|
+
],
|
|
15
23
|
};
|
|
16
24
|
export const REVENUE_KEYS = {
|
|
17
25
|
all: ["revenue-summary"],
|
|
18
|
-
summary: (businessId, period) => [
|
|
26
|
+
summary: (businessId, period, range) => [
|
|
27
|
+
...REVENUE_KEYS.all,
|
|
28
|
+
businessId,
|
|
29
|
+
period,
|
|
30
|
+
range?.date_from ?? null,
|
|
31
|
+
range?.date_to ?? null,
|
|
32
|
+
],
|
|
19
33
|
};
|
|
20
34
|
function useBusinessId() {
|
|
21
35
|
const id = useSyncExternalStore(subscribeBusiness, () => getActiveBusiness()?.id ?? null, () => null);
|
|
22
36
|
return id ?? undefined;
|
|
23
37
|
}
|
|
24
38
|
const OFFLINE_FIRST = { retry: false, networkMode: "offlineFirst" };
|
|
25
|
-
/**
|
|
26
|
-
* Backend-shaped aggregates computed from local SQLite rows (offline).
|
|
27
|
-
* Field names mirror orders.ex get_seller_analytics / get_revenue_summary
|
|
28
|
-
* exactly so screens render identically online and offline.
|
|
29
|
-
*/
|
|
30
39
|
/**
|
|
31
40
|
* Filter local rows to the same period boundaries as the backend
|
|
32
|
-
* (`
|
|
41
|
+
* (`period_bounds/2`):
|
|
33
42
|
* - today = current UTC day
|
|
34
43
|
* - this_week = Monday 00:00 UTC → now (backend uses ISO weekday)
|
|
35
44
|
* - this_month = 1st of the current UTC month → now
|
|
45
|
+
* - this_year = 1 January of the current UTC year → now
|
|
46
|
+
* - custom = inclusive `date_from` → `date_to` (YYYY-MM-DD)
|
|
36
47
|
*
|
|
37
48
|
* Exported so the period boundaries can be regression-tested with a fixed
|
|
38
49
|
* clock instead of depending on the day the suite runs.
|
|
39
50
|
*/
|
|
40
|
-
export function filterByPeriod(orders, period, now = new Date()) {
|
|
51
|
+
export function filterByPeriod(orders, period, now = new Date(), range) {
|
|
41
52
|
const todayStr = now.toISOString().slice(0, 10);
|
|
42
53
|
if (period === "today") {
|
|
43
54
|
return orders.filter((o) => (o.inserted_at ?? "").slice(0, 10) === todayStr);
|
|
44
55
|
}
|
|
56
|
+
if (period === "custom") {
|
|
57
|
+
const from = range?.date_from;
|
|
58
|
+
const to = range?.date_to;
|
|
59
|
+
if (!from || !to)
|
|
60
|
+
return [];
|
|
61
|
+
return orders.filter((o) => {
|
|
62
|
+
const d = (o.inserted_at ?? "").slice(0, 10);
|
|
63
|
+
return d >= from && d <= to;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
45
66
|
let startStr;
|
|
46
67
|
if (period === "this_week") {
|
|
47
68
|
// `getUTCDay()` is 0 for Sunday; shift so Monday is the first day.
|
|
@@ -51,6 +72,9 @@ export function filterByPeriod(orders, period, now = new Date()) {
|
|
|
51
72
|
startOfWeek.setUTCDate(todayUtc.getUTCDate() - daysSinceMonday);
|
|
52
73
|
startStr = startOfWeek.toISOString().slice(0, 10);
|
|
53
74
|
}
|
|
75
|
+
else if (period === "this_year") {
|
|
76
|
+
startStr = `${todayStr.slice(0, 4)}-01-01`;
|
|
77
|
+
}
|
|
54
78
|
else {
|
|
55
79
|
startStr = `${todayStr.slice(0, 7)}-01`;
|
|
56
80
|
}
|
|
@@ -59,8 +83,8 @@ export function filterByPeriod(orders, period, now = new Date()) {
|
|
|
59
83
|
return d >= startStr && d <= todayStr;
|
|
60
84
|
});
|
|
61
85
|
}
|
|
62
|
-
export function aggregateLocal(orders, period, opts) {
|
|
63
|
-
const filtered = filterByPeriod(orders, period);
|
|
86
|
+
export function aggregateLocal(orders, period, opts, range) {
|
|
87
|
+
const filtered = filterByPeriod(orders, period, new Date(), range);
|
|
64
88
|
// Backend parity (orders.ex get_business_analytics / get_revenue_summary):
|
|
65
89
|
// revenue-eligible = all EXCEPT cancelled. Cancelled orders must never
|
|
66
90
|
// inflate revenue, counts, breakdowns, or top items — online and offline.
|
|
@@ -68,7 +92,9 @@ export function aggregateLocal(orders, period, opts) {
|
|
|
68
92
|
const total_revenue_minor = revenueOrders.reduce((s, o) => s + (o.total_minor ?? 0), 0);
|
|
69
93
|
const total_orders = revenueOrders.length;
|
|
70
94
|
// Backend uses div (truncation) — mirror, don't round.
|
|
71
|
-
const average_order_value_minor = total_orders
|
|
95
|
+
const average_order_value_minor = total_orders
|
|
96
|
+
? Math.floor(total_revenue_minor / total_orders)
|
|
97
|
+
: 0;
|
|
72
98
|
const byMethod = new Map();
|
|
73
99
|
const byType = new Map();
|
|
74
100
|
const byDay = new Map();
|
|
@@ -98,7 +124,9 @@ export function aggregateLocal(orders, period, opts) {
|
|
|
98
124
|
itemRev.set(name, (itemRev.get(name) ?? 0) + rev);
|
|
99
125
|
}
|
|
100
126
|
}
|
|
101
|
-
const pct = (v) =>
|
|
127
|
+
const pct = (v) => total_revenue_minor > 0
|
|
128
|
+
? Math.round((v / total_revenue_minor) * 1000) / 10
|
|
129
|
+
: 0;
|
|
102
130
|
// Backend orders_by_status counts over ALL period orders (not revenue-only).
|
|
103
131
|
const orders_by_status = {
|
|
104
132
|
delivered: filtered.filter((o) => o.status === "delivered").length,
|
|
@@ -112,7 +140,11 @@ export function aggregateLocal(orders, period, opts) {
|
|
|
112
140
|
total_orders,
|
|
113
141
|
average_order_value_minor,
|
|
114
142
|
by_payment_method: [...byMethod.entries()]
|
|
115
|
-
.map(([method, v]) => ({
|
|
143
|
+
.map(([method, v]) => ({
|
|
144
|
+
method,
|
|
145
|
+
...v,
|
|
146
|
+
percentage: pct(v.revenue_minor),
|
|
147
|
+
}))
|
|
116
148
|
.sort((a, b) => b.revenue_minor - a.revenue_minor),
|
|
117
149
|
by_order_type: [...byType.entries()]
|
|
118
150
|
.map(([type, v]) => ({ type, ...v }))
|
|
@@ -130,7 +162,11 @@ export function aggregateLocal(orders, period, opts) {
|
|
|
130
162
|
average_order_value_minor,
|
|
131
163
|
total_refunds_minor: 0,
|
|
132
164
|
top_items: [...itemQty.entries()]
|
|
133
|
-
.map(([name, quantity]) => ({
|
|
165
|
+
.map(([name, quantity]) => ({
|
|
166
|
+
name,
|
|
167
|
+
quantity,
|
|
168
|
+
revenue_minor: itemRev.get(name) ?? 0,
|
|
169
|
+
}))
|
|
134
170
|
.sort((a, b) => b.revenue_minor - a.revenue_minor)
|
|
135
171
|
.slice(0, 10),
|
|
136
172
|
orders_by_status,
|
|
@@ -142,69 +178,145 @@ export function aggregateLocal(orders, period, opts) {
|
|
|
142
178
|
}
|
|
143
179
|
async function localOrders(businessId) {
|
|
144
180
|
const { data } = await listLocalOrders(businessId, {}, 1, 500);
|
|
145
|
-
return
|
|
181
|
+
return data ?? [];
|
|
182
|
+
}
|
|
183
|
+
function hasCustomRange(period, range) {
|
|
184
|
+
return (period !== "custom" || Boolean(range?.date_from && range?.date_to));
|
|
146
185
|
}
|
|
147
|
-
|
|
186
|
+
const ORDER_PULL_COOLDOWN_MS = 15000;
|
|
187
|
+
const lastOrderPullAt = new Map();
|
|
188
|
+
/**
|
|
189
|
+
* Refresh the local order read model in the background. Analytics renders
|
|
190
|
+
* from SQLite immediately; this pull is what makes a freshly opened screen
|
|
191
|
+
* converge with the server without blocking the first paint.
|
|
192
|
+
*/
|
|
193
|
+
async function syncOrdersForAnalytics(businessId, queryClient) {
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
const last = lastOrderPullAt.get(businessId) ?? 0;
|
|
196
|
+
if (now - last < ORDER_PULL_COOLDOWN_MS)
|
|
197
|
+
return;
|
|
198
|
+
lastOrderPullAt.set(businessId, now);
|
|
199
|
+
try {
|
|
200
|
+
await pullRemoteOrders(businessId);
|
|
201
|
+
await Promise.all([
|
|
202
|
+
queryClient.invalidateQueries({ queryKey: SELLER_ANALYTICS_KEYS.all }),
|
|
203
|
+
queryClient.invalidateQueries({ queryKey: REVENUE_KEYS.all }),
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
syncLogger.debug("Analytics", "background order sync failed", {
|
|
208
|
+
businessId,
|
|
209
|
+
err: String(error),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Keep local data when it knows about more orders than the server snapshot
|
|
215
|
+
* (e.g. an offline order still in the outbox). Otherwise the server snapshot
|
|
216
|
+
* wins because it may include older history the device has never pulled.
|
|
217
|
+
*/
|
|
218
|
+
function preferRemote(current, remote) {
|
|
219
|
+
const localOrders = current?.data?.total_orders ?? 0;
|
|
220
|
+
const remoteOrders = remote.data?.total_orders ?? 0;
|
|
221
|
+
return remoteOrders >= localOrders ? remote : (current ?? remote);
|
|
222
|
+
}
|
|
223
|
+
async function buildLocalAnalytics(businessId, period, range) {
|
|
224
|
+
try {
|
|
225
|
+
return {
|
|
226
|
+
data: aggregateLocal(await localOrders(businessId), period, { forRevenueSummary: false }, range),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return {
|
|
231
|
+
data: aggregateLocal([], period, { forRevenueSummary: false }, range),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function buildLocalRevenue(businessId, period, range) {
|
|
236
|
+
try {
|
|
237
|
+
return {
|
|
238
|
+
data: aggregateLocal(await localOrders(businessId), period, { forRevenueSummary: true }, range),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return {
|
|
243
|
+
data: aggregateLocal([], period, { forRevenueSummary: true }, range),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export function useSellerAnalyticsQuery(period = "today", enabled = true, range) {
|
|
148
248
|
const businessId = useBusinessId();
|
|
149
249
|
const online = useIsEffectivelyOnline();
|
|
250
|
+
const queryClient = useQueryClient();
|
|
251
|
+
const customReady = hasCustomRange(period, range);
|
|
252
|
+
const queryKey = SELLER_ANALYTICS_KEYS.analytics(businessId ?? "", period, range);
|
|
253
|
+
const active = enabled && Boolean(businessId) && customReady;
|
|
254
|
+
// Local-first: read SQLite on every query run. The network is a background
|
|
255
|
+
// refresh only, so opening the screen never waits on the server.
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (!active || !online || !businessId)
|
|
258
|
+
return;
|
|
259
|
+
void syncOrdersForAnalytics(businessId, queryClient);
|
|
260
|
+
}, [active, online, businessId, queryClient]);
|
|
150
261
|
return useQuery({
|
|
151
|
-
queryKey
|
|
262
|
+
queryKey,
|
|
152
263
|
queryFn: async () => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
forRevenueSummary: false,
|
|
171
|
-
}),
|
|
172
|
-
};
|
|
264
|
+
const local = await buildLocalAnalytics(businessId, period, range);
|
|
265
|
+
if (online && businessId) {
|
|
266
|
+
// Do not await. Once the server snapshot arrives, merge it into the
|
|
267
|
+
// cache only when it is at least as complete as the local model.
|
|
268
|
+
void analyticsService
|
|
269
|
+
.fetchSellerAnalytics(businessId, period, range)
|
|
270
|
+
.then((remote) => {
|
|
271
|
+
const remoteResponse = remote;
|
|
272
|
+
const current = queryClient.getQueryData(queryKey);
|
|
273
|
+
queryClient.setQueryData(queryKey, preferRemote(current, remoteResponse));
|
|
274
|
+
})
|
|
275
|
+
.catch((error) => {
|
|
276
|
+
syncLogger.debug("Analytics", "background analytics refresh failed", {
|
|
277
|
+
businessId,
|
|
278
|
+
err: String(error),
|
|
279
|
+
});
|
|
280
|
+
});
|
|
173
281
|
}
|
|
282
|
+
return local;
|
|
174
283
|
},
|
|
175
|
-
enabled:
|
|
284
|
+
enabled: active,
|
|
176
285
|
staleTime: 30000,
|
|
177
286
|
...OFFLINE_FIRST,
|
|
178
287
|
});
|
|
179
288
|
}
|
|
180
|
-
export function useRevenueSummaryQuery(period = "today", enabled = true) {
|
|
289
|
+
export function useRevenueSummaryQuery(period = "today", enabled = true, range) {
|
|
181
290
|
const businessId = useBusinessId();
|
|
182
291
|
const online = useIsEffectivelyOnline();
|
|
292
|
+
const queryClient = useQueryClient();
|
|
293
|
+
const customReady = hasCustomRange(period, range);
|
|
294
|
+
const queryKey = REVENUE_KEYS.summary(businessId ?? "", period, range);
|
|
295
|
+
const active = enabled && Boolean(businessId) && customReady;
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!active || !online || !businessId)
|
|
298
|
+
return;
|
|
299
|
+
void syncOrdersForAnalytics(businessId, queryClient);
|
|
300
|
+
}, [active, online, businessId, queryClient]);
|
|
183
301
|
return useQuery({
|
|
184
|
-
queryKey
|
|
302
|
+
queryKey,
|
|
185
303
|
queryFn: async () => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
return {
|
|
201
|
-
data: aggregateLocal(await localOrders(businessId), period, {
|
|
202
|
-
forRevenueSummary: true,
|
|
203
|
-
}),
|
|
204
|
-
};
|
|
304
|
+
const local = await buildLocalRevenue(businessId, period, range);
|
|
305
|
+
if (online && businessId) {
|
|
306
|
+
void analyticsService
|
|
307
|
+
.fetchRevenueSummary(businessId, period, range)
|
|
308
|
+
.then((remote) => {
|
|
309
|
+
const remoteResponse = remote;
|
|
310
|
+
const current = queryClient.getQueryData(queryKey);
|
|
311
|
+
queryClient.setQueryData(queryKey, preferRemote(current, remoteResponse));
|
|
312
|
+
})
|
|
313
|
+
.catch((error) => {
|
|
314
|
+
syncLogger.debug("Analytics", "background revenue refresh failed", { businessId, err: String(error) });
|
|
315
|
+
});
|
|
205
316
|
}
|
|
317
|
+
return local;
|
|
206
318
|
},
|
|
207
|
-
enabled:
|
|
319
|
+
enabled: active,
|
|
208
320
|
staleTime: 30000,
|
|
209
321
|
...OFFLINE_FIRST,
|
|
210
322
|
});
|
package/package.json
CHANGED