@burdenoff/microfe-subscriberbot 2026.1030.7 → 2026.1102.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 (128) hide show
  1. package/README.md +26 -0
  2. package/dist/stories/pages/AgentDetailPage.stories.d.ts +16 -0
  3. package/dist/stories/pages/AgentDetailPage.stories.d.ts.map +1 -0
  4. package/dist/stories/pages/AgentNewPage.stories.d.ts +12 -0
  5. package/dist/stories/pages/AgentNewPage.stories.d.ts.map +1 -0
  6. package/dist/stories/pages/AgentsListPage.stories.d.ts +16 -0
  7. package/dist/stories/pages/AgentsListPage.stories.d.ts.map +1 -0
  8. package/dist/stories/pages/AssetVaultPage.stories.d.ts +14 -0
  9. package/dist/stories/pages/AssetVaultPage.stories.d.ts.map +1 -0
  10. package/dist/stories/pages/DashboardPage.stories.d.ts +12 -0
  11. package/dist/stories/pages/DashboardPage.stories.d.ts.map +1 -0
  12. package/dist/stories/pages/DecisionDetailPage.stories.d.ts +13 -0
  13. package/dist/stories/pages/DecisionDetailPage.stories.d.ts.map +1 -0
  14. package/dist/stories/pages/DecisionsPage.stories.d.ts +14 -0
  15. package/dist/stories/pages/DecisionsPage.stories.d.ts.map +1 -0
  16. package/dist/stories/pages/EnterpriseSpendPage.stories.d.ts +13 -0
  17. package/dist/stories/pages/EnterpriseSpendPage.stories.d.ts.map +1 -0
  18. package/dist/stories/pages/InboxPage.stories.d.ts +20 -0
  19. package/dist/stories/pages/InboxPage.stories.d.ts.map +1 -0
  20. package/dist/stories/pages/InsightsPage.stories.d.ts +12 -0
  21. package/dist/stories/pages/InsightsPage.stories.d.ts.map +1 -0
  22. package/dist/stories/pages/InvoicesPage.stories.d.ts +16 -0
  23. package/dist/stories/pages/InvoicesPage.stories.d.ts.map +1 -0
  24. package/dist/stories/pages/NegotiationDetailPage.stories.d.ts +11 -0
  25. package/dist/stories/pages/NegotiationDetailPage.stories.d.ts.map +1 -0
  26. package/dist/stories/pages/NegotiationNewPage.stories.d.ts +9 -0
  27. package/dist/stories/pages/NegotiationNewPage.stories.d.ts.map +1 -0
  28. package/dist/stories/pages/NegotiationsPage.stories.d.ts +11 -0
  29. package/dist/stories/pages/NegotiationsPage.stories.d.ts.map +1 -0
  30. package/dist/stories/pages/ProviderDetailPage.stories.d.ts +15 -0
  31. package/dist/stories/pages/ProviderDetailPage.stories.d.ts.map +1 -0
  32. package/dist/stories/pages/ProviderNewPage.stories.d.ts +12 -0
  33. package/dist/stories/pages/ProviderNewPage.stories.d.ts.map +1 -0
  34. package/dist/stories/pages/ProvidersListPage.stories.d.ts +17 -0
  35. package/dist/stories/pages/ProvidersListPage.stories.d.ts.map +1 -0
  36. package/dist/stories/pages/RelationshipDetailPage.stories.d.ts +15 -0
  37. package/dist/stories/pages/RelationshipDetailPage.stories.d.ts.map +1 -0
  38. package/dist/stories/pages/RelationshipNewPage.stories.d.ts +12 -0
  39. package/dist/stories/pages/RelationshipNewPage.stories.d.ts.map +1 -0
  40. package/dist/stories/pages/RelationshipsListPage.stories.d.ts +14 -0
  41. package/dist/stories/pages/RelationshipsListPage.stories.d.ts.map +1 -0
  42. package/dist/stories/pages/SavingsPage.stories.d.ts +13 -0
  43. package/dist/stories/pages/SavingsPage.stories.d.ts.map +1 -0
  44. package/dist/stories/pages/WorkflowsPage.stories.d.ts +16 -0
  45. package/dist/stories/pages/WorkflowsPage.stories.d.ts.map +1 -0
  46. package/dist/stories/support/PageHarness.d.ts +28 -0
  47. package/dist/stories/support/PageHarness.d.ts.map +1 -0
  48. package/dist/stories/support/fixtures.d.ts +28 -0
  49. package/dist/stories/support/fixtures.d.ts.map +1 -0
  50. package/dist/stories/support/graphqlMock.d.ts +34 -0
  51. package/dist/stories/support/graphqlMock.d.ts.map +1 -0
  52. package/dist/stories/support/handlers.d.ts +8 -0
  53. package/dist/stories/support/handlers.d.ts.map +1 -0
  54. package/dist/stories/support/negotiationFixtures.d.ts +14 -0
  55. package/dist/stories/support/negotiationFixtures.d.ts.map +1 -0
  56. package/dist/stories/support/play.d.ts +7 -0
  57. package/dist/stories/support/play.d.ts.map +1 -0
  58. package/dist/subscriberbot/SubscriberBotRoutes.d.ts.map +1 -1
  59. package/dist/subscriberbot/SubscriberBotRoutes.js +18 -6
  60. package/dist/subscriberbot/SubscriberBotRoutes.js.map +1 -1
  61. package/dist/subscriberbot/admin/AdminDashboardPage.d.ts.map +1 -1
  62. package/dist/subscriberbot/admin/AdminDashboardPage.js +61 -51
  63. package/dist/subscriberbot/admin/AdminDashboardPage.js.map +1 -1
  64. package/dist/subscriberbot/admin/NegotiationDeskPanel.d.ts +4 -0
  65. package/dist/subscriberbot/admin/NegotiationDeskPanel.d.ts.map +1 -0
  66. package/dist/subscriberbot/admin/NegotiationDeskPanel.js +210 -0
  67. package/dist/subscriberbot/admin/NegotiationDeskPanel.js.map +1 -0
  68. package/dist/subscriberbot/components/Modal.d.ts.map +1 -1
  69. package/dist/subscriberbot/components/Modal.js +9 -6
  70. package/dist/subscriberbot/components/Modal.js.map +1 -1
  71. package/dist/subscriberbot/components/negotiations/NegotiationStatusPill.d.ts +8 -0
  72. package/dist/subscriberbot/components/negotiations/NegotiationStatusPill.d.ts.map +1 -0
  73. package/dist/subscriberbot/components/negotiations/NegotiationStatusPill.js +18 -0
  74. package/dist/subscriberbot/components/negotiations/NegotiationStatusPill.js.map +1 -0
  75. package/dist/subscriberbot/components/negotiations/NegotiationTimeline.d.ts +8 -0
  76. package/dist/subscriberbot/components/negotiations/NegotiationTimeline.d.ts.map +1 -0
  77. package/dist/subscriberbot/components/negotiations/NegotiationTimeline.js +49 -0
  78. package/dist/subscriberbot/components/negotiations/NegotiationTimeline.js.map +1 -0
  79. package/dist/subscriberbot/components/negotiations/OutcomeForm.d.ts +13 -0
  80. package/dist/subscriberbot/components/negotiations/OutcomeForm.d.ts.map +1 -0
  81. package/dist/subscriberbot/components/negotiations/OutcomeForm.js +112 -0
  82. package/dist/subscriberbot/components/negotiations/OutcomeForm.js.map +1 -0
  83. package/dist/subscriberbot/components/negotiations/index.d.ts +5 -0
  84. package/dist/subscriberbot/components/negotiations/index.d.ts.map +1 -0
  85. package/dist/subscriberbot/components/negotiations/index.js +4 -0
  86. package/dist/subscriberbot/components/negotiations/labels.d.ts +16 -0
  87. package/dist/subscriberbot/components/negotiations/labels.d.ts.map +1 -0
  88. package/dist/subscriberbot/components/negotiations/labels.js +179 -0
  89. package/dist/subscriberbot/components/negotiations/labels.js.map +1 -0
  90. package/dist/subscriberbot/hooks/useNegotiationApi.d.ts +51 -0
  91. package/dist/subscriberbot/hooks/useNegotiationApi.d.ts.map +1 -0
  92. package/dist/subscriberbot/hooks/useNegotiationApi.js +106 -0
  93. package/dist/subscriberbot/hooks/useNegotiationApi.js.map +1 -0
  94. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +1 -0
  95. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
  96. package/dist/subscriberbot/hooks/useSubscriberBotApi.js +1 -1
  97. package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
  98. package/dist/subscriberbot/pages/InvoicesPage.d.ts.map +1 -1
  99. package/dist/subscriberbot/pages/InvoicesPage.js +1 -0
  100. package/dist/subscriberbot/pages/InvoicesPage.js.map +1 -1
  101. package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.d.ts.map +1 -1
  102. package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.js +1 -0
  103. package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.js.map +1 -1
  104. package/dist/subscriberbot/pages/negotiations/NegotiationDetailPage.d.ts +11 -0
  105. package/dist/subscriberbot/pages/negotiations/NegotiationDetailPage.d.ts.map +1 -0
  106. package/dist/subscriberbot/pages/negotiations/NegotiationDetailPage.js +312 -0
  107. package/dist/subscriberbot/pages/negotiations/NegotiationDetailPage.js.map +1 -0
  108. package/dist/subscriberbot/pages/negotiations/NegotiationNewPage.d.ts +11 -0
  109. package/dist/subscriberbot/pages/negotiations/NegotiationNewPage.d.ts.map +1 -0
  110. package/dist/subscriberbot/pages/negotiations/NegotiationNewPage.js +375 -0
  111. package/dist/subscriberbot/pages/negotiations/NegotiationNewPage.js.map +1 -0
  112. package/dist/subscriberbot/pages/negotiations/NegotiationsPage.d.ts +10 -0
  113. package/dist/subscriberbot/pages/negotiations/NegotiationsPage.d.ts.map +1 -0
  114. package/dist/subscriberbot/pages/negotiations/NegotiationsPage.js +155 -0
  115. package/dist/subscriberbot/pages/negotiations/NegotiationsPage.js.map +1 -0
  116. package/dist/subscriberbot/pages/providers/ProvidersListPage.d.ts.map +1 -1
  117. package/dist/subscriberbot/pages/providers/ProvidersListPage.js +1 -0
  118. package/dist/subscriberbot/pages/providers/ProvidersListPage.js.map +1 -1
  119. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
  120. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +321 -307
  121. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
  122. package/dist/subscriberbot/types/negotiation.d.ts +101 -0
  123. package/dist/subscriberbot/types/negotiation.d.ts.map +1 -0
  124. package/dist/subscriberbot/utils/formatters.d.ts +1 -0
  125. package/dist/subscriberbot/utils/formatters.d.ts.map +1 -1
  126. package/dist/subscriberbot/utils/formatters.js +4 -3
  127. package/dist/subscriberbot/utils/formatters.js.map +1 -1
  128. package/package.json +2 -1
