@burdenoff/microfe-ggnomad 2026.830.1 → 2026.1016.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/ggnomad/components/CreateProviderDialog.d.ts +22 -0
  2. package/dist/ggnomad/components/CreateProviderDialog.d.ts.map +1 -0
  3. package/dist/ggnomad/components/CreateProviderDialog.js +209 -0
  4. package/dist/ggnomad/components/CreateProviderDialog.js.map +1 -0
  5. package/dist/ggnomad/components/ProviderDashboardCard.d.ts +28 -0
  6. package/dist/ggnomad/components/ProviderDashboardCard.d.ts.map +1 -0
  7. package/dist/ggnomad/components/ProviderDashboardCard.js +222 -0
  8. package/dist/ggnomad/components/ProviderDashboardCard.js.map +1 -0
  9. package/dist/ggnomad/components/index.d.ts +2 -0
  10. package/dist/ggnomad/components/index.d.ts.map +1 -1
  11. package/dist/ggnomad/components/index.js +2 -0
  12. package/dist/ggnomad/graphql.d.ts +1 -0
  13. package/dist/ggnomad/graphql.d.ts.map +1 -1
  14. package/dist/ggnomad/graphql.js +65 -47
  15. package/dist/ggnomad/graphql.js.map +1 -1
  16. package/dist/ggnomad/pages/ProviderPage.d.ts.map +1 -1
  17. package/dist/ggnomad/pages/ProviderPage.js +249 -134
  18. package/dist/ggnomad/pages/ProviderPage.js.map +1 -1
  19. package/dist/ggnomad/utils/format.d.ts +18 -0
  20. package/dist/ggnomad/utils/format.d.ts.map +1 -1
  21. package/dist/ggnomad/utils/format.js +23 -7
  22. package/dist/ggnomad/utils/format.js.map +1 -1
  23. package/dist/ggnomad/utils/providerListControls.d.ts +53 -0
  24. package/dist/ggnomad/utils/providerListControls.d.ts.map +1 -0
  25. package/dist/ggnomad/utils/providerListControls.js +50 -0
  26. package/dist/ggnomad/utils/providerListControls.js.map +1 -0
  27. package/dist/ggnomad/utils/providerTypeMeta.d.ts +79 -0
  28. package/dist/ggnomad/utils/providerTypeMeta.d.ts.map +1 -0
  29. package/dist/ggnomad/utils/providerTypeMeta.js +81 -0
  30. package/dist/ggnomad/utils/providerTypeMeta.js.map +1 -0
  31. package/package.json +1 -1
@@ -1,145 +1,260 @@
1
1
  import { GGNOMAD_PROVIDERS_QUERY as e } from "../graphql.js";
2
- import { formatDate as t } from "../utils/format.js";
3
- import { PageShell as n } from "./PageShell.js";
4
- import { StatusBadge as r } from "../components/StatusBadge.js";
5
- import { EntityCard as i } from "../components/EntityCard.js";
6
- import { LoadMoreButton as a } from "../components/LoadMoreButton.js";
7
- import { RatingSummary as o } from "../components/RatingSummary.js";
2
+ import { PageShell as t } from "./PageShell.js";
3
+ import { LoadMoreButton as n } from "../components/LoadMoreButton.js";
4
+ import { notify as r } from "../utils/notify.js";
5
+ import { getProviderTypeOptions as i } from "../utils/providerTypeMeta.js";
6
+ import { CreateProviderDialog as a } from "../components/CreateProviderDialog.js";
7
+ import { ProviderDashboardCard as o } from "../components/ProviderDashboardCard.js";
8
8
  import "../components/index.js";
9
- import { useState as s } from "react";
10
- import { Briefcase as c, CalendarPlus as l, Globe as u, Mail as d, Phone as f, Store as p } from "lucide-react";
11
- import { useI18n as m } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
12
- import { Link as h } from "react-router";
13
- import { useQuery as g } from "@apollo/client/react";
14
- import { GlassCard as _, IllustratedEmptyState as v, PagePurpose as y } from "@burdenoff/fe-libs/ui";
15
- import { jsx as b, jsxs as x } from "react/jsx-runtime";
9
+ import { DEFAULT_PROVIDER_QUERY as s, applyProviderControls as c } from "../utils/providerListControls.js";
10
+ import { useCallback as l, useEffect as u, useMemo as d, useRef as f, useState as p } from "react";
11
+ import { Briefcase as m, Plus as h, Search as g, X as _ } from "lucide-react";
12
+ import { useI18n as ee } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
13
+ import { useApolloClient as te } from "@apollo/client/react";
14
+ import { CTAOverflowMenu as ne, GlassCard as v, IllustratedEmptyState as y, PagePurpose as b } from "@burdenoff/fe-libs/ui";
15
+ import { jsx as x, jsxs as S } from "react/jsx-runtime";
16
16
  //#region src/ggnomad/pages/ProviderPage.tsx
