@burdenoff/microfe-ggnomad 2026.1029.3 → 2026.1030.2
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/generated/wspace-types.d.ts +102099 -35827
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/ggnomad/components/AuditTrailDialog.d.ts +6 -0
- package/dist/ggnomad/components/AuditTrailDialog.d.ts.map +1 -1
- package/dist/ggnomad/components/AuditTrailDialog.js +47 -42
- package/dist/ggnomad/components/AuditTrailDialog.js.map +1 -1
- package/dist/ggnomad/components/EntityCard.d.ts +9 -1
- package/dist/ggnomad/components/EntityCard.d.ts.map +1 -1
- package/dist/ggnomad/components/EntityCard.js +14 -7
- package/dist/ggnomad/components/EntityCard.js.map +1 -1
- package/dist/ggnomad/graphql.d.ts +6 -0
- package/dist/ggnomad/graphql.d.ts.map +1 -1
- package/dist/ggnomad/graphql.js +39 -18
- package/dist/ggnomad/graphql.js.map +1 -1
- package/dist/ggnomad/hooks/useFocusedRecordParam.d.ts +23 -0
- package/dist/ggnomad/hooks/useFocusedRecordParam.d.ts.map +1 -0
- package/dist/ggnomad/hooks/useFocusedRecordParam.js +15 -0
- package/dist/ggnomad/hooks/useFocusedRecordParam.js.map +1 -0
- package/dist/ggnomad/pages/BookingsPage.d.ts.map +1 -1
- package/dist/ggnomad/pages/BookingsPage.js +149 -142
- package/dist/ggnomad/pages/BookingsPage.js.map +1 -1
- package/dist/ggnomad/pages/ProviderAnalyticsPage.d.ts.map +1 -1
- package/dist/ggnomad/pages/ProviderAnalyticsPage.js +135 -134
- package/dist/ggnomad/pages/ProviderAnalyticsPage.js.map +1 -1
- package/dist/services/assistantQuota.d.ts +69 -0
- package/dist/services/assistantQuota.d.ts.map +1 -0
- package/dist/services/assistantQuota.js +52 -0
- package/dist/services/assistantQuota.js.map +1 -0
- package/dist/services/createSandboxAssistantTransport.d.ts +5 -3
- package/dist/services/createSandboxAssistantTransport.d.ts.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js +26 -6
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/dist/utils/pageContext.d.ts +62 -8
- package/dist/utils/pageContext.d.ts.map +1 -1
- package/dist/utils/pageContext.js +114 -39
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProviderAnalyticsPage.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/pages/ProviderAnalyticsPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ProviderAnalyticsPage.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/pages/ProviderAnalyticsPage.tsx"],"names":[],"mappings":"AAyDA;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,gCAkTpC"}
|
|
@@ -3,249 +3,250 @@ import { PageShell as n } from "./PageShell.js";
|
|
|
3
3
|
import { StatCard as r } from "../components/StatCard.js";
|
|
4
4
|
import { SectionCard as i } from "../components/SectionCard.js";
|
|
5
5
|
import "../components/index.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
6
|
+
import { useFocusedRecordParam as a } from "../hooks/useFocusedRecordParam.js";
|
|
7
|
+
import { useMemo as o, useState as s } from "react";
|
|
8
|
+
import { BarChart3 as c, CalendarCheck as l, DollarSign as u, Gauge as d, TrendingUp as f, XCircle as p } from "lucide-react";
|
|
9
|
+
import { useI18n as m } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
|
+
import { useQuery as h } from "@apollo/client/react";
|
|
11
|
+
import { IllustratedEmptyState as g, PagePurpose as _, Spinner as v } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
12
13
|
//#region src/ggnomad/pages/ProviderAnalyticsPage.tsx
|
|
13
|
-
var
|
|
14
|
+
var S = [
|
|
14
15
|
"var(--color-chart-1)",
|
|
15
16
|
"var(--color-chart-2)",
|
|
16
17
|
"var(--color-chart-3)",
|
|
17
18
|
"var(--color-chart-4)",
|
|
18
19
|
"var(--color-chart-5)"
|
|
19
|
-
],
|
|
20
|
-
function
|
|
20
|
+
], C = "mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg";
|
|
21
|
+
function w(e) {
|
|
21
22
|
return e.toISOString().slice(0, 10);
|
|
22
23
|
}
|
|
23
|
-
function
|
|
24
|
+
function T(e) {
|
|
24
25
|
return /* @__PURE__ */ new Date(Date.now() - e * 24 * 60 * 60 * 1e3);
|
|
25
26
|
}
|
|
26
|
-
function
|
|
27
|
-
let { t:
|
|
27
|
+
function E() {
|
|
28
|
+
let { t: E } = m(), { data: D, loading: O } = h(e), k = D?.myGgnomadProviders ?? [], [A, j] = a(), M = k.some((e) => e.id === A) ? A ?? "" : "", [N, P] = s(() => w(T(30))), [F, I] = s(() => w(/* @__PURE__ */ new Date())), [L, R] = s("day"), z = M || k[0]?.id || "", { data: B, loading: V } = h(t, {
|
|
28
29
|
variables: {
|
|
29
|
-
providerId:
|
|
30
|
-
dateFrom: `${
|
|
31
|
-
dateTo: `${
|
|
32
|
-
granularity:
|
|
30
|
+
providerId: z,
|
|
31
|
+
dateFrom: `${N}T00:00:00.000Z`,
|
|
32
|
+
dateTo: `${F}T23:59:59.999Z`,
|
|
33
|
+
granularity: L
|
|
33
34
|
},
|
|
34
|
-
skip: !
|
|
35
|
-
}),
|
|
36
|
-
let e =
|
|
35
|
+
skip: !z
|
|
36
|
+
}), H = B?.myGgnomadProviderAnalytics, U = o(() => {
|
|
37
|
+
let e = H?.bookingsOverTime ?? [];
|
|
37
38
|
return e.length > 0 ? Math.max(...e.map((e) => e.bookings)) : 0;
|
|
38
|
-
}, [
|
|
39
|
-
return /* @__PURE__ */
|
|
40
|
-
title:
|
|
41
|
-
titleAccent:
|
|
42
|
-
subtitle:
|
|
43
|
-
children: [/* @__PURE__ */
|
|
44
|
-
icon: /* @__PURE__ */
|
|
45
|
-
children:
|
|
46
|
-
}), !
|
|
39
|
+
}, [H?.bookingsOverTime]);
|
|
40
|
+
return /* @__PURE__ */ x(n, {
|
|
41
|
+
title: E("ggnomad.providerAnalytics.title", "Provider Analytics"),
|
|
42
|
+
titleAccent: E("ggnomad.providerAnalytics.title.accent", "Analytics"),
|
|
43
|
+
subtitle: E("ggnomad.providerAnalytics.subtitle", "Your bookings, revenue and listing performance"),
|
|
44
|
+
children: [/* @__PURE__ */ b(_, {
|
|
45
|
+
icon: /* @__PURE__ */ b(f, { className: "size-4" }),
|
|
46
|
+
children: E("ggnomad.providerAnalytics.purpose", "A self-service dashboard for providers: bookings and revenue over time, cancellation rate, capacity utilization, and a per-listing breakdown — scoped to only the listings you own. Pick a date range and a listing granularity to drill in.")
|
|
47
|
+
}), !O && k.length === 0 ? /* @__PURE__ */ b(g, {
|
|
47
48
|
illustration: "empty-people",
|
|
48
|
-
title:
|
|
49
|
-
description:
|
|
50
|
-
}) : /* @__PURE__ */
|
|
51
|
-
title:
|
|
52
|
-
children: /* @__PURE__ */
|
|
49
|
+
title: E("ggnomad.providerAnalytics.emptyTitle", "No provider profile yet"),
|
|
50
|
+
description: E("ggnomad.providerAnalytics.emptyDescription", "Once you create a GGNomad provider profile, your bookings and revenue analytics will appear here.")
|
|
51
|
+
}) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(i, {
|
|
52
|
+
title: E("ggnomad.providerAnalytics.filters", "Filters"),
|
|
53
|
+
children: /* @__PURE__ */ x("div", {
|
|
53
54
|
className: "grid gap-3 sm:grid-cols-4",
|
|
54
55
|
children: [
|
|
55
|
-
/* @__PURE__ */
|
|
56
|
+
/* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("label", {
|
|
56
57
|
htmlFor: "provider-analytics-provider",
|
|
57
58
|
className: "block text-sm font-medium text-text-primary",
|
|
58
|
-
children:
|
|
59
|
-
}), /* @__PURE__ */
|
|
59
|
+
children: E("ggnomad.providerAnalytics.provider", "Provider")
|
|
60
|
+
}), /* @__PURE__ */ b("select", {
|
|
60
61
|
id: "provider-analytics-provider",
|
|
61
|
-
value:
|
|
62
|
-
onChange: (e) =>
|
|
63
|
-
className:
|
|
64
|
-
children:
|
|
62
|
+
value: z,
|
|
63
|
+
onChange: (e) => j(e.target.value),
|
|
64
|
+
className: C,
|
|
65
|
+
children: k.map((e) => /* @__PURE__ */ b("option", {
|
|
65
66
|
value: e.id,
|
|
66
67
|
children: e.name
|
|
67
68
|
}, e.id))
|
|
68
69
|
})] }),
|
|
69
|
-
/* @__PURE__ */
|
|
70
|
+
/* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("label", {
|
|
70
71
|
htmlFor: "provider-analytics-from",
|
|
71
72
|
className: "block text-sm font-medium text-text-primary",
|
|
72
|
-
children:
|
|
73
|
-
}), /* @__PURE__ */
|
|
73
|
+
children: E("ggnomad.providerAnalytics.from", "From")
|
|
74
|
+
}), /* @__PURE__ */ b("input", {
|
|
74
75
|
id: "provider-analytics-from",
|
|
75
76
|
type: "date",
|
|
76
|
-
value:
|
|
77
|
-
max:
|
|
78
|
-
onChange: (e) =>
|
|
79
|
-
className:
|
|
77
|
+
value: N,
|
|
78
|
+
max: F,
|
|
79
|
+
onChange: (e) => P(e.target.value),
|
|
80
|
+
className: C
|
|
80
81
|
})] }),
|
|
81
|
-
/* @__PURE__ */
|
|
82
|
+
/* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("label", {
|
|
82
83
|
htmlFor: "provider-analytics-to",
|
|
83
84
|
className: "block text-sm font-medium text-text-primary",
|
|
84
|
-
children:
|
|
85
|
-
}), /* @__PURE__ */
|
|
85
|
+
children: E("ggnomad.providerAnalytics.to", "To")
|
|
86
|
+
}), /* @__PURE__ */ b("input", {
|
|
86
87
|
id: "provider-analytics-to",
|
|
87
88
|
type: "date",
|
|
88
|
-
value:
|
|
89
|
-
min:
|
|
90
|
-
max:
|
|
91
|
-
onChange: (e) =>
|
|
92
|
-
className:
|
|
89
|
+
value: F,
|
|
90
|
+
min: N,
|
|
91
|
+
max: w(/* @__PURE__ */ new Date()),
|
|
92
|
+
onChange: (e) => I(e.target.value),
|
|
93
|
+
className: C
|
|
93
94
|
})] }),
|
|
94
|
-
/* @__PURE__ */
|
|
95
|
+
/* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("label", {
|
|
95
96
|
htmlFor: "provider-analytics-granularity",
|
|
96
97
|
className: "block text-sm font-medium text-text-primary",
|
|
97
|
-
children:
|
|
98
|
-
}), /* @__PURE__ */
|
|
98
|
+
children: E("ggnomad.providerAnalytics.granularity", "Granularity")
|
|
99
|
+
}), /* @__PURE__ */ x("select", {
|
|
99
100
|
id: "provider-analytics-granularity",
|
|
100
|
-
value:
|
|
101
|
-
onChange: (e) =>
|
|
102
|
-
className:
|
|
101
|
+
value: L,
|
|
102
|
+
onChange: (e) => R(e.target.value),
|
|
103
|
+
className: C,
|
|
103
104
|
children: [
|
|
104
|
-
/* @__PURE__ */
|
|
105
|
+
/* @__PURE__ */ b("option", {
|
|
105
106
|
value: "day",
|
|
106
|
-
children:
|
|
107
|
+
children: E("ggnomad.providerAnalytics.day", "Day")
|
|
107
108
|
}),
|
|
108
|
-
/* @__PURE__ */
|
|
109
|
+
/* @__PURE__ */ b("option", {
|
|
109
110
|
value: "week",
|
|
110
|
-
children:
|
|
111
|
+
children: E("ggnomad.providerAnalytics.week", "Week")
|
|
111
112
|
}),
|
|
112
|
-
/* @__PURE__ */
|
|
113
|
+
/* @__PURE__ */ b("option", {
|
|
113
114
|
value: "month",
|
|
114
|
-
children:
|
|
115
|
+
children: E("ggnomad.providerAnalytics.month", "Month")
|
|
115
116
|
})
|
|
116
117
|
]
|
|
117
118
|
})] })
|
|
118
119
|
]
|
|
119
120
|
})
|
|
120
|
-
}),
|
|
121
|
+
}), V ? /* @__PURE__ */ b("div", {
|
|
121
122
|
className: "flex justify-center py-12",
|
|
122
|
-
children: /* @__PURE__ */
|
|
123
|
-
}) :
|
|
124
|
-
/* @__PURE__ */
|
|
123
|
+
children: /* @__PURE__ */ b(v, { size: "lg" })
|
|
124
|
+
}) : H ? /* @__PURE__ */ x(y, { children: [
|
|
125
|
+
/* @__PURE__ */ x("section", {
|
|
125
126
|
className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4",
|
|
126
127
|
children: [
|
|
127
|
-
/* @__PURE__ */
|
|
128
|
+
/* @__PURE__ */ b(r, {
|
|
128
129
|
hero: !0,
|
|
129
|
-
label:
|
|
130
|
-
value:
|
|
131
|
-
icon: /* @__PURE__ */
|
|
130
|
+
label: E("ggnomad.providerAnalytics.totalBookings", "Bookings"),
|
|
131
|
+
value: H.totalBookings.toLocaleString(),
|
|
132
|
+
icon: /* @__PURE__ */ b(l, {})
|
|
132
133
|
}),
|
|
133
|
-
/* @__PURE__ */
|
|
134
|
-
label:
|
|
135
|
-
value: `$${Number(
|
|
136
|
-
icon: /* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ b(r, {
|
|
135
|
+
label: E("ggnomad.providerAnalytics.totalRevenue", "Revenue"),
|
|
136
|
+
value: `$${Number(H.totalRevenue).toLocaleString()}`,
|
|
137
|
+
icon: /* @__PURE__ */ b(u, {})
|
|
137
138
|
}),
|
|
138
|
-
/* @__PURE__ */
|
|
139
|
-
label:
|
|
140
|
-
value: `${(
|
|
141
|
-
icon: /* @__PURE__ */
|
|
139
|
+
/* @__PURE__ */ b(r, {
|
|
140
|
+
label: E("ggnomad.providerAnalytics.cancellationRate", "Cancellation rate"),
|
|
141
|
+
value: `${(H.cancellationRate * 100).toFixed(1)}%`,
|
|
142
|
+
icon: /* @__PURE__ */ b(p, {})
|
|
142
143
|
}),
|
|
143
|
-
/* @__PURE__ */
|
|
144
|
-
label:
|
|
145
|
-
value: `${(
|
|
146
|
-
icon: /* @__PURE__ */
|
|
144
|
+
/* @__PURE__ */ b(r, {
|
|
145
|
+
label: E("ggnomad.providerAnalytics.capacityUtilization", "Capacity utilization"),
|
|
146
|
+
value: `${(H.capacityUtilizationRate * 100).toFixed(1)}%`,
|
|
147
|
+
icon: /* @__PURE__ */ b(d, {})
|
|
147
148
|
})
|
|
148
149
|
]
|
|
149
150
|
}),
|
|
150
|
-
/* @__PURE__ */
|
|
151
|
-
title:
|
|
152
|
-
children:
|
|
151
|
+
/* @__PURE__ */ b(i, {
|
|
152
|
+
title: E("ggnomad.providerAnalytics.bookingsOverTime", "Bookings over time"),
|
|
153
|
+
children: H.bookingsOverTime.length === 0 ? /* @__PURE__ */ x("div", {
|
|
153
154
|
className: "flex flex-col items-center gap-2 py-8 text-center",
|
|
154
|
-
children: [/* @__PURE__ */
|
|
155
|
+
children: [/* @__PURE__ */ b("span", {
|
|
155
156
|
className: "flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary",
|
|
156
|
-
children: /* @__PURE__ */
|
|
157
|
-
}), /* @__PURE__ */
|
|
157
|
+
children: /* @__PURE__ */ b(c, { className: "size-5" })
|
|
158
|
+
}), /* @__PURE__ */ b("p", {
|
|
158
159
|
className: "text-sm font-medium text-text-primary",
|
|
159
|
-
children:
|
|
160
|
+
children: E("ggnomad.providerAnalytics.trendEmpty", "No bookings in this window yet")
|
|
160
161
|
})]
|
|
161
|
-
}) : /* @__PURE__ */
|
|
162
|
+
}) : /* @__PURE__ */ x("div", {
|
|
162
163
|
className: "flex flex-col gap-4",
|
|
163
|
-
children: [/* @__PURE__ */
|
|
164
|
+
children: [/* @__PURE__ */ b("div", {
|
|
164
165
|
className: "flex items-end gap-2 overflow-x-auto pb-2",
|
|
165
|
-
children:
|
|
166
|
+
children: H.bookingsOverTime.map((e, t) => /* @__PURE__ */ x("div", {
|
|
166
167
|
className: "flex flex-col items-center gap-1",
|
|
167
|
-
children: [/* @__PURE__ */
|
|
168
|
+
children: [/* @__PURE__ */ b("div", {
|
|
168
169
|
className: "flex h-24 items-end",
|
|
169
|
-
children: /* @__PURE__ */
|
|
170
|
+
children: /* @__PURE__ */ b("div", {
|
|
170
171
|
className: "w-8 rounded-t transition-all duration-500 ease-out",
|
|
171
172
|
style: {
|
|
172
|
-
height: `${
|
|
173
|
-
backgroundColor:
|
|
173
|
+
height: `${U > 0 ? Math.max(e.bookings / U * 100, 5) : 5}%`,
|
|
174
|
+
backgroundColor: S[t % S.length]
|
|
174
175
|
},
|
|
175
176
|
title: `${e.period}: ${e.bookings} bookings, $${e.revenue.toLocaleString()} revenue`
|
|
176
177
|
})
|
|
177
|
-
}), /* @__PURE__ */
|
|
178
|
+
}), /* @__PURE__ */ b("p", {
|
|
178
179
|
className: "truncate text-xs text-text-secondary",
|
|
179
180
|
children: e.period
|
|
180
181
|
})]
|
|
181
182
|
}, e.period))
|
|
182
|
-
}), /* @__PURE__ */
|
|
183
|
+
}), /* @__PURE__ */ b("p", {
|
|
183
184
|
className: "text-xs text-text-secondary",
|
|
184
|
-
children:
|
|
185
|
+
children: E("ggnomad.providerAnalytics.trendNote", "Bar height is booking volume per period; hover a bar for its exact bookings and revenue.")
|
|
185
186
|
})]
|
|
186
187
|
})
|
|
187
188
|
}),
|
|
188
|
-
/* @__PURE__ */
|
|
189
|
-
title:
|
|
190
|
-
children:
|
|
189
|
+
/* @__PURE__ */ b(i, {
|
|
190
|
+
title: E("ggnomad.providerAnalytics.topListings", "Top listings"),
|
|
191
|
+
children: H.topListings.length === 0 ? /* @__PURE__ */ b("p", {
|
|
191
192
|
className: "py-6 text-center text-sm text-text-secondary",
|
|
192
|
-
children:
|
|
193
|
-
}) : /* @__PURE__ */
|
|
193
|
+
children: E("ggnomad.providerAnalytics.topListingsEmpty", "No listing bookings in this range yet.")
|
|
194
|
+
}) : /* @__PURE__ */ b("div", {
|
|
194
195
|
className: "overflow-x-auto",
|
|
195
|
-
children: /* @__PURE__ */
|
|
196
|
+
children: /* @__PURE__ */ x("table", {
|
|
196
197
|
className: "w-full min-w-[560px] text-sm",
|
|
197
|
-
children: [/* @__PURE__ */
|
|
198
|
+
children: [/* @__PURE__ */ b("thead", { children: /* @__PURE__ */ x("tr", {
|
|
198
199
|
className: "border-b border-border-seam text-left text-text-secondary",
|
|
199
200
|
children: [
|
|
200
|
-
/* @__PURE__ */
|
|
201
|
+
/* @__PURE__ */ b("th", {
|
|
201
202
|
className: "py-2 pr-3 font-medium",
|
|
202
|
-
children:
|
|
203
|
+
children: E("ggnomad.providerAnalytics.listing", "Listing")
|
|
203
204
|
}),
|
|
204
|
-
/* @__PURE__ */
|
|
205
|
+
/* @__PURE__ */ b("th", {
|
|
205
206
|
className: "py-2 pr-3 font-medium",
|
|
206
|
-
children:
|
|
207
|
+
children: E("ggnomad.providerAnalytics.type", "Type")
|
|
207
208
|
}),
|
|
208
|
-
/* @__PURE__ */
|
|
209
|
+
/* @__PURE__ */ b("th", {
|
|
209
210
|
className: "py-2 pr-3 text-right font-medium",
|
|
210
|
-
children:
|
|
211
|
+
children: E("ggnomad.providerAnalytics.bookings", "Bookings")
|
|
211
212
|
}),
|
|
212
|
-
/* @__PURE__ */
|
|
213
|
+
/* @__PURE__ */ b("th", {
|
|
213
214
|
className: "py-2 pr-3 text-right font-medium",
|
|
214
|
-
children:
|
|
215
|
+
children: E("ggnomad.providerAnalytics.revenue", "Revenue")
|
|
215
216
|
}),
|
|
216
|
-
/* @__PURE__ */
|
|
217
|
+
/* @__PURE__ */ b("th", {
|
|
217
218
|
className: "py-2 pr-3 text-right font-medium",
|
|
218
|
-
children:
|
|
219
|
+
children: E("ggnomad.providerAnalytics.cancellation", "Cancellation")
|
|
219
220
|
}),
|
|
220
|
-
/* @__PURE__ */
|
|
221
|
+
/* @__PURE__ */ b("th", {
|
|
221
222
|
className: "py-2 text-right font-medium",
|
|
222
|
-
children:
|
|
223
|
+
children: E("ggnomad.providerAnalytics.utilization", "Utilization")
|
|
223
224
|
})
|
|
224
225
|
]
|
|
225
|
-
}) }), /* @__PURE__ */
|
|
226
|
+
}) }), /* @__PURE__ */ b("tbody", { children: H.topListings.map((e) => /* @__PURE__ */ x("tr", {
|
|
226
227
|
className: "border-b border-border-seam/50 last:border-0",
|
|
227
228
|
children: [
|
|
228
|
-
/* @__PURE__ */
|
|
229
|
+
/* @__PURE__ */ b("td", {
|
|
229
230
|
className: "py-2 pr-3 font-medium text-text-primary",
|
|
230
231
|
children: e.listingName
|
|
231
232
|
}),
|
|
232
|
-
/* @__PURE__ */
|
|
233
|
+
/* @__PURE__ */ b("td", {
|
|
233
234
|
className: "py-2 pr-3 text-text-secondary",
|
|
234
235
|
children: e.listingType
|
|
235
236
|
}),
|
|
236
|
-
/* @__PURE__ */
|
|
237
|
+
/* @__PURE__ */ b("td", {
|
|
237
238
|
className: "py-2 pr-3 text-right tabular-nums text-text-primary",
|
|
238
239
|
children: e.bookings
|
|
239
240
|
}),
|
|
240
|
-
/* @__PURE__ */
|
|
241
|
+
/* @__PURE__ */ x("td", {
|
|
241
242
|
className: "py-2 pr-3 text-right tabular-nums text-text-primary",
|
|
242
243
|
children: ["$", e.revenue.toLocaleString()]
|
|
243
244
|
}),
|
|
244
|
-
/* @__PURE__ */
|
|
245
|
+
/* @__PURE__ */ x("td", {
|
|
245
246
|
className: "py-2 pr-3 text-right tabular-nums text-text-primary",
|
|
246
247
|
children: [(e.cancellationRate * 100).toFixed(0), "%"]
|
|
247
248
|
}),
|
|
248
|
-
/* @__PURE__ */
|
|
249
|
+
/* @__PURE__ */ x("td", {
|
|
249
250
|
className: "py-2 text-right tabular-nums text-text-primary",
|
|
250
251
|
children: [(e.capacityUtilizationRate * 100).toFixed(0), "%"]
|
|
251
252
|
})
|
|
@@ -254,14 +255,14 @@ function T() {
|
|
|
254
255
|
})
|
|
255
256
|
})
|
|
256
257
|
})
|
|
257
|
-
] }) : /* @__PURE__ */
|
|
258
|
+
] }) : /* @__PURE__ */ b(g, {
|
|
258
259
|
illustration: "empty-people",
|
|
259
|
-
title:
|
|
260
|
-
description:
|
|
260
|
+
title: E("ggnomad.providerAnalytics.noDataTitle", "No bookings in this range"),
|
|
261
|
+
description: E("ggnomad.providerAnalytics.noDataDescription", "Try widening the date range — analytics appear here once bookings come in for your listings.")
|
|
261
262
|
})] })]
|
|
262
263
|
});
|
|
263
264
|
}
|
|
264
265
|
//#endregion
|
|
265
|
-
export {
|
|
266
|
+
export { E as ProviderAnalyticsPage };
|
|
266
267
|
|
|
267
268
|
//# sourceMappingURL=ProviderAnalyticsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProviderAnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/ProviderAnalyticsPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { BarChart3, CalendarCheck, DollarSign, Gauge, TrendingUp, XCircle } from 'lucide-react';\nimport { IllustratedEmptyState, PagePurpose, Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_MY_PROVIDERS_QUERY, GGNOMAD_MY_PROVIDER_ANALYTICS_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst INPUT_CLASSES =\n 'mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg';\n\ntype MyProvider = { id: string; name: string; type: string; status: string };\n\ntype AnalyticsPoint = { period: string; bookings: number; revenue: number };\n\ntype ListingAnalytics = {\n listingId: string;\n listingType: string;\n listingName: string;\n bookings: number;\n revenue: number;\n cancellationRate: number;\n capacityUtilizationRate: number;\n};\n\ntype ProviderAnalytics = {\n providerId: string;\n providerName: string;\n totalBookings: number;\n totalRevenue: number;\n cancelledBookings: number;\n cancellationRate: number;\n capacityUtilizationRate: number;\n bookingsOverTime: AnalyticsPoint[];\n topListings: ListingAnalytics[];\n};\n\nfunction toISODate(date: Date): string {\n return date.toISOString().slice(0, 10);\n}\n\nfunction daysAgo(days: number): Date {\n return new Date(Date.now() - days * 24 * 60 * 60 * 1000);\n}\n\n/**\n * BOFF-4390 — provider-facing analytics dashboard: bookings-over-time,\n * revenue, cancellation rate and capacity utilization, plus a per-listing\n * drill-down table. Self-scoped: only providers the calling actor created\n * are listed, and the analytics call is owner-verified server-side — a\n * provider can never see another provider's numbers. Forecasting and\n * custom-report building are explicitly out of scope for this core; see the\n * `myGgnomadProviderAnalytics` schema doc comment.\n */\nexport function ProviderAnalyticsPage() {\n const { t } = useI18n();\n\n const { data: providersData, loading: providersLoading } = useQuery(\n GGNOMAD_MY_PROVIDERS_QUERY as TypedDocumentNode<\n { myGgnomadProviders: MyProvider[] },\n Record<string, never>\n >\n );\n const providers = providersData?.myGgnomadProviders ?? [];\n\n const [selectedProviderId, setSelectedProviderId] = useState('');\n // `dateFrom`/`dateTo` are the plain YYYY-MM-DD strings bound to the <input\n // type=\"date\"> controls. They are NOT sent to GraphQL as-is: a bare date\n // string parses to UTC midnight, so using \"today\" as dateTo would exclude\n // every booking made later that same day. The query variables below\n // convert dateFrom to the start of that UTC day and dateTo to the end of\n // it, so \"today\" always includes today's activity.\n const [dateFrom, setDateFrom] = useState(() => toISODate(daysAgo(30)));\n const [dateTo, setDateTo] = useState(() => toISODate(new Date()));\n const [granularity, setGranularity] = useState<'day' | 'week' | 'month'>('day');\n\n const activeProviderId = selectedProviderId || providers[0]?.id || '';\n\n const { data: analyticsData, loading: analyticsLoading } = useQuery(\n GGNOMAD_MY_PROVIDER_ANALYTICS_QUERY as TypedDocumentNode<\n { myGgnomadProviderAnalytics: ProviderAnalytics },\n { providerId: string; dateFrom?: string; dateTo?: string; granularity?: string }\n >,\n {\n variables: {\n providerId: activeProviderId,\n dateFrom: `${dateFrom}T00:00:00.000Z`,\n dateTo: `${dateTo}T23:59:59.999Z`,\n granularity,\n },\n skip: !activeProviderId,\n }\n );\n const analytics = analyticsData?.myGgnomadProviderAnalytics;\n\n const maxTrend = useMemo(() => {\n const points = analytics?.bookingsOverTime ?? [];\n return points.length > 0 ? Math.max(...points.map((p) => p.bookings)) : 0;\n }, [analytics?.bookingsOverTime]);\n\n return (\n <PageShell\n title={t('ggnomad.providerAnalytics.title', 'Provider Analytics')}\n titleAccent={t('ggnomad.providerAnalytics.title.accent', 'Analytics')}\n subtitle={t(\n 'ggnomad.providerAnalytics.subtitle',\n 'Your bookings, revenue and listing performance'\n )}\n >\n <PagePurpose icon={<TrendingUp className=\"size-4\" />}>\n {t(\n 'ggnomad.providerAnalytics.purpose',\n 'A self-service dashboard for providers: bookings and revenue over time, cancellation rate, capacity utilization, and a per-listing breakdown — scoped to only the listings you own. Pick a date range and a listing granularity to drill in.'\n )}\n </PagePurpose>\n\n {!providersLoading && providers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.providerAnalytics.emptyTitle', 'No provider profile yet')}\n description={t(\n 'ggnomad.providerAnalytics.emptyDescription',\n 'Once you create a GGNomad provider profile, your bookings and revenue analytics will appear here.'\n )}\n />\n ) : (\n <>\n <SectionCard title={t('ggnomad.providerAnalytics.filters', 'Filters')}>\n <div className=\"grid gap-3 sm:grid-cols-4\">\n <div>\n <label\n htmlFor=\"provider-analytics-provider\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.provider', 'Provider')}\n </label>\n <select\n id=\"provider-analytics-provider\"\n value={activeProviderId}\n onChange={(e) => setSelectedProviderId(e.target.value)}\n className={INPUT_CLASSES}\n >\n {providers.map((provider) => (\n <option key={provider.id} value={provider.id}>\n {provider.name}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-from\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.from', 'From')}\n </label>\n <input\n id=\"provider-analytics-from\"\n type=\"date\"\n value={dateFrom}\n max={dateTo}\n onChange={(e) => setDateFrom(e.target.value)}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-to\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.to', 'To')}\n </label>\n <input\n id=\"provider-analytics-to\"\n type=\"date\"\n value={dateTo}\n min={dateFrom}\n max={toISODate(new Date())}\n onChange={(e) => setDateTo(e.target.value)}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-granularity\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.granularity', 'Granularity')}\n </label>\n <select\n id=\"provider-analytics-granularity\"\n value={granularity}\n onChange={(e) => setGranularity(e.target.value as 'day' | 'week' | 'month')}\n className={INPUT_CLASSES}\n >\n <option value=\"day\">{t('ggnomad.providerAnalytics.day', 'Day')}</option>\n <option value=\"week\">{t('ggnomad.providerAnalytics.week', 'Week')}</option>\n <option value=\"month\">{t('ggnomad.providerAnalytics.month', 'Month')}</option>\n </select>\n </div>\n </div>\n </SectionCard>\n\n {analyticsLoading ? (\n <div className=\"flex justify-center py-12\">\n <Spinner size=\"lg\" />\n </div>\n ) : !analytics ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.providerAnalytics.noDataTitle', 'No bookings in this range')}\n description={t(\n 'ggnomad.providerAnalytics.noDataDescription',\n 'Try widening the date range — analytics appear here once bookings come in for your listings.'\n )}\n />\n ) : (\n <>\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.providerAnalytics.totalBookings', 'Bookings')}\n value={analytics.totalBookings.toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.totalRevenue', 'Revenue')}\n value={`$${Number(analytics.totalRevenue).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.cancellationRate', 'Cancellation rate')}\n value={`${(analytics.cancellationRate * 100).toFixed(1)}%`}\n icon={<XCircle />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.capacityUtilization', 'Capacity utilization')}\n value={`${(analytics.capacityUtilizationRate * 100).toFixed(1)}%`}\n icon={<Gauge />}\n />\n </section>\n\n <SectionCard\n title={t('ggnomad.providerAnalytics.bookingsOverTime', 'Bookings over time')}\n >\n {analytics.bookingsOverTime.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.providerAnalytics.trendEmpty', 'No bookings in this window yet')}\n </p>\n </div>\n ) : (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-end gap-2 overflow-x-auto pb-2\">\n {analytics.bookingsOverTime.map((point, index) => {\n const pct =\n maxTrend > 0 ? Math.max((point.bookings / maxTrend) * 100, 5) : 5;\n return (\n <div key={point.period} className=\"flex flex-col items-center gap-1\">\n <div className=\"flex h-24 items-end\">\n <div\n className=\"w-8 rounded-t transition-all duration-500 ease-out\"\n style={{\n height: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n title={`${point.period}: ${point.bookings} bookings, $${point.revenue.toLocaleString()} revenue`}\n />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">{point.period}</p>\n </div>\n );\n })}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'ggnomad.providerAnalytics.trendNote',\n 'Bar height is booking volume per period; hover a bar for its exact bookings and revenue.'\n )}\n </p>\n </div>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.providerAnalytics.topListings', 'Top listings')}>\n {analytics.topListings.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t(\n 'ggnomad.providerAnalytics.topListingsEmpty',\n 'No listing bookings in this range yet.'\n )}\n </p>\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-left text-text-secondary\">\n <th className=\"py-2 pr-3 font-medium\">\n {t('ggnomad.providerAnalytics.listing', 'Listing')}\n </th>\n <th className=\"py-2 pr-3 font-medium\">\n {t('ggnomad.providerAnalytics.type', 'Type')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.bookings', 'Bookings')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.revenue', 'Revenue')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.cancellation', 'Cancellation')}\n </th>\n <th className=\"py-2 text-right font-medium\">\n {t('ggnomad.providerAnalytics.utilization', 'Utilization')}\n </th>\n </tr>\n </thead>\n <tbody>\n {analytics.topListings.map((listing) => (\n <tr\n key={listing.listingId}\n className=\"border-b border-border-seam/50 last:border-0\"\n >\n <td className=\"py-2 pr-3 font-medium text-text-primary\">\n {listing.listingName}\n </td>\n <td className=\"py-2 pr-3 text-text-secondary\">{listing.listingType}</td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n {listing.bookings}\n </td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n ${listing.revenue.toLocaleString()}\n </td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n {(listing.cancellationRate * 100).toFixed(0)}%\n </td>\n <td className=\"py-2 text-right tabular-nums text-text-primary\">\n {(listing.capacityUtilizationRate * 100).toFixed(0)}%\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </SectionCard>\n </>\n )}\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAWA,IAAM,IAAe;CACnB;CACA;CACA;CACA;CACA;AACF,GAEM,IACJ;AA4BF,SAAS,EAAU,GAAoB;CACrC,OAAO,EAAK,YAAY,CAAC,CAAC,MAAM,GAAG,EAAE;AACvC;AAEA,SAAS,EAAQ,GAAoB;CACnC,uBAAO,IAAI,KAAK,KAAK,IAAI,IAAI,IAAO,KAAK,KAAK,KAAK,GAAI;AACzD;AAWA,SAAgB,IAAwB;CACtC,IAAM,EAAE,SAAM,EAAQ,GAEhB,EAAE,MAAM,GAAe,SAAS,MAAqB,EACzD,CAIF,GACM,IAAY,GAAe,sBAAsB,CAAC,GAElD,CAAC,GAAoB,KAAyB,EAAS,EAAE,GAOzD,CAAC,GAAU,KAAe,QAAe,EAAU,EAAQ,EAAE,CAAC,CAAC,GAC/D,CAAC,GAAQ,KAAa,QAAe,kBAAU,IAAI,KAAK,CAAC,CAAC,GAC1D,CAAC,GAAa,KAAkB,EAAmC,KAAK,GAExE,IAAmB,KAAsB,EAAU,EAAE,EAAE,MAAM,IAE7D,EAAE,MAAM,GAAe,SAAS,MAAqB,EACzD,GAIA;EACE,WAAW;GACT,YAAY;GACZ,UAAU,GAAG,EAAS;GACtB,QAAQ,GAAG,EAAO;GAClB;EACF;EACA,MAAM,CAAC;CACT,CACF,GACM,IAAY,GAAe,4BAE3B,IAAW,QAAc;EAC7B,IAAM,IAAS,GAAW,oBAAoB,CAAC;EAC/C,OAAO,EAAO,SAAS,IAAI,KAAK,IAAI,GAAG,EAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,IAAI;CAC1E,GAAG,CAAC,GAAW,gBAAgB,CAAC;CAEhC,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,mCAAmC,oBAAoB;EAChE,aAAa,EAAE,0CAA0C,WAAW;EACpE,UAAU,EACR,sCACA,gDACF;YANF,CAQE,kBAAC,GAAD;GAAa,MAAM,kBAAC,GAAD,EAAY,WAAU,SAAU,CAAA;aAChD,EACC,qCACA,8OACF;EACW,CAAA,GAEZ,CAAC,KAAoB,EAAU,WAAW,IACzC,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,wCAAwC,yBAAyB;GAC1E,aAAa,EACX,8CACA,mGACF;EACD,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;GAAa,OAAO,EAAE,qCAAqC,SAAS;aAClE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,sCAAsC,UAAU;KAC9C,CAAA,GACP,kBAAC,UAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAsB,EAAE,OAAO,KAAK;MACrD,WAAW;gBAEV,EAAU,KAAK,MACd,kBAAC,UAAD;OAA0B,OAAO,EAAS;iBACvC,EAAS;MACJ,GAFK,EAAS,EAEd,CACT;KACK,CAAA,CACL,EAAA,CAAA;KACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,kCAAkC,MAAM;KACtC,CAAA,GACP,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,KAAK;MACL,WAAW,MAAM,EAAY,EAAE,OAAO,KAAK;MAC3C,WAAW;KACZ,CAAA,CACE,EAAA,CAAA;KACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,gCAAgC,IAAI;KAClC,CAAA,GACP,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,KAAK;MACL,KAAK,kBAAU,IAAI,KAAK,CAAC;MACzB,WAAW,MAAM,EAAU,EAAE,OAAO,KAAK;MACzC,WAAW;KACZ,CAAA,CACE,EAAA,CAAA;KACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,yCAAyC,aAAa;KACpD,CAAA,GACP,kBAAC,UAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAe,EAAE,OAAO,KAAiC;MAC1E,WAAW;gBAJb;OAME,kBAAC,UAAD;QAAQ,OAAM;kBAAO,EAAE,iCAAiC,KAAK;OAAU,CAAA;OACvE,kBAAC,UAAD;QAAQ,OAAM;kBAAQ,EAAE,kCAAkC,MAAM;OAAU,CAAA;OAC1E,kBAAC,UAAD;QAAQ,OAAM;kBAAS,EAAE,mCAAmC,OAAO;OAAU,CAAA;MACvE;OACL,EAAA,CAAA;IACF;;EACM,CAAA,GAEZ,IACC,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA;EACjB,CAAA,IACF,IAUH,kBAAA,GAAA,EAAA,UAAA;GACE,kBAAC,WAAD;IAAS,WAAU;cAAnB;KACE,kBAAC,GAAD;MACE,MAAA;MACA,OAAO,EAAE,2CAA2C,UAAU;MAC9D,OAAO,EAAU,cAAc,eAAe;MAC9C,MAAM,kBAAC,GAAD,CAAgB,CAAA;KACvB,CAAA;KACD,kBAAC,GAAD;MACE,OAAO,EAAE,0CAA0C,SAAS;MAC5D,OAAO,IAAI,OAAO,EAAU,YAAY,CAAC,CAAC,eAAe;MACzD,MAAM,kBAAC,GAAD,CAAa,CAAA;KACpB,CAAA;KACD,kBAAC,GAAD;MACE,OAAO,EAAE,8CAA8C,mBAAmB;MAC1E,OAAO,IAAI,EAAU,mBAAmB,IAAA,CAAK,QAAQ,CAAC,EAAE;MACxD,MAAM,kBAAC,GAAD,CAAU,CAAA;KACjB,CAAA;KACD,kBAAC,GAAD;MACE,OAAO,EAAE,iDAAiD,sBAAsB;MAChF,OAAO,IAAI,EAAU,0BAA0B,IAAA,CAAK,QAAQ,CAAC,EAAE;MAC/D,MAAM,kBAAC,GAAD,CAAQ,CAAA;KACf,CAAA;IACM;;GAET,kBAAC,GAAD;IACE,OAAO,EAAE,8CAA8C,oBAAoB;cAE1E,EAAU,iBAAiB,WAAW,IACrC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBACd,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA;KAC3B,CAAA,GACN,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,wCAAwC,gCAAgC;KAC1E,CAAA,CACA;SAEL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAU,iBAAiB,KAAK,GAAO,MAIpC,kBAAC,OAAD;OAAwB,WAAU;iBAAlC,CACE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SACE,WAAU;SACV,OAAO;UACL,QAAQ,GAPhB,IAAW,IAAI,KAAK,IAAK,EAAM,WAAW,IAAY,KAAK,CAAC,IAAI,EAOzC;UACf,iBAAiB,EAAa,IAAQ,EAAa;SACrD;SACA,OAAO,GAAG,EAAM,OAAO,IAAI,EAAM,SAAS,cAAc,EAAM,QAAQ,eAAe,EAAE;QACxF,CAAA;OACE,CAAA,GACL,kBAAC,KAAD;QAAG,WAAU;kBAAwC,EAAM;OAAU,CAAA,CAClE;SAZK,EAAM,MAYX,CAER;KACE,CAAA,GACL,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,uCACA,0FACF;KACC,CAAA,CACA;;GAEI,CAAA;GAEb,kBAAC,GAAD;IAAa,OAAO,EAAE,yCAAyC,cAAc;cAC1E,EAAU,YAAY,WAAW,IAChC,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,8CACA,wCACF;IACC,CAAA,IAEH,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,qCAAqC,SAAS;QAC/C,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,kCAAkC,MAAM;QACzC,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,sCAAsC,UAAU;QACjD,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,qCAAqC,SAAS;QAC/C,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,0CAA0C,cAAc;QACzD,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,yCAAyC,aAAa;QACvD,CAAA;OACF;SACC,CAAA,GACP,kBAAC,SAAD,EAAA,UACG,EAAU,YAAY,KAAK,MAC1B,kBAAC,MAAD;OAEE,WAAU;iBAFZ;QAIE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAQ;QACP,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAiC,EAAQ;QAAgB,CAAA;QACvE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAQ;QACP,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd,CAAoE,KAChE,EAAQ,QAAQ,eAAe,CAC/B;;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd,EACI,EAAQ,mBAAmB,IAAA,CAAK,QAAQ,CAAC,GAAE,GAC3C;;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd,EACI,EAAQ,0BAA0B,IAAA,CAAK,QAAQ,CAAC,GAAE,GAClD;;OACF;SAnBG,EAAQ,SAmBX,CACL,EACI,CAAA,CACF;;IACJ,CAAA;GAEI,CAAA;EACb,EAAA,CAAA,IA7IF,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,yCAAyC,2BAA2B;GAC7E,aAAa,EACX,+CACA,8FACF;EACD,CAAA,CAwIH,EAAA,CAAA,CAEK;;AAEf"}
|
|
1
|
+
{"version":3,"file":"ProviderAnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/ProviderAnalyticsPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { BarChart3, CalendarCheck, DollarSign, Gauge, TrendingUp, XCircle } from 'lucide-react';\nimport { IllustratedEmptyState, PagePurpose, Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_MY_PROVIDERS_QUERY, GGNOMAD_MY_PROVIDER_ANALYTICS_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\nimport { useFocusedRecordParam } from '../hooks/useFocusedRecordParam';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst INPUT_CLASSES =\n 'mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg';\n\ntype MyProvider = { id: string; name: string; type: string; status: string };\n\ntype AnalyticsPoint = { period: string; bookings: number; revenue: number };\n\ntype ListingAnalytics = {\n listingId: string;\n listingType: string;\n listingName: string;\n bookings: number;\n revenue: number;\n cancellationRate: number;\n capacityUtilizationRate: number;\n};\n\ntype ProviderAnalytics = {\n providerId: string;\n providerName: string;\n totalBookings: number;\n totalRevenue: number;\n cancelledBookings: number;\n cancellationRate: number;\n capacityUtilizationRate: number;\n bookingsOverTime: AnalyticsPoint[];\n topListings: ListingAnalytics[];\n};\n\nfunction toISODate(date: Date): string {\n return date.toISOString().slice(0, 10);\n}\n\nfunction daysAgo(days: number): Date {\n return new Date(Date.now() - days * 24 * 60 * 60 * 1000);\n}\n\n/**\n * BOFF-4390 — provider-facing analytics dashboard: bookings-over-time,\n * revenue, cancellation rate and capacity utilization, plus a per-listing\n * drill-down table. Self-scoped: only providers the calling actor created\n * are listed, and the analytics call is owner-verified server-side — a\n * provider can never see another provider's numbers. Forecasting and\n * custom-report building are explicitly out of scope for this core; see the\n * `myGgnomadProviderAnalytics` schema doc comment.\n */\nexport function ProviderAnalyticsPage() {\n const { t } = useI18n();\n\n const { data: providersData, loading: providersLoading } = useQuery(\n GGNOMAD_MY_PROVIDERS_QUERY as TypedDocumentNode<\n { myGgnomadProviders: MyProvider[] },\n Record<string, never>\n >\n );\n const providers = providersData?.myGgnomadProviders ?? [];\n\n // The chosen provider lives in `?id=` so the assistant's page context resolves\n // it (GGNOMAD_PROVIDER) and the view survives a reload or a shared link.\n const [focusedProviderId, setSelectedProviderId] = useFocusedRecordParam();\n const selectedProviderId = providers.some((provider) => provider.id === focusedProviderId)\n ? (focusedProviderId ?? '')\n : '';\n // `dateFrom`/`dateTo` are the plain YYYY-MM-DD strings bound to the <input\n // type=\"date\"> controls. They are NOT sent to GraphQL as-is: a bare date\n // string parses to UTC midnight, so using \"today\" as dateTo would exclude\n // every booking made later that same day. The query variables below\n // convert dateFrom to the start of that UTC day and dateTo to the end of\n // it, so \"today\" always includes today's activity.\n const [dateFrom, setDateFrom] = useState(() => toISODate(daysAgo(30)));\n const [dateTo, setDateTo] = useState(() => toISODate(new Date()));\n const [granularity, setGranularity] = useState<'day' | 'week' | 'month'>('day');\n\n const activeProviderId = selectedProviderId || providers[0]?.id || '';\n\n const { data: analyticsData, loading: analyticsLoading } = useQuery(\n GGNOMAD_MY_PROVIDER_ANALYTICS_QUERY as TypedDocumentNode<\n { myGgnomadProviderAnalytics: ProviderAnalytics },\n { providerId: string; dateFrom?: string; dateTo?: string; granularity?: string }\n >,\n {\n variables: {\n providerId: activeProviderId,\n dateFrom: `${dateFrom}T00:00:00.000Z`,\n dateTo: `${dateTo}T23:59:59.999Z`,\n granularity,\n },\n skip: !activeProviderId,\n }\n );\n const analytics = analyticsData?.myGgnomadProviderAnalytics;\n\n const maxTrend = useMemo(() => {\n const points = analytics?.bookingsOverTime ?? [];\n return points.length > 0 ? Math.max(...points.map((p) => p.bookings)) : 0;\n }, [analytics?.bookingsOverTime]);\n\n return (\n <PageShell\n title={t('ggnomad.providerAnalytics.title', 'Provider Analytics')}\n titleAccent={t('ggnomad.providerAnalytics.title.accent', 'Analytics')}\n subtitle={t(\n 'ggnomad.providerAnalytics.subtitle',\n 'Your bookings, revenue and listing performance'\n )}\n >\n <PagePurpose icon={<TrendingUp className=\"size-4\" />}>\n {t(\n 'ggnomad.providerAnalytics.purpose',\n 'A self-service dashboard for providers: bookings and revenue over time, cancellation rate, capacity utilization, and a per-listing breakdown — scoped to only the listings you own. Pick a date range and a listing granularity to drill in.'\n )}\n </PagePurpose>\n\n {!providersLoading && providers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.providerAnalytics.emptyTitle', 'No provider profile yet')}\n description={t(\n 'ggnomad.providerAnalytics.emptyDescription',\n 'Once you create a GGNomad provider profile, your bookings and revenue analytics will appear here.'\n )}\n />\n ) : (\n <>\n <SectionCard title={t('ggnomad.providerAnalytics.filters', 'Filters')}>\n <div className=\"grid gap-3 sm:grid-cols-4\">\n <div>\n <label\n htmlFor=\"provider-analytics-provider\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.provider', 'Provider')}\n </label>\n <select\n id=\"provider-analytics-provider\"\n value={activeProviderId}\n onChange={(e) => setSelectedProviderId(e.target.value)}\n className={INPUT_CLASSES}\n >\n {providers.map((provider) => (\n <option key={provider.id} value={provider.id}>\n {provider.name}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-from\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.from', 'From')}\n </label>\n <input\n id=\"provider-analytics-from\"\n type=\"date\"\n value={dateFrom}\n max={dateTo}\n onChange={(e) => setDateFrom(e.target.value)}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-to\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.to', 'To')}\n </label>\n <input\n id=\"provider-analytics-to\"\n type=\"date\"\n value={dateTo}\n min={dateFrom}\n max={toISODate(new Date())}\n onChange={(e) => setDateTo(e.target.value)}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-granularity\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.granularity', 'Granularity')}\n </label>\n <select\n id=\"provider-analytics-granularity\"\n value={granularity}\n onChange={(e) => setGranularity(e.target.value as 'day' | 'week' | 'month')}\n className={INPUT_CLASSES}\n >\n <option value=\"day\">{t('ggnomad.providerAnalytics.day', 'Day')}</option>\n <option value=\"week\">{t('ggnomad.providerAnalytics.week', 'Week')}</option>\n <option value=\"month\">{t('ggnomad.providerAnalytics.month', 'Month')}</option>\n </select>\n </div>\n </div>\n </SectionCard>\n\n {analyticsLoading ? (\n <div className=\"flex justify-center py-12\">\n <Spinner size=\"lg\" />\n </div>\n ) : !analytics ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.providerAnalytics.noDataTitle', 'No bookings in this range')}\n description={t(\n 'ggnomad.providerAnalytics.noDataDescription',\n 'Try widening the date range — analytics appear here once bookings come in for your listings.'\n )}\n />\n ) : (\n <>\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.providerAnalytics.totalBookings', 'Bookings')}\n value={analytics.totalBookings.toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.totalRevenue', 'Revenue')}\n value={`$${Number(analytics.totalRevenue).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.cancellationRate', 'Cancellation rate')}\n value={`${(analytics.cancellationRate * 100).toFixed(1)}%`}\n icon={<XCircle />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.capacityUtilization', 'Capacity utilization')}\n value={`${(analytics.capacityUtilizationRate * 100).toFixed(1)}%`}\n icon={<Gauge />}\n />\n </section>\n\n <SectionCard\n title={t('ggnomad.providerAnalytics.bookingsOverTime', 'Bookings over time')}\n >\n {analytics.bookingsOverTime.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.providerAnalytics.trendEmpty', 'No bookings in this window yet')}\n </p>\n </div>\n ) : (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-end gap-2 overflow-x-auto pb-2\">\n {analytics.bookingsOverTime.map((point, index) => {\n const pct =\n maxTrend > 0 ? Math.max((point.bookings / maxTrend) * 100, 5) : 5;\n return (\n <div key={point.period} className=\"flex flex-col items-center gap-1\">\n <div className=\"flex h-24 items-end\">\n <div\n className=\"w-8 rounded-t transition-all duration-500 ease-out\"\n style={{\n height: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n title={`${point.period}: ${point.bookings} bookings, $${point.revenue.toLocaleString()} revenue`}\n />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">{point.period}</p>\n </div>\n );\n })}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'ggnomad.providerAnalytics.trendNote',\n 'Bar height is booking volume per period; hover a bar for its exact bookings and revenue.'\n )}\n </p>\n </div>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.providerAnalytics.topListings', 'Top listings')}>\n {analytics.topListings.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t(\n 'ggnomad.providerAnalytics.topListingsEmpty',\n 'No listing bookings in this range yet.'\n )}\n </p>\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-left text-text-secondary\">\n <th className=\"py-2 pr-3 font-medium\">\n {t('ggnomad.providerAnalytics.listing', 'Listing')}\n </th>\n <th className=\"py-2 pr-3 font-medium\">\n {t('ggnomad.providerAnalytics.type', 'Type')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.bookings', 'Bookings')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.revenue', 'Revenue')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.cancellation', 'Cancellation')}\n </th>\n <th className=\"py-2 text-right font-medium\">\n {t('ggnomad.providerAnalytics.utilization', 'Utilization')}\n </th>\n </tr>\n </thead>\n <tbody>\n {analytics.topListings.map((listing) => (\n <tr\n key={listing.listingId}\n className=\"border-b border-border-seam/50 last:border-0\"\n >\n <td className=\"py-2 pr-3 font-medium text-text-primary\">\n {listing.listingName}\n </td>\n <td className=\"py-2 pr-3 text-text-secondary\">{listing.listingType}</td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n {listing.bookings}\n </td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n ${listing.revenue.toLocaleString()}\n </td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n {(listing.cancellationRate * 100).toFixed(0)}%\n </td>\n <td className=\"py-2 text-right tabular-nums text-text-primary\">\n {(listing.capacityUtilizationRate * 100).toFixed(0)}%\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </SectionCard>\n </>\n )}\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,IAAM,IAAe;CACnB;CACA;CACA;CACA;CACA;AACF,GAEM,IACJ;AA4BF,SAAS,EAAU,GAAoB;CACrC,OAAO,EAAK,YAAY,CAAC,CAAC,MAAM,GAAG,EAAE;AACvC;AAEA,SAAS,EAAQ,GAAoB;CACnC,uBAAO,IAAI,KAAK,KAAK,IAAI,IAAI,IAAO,KAAK,KAAK,KAAK,GAAI;AACzD;AAWA,SAAgB,IAAwB;CACtC,IAAM,EAAE,SAAM,EAAQ,GAEhB,EAAE,MAAM,GAAe,SAAS,MAAqB,EACzD,CAIF,GACM,IAAY,GAAe,sBAAsB,CAAC,GAIlD,CAAC,GAAmB,KAAyB,EAAsB,GACnE,IAAqB,EAAU,MAAM,MAAa,EAAS,OAAO,CAAiB,IACpF,KAAqB,KACtB,IAOE,CAAC,GAAU,KAAe,QAAe,EAAU,EAAQ,EAAE,CAAC,CAAC,GAC/D,CAAC,GAAQ,KAAa,QAAe,kBAAU,IAAI,KAAK,CAAC,CAAC,GAC1D,CAAC,GAAa,KAAkB,EAAmC,KAAK,GAExE,IAAmB,KAAsB,EAAU,EAAE,EAAE,MAAM,IAE7D,EAAE,MAAM,GAAe,SAAS,MAAqB,EACzD,GAIA;EACE,WAAW;GACT,YAAY;GACZ,UAAU,GAAG,EAAS;GACtB,QAAQ,GAAG,EAAO;GAClB;EACF;EACA,MAAM,CAAC;CACT,CACF,GACM,IAAY,GAAe,4BAE3B,IAAW,QAAc;EAC7B,IAAM,IAAS,GAAW,oBAAoB,CAAC;EAC/C,OAAO,EAAO,SAAS,IAAI,KAAK,IAAI,GAAG,EAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,IAAI;CAC1E,GAAG,CAAC,GAAW,gBAAgB,CAAC;CAEhC,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,mCAAmC,oBAAoB;EAChE,aAAa,EAAE,0CAA0C,WAAW;EACpE,UAAU,EACR,sCACA,gDACF;YANF,CAQE,kBAAC,GAAD;GAAa,MAAM,kBAAC,GAAD,EAAY,WAAU,SAAU,CAAA;aAChD,EACC,qCACA,8OACF;EACW,CAAA,GAEZ,CAAC,KAAoB,EAAU,WAAW,IACzC,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,wCAAwC,yBAAyB;GAC1E,aAAa,EACX,8CACA,mGACF;EACD,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;GAAa,OAAO,EAAE,qCAAqC,SAAS;aAClE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,sCAAsC,UAAU;KAC9C,CAAA,GACP,kBAAC,UAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAsB,EAAE,OAAO,KAAK;MACrD,WAAW;gBAEV,EAAU,KAAK,MACd,kBAAC,UAAD;OAA0B,OAAO,EAAS;iBACvC,EAAS;MACJ,GAFK,EAAS,EAEd,CACT;KACK,CAAA,CACL,EAAA,CAAA;KACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,kCAAkC,MAAM;KACtC,CAAA,GACP,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,KAAK;MACL,WAAW,MAAM,EAAY,EAAE,OAAO,KAAK;MAC3C,WAAW;KACZ,CAAA,CACE,EAAA,CAAA;KACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,gCAAgC,IAAI;KAClC,CAAA,GACP,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,KAAK;MACL,KAAK,kBAAU,IAAI,KAAK,CAAC;MACzB,WAAW,MAAM,EAAU,EAAE,OAAO,KAAK;MACzC,WAAW;KACZ,CAAA,CACE,EAAA,CAAA;KACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,yCAAyC,aAAa;KACpD,CAAA,GACP,kBAAC,UAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAe,EAAE,OAAO,KAAiC;MAC1E,WAAW;gBAJb;OAME,kBAAC,UAAD;QAAQ,OAAM;kBAAO,EAAE,iCAAiC,KAAK;OAAU,CAAA;OACvE,kBAAC,UAAD;QAAQ,OAAM;kBAAQ,EAAE,kCAAkC,MAAM;OAAU,CAAA;OAC1E,kBAAC,UAAD;QAAQ,OAAM;kBAAS,EAAE,mCAAmC,OAAO;OAAU,CAAA;MACvE;OACL,EAAA,CAAA;IACF;;EACM,CAAA,GAEZ,IACC,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA;EACjB,CAAA,IACF,IAUH,kBAAA,GAAA,EAAA,UAAA;GACE,kBAAC,WAAD;IAAS,WAAU;cAAnB;KACE,kBAAC,GAAD;MACE,MAAA;MACA,OAAO,EAAE,2CAA2C,UAAU;MAC9D,OAAO,EAAU,cAAc,eAAe;MAC9C,MAAM,kBAAC,GAAD,CAAgB,CAAA;KACvB,CAAA;KACD,kBAAC,GAAD;MACE,OAAO,EAAE,0CAA0C,SAAS;MAC5D,OAAO,IAAI,OAAO,EAAU,YAAY,CAAC,CAAC,eAAe;MACzD,MAAM,kBAAC,GAAD,CAAa,CAAA;KACpB,CAAA;KACD,kBAAC,GAAD;MACE,OAAO,EAAE,8CAA8C,mBAAmB;MAC1E,OAAO,IAAI,EAAU,mBAAmB,IAAA,CAAK,QAAQ,CAAC,EAAE;MACxD,MAAM,kBAAC,GAAD,CAAU,CAAA;KACjB,CAAA;KACD,kBAAC,GAAD;MACE,OAAO,EAAE,iDAAiD,sBAAsB;MAChF,OAAO,IAAI,EAAU,0BAA0B,IAAA,CAAK,QAAQ,CAAC,EAAE;MAC/D,MAAM,kBAAC,GAAD,CAAQ,CAAA;KACf,CAAA;IACM;;GAET,kBAAC,GAAD;IACE,OAAO,EAAE,8CAA8C,oBAAoB;cAE1E,EAAU,iBAAiB,WAAW,IACrC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBACd,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA;KAC3B,CAAA,GACN,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,wCAAwC,gCAAgC;KAC1E,CAAA,CACA;SAEL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAU,iBAAiB,KAAK,GAAO,MAIpC,kBAAC,OAAD;OAAwB,WAAU;iBAAlC,CACE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SACE,WAAU;SACV,OAAO;UACL,QAAQ,GAPhB,IAAW,IAAI,KAAK,IAAK,EAAM,WAAW,IAAY,KAAK,CAAC,IAAI,EAOzC;UACf,iBAAiB,EAAa,IAAQ,EAAa;SACrD;SACA,OAAO,GAAG,EAAM,OAAO,IAAI,EAAM,SAAS,cAAc,EAAM,QAAQ,eAAe,EAAE;QACxF,CAAA;OACE,CAAA,GACL,kBAAC,KAAD;QAAG,WAAU;kBAAwC,EAAM;OAAU,CAAA,CAClE;SAZK,EAAM,MAYX,CAER;KACE,CAAA,GACL,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,uCACA,0FACF;KACC,CAAA,CACA;;GAEI,CAAA;GAEb,kBAAC,GAAD;IAAa,OAAO,EAAE,yCAAyC,cAAc;cAC1E,EAAU,YAAY,WAAW,IAChC,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,8CACA,wCACF;IACC,CAAA,IAEH,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,qCAAqC,SAAS;QAC/C,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,kCAAkC,MAAM;QACzC,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,sCAAsC,UAAU;QACjD,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,qCAAqC,SAAS;QAC/C,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,0CAA0C,cAAc;QACzD,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,yCAAyC,aAAa;QACvD,CAAA;OACF;SACC,CAAA,GACP,kBAAC,SAAD,EAAA,UACG,EAAU,YAAY,KAAK,MAC1B,kBAAC,MAAD;OAEE,WAAU;iBAFZ;QAIE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAQ;QACP,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAiC,EAAQ;QAAgB,CAAA;QACvE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAQ;QACP,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd,CAAoE,KAChE,EAAQ,QAAQ,eAAe,CAC/B;;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd,EACI,EAAQ,mBAAmB,IAAA,CAAK,QAAQ,CAAC,GAAE,GAC3C;;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd,EACI,EAAQ,0BAA0B,IAAA,CAAK,QAAQ,CAAC,GAAE,GAClD;;OACF;SAnBG,EAAQ,SAmBX,CACL,EACI,CAAA,CACF;;IACJ,CAAA;GAEI,CAAA;EACb,EAAA,CAAA,IA7IF,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,yCAAyC,2BAA2B;GAC7E,aAAa,EACX,+CACA,8FACF;EACD,CAAA,CAwIH,EAAA,CAAA,CAEK;;AAEf"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the GGNomad assistant does when a quota refuses it.
|
|
3
|
+
*
|
|
4
|
+
* Two allowances gate the assistant, both provisioned per plan in
|
|
5
|
+
* global-billing-svc: `ggnomad.ai.credits` (the per-token AI credit budget,
|
|
6
|
+
* charged after each answer) and `ggnomad.sandbox.environments` (the sandbox
|
|
7
|
+
* the assistant runs in). When either is used up the user must be told plainly
|
|
8
|
+
* and shown the way to more, never left with a generic failure:
|
|
9
|
+
*
|
|
10
|
+
* - the shell's `QuotaUpgradeBridge` (ggnomad-app) listens for
|
|
11
|
+
* `ggnomad:quota-exhausted` and opens ONE upgrade dialog with GGNomad's copy;
|
|
12
|
+
* - the chat itself shows an explicit "AI credits used up" answer instead of
|
|
13
|
+
* the gateway's raw refusal text.
|
|
14
|
+
*
|
|
15
|
+
* The transport can see the same wall twice in one send (the advisory credits
|
|
16
|
+
* preflight and the turn itself), so the dialog is announced at most once per
|
|
17
|
+
* {@link ANNOUNCE_WINDOW_MS}: one refusal, one dialog, no stacked prompts.
|
|
18
|
+
*/
|
|
19
|
+
/** The product id the shared assistant and the shell's bridge use for GGNomad. */
|
|
20
|
+
export declare const GGNOMAD_ASSISTANT_PRODUCT = "ggnomad";
|
|
21
|
+
/** Quota names in global-billing-svc that gate the GGNomad assistant. */
|
|
22
|
+
export declare const GGNOMAD_AI_CREDITS_QUOTA = "ggnomad.ai.credits";
|
|
23
|
+
export declare const GGNOMAD_SANDBOX_QUOTA = "ggnomad.sandbox.environments";
|
|
24
|
+
/**
|
|
25
|
+
* The shared assistant's product-namespaced window events (fe-libs
|
|
26
|
+
* `quotaExhaustedEventName` / `aiCreditsUpdatedEventName`: the shared code owns
|
|
27
|
+
* the mechanism, the product owns the `<product>:` namespace). The shell's
|
|
28
|
+
* `QuotaUpgradeBridge` listens for the first; a credits meter for the second.
|
|
29
|
+
*/
|
|
30
|
+
export declare const QUOTA_EXHAUSTED_EVENT = "ggnomad:quota-exhausted";
|
|
31
|
+
export declare const AI_CREDITS_UPDATED_EVENT = "ggnomad:ai-credits-updated";
|
|
32
|
+
/** Repeated refusals inside this window open no further dialog. */
|
|
33
|
+
export declare const ANNOUNCE_WINDOW_MS = 10000;
|
|
34
|
+
export type AssistantQuotaWall = 'ai-credits' | 'sandbox';
|
|
35
|
+
/** English fallbacks; the i18n keys are looked up first. */
|
|
36
|
+
export declare const QUOTA_MESSAGES: Record<AssistantQuotaWall, {
|
|
37
|
+
key: string;
|
|
38
|
+
fallback: string;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Which assistant wall an error is, or null when it is not an exhausted quota.
|
|
42
|
+
*
|
|
43
|
+
* A quota that was never provisioned (`QUOTA_NOT_PROVISIONED`) is not
|
|
44
|
+
* exhaustion: buying more would not unblock it, so it is left to the generic
|
|
45
|
+
* error path rather than shown as "credits used up".
|
|
46
|
+
*/
|
|
47
|
+
export declare function assistantQuotaWallOf(error: unknown): AssistantQuotaWall | null;
|
|
48
|
+
/**
|
|
49
|
+
* Every `extensions.quotaName` the gateway attached to a refusal, wherever the
|
|
50
|
+
* raw-fetch transport put the GraphQL errors (`errors`, `result.errors`,
|
|
51
|
+
* `graphQLErrors`, or the error's `cause`).
|
|
52
|
+
*/
|
|
53
|
+
export declare function quotaNamesIn(error: unknown): string[];
|
|
54
|
+
/** Opens the shell's upgrade dialog for this wall, at most once per window. */
|
|
55
|
+
export declare function announceAssistantQuotaWall(wall: AssistantQuotaWall, now?: number): boolean;
|
|
56
|
+
/** Test seam: forget the last announcement. */
|
|
57
|
+
export declare function resetAssistantQuotaAnnouncements(): void;
|
|
58
|
+
/** `onQuotaExhausted` for the shared transport (the advisory credits preflight). */
|
|
59
|
+
export declare function handleAssistantQuotaExhausted(error: unknown): void;
|
|
60
|
+
/** `onCreditsUpdated` for the shared transport: lets a credits meter refetch. */
|
|
61
|
+
export declare function handleAssistantCreditsUpdated(): void;
|
|
62
|
+
type Translate = (key: string, fallback: string) => string;
|
|
63
|
+
/**
|
|
64
|
+
* The error a failed turn rethrows. A quota wall becomes an explicit message
|
|
65
|
+
* (and announces the dialog); anything else is returned unchanged.
|
|
66
|
+
*/
|
|
67
|
+
export declare function toAssistantTurnError(error: unknown, translate?: Translate): unknown;
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=assistantQuota.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistantQuota.d.ts","sourceRoot":"","sources":["../../src/services/assistantQuota.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,kFAAkF;AAClF,eAAO,MAAM,yBAAyB,YAAY,CAAC;AAEnD,yEAAyE;AACzE,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAC7D,eAAO,MAAM,qBAAqB,iCAAiC,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,4BAAiD,CAAC;AACpF,eAAO,MAAM,wBAAwB,+BAAoD,CAAC;AAE1F,mEAAmE;AACnE,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,SAAS,CAAC;AAE1D,4DAA4D;AAC5D,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,kBAAkB,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAWxF,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAI9E;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAErD;AA+BD,+EAA+E;AAC/E,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,kBAAkB,EACxB,GAAG,GAAE,MAAmB,GACvB,OAAO,CAOT;AAED,+CAA+C;AAC/C,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD;AAED,oFAAoF;AACpF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAElE;AAED,iFAAiF;AACjF,wBAAgB,6BAA6B,IAAI,IAAI,CAGpD;AAED,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,SAAS,GAAG,OAAO,CAQnF"}
|