@wix/himalaya-cli 0.1253.0 → 0.1254.0
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/package.json +2 -2
- package/templates/beauty-salon-owner/dev/index.ts +7 -5
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/community-club-owner/dev/index.ts +7 -5
- package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/community-club-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/community-club-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/community-club-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/community-club-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/events-meetups-owner/dev/index.ts +7 -5
- package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/events-meetups-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/gym-fitness-owner/dev/index.ts +7 -5
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/nonprofit-cause-owner/dev/index.ts +7 -5
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/owner-lite/dev/index.ts +7 -5
- package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/owner-lite/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/owner-lite/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/owner-lite/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/owner-lite/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/podcast-creator-owner/dev/index.ts +7 -5
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/portfolio-creative-owner/dev/index.ts +7 -5
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/restaurant-owner/dev/index.ts +7 -5
- package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/restaurant-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/retail-store-owner/dev/index.ts +7 -5
- package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/retail-store-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
- package/templates/yoga-studio-owner/dev/index.ts +7 -5
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +78 -7
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
|
@@ -19,8 +19,277 @@
|
|
|
19
19
|
|
|
20
20
|
import { defineService } from "@himalaya/state";
|
|
21
21
|
import { serializeForAgent } from "@himalaya/agent";
|
|
22
|
+
import { searchOrders } from "@himalaya/wix-ecom";
|
|
23
|
+
import { queryBookings, type WixBooking } from "@himalaya/wix-bookings";
|
|
24
|
+
import { site } from "@himalaya/wix-platform";
|
|
22
25
|
import { activeScreenFromInput, attentionRow, navRow, withDerived, type DrawerState } from "./logic.js";
|
|
23
|
-
import {
|
|
26
|
+
import { UPCOMING_BOOKING_STATUSES, upcomingBookingCount } from "../_shared/owner-bookings.js";
|
|
27
|
+
import { ownerOrderAttentionFilter } from "../_shared/owner-order-attention.js";
|
|
28
|
+
import { fallbackNameFrom, readSelectedSite, selectedSiteName } from "../_shared/owner-selected-site.js";
|
|
29
|
+
|
|
30
|
+
const ATTENTION_PAGE = 50;
|
|
31
|
+
const DISMISSED_AT_KEY = "himi.ownerLite.drawer.attentionDismissedAt.v1";
|
|
32
|
+
const OBSERVED_ACTIVITY_AT_KEY = "himi.ownerLite.drawer.latestObservedActivityAt.v1";
|
|
33
|
+
const DISMISSED_BOOKINGS_KEY = "himi.ownerLite.drawer.dismissedBookings.v1";
|
|
34
|
+
const OBSERVED_BOOKINGS_KEY = "himi.ownerLite.drawer.observedBookings.v1";
|
|
35
|
+
const DISMISSED_ORDERS_KEY = "himi.ownerLite.drawer.dismissedOrders.v1";
|
|
36
|
+
const OBSERVED_ORDERS_KEY = "himi.ownerLite.drawer.observedOrders.v1";
|
|
37
|
+
let refreshRevision = 0;
|
|
38
|
+
|
|
39
|
+
type DrawerStorage = { getItem(key: string): string | null; setItem(key: string, value: string): void };
|
|
40
|
+
type DrawerHost = typeof globalThis & { himalaya?: { storage?: DrawerStorage; input?: Record<string, unknown> } };
|
|
41
|
+
|
|
42
|
+
function currentSiteId(): string {
|
|
43
|
+
const selected = readSelectedSite()?.id;
|
|
44
|
+
if (selected) return selected;
|
|
45
|
+
const business = (globalThis as DrawerHost).himalaya?.input?.business;
|
|
46
|
+
return typeof business === "string" && business.trim() !== "default" ? business.trim() : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function dismissalKey(siteId = currentSiteId()): string {
|
|
50
|
+
const input = (globalThis as DrawerHost).himalaya?.input;
|
|
51
|
+
const key = siteId || (typeof input?.business === "string" ? input.business.trim() : "") || "default";
|
|
52
|
+
return DISMISSED_AT_KEY + ":" + encodeURIComponent(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function dismissedAt(key = dismissalKey()): string {
|
|
56
|
+
// If persistence is unavailable, fail the refresh instead of treating a failed
|
|
57
|
+
// read as “not dismissed” and unexpectedly restoring hidden reminders.
|
|
58
|
+
const value = (globalThis as DrawerHost).himalaya?.storage?.getItem(key) ?? "";
|
|
59
|
+
return value && Number.isFinite(Date.parse(value)) ? value : "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function latestActivityAt(record: { createdDate?: unknown; updatedDate?: unknown }): string {
|
|
63
|
+
const timestamps = [record.createdDate, record.updatedDate]
|
|
64
|
+
.filter((value): value is string => typeof value === "string" && Number.isFinite(Date.parse(value)))
|
|
65
|
+
.sort((a, b) => Date.parse(b) - Date.parse(a));
|
|
66
|
+
return timestamps[0] ?? "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function newerActivity(left: string, right: string): string {
|
|
70
|
+
return !left || (right && Date.parse(right) > Date.parse(left)) ? right : left;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function rememberObservedActivity(siteKey: string, value: string): void {
|
|
74
|
+
if (!value) return;
|
|
75
|
+
try {
|
|
76
|
+
const storage = (globalThis as DrawerHost).himalaya?.storage;
|
|
77
|
+
const key = `${OBSERVED_ACTIVITY_AT_KEY}:${encodeURIComponent(siteKey)}`;
|
|
78
|
+
const previous = storage?.getItem(key) ?? "";
|
|
79
|
+
const latest = newerActivity(previous, value);
|
|
80
|
+
if (latest) storage?.setItem(key, latest);
|
|
81
|
+
} catch { /* best-effort local preference */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type RecordSnapshot = Record<string, string>;
|
|
85
|
+
type BookingSnapshot = RecordSnapshot;
|
|
86
|
+
type OrderSnapshot = RecordSnapshot;
|
|
87
|
+
|
|
88
|
+
function bookingSnapshotKey(prefix: string, siteKey: string): string {
|
|
89
|
+
return `${prefix}:${encodeURIComponent(siteKey)}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readRecordSnapshot(key: string): RecordSnapshot | undefined {
|
|
93
|
+
const raw = (globalThis as DrawerHost).himalaya?.storage?.getItem(key);
|
|
94
|
+
if (raw === null || raw === undefined) return undefined;
|
|
95
|
+
const value = JSON.parse(raw) as unknown;
|
|
96
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
97
|
+
throw new Error("Stored drawer reminder snapshot must be an object");
|
|
98
|
+
}
|
|
99
|
+
const entries = Object.entries(value);
|
|
100
|
+
if (entries.some((entry) => typeof entry[1] !== "string")) {
|
|
101
|
+
throw new Error("Stored drawer reminder snapshot values must be strings");
|
|
102
|
+
}
|
|
103
|
+
return Object.fromEntries(entries) as RecordSnapshot;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const readBookingSnapshot = (key: string): BookingSnapshot | undefined => readRecordSnapshot(key);
|
|
107
|
+
|
|
108
|
+
function orderSignature(order: {
|
|
109
|
+
archived?: unknown;
|
|
110
|
+
status?: unknown;
|
|
111
|
+
paymentStatus?: unknown;
|
|
112
|
+
fulfillmentStatus?: unknown;
|
|
113
|
+
createdDate?: unknown;
|
|
114
|
+
updatedDate?: unknown;
|
|
115
|
+
}): string {
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
archived: order.archived ?? null,
|
|
118
|
+
status: order.status ?? "",
|
|
119
|
+
paymentStatus: order.paymentStatus ?? "",
|
|
120
|
+
fulfillmentStatus: order.fulfillmentStatus ?? "",
|
|
121
|
+
createdDate: order.createdDate ?? "",
|
|
122
|
+
updatedDate: order.updatedDate ?? "",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function orderSnapshot(orders: Array<{ id?: string } & Parameters<typeof orderSignature>[0]>): OrderSnapshot {
|
|
127
|
+
return Object.fromEntries(orders.filter((order) => order.id)
|
|
128
|
+
.map((order) => [order.id!, orderSignature(order)]));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function rememberObservedOrders(siteKey: string, snapshot: OrderSnapshot): void {
|
|
132
|
+
try {
|
|
133
|
+
(globalThis as DrawerHost).himalaya?.storage?.setItem(
|
|
134
|
+
bookingSnapshotKey(OBSERVED_ORDERS_KEY, siteKey), JSON.stringify(snapshot),
|
|
135
|
+
);
|
|
136
|
+
} catch { /* best-effort local preference */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function bookingSignature(booking: WixBooking): string {
|
|
140
|
+
return JSON.stringify({
|
|
141
|
+
status: booking.status ?? "",
|
|
142
|
+
paymentStatus: booking.paymentStatus ?? "",
|
|
143
|
+
startDate: booking.startDate ?? "",
|
|
144
|
+
endDate: booking.endDate ?? "",
|
|
145
|
+
slotStartDate: booking.bookedEntity?.slot?.startDate ?? "",
|
|
146
|
+
slotEndDate: booking.bookedEntity?.slot?.endDate ?? "",
|
|
147
|
+
totalParticipants: booking.totalParticipants ?? null,
|
|
148
|
+
serviceId: booking.bookedEntity?.slot?.serviceId ?? "",
|
|
149
|
+
serviceTitle: booking.bookedEntity?.title ?? "",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function bookingSnapshot(bookings: WixBooking[]): BookingSnapshot {
|
|
154
|
+
return Object.fromEntries(bookings.filter((booking) => booking.id)
|
|
155
|
+
.map((booking) => [booking.id, bookingSignature(booking)]));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function rememberObservedBookings(siteKey: string, snapshot: BookingSnapshot): void {
|
|
159
|
+
try {
|
|
160
|
+
(globalThis as DrawerHost).himalaya?.storage?.setItem(
|
|
161
|
+
bookingSnapshotKey(OBSERVED_BOOKINGS_KEY, siteKey), JSON.stringify(snapshot),
|
|
162
|
+
);
|
|
163
|
+
} catch { /* best-effort local preference */ }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function rememberDismissal(siteKey: string): void {
|
|
167
|
+
try {
|
|
168
|
+
const storage = (globalThis as DrawerHost).himalaya?.storage;
|
|
169
|
+
const observedKey = bookingSnapshotKey(OBSERVED_BOOKINGS_KEY, siteKey);
|
|
170
|
+
const dismissedKey = bookingSnapshotKey(DISMISSED_BOOKINGS_KEY, siteKey);
|
|
171
|
+
storage?.setItem(dismissedKey, storage?.getItem(observedKey) ?? "{}");
|
|
172
|
+
const observedOrdersKey = bookingSnapshotKey(OBSERVED_ORDERS_KEY, siteKey);
|
|
173
|
+
const dismissedOrdersKey = bookingSnapshotKey(DISMISSED_ORDERS_KEY, siteKey);
|
|
174
|
+
storage?.setItem(dismissedOrdersKey, storage?.getItem(observedOrdersKey) ?? "{}");
|
|
175
|
+
} catch { /* best-effort local preference */ }
|
|
176
|
+
try {
|
|
177
|
+
const storage = (globalThis as DrawerHost).himalaya?.storage;
|
|
178
|
+
const observed = storage?.getItem(`${OBSERVED_ACTIVITY_AT_KEY}:${encodeURIComponent(siteKey)}`) ?? "";
|
|
179
|
+
// Wix record timestamps are server-generated. Do not substitute device time:
|
|
180
|
+
// an ahead clock can hide later Wix activity. Order snapshots cover records
|
|
181
|
+
// without timestamps.
|
|
182
|
+
const previous = dismissedAt(siteKey);
|
|
183
|
+
storage?.setItem(siteKey, observed || previous);
|
|
184
|
+
} catch { /* best-effort local preference */ }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function countAttentionOrders(
|
|
188
|
+
filter: Record<string, unknown>, siteId: string, dismissed: string, dismissedSnapshot: OrderSnapshot | undefined,
|
|
189
|
+
): Promise<{ count: number; latestActivity: string; snapshot: OrderSnapshot }> {
|
|
190
|
+
const orders: Array<{ id?: string } & Parameters<typeof orderSignature>[0]> = [];
|
|
191
|
+
let latestActivity = "";
|
|
192
|
+
let cursor = "";
|
|
193
|
+
const seenIds = new Set<string>();
|
|
194
|
+
const seen = new Set<string>();
|
|
195
|
+
while (true) {
|
|
196
|
+
const response = await searchOrders({ search: {
|
|
197
|
+
filter,
|
|
198
|
+
sort: [{ fieldName: "createdDate", order: "DESC" }, { fieldName: "id", order: "ASC" }],
|
|
199
|
+
cursorPaging: { limit: ATTENTION_PAGE, ...(cursor ? { cursor } : {}) },
|
|
200
|
+
} }, siteId ? site.forSite(siteId) : undefined);
|
|
201
|
+
for (const order of response.orders ?? []) {
|
|
202
|
+
latestActivity = newerActivity(latestActivity, latestActivityAt(order));
|
|
203
|
+
if (order.id && seenIds.has(order.id)) continue;
|
|
204
|
+
if (order.id) seenIds.add(order.id);
|
|
205
|
+
orders.push(order);
|
|
206
|
+
}
|
|
207
|
+
const metadata = response.metadata as { hasNext?: boolean; cursors?: { next?: string | null } } | undefined;
|
|
208
|
+
const next = metadata?.cursors?.next ?? "";
|
|
209
|
+
if (metadata?.hasNext === false) break;
|
|
210
|
+
// Keep the count already observed if the API advertises another page without
|
|
211
|
+
// giving us a usable cursor. This is a lower bound, but still alerts the owner.
|
|
212
|
+
if (!next || seen.has(next)) break;
|
|
213
|
+
seen.add(next);
|
|
214
|
+
cursor = next;
|
|
215
|
+
}
|
|
216
|
+
const snapshot = orderSnapshot(orders);
|
|
217
|
+
const hasDismissal = Boolean(dismissed) || dismissedSnapshot !== undefined;
|
|
218
|
+
const count = hasDismissal
|
|
219
|
+
? orders.filter((order) => {
|
|
220
|
+
const activity = latestActivityAt(order);
|
|
221
|
+
if (dismissed && activity) return Date.parse(activity) > Date.parse(dismissed);
|
|
222
|
+
if (dismissedSnapshot !== undefined) {
|
|
223
|
+
return !order.id || dismissedSnapshot[order.id] === undefined
|
|
224
|
+
|| dismissedSnapshot[order.id] !== orderSignature(order);
|
|
225
|
+
}
|
|
226
|
+
return activity !== "" && Date.parse(activity) > Date.parse(dismissed);
|
|
227
|
+
}).length
|
|
228
|
+
: orders.length;
|
|
229
|
+
return { count, latestActivity, snapshot };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function countUpcomingBookings(
|
|
233
|
+
filter: Record<string, unknown>, now: number, dismissed: string, dismissedSnapshot: BookingSnapshot | undefined, siteId: string,
|
|
234
|
+
): Promise<{ count: number; latestActivity: string; snapshot: BookingSnapshot }> {
|
|
235
|
+
const bookings: WixBooking[] = [];
|
|
236
|
+
const seenIds = new Set<string>();
|
|
237
|
+
const seenPages = new Set<string>();
|
|
238
|
+
let offset = 0;
|
|
239
|
+
let latestActivity = "";
|
|
240
|
+
const result = () => {
|
|
241
|
+
const hasDismissal = Boolean(dismissed) || dismissedSnapshot !== undefined;
|
|
242
|
+
const visible = hasDismissal
|
|
243
|
+
? bookings.filter((booking) => {
|
|
244
|
+
const createdAfterDismissal = dismissed !== ""
|
|
245
|
+
&& Date.parse(booking.createdDate ?? "") > Date.parse(dismissed);
|
|
246
|
+
if (createdAfterDismissal) return true;
|
|
247
|
+
if (!dismissedSnapshot) return false; // Legacy timestamp-only dismissal.
|
|
248
|
+
if (!booking.id) return false;
|
|
249
|
+
return dismissedSnapshot[booking.id] === undefined
|
|
250
|
+
|| dismissedSnapshot[booking.id] !== bookingSignature(booking);
|
|
251
|
+
})
|
|
252
|
+
: bookings;
|
|
253
|
+
return {
|
|
254
|
+
count: upcomingBookingCount(visible, now),
|
|
255
|
+
latestActivity,
|
|
256
|
+
snapshot: bookingSnapshot(bookings),
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
while (true) {
|
|
260
|
+
const response = await queryBookings({ query: {
|
|
261
|
+
filter,
|
|
262
|
+
sort: [{ fieldName: "startDate", order: "ASC" }, { fieldName: "id", order: "ASC" }],
|
|
263
|
+
paging: { limit: ATTENTION_PAGE, offset },
|
|
264
|
+
} }, siteId ? site.forSite(siteId) : undefined);
|
|
265
|
+
const page = response.bookings ?? [];
|
|
266
|
+
const pageSignature = page.map((booking) => booking.id
|
|
267
|
+
? `id:${booking.id}`
|
|
268
|
+
: `record:${JSON.stringify(booking)}`).sort().join("\0");
|
|
269
|
+
if (pageSignature && seenPages.has(pageSignature)) {
|
|
270
|
+
return result();
|
|
271
|
+
}
|
|
272
|
+
if (pageSignature) seenPages.add(pageSignature);
|
|
273
|
+
for (const booking of page) {
|
|
274
|
+
latestActivity = newerActivity(latestActivity, latestActivityAt(booking));
|
|
275
|
+
if (booking.id && seenIds.has(booking.id)) continue;
|
|
276
|
+
if (booking.id) seenIds.add(booking.id);
|
|
277
|
+
bookings.push(booking);
|
|
278
|
+
}
|
|
279
|
+
// Some Wix responses mark a total as too large to count even though the
|
|
280
|
+
// committed SDK metadata type does not include that wire field.
|
|
281
|
+
const metadata = response.pagingMetadata as (typeof response.pagingMetadata & { tooManyToCount?: boolean }) | undefined;
|
|
282
|
+
const total = metadata?.total;
|
|
283
|
+
if (typeof total === "number" && Number.isFinite(total) && metadata?.tooManyToCount !== true) {
|
|
284
|
+
const expected = Math.max(0, Math.floor(total));
|
|
285
|
+
if (bookings.length >= expected || page.length === 0) return result();
|
|
286
|
+
offset += page.length;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (page.length < ATTENTION_PAGE) return result();
|
|
290
|
+
offset += page.length;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
24
293
|
|
|
25
294
|
/** The shell passes the screen it is showing as worker input, so the drawer can mark
|
|
26
295
|
* that destination selected the way the declarative drawer does. */
|
|
@@ -35,7 +304,7 @@ const currentSiteName = () => selectedSiteName(fallbackBusinessName);
|
|
|
35
304
|
const drawer = defineService<DrawerState, {
|
|
36
305
|
/** The panel opened (root onAppear): re-read the live site and the active screen.
|
|
37
306
|
* `arg.businessName` is the cold-start fallback for the site name. */
|
|
38
|
-
refresh: (s: DrawerState, arg?: unknown) => DrawerState
|
|
307
|
+
refresh: (s: DrawerState, arg?: unknown) => Promise<DrawerState>;
|
|
39
308
|
/** Re-read the active screen from host input. An explicit
|
|
40
309
|
* `{ screenId }` wins, for a host that pushes the change instead. */
|
|
41
310
|
syncActive: (s: DrawerState, args?: { screenId?: string }) => DrawerState;
|
|
@@ -57,20 +326,59 @@ const drawer = defineService<DrawerState, {
|
|
|
57
326
|
navRow("drawer_manage", "Manage", "grid", "himi_owner_lite_manage"),
|
|
58
327
|
navRow("drawer_account", "Account", "person", "himi_owner_lite_account"),
|
|
59
328
|
],
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
attentionRow("att_appts", "Appointments", "calendar", "himi_owner_lite_appointments", 1),
|
|
63
|
-
],
|
|
329
|
+
// Live rows arrive only after refresh. A cold/failed read never invents reminders.
|
|
330
|
+
attention: [],
|
|
64
331
|
confirmingClear: false,
|
|
65
332
|
activeScreenId: hostActiveScreen(),
|
|
66
333
|
}),
|
|
67
334
|
actions: {
|
|
68
|
-
refresh: (s, arg) => {
|
|
335
|
+
refresh: async (s, arg): Promise<DrawerState> => {
|
|
69
336
|
fallbackBusinessName = fallbackNameFrom(arg, fallbackBusinessName);
|
|
70
337
|
const screenId = (arg as { screenId?: unknown } | undefined)?.screenId;
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
// Invalidate older work before any read that can fail, including persistence.
|
|
340
|
+
const revision = ++refreshRevision;
|
|
341
|
+
const siteId = currentSiteId();
|
|
342
|
+
const siteKey = dismissalKey(siteId);
|
|
343
|
+
const dismissed = dismissedAt(siteKey);
|
|
344
|
+
const dismissedSnapshot = readBookingSnapshot(bookingSnapshotKey(DISMISSED_BOOKINGS_KEY, siteKey));
|
|
345
|
+
const dismissedOrders = readRecordSnapshot(bookingSnapshotKey(DISMISSED_ORDERS_KEY, siteKey));
|
|
346
|
+
// Only the newest same-site refresh may apply, so a slower earlier request
|
|
347
|
+
// cannot replace a result from a newer refresh.
|
|
348
|
+
const [ordersResult, bookingsResult] = await Promise.allSettled([
|
|
349
|
+
countAttentionOrders(ownerOrderAttentionFilter(), siteId, dismissed, dismissedOrders),
|
|
350
|
+
countUpcomingBookings({
|
|
351
|
+
startDate: { $gte: new Date(now).toISOString() },
|
|
352
|
+
status: { $in: UPCOMING_BOOKING_STATUSES },
|
|
353
|
+
}, now, dismissed, dismissedSnapshot, siteId),
|
|
354
|
+
]);
|
|
355
|
+
// A user may dismiss while either request is in flight. Do not let this
|
|
356
|
+
// older result put the dismissed rows back into the current state.
|
|
357
|
+
if (revision !== refreshRevision || siteKey !== dismissalKey()) return drawer.getState();
|
|
358
|
+
const latestActivity = newerActivity(
|
|
359
|
+
ordersResult.status === "fulfilled" ? ordersResult.value.latestActivity : "",
|
|
360
|
+
bookingsResult.status === "fulfilled" ? bookingsResult.value.latestActivity : "",
|
|
361
|
+
);
|
|
362
|
+
rememberObservedActivity(siteKey, latestActivity);
|
|
363
|
+
if (ordersResult.status === "fulfilled") rememberObservedOrders(siteKey, ordersResult.value.snapshot);
|
|
364
|
+
if (bookingsResult.status === "fulfilled") rememberObservedBookings(siteKey, bookingsResult.value.snapshot);
|
|
365
|
+
const attention: DrawerState["attention"] = [];
|
|
366
|
+
if (ordersResult.status === "fulfilled") {
|
|
367
|
+
const count = ordersResult.value.count;
|
|
368
|
+
if (count > 0) attention.push(attentionRow("att_orders", "Orders", "cart", "himi_owner_lite_orders", count));
|
|
369
|
+
}
|
|
370
|
+
if (bookingsResult.status === "fulfilled") {
|
|
371
|
+
const count = bookingsResult.value.count;
|
|
372
|
+
if (count > 0) attention.push(attentionRow("att_appts", "Appointments", "calendar", "himi_owner_lite_appointments", count));
|
|
373
|
+
}
|
|
374
|
+
// Other synchronous drawer actions (such as opening the clear-confirmation
|
|
375
|
+
// prompt) may run while the requests are in flight. Keep those latest fields
|
|
376
|
+
// when committing the refreshed data instead of restoring this action's snapshot.
|
|
377
|
+
const latest = drawer.getState();
|
|
71
378
|
return snap(withDerived({
|
|
72
|
-
...
|
|
379
|
+
...latest,
|
|
73
380
|
siteName: currentSiteName(),
|
|
381
|
+
attention,
|
|
74
382
|
activeScreenId: typeof screenId === "string" ? screenId : hostActiveScreen(),
|
|
75
383
|
}));
|
|
76
384
|
},
|
|
@@ -80,7 +388,11 @@ const drawer = defineService<DrawerState, {
|
|
|
80
388
|
})),
|
|
81
389
|
askClearAttention: (s) => snap(withDerived({ ...s, confirmingClear: true })),
|
|
82
390
|
cancelClearAttention: (s) => snap(withDerived({ ...s, confirmingClear: false })),
|
|
83
|
-
clearAttention: (s) =>
|
|
391
|
+
clearAttention: (s) => {
|
|
392
|
+
refreshRevision += 1;
|
|
393
|
+
rememberDismissal(dismissalKey());
|
|
394
|
+
return snap(withDerived({ ...s, attention: [], confirmingClear: false }));
|
|
395
|
+
},
|
|
84
396
|
},
|
|
85
397
|
});
|
|
86
398
|
|
|
@@ -10,6 +10,7 @@ import { countSites } from "@himalaya/wix-site-list";
|
|
|
10
10
|
import { searchOrders, type WixOrder } from "@himalaya/wix-ecom";
|
|
11
11
|
import { queryBookings } from "@himalaya/wix-bookings";
|
|
12
12
|
import { UPCOMING_BOOKING_STATUSES, upcomingBookingCount } from "../_shared/owner-bookings.js";
|
|
13
|
+
import { ownerOrderAttentionFilter } from "../_shared/owner-order-attention.js";
|
|
13
14
|
import { queryMembers } from "@himalaya/wix-members";
|
|
14
15
|
import { queryReviews } from "@himalaya/wix-reviews";
|
|
15
16
|
import { queryReservations } from "@himalaya/wix-table-reservations";
|
|
@@ -102,8 +103,6 @@ const UPCOMING_RESERVATION_STATUSES = ["HELD", "RESERVED", "REQUESTED"];
|
|
|
102
103
|
const UPCOMING_EVENT_STATUSES = ["UPCOMING", "SCHEDULED"];
|
|
103
104
|
const ATTENTION_MEMBER_STATUSES = ["PENDING"];
|
|
104
105
|
const ATTENTION_REVIEW_STATUSES = ["IN_MODERATION", "SUBMITTED"];
|
|
105
|
-
const ATTENTION_PAYMENT_STATUSES = ["NOT_PAID", "PARTIALLY_PAID"];
|
|
106
|
-
const ATTENTION_FULFILLMENT_STATUSES = ["NOT_FULFILLED", "PARTIALLY_FULFILLED"];
|
|
107
106
|
const SOURCE_LABELS: Record<HomeSourceId, string> = {
|
|
108
107
|
products: "Products", posts: "Posts", events: "Events", sites: "Sites",
|
|
109
108
|
orders: "Orders", bookings: "Bookings", members: "Members",
|
|
@@ -469,14 +468,7 @@ async function loadSourcePart(source: HomeSourceId, part: SourcePart): Promise<S
|
|
|
469
468
|
case "sites": return countSites({}) as Promise<SourcePayload>;
|
|
470
469
|
case "orders": {
|
|
471
470
|
if (part === "primary") return searchOrders({ search: {
|
|
472
|
-
filter:
|
|
473
|
-
archived: false,
|
|
474
|
-
status: { $nin: ["CANCELED", "CANCELLED"] },
|
|
475
|
-
$or: [
|
|
476
|
-
{ paymentStatus: { $in: ATTENTION_PAYMENT_STATUSES } },
|
|
477
|
-
{ paymentStatus: "PAID", fulfillmentStatus: { $in: ATTENTION_FULFILLMENT_STATUSES } },
|
|
478
|
-
],
|
|
479
|
-
},
|
|
471
|
+
filter: ownerOrderAttentionFilter(),
|
|
480
472
|
sort: [{ fieldName: "createdDate", order: "DESC" }, { fieldName: "id", order: "ASC" }],
|
|
481
473
|
cursorPaging: { limit: TODAY_PAGE },
|
|
482
474
|
} }) as Promise<SourcePayload>;
|
|
@@ -213,7 +213,10 @@ function copyFromParams(): SearchCopy {
|
|
|
213
213
|
allFailed: copyParam("ownerSearch.allFailed", DEFAULT_COPY.allFailed),
|
|
214
214
|
loadedOnly: copyParam("ownerSearch.loadedOnly", DEFAULT_COPY.loadedOnly),
|
|
215
215
|
loadingSummary: copyParam("ownerSearch.loading", DEFAULT_COPY.loadingSummary),
|
|
216
|
-
|
|
216
|
+
// Keep the pre-worker-heading copy key alive for existing copy packs. The
|
|
217
|
+
// newer `empty` key takes precedence when present; both flow through the
|
|
218
|
+
// query interpolator so neither can hide what the owner searched.
|
|
219
|
+
emptySummary: copyParam("ownerSearch.empty", copyParam("ownerSearch.emptyTitle", DEFAULT_COPY.emptySummary)),
|
|
217
220
|
idleSummary: copyParam("ownerSearch.idle", DEFAULT_COPY.idleSummary),
|
|
218
221
|
permission: copyParam("ownerSearch.permission", DEFAULT_COPY.permission),
|
|
219
222
|
setup: copyParam("ownerSearch.setup", DEFAULT_COPY.setup),
|
|
@@ -240,6 +243,7 @@ const IDLE: State = {
|
|
|
240
243
|
anyLoading: false,
|
|
241
244
|
showEmpty: false,
|
|
242
245
|
hideEmpty: true,
|
|
246
|
+
emptyTitle: "",
|
|
243
247
|
};
|
|
244
248
|
|
|
245
249
|
/** Rebuilt on `load`, because the allowlist arrives with the descriptor. */
|
|
@@ -106,6 +106,14 @@ export interface SearchItem {
|
|
|
106
106
|
/** Stable across refreshes, so a re-fetch cannot re-key a row under the
|
|
107
107
|
* user's finger: `h:<source>`, `r:<source>:<id>`, `f:<source>`. */
|
|
108
108
|
key: string;
|
|
109
|
+
/** The row's LIST identity — always equal to `key`, never blank.
|
|
110
|
+
*
|
|
111
|
+
* A bound list keys its rows on `id` (the web BoundList does, verbatim). This
|
|
112
|
+
* carried the record id, which left every heading and footer at `""` and let
|
|
113
|
+
* two domains that reuse a record id collide — so React reconciled headings
|
|
114
|
+
* onto each other as groups landed and painted "Contacts" three times.
|
|
115
|
+
* What a record OPENS is `recordId`. */
|
|
116
|
+
id: string;
|
|
109
117
|
/** The source this row belongs to — a retry or a pager addresses it. */
|
|
110
118
|
sourceId: string;
|
|
111
119
|
// Exactly one kind is true. Pre-negated, because a `hiddenWhen` takes one path.
|
|
@@ -122,7 +130,9 @@ export interface SearchItem {
|
|
|
122
130
|
isLoading: boolean;
|
|
123
131
|
notLoading: boolean;
|
|
124
132
|
// Record.
|
|
125
|
-
id
|
|
133
|
+
/** The record this row opens — the destination loads it by this id. Blank on
|
|
134
|
+
* headings and footers, which open nothing. */
|
|
135
|
+
recordId: string;
|
|
126
136
|
title: string;
|
|
127
137
|
subtitle: string;
|
|
128
138
|
statusLabel: string;
|
|
@@ -163,6 +173,10 @@ export interface SearchState {
|
|
|
163
173
|
* nobody has typed into yet, and again over every result still in flight. */
|
|
164
174
|
showEmpty: boolean;
|
|
165
175
|
hideEmpty: boolean;
|
|
176
|
+
/** The empty state's heading, naming the query ("No records match “mug”").
|
|
177
|
+
* Blank unless `showEmpty` — and while it shows, `summary` is blank, because
|
|
178
|
+
* the two sat one above the other saying the same sentence. */
|
|
179
|
+
emptyTitle: string;
|
|
166
180
|
}
|
|
167
181
|
|
|
168
182
|
export interface SearchCopy {
|
|
@@ -192,7 +206,9 @@ export const DEFAULT_COPY: SearchCopy = {
|
|
|
192
206
|
allFailed: "No area could be searched",
|
|
193
207
|
loadedOnly: "{n} in loaded records",
|
|
194
208
|
loadingSummary: "Searching…",
|
|
195
|
-
|
|
209
|
+
/** The empty state's heading. `{q}` is the query the owner typed, so the
|
|
210
|
+
* state says WHAT matched nothing rather than only that something did not. */
|
|
211
|
+
emptySummary: "No records match “{q}”",
|
|
196
212
|
idleSummary: "Search your business records",
|
|
197
213
|
permission: "You don’t have access to this area",
|
|
198
214
|
setup: "This area isn’t set up on this site",
|
|
@@ -231,6 +247,14 @@ function fill(template: string, n: number, token = "{n}"): string {
|
|
|
231
247
|
return template.includes(token) ? template.split(token).join(String(n)) : `${template} ${n}`;
|
|
232
248
|
}
|
|
233
249
|
|
|
250
|
+
/** `{q}` → the query. Custom copy stays query-specific even when it omits the
|
|
251
|
+
* placeholder, so the empty state always identifies what was searched. */
|
|
252
|
+
function fillQuery(template: string, query: string): string {
|
|
253
|
+
return template.includes("{q}")
|
|
254
|
+
? template.split("{q}").join(query)
|
|
255
|
+
: `${template}: “${query}”`;
|
|
256
|
+
}
|
|
257
|
+
|
|
234
258
|
function idleGroup(adapter: SearchAdapter): SearchGroup {
|
|
235
259
|
return decorate({
|
|
236
260
|
id: adapter.id,
|
|
@@ -273,9 +297,9 @@ function baseItem(sourceId: string): SearchItem {
|
|
|
273
297
|
isHeader: false, notHeader: true,
|
|
274
298
|
isRecord: false, notRecord: true,
|
|
275
299
|
isFooter: false, notFooter: true,
|
|
276
|
-
label: "", countLabel: "",
|
|
300
|
+
id: "", label: "", countLabel: "",
|
|
277
301
|
isLoading: false, notLoading: true,
|
|
278
|
-
|
|
302
|
+
recordId: "", title: "", subtitle: "", statusLabel: "", image: "", screenId: "",
|
|
279
303
|
error: "", hasError: false, noError: true,
|
|
280
304
|
canLoadMore: false, cannotLoadMore: true, isLoadingMore: false,
|
|
281
305
|
};
|
|
@@ -302,7 +326,7 @@ export function projectItems(groups: SearchGroup[]): SearchItem[] {
|
|
|
302
326
|
if (silent) continue;
|
|
303
327
|
items.push({
|
|
304
328
|
...baseItem(group.id),
|
|
305
|
-
key: `h:${group.id}`,
|
|
329
|
+
key: `h:${group.id}`, id: `h:${group.id}`,
|
|
306
330
|
isHeader: true, notHeader: false,
|
|
307
331
|
label: group.label,
|
|
308
332
|
// A count is only shown once there is something to count: "0" under a
|
|
@@ -313,9 +337,9 @@ export function projectItems(groups: SearchGroup[]): SearchItem[] {
|
|
|
313
337
|
for (const row of group.rows) {
|
|
314
338
|
items.push({
|
|
315
339
|
...baseItem(group.id),
|
|
316
|
-
key: `r:${group.id}:${row.id}`,
|
|
340
|
+
key: `r:${group.id}:${row.id}`, id: `r:${group.id}:${row.id}`,
|
|
317
341
|
isRecord: true, notRecord: false,
|
|
318
|
-
|
|
342
|
+
recordId: row.id, title: row.title, subtitle: row.subtitle,
|
|
319
343
|
statusLabel: row.statusLabel, image: row.image, screenId: row.screenId,
|
|
320
344
|
});
|
|
321
345
|
}
|
|
@@ -325,7 +349,7 @@ export function projectItems(groups: SearchGroup[]): SearchItem[] {
|
|
|
325
349
|
if (group.hasError || group.problem !== "" || group.canLoadMore) {
|
|
326
350
|
items.push({
|
|
327
351
|
...baseItem(group.id),
|
|
328
|
-
key: `f:${group.id}`,
|
|
352
|
+
key: `f:${group.id}`, id: `f:${group.id}`,
|
|
329
353
|
isFooter: true, notFooter: false,
|
|
330
354
|
error: group.error,
|
|
331
355
|
hasError: group.error !== "", noError: group.error === "",
|
|
@@ -393,7 +417,9 @@ export function createGroupedSearch(
|
|
|
393
417
|
: anyLoading && total === 0
|
|
394
418
|
? copy.loadingSummary
|
|
395
419
|
: total === 0
|
|
396
|
-
|
|
420
|
+
// Empty and answered: the empty block carries the sentence, so the
|
|
421
|
+
// summary line above it steps aside rather than repeating it.
|
|
422
|
+
? (anyAnswered ? "" : copy.allFailed)
|
|
397
423
|
: total === 1
|
|
398
424
|
? copy.resultsOne
|
|
399
425
|
: areas === 1
|
|
@@ -416,6 +442,7 @@ export function createGroupedSearch(
|
|
|
416
442
|
anyLoading,
|
|
417
443
|
showEmpty: emptyAndAnswered,
|
|
418
444
|
hideEmpty: !emptyAndAnswered,
|
|
445
|
+
emptyTitle: emptyAndAnswered ? fillQuery(copy.emptySummary, query) : "",
|
|
419
446
|
};
|
|
420
447
|
};
|
|
421
448
|
|
|
@@ -52966,7 +52966,7 @@ const SCREENS = runtimeSetting({
|
|
|
52966
52966
|
"contentType": "application/javascript+ts-bundle",
|
|
52967
52967
|
"declaredCapabilities": [
|
|
52968
52968
|
"wix_api",
|
|
52969
|
-
"
|
|
52969
|
+
"wix_platform"
|
|
52970
52970
|
],
|
|
52971
52971
|
"minBinaryVersion": "0.1.0",
|
|
52972
52972
|
"bundleVersion": "0.0.1"
|
|
@@ -53051,7 +53051,8 @@ const SCREENS = runtimeSetting({
|
|
|
53051
53051
|
"props": {
|
|
53052
53052
|
"text": "$service:himiOwnerLiteSearch.summary",
|
|
53053
53053
|
"style": "caption",
|
|
53054
|
-
"color": "textSecondary"
|
|
53054
|
+
"color": "textSecondary",
|
|
53055
|
+
"collapseWhenEmpty": true
|
|
53055
53056
|
}
|
|
53056
53057
|
}
|
|
53057
53058
|
},
|
|
@@ -53166,7 +53167,7 @@ const SCREENS = runtimeSetting({
|
|
|
53166
53167
|
"actionId": "himi.open",
|
|
53167
53168
|
"params": {
|
|
53168
53169
|
"screenId": "$item.screenId",
|
|
53169
|
-
"id": "$item.
|
|
53170
|
+
"id": "$item.recordId"
|
|
53170
53171
|
}
|
|
53171
53172
|
}
|
|
53172
53173
|
}
|
|
@@ -53272,8 +53273,9 @@ const SCREENS = runtimeSetting({
|
|
|
53272
53273
|
"package": "himalaya",
|
|
53273
53274
|
"type": "Text",
|
|
53274
53275
|
"props": {
|
|
53275
|
-
"text": "
|
|
53276
|
-
"style": "bodyEmphasis"
|
|
53276
|
+
"text": "$service:himiOwnerLiteSearch.emptyTitle",
|
|
53277
|
+
"style": "bodyEmphasis",
|
|
53278
|
+
"maxLines": 2
|
|
53277
53279
|
}
|
|
53278
53280
|
}
|
|
53279
53281
|
},
|