@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.
- package/dist/ggnomad/components/CreateProviderDialog.d.ts +22 -0
- package/dist/ggnomad/components/CreateProviderDialog.d.ts.map +1 -0
- package/dist/ggnomad/components/CreateProviderDialog.js +209 -0
- package/dist/ggnomad/components/CreateProviderDialog.js.map +1 -0
- package/dist/ggnomad/components/ProviderDashboardCard.d.ts +28 -0
- package/dist/ggnomad/components/ProviderDashboardCard.d.ts.map +1 -0
- package/dist/ggnomad/components/ProviderDashboardCard.js +222 -0
- package/dist/ggnomad/components/ProviderDashboardCard.js.map +1 -0
- package/dist/ggnomad/components/index.d.ts +2 -0
- package/dist/ggnomad/components/index.d.ts.map +1 -1
- package/dist/ggnomad/components/index.js +2 -0
- package/dist/ggnomad/graphql.d.ts +1 -0
- package/dist/ggnomad/graphql.d.ts.map +1 -1
- package/dist/ggnomad/graphql.js +65 -47
- package/dist/ggnomad/graphql.js.map +1 -1
- package/dist/ggnomad/pages/ProviderPage.d.ts.map +1 -1
- package/dist/ggnomad/pages/ProviderPage.js +249 -134
- package/dist/ggnomad/pages/ProviderPage.js.map +1 -1
- package/dist/ggnomad/utils/format.d.ts +18 -0
- package/dist/ggnomad/utils/format.d.ts.map +1 -1
- package/dist/ggnomad/utils/format.js +23 -7
- package/dist/ggnomad/utils/format.js.map +1 -1
- package/dist/ggnomad/utils/providerListControls.d.ts +53 -0
- package/dist/ggnomad/utils/providerListControls.d.ts.map +1 -0
- package/dist/ggnomad/utils/providerListControls.js +50 -0
- package/dist/ggnomad/utils/providerListControls.js.map +1 -0
- package/dist/ggnomad/utils/providerTypeMeta.d.ts +79 -0
- package/dist/ggnomad/utils/providerTypeMeta.d.ts.map +1 -0
- package/dist/ggnomad/utils/providerTypeMeta.js +81 -0
- package/dist/ggnomad/utils/providerTypeMeta.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,145 +1,260 @@
|
|
|
1
1
|
import { GGNOMAD_PROVIDERS_QUERY as e } from "../graphql.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { GlassCard as
|
|
15
|
-
import { jsx as
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
})
|
|
101
|
-
})
|
|
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
|
-
})
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|