@wix/himalaya-cli 0.1252.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.
Files changed (97) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/base44/src/index.d.ts +3 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +1 -1
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/base44.mjs +4 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -1
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -1
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -1
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -1
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -1
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +1 -1
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -1
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -1
  13. package/dist/cli.mjs +22 -22
  14. package/dist/preview-assets.generated.bin +0 -0
  15. package/guides/base44-app-to-himi-app.md +30 -8
  16. package/guides/getting-started.md +8 -0
  17. package/guides/himi-skill.md +8 -0
  18. package/guides/sdk-reference.json +7 -1
  19. package/guides/skill.md +2 -1
  20. package/package.json +2 -2
  21. package/templates/beauty-salon-owner/dev/index.ts +7 -5
  22. package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  23. package/templates/beauty-salon-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  24. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  25. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  26. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  27. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  28. package/templates/community-club-owner/dev/index.ts +7 -5
  29. package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  30. package/templates/community-club-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  31. package/templates/community-club-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  32. package/templates/community-club-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  33. package/templates/community-club-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  34. package/templates/community-club-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  35. package/templates/events-meetups-owner/dev/index.ts +7 -5
  36. package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  37. package/templates/events-meetups-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  38. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  39. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  40. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  41. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  42. package/templates/gym-fitness-owner/dev/index.ts +7 -5
  43. package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  44. package/templates/gym-fitness-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  45. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  46. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  47. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  48. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  49. package/templates/nonprofit-cause-owner/dev/index.ts +7 -5
  50. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  51. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  52. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  53. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  54. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  55. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  56. package/templates/owner-lite/dev/index.ts +7 -5
  57. package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +78 -7
  58. package/templates/owner-lite/tier5-src/_shared/owner-order-attention.ts +18 -0
  59. package/templates/owner-lite/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  60. package/templates/owner-lite/tier5-src/himi-owner-lite-home/index.ts +2 -10
  61. package/templates/owner-lite/tier5-src/himi-owner-lite-search/index.ts +5 -1
  62. package/templates/owner-lite/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  63. package/templates/podcast-creator-owner/dev/index.ts +7 -5
  64. package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  65. package/templates/podcast-creator-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  66. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  67. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  68. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  69. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  70. package/templates/portfolio-creative-owner/dev/index.ts +7 -5
  71. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  72. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  73. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  74. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  75. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  76. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  77. package/templates/restaurant-owner/dev/index.ts +7 -5
  78. package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  79. package/templates/restaurant-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  80. package/templates/restaurant-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  81. package/templates/restaurant-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  82. package/templates/restaurant-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  83. package/templates/restaurant-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  84. package/templates/retail-store-owner/dev/index.ts +7 -5
  85. package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  86. package/templates/retail-store-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  87. package/templates/retail-store-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  88. package/templates/retail-store-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  89. package/templates/retail-store-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  90. package/templates/retail-store-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
  91. package/templates/yoga-studio-owner/dev/index.ts +7 -5
  92. package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +78 -7
  93. package/templates/yoga-studio-owner/tier5-src/_shared/owner-order-attention.ts +18 -0
  94. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-drawer/index.ts +321 -9
  95. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-home/index.ts +2 -10
  96. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-search/index.ts +5 -1
  97. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-search/logic.ts +36 -9
@@ -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
- emptySummary: copyParam("ownerSearch.empty", DEFAULT_COPY.emptySummary),
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: string;
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
- emptySummary: "No records matched",
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
- id: "", title: "", subtitle: "", statusLabel: "", image: "", screenId: "",
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
- id: row.id, title: row.title, subtitle: row.subtitle,
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
- ? (anyAnswered ? copy.emptySummary : copy.allFailed)
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
- "local_database"
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.id"
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": "No records matched",
53276
- "style": "bodyEmphasis"
53276
+ "text": "$service:himiOwnerLiteSearch.emptyTitle",
53277
+ "style": "bodyEmphasis",
53278
+ "maxLines": 2
53277
53279
  }
53278
53280
  }
53279
53281
  },
@@ -49,6 +49,76 @@ function siteZone(): Promise<string | undefined> {
49
49
  return zoneLookup;
50
50
  }
51
51
 