@@ -0,0 +1,375 @@
1
+ import { useSubscriberBot as e } from "../../context/SubscriberBotContext.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { PageLayout as n } from "../../components/PageLayout.js";
4
+ import { formatEnumLabel as r } from "../../utils/formatters.js";
5
+ import { useTr as i } from "../../hooks/useTr.js";
6
+ import { useRelationships as a } from "../../hooks/useSubscriberBotApi.js";
7
+ import { BILL_TYPES as o, BILL_TYPE_LABEL as s, TARGETS as ee, TARGET_LABEL as c } from "../../components/negotiations/labels.js";
8
+ import "../../components/negotiations/index.js";
9
+ import { useCreateNegotiation as l, useNegotiationTerms as u } from "../../hooks/useNegotiationApi.js";
10
+ import { useEffect as te, useMemo as d, useRef as ne, useState as f } from "react";
11
+ import { ArrowLeft as re, Handshake as p, Receipt as ie, Target as ae, UserRound as oe } from "lucide-react";
12
+ import { useSearchParams as se } from "react-router";
13
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
14
+ import { GlassCard as g, PagePurpose as ce } from "@burdenoff/fe-libs/ui";
15
+ //#region src/subscriberbot/pages/negotiations/NegotiationNewPage.tsx
16
+ var _ = [
17
+ "MONTHLY",
18
+ "YEARLY",
19
+ "QUARTERLY",
20
+ "WEEKLY"
21
+ ], v = "w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2.5 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]", y = "mb-1.5 block text-xs font-medium text-text-secondary";
22
+ function b({ icon: e, title: t, description: n, children: r }) {
23
+ return /* @__PURE__ */ h(g, {
24
+ treatment: "metric",
25
+ className: "p-5 sm:p-6",
26
+ children: [/* @__PURE__ */ h("div", {
27
+ className: "flex items-start gap-3",
28
+ children: [/* @__PURE__ */ m("span", {
29
+ className: "mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary",
30
+ children: e
31
+ }), /* @__PURE__ */ h("div", {
32
+ className: "min-w-0",
33
+ children: [/* @__PURE__ */ m("h2", {
34
+ className: "text-sm font-semibold text-text-primary",
35
+ children: t
36
+ }), /* @__PURE__ */ m("p", {
37
+ className: "mt-0.5 text-xs text-text-secondary",
38
+ children: n
39
+ })]
40
+ })]
41
+ }), /* @__PURE__ */ m("div", {
42
+ className: "mt-4",
43
+ children: r
44
+ })]
45
+ });
46
+ }
47
+ function x() {
48
+ let g = i(), { navigateTo: x } = e(), [S] = se(), C = l(), w = u(), T = a({ pagination: {
49
+ limit: 100,
50
+ offset: 0
51
+ } }), E = d(() => T.data?.items ?? [], [T.data]), [D, le] = f(S.get("relationshipId") ?? ""), [O, k] = f(""), [A, ue] = f("INTERNET"), [j, M] = f(""), [N, P] = f("USD"), [F, I] = f("MONTHLY"), [L, R] = f("LOWER_PRICE"), [z, B] = f(""), [V, H] = f(""), [U, de] = f(""), [W, fe] = f("SELF_SERVE"), [G, pe] = f(""), [K, me] = f(""), [q, he] = f(""), [J, Y] = f(null), X = E.find((e) => e.id === D), Z = ne(null);
52
+ te(() => {
53
+ !X || Z.current === X.id || (Z.current = X.id, k(X.label), X.amount > 0 && M(String(X.amount)), P(X.currency), _.includes(X.interval) && I(X.interval));
54
+ }, [X]);
55
+ let Q = w.data ? Math.round(w.data.managedFeeRate * 100) : null, ge = (e) => {
56
+ e.preventDefault(), Y(null);
57
+ let t = Number(j);
58
+ if (!O.trim()) {
59
+ Y(g("negotiations.form.providerRequired", "Who is the bill from?"));
60
+ return;
61
+ }
62
+ if (!j || !Number.isFinite(t) || t <= 0) {
63
+ Y(g("negotiations.form.amountRequired", "Enter what you pay today."));
64
+ return;
65
+ }
66
+ if (W === "MANAGED" && !G.trim()) {
67
+ Y(g("negotiations.form.holderRequired", "The negotiation desk needs the name on the provider account."));
68
+ return;
69
+ }
70
+ let n = {
71
+ providerName: O.trim(),
72
+ billType: A,
73
+ mode: W,
74
+ targetOutcome: L,
75
+ currentAmount: t,
76
+ currency: N.trim().toUpperCase() || "USD",
77
+ interval: F,
78
+ ...D ? { relationshipId: D } : {},
79
+ ...z ? { targetAmount: Number(z) } : {},
80
+ ...V ? { tenureMonths: Number(V) } : {},
81
+ ...U.trim() ? { competitorOffer: U.trim() } : {},
82
+ ...G.trim() ? { accountHolderName: G.trim() } : {},
83
+ ...K.trim() ? { accountNumber: K.trim() } : {},
84
+ ...q.trim() ? { notes: q.trim() } : {}
85
+ };
86
+ C.mutate({ input: n }, {
87
+ onSuccess: (e) => x(`/negotiations/${e.id}`),
88
+ onError: (e) => Y(e instanceof Error ? e.message : g("negotiations.form.error", "Could not create the request."))
89
+ });
90
+ }, $ = (e, n, r) => /* @__PURE__ */ h("label", {
91
+ className: t("flex cursor-pointer flex-col gap-1 rounded-xl border p-4 text-left", W === e ? "border-action-primary-bg bg-[var(--color-accent-soft)]" : "border-border-subtle bg-bg-surface hover:bg-bg-sunken"),
92
+ children: [/* @__PURE__ */ h("span", {
93
+ className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
94
+ children: [/* @__PURE__ */ m("input", {
95
+ type: "radio",
96
+ name: "mode",
97
+ value: e,
98
+ checked: W === e,
99
+ onChange: () => fe(e)
100
+ }), n]
101
+ }), /* @__PURE__ */ m("span", {
102
+ className: "text-xs text-text-secondary",
103
+ children: r
104
+ })]
105
+ });
106
+ return /* @__PURE__ */ h(n, {
107
+ title: g("negotiations.new.title", "Negotiate a bill"),
108
+ description: g("negotiations.new.description", "Tell us about the bill and how you want to handle it."),
109
+ actions: /* @__PURE__ */ h("button", {
110
+ type: "button",
111
+ onClick: () => x("/negotiations"),
112
+ className: "inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken",
113
+ children: [
114
+ /* @__PURE__ */ m(re, { className: "h-4 w-4" }),
115
+ " ",
116
+ g("negotiations.back", "Back")
117
+ ]
118
+ }),
119
+ children: [/* @__PURE__ */ m(ce, {
120
+ className: "mb-6",
121
+ children: g("negotiations.new.purpose", "You will get a negotiation playbook for this bill right away. Nothing is sent to the provider until you submit the request.")
122
+ }), /* @__PURE__ */ h("form", {
123
+ onSubmit: ge,
124
+ className: "max-w-3xl space-y-5",
125
+ "data-testid": "negotiation-form",
126
+ children: [
127
+ J && /* @__PURE__ */ m("div", {
128
+ role: "alert",
129
+ className: "rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text",
130
+ children: J
131
+ }),
132
+ /* @__PURE__ */ m(b, {
133
+ icon: /* @__PURE__ */ m(ie, { className: "h-4 w-4" }),
134
+ title: g("negotiations.form.bill.title", "The bill"),
135
+ description: g("negotiations.form.bill.description", "Link a tracked subscription to prefill it, or describe the bill."),
136
+ children: /* @__PURE__ */ h("div", {
137
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
138
+ children: [
139
+ /* @__PURE__ */ h("div", {
140
+ className: "sm:col-span-2",
141
+ children: [/* @__PURE__ */ m("label", {
142
+ htmlFor: "neg-relationship",
143
+ className: y,
144
+ children: g("negotiations.form.relationship", "Tracked subscription (optional)")
145
+ }), /* @__PURE__ */ h("select", {
146
+ id: "neg-relationship",
147
+ value: D,
148
+ onChange: (e) => le(e.target.value),
149
+ className: v,
150
+ children: [/* @__PURE__ */ m("option", {
151
+ value: "",
152
+ children: g("negotiations.form.noRelationship", "Not linked")
153
+ }), E.map((e) => /* @__PURE__ */ m("option", {
154
+ value: e.id,
155
+ children: e.label
156
+ }, e.id))]
157
+ })]
158
+ }),
159
+ /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ h("label", {
160
+ htmlFor: "neg-provider",
161
+ className: y,
162
+ children: [
163
+ g("negotiations.form.provider", "Provider"),
164
+ " ",
165
+ /* @__PURE__ */ m("span", {
166
+ className: "text-status-error-text",
167
+ children: "*"
168
+ })
169
+ ]
170
+ }), /* @__PURE__ */ m("input", {
171
+ id: "neg-provider",
172
+ value: O,
173
+ onChange: (e) => k(e.target.value),
174
+ placeholder: g("negotiations.form.providerPlaceholder", "e.g. Comcast Xfinity"),
175
+ className: v
176
+ })] }),
177
+ /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
178
+ htmlFor: "neg-bill-type",
179
+ className: y,
180
+ children: g("negotiations.form.billType", "Type of bill")
181
+ }), /* @__PURE__ */ m("select", {
182
+ id: "neg-bill-type",
183
+ value: A,
184
+ onChange: (e) => ue(e.target.value),
185
+ className: v,
186
+ children: o.map((e) => /* @__PURE__ */ m("option", {
187
+ value: e,
188
+ children: g(s[e].key, s[e].fallback)
189
+ }, e))
190
+ })] }),
191
+ /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ h("label", {
192
+ htmlFor: "neg-amount",
193
+ className: y,
194
+ children: [
195
+ g("negotiations.form.amount", "What you pay today"),
196
+ " ",
197
+ /* @__PURE__ */ m("span", {
198
+ className: "text-status-error-text",
199
+ children: "*"
200
+ })
201
+ ]
202
+ }), /* @__PURE__ */ m("input", {
203
+ id: "neg-amount",
204
+ type: "number",
205
+ min: "0",
206
+ step: "0.01",
207
+ value: j,
208
+ onChange: (e) => M(e.target.value),
209
+ className: v
210
+ })] }),
211
+ /* @__PURE__ */ h("div", {
212
+ className: "grid grid-cols-2 gap-3",
213
+ children: [/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
214
+ htmlFor: "neg-currency",
215
+ className: y,
216
+ children: g("negotiations.form.currency", "Currency")
217
+ }), /* @__PURE__ */ m("input", {
218
+ id: "neg-currency",
219
+ value: N,
220
+ maxLength: 3,
221
+ onChange: (e) => P(e.target.value),
222
+ className: v
223
+ })] }), /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
224
+ htmlFor: "neg-interval",
225
+ className: y,
226
+ children: g("negotiations.form.interval", "Billed")
227
+ }), /* @__PURE__ */ m("select", {
228
+ id: "neg-interval",
229
+ value: F,
230
+ onChange: (e) => I(e.target.value),
231
+ className: v,
232
+ children: _.map((e) => /* @__PURE__ */ m("option", {
233
+ value: e,
234
+ children: r(e)
235
+ }, e))
236
+ })] })]
237
+ })
238
+ ]
239
+ })
240
+ }),
241
+ /* @__PURE__ */ m(b, {
242
+ icon: /* @__PURE__ */ m(ae, { className: "h-4 w-4" }),
243
+ title: g("negotiations.form.goal.title", "Your goal and leverage"),
244
+ description: g("negotiations.form.goal.description", "Loyalty and a competing offer are the strongest arguments."),
245
+ children: /* @__PURE__ */ h("div", {
246
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
247
+ children: [
248
+ /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
249
+ htmlFor: "neg-target",
250
+ className: y,
251
+ children: g("negotiations.form.target", "What you want")
252
+ }), /* @__PURE__ */ m("select", {
253
+ id: "neg-target",
254
+ value: L,
255
+ onChange: (e) => R(e.target.value),
256
+ className: v,
257
+ children: ee.map((e) => /* @__PURE__ */ m("option", {
258
+ value: e,
259
+ children: g(c[e].key, c[e].fallback)
260
+ }, e))
261
+ })] }),
262
+ /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
263
+ htmlFor: "neg-target-amount",
264
+ className: y,
265
+ children: g("negotiations.form.targetAmount", "Price you would be happy with")
266
+ }), /* @__PURE__ */ m("input", {
267
+ id: "neg-target-amount",
268
+ type: "number",
269
+ min: "0",
270
+ step: "0.01",
271
+ value: z,
272
+ onChange: (e) => B(e.target.value),
273
+ className: v
274
+ })] }),
275
+ /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
276
+ htmlFor: "neg-tenure",
277
+ className: y,
278
+ children: g("negotiations.form.tenure", "Months as a customer")
279
+ }), /* @__PURE__ */ m("input", {
280
+ id: "neg-tenure",
281
+ type: "number",
282
+ min: "0",
283
+ step: "1",
284
+ value: V,
285
+ onChange: (e) => H(e.target.value),
286
+ className: v
287
+ })] }),
288
+ /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
289
+ htmlFor: "neg-competitor",
290
+ className: y,
291
+ children: g("negotiations.form.competitor", "Competing offer you have seen")
292
+ }), /* @__PURE__ */ m("input", {
293
+ id: "neg-competitor",
294
+ value: U,
295
+ onChange: (e) => de(e.target.value),
296
+ placeholder: g("negotiations.form.competitorPlaceholder", "e.g. Acme Fiber 45/mo"),
297
+ className: v
298
+ })] })
299
+ ]
300
+ })
301
+ }),
302
+ /* @__PURE__ */ m(b, {
303
+ icon: /* @__PURE__ */ m(p, { className: "h-4 w-4" }),
304
+ title: g("negotiations.form.mode.title", "Who negotiates"),
305
+ description: g("negotiations.form.mode.description", "You can switch until you submit the request."),
306
+ children: /* @__PURE__ */ h("div", {
307
+ className: "grid grid-cols-1 gap-3 sm:grid-cols-2",
308
+ "data-testid": "negotiation-mode",
309
+ children: [$("SELF_SERVE", g("negotiations.mode.selfServe", "I will call (free)"), g("negotiations.mode.selfServeBody", "Use the playbook and written request, then record what you got.")), $("MANAGED", g("negotiations.mode.managed", "Negotiate for me"), Q == null ? g("negotiations.mode.managedBodyNoFee", "The negotiation desk contacts the provider for you. You only pay a share of the savings.") : g("negotiations.mode.managedBody", "The negotiation desk contacts the provider for you. Fee: {{pct}}% of first-year savings, only if your bill goes down.", { pct: Q }))]
310
+ })
311
+ }),
312
+ W === "MANAGED" && /* @__PURE__ */ m(b, {
313
+ icon: /* @__PURE__ */ m(oe, { className: "h-4 w-4" }),
314
+ title: g("negotiations.form.account.title", "Provider account"),
315
+ description: g("negotiations.form.account.description", "So the desk can find your account. Only the last four characters of the account number are kept."),
316
+ children: /* @__PURE__ */ h("div", {
317
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
318
+ children: [/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ h("label", {
319
+ htmlFor: "neg-holder",
320
+ className: y,
321
+ children: [
322
+ g("negotiations.form.holder", "Name on the account"),
323
+ " ",
324
+ /* @__PURE__ */ m("span", {
325
+ className: "text-status-error-text",
326
+ children: "*"
327
+ })
328
+ ]
329
+ }), /* @__PURE__ */ m("input", {
330
+ id: "neg-holder",
331
+ value: G,
332
+ autoComplete: "name",
333
+ onChange: (e) => pe(e.target.value),
334
+ className: v
335
+ })] }), /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
336
+ htmlFor: "neg-account",
337
+ className: y,
338
+ children: g("negotiations.form.account", "Account number")
339
+ }), /* @__PURE__ */ m("input", {
340
+ id: "neg-account",
341
+ value: K,
342
+ autoComplete: "off",
343
+ onChange: (e) => me(e.target.value),
344
+ className: v
345
+ })] })]
346
+ })
347
+ }),
348
+ /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
349
+ htmlFor: "neg-notes",
350
+ className: y,
351
+ children: g("negotiations.form.notes", "Anything else the negotiator should know")
352
+ }), /* @__PURE__ */ m("textarea", {
353
+ id: "neg-notes",
354
+ rows: 3,
355
+ value: q,
356
+ onChange: (e) => he(e.target.value),
357
+ className: v
358
+ })] }),
359
+ /* @__PURE__ */ m("div", {
360
+ className: "flex justify-end",
361
+ children: /* @__PURE__ */ m("button", {
362
+ type: "submit",
363
+ disabled: C.isPending,
364
+ className: "rounded-lg bg-action-primary-bg px-5 py-2.5 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50",
365
+ children: C.isPending ? g("negotiations.form.creating", "Creating…") : g("negotiations.form.create", "Create request")
366
+ })
367
+ })
368
+ ]
369
+ })]
370
+ });
371
+ }
372
+ //#endregion
373
+ export { x as NegotiationNewPage };
374
+
375
+ //# sourceMappingURL=NegotiationNewPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NegotiationNewPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/negotiations/NegotiationNewPage.tsx"],"sourcesContent":["/**\n * New bill negotiation request\n * @module subscriberbot/pages/negotiations\n *\n * `/subscriberbot/negotiations/new[?relationshipId=…]` — describe the bill and\n * pick a service mode. Creates a DRAFT through `subCreateNegotiation`; the\n * playbook is generated server-side and the customer submits from the detail\n * page (where the managed-desk authorisation is accepted).\n */\n\nimport { useEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { ArrowLeft, Handshake, Receipt, Target, UserRound } from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { BILL_TYPES, BILL_TYPE_LABEL, TARGETS, TARGET_LABEL } from '../../components/negotiations';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useCreateNegotiation, useNegotiationTerms } from '../../hooks/useNegotiationApi';\nimport { useRelationships } from '../../hooks/useSubscriberBotApi';\nimport { useTr } from '../../hooks/useTr';\nimport { cn } from '../../utils/cn';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport type { SubBillingInterval } from '../../types';\nimport type {\n SubCreateNegotiationInput,\n SubNegotiationBillType,\n SubNegotiationMode,\n SubNegotiationTarget,\n} from '../../types/negotiation';\n\nconst INTERVALS: SubBillingInterval[] = ['MONTHLY', 'YEARLY', 'QUARTERLY', 'WEEKLY'];\n\nconst inputCls =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2.5 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst labelCls = 'mb-1.5 block text-xs font-medium text-text-secondary';\n\nfunction FormSection({\n icon,\n title,\n description,\n children,\n}: {\n icon: ReactNode;\n title: string;\n description: string;\n children: ReactNode;\n}) {\n return (\n <GlassCard treatment=\"metric\" className=\"p-5 sm:p-6\">\n <div className=\"flex items-start gap-3\">\n <span className=\"mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <h2 className=\"text-sm font-semibold text-text-primary\">{title}</h2>\n <p className=\"mt-0.5 text-xs text-text-secondary\">{description}</p>\n </div>\n </div>\n <div className=\"mt-4\">{children}</div>\n </GlassCard>\n );\n}\n\nexport function NegotiationNewPage() {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const [params] = useSearchParams();\n const create = useCreateNegotiation();\n const terms = useNegotiationTerms();\n const relationshipsQ = useRelationships({ pagination: { limit: 100, offset: 0 } });\n const relationships = useMemo(() => relationshipsQ.data?.items ?? [], [relationshipsQ.data]);\n\n const [relationshipId, setRelationshipId] = useState(params.get('relationshipId') ?? '');\n const [providerName, setProviderName] = useState('');\n const [billType, setBillType] = useState<SubNegotiationBillType>('INTERNET');\n const [currentAmount, setCurrentAmount] = useState('');\n const [currency, setCurrency] = useState('USD');\n const [billingInterval, setBillingInterval] = useState<SubBillingInterval>('MONTHLY');\n const [targetOutcome, setTargetOutcome] = useState<SubNegotiationTarget>('LOWER_PRICE');\n const [targetAmount, setTargetAmount] = useState('');\n const [tenureMonths, setTenureMonths] = useState('');\n const [competitorOffer, setCompetitorOffer] = useState('');\n const [mode, setMode] = useState<SubNegotiationMode>('SELF_SERVE');\n const [accountHolderName, setAccountHolderName] = useState('');\n const [accountNumber, setAccountNumber] = useState('');\n const [notes, setNotes] = useState('');\n const [formError, setFormError] = useState<string | null>(null);\n\n // Prefill from the linked relationship once per selected relationship, so a\n // background refetch of the relationships list never overwrites the user's edits.\n const linked = relationships.find((r) => r.id === relationshipId);\n const prefilledFor = useRef<string | null>(null);\n useEffect(() => {\n if (!linked || prefilledFor.current === linked.id) return;\n prefilledFor.current = linked.id;\n setProviderName(linked.label);\n if (linked.amount > 0) setCurrentAmount(String(linked.amount));\n setCurrency(linked.currency);\n if (INTERVALS.includes(linked.interval)) setBillingInterval(linked.interval);\n }, [linked]);\n\n const feePct = terms.data ? Math.round(terms.data.managedFeeRate * 100) : null;\n\n const handleSubmit = (e: FormEvent) => {\n e.preventDefault();\n setFormError(null);\n const amount = Number(currentAmount);\n if (!providerName.trim()) {\n setFormError(tr('negotiations.form.providerRequired', 'Who is the bill from?'));\n return;\n }\n if (!currentAmount || !Number.isFinite(amount) || amount <= 0) {\n setFormError(tr('negotiations.form.amountRequired', 'Enter what you pay today.'));\n return;\n }\n if (mode === 'MANAGED' && !accountHolderName.trim()) {\n setFormError(\n tr(\n 'negotiations.form.holderRequired',\n 'The negotiation desk needs the name on the provider account.'\n )\n );\n return;\n }\n const input: SubCreateNegotiationInput = {\n providerName: providerName.trim(),\n billType,\n mode,\n targetOutcome,\n currentAmount: amount,\n currency: currency.trim().toUpperCase() || 'USD',\n interval: billingInterval,\n ...(relationshipId ? { relationshipId } : {}),\n ...(targetAmount ? { targetAmount: Number(targetAmount) } : {}),\n ...(tenureMonths ? { tenureMonths: Number(tenureMonths) } : {}),\n ...(competitorOffer.trim() ? { competitorOffer: competitorOffer.trim() } : {}),\n ...(accountHolderName.trim() ? { accountHolderName: accountHolderName.trim() } : {}),\n ...(accountNumber.trim() ? { accountNumber: accountNumber.trim() } : {}),\n ...(notes.trim() ? { notes: notes.trim() } : {}),\n };\n create.mutate(\n { input },\n {\n onSuccess: (n) => navigateTo(`/negotiations/${n.id}`),\n onError: (err) =>\n setFormError(\n err instanceof Error\n ? err.message\n : tr('negotiations.form.error', 'Could not create the request.')\n ),\n }\n );\n };\n\n const modeCard = (value: SubNegotiationMode, title: string, body: string) => (\n <label\n className={cn(\n 'flex cursor-pointer flex-col gap-1 rounded-xl border p-4 text-left',\n mode === value\n ? 'border-action-primary-bg bg-[var(--color-accent-soft)]'\n : 'border-border-subtle bg-bg-surface hover:bg-bg-sunken'\n )}\n >\n <span className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <input\n type=\"radio\"\n name=\"mode\"\n value={value}\n checked={mode === value}\n onChange={() => setMode(value)}\n />\n {title}\n </span>\n <span className=\"text-xs text-text-secondary\">{body}</span>\n </label>\n );\n\n return (\n <PageLayout\n title={tr('negotiations.new.title', 'Negotiate a bill')}\n description={tr(\n 'negotiations.new.description',\n 'Tell us about the bill and how you want to handle it.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/negotiations')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> {tr('negotiations.back', 'Back')}\n </button>\n }\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'negotiations.new.purpose',\n 'You will get a negotiation playbook for this bill right away. Nothing is sent to the provider until you submit the request.'\n )}\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"max-w-3xl space-y-5\" data-testid=\"negotiation-form\">\n {formError && (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\"\n >\n {formError}\n </div>\n )}\n\n <FormSection\n icon={<Receipt className=\"h-4 w-4\" />}\n title={tr('negotiations.form.bill.title', 'The bill')}\n description={tr(\n 'negotiations.form.bill.description',\n 'Link a tracked subscription to prefill it, or describe the bill.'\n )}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div className=\"sm:col-span-2\">\n <label htmlFor=\"neg-relationship\" className={labelCls}>\n {tr('negotiations.form.relationship', 'Tracked subscription (optional)')}\n </label>\n <select\n id=\"neg-relationship\"\n value={relationshipId}\n onChange={(e) => setRelationshipId(e.target.value)}\n className={inputCls}\n >\n <option value=\"\">{tr('negotiations.form.noRelationship', 'Not linked')}</option>\n {relationships.map((r) => (\n <option key={r.id} value={r.id}>\n {r.label}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"neg-provider\" className={labelCls}>\n {tr('negotiations.form.provider', 'Provider')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"neg-provider\"\n value={providerName}\n onChange={(e) => setProviderName(e.target.value)}\n placeholder={tr('negotiations.form.providerPlaceholder', 'e.g. Comcast Xfinity')}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"neg-bill-type\" className={labelCls}>\n {tr('negotiations.form.billType', 'Type of bill')}\n </label>\n <select\n id=\"neg-bill-type\"\n value={billType}\n onChange={(e) => setBillType(e.target.value as SubNegotiationBillType)}\n className={inputCls}\n >\n {BILL_TYPES.map((b) => (\n <option key={b} value={b}>\n {tr(BILL_TYPE_LABEL[b].key, BILL_TYPE_LABEL[b].fallback)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"neg-amount\" className={labelCls}>\n {tr('negotiations.form.amount', 'What you pay today')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"neg-amount\"\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={currentAmount}\n onChange={(e) => setCurrentAmount(e.target.value)}\n className={inputCls}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"neg-currency\" className={labelCls}>\n {tr('negotiations.form.currency', 'Currency')}\n </label>\n <input\n id=\"neg-currency\"\n value={currency}\n maxLength={3}\n onChange={(e) => setCurrency(e.target.value)}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"neg-interval\" className={labelCls}>\n {tr('negotiations.form.interval', 'Billed')}\n </label>\n <select\n id=\"neg-interval\"\n value={billingInterval}\n onChange={(e) => setBillingInterval(e.target.value as SubBillingInterval)}\n className={inputCls}\n >\n {INTERVALS.map((i) => (\n <option key={i} value={i}>\n {formatEnumLabel(i)}\n </option>\n ))}\n </select>\n </div>\n </div>\n </div>\n </FormSection>\n\n <FormSection\n icon={<Target className=\"h-4 w-4\" />}\n title={tr('negotiations.form.goal.title', 'Your goal and leverage')}\n description={tr(\n 'negotiations.form.goal.description',\n 'Loyalty and a competing offer are the strongest arguments.'\n )}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"neg-target\" className={labelCls}>\n {tr('negotiations.form.target', 'What you want')}\n </label>\n <select\n id=\"neg-target\"\n value={targetOutcome}\n onChange={(e) => setTargetOutcome(e.target.value as SubNegotiationTarget)}\n className={inputCls}\n >\n {TARGETS.map((t) => (\n <option key={t} value={t}>\n {tr(TARGET_LABEL[t].key, TARGET_LABEL[t].fallback)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"neg-target-amount\" className={labelCls}>\n {tr('negotiations.form.targetAmount', 'Price you would be happy with')}\n </label>\n <input\n id=\"neg-target-amount\"\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={targetAmount}\n onChange={(e) => setTargetAmount(e.target.value)}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"neg-tenure\" className={labelCls}>\n {tr('negotiations.form.tenure', 'Months as a customer')}\n </label>\n <input\n id=\"neg-tenure\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={tenureMonths}\n onChange={(e) => setTenureMonths(e.target.value)}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"neg-competitor\" className={labelCls}>\n {tr('negotiations.form.competitor', 'Competing offer you have seen')}\n </label>\n <input\n id=\"neg-competitor\"\n value={competitorOffer}\n onChange={(e) => setCompetitorOffer(e.target.value)}\n placeholder={tr('negotiations.form.competitorPlaceholder', 'e.g. Acme Fiber 45/mo')}\n className={inputCls}\n />\n </div>\n </div>\n </FormSection>\n\n <FormSection\n icon={<Handshake className=\"h-4 w-4\" />}\n title={tr('negotiations.form.mode.title', 'Who negotiates')}\n description={tr(\n 'negotiations.form.mode.description',\n 'You can switch until you submit the request.'\n )}\n >\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\" data-testid=\"negotiation-mode\">\n {modeCard(\n 'SELF_SERVE',\n tr('negotiations.mode.selfServe', 'I will call (free)'),\n tr(\n 'negotiations.mode.selfServeBody',\n 'Use the playbook and written request, then record what you got.'\n )\n )}\n {modeCard(\n 'MANAGED',\n tr('negotiations.mode.managed', 'Negotiate for me'),\n feePct != null\n ? tr(\n 'negotiations.mode.managedBody',\n 'The negotiation desk contacts the provider for you. Fee: {{pct}}% of first-year savings, only if your bill goes down.',\n { pct: feePct }\n )\n : tr(\n 'negotiations.mode.managedBodyNoFee',\n 'The negotiation desk contacts the provider for you. You only pay a share of the savings.'\n )\n )}\n </div>\n </FormSection>\n\n {mode === 'MANAGED' && (\n <FormSection\n icon={<UserRound className=\"h-4 w-4\" />}\n title={tr('negotiations.form.account.title', 'Provider account')}\n description={tr(\n 'negotiations.form.account.description',\n 'So the desk can find your account. Only the last four characters of the account number are kept.'\n )}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"neg-holder\" className={labelCls}>\n {tr('negotiations.form.holder', 'Name on the account')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"neg-holder\"\n value={accountHolderName}\n autoComplete=\"name\"\n onChange={(e) => setAccountHolderName(e.target.value)}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"neg-account\" className={labelCls}>\n {tr('negotiations.form.account', 'Account number')}\n </label>\n <input\n id=\"neg-account\"\n value={accountNumber}\n autoComplete=\"off\"\n onChange={(e) => setAccountNumber(e.target.value)}\n className={inputCls}\n />\n </div>\n </div>\n </FormSection>\n )}\n\n <div>\n <label htmlFor=\"neg-notes\" className={labelCls}>\n {tr('negotiations.form.notes', 'Anything else the negotiator should know')}\n </label>\n <textarea\n id=\"neg-notes\"\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n className={inputCls}\n />\n </div>\n\n <div className=\"flex justify-end\">\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"rounded-lg bg-action-primary-bg px-5 py-2.5 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending\n ? tr('negotiations.form.creating', 'Creating…')\n : tr('negotiations.form.create', 'Create request')}\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Bill negotiation overview
3
+ * @module subscriberbot/pages/negotiations
4
+ *
5
+ * `/subscriberbot/negotiations` — every bill negotiation request in the
6
+ * workspace (BOFF-4154) with the savings they produced, backed by the real
7
+ * `subListNegotiations` / `subNegotiationSummary` queries.
8
+ */
9
+ export declare function NegotiationsPage(): import("react").JSX.Element;
10
+ //# sourceMappingURL=NegotiationsPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NegotiationsPage.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/pages/negotiations/NegotiationsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA8BH,wBAAgB,gBAAgB,gCAwK/B"}
@@ -0,0 +1,155 @@
1
+ import { useSubscriberBot as e } from "../../context/SubscriberBotContext.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { PageLayout as n } from "../../components/PageLayout.js";
4
+ import { MetricsCard as r } from "../../components/MetricsCard.js";
5
+ import { formatCurrency as i, formatDate as a, formatIntervalSuffix as o } from "../../utils/formatters.js";
6
+ import { EmptyState as s } from "../../components/EmptyState.js";
7
+ import { useTr as c } from "../../hooks/useTr.js";
8
+ import { ErrorView as l, LoadingView as u } from "../../components/StateViews.js";
9
+ import "../../components/index.js";
10
+ import { BILL_TYPE_LABEL as d, STATUS_LABEL as f, TARGET_LABEL as p } from "../../components/negotiations/labels.js";
11
+ import { NegotiationStatusPill as m } from "../../components/negotiations/NegotiationStatusPill.js";
12
+ import "../../components/negotiations/index.js";
13
+ import { useNegotiationSummary as h, useNegotiations as g } from "../../hooks/useNegotiationApi.js";
14
+ import { useState as _ } from "react";
15
+ import { BadgePercent as v, Handshake as y, PiggyBank as b, Plus as x, Timer as S } from "lucide-react";
16
+ import { Fragment as C, jsx as w, jsxs as T } from "react/jsx-runtime";
17
+ import { PagePurpose as E } from "@burdenoff/fe-libs/ui";
18
+ //#region src/subscriberbot/pages/negotiations/NegotiationsPage.tsx
19
+ var D = [
20
+ "ALL",
21
+ "DRAFT",
22
+ "SUBMITTED",
23
+ "IN_PROGRESS",
24
+ "AWAITING_CUSTOMER",
25
+ "SUCCEEDED",
26
+ "NO_SAVINGS",
27
+ "CANCELLED"
28
+ ];
29
+ function O() {
30
+ let O = c(), { navigateTo: k } = e(), [A, j] = _("ALL"), M = g(A === "ALL" ? null : A), N = h(), P = M.data?.items ?? [], F = N.data?.totals.filter((e) => e.firstYearSavings > 0).map((e) => i(e.firstYearSavings, e.currency)).join(" + ") || i(0, "USD"), I = /* @__PURE__ */ T("button", {
31
+ type: "button",
32
+ onClick: () => k("/negotiations/new"),
33
+ className: "inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:opacity-90",
34
+ children: [
35
+ /* @__PURE__ */ w(x, {
36
+ className: "h-4 w-4",
37
+ "aria-hidden": !0
38
+ }),
39
+ " ",
40
+ O("negotiations.new", "Negotiate a bill")
41
+ ]
42
+ });
43
+ return /* @__PURE__ */ T(n, {
44
+ title: O("negotiations.title", "Bill negotiation"),
45
+ description: O("negotiations.description", "Get cable, internet, phone, insurance, utility and subscription bills lowered."),
46
+ actions: I,
47
+ children: [
48
+ /* @__PURE__ */ w(E, {
49
+ className: "mb-6",
50
+ children: O("negotiations.purpose", "Do it yourself with a negotiation playbook written for your bill (free), or let the Subscriber Bot negotiation desk negotiate for you. The desk only charges a share of the savings it gets you: no savings, no fee.")
51
+ }),
52
+ !N.isError && /* @__PURE__ */ T("div", {
53
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
54
+ children: [
55
+ /* @__PURE__ */ w(r, {
56
+ icon: /* @__PURE__ */ w(S, { className: "h-5 w-5" }),
57
+ value: N.isLoading ? "—" : N.data?.openCount ?? 0,
58
+ label: O("negotiations.metrics.open", "Open requests")
59
+ }),
60
+ /* @__PURE__ */ w(r, {
61
+ icon: /* @__PURE__ */ w(y, { className: "h-5 w-5" }),
62
+ value: N.isLoading ? "—" : N.data?.succeededCount ?? 0,
63
+ label: O("negotiations.metrics.succeeded", "Bills lowered")
64
+ }),
65
+ /* @__PURE__ */ w(r, {
66
+ icon: /* @__PURE__ */ w(b, { className: "h-5 w-5" }),
67
+ value: N.isLoading ? "—" : F,
68
+ label: O("negotiations.metrics.savings", "First-year savings")
69
+ }),
70
+ /* @__PURE__ */ w(r, {
71
+ icon: /* @__PURE__ */ w(v, { className: "h-5 w-5" }),
72
+ value: N.isLoading ? "—" : N.data?.noSavingsCount ?? 0,
73
+ label: O("negotiations.metrics.noSavings", "No savings (no fee)")
74
+ })
75
+ ]
76
+ }),
77
+ /* @__PURE__ */ w("div", {
78
+ role: "tablist",
79
+ "aria-label": O("negotiations.filter.label", "Request status"),
80
+ className: "mt-6 flex flex-wrap gap-1.5",
81
+ children: D.map((e) => /* @__PURE__ */ w("button", {
82
+ type: "button",
83
+ role: "tab",
84
+ "aria-selected": A === e,
85
+ onClick: () => j(e),
86
+ className: t("rounded-lg px-3 py-1.5 text-xs font-medium", A === e ? "bg-action-primary-bg text-action-primary-text" : "border border-border-subtle text-text-primary hover:bg-bg-sunken"),
87
+ children: e === "ALL" ? O("negotiations.filter.all", "All") : O(f[e].key, f[e].fallback)
88
+ }, e))
89
+ }),
90
+ /* @__PURE__ */ w("div", {
91
+ className: "mt-4",
92
+ children: M.isLoading ? /* @__PURE__ */ w(u, { label: O("negotiations.loading", "Loading negotiations…") }) : M.isError ? /* @__PURE__ */ w(l, {
93
+ message: M.error instanceof Error ? M.error.message : void 0,
94
+ onRetry: () => void M.refetch()
95
+ }) : P.length === 0 ? /* @__PURE__ */ w(s, {
96
+ illustration: "empty-data",
97
+ title: O("negotiations.empty.title", "No negotiation requests yet"),
98
+ description: O("negotiations.empty.description", "Pick a bill that feels too high. You get a playbook straight away, and can hand it to the negotiation desk if you would rather not make the call."),
99
+ action: I
100
+ }) : /* @__PURE__ */ w("ul", {
101
+ className: "grid grid-cols-1 gap-3 lg:grid-cols-2",
102
+ "data-testid": "negotiation-list",
103
+ children: P.map((e) => /* @__PURE__ */ w("li", { children: /* @__PURE__ */ T("button", {
104
+ type: "button",
105
+ onClick: () => k(`/negotiations/${e.id}`),
106
+ className: "w-full rounded-xl border border-border-subtle bg-bg-surface p-4 text-left hover:bg-bg-sunken focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]",
107
+ children: [/* @__PURE__ */ T("div", {
108
+ className: "flex items-start justify-between gap-3",
109
+ children: [/* @__PURE__ */ T("div", {
110
+ className: "min-w-0",
111
+ children: [/* @__PURE__ */ w("p", {
112
+ className: "truncate text-sm font-semibold text-text-primary",
113
+ children: e.providerName
114
+ }), /* @__PURE__ */ T("p", {
115
+ className: "mt-0.5 text-xs text-text-secondary",
116
+ children: [
117
+ O(d[e.billType].key, d[e.billType].fallback),
118
+ " ",
119
+ "·",
120
+ " ",
121
+ O(p[e.targetOutcome].key, p[e.targetOutcome].fallback),
122
+ " ",
123
+ "·",
124
+ " ",
125
+ e.mode === "MANAGED" ? O("negotiations.mode.managedShort", "Negotiation desk") : O("negotiations.mode.selfServeShort", "Do it yourself")
126
+ ]
127
+ })]
128
+ }), /* @__PURE__ */ w(m, { status: e.status })]
129
+ }), /* @__PURE__ */ T("div", {
130
+ className: "mt-3 flex flex-wrap items-end justify-between gap-2 text-sm",
131
+ children: [/* @__PURE__ */ T("span", {
132
+ className: "text-text-primary",
133
+ children: [
134
+ i(e.currentAmount, e.currency),
135
+ o(e.interval),
136
+ e.newAmount != null && e.status === "SUCCEEDED" && /* @__PURE__ */ T(C, { children: [" → ", /* @__PURE__ */ T("span", {
137
+ className: "font-semibold text-status-success-text",
138
+ children: [i(e.newAmount, e.currency), o(e.interval)]
139
+ })] })
140
+ ]
141
+ }), /* @__PURE__ */ w("span", {
142
+ className: "text-xs text-text-secondary",
143
+ children: e.firstYearSavings > 0 ? O("negotiations.card.saved", "Saves {{amount}} in year one", { amount: i(e.firstYearSavings, e.currency) }) : a(e.createdAt)
144
+ })]
145
+ })]
146
+ }) }, e.id))
147
+ })
148
+ })
149
+ ]
150
+ });
151
+ }
152
+ //#endregion
153
+ export { O as NegotiationsPage };
154
+
155
+ //# sourceMappingURL=NegotiationsPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NegotiationsPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/negotiations/NegotiationsPage.tsx"],"sourcesContent":["/**\n * Bill negotiation overview\n * @module subscriberbot/pages/negotiations\n *\n * `/subscriberbot/negotiations` — every bill negotiation request in the\n * workspace (BOFF-4154) with the savings they produced, backed by the real\n * `subListNegotiations` / `subNegotiationSummary` queries.\n */\n\nimport { useState } from 'react';\nimport { BadgePercent, Handshake, PiggyBank, Plus, Timer } from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { EmptyState, ErrorView, LoadingView, MetricsCard, PageLayout } from '../../components';\nimport {\n BILL_TYPE_LABEL,\n NegotiationStatusPill,\n STATUS_LABEL,\n TARGET_LABEL,\n} from '../../components/negotiations';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useNegotiations, useNegotiationSummary } from '../../hooks/useNegotiationApi';\nimport { useTr } from '../../hooks/useTr';\nimport { cn } from '../../utils/cn';\nimport { formatCurrency, formatDate, formatIntervalSuffix } from '../../utils/formatters';\nimport type { SubNegotiationStatus } from '../../types/negotiation';\n\nconst FILTERS: Array<SubNegotiationStatus | 'ALL'> = [\n 'ALL',\n 'DRAFT',\n 'SUBMITTED',\n 'IN_PROGRESS',\n 'AWAITING_CUSTOMER',\n 'SUCCEEDED',\n 'NO_SAVINGS',\n 'CANCELLED',\n];\n\nexport function NegotiationsPage() {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const [filter, setFilter] = useState<SubNegotiationStatus | 'ALL'>('ALL');\n const list = useNegotiations(filter === 'ALL' ? null : filter);\n const summary = useNegotiationSummary();\n const items = list.data?.items ?? [];\n\n const savingsText =\n summary.data?.totals\n .filter((t) => t.firstYearSavings > 0)\n .map((t) => formatCurrency(t.firstYearSavings, t.currency))\n .join(' + ') || formatCurrency(0, 'USD');\n\n const newButton = (\n <button\n type=\"button\"\n onClick={() => navigateTo('/negotiations/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" aria-hidden /> {tr('negotiations.new', 'Negotiate a bill')}\n </button>\n );\n\n return (\n <PageLayout\n title={tr('negotiations.title', 'Bill negotiation')}\n description={tr(\n 'negotiations.description',\n 'Get cable, internet, phone, insurance, utility and subscription bills lowered.'\n )}\n actions={newButton}\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'negotiations.purpose',\n 'Do it yourself with a negotiation playbook written for your bill (free), or let the Subscriber Bot negotiation desk negotiate for you. The desk only charges a share of the savings it gets you: no savings, no fee.'\n )}\n </PagePurpose>\n\n {!summary.isError && (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <MetricsCard\n icon={<Timer className=\"h-5 w-5\" />}\n value={summary.isLoading ? '—' : (summary.data?.openCount ?? 0)}\n label={tr('negotiations.metrics.open', 'Open requests')}\n />\n <MetricsCard\n icon={<Handshake className=\"h-5 w-5\" />}\n value={summary.isLoading ? '—' : (summary.data?.succeededCount ?? 0)}\n label={tr('negotiations.metrics.succeeded', 'Bills lowered')}\n />\n <MetricsCard\n icon={<PiggyBank className=\"h-5 w-5\" />}\n value={summary.isLoading ? '—' : savingsText}\n label={tr('negotiations.metrics.savings', 'First-year savings')}\n />\n <MetricsCard\n icon={<BadgePercent className=\"h-5 w-5\" />}\n value={summary.isLoading ? '—' : (summary.data?.noSavingsCount ?? 0)}\n label={tr('negotiations.metrics.noSavings', 'No savings (no fee)')}\n />\n </div>\n )}\n\n <div\n role=\"tablist\"\n aria-label={tr('negotiations.filter.label', 'Request status')}\n className=\"mt-6 flex flex-wrap gap-1.5\"\n >\n {FILTERS.map((f) => (\n <button\n key={f}\n type=\"button\"\n role=\"tab\"\n aria-selected={filter === f}\n onClick={() => setFilter(f)}\n className={cn(\n 'rounded-lg px-3 py-1.5 text-xs font-medium',\n filter === f\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'border border-border-subtle text-text-primary hover:bg-bg-sunken'\n )}\n >\n {f === 'ALL'\n ? tr('negotiations.filter.all', 'All')\n : tr(STATUS_LABEL[f].key, STATUS_LABEL[f].fallback)}\n </button>\n ))}\n </div>\n\n <div className=\"mt-4\">\n {list.isLoading ? (\n <LoadingView label={tr('negotiations.loading', 'Loading negotiations…')} />\n ) : list.isError ? (\n <ErrorView\n message={list.error instanceof Error ? list.error.message : undefined}\n onRetry={() => void list.refetch()}\n />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('negotiations.empty.title', 'No negotiation requests yet')}\n description={tr(\n 'negotiations.empty.description',\n 'Pick a bill that feels too high. You get a playbook straight away, and can hand it to the negotiation desk if you would rather not make the call.'\n )}\n action={newButton}\n />\n ) : (\n <ul className=\"grid grid-cols-1 gap-3 lg:grid-cols-2\" data-testid=\"negotiation-list\">\n {items.map((n) => (\n <li key={n.id}>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/negotiations/${n.id}`)}\n className=\"w-full rounded-xl border border-border-subtle bg-bg-surface p-4 text-left hover:bg-bg-sunken focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {n.providerName}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {tr(BILL_TYPE_LABEL[n.billType].key, BILL_TYPE_LABEL[n.billType].fallback)}{' '}\n ·{' '}\n {tr(\n TARGET_LABEL[n.targetOutcome].key,\n TARGET_LABEL[n.targetOutcome].fallback\n )}{' '}\n ·{' '}\n {n.mode === 'MANAGED'\n ? tr('negotiations.mode.managedShort', 'Negotiation desk')\n : tr('negotiations.mode.selfServeShort', 'Do it yourself')}\n </p>\n </div>\n <NegotiationStatusPill status={n.status} />\n </div>\n <div className=\"mt-3 flex flex-wrap items-end justify-between gap-2 text-sm\">\n <span className=\"text-text-primary\">\n {formatCurrency(n.currentAmount, n.currency)}\n {formatIntervalSuffix(n.interval)}\n {n.newAmount != null && n.status === 'SUCCEEDED' && (\n <>\n {' → '}\n <span className=\"font-semibold text-status-success-text\">\n {formatCurrency(n.newAmount, n.currency)}\n {formatIntervalSuffix(n.interval)}\n </span>\n </>\n )}\n </span>\n <span className=\"text-xs text-text-secondary\">\n {n.firstYearSavings > 0\n ? tr('negotiations.card.saved', 'Saves {{amount}} in year one', {\n amount: formatCurrency(n.firstYearSavings, n.currency),\n })\n : formatDate(n.createdAt)}\n </span>\n </div>\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0BA,IAAM,IAA+C;CACnD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAgB,IAAmB;CACjC,IAAM,IAAK,EAAM,GACX,EAAE,kBAAe,EAAiB,GAClC,CAAC,GAAQ,KAAa,EAAuC,KAAK,GAClE,IAAO,EAAgB,MAAW,QAAQ,OAAO,CAAM,GACvD,IAAU,EAAsB,GAChC,IAAQ,EAAK,MAAM,SAAS,CAAC,GAE7B,IACJ,EAAQ,MAAM,OACX,QAAQ,MAAM,EAAE,mBAAmB,CAAC,EACpC,KAAK,MAAM,EAAe,EAAE,kBAAkB,EAAE,QAAQ,CAAC,EACzD,KAAK,KAAK,KAAK,EAAe,GAAG,KAAK,GAErC,IACJ,kBAAC,UAAD;EACE,MAAK;EACL,eAAe,EAAW,mBAAmB;EAC7C,WAAU;YAHZ;GAKE,kBAAC,GAAD;IAAM,WAAU;IAAU,eAAA;GAAa,CAAA;GAAC;GAAE,EAAG,oBAAoB,kBAAkB;EAC7E;;CAGV,OACE,kBAAC,GAAD;EACE,OAAO,EAAG,sBAAsB,kBAAkB;EAClD,aAAa,EACX,4BACA,gFACF;EACA,SAAS;YANX;GAQE,kBAAC,GAAD;IAAa,WAAU;cACpB,EACC,wBACA,sNACF;GACW,CAAA;GAEZ,CAAC,EAAQ,WACR,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;MAClC,OAAO,EAAQ,YAAY,MAAO,EAAQ,MAAM,aAAa;MAC7D,OAAO,EAAG,6BAA6B,eAAe;KACvD,CAAA;KACD,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;MACtC,OAAO,EAAQ,YAAY,MAAO,EAAQ,MAAM,kBAAkB;MAClE,OAAO,EAAG,kCAAkC,eAAe;KAC5D,CAAA;KACD,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;MACtC,OAAO,EAAQ,YAAY,MAAM;MACjC,OAAO,EAAG,gCAAgC,oBAAoB;KAC/D,CAAA;KACD,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA;MACzC,OAAO,EAAQ,YAAY,MAAO,EAAQ,MAAM,kBAAkB;MAClE,OAAO,EAAG,kCAAkC,qBAAqB;KAClE,CAAA;IACE;;GAGP,kBAAC,OAAD;IACE,MAAK;IACL,cAAY,EAAG,6BAA6B,gBAAgB;IAC5D,WAAU;cAET,EAAQ,KAAK,MACZ,kBAAC,UAAD;KAEE,MAAK;KACL,MAAK;KACL,iBAAe,MAAW;KAC1B,eAAe,EAAU,CAAC;KAC1B,WAAW,EACT,8CACA,MAAW,IACP,kDACA,kEACN;eAEC,MAAM,QACH,EAAG,2BAA2B,KAAK,IACnC,EAAG,EAAa,GAAG,KAAK,EAAa,GAAG,QAAQ;IAC9C,GAfD,CAeC,CACT;GACE,CAAA;GAEL,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAK,YACJ,kBAAC,GAAD,EAAa,OAAO,EAAG,wBAAwB,uBAAuB,EAAI,CAAA,IACxE,EAAK,UACP,kBAAC,GAAD;KACE,SAAS,EAAK,iBAAiB,QAAQ,EAAK,MAAM,UAAU,KAAA;KAC5D,eAAe,KAAK,EAAK,QAAQ;IAClC,CAAA,IACC,EAAM,WAAW,IACnB,kBAAC,GAAD;KACE,cAAa;KACb,OAAO,EAAG,4BAA4B,6BAA6B;KACnE,aAAa,EACX,kCACA,mJACF;KACA,QAAQ;IACT,CAAA,IAED,kBAAC,MAAD;KAAI,WAAU;KAAwC,eAAY;eAC/D,EAAM,KAAK,MACV,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAW,iBAAiB,EAAE,IAAI;MACjD,WAAU;gBAHZ,CAKE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAE;QACF,CAAA,GACH,kBAAC,KAAD;SAAG,WAAU;mBAAb;UACG,EAAG,EAAgB,EAAE,UAAU,KAAK,EAAgB,EAAE,UAAU,QAAQ;UAAG;UAAI;UAC9E;UACD,EACC,EAAa,EAAE,eAAe,KAC9B,EAAa,EAAE,eAAe,QAChC;UAAG;UAAI;UACL;UACD,EAAE,SAAS,YACR,EAAG,kCAAkC,kBAAkB,IACvD,EAAG,oCAAoC,gBAAgB;SAC1D;UACA;WACL,kBAAC,GAAD,EAAuB,QAAQ,EAAE,OAAS,CAAA,CACvC;UACL,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAe,EAAE,eAAe,EAAE,QAAQ;SAC1C,EAAqB,EAAE,QAAQ;SAC/B,EAAE,aAAa,QAAQ,EAAE,WAAW,eACnC,kBAAA,GAAA,EAAA,UAAA,CACG,OACD,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACG,EAAe,EAAE,WAAW,EAAE,QAAQ,GACtC,EAAqB,EAAE,QAAQ,CAC5B;WACN,EAAA,CAAA;QAEA;WACN,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAE,mBAAmB,IAClB,EAAG,2BAA2B,gCAAgC,EAC5D,QAAQ,EAAe,EAAE,kBAAkB,EAAE,QAAQ,EACvD,CAAC,IACD,EAAW,EAAE,SAAS;OACtB,CAAA,CACH;QACC;QACN,GAjDK,EAAE,EAiDP,CACL;IACC,CAAA;GAEH,CAAA;EACK;;AAEhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProvidersListPage.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/pages/providers/ProvidersListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoKH,wBAAgB,iBAAiB,gCA8PhC"}
1
+ {"version":3,"file":"ProvidersListPage.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/pages/providers/ProvidersListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoKH,wBAAgB,iBAAiB,gCA+PhC"}