@wix/himalaya-cli 0.1497.0 → 0.1499.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/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/display.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/index.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores-collections.mjs +2 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +3 -2
- package/dist/cli.mjs +30 -30
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +8 -2
- package/guides/trim-a-template.md +1 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +2465 -2035
- package/templates/beauty-salon/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/beauty-salon-owner/dev/index.ts +571 -0
- package/templates/beauty-salon-owner/himalaya.content.json +6 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/branded-lite/dev/index.ts +2989 -2197
- package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +46 -8
- package/templates/branded-lite/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/branded-lite/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/branded-lite/tier5-src/business-chat/index.ts +3 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/community-club/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/community-club/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/community-club-owner/dev/index.ts +571 -0
- package/templates/community-club-owner/himalaya.content.json +6 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/community-club-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/events-meetups/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/events-meetups-owner/dev/index.ts +571 -0
- package/templates/events-meetups-owner/himalaya.content.json +6 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/gym-fitness/dev/index.ts +2600 -2170
- package/templates/gym-fitness/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/gym-fitness-owner/dev/index.ts +571 -0
- package/templates/gym-fitness-owner/himalaya.content.json +6 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/nonprofit-cause/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/nonprofit-cause-owner/dev/index.ts +571 -0
- package/templates/nonprofit-cause-owner/himalaya.content.json +6 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/owner-lite/dev/index.ts +571 -0
- package/templates/owner-lite/himalaya.content.json +6 -0
- package/templates/owner-lite/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/owner-lite/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/owner-lite/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/podcast-creator/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/podcast-creator-owner/dev/index.ts +571 -0
- package/templates/podcast-creator-owner/himalaya.content.json +6 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/portfolio-creative/dev/index.ts +2470 -2040
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/portfolio-creative-owner/dev/index.ts +571 -0
- package/templates/portfolio-creative-owner/himalaya.content.json +6 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/restaurant/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/restaurant-owner/dev/index.ts +571 -0
- package/templates/restaurant-owner/himalaya.content.json +6 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/retail-store/dev/index.ts +2583 -2137
- package/templates/retail-store/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/retail-store/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/retail-store/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/retail-store-owner/dev/index.ts +571 -0
- package/templates/retail-store-owner/himalaya.content.json +6 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/yoga-studio-owner/dev/index.ts +571 -0
- package/templates/yoga-studio-owner/himalaya.content.json +6 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/beauty-salon/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/branded-lite/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/branded-lite/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/gym-fitness/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/gym-fitness/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/portfolio-creative/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/retail-store/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/retail-store/tier5-src/himi-store-shop/index.ts +0 -173
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// The owner's Today ROLLUP — one glance over every business, for the Home Screen widget that
|
|
2
|
+
// summarises them all (himi-owner-lite-today-rollup). Pure: the worker reads, this shapes.
|
|
3
|
+
//
|
|
4
|
+
// What an owner with several businesses wants from the Home Screen is not each business in turn
|
|
5
|
+
// but the day as a whole — how many bookings, how many orders waiting, what came in — and which
|
|
6
|
+
// business is carrying it. So the frame is totals across the businesses that were read, plus the
|
|
7
|
+
// busiest one by name.
|
|
8
|
+
//
|
|
9
|
+
// A business that could not be read is LEFT OUT of the totals and said so ("1 business not
|
|
10
|
+
// counted"), never counted as zero: a total that silently drops a business reads as a quiet day.
|
|
11
|
+
// Only when no business could be read is the frame an error, which the phone does not publish —
|
|
12
|
+
// the last good frame stays.
|
|
13
|
+
|
|
14
|
+
import { formatCurrency } from "./engine-intl.js";
|
|
15
|
+
|
|
16
|
+
type Rec = Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
/** A glance, not a list: a count at the cap reads as "50+". Per business and per read. */
|
|
19
|
+
export const ROLLUP_LIMIT = 50;
|
|
20
|
+
/** Bounds the fan-out: three reads per business, concurrently. An owner past it sees a note. */
|
|
21
|
+
export const ROLLUP_MAX_SITES = 25;
|
|
22
|
+
|
|
23
|
+
export const ROLLUP_COPY = {
|
|
24
|
+
signedOut: "Sign in to Owner to see today",
|
|
25
|
+
error: "Open Owner to refresh",
|
|
26
|
+
noSites: "No businesses on this account yet",
|
|
27
|
+
nothing: "Open Owner to see your businesses",
|
|
28
|
+
quiet: "All quiet across your businesses",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
/** One vertical's read for one business: its rows, "absent" when the vertical cannot exist for
|
|
32
|
+
* this owner (not installed, no permission, not in this build), or null when the read failed. */
|
|
33
|
+
export type VerticalRead = Rec[] | "absent" | null;
|
|
34
|
+
|
|
35
|
+
export interface SiteReads {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
/** CONFIRMED bookings starting today (local day). */
|
|
39
|
+
bookings: VerticalRead;
|
|
40
|
+
/** Orders needing attention (the Home dashboard's filter). */
|
|
41
|
+
orders: VerticalRead;
|
|
42
|
+
/** PAID orders created today — the revenue so far. */
|
|
43
|
+
sales: VerticalRead;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RollupSite {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
bookings: number;
|
|
50
|
+
orders: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RollupState {
|
|
54
|
+
status: "loading" | "ready" | "error" | "signedOut";
|
|
55
|
+
/** Businesses the host (or the account) listed, after the cap. */
|
|
56
|
+
siteCount: number;
|
|
57
|
+
/** Of those, the ones whose reads all answered. Totals are over these only. */
|
|
58
|
+
countedCount: number;
|
|
59
|
+
/** "Today · 3 businesses", or "Today · <name>" for an owner with one. */
|
|
60
|
+
header: string;
|
|
61
|
+
bookingsValue: string;
|
|
62
|
+
/** "bookings today" / "booking today". */
|
|
63
|
+
bookingsLabel: string;
|
|
64
|
+
ordersValue: string;
|
|
65
|
+
/** "orders to handle" / "order to handle". */
|
|
66
|
+
ordersLabel: string;
|
|
67
|
+
/** "$1,240.50", "$1,240.50+" at a read cap, "—" with no sales or sales in more than one currency. */
|
|
68
|
+
revenueValue: string;
|
|
69
|
+
/** "revenue so far", "no sales yet", or "revenue in 2 currencies". */
|
|
70
|
+
revenueLabel: string;
|
|
71
|
+
/** The small widget's labels, short enough for a 2×2 at body size: "bookings", "to handle",
|
|
72
|
+
* "so far" / "no sales yet" / "2 currencies". */
|
|
73
|
+
bookingsShort: string;
|
|
74
|
+
ordersShort: string;
|
|
75
|
+
revenueShort: string;
|
|
76
|
+
hideBookings: boolean;
|
|
77
|
+
hideOrders: boolean;
|
|
78
|
+
hideRevenue: boolean;
|
|
79
|
+
/** "Busiest: Riverbend Studio", the quiet line when nothing is happening, "" for one business. */
|
|
80
|
+
busiestLine: string;
|
|
81
|
+
hideBusiest: boolean;
|
|
82
|
+
/** The small widget's two-line form of it: "Busiest" over the name, or "All quiet" alone. */
|
|
83
|
+
busiestCaption: string;
|
|
84
|
+
busiestName: string;
|
|
85
|
+
hideBusiestName: boolean;
|
|
86
|
+
/** Compact counts for the small widget: "12 bookings · 5 orders". */
|
|
87
|
+
countsShort: string;
|
|
88
|
+
/** "1 business not counted", "Showing 25 of 30 businesses", or "". */
|
|
89
|
+
note: string;
|
|
90
|
+
hideNote: boolean;
|
|
91
|
+
/** One-line state copy for a degrade or an owner with no businesses. Hides the stats. */
|
|
92
|
+
message: string;
|
|
93
|
+
hideMessage: boolean;
|
|
94
|
+
/** True when `message` replaces the stats. */
|
|
95
|
+
hideStats: boolean;
|
|
96
|
+
/** Epoch SECONDS: the next local midnight, when "today" stops being true. */
|
|
97
|
+
validUntil: number;
|
|
98
|
+
sites: RollupSite[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface Verticals {
|
|
102
|
+
bookings: boolean;
|
|
103
|
+
store: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function capped(n: number, atCap: boolean): string {
|
|
107
|
+
return atCap ? `${ROLLUP_LIMIT}+` : String(n);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function plural(n: number, one: string, many: string): string {
|
|
111
|
+
return n === 1 ? one : many;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function rows(read: VerticalRead): Rec[] {
|
|
115
|
+
return Array.isArray(read) ? read : [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function str(r: unknown, path: string): string {
|
|
119
|
+
let v: unknown = r;
|
|
120
|
+
for (const k of path.split(".")) v = v && typeof v === "object" ? (v as Rec)[k] : undefined;
|
|
121
|
+
return typeof v === "string" ? v : typeof v === "number" ? String(v) : "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sameLocalDay(a: Date, b: Date): boolean {
|
|
125
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Local midnight at the start of `now`'s day, and the next one. */
|
|
129
|
+
export function dayBounds(now: Date): { start: Date; end: Date } {
|
|
130
|
+
return {
|
|
131
|
+
start: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
|
|
132
|
+
end: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Re-filtered even though the queries filter: a server (or a fixture) that returns a cancelled
|
|
137
|
+
// booking, yesterday's order or an unpaid sale must not reach a number the owner reads as fact.
|
|
138
|
+
function bookingsToday(read: VerticalRead, now: Date): Rec[] {
|
|
139
|
+
return rows(read).filter((b) => str(b, "id") && str(b, "status").toUpperCase() === "CONFIRMED"
|
|
140
|
+
&& sameLocalDay(new Date(str(b, "startDate") || str(b, "bookedEntity.slot.startDate")), now));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ordersToHandle(read: VerticalRead): Rec[] {
|
|
144
|
+
return rows(read).filter((o) => {
|
|
145
|
+
if (!str(o, "id") || ["CANCELED", "CANCELLED"].includes(str(o, "status").toUpperCase())) return false;
|
|
146
|
+
const payment = str(o, "paymentStatus").toUpperCase();
|
|
147
|
+
const fulfillment = str(o, "fulfillmentStatus").toUpperCase();
|
|
148
|
+
return payment === "NOT_PAID" || payment === "PARTIALLY_PAID"
|
|
149
|
+
|| (payment === "PAID" && (fulfillment === "NOT_FULFILLED" || fulfillment === "PARTIALLY_FULFILLED"));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function salesToday(read: VerticalRead, now: Date): Rec[] {
|
|
154
|
+
return rows(read).filter((o) => str(o, "id") && str(o, "paymentStatus").toUpperCase() === "PAID"
|
|
155
|
+
&& !["CANCELED", "CANCELLED"].includes(str(o, "status").toUpperCase())
|
|
156
|
+
&& sameLocalDay(new Date(str(o, "createdDate")), now));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A sale's total and currency. Summed as numbers and rounded once, when formatted. A total the
|
|
160
|
+
* API did not send is skipped, never read as 0. */
|
|
161
|
+
function saleTotal(o: Rec): { amount: number; currency: string } | null {
|
|
162
|
+
const amount = Number(str(o, "priceSummary.total.amount"));
|
|
163
|
+
const currency = (str(o, "currency") || str(o, "priceSummary.total.currency")).trim().toUpperCase();
|
|
164
|
+
return Number.isFinite(amount) && str(o, "priceSummary.total.amount") !== "" && currency ? { amount, currency } : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Pure: the frame for what was read across the businesses. `now` is injectable for tests. */
|
|
168
|
+
export function shapeRollup(reads: SiteReads[], now: Date, verticals: Verticals, listed = reads.length): RollupState {
|
|
169
|
+
const validUntil = Math.floor(dayBounds(now).end.getTime() / 1000);
|
|
170
|
+
const base = { siteCount: reads.length, validUntil };
|
|
171
|
+
if (reads.length === 0) return message("ready", ROLLUP_COPY.noSites, base);
|
|
172
|
+
|
|
173
|
+
// A business is counted only when every read it needs answered (rows or "absent").
|
|
174
|
+
const counted = reads.filter((r) => r.bookings !== null && r.orders !== null && r.sales !== null);
|
|
175
|
+
if (counted.length === 0) return message("error", ROLLUP_COPY.error, base);
|
|
176
|
+
|
|
177
|
+
// Said on every frame that has businesses, the one-line ones included: a frame that drops a
|
|
178
|
+
// business, or stops at the cap, must say so whatever else it shows.
|
|
179
|
+
const skipped = reads.length - counted.length;
|
|
180
|
+
const note = skipped > 0
|
|
181
|
+
? `${skipped} ${plural(skipped, "business", "businesses")} not counted`
|
|
182
|
+
: listed > reads.length ? `Showing ${reads.length} of ${listed} businesses` : "";
|
|
183
|
+
|
|
184
|
+
const hasBookings = verticals.bookings && counted.some((r) => r.bookings !== "absent");
|
|
185
|
+
const hasStore = verticals.store && counted.some((r) => r.orders !== "absent");
|
|
186
|
+
if (!hasBookings && !hasStore) return message("ready", ROLLUP_COPY.nothing, { ...base, countedCount: counted.length, note });
|
|
187
|
+
|
|
188
|
+
let bookingsCapped = false;
|
|
189
|
+
let ordersCapped = false;
|
|
190
|
+
let salesCapped = false;
|
|
191
|
+
const revenue = new Map<string, number>();
|
|
192
|
+
const sites: RollupSite[] = counted.map((r) => {
|
|
193
|
+
const b = bookingsToday(r.bookings, now);
|
|
194
|
+
const o = ordersToHandle(r.orders);
|
|
195
|
+
const sold = salesToday(r.sales, now);
|
|
196
|
+
bookingsCapped ||= rows(r.bookings).length >= ROLLUP_LIMIT;
|
|
197
|
+
ordersCapped ||= rows(r.orders).length >= ROLLUP_LIMIT;
|
|
198
|
+
salesCapped ||= rows(r.sales).length >= ROLLUP_LIMIT;
|
|
199
|
+
for (const sale of sold) {
|
|
200
|
+
const t = saleTotal(sale);
|
|
201
|
+
if (t) revenue.set(t.currency, (revenue.get(t.currency) ?? 0) + t.amount);
|
|
202
|
+
}
|
|
203
|
+
return { id: r.id, name: r.name, bookings: b.length, orders: o.length };
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const bookings = sites.reduce((n, s) => n + s.bookings, 0);
|
|
207
|
+
const orders = sites.reduce((n, s) => n + s.orders, 0);
|
|
208
|
+
const currencies = [...revenue.keys()];
|
|
209
|
+
let revenueValue = "—";
|
|
210
|
+
let revenueLabel = "revenue so far";
|
|
211
|
+
let revenueShort = "so far";
|
|
212
|
+
if (currencies.length === 1) {
|
|
213
|
+
revenueValue = `${formatCurrency(revenue.get(currencies[0]) ?? 0, currencies[0])}${salesCapped ? "+" : ""}`;
|
|
214
|
+
} else if (currencies.length === 0) {
|
|
215
|
+
revenueLabel = "no sales yet";
|
|
216
|
+
revenueShort = "no sales yet";
|
|
217
|
+
} else {
|
|
218
|
+
// Adding dollars to shekels is not a number anyone can use; say what the day holds instead.
|
|
219
|
+
revenueLabel = `revenue in ${currencies.length} currencies`;
|
|
220
|
+
revenueShort = `${currencies.length} currencies`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// The busiest business is the one carrying the most of today. Ties go to the host's order (the
|
|
224
|
+
// order the owner sees their businesses in), so the name does not flicker between refreshes.
|
|
225
|
+
const busiest = sites.reduce<RollupSite | undefined>((best, s) =>
|
|
226
|
+
!best || s.bookings + s.orders > best.bookings + best.orders ? s : best, undefined);
|
|
227
|
+
const quiet = bookings === 0 && orders === 0;
|
|
228
|
+
const busiestLine = reads.length === 1 ? "" : quiet ? ROLLUP_COPY.quiet : `Busiest: ${busiest?.name ?? ""}`;
|
|
229
|
+
const busiestName = reads.length === 1 || quiet ? "" : busiest?.name ?? "";
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
const shortParts = [
|
|
233
|
+
...(hasBookings ? [`${capped(bookings, bookingsCapped)} ${plural(bookings, "booking", "bookings")}`] : []),
|
|
234
|
+
...(hasStore ? [`${capped(orders, ordersCapped)} ${plural(orders, "order", "orders")}`] : []),
|
|
235
|
+
];
|
|
236
|
+
return {
|
|
237
|
+
status: "ready",
|
|
238
|
+
...base,
|
|
239
|
+
countedCount: counted.length,
|
|
240
|
+
header: reads.length === 1 ? `Today · ${reads[0].name}` : `Today · ${reads.length} businesses`,
|
|
241
|
+
bookingsValue: capped(bookings, bookingsCapped),
|
|
242
|
+
bookingsLabel: `${plural(bookings, "booking", "bookings")} today`,
|
|
243
|
+
ordersValue: capped(orders, ordersCapped),
|
|
244
|
+
ordersLabel: `${plural(orders, "order", "orders")} to handle`,
|
|
245
|
+
revenueValue,
|
|
246
|
+
revenueLabel,
|
|
247
|
+
bookingsShort: plural(bookings, "booking", "bookings"),
|
|
248
|
+
ordersShort: "to handle",
|
|
249
|
+
revenueShort,
|
|
250
|
+
hideBookings: !hasBookings,
|
|
251
|
+
hideOrders: !hasStore,
|
|
252
|
+
hideRevenue: !hasStore,
|
|
253
|
+
busiestLine,
|
|
254
|
+
hideBusiest: !busiestLine,
|
|
255
|
+
busiestCaption: !busiestLine ? "" : quiet ? "All quiet" : "Busiest",
|
|
256
|
+
busiestName,
|
|
257
|
+
hideBusiestName: !busiestName,
|
|
258
|
+
countsShort: shortParts.join(" · "),
|
|
259
|
+
note,
|
|
260
|
+
hideNote: !note,
|
|
261
|
+
message: "",
|
|
262
|
+
hideMessage: true,
|
|
263
|
+
hideStats: false,
|
|
264
|
+
sites,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A one-line frame: no stats, no business names (after a sign-out they may be someone else's). */
|
|
269
|
+
export function message(
|
|
270
|
+
status: RollupState["status"],
|
|
271
|
+
text: string,
|
|
272
|
+
base: { siteCount?: number; countedCount?: number; validUntil?: number; note?: string } = {},
|
|
273
|
+
): RollupState {
|
|
274
|
+
return {
|
|
275
|
+
status,
|
|
276
|
+
siteCount: base.siteCount ?? 0,
|
|
277
|
+
countedCount: base.countedCount ?? 0,
|
|
278
|
+
header: "Today",
|
|
279
|
+
bookingsValue: "",
|
|
280
|
+
bookingsLabel: "",
|
|
281
|
+
ordersValue: "",
|
|
282
|
+
ordersLabel: "",
|
|
283
|
+
revenueValue: "",
|
|
284
|
+
revenueLabel: "",
|
|
285
|
+
bookingsShort: "",
|
|
286
|
+
ordersShort: "",
|
|
287
|
+
revenueShort: "",
|
|
288
|
+
hideBookings: true,
|
|
289
|
+
hideOrders: true,
|
|
290
|
+
hideRevenue: true,
|
|
291
|
+
busiestLine: "",
|
|
292
|
+
hideBusiest: true,
|
|
293
|
+
busiestCaption: "",
|
|
294
|
+
busiestName: "",
|
|
295
|
+
hideBusiestName: true,
|
|
296
|
+
countsShort: "",
|
|
297
|
+
note: base.note ?? "",
|
|
298
|
+
hideNote: !base.note,
|
|
299
|
+
message: text,
|
|
300
|
+
hideMessage: !text,
|
|
301
|
+
hideStats: true,
|
|
302
|
+
// A fixed 0 for the initial and degrade frames: the template contract forbids a worker's
|
|
303
|
+
// initial state from reading the clock (and the baked services hash would vary by timezone).
|
|
304
|
+
validUntil: base.validUntil ?? 0,
|
|
305
|
+
sites: [],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// How an owner widget worker reads a failed Wix call: a sign-out, a section that cannot exist for
|
|
2
|
+
// this owner, or an ordinary error. Shared by the Today widget (one business) and the Today rollup
|
|
3
|
+
// (every business the host lists), so the two never disagree about what hides a section.
|
|
4
|
+
|
|
5
|
+
/** No usable owner session behind the bridge (the bridge refusing, or a 401). A transient
|
|
6
|
+
* 5xx is an ordinary error, not a sign-out — and a 403 is a missing PERMISSION for one
|
|
7
|
+
* vertical (a collaborator whose role lacks Bookings), not a sign-out either. */
|
|
8
|
+
export function isSignedOut(e: unknown): boolean {
|
|
9
|
+
const text = String((e as Error)?.message ?? e);
|
|
10
|
+
return /^HTTP 401\b/.test(text) || /wixApi not enabled|session unavailable|not signed in|webwix_bridge/i.test(text);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** This section cannot exist for this owner: the vertical is not installed on the site (its
|
|
14
|
+
* API answers 428, or 404 for the route), or the owner's role may not read it (403). Hides
|
|
15
|
+
* that one section; every other failure is an error for the whole frame.
|
|
16
|
+
*
|
|
17
|
+
* Read from the LEADING status only — the bridges format a failure as `HTTP <status>: <body>`
|
|
18
|
+
* or `HTTP <status> [<code>]: <message>` — never from anywhere in the message: a transient
|
|
19
|
+
* `HTTP 503: {"message":"upstream returned 404"}` must stay an error, not hide the section. */
|
|
20
|
+
export function isSectionUnavailable(e: unknown): boolean {
|
|
21
|
+
const text = String((e as Error)?.message ?? e);
|
|
22
|
+
return /^HTTP (?:403|404|428)\b/.test(text) || /^HTTP \d{3} \[(?:FAILED_PRECONDITION|PERMISSION_DENIED)\]/.test(text);
|
|
23
|
+
}
|
|
@@ -18,6 +18,9 @@ import { queryBookings } from "@himalaya/wix-bookings";
|
|
|
18
18
|
import { searchOrders } from "@himalaya/wix-ecom";
|
|
19
19
|
import { ownerOrderAttentionFilter } from "../_shared/owner-order-attention.js";
|
|
20
20
|
import { readSelectedSite } from "../_shared/owner-selected-site.js";
|
|
21
|
+
import { isSectionUnavailable, isSignedOut } from "../_shared/owner-widget-reads.js";
|
|
22
|
+
|
|
23
|
+
export { isSectionUnavailable, isSignedOut };
|
|
21
24
|
|
|
22
25
|
type Rec = Record<string, unknown>;
|
|
23
26
|
|
|
@@ -215,27 +218,12 @@ export function degraded(status: "error" | "signedOut" | "loading", siteName: st
|
|
|
215
218
|
return { ...base, status, message, hideMessage: !message, validUntil: 0 };
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
export function isSignedOut(e: unknown): boolean {
|
|
222
|
-
const text = String((e as Error)?.message ?? e);
|
|
223
|
-
return /^HTTP 401\b/.test(text) || /wixApi not enabled|session unavailable|not signed in|webwix_bridge/i.test(text);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** This section cannot exist for this owner: the vertical is not installed on the site (its
|
|
227
|
-
* API answers 428, or 404 for the route), or the owner's role may not read it (403). Hides
|
|
228
|
-
* that one section; every other failure is an error for the whole frame.
|
|
229
|
-
*
|
|
230
|
-
* Read from the LEADING status only — the bridges format a failure as `HTTP <status>: <body>`
|
|
231
|
-
* or `HTTP <status> [<code>]: <message>` — never from anywhere in the message: a transient
|
|
232
|
-
* `HTTP 503: {"message":"upstream returned 404"}` must stay an error, not hide the section. */
|
|
233
|
-
export function isSectionUnavailable(e: unknown): boolean {
|
|
234
|
-
const text = String((e as Error)?.message ?? e);
|
|
235
|
-
return /^HTTP (?:403|404|428)\b/.test(text) || /^HTTP \d{3} \[(?:FAILED_PRECONDITION|PERMISSION_DENIED)\]/.test(text);
|
|
236
|
-
}
|
|
237
|
-
|
|
221
|
+
/** The business this frame is about. An embedding host that publishes this widget once per
|
|
222
|
+
* business names each one in its input (`siteName`): there, the app's selected-site storage is
|
|
223
|
+
* whichever business the owner last opened, not the one this frame was read for. */
|
|
238
224
|
function siteName(): string {
|
|
225
|
+
const hosted = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input?.siteName;
|
|
226
|
+
if (typeof hosted === "string" && hosted.trim()) return hosted.trim();
|
|
239
227
|
return readSelectedSite()?.name ?? "";
|
|
240
228
|
}
|
|
241
229
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Tier-5 bundle: __HIMI_APP_NAME__ — TODAY ROLLUP. The worker behind the Home Screen widget that sums
|
|
2
|
+
// up EVERY business the owner owns: today's bookings, the orders waiting, the revenue so far, and
|
|
3
|
+
// the busiest business by name. The per-business widget is himi-owner-lite-today.
|
|
4
|
+
//
|
|
5
|
+
// Like that worker it runs headlessly on the phone (or in an embedding host's
|
|
6
|
+
// `HimalayaEmbedSurfaces.publishWidgets`), and the widget only ever draws the frozen result.
|
|
7
|
+
//
|
|
8
|
+
// It reads each business with `site.forSite(msid)`, as the owner overview does, so the active
|
|
9
|
+
// business never moves. Embedded, the host names the businesses (`hostOwnedSites`, answered by a
|
|
10
|
+
// data source that conforms to `HimalayaOwnedSitesDataSource`) and signs each call for the one it
|
|
11
|
+
// names; standalone, the account lists them (`querySites`). Read-only: a widget never writes.
|
|
12
|
+
|
|
13
|
+
import { defineService } from "@himalaya/state";
|
|
14
|
+
import { querySites, type WixSite } from "@himalaya/wix-site-list";
|
|
15
|
+
import { site } from "@himalaya/wix-platform";
|
|
16
|
+
import { queryBookings } from "@himalaya/wix-bookings";
|
|
17
|
+
import { searchOrders } from "@himalaya/wix-ecom";
|
|
18
|
+
import { ownerOrderAttentionFilter } from "../_shared/owner-order-attention.js";
|
|
19
|
+
import { hostOwnedSites } from "../_shared/owner-hosted-sites.js";
|
|
20
|
+
import { isSectionUnavailable, isSignedOut } from "../_shared/owner-widget-reads.js";
|
|
21
|
+
import {
|
|
22
|
+
ROLLUP_COPY, ROLLUP_LIMIT, ROLLUP_MAX_SITES, dayBounds, message, shapeRollup,
|
|
23
|
+
type RollupState, type SiteReads, type VerticalRead, type Verticals,
|
|
24
|
+
} from "../_shared/owner-today-rollup.js";
|
|
25
|
+
|
|
26
|
+
export type { RollupState };
|
|
27
|
+
|
|
28
|
+
/** The build's verticals from the descriptor's baked `input.verticals` ("bookings,store"). A
|
|
29
|
+
* descriptor without one reads both. */
|
|
30
|
+
export function buildVerticals(): Verticals {
|
|
31
|
+
const raw = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input?.verticals;
|
|
32
|
+
if (typeof raw !== "string") return { bookings: true, store: true };
|
|
33
|
+
const set = new Set(raw.split(",").map((v) => v.trim()));
|
|
34
|
+
return { bookings: set.has("bookings"), store: set.has("store") };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Rung 1: the host's list when embedded, else the account's own. */
|
|
38
|
+
async function listBusinesses(): Promise<Array<{ id: string; name: string }>> {
|
|
39
|
+
const hosted = await hostOwnedSites();
|
|
40
|
+
if (hosted) return hosted.map((h) => ({ id: h.id, name: h.name || "Business" }));
|
|
41
|
+
const res = await querySites({ query: { cursorPaging: { limit: 100 } } });
|
|
42
|
+
return (res.sites ?? [])
|
|
43
|
+
.filter((r: WixSite) => r?.id)
|
|
44
|
+
.map((r: WixSite) => ({ id: r.id || "", name: r.displayName || (r as { name?: string }).name || "Business" }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Thrown through `Promise.all` when any read finds the owner signed out. */
|
|
48
|
+
const SIGNED_OUT = { signedOut: true } as const;
|
|
49
|
+
|
|
50
|
+
/** One vertical, one business. A section that cannot exist is "absent"; a sign-out stops the
|
|
51
|
+
* whole read (the frame must not keep any business's numbers); anything else is null. */
|
|
52
|
+
async function vertical(enabled: boolean, key: string, read: () => Promise<unknown>): Promise<VerticalRead> {
|
|
53
|
+
if (!enabled) return "absent";
|
|
54
|
+
try {
|
|
55
|
+
const list = ((await read()) as Record<string, unknown> | undefined)?.[key];
|
|
56
|
+
return Array.isArray(list) ? (list as Record<string, unknown>[]) : [];
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (isSignedOut(e)) throw SIGNED_OUT;
|
|
59
|
+
return isSectionUnavailable(e) ? "absent" : null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readSite(b: { id: string; name: string }, verticals: Verticals, now: Date): Promise<SiteReads> {
|
|
64
|
+
const { start, end } = dayBounds(now);
|
|
65
|
+
const scope = site.forSite(b.id);
|
|
66
|
+
const [bookings, orders, sales] = await Promise.all([
|
|
67
|
+
vertical(verticals.bookings, "bookings", () => queryBookings({ query: {
|
|
68
|
+
filter: { startDate: { $gte: start.toISOString(), $lt: end.toISOString() }, status: { $in: ["CONFIRMED"] } },
|
|
69
|
+
sort: [{ fieldName: "startDate", order: "ASC" }, { fieldName: "id", order: "ASC" }],
|
|
70
|
+
paging: { limit: ROLLUP_LIMIT, offset: 0 },
|
|
71
|
+
} }, scope)),
|
|
72
|
+
vertical(verticals.store, "orders", () => searchOrders({ search: {
|
|
73
|
+
filter: ownerOrderAttentionFilter(),
|
|
74
|
+
sort: [{ fieldName: "createdDate", order: "DESC" }, { fieldName: "id", order: "ASC" }],
|
|
75
|
+
cursorPaging: { limit: ROLLUP_LIMIT },
|
|
76
|
+
} }, scope)),
|
|
77
|
+
vertical(verticals.store, "orders", () => searchOrders({ search: {
|
|
78
|
+
filter: {
|
|
79
|
+
archived: false,
|
|
80
|
+
status: { $nin: ["CANCELED", "CANCELLED"] },
|
|
81
|
+
paymentStatus: "PAID",
|
|
82
|
+
createdDate: { $gte: start.toISOString() },
|
|
83
|
+
},
|
|
84
|
+
sort: [{ fieldName: "createdDate", order: "DESC" }, { fieldName: "id", order: "ASC" }],
|
|
85
|
+
cursorPaging: { limit: ROLLUP_LIMIT },
|
|
86
|
+
} }, scope)),
|
|
87
|
+
]);
|
|
88
|
+
return { id: b.id, name: b.name, bookings, orders, sales };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function read(): Promise<RollupState> {
|
|
92
|
+
const now = new Date();
|
|
93
|
+
const verticals = buildVerticals();
|
|
94
|
+
let listed: Array<{ id: string; name: string }>;
|
|
95
|
+
try {
|
|
96
|
+
listed = await listBusinesses();
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return isSignedOut(e) ? message("signedOut", ROLLUP_COPY.signedOut) : message("error", ROLLUP_COPY.error);
|
|
99
|
+
}
|
|
100
|
+
const businesses = listed.slice(0, ROLLUP_MAX_SITES);
|
|
101
|
+
try {
|
|
102
|
+
const reads = await Promise.all(businesses.map((b) => readSite(b, verticals, now)));
|
|
103
|
+
return shapeRollup(reads, now, verticals, listed.length);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
if (e === SIGNED_OUT) return message("signedOut", ROLLUP_COPY.signedOut);
|
|
106
|
+
return message("error", ROLLUP_COPY.error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const svc = defineService<RollupState, {
|
|
111
|
+
load: (s: RollupState) => Promise<RollupState>;
|
|
112
|
+
refresh: (s: RollupState) => Promise<RollupState>;
|
|
113
|
+
}>("ownerTodayRollup", {
|
|
114
|
+
state: message("loading", ""),
|
|
115
|
+
actions: {
|
|
116
|
+
load: async () => read(),
|
|
117
|
+
refresh: async () => read(),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export { svc };
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { withCollectionSaving, type CollectionSaving } from './collection-service.js';
|
|
2
|
-
import { readSavedProducts, writeSavedProducts } from './himi-store-saved.js';
|
|
3
|
-
|
|
4
|
-
export const storeCollectionSaving: CollectionSaving = {
|
|
5
|
-
read: () => readSavedProducts().map(row => row.id),
|
|
6
|
-
write(ids, rows) {
|
|
7
|
-
const products = new Map(readSavedProducts().map(row => [row.id, row]));
|
|
8
|
-
for (const row of rows) {
|
|
9
|
-
if (typeof row.id !== 'string' || typeof row.name !== 'string') continue;
|
|
10
|
-
const previous = products.get(row.id);
|
|
11
|
-
const freshPrice = typeof row.commercePrice === 'string' ? row.commercePrice : typeof row.subtitle === 'string' ? row.subtitle : '';
|
|
12
|
-
const wasPrice = typeof row.wasPrice === 'string' ? row.wasPrice || undefined : previous?.price === freshPrice ? previous?.wasPrice : undefined;
|
|
13
|
-
products.set(row.id, { ...previous, wasPrice, id: row.id, name: row.name,
|
|
14
|
-
price: freshPrice || previous?.price || '',
|
|
15
|
-
image: typeof row.image === 'string' && row.image ? row.image : previous?.image ?? '' });
|
|
16
|
-
}
|
|
17
|
-
const selected = ids.map(id => products.get(id));
|
|
18
|
-
if (selected.some(row => !row)) throw new Error('Saved product metadata is unavailable');
|
|
19
|
-
writeSavedProducts(selected.filter((row): row is NonNullable<typeof row> => !!row));
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
export const defineService = withCollectionSaving(storeCollectionSaving);
|