@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,4 @@
1
+ import { FC } from 'react';
2
+
3
+ export declare const NegotiationDeskPanel: FC;
4
+ //# sourceMappingURL=NegotiationDeskPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NegotiationDeskPanel.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/admin/NegotiationDeskPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAoN1C,eAAO,MAAM,oBAAoB,EAAE,EA6ClC,CAAC"}
@@ -0,0 +1,210 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { formatCurrency as t, formatDateTime as n, formatIntervalSuffix as r } from "../utils/formatters.js";
3
+ import { useTr as i } from "../hooks/useTr.js";
4
+ import { BILL_TYPE_LABEL as a, FEE_STATUS_LABEL as o, STATUS_LABEL as s } from "../components/negotiations/labels.js";
5
+ import { NegotiationStatusPill as c } from "../components/negotiations/NegotiationStatusPill.js";
6
+ import { NegotiationTimeline as l } from "../components/negotiations/NegotiationTimeline.js";
7
+ import { OutcomeForm as u } from "../components/negotiations/OutcomeForm.js";
8
+ import "../components/negotiations/index.js";
9
+ import { useDeskNegotiations as d, useDeskRecordOutcome as f, useDeskRetryFeeBilling as p, useDeskSetStatus as m, useDeskWaiveFee as h } from "../hooks/useNegotiationApi.js";
10
+ import { useState as g } from "react";
11
+ import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
12
+ //#region src/subscriberbot/admin/NegotiationDeskPanel.tsx
13
+ var b = [
14
+ null,
15
+ "SUBMITTED",
16
+ "IN_PROGRESS",
17
+ "AWAITING_CUSTOMER",
18
+ "SUCCEEDED",
19
+ "NO_SAVINGS",
20
+ "CANCELLED"
21
+ ], x = "w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]", S = "rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50", C = (e) => e ? e instanceof Error ? e.message : String(e) : null, w = ({ negotiation: e }) => {
22
+ let s = i(), d = m(), b = f(), w = p(), T = h(), [E, D] = g(""), [O, k] = g(!1), A = [
23
+ "SUBMITTED",
24
+ "IN_PROGRESS",
25
+ "AWAITING_CUSTOMER"
26
+ ].includes(e.status), j = o[e.feeStatus], M = C(d.error) ?? C(w.error) ?? C(T.error);
27
+ return /* @__PURE__ */ y("li", {
28
+ className: "rounded-xl border border-border-subtle bg-bg-surface p-4",
29
+ "data-testid": "desk-item",
30
+ children: [
31
+ /* @__PURE__ */ y("div", {
32
+ className: "flex flex-wrap items-start justify-between gap-3",
33
+ children: [/* @__PURE__ */ y("div", {
34
+ className: "min-w-0",
35
+ children: [/* @__PURE__ */ y("p", {
36
+ className: "text-sm font-semibold text-text-primary",
37
+ children: [
38
+ e.providerName,
39
+ " ·",
40
+ " ",
41
+ s(a[e.billType].key, a[e.billType].fallback)
42
+ ]
43
+ }), /* @__PURE__ */ y("p", {
44
+ className: "mt-0.5 text-xs text-text-secondary",
45
+ children: [
46
+ t(e.currentAmount, e.currency),
47
+ r(e.interval),
48
+ e.targetAmount != null && ` → ${s("admin.desk.target", "target")} ${t(e.targetAmount, e.currency)}`,
49
+ " · ",
50
+ e.accountHolderName ?? "—",
51
+ e.accountHint ? ` ··${e.accountHint}` : "",
52
+ e.submittedAt ? ` · ${n(e.submittedAt)}` : ""
53
+ ]
54
+ })]
55
+ }), /* @__PURE__ */ y("div", {
56
+ className: "flex items-center gap-2",
57
+ children: [e.feeStatus !== "NONE" && /* @__PURE__ */ y("span", {
58
+ className: "text-xs text-text-secondary",
59
+ children: [
60
+ t(e.feeAmount, e.currency),
61
+ " · ",
62
+ s(j.key, j.fallback)
63
+ ]
64
+ }), /* @__PURE__ */ v(c, { status: e.status })]
65
+ })]
66
+ }),
67
+ /* @__PURE__ */ y("div", {
68
+ className: "mt-3 flex flex-wrap gap-2",
69
+ children: [
70
+ (e.status === "SUBMITTED" || e.status === "AWAITING_CUSTOMER") && /* @__PURE__ */ v("button", {
71
+ type: "button",
72
+ className: S,
73
+ disabled: d.isPending,
74
+ onClick: () => d.mutate({
75
+ id: e.id,
76
+ status: "IN_PROGRESS"
77
+ }),
78
+ children: e.status === "SUBMITTED" ? s("admin.desk.pickUp", "Pick up") : s("admin.desk.resume", "Resume")
79
+ }),
80
+ e.feeStatus === "PENDING_BILLING" && /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v("button", {
81
+ type: "button",
82
+ className: S,
83
+ disabled: w.isPending,
84
+ onClick: () => w.mutate({ id: e.id }),
85
+ children: s("admin.desk.retryFee", "Retry fee billing")
86
+ }), /* @__PURE__ */ v("button", {
87
+ type: "button",
88
+ className: S,
89
+ disabled: T.isPending || !E.trim(),
90
+ onClick: () => T.mutate({
91
+ id: e.id,
92
+ reason: E.trim()
93
+ }),
94
+ children: s("admin.desk.waiveFee", "Waive fee (reason below)")
95
+ })] }),
96
+ /* @__PURE__ */ v("button", {
97
+ type: "button",
98
+ className: S,
99
+ onClick: () => k((e) => !e),
100
+ children: O ? s("admin.desk.hide", "Hide details") : s("admin.desk.show", "Playbook, notes and timeline")
101
+ })
102
+ ]
103
+ }),
104
+ (A || e.feeStatus === "PENDING_BILLING") && /* @__PURE__ */ y("div", {
105
+ className: "mt-3 space-y-2",
106
+ children: [/* @__PURE__ */ v("textarea", {
107
+ rows: 2,
108
+ value: E,
109
+ onChange: (e) => D(e.target.value),
110
+ "aria-label": s("admin.desk.message", "Message to the customer or reason"),
111
+ placeholder: s("admin.desk.messagePlaceholder", "Message to the customer, or reason"),
112
+ className: x
113
+ }), A && /* @__PURE__ */ y("div", {
114
+ className: "flex flex-wrap gap-2",
115
+ children: [e.status === "IN_PROGRESS" && /* @__PURE__ */ v("button", {
116
+ type: "button",
117
+ className: S,
118
+ disabled: d.isPending || !E.trim(),
119
+ onClick: () => d.mutate({
120
+ id: e.id,
121
+ status: "AWAITING_CUSTOMER",
122
+ message: E.trim()
123
+ }, { onSuccess: () => D("") }),
124
+ children: s("admin.desk.askCustomer", "Ask the customer")
125
+ }), /* @__PURE__ */ v("button", {
126
+ type: "button",
127
+ className: S,
128
+ disabled: d.isPending,
129
+ onClick: () => d.mutate({
130
+ id: e.id,
131
+ status: "CANCELLED",
132
+ message: E.trim() || null
133
+ }),
134
+ children: s("admin.desk.close", "Close request")
135
+ })]
136
+ })]
137
+ }),
138
+ M && /* @__PURE__ */ v("p", {
139
+ role: "alert",
140
+ className: "mt-2 text-sm text-status-error-text",
141
+ children: M
142
+ }),
143
+ A && /* @__PURE__ */ y("div", {
144
+ className: "mt-4 rounded-lg border border-border-subtle p-3",
145
+ children: [/* @__PURE__ */ v("p", {
146
+ className: "mb-2 text-xs font-medium text-text-secondary",
147
+ children: s("admin.desk.recordOutcome", "Record the outcome")
148
+ }), /* @__PURE__ */ v(u, {
149
+ negotiation: e,
150
+ pending: b.isPending,
151
+ error: C(b.error),
152
+ submitLabel: s("admin.desk.saveOutcome", "Save outcome and bill fee"),
153
+ onSubmit: (t) => b.mutate({
154
+ id: e.id,
155
+ input: t
156
+ })
157
+ })]
158
+ }),
159
+ O && /* @__PURE__ */ y("div", {
160
+ className: "mt-4 space-y-3",
161
+ children: [
162
+ e.notes && /* @__PURE__ */ v("p", {
163
+ className: "whitespace-pre-wrap rounded-lg bg-bg-sunken p-3 text-sm text-text-primary",
164
+ children: e.notes
165
+ }),
166
+ e.playbook && /* @__PURE__ */ v("pre", {
167
+ className: "whitespace-pre-wrap break-words rounded-lg bg-bg-sunken p-3 font-sans text-xs text-text-primary",
168
+ children: e.playbook
169
+ }),
170
+ /* @__PURE__ */ v(l, {
171
+ events: e.events ?? [],
172
+ customerLabel: s("admin.desk.customer", "Customer")
173
+ })
174
+ ]
175
+ })
176
+ ]
177
+ });
178
+ }, T = () => {
179
+ let t = i(), [n, r] = g(null), { data: a, isLoading: o, isError: c, error: l } = d(n), u = a?.items ?? [];
180
+ return /* @__PURE__ */ y("div", {
181
+ "data-testid": "negotiation-desk",
182
+ children: [/* @__PURE__ */ v("div", {
183
+ className: "mb-4 flex flex-wrap gap-1.5",
184
+ children: b.map((i) => /* @__PURE__ */ v("button", {
185
+ type: "button",
186
+ "aria-pressed": n === i,
187
+ onClick: () => r(i),
188
+ className: e("rounded-lg px-3 py-1.5 text-xs font-medium", n === i ? "bg-action-primary-bg text-action-primary-text" : "border border-border-subtle text-text-primary hover:bg-bg-sunken"),
189
+ children: i ? t(s[i].key, s[i].fallback) : t("admin.desk.all", "All")
190
+ }, i ?? "ALL"))
191
+ }), o ? /* @__PURE__ */ v("p", {
192
+ className: "text-sm text-text-secondary",
193
+ children: t("admin.desk.loading", "Loading queue…")
194
+ }) : c ? /* @__PURE__ */ v("p", {
195
+ role: "alert",
196
+ className: "text-sm text-status-error-text",
197
+ children: C(l)
198
+ }) : u.length === 0 ? /* @__PURE__ */ v("p", {
199
+ className: "text-sm text-text-secondary",
200
+ children: t("admin.desk.empty", "No managed negotiations in this view.")
201
+ }) : /* @__PURE__ */ v("ul", {
202
+ className: "space-y-3",
203
+ children: u.map((e) => /* @__PURE__ */ v(w, { negotiation: e }, e.id))
204
+ })]
205
+ });
206
+ };
207
+ //#endregion
208
+ export { T as NegotiationDeskPanel };
209
+
210
+ //# sourceMappingURL=NegotiationDeskPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NegotiationDeskPanel.js","names":[],"sources":["../../../src/subscriberbot/admin/NegotiationDeskPanel.tsx"],"sourcesContent":["/**\n * Negotiation desk (super admin, admin-app) — BOFF-4154.\n *\n * The queue of managed bill negotiations across all workspaces, oldest\n * submission first. A negotiator picks a request up, messages the customer\n * when information is missing, records the outcome (which bills the success\n * fee in global billing) and can retry or waive an unbilled fee.\n */\n\nimport { useState, type FC } from 'react';\nimport {\n NegotiationStatusPill,\n NegotiationTimeline,\n OutcomeForm,\n STATUS_LABEL,\n BILL_TYPE_LABEL,\n FEE_STATUS_LABEL,\n} from '../components/negotiations';\nimport {\n useDeskNegotiations,\n useDeskRecordOutcome,\n useDeskRetryFeeBilling,\n useDeskSetStatus,\n useDeskWaiveFee,\n} from '../hooks/useNegotiationApi';\nimport { useTr } from '../hooks/useTr';\nimport { cn } from '../utils/cn';\nimport { formatCurrency, formatDateTime, formatIntervalSuffix } from '../utils/formatters';\nimport type { SubNegotiation, SubNegotiationStatus } from '../types/negotiation';\n\nconst QUEUE_FILTERS: Array<SubNegotiationStatus | null> = [\n null,\n 'SUBMITTED',\n 'IN_PROGRESS',\n 'AWAITING_CUSTOMER',\n 'SUCCEEDED',\n 'NO_SAVINGS',\n 'CANCELLED',\n];\n\nconst inputCls =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst secondaryBtn =\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50';\n\nconst errorText = (err: unknown): string | null =>\n err ? (err instanceof Error ? err.message : String(err)) : null;\n\nconst DeskItem: FC<{ negotiation: SubNegotiation }> = ({ negotiation: n }) => {\n const tr = useTr();\n const setStatus = useDeskSetStatus();\n const record = useDeskRecordOutcome();\n const retry = useDeskRetryFeeBilling();\n const waive = useDeskWaiveFee();\n const [message, setMessage] = useState('');\n const [expanded, setExpanded] = useState(false);\n const working = ['SUBMITTED', 'IN_PROGRESS', 'AWAITING_CUSTOMER'].includes(n.status);\n const fee = FEE_STATUS_LABEL[n.feeStatus];\n const actionError =\n errorText(setStatus.error) ?? errorText(retry.error) ?? errorText(waive.error);\n\n return (\n <li\n className=\"rounded-xl border border-border-subtle bg-bg-surface p-4\"\n data-testid=\"desk-item\"\n >\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {n.providerName} ·{' '}\n {tr(BILL_TYPE_LABEL[n.billType].key, BILL_TYPE_LABEL[n.billType].fallback)}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatCurrency(n.currentAmount, n.currency)}\n {formatIntervalSuffix(n.interval)}\n {n.targetAmount != null &&\n ` → ${tr('admin.desk.target', 'target')} ${formatCurrency(n.targetAmount, n.currency)}`}\n {' · '}\n {n.accountHolderName ?? '—'}\n {n.accountHint ? ` ··${n.accountHint}` : ''}\n {n.submittedAt ? ` · ${formatDateTime(n.submittedAt)}` : ''}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {n.feeStatus !== 'NONE' && (\n <span className=\"text-xs text-text-secondary\">\n {formatCurrency(n.feeAmount, n.currency)} · {tr(fee.key, fee.fallback)}\n </span>\n )}\n <NegotiationStatusPill status={n.status} />\n </div>\n </div>\n\n <div className=\"mt-3 flex flex-wrap gap-2\">\n {(n.status === 'SUBMITTED' || n.status === 'AWAITING_CUSTOMER') && (\n <button\n type=\"button\"\n className={secondaryBtn}\n disabled={setStatus.isPending}\n onClick={() => setStatus.mutate({ id: n.id, status: 'IN_PROGRESS' })}\n >\n {n.status === 'SUBMITTED'\n ? tr('admin.desk.pickUp', 'Pick up')\n : tr('admin.desk.resume', 'Resume')}\n </button>\n )}\n {n.feeStatus === 'PENDING_BILLING' && (\n <>\n <button\n type=\"button\"\n className={secondaryBtn}\n disabled={retry.isPending}\n onClick={() => retry.mutate({ id: n.id })}\n >\n {tr('admin.desk.retryFee', 'Retry fee billing')}\n </button>\n <button\n type=\"button\"\n className={secondaryBtn}\n disabled={waive.isPending || !message.trim()}\n onClick={() => waive.mutate({ id: n.id, reason: message.trim() })}\n >\n {tr('admin.desk.waiveFee', 'Waive fee (reason below)')}\n </button>\n </>\n )}\n <button type=\"button\" className={secondaryBtn} onClick={() => setExpanded((v) => !v)}>\n {expanded\n ? tr('admin.desk.hide', 'Hide details')\n : tr('admin.desk.show', 'Playbook, notes and timeline')}\n </button>\n </div>\n\n {(working || n.feeStatus === 'PENDING_BILLING') && (\n <div className=\"mt-3 space-y-2\">\n <textarea\n rows={2}\n value={message}\n onChange={(e) => setMessage(e.target.value)}\n aria-label={tr('admin.desk.message', 'Message to the customer or reason')}\n placeholder={tr('admin.desk.messagePlaceholder', 'Message to the customer, or reason')}\n className={inputCls}\n />\n {working && (\n <div className=\"flex flex-wrap gap-2\">\n {n.status === 'IN_PROGRESS' && (\n <button\n type=\"button\"\n className={secondaryBtn}\n disabled={setStatus.isPending || !message.trim()}\n onClick={() =>\n setStatus.mutate(\n { id: n.id, status: 'AWAITING_CUSTOMER', message: message.trim() },\n { onSuccess: () => setMessage('') }\n )\n }\n >\n {tr('admin.desk.askCustomer', 'Ask the customer')}\n </button>\n )}\n <button\n type=\"button\"\n className={secondaryBtn}\n disabled={setStatus.isPending}\n onClick={() =>\n setStatus.mutate({\n id: n.id,\n status: 'CANCELLED',\n message: message.trim() || null,\n })\n }\n >\n {tr('admin.desk.close', 'Close request')}\n </button>\n </div>\n )}\n </div>\n )}\n {actionError && (\n <p role=\"alert\" className=\"mt-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {working && (\n <div className=\"mt-4 rounded-lg border border-border-subtle p-3\">\n <p className=\"mb-2 text-xs font-medium text-text-secondary\">\n {tr('admin.desk.recordOutcome', 'Record the outcome')}\n </p>\n <OutcomeForm\n negotiation={n}\n pending={record.isPending}\n error={errorText(record.error)}\n submitLabel={tr('admin.desk.saveOutcome', 'Save outcome and bill fee')}\n onSubmit={(input) => record.mutate({ id: n.id, input })}\n />\n </div>\n )}\n\n {expanded && (\n <div className=\"mt-4 space-y-3\">\n {n.notes && (\n <p className=\"whitespace-pre-wrap rounded-lg bg-bg-sunken p-3 text-sm text-text-primary\">\n {n.notes}\n </p>\n )}\n {n.playbook && (\n <pre className=\"whitespace-pre-wrap break-words rounded-lg bg-bg-sunken p-3 font-sans text-xs text-text-primary\">\n {n.playbook}\n </pre>\n )}\n <NegotiationTimeline\n events={n.events ?? []}\n customerLabel={tr('admin.desk.customer', 'Customer')}\n />\n </div>\n )}\n </li>\n );\n};\n\nexport const NegotiationDeskPanel: FC = () => {\n const tr = useTr();\n const [status, setStatus] = useState<SubNegotiationStatus | null>(null);\n const { data, isLoading, isError, error } = useDeskNegotiations(status);\n const items = data?.items ?? [];\n\n return (\n <div data-testid=\"negotiation-desk\">\n <div className=\"mb-4 flex flex-wrap gap-1.5\">\n {QUEUE_FILTERS.map((f) => (\n <button\n key={f ?? 'ALL'}\n type=\"button\"\n aria-pressed={status === f}\n onClick={() => setStatus(f)}\n className={cn(\n 'rounded-lg px-3 py-1.5 text-xs font-medium',\n status === 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 ? tr(STATUS_LABEL[f].key, STATUS_LABEL[f].fallback) : tr('admin.desk.all', 'All')}\n </button>\n ))}\n </div>\n {isLoading ? (\n <p className=\"text-sm text-text-secondary\">{tr('admin.desk.loading', 'Loading queue…')}</p>\n ) : isError ? (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {errorText(error)}\n </p>\n ) : items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('admin.desk.empty', 'No managed negotiations in this view.')}\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((n) => (\n <DeskItem key={n.id} negotiation={n} />\n ))}\n </ul>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;AA8BA,IAAM,IAAoD;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;AACF,GAEM,IACJ,wMACI,IACJ,mIAEI,KAAa,MACjB,IAAO,aAAe,QAAQ,EAAI,UAAU,OAAO,CAAG,IAAK,MAEvD,KAAiD,EAAE,aAAa,QAAQ;CAC5E,IAAM,IAAK,EAAM,GACX,IAAY,EAAiB,GAC7B,IAAS,EAAqB,GAC9B,IAAQ,EAAuB,GAC/B,IAAQ,EAAgB,GACxB,CAAC,GAAS,KAAc,EAAS,EAAE,GACnC,CAAC,GAAU,KAAe,EAAS,EAAK,GACxC,IAAU;EAAC;EAAa;EAAe;CAAmB,EAAE,SAAS,EAAE,MAAM,GAC7E,IAAM,EAAiB,EAAE,YACzB,IACJ,EAAU,EAAU,KAAK,KAAK,EAAU,EAAM,KAAK,KAAK,EAAU,EAAM,KAAK;CAE/E,OACE,kBAAC,MAAD;EACE,WAAU;EACV,eAAY;YAFd;GAIE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAE;OAAa;OAAG;OAClB,EAAG,EAAgB,EAAE,UAAU,KAAK,EAAgB,EAAE,UAAU,QAAQ;MACxE;SACH,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAe,EAAE,eAAe,EAAE,QAAQ;OAC1C,EAAqB,EAAE,QAAQ;OAC/B,EAAE,gBAAgB,QACjB,MAAM,EAAG,qBAAqB,QAAQ,EAAE,GAAG,EAAe,EAAE,cAAc,EAAE,QAAQ;OACrF;OACA,EAAE,qBAAqB;OACvB,EAAE,cAAc,MAAM,EAAE,gBAAgB;OACxC,EAAE,cAAc,MAAM,EAAe,EAAE,WAAW,MAAM;MACxD;OACA;QACL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAE,cAAc,UACf,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACG,EAAe,EAAE,WAAW,EAAE,QAAQ;OAAE;OAAI,EAAG,EAAI,KAAK,EAAI,QAAQ;MACjE;SAER,kBAAC,GAAD,EAAuB,QAAQ,EAAE,OAAS,CAAA,CACvC;MACF;;GAEL,kBAAC,OAAD;IAAK,WAAU;cAAf;MACI,EAAE,WAAW,eAAe,EAAE,WAAW,wBACzC,kBAAC,UAAD;MACE,MAAK;MACL,WAAW;MACX,UAAU,EAAU;MACpB,eAAe,EAAU,OAAO;OAAE,IAAI,EAAE;OAAI,QAAQ;MAAc,CAAC;gBAElE,EAAE,WAAW,cACV,EAAG,qBAAqB,SAAS,IACjC,EAAG,qBAAqB,QAAQ;KAC9B,CAAA;KAET,EAAE,cAAc,qBACf,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;MACE,MAAK;MACL,WAAW;MACX,UAAU,EAAM;MAChB,eAAe,EAAM,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC;gBAEvC,EAAG,uBAAuB,mBAAmB;KACxC,CAAA,GACR,kBAAC,UAAD;MACE,MAAK;MACL,WAAW;MACX,UAAU,EAAM,aAAa,CAAC,EAAQ,KAAK;MAC3C,eAAe,EAAM,OAAO;OAAE,IAAI,EAAE;OAAI,QAAQ,EAAQ,KAAK;MAAE,CAAC;gBAE/D,EAAG,uBAAuB,0BAA0B;KAC/C,CAAA,CACR,EAAA,CAAA;KAEJ,kBAAC,UAAD;MAAQ,MAAK;MAAS,WAAW;MAAc,eAAe,GAAa,MAAM,CAAC,CAAC;gBAChF,IACG,EAAG,mBAAmB,cAAc,IACpC,EAAG,mBAAmB,8BAA8B;KAClD,CAAA;IACL;;IAEH,KAAW,EAAE,cAAc,sBAC3B,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,YAAD;KACE,MAAM;KACN,OAAO;KACP,WAAW,MAAM,EAAW,EAAE,OAAO,KAAK;KAC1C,cAAY,EAAG,sBAAsB,mCAAmC;KACxE,aAAa,EAAG,iCAAiC,oCAAoC;KACrF,WAAW;IACZ,CAAA,GACA,KACC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAE,WAAW,iBACZ,kBAAC,UAAD;MACE,MAAK;MACL,WAAW;MACX,UAAU,EAAU,aAAa,CAAC,EAAQ,KAAK;MAC/C,eACE,EAAU,OACR;OAAE,IAAI,EAAE;OAAI,QAAQ;OAAqB,SAAS,EAAQ,KAAK;MAAE,GACjE,EAAE,iBAAiB,EAAW,EAAE,EAAE,CACpC;gBAGD,EAAG,0BAA0B,kBAAkB;KAC1C,CAAA,GAEV,kBAAC,UAAD;MACE,MAAK;MACL,WAAW;MACX,UAAU,EAAU;MACpB,eACE,EAAU,OAAO;OACf,IAAI,EAAE;OACN,QAAQ;OACR,SAAS,EAAQ,KAAK,KAAK;MAC7B,CAAC;gBAGF,EAAG,oBAAoB,eAAe;KACjC,CAAA,CACL;MAEJ;;GAEN,KACC,kBAAC,KAAD;IAAG,MAAK;IAAQ,WAAU;cACvB;GACA,CAAA;GAGJ,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,4BAA4B,oBAAoB;IACnD,CAAA,GACH,kBAAC,GAAD;KACE,aAAa;KACb,SAAS,EAAO;KAChB,OAAO,EAAU,EAAO,KAAK;KAC7B,aAAa,EAAG,0BAA0B,2BAA2B;KACrE,WAAW,MAAU,EAAO,OAAO;MAAE,IAAI,EAAE;MAAI;KAAM,CAAC;IACvD,CAAA,CACE;;GAGN,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,EAAE,SACD,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE;KACF,CAAA;KAEJ,EAAE,YACD,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAE;KACA,CAAA;KAEP,kBAAC,GAAD;MACE,QAAQ,EAAE,UAAU,CAAC;MACrB,eAAe,EAAG,uBAAuB,UAAU;KACpD,CAAA;IACE;;EAEL;;AAER,GAEa,UAAiC;CAC5C,IAAM,IAAK,EAAM,GACX,CAAC,GAAQ,KAAa,EAAsC,IAAI,GAChE,EAAE,SAAM,cAAW,YAAS,aAAU,EAAoB,CAAM,GAChE,IAAQ,GAAM,SAAS,CAAC;CAE9B,OACE,kBAAC,OAAD;EAAK,eAAY;YAAjB,CACE,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAc,KAAK,MAClB,kBAAC,UAAD;IAEE,MAAK;IACL,gBAAc,MAAW;IACzB,eAAe,EAAU,CAAC;IAC1B,WAAW,EACT,8CACA,MAAW,IACP,kDACA,kEACN;cAEC,IAAI,EAAG,EAAa,GAAG,KAAK,EAAa,GAAG,QAAQ,IAAI,EAAG,kBAAkB,KAAK;GAC7E,GAZD,KAAK,KAYJ,CACT;EACE,CAAA,GACJ,IACC,kBAAC,KAAD;GAAG,WAAU;aAA+B,EAAG,sBAAsB,gBAAgB;EAAK,CAAA,IACxF,IACF,kBAAC,KAAD;GAAG,MAAK;GAAQ,WAAU;aACvB,EAAU,CAAK;EACf,CAAA,IACD,EAAM,WAAW,IACnB,kBAAC,KAAD;GAAG,WAAU;aACV,EAAG,oBAAoB,uCAAuC;EAC9D,CAAA,IAEH,kBAAC,MAAD;GAAI,WAAU;aACX,EAAM,KAAK,MACV,kBAAC,GAAD,EAAqB,aAAa,EAAI,GAAvB,EAAE,EAAqB,CACvC;EACC,CAAA,CAEH;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/components/Modal.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA4B,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG1E,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,KAAK,EAAE,EAAE,CAAC,UAAU,CAgEhC,CAAC"}
1
+ {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/components/Modal.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA4B,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG1E,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,KAAK,EAAE,EAAE,CAAC,UAAU,CA2EhC,CAAC"}
@@ -3,15 +3,18 @@ import { X as r } from "lucide-react";
3
3
  import { jsx as i, jsxs as a } from "react/jsx-runtime";
4
4
  //#region src/subscriberbot/components/Modal.tsx
5
5
  var o = ({ title: o, description: s, onClose: c, children: l, maxWidthClassName: u = "max-w-lg" }) => {
6
- let d = t(), f = n(null);
6
+ let d = t(), f = n(null), p = n(c);
7
7
  return e(() => {
8
+ p.current = c;
9
+ }, [c]), e(() => {
8
10
  let e = (e) => {
9
- e.key === "Escape" && c();
11
+ e.key === "Escape" && p.current();
10
12
  };
11
- document.addEventListener("keydown", e);
12
- let t = f.current;
13
- return (t?.querySelector("input:not([type=\"hidden\"]), select, textarea") ?? t?.querySelector("button, [href], [tabindex]:not([tabindex=\"-1\"])"))?.focus(), () => document.removeEventListener("keydown", e);
14
- }, [c]), /* @__PURE__ */ i("div", {
13
+ return document.addEventListener("keydown", e), () => document.removeEventListener("keydown", e);
14
+ }, []), e(() => {
15
+ let e = f.current;
16
+ (e?.querySelector("input:not([type=\"hidden\"]), select, textarea") ?? e?.querySelector("button, [href], [tabindex]:not([tabindex=\"-1\"])"))?.focus();
17
+ }, []), /* @__PURE__ */ i("div", {
15
18
  className: "fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4 backdrop-blur-sm",
16
19
  onMouseDown: (e) => {
17
20
  e.target === e.currentTarget && c();
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.js","names":[],"sources":["../../../src/subscriberbot/components/Modal.tsx"],"sourcesContent":["/**\n * Accessible modal dialog\n * @module subscriberbot/components\n *\n * A small, dependency-light dialog used for the MFE's create/edit forms.\n * Handles the a11y contract every modal here must meet:\n * - `role=\"dialog\"` + `aria-modal=\"true\"` + `aria-labelledby` (wired to the\n * heading id),\n * - Escape-to-close,\n * - initial focus moved to the first form field,\n * - a semantic overlay scrim (`bg-bg-overlay`, never raw black),\n * - responsive layout (full-width on mobile, scrolls when the form is long).\n */\n\nimport { useEffect, useId, useRef, type FC, type ReactNode } from 'react';\nimport { X } from 'lucide-react';\n\ninterface ModalProps {\n title: string;\n description?: string;\n onClose: () => void;\n children: ReactNode;\n /** Constrain width. Defaults to a comfortable single-column form width. */\n maxWidthClassName?: string;\n}\n\nexport const Modal: FC<ModalProps> = ({\n title,\n description,\n onClose,\n children,\n maxWidthClassName = 'max-w-lg',\n}) => {\n const titleId = useId();\n const panelRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onClose();\n };\n document.addEventListener('keydown', onKeyDown);\n\n // Move initial focus to the first interactive form field (falling back to\n // any focusable control) so keyboard users land inside the dialog.\n const panel = panelRef.current;\n const firstField = panel?.querySelector<HTMLElement>(\n 'input:not([type=\"hidden\"]), select, textarea'\n );\n (\n firstField ??\n panel?.querySelector<HTMLElement>('button, [href], [tabindex]:not([tabindex=\"-1\"])')\n )?.focus();\n\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [onClose]);\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4 backdrop-blur-sm\"\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n <div\n ref={panelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n className={`max-h-[90vh] w-full ${maxWidthClassName} overflow-y-auto rounded-lg border border-border-subtle bg-bg-surface p-5 shadow-lg`}\n >\n <div className=\"mb-4 flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <h3 id={titleId} className=\"text-lg font-semibold text-text-primary\">\n {title}\n </h3>\n {description && <p className=\"mt-0.5 text-xs text-text-secondary\">{description}</p>}\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close dialog\"\n className=\"shrink-0 rounded-md p-1 text-text-secondary hover:text-text-primary\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n </div>\n {children}\n </div>\n </div>\n );\n};\n"],"mappings":";;;;AA0BA,IAAa,KAAyB,EACpC,UACA,gBACA,YACA,aACA,uBAAoB,iBAChB;CACJ,IAAM,IAAU,EAAM,GAChB,IAAW,EAAuB,IAAI;CAsB5C,OApBA,QAAgB;EACd,IAAM,KAAa,MAAqB;GACtC,AAAI,EAAE,QAAQ,YAAU,EAAQ;EAClC;EACA,SAAS,iBAAiB,WAAW,CAAS;EAI9C,IAAM,IAAQ,EAAS;EASvB,QARmB,GAAO,cACxB,gDACF,KAGE,GAAO,cAA2B,mDAAiD,IAClF,MAAM,SAEI,SAAS,oBAAoB,WAAW,CAAS;CAChE,GAAG,CAAC,CAAO,CAAC,GAGV,kBAAC,OAAD;EACE,WAAU;EACV,cAAc,MAAM;GAClB,AAAI,EAAE,WAAW,EAAE,iBAAe,EAAQ;EAC5C;YAEA,kBAAC,OAAD;GACE,KAAK;GACL,MAAK;GACL,cAAW;GACX,mBAAiB;GACjB,WAAW,uBAAuB,EAAkB;aALtD,CAOE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,IAAI;MAAS,WAAU;gBACxB;KACC,CAAA,GACH,KAAe,kBAAC,KAAD;MAAG,WAAU;gBAAsC;KAAe,CAAA,CAC/E;QACL,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;IAClB,CAAA,CACL;OACJ,CACE;;CACF,CAAA;AAET"}
1
+ {"version":3,"file":"Modal.js","names":[],"sources":["../../../src/subscriberbot/components/Modal.tsx"],"sourcesContent":["/**\n * Accessible modal dialog\n * @module subscriberbot/components\n *\n * A small, dependency-light dialog used for the MFE's create/edit forms.\n * Handles the a11y contract every modal here must meet:\n * - `role=\"dialog\"` + `aria-modal=\"true\"` + `aria-labelledby` (wired to the\n * heading id),\n * - Escape-to-close,\n * - initial focus moved to the first form field,\n * - a semantic overlay scrim (`bg-bg-overlay`, never raw black),\n * - responsive layout (full-width on mobile, scrolls when the form is long).\n */\n\nimport { useEffect, useId, useRef, type FC, type ReactNode } from 'react';\nimport { X } from 'lucide-react';\n\ninterface ModalProps {\n title: string;\n description?: string;\n onClose: () => void;\n children: ReactNode;\n /** Constrain width. Defaults to a comfortable single-column form width. */\n maxWidthClassName?: string;\n}\n\nexport const Modal: FC<ModalProps> = ({\n title,\n description,\n onClose,\n children,\n maxWidthClassName = 'max-w-lg',\n}) => {\n const titleId = useId();\n const panelRef = useRef<HTMLDivElement>(null);\n\n // Callers usually pass an inline `onClose`, which is a new function on every\n // parent render. Keep the latest one in a ref so neither effect below re-runs\n // because of it: re-running the focus effect would yank focus back to the\n // first field on every keystroke whenever the form state lives in the parent.\n const onCloseRef = useRef(onClose);\n useEffect(() => {\n onCloseRef.current = onClose;\n }, [onClose]);\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onCloseRef.current();\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, []);\n\n useEffect(() => {\n // Move initial focus to the first interactive form field (falling back to\n // any focusable control) so keyboard users land inside the dialog. This\n // runs once, when the dialog opens.\n const panel = panelRef.current;\n const firstField = panel?.querySelector<HTMLElement>(\n 'input:not([type=\"hidden\"]), select, textarea'\n );\n (\n firstField ??\n panel?.querySelector<HTMLElement>('button, [href], [tabindex]:not([tabindex=\"-1\"])')\n )?.focus();\n }, []);\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4 backdrop-blur-sm\"\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n <div\n ref={panelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n className={`max-h-[90vh] w-full ${maxWidthClassName} overflow-y-auto rounded-lg border border-border-subtle bg-bg-surface p-5 shadow-lg`}\n >\n <div className=\"mb-4 flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <h3 id={titleId} className=\"text-lg font-semibold text-text-primary\">\n {title}\n </h3>\n {description && <p className=\"mt-0.5 text-xs text-text-secondary\">{description}</p>}\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close dialog\"\n className=\"shrink-0 rounded-md p-1 text-text-secondary hover:text-text-primary\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n </div>\n {children}\n </div>\n </div>\n );\n};\n"],"mappings":";;;;AA0BA,IAAa,KAAyB,EACpC,UACA,gBACA,YACA,aACA,uBAAoB,iBAChB;CACJ,IAAM,IAAU,EAAM,GAChB,IAAW,EAAuB,IAAI,GAMtC,IAAa,EAAO,CAAO;CA2BjC,OA1BA,QAAgB;EACd,EAAW,UAAU;CACvB,GAAG,CAAC,CAAO,CAAC,GAEZ,QAAgB;EACd,IAAM,KAAa,MAAqB;GACtC,AAAI,EAAE,QAAQ,YAAU,EAAW,QAAQ;EAC7C;EAEA,OADA,SAAS,iBAAiB,WAAW,CAAS,SACjC,SAAS,oBAAoB,WAAW,CAAS;CAChE,GAAG,CAAC,CAAC,GAEL,QAAgB;EAId,IAAM,IAAQ,EAAS;EAIvB,CAHmB,GAAO,cACxB,gDACF,KAGE,GAAO,cAA2B,mDAAiD,IAClF,MAAM;CACX,GAAG,CAAC,CAAC,GAGH,kBAAC,OAAD;EACE,WAAU;EACV,cAAc,MAAM;GAClB,AAAI,EAAE,WAAW,EAAE,iBAAe,EAAQ;EAC5C;YAEA,kBAAC,OAAD;GACE,KAAK;GACL,MAAK;GACL,cAAW;GACX,mBAAiB;GACjB,WAAW,uBAAuB,EAAkB;aALtD,CAOE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,IAAI;MAAS,WAAU;gBACxB;KACC,CAAA,GACH,KAAe,kBAAC,KAAD;MAAG,WAAU;gBAAsC;KAAe,CAAA,CAC/E;QACL,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;IAClB,CAAA,CACL;OACJ,CACE;;CACF,CAAA;AAET"}
@@ -0,0 +1,8 @@
1
+ import { FC } from 'react';
2
+ import { SubNegotiationStatus } from '../../types/negotiation';
3
+
4
+ export declare const NegotiationStatusPill: FC<{
5
+ status: SubNegotiationStatus;
6
+ className?: string;
7
+ }>;
8
+ //# sourceMappingURL=NegotiationStatusPill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NegotiationStatusPill.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/negotiations/NegotiationStatusPill.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAGhC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAGpE,eAAO,MAAM,qBAAqB,EAAE,EAAE,CAAC;IAAE,MAAM,EAAE,oBAAoB,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAmB1F,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { useTr as t } from "../../hooks/useTr.js";
3
+ import { STATUS_LABEL as n, statusTone as r } from "./labels.js";
4
+ import { jsx as i } from "react/jsx-runtime";
5
+ //#region src/subscriberbot/components/negotiations/NegotiationStatusPill.tsx
6
+ var a = ({ status: a, className: o }) => {
7
+ let s = t(), c = n[a];
8
+ return /* @__PURE__ */ i("span", {
9
+ "data-testid": "negotiation-status",
10
+ "data-status": a,
11
+ className: e("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium", r(a), o),
12
+ children: s(c.key, c.fallback)
13
+ });
14
+ };
15
+ //#endregion
16
+ export { a as NegotiationStatusPill };
17
+
18
+ //# sourceMappingURL=NegotiationStatusPill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NegotiationStatusPill.js","names":[],"sources":["../../../../src/subscriberbot/components/negotiations/NegotiationStatusPill.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useTr } from '../../hooks/useTr';\nimport { cn } from '../../utils/cn';\nimport type { SubNegotiationStatus } from '../../types/negotiation';\nimport { STATUS_LABEL, statusTone } from './labels';\n\nexport const NegotiationStatusPill: FC<{ status: SubNegotiationStatus; className?: string }> = ({\n status,\n className,\n}) => {\n const tr = useTr();\n const label = STATUS_LABEL[status];\n return (\n <span\n data-testid=\"negotiation-status\"\n data-status={status}\n className={cn(\n 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',\n statusTone(status),\n className\n )}\n >\n {tr(label.key, label.fallback)}\n </span>\n );\n};\n"],"mappings":";;;;;AAMA,IAAa,KAAmF,EAC9F,WACA,mBACI;CACJ,IAAM,IAAK,EAAM,GACX,IAAQ,EAAa;CAC3B,OACE,kBAAC,QAAD;EACE,eAAY;EACZ,eAAa;EACb,WAAW,EACT,2EACA,EAAW,CAAM,GACjB,CACF;YAEC,EAAG,EAAM,KAAK,EAAM,QAAQ;CACzB,CAAA;AAEV"}
@@ -0,0 +1,8 @@
1
+ import { FC } from 'react';
2
+ import { SubNegotiationEvent } from '../../types/negotiation';
3
+
4
+ export declare const NegotiationTimeline: FC<{
5
+ events: SubNegotiationEvent[];
6
+ customerLabel?: string;
7
+ }>;
8
+ //# sourceMappingURL=NegotiationTimeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NegotiationTimeline.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/negotiations/NegotiationTimeline.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAGhC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AASnE,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC;IAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAqC7F,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { formatDateTime as e } from "../../utils/formatters.js";
2
+ import { useTr as t } from "../../hooks/useTr.js";
3
+ import { EVENT_LABEL as n, STATUS_LABEL as r } from "./labels.js";
4
+ import { jsx as i, jsxs as a } from "react/jsx-runtime";
5
+ //#region src/subscriberbot/components/negotiations/NegotiationTimeline.tsx
6
+ var o = {
7
+ customer: {
8
+ key: "negotiations.role.customer",
9
+ fallback: "You"
10
+ },
11
+ negotiator: {
12
+ key: "negotiations.role.negotiator",
13
+ fallback: "Negotiation desk"
14
+ },
15
+ system: {
16
+ key: "negotiations.role.system",
17
+ fallback: "Subscriber Bot"
18
+ }
19
+ }, s = ({ events: s, customerLabel: c }) => {
20
+ let l = t();
21
+ return s.length === 0 ? null : /* @__PURE__ */ i("ol", {
22
+ className: "space-y-3",
23
+ "data-testid": "negotiation-timeline",
24
+ children: s.map((t) => {
25
+ let s = o[t.actorRole] ?? o.system, u = t.actorRole === "customer" && c ? c : l(s.key, s.fallback), d = n[t.kind], f = t.toStatus ? r[t.toStatus] : null;
26
+ return /* @__PURE__ */ a("li", {
27
+ className: "rounded-lg border border-border-subtle bg-bg-surface p-3",
28
+ children: [/* @__PURE__ */ a("div", {
29
+ className: "flex flex-wrap items-center justify-between gap-2 text-xs text-text-secondary",
30
+ children: [/* @__PURE__ */ a("span", {
31
+ className: "font-medium text-text-primary",
32
+ children: [l(d.key, d.fallback), f && t.kind !== "CREATED" ? ` → ${l(f.key, f.fallback)}` : ""]
33
+ }), /* @__PURE__ */ a("span", { children: [
34
+ u,
35
+ " · ",
36
+ e(t.createdAt)
37
+ ] })]
38
+ }), t.message && /* @__PURE__ */ i("p", {
39
+ className: "mt-1.5 whitespace-pre-wrap text-sm text-text-primary",
40
+ children: t.message
41
+ })]
42
+ }, t.id);
43
+ })
44
+ });
45
+ };
46
+ //#endregion
47
+ export { s as NegotiationTimeline };
48
+
49
+ //# sourceMappingURL=NegotiationTimeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NegotiationTimeline.js","names":[],"sources":["../../../../src/subscriberbot/components/negotiations/NegotiationTimeline.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useTr } from '../../hooks/useTr';\nimport { formatDateTime } from '../../utils/formatters';\nimport type { SubNegotiationEvent } from '../../types/negotiation';\nimport { EVENT_LABEL, STATUS_LABEL } from './labels';\n\nconst ROLE_LABEL: Record<string, { key: string; fallback: string }> = {\n customer: { key: 'negotiations.role.customer', fallback: 'You' },\n negotiator: { key: 'negotiations.role.negotiator', fallback: 'Negotiation desk' },\n system: { key: 'negotiations.role.system', fallback: 'Subscriber Bot' },\n};\n\nexport const NegotiationTimeline: FC<{ events: SubNegotiationEvent[]; customerLabel?: string }> = ({\n events,\n customerLabel,\n}) => {\n const tr = useTr();\n if (events.length === 0) return null;\n return (\n <ol className=\"space-y-3\" data-testid=\"negotiation-timeline\">\n {events.map((event) => {\n const role = ROLE_LABEL[event.actorRole] ?? ROLE_LABEL.system!;\n const who =\n event.actorRole === 'customer' && customerLabel\n ? customerLabel\n : tr(role.key, role.fallback);\n const label = EVENT_LABEL[event.kind];\n const to = event.toStatus ? STATUS_LABEL[event.toStatus] : null;\n return (\n <li key={event.id} className=\"rounded-lg border border-border-subtle bg-bg-surface p-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-2 text-xs text-text-secondary\">\n <span className=\"font-medium text-text-primary\">\n {tr(label.key, label.fallback)}\n {to && event.kind !== 'CREATED' ? ` → ${tr(to.key, to.fallback)}` : ''}\n </span>\n <span>\n {who} · {formatDateTime(event.createdAt)}\n </span>\n </div>\n {event.message && (\n <p className=\"mt-1.5 whitespace-pre-wrap text-sm text-text-primary\">\n {event.message}\n </p>\n )}\n </li>\n );\n })}\n </ol>\n );\n};\n"],"mappings":";;;;;AAMA,IAAM,IAAgE;CACpE,UAAU;EAAE,KAAK;EAA8B,UAAU;CAAM;CAC/D,YAAY;EAAE,KAAK;EAAgC,UAAU;CAAmB;CAChF,QAAQ;EAAE,KAAK;EAA4B,UAAU;CAAiB;AACxE,GAEa,KAAsF,EACjG,WACA,uBACI;CACJ,IAAM,IAAK,EAAM;CAEjB,OADI,EAAO,WAAW,IAAU,OAE9B,kBAAC,MAAD;EAAI,WAAU;EAAY,eAAY;YACnC,EAAO,KAAK,MAAU;GACrB,IAAM,IAAO,EAAW,EAAM,cAAc,EAAW,QACjD,IACJ,EAAM,cAAc,cAAc,IAC9B,IACA,EAAG,EAAK,KAAK,EAAK,QAAQ,GAC1B,IAAQ,EAAY,EAAM,OAC1B,IAAK,EAAM,WAAW,EAAa,EAAM,YAAY;GAC3D,OACE,kBAAC,MAAD;IAAmB,WAAU;cAA7B,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,EAAG,EAAM,KAAK,EAAM,QAAQ,GAC5B,KAAM,EAAM,SAAS,YAAY,MAAM,EAAG,EAAG,KAAK,EAAG,QAAQ,MAAM,EAChE;SACN,kBAAC,QAAD,EAAA,UAAA;MACG;MAAI;MAAI,EAAe,EAAM,SAAS;KACnC,EAAA,CAAA,CACH;QACJ,EAAM,WACL,kBAAC,KAAD;KAAG,WAAU;eACV,EAAM;IACN,CAAA,CAEH;MAfK,EAAM,EAeX;EAER,CAAC;CACC,CAAA;AAER"}
@@ -0,0 +1,13 @@
1
+ import { FC } from 'react';
2
+ import { SubNegotiation, SubRecordNegotiationOutcomeInput } from '../../types/negotiation';
3
+
4
+ interface OutcomeFormProps {
5
+ negotiation: SubNegotiation;
6
+ pending: boolean;
7
+ error?: string | null;
8
+ submitLabel: string;
9
+ onSubmit: (input: SubRecordNegotiationOutcomeInput) => void;
10
+ }
11
+ export declare const OutcomeForm: FC<OutcomeFormProps>;
12
+ export {};
13
+ //# sourceMappingURL=OutcomeForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OutcomeForm.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/negotiations/OutcomeForm.tsx"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAY,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;AAG1D,OAAO,KAAK,EACV,cAAc,EAEd,gCAAgC,EACjC,MAAM,yBAAyB,CAAC;AAMjC,UAAU,gBAAgB;IACxB,WAAW,EAAE,cAAc,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,CAAC;CAC7D;AAED,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CAoH5C,CAAC"}
@@ -0,0 +1,112 @@
1
+ import { formatCurrency as e } from "../../utils/formatters.js";
2
+ import { useTr as t } from "../../hooks/useTr.js";
3
+ import { useState as n } from "react";
4
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
5
+ //#region src/subscriberbot/components/negotiations/OutcomeForm.tsx
6
+ var a = "w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]", o = "mb-1 block text-xs font-medium text-text-secondary", s = ({ negotiation: s, pending: c, error: l, submitLabel: u, onSubmit: d }) => {
7
+ let f = t(), [p, m] = n("SUCCEEDED"), [h, g] = n(""), [_, v] = n(""), [y, b] = n(""), [x, S] = n(!!s.relationshipId);
8
+ return /* @__PURE__ */ i("form", {
9
+ onSubmit: (e) => {
10
+ e.preventDefault(), d({
11
+ outcome: p,
12
+ ...p === "SUCCEEDED" && h ? { newAmount: Number(h) } : {},
13
+ ...p === "SUCCEEDED" && _ ? { oneTimeCredit: Number(_) } : {},
14
+ ...y.trim() ? { summary: y.trim() } : {},
15
+ ...p === "SUCCEEDED" && s.relationshipId ? { applyToRelationship: x } : {}
16
+ });
17
+ },
18
+ className: "space-y-3",
19
+ "data-testid": "negotiation-outcome-form",
20
+ children: [
21
+ /* @__PURE__ */ i("fieldset", {
22
+ className: "flex flex-wrap gap-4 text-sm text-text-primary",
23
+ children: [
24
+ /* @__PURE__ */ r("legend", {
25
+ className: o,
26
+ children: f("negotiations.outcome.result", "Result")
27
+ }),
28
+ /* @__PURE__ */ i("label", {
29
+ className: "inline-flex items-center gap-2",
30
+ children: [/* @__PURE__ */ r("input", {
31
+ type: "radio",
32
+ name: "outcome",
33
+ checked: p === "SUCCEEDED",
34
+ onChange: () => m("SUCCEEDED")
35
+ }), f("negotiations.outcome.succeeded", "The bill was lowered or credited")]
36
+ }),
37
+ /* @__PURE__ */ i("label", {
38
+ className: "inline-flex items-center gap-2",
39
+ children: [/* @__PURE__ */ r("input", {
40
+ type: "radio",
41
+ name: "outcome",
42
+ checked: p === "NO_SAVINGS",
43
+ onChange: () => m("NO_SAVINGS")
44
+ }), f("negotiations.outcome.noSavings", "No savings offered")]
45
+ })
46
+ ]
47
+ }),
48
+ p === "SUCCEEDED" && /* @__PURE__ */ i("div", {
49
+ className: "grid grid-cols-1 gap-3 sm:grid-cols-2",
50
+ children: [/* @__PURE__ */ i("div", { children: [/* @__PURE__ */ r("label", {
51
+ htmlFor: "outcome-new-amount",
52
+ className: o,
53
+ children: f("negotiations.outcome.newAmount", "New price ({{current}} today)", { current: e(s.currentAmount, s.currency) })
54
+ }), /* @__PURE__ */ r("input", {
55
+ id: "outcome-new-amount",
56
+ type: "number",
57
+ min: "0",
58
+ step: "0.01",
59
+ value: h,
60
+ onChange: (e) => g(e.target.value),
61
+ className: a
62
+ })] }), /* @__PURE__ */ i("div", { children: [/* @__PURE__ */ r("label", {
63
+ htmlFor: "outcome-credit",
64
+ className: o,
65
+ children: f("negotiations.outcome.credit", "One-off credit or refund")
66
+ }), /* @__PURE__ */ r("input", {
67
+ id: "outcome-credit",
68
+ type: "number",
69
+ min: "0",
70
+ step: "0.01",
71
+ value: _,
72
+ onChange: (e) => v(e.target.value),
73
+ className: a
74
+ })] })]
75
+ }),
76
+ /* @__PURE__ */ i("div", { children: [/* @__PURE__ */ r("label", {
77
+ htmlFor: "outcome-summary",
78
+ className: o,
79
+ children: f("negotiations.outcome.summary", "What was agreed")
80
+ }), /* @__PURE__ */ r("textarea", {
81
+ id: "outcome-summary",
82
+ rows: 2,
83
+ value: y,
84
+ onChange: (e) => b(e.target.value),
85
+ className: a
86
+ })] }),
87
+ p === "SUCCEEDED" && s.relationshipId && /* @__PURE__ */ i("label", {
88
+ className: "inline-flex items-center gap-2 text-sm text-text-primary",
89
+ children: [/* @__PURE__ */ r("input", {
90
+ type: "checkbox",
91
+ checked: x,
92
+ onChange: (e) => S(e.target.checked)
93
+ }), f("negotiations.outcome.apply", "Update the tracked subscription with the new price")]
94
+ }),
95
+ l && /* @__PURE__ */ r("p", {
96
+ role: "alert",
97
+ className: "text-sm text-status-error-text",
98
+ children: l
99
+ }),
100
+ /* @__PURE__ */ r("button", {
101
+ type: "submit",
102
+ disabled: c,
103
+ className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50",
104
+ children: u
105
+ })
106
+ ]
107
+ });
108
+ };
109
+ //#endregion
110
+ export { s as OutcomeForm };
111
+
112
+ //# sourceMappingURL=OutcomeForm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OutcomeForm.js","names":[],"sources":["../../../../src/subscriberbot/components/negotiations/OutcomeForm.tsx"],"sourcesContent":["/**\n * Outcome form shared by the customer (self-serve) and the negotiation desk.\n */\nimport { useState, type FC, type FormEvent } from 'react';\nimport { useTr } from '../../hooks/useTr';\nimport { formatCurrency } from '../../utils/formatters';\nimport type {\n SubNegotiation,\n SubNegotiationOutcome,\n SubRecordNegotiationOutcomeInput,\n} from '../../types/negotiation';\n\nconst inputCls =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 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 block text-xs font-medium text-text-secondary';\n\ninterface OutcomeFormProps {\n negotiation: SubNegotiation;\n pending: boolean;\n error?: string | null;\n submitLabel: string;\n onSubmit: (input: SubRecordNegotiationOutcomeInput) => void;\n}\n\nexport const OutcomeForm: FC<OutcomeFormProps> = ({\n negotiation,\n pending,\n error,\n submitLabel,\n onSubmit,\n}) => {\n const tr = useTr();\n const [outcome, setOutcome] = useState<SubNegotiationOutcome>('SUCCEEDED');\n const [newAmount, setNewAmount] = useState('');\n const [credit, setCredit] = useState('');\n const [summary, setSummary] = useState('');\n const [apply, setApply] = useState(Boolean(negotiation.relationshipId));\n\n const handleSubmit = (e: FormEvent) => {\n e.preventDefault();\n onSubmit({\n outcome,\n ...(outcome === 'SUCCEEDED' && newAmount ? { newAmount: Number(newAmount) } : {}),\n ...(outcome === 'SUCCEEDED' && credit ? { oneTimeCredit: Number(credit) } : {}),\n ...(summary.trim() ? { summary: summary.trim() } : {}),\n ...(outcome === 'SUCCEEDED' && negotiation.relationshipId\n ? { applyToRelationship: apply }\n : {}),\n });\n };\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-3\" data-testid=\"negotiation-outcome-form\">\n <fieldset className=\"flex flex-wrap gap-4 text-sm text-text-primary\">\n <legend className={labelCls}>{tr('negotiations.outcome.result', 'Result')}</legend>\n <label className=\"inline-flex items-center gap-2\">\n <input\n type=\"radio\"\n name=\"outcome\"\n checked={outcome === 'SUCCEEDED'}\n onChange={() => setOutcome('SUCCEEDED')}\n />\n {tr('negotiations.outcome.succeeded', 'The bill was lowered or credited')}\n </label>\n <label className=\"inline-flex items-center gap-2\">\n <input\n type=\"radio\"\n name=\"outcome\"\n checked={outcome === 'NO_SAVINGS'}\n onChange={() => setOutcome('NO_SAVINGS')}\n />\n {tr('negotiations.outcome.noSavings', 'No savings offered')}\n </label>\n </fieldset>\n {outcome === 'SUCCEEDED' && (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"outcome-new-amount\" className={labelCls}>\n {tr('negotiations.outcome.newAmount', 'New price ({{current}} today)', {\n current: formatCurrency(negotiation.currentAmount, negotiation.currency),\n })}\n </label>\n <input\n id=\"outcome-new-amount\"\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={newAmount}\n onChange={(e) => setNewAmount(e.target.value)}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"outcome-credit\" className={labelCls}>\n {tr('negotiations.outcome.credit', 'One-off credit or refund')}\n </label>\n <input\n id=\"outcome-credit\"\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={credit}\n onChange={(e) => setCredit(e.target.value)}\n className={inputCls}\n />\n </div>\n </div>\n )}\n <div>\n <label htmlFor=\"outcome-summary\" className={labelCls}>\n {tr('negotiations.outcome.summary', 'What was agreed')}\n </label>\n <textarea\n id=\"outcome-summary\"\n rows={2}\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n className={inputCls}\n />\n </div>\n {outcome === 'SUCCEEDED' && negotiation.relationshipId && (\n <label className=\"inline-flex items-center gap-2 text-sm text-text-primary\">\n <input type=\"checkbox\" checked={apply} onChange={(e) => setApply(e.target.checked)} />\n {tr('negotiations.outcome.apply', 'Update the tracked subscription with the new price')}\n </label>\n )}\n {error && (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {error}\n </p>\n )}\n <button\n type=\"submit\"\n disabled={pending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {submitLabel}\n </button>\n </form>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export * from './labels';
2
+ export { NegotiationStatusPill } from './NegotiationStatusPill';
3
+ export { NegotiationTimeline } from './NegotiationTimeline';
4
+ export { OutcomeForm } from './OutcomeForm';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/negotiations/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,4 @@
1
+ import "./labels.js";
2
+ import "./NegotiationStatusPill.js";
3
+ import "./NegotiationTimeline.js";
4
+ import "./OutcomeForm.js";
@@ -0,0 +1,16 @@
1
+ import { SubNegotiationBillType, SubNegotiationEventKind, SubNegotiationFeeStatus, SubNegotiationStatus, SubNegotiationTarget } from '../../types/negotiation';
2
+
3
+ export type Label = {
4
+ key: string;
5
+ fallback: string;
6
+ };
7
+ export declare const BILL_TYPES: SubNegotiationBillType[];
8
+ export declare const BILL_TYPE_LABEL: Record<SubNegotiationBillType, Label>;
9
+ export declare const TARGETS: SubNegotiationTarget[];
10
+ export declare const TARGET_LABEL: Record<SubNegotiationTarget, Label>;
11
+ export declare const STATUS_LABEL: Record<SubNegotiationStatus, Label>;
12
+ export declare const FEE_STATUS_LABEL: Record<SubNegotiationFeeStatus, Label>;
13
+ export declare const EVENT_LABEL: Record<SubNegotiationEventKind, Label>;
14
+ export declare function statusTone(status: SubNegotiationStatus): string;
15
+ export declare const OPEN_STATUSES: SubNegotiationStatus[];
16
+ //# sourceMappingURL=labels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/negotiations/labels.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EACV,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,MAAM,KAAK,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,eAAO,MAAM,UAAU,EAAE,sBAAsB,EAS9C,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,sBAAsB,EAAE,KAAK,CASjE,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,oBAAoB,EAMzC,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,oBAAoB,EAAE,KAAK,CAY5D,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,oBAAoB,EAAE,KAAK,CAQ5D,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,uBAAuB,EAAE,KAAK,CAKnE,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,uBAAuB,EAAE,KAAK,CAc9D,CAAC;AAYF,wBAAgB,UAAU,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAE/D;AAED,eAAO,MAAM,aAAa,EAAE,oBAAoB,EAK/C,CAAC"}