52
+ /** A subscription order stores its buyer as an internal member/contact id. Resolve the
53
+ * display name so the owner can search subscriptions by the same person name shown in
54
+ * the dedicated subscriptions screen. Fetch member ids directly, and page the member
55
+ * directory only for contact ids. This is best-effort: missing member access must not
56
+ * hide otherwise readable plan orders. */
57
+ async function subscriptionMemberNames(orders: unknown[]): Promise<Map<string, string>> {
58
+ const memberIds = [...new Set(orders.map(order => field(order, 'buyer.memberId')).filter(Boolean))];
59
+ const unresolvedContacts = new Set(orders.map(order => field(order, 'buyer.contactId')).filter(Boolean));
60
+ const names = new Map<string, string>();
61
+ const remember = (member: unknown) => {
62
+ const emails = at(member, 'contact.emails');
63
+ const email = Array.isArray(emails) ? scalar(emails[0]) : '';
64
+ const displayName = name(member, 'contact.firstName', 'contact.lastName')
65
+ || field(member, 'profile.nickname') || email || field(member, 'loginEmail');
66
+ if (!displayName) return;
67
+ const id = field(member, 'id'), contactId = field(member, 'contactId');
68
+ if (id) names.set(id, displayName);
69
+ if (contactId) {
70
+ names.set(contactId, displayName);
71
+ unresolvedContacts.delete(contactId);
72
+ }
73
+ };
74
+
75
+ // The first directory page is useful for buyers already in view. For a buyer outside
76
+ // that page, fetch the known member ID directly instead of walking the whole directory.
77
+ const pageSize = 100;
78
+ let offset = 0;
79
+ let canPageDirectory = false;
80
+ try {
81
+ const response = await queryMembers({ query: { paging: { limit: pageSize, offset } } });
82
+ const members = response.members ?? [];
83
+ for (const member of members) remember(member);
84
+ offset += members.length;
85
+ const total = response.metadata?.total;
86
+ canPageDirectory = members.length > 0
87
+ && (typeof total === 'number' ? offset < total : members.length === pageSize);
88
+ } catch {
89
+ // Member details below can still work when the directory query is unavailable.
90
+ }
91
+
92
+ const unresolvedMembers = memberIds.filter(id => !names.has(id));
93
+ for (let start = 0; start < unresolvedMembers.length; start += 10) {
94
+ const members = await Promise.all(unresolvedMembers.slice(start, start + 10).map(async id => {
95
+ try { return (await getMember(id)).member; } catch { return undefined; }
96
+ }));
97
+ for (const member of members) if (member) remember(member);
98
+ }
99
+
100
+ if (!unresolvedContacts.size || !canPageDirectory) return names;
101
+ try {
102
+ while (unresolvedContacts.size) {
103
+ const response = await queryMembers({ query: { paging: { limit: pageSize, offset } } });
104
+ const members = response.members ?? [];
105
+ for (const member of members) remember(member);
106
+ if (!members.length) break;
107
+ offset += members.length;
108
+ const total = response.metadata?.total;
109
+ if (typeof total === 'number' ? offset >= total : members.length < pageSize) break;
110
+ }
111
+ } catch {
112
+ // A later page can fail after earlier pages already resolved some buyers.
113
+ // Keep those names; this is best-effort context, never a reason to hide an order.
114
+ }
115
+ return names;
116
+ }
117
+
118
+ function subscriptionBuyerName(order: unknown, names: Map<string, string>): string {
119
+ return names.get(field(order, 'buyer.memberId')) || names.get(field(order, 'buyer.contactId')) || '';
120
+ }
121
+
52
122
  /** A Wix date value as an instant. A calendar date with no time ("2026-11-07") is a DAY, not an
53
123
  * instant: it is built at local midnight and formatted with no zone, so it never shifts across
54
124
  * one — including on engines without Intl, where a zone could not be applied anyway. */
@@ -99,7 +169,8 @@ export async function queryOwnerModule(module: OwnerModuleId, cursor = ''): Prom
99
169
  // The zone lookup runs beside the query, not after it: the two are independent, and serial
100
170
  // they would double the time to a first paint.
101
171
  const [raw, zone] = await Promise.all([queryRawOwnerModule(module, cursor), siteZone()]);