17
- function S() {
18
- let { t: S } = m(), [C, w] = s(12), { data: T } = g(e, { variables: {
19
- limit: C,
20
- offset: 0
21
- } }), E = T?.ggnomadProviders ?? [];
22
- return /* @__PURE__ */ x(n, {
23
- title: S("ggnomad.provider.title", "Provider Dashboard"),
24
- titleAccent: S("ggnomad.provider.title.accent", "Provider"),
25
- subtitle: S("ggnomad.provider.subtitle", "B2B provider operations and onboarding metrics"),
26
- children: [/* @__PURE__ */ b(y, {
27
- icon: /* @__PURE__ */ b(c, { className: "size-4" }),
28
- children: S("ggnomad.provider.purpose", "Track the supply side of the marketplace — the hotels, tour operators and venues that publish inventory. Use it to monitor each provider’s status, listing count, revenue and rating, and to spot who needs onboarding attention.")
29
- }), E.length === 0 ? /* @__PURE__ */ b(v, {
30
- illustration: "empty-people",
31
- title: S("ggnomad.provider.emptyTitle", "No providers onboarded"),
32
- description: S("ggnomad.provider.emptyDescription", "Onboarded B2B providers and their performance will be tracked here.")
33
- }) : /* @__PURE__ */ x(_, {
34
- treatment: "glass",
35
- glow: !0,
36
- className: "border-border-seam p-5",
37
- children: [
38
- /* @__PURE__ */ b("h2", {
39
- className: "mb-4 text-lg font-medium tracking-tight text-text-primary",
40
- children: S("ggnomad.provider.onboardedProviders", "Onboarded providers")
41
- }),
42
- /* @__PURE__ */ b("div", {
43
- className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3",
44
- children: E.map((e) => /* @__PURE__ */ b(i, {
45
- title: e.name,
46
- meta: e.description ?? e.type,
47
- badge: /* @__PURE__ */ b(r, {
48
- status: e.status,
49
- live: e.status === "ACTIVE"
50
- }),
51
- footer: /* @__PURE__ */ x("div", {
52
- className: "space-y-2 text-xs",
53
- children: [
54
- /* @__PURE__ */ b(o, {
55
- rating: e.rating,
56
- reviewCount: e.reviewCount
57
- }),
58
- /* @__PURE__ */ x("dl", {
59
- className: "grid grid-cols-3 gap-2",
60
- children: [
61
- /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("dt", {
62
- className: "text-text-secondary",
63
- children: S("ggnomad.provider.listings", "Listings")
64
- }), /* @__PURE__ */ b("dd", {
65
- className: "mt-0.5 font-medium tabular-nums text-text-primary",
66
- children: e.listingCount
67
- })] }),
68
- /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("dt", {
69
- className: "text-text-secondary",
70
- children: S("ggnomad.provider.revenue", "Revenue")
71
- }), /* @__PURE__ */ x("dd", {
72
- className: "mt-0.5 font-medium tabular-nums text-text-primary",
73
- children: ["$", Number(e.totalRevenue).toLocaleString()]
74
- })] }),
75
- /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("dt", {
76
- className: "text-text-secondary",
77
- children: S("ggnomad.provider.type", "Type")
78
- }), /* @__PURE__ */ b("dd", {
79
- className: "mt-0.5 font-medium text-text-primary",
80
- children: e.type
81
- })] })
82
- ]
83
- }),
84
- /* @__PURE__ */ x("p", {
85
- className: "inline-flex items-center gap-1 text-text-secondary",
86
- children: [/* @__PURE__ */ b(l, {
87
- className: "size-3.5",
88
- "aria-hidden": "true"
89
- }), S("ggnomad.provider.joined", "Joined {{date}}").replace("{{date}}", t(e.createdAt) ?? "—")]
90
- }),
91
- e.contactEmail || e.contactPhone || e.website ? /* @__PURE__ */ x("div", {
92
- className: "flex flex-wrap items-center gap-x-3 gap-y-1",
17
+ var C = 100, w = 20, T = 250, E = "rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg", D = E, O = "rounded-full border border-border-seam px-3 py-1.5 text-sm transition-colors", k = " border-border-seam bg-bg-sunken font-medium text-text-primary", A = " text-text-secondary hover:bg-bg-sunken", j = [
18
+ {
19
+ value: "newest",
20
+ labelKey: "ggnomad.provider.toolbar.sortNewest",
21
+ labelFallback: "Newest first"
22
+ },
23
+ {
24
+ value: "oldest",
25
+ labelKey: "ggnomad.provider.toolbar.sortOldest",
26
+ labelFallback: "Oldest first"
27
+ },
28
+ {
29
+ value: "nameAsc",
30
+ labelKey: "ggnomad.provider.toolbar.sortNameAsc",
31
+ labelFallback: "Name A to Z"
32
+ },
33
+ {
34
+ value: "nameDesc",
35
+ labelKey: "ggnomad.provider.toolbar.sortNameDesc",
36
+ labelFallback: "Name Z to A"
37
+ },
38
+ {
39
+ value: "revenueDesc",
40
+ labelKey: "ggnomad.provider.toolbar.sortRevenue",
41
+ labelFallback: "Most revenue"
42
+ }
43
+ ];
44
+ function M() {
45
+ let { t: M } = ee(), N = te(), [P, re] = p(null), [ie, F] = p(!1), [I, L] = p(!1), R = f(null);
46
+ u(() => {
47
+ R.current = P;
48
+ }, [P]);
49
+ let [z, B] = p(""), [V, H] = p(""), [U, W] = p(null), [G, K] = p(s.sort), [q, J] = p(12), Y = f(null), X = l(async () => {
50
+ try {
51
+ let t = [], n = 0;
52
+ for (let r = 0; r < w; r += 1) {
53
+ let { data: r } = await N.query({
54
+ query: e,
55
+ variables: {
56
+ limit: C,
57
+ offset: n
58
+ },
59
+ fetchPolicy: "network-only"
60
+ }), i = r?.ggnomadProviders ?? [];
61
+ if (t.push(...i), i.length < C) break;
62
+ n += C;
63
+ }
64
+ re(t), F(!1);
65
+ } catch {
66
+ R.current === null ? F(!0) : r.error(M("ggnomad.provider.reloadError", "Could not refresh provider list"), "");
67
+ }
68
+ }, [N, M]);
69
+ u(() => {
70
+ let e = setTimeout(() => void X(), 0);
71
+ return () => clearTimeout(e);
72
+ }, [X]), u(() => () => {
73
+ Y.current && clearTimeout(Y.current);
74
+ }, []);
75
+ let ae = (e) => {
76
+ B(e), Y.current && clearTimeout(Y.current), Y.current = setTimeout(() => {
77
+ H(e), J(12);
78
+ }, T);
79
+ }, oe = () => {
80
+ Y.current && clearTimeout(Y.current), B(""), H(""), J(12);
81
+ }, Z = (e) => {
82
+ W(e), J(12);
83
+ }, se = (e) => {
84
+ K(e), J(12);
85
+ }, Q = d(() => ({
86
+ search: V,
87
+ type: U,
88
+ sort: G
89
+ }), [
90
+ V,
91
+ U,
92
+ G
93
+ ]), $ = d(() => P === null ? null : c(P, Q, q), [
94
+ P,
95
+ Q,
96
+ q
97
+ ]), ce = () => L(!0), le = async () => {
98
+ await X();
99
+ }, ue = () => {
100
+ Y.current && clearTimeout(Y.current), B(""), H(""), W(null), K(s.sort), J(12);
101
+ }, de = i(M);
102
+ return /* @__PURE__ */ S(t, {
103
+ title: M("ggnomad.provider.title", "Provider Dashboard"),
104
+ titleAccent: M("ggnomad.provider.title.accent", "Provider"),
105
+ subtitle: M("ggnomad.provider.subtitle", "B2B provider operations and onboarding metrics"),
106
+ actions: /* @__PURE__ */ x(ne, {
107
+ primary: {
108
+ label: M("ggnomad.provider.actions.newProvider", "New provider"),
109
+ onSelect: ce,
110
+ icon: /* @__PURE__ */ x(h, {})
111
+ },
112
+ actions: []
113
+ }),
114
+ children: [
115
+ /* @__PURE__ */ x(b, {
116
+ icon: /* @__PURE__ */ x(m, { className: "size-4" }),
117
+ children: M("ggnomad.provider.purpose", "Track the supply side of the marketplace — the hotels, tour operators and venues that publish inventory. Use it to monitor each provider’s status, listing count, revenue and rating, and to spot who needs onboarding attention.")
118
+ }),
119
+ ie ? /* @__PURE__ */ x("p", {
120
+ className: "text-sm text-text-secondary",
121
+ children: M("ggnomad.provider.loadError", "Providers could not be loaded. Refresh the page to try again.")
122
+ }) : P === null ? /* @__PURE__ */ x("p", {
123
+ className: "text-sm text-text-secondary",
124
+ children: M("ggnomad.provider.loading", "Loading providers…")
125
+ }) : P.length === 0 ? /* @__PURE__ */ x(y, {
126
+ illustration: "empty-people",
127
+ title: M("ggnomad.provider.emptyTitle", "No providers onboarded"),
128
+ description: M("ggnomad.provider.emptyDescription", "Onboarded B2B providers and their performance will be tracked here.")
129
+ }) : $ && /* @__PURE__ */ S(v, {
130
+ treatment: "glass",
131
+ glow: !0,
132
+ className: "border-border-seam p-5",
133
+ children: [
134
+ /* @__PURE__ */ x("h2", {
135
+ className: "mb-4 text-lg font-medium tracking-tight text-text-primary",
136
+ children: M("ggnomad.provider.onboardedProviders", "Onboarded providers")
137
+ }),
138
+ /* @__PURE__ */ S("div", {
139
+ className: "mb-4 flex flex-col gap-3",
140
+ children: [
141
+ /* @__PURE__ */ S("div", {
142
+ className: "flex flex-wrap items-center gap-2",
143
+ children: [/* @__PURE__ */ S("div", {
144
+ className: "relative w-full sm:w-72",
93
145
  children: [
94
- e.contactEmail ? /* @__PURE__ */ x("a", {
95
- href: `mailto:${e.contactEmail}`,
96
- className: "inline-flex items-center gap-1 text-action-primary-bg hover:underline",
97
- children: [/* @__PURE__ */ b(d, {
98
- className: "size-3.5",
146
+ /* @__PURE__ */ x(g, {
147
+ className: "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-text-secondary",
148
+ "aria-hidden": "true"
149
+ }),
150
+ /* @__PURE__ */ x("input", {
151
+ id: "provider-search",
152
+ type: "text",
153
+ value: z,
154
+ onChange: (e) => ae(e.target.value),
155
+ placeholder: M("ggnomad.provider.toolbar.searchPlaceholder", "Search providers"),
156
+ "aria-label": M("ggnomad.provider.toolbar.searchLabel", "Search providers"),
157
+ className: `${E} w-full pl-9 pr-8`
158
+ }),
159
+ z && /* @__PURE__ */ x("button", {
160
+ type: "button",
161
+ onClick: oe,
162
+ "aria-label": M("ggnomad.provider.toolbar.clearSearch", "Clear search"),
163
+ className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary",
164
+ children: /* @__PURE__ */ x(_, {
165
+ className: "size-4",
99
166
  "aria-hidden": "true"
100
- }), e.contactEmail]
101
- }) : null,
102
- e.contactPhone ? /* @__PURE__ */ x("a", {
103
- href: `tel:${e.contactPhone}`,
104
- className: "inline-flex items-center gap-1 text-action-primary-bg hover:underline",
105
- children: [/* @__PURE__ */ b(f, {
106
- className: "size-3.5",
107
- "aria-hidden": "true"
108
- }), e.contactPhone]
109
- }) : null,
110
- e.website ? /* @__PURE__ */ x("a", {
111
- href: e.website,
112
- target: "_blank",
113
- rel: "noreferrer noopener",
114
- className: "inline-flex items-center gap-1 text-action-primary-bg hover:underline",
115
- children: [/* @__PURE__ */ b(u, {
116
- className: "size-3.5",
117
- "aria-hidden": "true"
118
- }), S("ggnomad.provider.website", "Website")]
119
- }) : null
167
+ })
168
+ })
120
169
  ]
121
- }) : null,
122
- /* @__PURE__ */ b("div", {
123
- className: "mt-1 border-t border-border-seam pt-2",
124
- children: /* @__PURE__ */ x(h, {
125
- to: `${e.id}/storefront`,
126
- className: "inline-flex items-center gap-1 text-xs font-medium text-action-primary-bg hover:underline",
127
- children: [/* @__PURE__ */ b(p, { className: "size-3.5" }), S("ggnomad.provider.manageStorefront", "Manage storefront")]
128
- })
129
- })
130
- ]
131
- })
132
- }, e.id))
133
- }),
134
- /* @__PURE__ */ b(a, {
135
- visible: E.length >= C,
136
- onLoadMore: () => w((e) => e + 12)
137
- })
138
- ]
139
- })]
170
+ }), /* @__PURE__ */ x("select", {
171
+ id: "provider-sort",
172
+ value: G,
173
+ onChange: (e) => se(e.target.value),
174
+ "aria-label": M("ggnomad.provider.toolbar.sortLabel", "Sort providers"),
175
+ className: D,
176
+ children: j.map((e) => /* @__PURE__ */ x("option", {
177
+ value: e.value,
178
+ children: M(e.labelKey, e.labelFallback)
179
+ }, e.value))
180
+ })]
181
+ }),
182
+ /* @__PURE__ */ S("div", {
183
+ className: "flex flex-wrap gap-2",
184
+ role: "group",
185
+ "aria-label": M("ggnomad.provider.toolbar.typeLabel", "Filter by type"),
186
+ children: [/* @__PURE__ */ x("button", {
187
+ type: "button",
188
+ onClick: () => Z(null),
189
+ "aria-pressed": U === null,
190
+ className: O + (U === null ? k : A),
191
+ children: M("ggnomad.provider.toolbar.allTypes", "All")
192
+ }), de.map((e) => {
193
+ let t = U === e.value;
194
+ return /* @__PURE__ */ x("button", {
195
+ type: "button",
196
+ onClick: () => Z(e.value),
197
+ "aria-pressed": t,
198
+ className: O + (t ? k : A),
199
+ children: e.label
200
+ }, e.value);
201
+ })]
202
+ }),
203
+ /* @__PURE__ */ S("p", {
204
+ className: "text-xs text-text-secondary",
205
+ "aria-live": "polite",
206
+ children: [M("ggnomad.provider.toolbar.showing", "Showing {{shown}} of {{total}} providers", {
207
+ shown: $.shown.length,
208
+ total: $.total
209
+ }), $.isFiltered ? M("ggnomad.provider.toolbar.filteredSuffix", " (filtered)") : ""]
210
+ })
211
+ ]
212
+ }),
213
+ $.total === 0 ? /* @__PURE__ */ S("div", {
214
+ className: "flex flex-col items-center gap-3 py-12 text-center",
215
+ children: [/* @__PURE__ */ x("p", {
216
+ className: "text-sm text-text-secondary",
217
+ children: M("ggnomad.provider.toolbar.noMatches", "No providers match your filters")
218
+ }), /* @__PURE__ */ x("button", {
219
+ type: "button",
220
+ onClick: ue,
221
+ className: "rounded-lg border border-border-seam bg-bg-surface px-5 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg",
222
+ children: M("ggnomad.provider.toolbar.clearFilters", "Clear filters")
223
+ })]
224
+ }) : /* @__PURE__ */ x("div", {
225
+ className: "grid gap-4",
226
+ style: { gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))" },
227
+ children: $.shown.map((e) => /* @__PURE__ */ x(o, {
228
+ id: e.id,
229
+ name: e.name,
230
+ type: e.type,
231
+ status: e.status,
232
+ description: e.description,
233
+ listingCount: e.listingCount,
234
+ totalRevenue: e.totalRevenue,
235
+ rating: e.rating,
236
+ reviewCount: e.reviewCount,
237
+ contactEmail: e.contactEmail,
238
+ contactPhone: e.contactPhone,
239
+ website: e.website,
240
+ createdAt: e.createdAt
241
+ }, e.id))
242
+ }),
243
+ /* @__PURE__ */ x(n, {
244
+ visible: $.hasMore,
245
+ onLoadMore: () => J((e) => e + 12)
246
+ })
247
+ ]
248
+ }),
249
+ /* @__PURE__ */ x(a, {
250
+ open: I,
251
+ onOpenChange: L,
252
+ onCreated: le
253
+ })
254
+ ]
140
255
  });