102
- return { ...raw, items: raw.items.map(row => ownerRecord(module, row, zone)) };
172
+ const subscribers = module === 'subscriptions' ? await subscriptionMemberNames(raw.items) : new Map<string, string>();
173
+ return { ...raw, items: raw.items.map(row => ownerRecord(module, row, zone, subscriptionBuyerName(row, subscribers))) };
103
174
  }
104
175
 
105
176
  async function queryRawOwnerModule(module: OwnerModuleId, cursor: string): Promise<CollectionPage<unknown>> {
@@ -171,7 +242,7 @@ async function queryRawOwnerModule(module: OwnerModuleId, cursor: string): Promi
171
242
  }
172
243
  }
173
244
 
174
- export function ownerRecord(module: OwnerModuleId, raw: unknown, zone?: string): OwnerRecord {
245
+ export function ownerRecord(module: OwnerModuleId, raw: unknown, zone?: string, subscriberName = ''): OwnerRecord {
175
246
  const when = (path: string, style?: DateStyle) => ownerDate(field(raw, path), zone, style);
176
247
  const id = field(raw, 'id');
177
248
  if (!id) throw new Error('Wix returned a record without an identity. Refresh this view.');
@@ -239,14 +310,13 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown, zone?: string):
239
310
  details = [['Price', join(field(raw, 'pricing.price.value'), field(raw, 'pricing.price.currency'))], ['Free trial days', field(raw, 'pricing.freeTrialDays')]];
240
311
  break;
241
312
  case 'subscriptions':
242
- // The buyer is only a member UUID here — not something an owner can read — so the
243
- // row says when the plan started instead of echoing an internal id.
244
313
  title = field(raw, 'planName'); statusLabel = words(field(raw, 'status'));
245
314
  {
246
315
  const started = when('startDate', 'short');
247
- subtitle = started ? copyTemplate('ownerModules.startedOn', 'Started {date}', { date: started }) : '';
316
+ const startedLabel = started ? copyTemplate('ownerModules.startedOn', 'Started {date}', { date: started }) : '';
317
+ subtitle = join(subscriberName, startedLabel);
248
318
  }
249
- details = [['Started', when('startDate')], ['Ends', when('endDate')], ['Payment', words(field(raw, 'lastPaymentStatus'))]];
319
+ details = [['Subscriber', subscriberName], ['Started', when('startDate')], ['Ends', when('endDate')], ['Payment', words(field(raw, 'lastPaymentStatus'))]];
250
320
  break;
251
321
  case 'programs':
252
322
  title = field(raw, 'description.title') || field(raw, 'details.title') || field(raw, 'name');
@@ -344,7 +414,8 @@ export async function getOwnerRecord(module: OwnerModuleId, id: string): Promise
344
414
  }
345
415
  case 'donationCampaigns': raw = (await getDonationCampaign(id)).donationCampaign; break;
346
416
  }
347
- const record = ownerRecord(module, raw, await zone);
417
+ const subscribers = module === 'subscriptions' ? await subscriptionMemberNames([raw]) : new Map<string, string>();
418
+ const record = ownerRecord(module, raw, await zone, subscriptionBuyerName(raw, subscribers));
348
419
  if (record.id !== id) throw new Error('The returned record does not match this link. Refresh the collection.');
349
420
  return record;
350
421
  }
@@ -0,0 +1,18 @@
1
+ /** The Home dashboard's canonical Orders attention filter. Keep drawer reminders on
2
+ * the same definition so the two surfaces never disagree about what needs review. */
3
+ export function ownerOrderAttentionFilter(updatedAfter?: string): Record<string, unknown> {
4
+ const filter: Record<string, unknown> = {
5
+ archived: false,
6
+ status: { $nin: ["CANCELED", "CANCELLED"] },
7
+ $or: [
8
+ { paymentStatus: { $in: ["NOT_PAID", "PARTIALLY_PAID"] } },
9
+ { paymentStatus: "PAID", fulfillmentStatus: { $in: ["NOT_FULFILLED", "PARTIALLY_FULFILLED"] } },
10
+ ],
11
+ };
12
+ return updatedAfter
13
+ ? { $and: [filter, { $or: [
14
+ { createdDate: { $gt: updatedAfter } },
15
+ { updatedDate: { $gt: updatedAfter } },
16
+ ] }] }
17
+ : filter;
18
+ }