141
256
  }
142
257
  //#endregion
143
- export { S as ProviderPage };
258
+ export { M as ProviderPage };
144
259
 
145
260
  //# sourceMappingURL=ProviderPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ProviderPage.js","names":[],"sources":["../../../src/ggnomad/pages/ProviderPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { Link } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Briefcase, CalendarPlus, Globe, Mail, Phone, Store } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROVIDERS_QUERY } from '../graphql';\nimport { formatDate } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport { EntityCard, LoadMoreButton, RatingSummary, StatusBadge } from '../components';\n\nexport function ProviderPage() {\n const { t } = useI18n();\n const [limit, setLimit] = useState(12);\n const { data } = useQuery(\n GGNOMAD_PROVIDERS_QUERY as TypedDocumentNode<\n {\n ggnomadProviders: Array<{\n id: string;\n name: string;\n type: string;\n status: string;\n listingCount: number;\n totalRevenue: number;\n rating?: number | null;\n reviewCount: number;\n description?: string | null;\n contactEmail?: string | null;\n contactPhone?: string | null;\n website?: string | null;\n createdAt: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit, offset: 0 } }\n );\n const providers = data?.ggnomadProviders ?? [];\n\n return (\n <PageShell\n title={t('ggnomad.provider.title', 'Provider Dashboard')}\n titleAccent={t('ggnomad.provider.title.accent', 'Provider')}\n subtitle={t('ggnomad.provider.subtitle', 'B2B provider operations and onboarding metrics')}\n >\n <PagePurpose icon={<Briefcase className=\"size-4\" />}>\n {t(\n 'ggnomad.provider.purpose',\n 'Track the supply side of the marketplace — the hotels, tour operators and venues that publish inventory. Use it to monitor each provider’s status, listing count, revenue and rating, and to spot who needs onboarding attention.'\n )}\n </PagePurpose>\n\n {providers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.provider.emptyTitle', 'No providers onboarded')}\n description={t(\n 'ggnomad.provider.emptyDescription',\n 'Onboarded B2B providers and their performance will be tracked here.'\n )}\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.provider.onboardedProviders', 'Onboarded providers')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {providers.map((provider) => (\n <EntityCard\n key={provider.id}\n title={provider.name}\n meta={provider.description ?? provider.type}\n badge={<StatusBadge status={provider.status} live={provider.status === 'ACTIVE'} />}\n footer={\n <div className=\"space-y-2 text-xs\">\n <RatingSummary rating={provider.rating} reviewCount={provider.reviewCount} />\n <dl className=\"grid grid-cols-3 gap-2\">\n <div>\n <dt className=\"text-text-secondary\">\n {t('ggnomad.provider.listings', 'Listings')}\n </dt>\n <dd className=\"mt-0.5 font-medium tabular-nums text-text-primary\">\n {provider.listingCount}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('ggnomad.provider.revenue', 'Revenue')}\n </dt>\n <dd className=\"mt-0.5 font-medium tabular-nums text-text-primary\">\n ${Number(provider.totalRevenue).toLocaleString()}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('ggnomad.provider.type', 'Type')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">{provider.type}</dd>\n </div>\n </dl>\n <p className=\"inline-flex items-center gap-1 text-text-secondary\">\n <CalendarPlus className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.provider.joined', 'Joined {{date}}').replace(\n '{{date}}',\n formatDate(provider.createdAt) ?? '—'\n )}\n </p>\n {provider.contactEmail || provider.contactPhone || provider.website ? (\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n {provider.contactEmail ? (\n <a\n href={`mailto:${provider.contactEmail}`}\n className=\"inline-flex items-center gap-1 text-action-primary-bg hover:underline\"\n >\n <Mail className=\"size-3.5\" aria-hidden=\"true\" />\n {provider.contactEmail}\n </a>\n ) : null}\n {provider.contactPhone ? (\n <a\n href={`tel:${provider.contactPhone}`}\n className=\"inline-flex items-center gap-1 text-action-primary-bg hover:underline\"\n >\n <Phone className=\"size-3.5\" aria-hidden=\"true\" />\n {provider.contactPhone}\n </a>\n ) : null}\n {provider.website ? (\n <a\n href={provider.website}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex items-center gap-1 text-action-primary-bg hover:underline\"\n >\n <Globe className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.provider.website', 'Website')}\n </a>\n ) : null}\n </div>\n ) : null}\n <div className=\"mt-1 border-t border-border-seam pt-2\">\n <Link\n to={`${provider.id}/storefront`}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-action-primary-bg hover:underline\"\n >\n <Store className=\"size-3.5\" />\n {t('ggnomad.provider.manageStorefront', 'Manage storefront')}\n </Link>\n </div>\n </div>\n }\n />\n ))}\n </div>\n <LoadMoreButton\n visible={providers.length >= limit}\n onLoadMore={() => setLimit((l) => l + 12)}\n />\n </GlassCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProviderPage.js","names":[],"sources":["../../../src/ggnomad/pages/ProviderPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Briefcase, Plus, Search, X } from 'lucide-react';\nimport {\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROVIDERS_QUERY } from '../graphql';\nimport { notify } from '../utils/notify';\nimport { PageShell } from './PageShell';\nimport { CreateProviderDialog, LoadMoreButton, ProviderDashboardCard } from '../components';\nimport { getProviderTypeOptions } from '../utils/providerTypeMeta';\nimport {\n applyProviderControls,\n DEFAULT_PROVIDER_QUERY,\n PROVIDER_PAGE_SIZE,\n type ProviderListQuery,\n type ProviderSort,\n} from '../utils/providerListControls';\n\ninterface Provider {\n id: string;\n name: string;\n type: string;\n status: string;\n listingCount: number;\n totalRevenue: number;\n rating?: number | null;\n reviewCount: number;\n description?: string | null;\n contactEmail?: string | null;\n contactPhone?: string | null;\n website?: string | null;\n createdAt: string;\n}\n\n/** One backend request can return at most MAX_LIMIT (100) rows. */\nconst FETCH_PAGE_SIZE = 100;\n/** Safety stop so a misbehaving backend can never loop this page forever. */\nconst MAX_FETCH_PAGES = 20;\n/** Debounce window for the search box before filters re-apply. */\nconst SEARCH_DEBOUNCE_MS = 250;\n\n// Toolbar control styling — same class families as the create dialog's\n// inputs and LoadMoreButton (bg-*/border-*/text-* tokens), so everything\n// the toolbar uses already renders on this page in light and dark mode.\nconst toolbarInputClass =\n 'rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg';\nconst toolbarSelectClass = toolbarInputClass;\nconst chipBaseClass =\n 'rounded-full border border-border-seam px-3 py-1.5 text-sm transition-colors';\nconst chipActiveClass = ' border-border-seam bg-bg-sunken font-medium text-text-primary';\nconst chipIdleClass = ' text-text-secondary hover:bg-bg-sunken';\n\nconst SORT_OPTIONS: Array<{ value: ProviderSort; labelKey: string; labelFallback: string }> = [\n {\n value: 'newest',\n labelKey: 'ggnomad.provider.toolbar.sortNewest',\n labelFallback: 'Newest first',\n },\n {\n value: 'oldest',\n labelKey: 'ggnomad.provider.toolbar.sortOldest',\n labelFallback: 'Oldest first',\n },\n {\n value: 'nameAsc',\n labelKey: 'ggnomad.provider.toolbar.sortNameAsc',\n labelFallback: 'Name A to Z',\n },\n {\n value: 'nameDesc',\n labelKey: 'ggnomad.provider.toolbar.sortNameDesc',\n labelFallback: 'Name Z to A',\n },\n {\n value: 'revenueDesc',\n labelKey: 'ggnomad.provider.toolbar.sortRevenue',\n labelFallback: 'Most revenue',\n },\n];\n\nexport function ProviderPage() {\n const { t } = useI18n();\n const client = useApolloClient();\n const [providers, setProviders] = useState<Provider[] | null>(null);\n const [loadError, setLoadError] = useState(false);\n const [showCreateDialog, setShowCreateDialog] = useState(false);\n\n // Mirror the providers state into a ref so `loadProviders` can read the\n // current value without listing `providers` in its useCallback deps.\n // Without this, every successful setProviders() rebuilds the callback,\n // which retriggers the useEffect that depends on it, which calls\n // loadProviders again — an infinite refetch loop.\n const providersRef = useRef<Provider[] | null>(null);\n useEffect(() => {\n providersRef.current = providers;\n }, [providers]);\n\n const [searchInput, setSearchInput] = useState('');\n const [appliedSearch, setAppliedSearch] = useState('');\n const [typeFilter, setTypeFilter] = useState<string | null>(null);\n const [sort, setSort] = useState<ProviderSort>(DEFAULT_PROVIDER_QUERY.sort);\n const [visibleCount, setVisibleCount] = useState(PROVIDER_PAGE_SIZE);\n const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // The backend caps one request at 100 rows and exposes no filters or\n // totalCount, so load every page up front; the toolbar then filters\n // client-side over the complete list (see providerListControls.ts).\n const loadProviders = useCallback(async () => {\n try {\n const all: Provider[] = [];\n let offset = 0;\n for (let page = 0; page < MAX_FETCH_PAGES; page += 1) {\n const { data } = await client.query({\n query: GGNOMAD_PROVIDERS_QUERY as TypedDocumentNode<\n { ggnomadProviders: Provider[] },\n { limit: number; offset: number }\n >,\n variables: { limit: FETCH_PAGE_SIZE, offset },\n fetchPolicy: 'network-only',\n });\n const batch = data?.ggnomadProviders ?? [];\n all.push(...batch);\n if (batch.length < FETCH_PAGE_SIZE) break;\n offset += FETCH_PAGE_SIZE;\n }\n setProviders(all);\n setLoadError(false);\n } catch {\n // Preserve the existing grid if we already have data — a failed\n // reload after a successful create would otherwise hide the\n // already-loaded providers behind a generic error state even\n // though the data is still in memory. Only escalate to the\n // full-page error when there is no data to fall back to.\n if (providersRef.current === null) setLoadError(true);\n else notify.error(t('ggnomad.provider.reloadError', 'Could not refresh provider list'), '');\n }\n }, [client, t]);\n\n useEffect(() => {\n // Defer one tick so the fetch (and its setState) is async from the\n // effect body — satisfies the no-sync-setState-in-effect rule.\n const timer = setTimeout(() => void loadProviders(), 0);\n return () => clearTimeout(timer);\n }, [loadProviders]);\n\n // Cleanup only: never leave a pending debounce timer mounted.\n useEffect(\n () => () => {\n if (searchTimer.current) clearTimeout(searchTimer.current);\n },\n []\n );\n\n // Debounced search: re-apply filters 250ms after typing stops. Timer is\n // managed from the change handler (not an effect) so typing state stays\n // in the event flow; the applied value resets the grid to page one.\n const handleSearchChange = (value: string) => {\n setSearchInput(value);\n if (searchTimer.current) clearTimeout(searchTimer.current);\n searchTimer.current = setTimeout(() => {\n setAppliedSearch(value);\n setVisibleCount(PROVIDER_PAGE_SIZE);\n }, SEARCH_DEBOUNCE_MS);\n };\n\n const handleClearSearch = () => {\n if (searchTimer.current) clearTimeout(searchTimer.current);\n setSearchInput('');\n setAppliedSearch('');\n setVisibleCount(PROVIDER_PAGE_SIZE);\n };\n\n // Every control change returns the grid to the first page.\n const handleTypeChange = (value: string | null) => {\n setTypeFilter(value);\n setVisibleCount(PROVIDER_PAGE_SIZE);\n };\n\n const handleSortChange = (value: ProviderSort) => {\n setSort(value);\n setVisibleCount(PROVIDER_PAGE_SIZE);\n };\n\n const query: ProviderListQuery = useMemo(\n () => ({ search: appliedSearch, type: typeFilter, sort }),\n [appliedSearch, typeFilter, sort]\n );\n\n const result = useMemo(\n () => (providers === null ? null : applyProviderControls(providers, query, visibleCount)),\n [providers, query, visibleCount]\n );\n\n const openCreateDialog = () => setShowCreateDialog(true);\n const handleCreated = async () => {\n // The dialog already showed a success toast; a quiet reload\n // reconciles the new provider with the server.\n await loadProviders();\n };\n\n const clearFilters = () => {\n if (searchTimer.current) clearTimeout(searchTimer.current);\n setSearchInput('');\n setAppliedSearch('');\n setTypeFilter(null);\n setSort(DEFAULT_PROVIDER_QUERY.sort);\n setVisibleCount(PROVIDER_PAGE_SIZE);\n };\n\n const typeOptions = getProviderTypeOptions(t);\n\n return (\n <PageShell\n title={t('ggnomad.provider.title', 'Provider Dashboard')}\n titleAccent={t('ggnomad.provider.title.accent', 'Provider')}\n subtitle={t('ggnomad.provider.subtitle', 'B2B provider operations and onboarding metrics')}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.provider.actions.newProvider', 'New provider'),\n onSelect: openCreateDialog,\n icon: <Plus />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<Briefcase className=\"size-4\" />}>\n {t(\n 'ggnomad.provider.purpose',\n 'Track the supply side of the marketplace — the hotels, tour operators and venues that publish inventory. Use it to monitor each provider’s status, listing count, revenue and rating, and to spot who needs onboarding attention.'\n )}\n </PagePurpose>\n\n {loadError ? (\n <p className=\"text-sm text-text-secondary\">\n {t(\n 'ggnomad.provider.loadError',\n 'Providers could not be loaded. Refresh the page to try again.'\n )}\n </p>\n ) : providers === null ? (\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.provider.loading', 'Loading providers…')}\n </p>\n ) : providers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.provider.emptyTitle', 'No providers onboarded')}\n description={t(\n 'ggnomad.provider.emptyDescription',\n 'Onboarded B2B providers and their performance will be tracked here.'\n )}\n />\n ) : (\n result && (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.provider.onboardedProviders', 'Onboarded providers')}\n </h2>\n\n {/* Toolbar: search / status / sort row, type chips row, count line. */}\n <div className=\"mb-4 flex flex-col gap-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative w-full sm:w-72\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"provider-search\"\n type=\"text\"\n value={searchInput}\n onChange={(e) => handleSearchChange(e.target.value)}\n placeholder={t(\n 'ggnomad.provider.toolbar.searchPlaceholder',\n 'Search providers'\n )}\n aria-label={t('ggnomad.provider.toolbar.searchLabel', 'Search providers')}\n className={`${toolbarInputClass} w-full pl-9 pr-8`}\n />\n {searchInput && (\n <button\n type=\"button\"\n onClick={handleClearSearch}\n aria-label={t('ggnomad.provider.toolbar.clearSearch', 'Clear search')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary\"\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n\n <select\n id=\"provider-sort\"\n value={sort}\n onChange={(e) => handleSortChange(e.target.value as ProviderSort)}\n aria-label={t('ggnomad.provider.toolbar.sortLabel', 'Sort providers')}\n className={toolbarSelectClass}\n >\n {SORT_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {t(option.labelKey, option.labelFallback)}\n </option>\n ))}\n </select>\n </div>\n\n <div\n className=\"flex flex-wrap gap-2\"\n role=\"group\"\n aria-label={t('ggnomad.provider.toolbar.typeLabel', 'Filter by type')}\n >\n <button\n type=\"button\"\n onClick={() => handleTypeChange(null)}\n aria-pressed={typeFilter === null}\n className={\n chipBaseClass + (typeFilter === null ? chipActiveClass : chipIdleClass)\n }\n >\n {t('ggnomad.provider.toolbar.allTypes', 'All')}\n </button>\n {typeOptions.map((option) => {\n const isActive = typeFilter === option.value;\n return (\n <button\n key={option.value}\n type=\"button\"\n onClick={() => handleTypeChange(option.value)}\n aria-pressed={isActive}\n className={chipBaseClass + (isActive ? chipActiveClass : chipIdleClass)}\n >\n {option.label}\n </button>\n );\n })}\n </div>\n\n <p className=\"text-xs text-text-secondary\" aria-live=\"polite\">\n {t('ggnomad.provider.toolbar.showing', 'Showing {{shown}} of {{total}} providers', {\n shown: result.shown.length,\n total: result.total,\n })}\n {result.isFiltered\n ? t('ggnomad.provider.toolbar.filteredSuffix', ' (filtered)')\n : ''}\n </p>\n </div>\n\n {result.total === 0 ? (\n <div className=\"flex flex-col items-center gap-3 py-12 text-center\">\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.provider.toolbar.noMatches', 'No providers match your filters')}\n </p>\n <button\n type=\"button\"\n onClick={clearFilters}\n className=\"rounded-lg border border-border-seam bg-bg-surface px-5 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg\"\n >\n {t('ggnomad.provider.toolbar.clearFilters', 'Clear filters')}\n </button>\n </div>\n ) : (\n <div\n className=\"grid gap-4\"\n style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))' }}\n >\n {result.shown.map((provider) => (\n <ProviderDashboardCard\n key={provider.id}\n id={provider.id}\n name={provider.name}\n type={provider.type}\n status={provider.status}\n description={provider.description}\n listingCount={provider.listingCount}\n totalRevenue={provider.totalRevenue}\n rating={provider.rating}\n reviewCount={provider.reviewCount}\n contactEmail={provider.contactEmail}\n contactPhone={provider.contactPhone}\n website={provider.website}\n createdAt={provider.createdAt}\n />\n ))}\n </div>\n )}\n\n <LoadMoreButton\n visible={result.hasMore}\n onLoadMore={() => setVisibleCount((count) => count + PROVIDER_PAGE_SIZE)}\n />\n </GlassCard>\n )\n )}\n\n <CreateProviderDialog\n open={showCreateDialog}\n onOpenChange={setShowCreateDialog}\n onCreated={handleCreated}\n />\n </PageShell>\n );\n}\n"],"mappings":"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"}
@@ -25,6 +25,24 @@ export declare function jsonLabel(value: unknown): string | null;
25
25
  /** Number of entries in a JSON array field (e.g. `GgnomadTrip.stops`), or null. */
26
26
  export declare function jsonArrayLength(value: unknown): number | null;
27
27
  export declare function formatMoney(amount: number, currency: string): string;
28
+ /**
29
+ * Currency-formatted number with compact notation at >=10,000. Used by
30
+ * the Provider Dashboard card so large revenues don't break the stats
31
+ * row. The full unformatted amount is returned in a `title` attribute
32
+ * via the second tuple element when callers need the exact value.
33
+ *
34
+ * `GgnomadProvider` does not currently expose a `currency` field in the
35
+ * GraphQL schema, so callers pass `'USD'` as the default until the
36
+ * backend adds the field. Keeping the parameter preserves the call site
37
+ * for when it does.
38
+ *
39
+ * Returns `{ display, exact }` so the card can render `$98.5K` and
40
+ * keep the full `$98,450` in the tooltip.
41
+ */
42
+ export declare function formatRevenue(amount: number, currency?: string): {
43
+ display: string;
44
+ exact: string;
45
+ };
28
46
  export declare function formatDate(value?: string | null): string | null;
29
47
  export declare function formatDateTime(value?: string | null): string | null;
30
48
  export declare function formatTimeOnly(value?: string | null): string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/utils/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIlD;AAMD,wFAAwF;AACxF,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAc9F;AAID;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CASvD;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAE7D;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK/D;AAED,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAWnE;AAED,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAKnE;AAED,uBAAuB;AACvB,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAO7D;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMvE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAyCxF"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/utils/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIlD;AAMD,wFAAwF;AACxF,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAc9F;AAID;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CASvD;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAE7D;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,QAAQ,SAAQ,GACf;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAepC;AAED,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK/D;AAED,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAWnE;AAED,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAKnE;AAED,uBAAuB;AACvB,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAO7D;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMvE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAyCxF"}
@@ -51,7 +51,23 @@ function a(e) {
51
51
  function o(e, t) {
52
52
  return `${t} ${Number(e).toFixed(2)}`;
53
53
  }
54
- function s(e) {
54
+ function s(e, t = "USD") {
55
+ let n = Number.isFinite(e) ? e : 0, r = Math.abs(n) >= 1e4, i = new Intl.NumberFormat("en-US", {
56
+ style: "currency",
57
+ currency: t,
58
+ notation: r ? "compact" : "standard",
59
+ maximumFractionDigits: +!!r
60
+ }), a = new Intl.NumberFormat("en-US", {
61
+ style: "currency",
62
+ currency: t,
63
+ maximumFractionDigits: 0
64
+ }).format(n);
65
+ return {
66
+ display: i.format(n),
67
+ exact: a
68
+ };
69
+ }
70
+ function c(e) {
55
71
  if (!e) return null;
56
72
  let t = new Date(e);
57
73
  return Number.isNaN(t.getTime()) ? null : t.toLocaleDateString(void 0, {
@@ -60,7 +76,7 @@ function s(e) {
60
76
  year: "numeric"
61
77
  });
62
78
  }
63
- function c(e) {
79
+ function l(e) {
64
80
  if (!e) return null;
65
81
  let t = new Date(e);
66
82
  return Number.isNaN(t.getTime()) ? null : t.toLocaleString(void 0, {
@@ -71,7 +87,7 @@ function c(e) {
71
87
  minute: "2-digit"
72
88
  });
73
89
  }
74
- function l(e) {
90
+ function u(e) {
75
91
  if (!e) return null;
76
92
  let t = new Date(e);
77
93
  return Number.isNaN(t.getTime()) ? null : t.toLocaleTimeString(void 0, {
@@ -79,16 +95,16 @@ function l(e) {
79
95
  minute: "2-digit"
80
96
  });
81
97
  }
82
- function u(e) {
98
+ function d(e) {
83
99
  let t = Math.max(0, Math.round(e)), n = Math.floor(t / 60), r = t % 60;
84
100
  return n === 0 ? `${r}m` : r === 0 ? `${n}h` : `${n}h ${r}m`;
85
101
  }
86
- function d(e, t) {
102
+ function f(e, t) {
87
103
  if (!e || !t) return 0;
88
104
  let n = new Date(e).getTime(), r = new Date(t).getTime();
89
105
  return Number.isNaN(n) || Number.isNaN(r) || r <= n ? 0 : Math.round((r - n) / 864e5);
90
106
  }
91
- function f(n) {
107
+ function p(n) {
92
108
  let i = [];
93
109
  if (Array.isArray(n)) {
94
110
  for (let e of n) {
@@ -120,6 +136,6 @@ function f(n) {
120
136
  return i;
121
137
  }
122
138
  //#endregion
123
- export { s as formatDate, c as formatDateTime, u as formatDurationMinutes, o as formatMoney, l as formatTimeOnly, e as humanizeEnum, a as jsonArrayLength, n as jsonFacts, i as jsonLabel, d as nightsBetween, f as parseNamedCounts };
139
+ export { c as formatDate, l as formatDateTime, d as formatDurationMinutes, o as formatMoney, s as formatRevenue, u as formatTimeOnly, e as humanizeEnum, a as jsonArrayLength, n as jsonFacts, i as jsonLabel, f as nightsBetween, p as parseNamedCounts };
124
140
 
125
141
  //# sourceMappingURL=format.js.map