@burdenoff/microfe-subscriberbot 2026.706.1 → 2026.706.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/subscriberbot/SubscriberBotRoutes.js +14 -6
- package/dist/subscriberbot/SubscriberBotRoutes.js.map +1 -1
- package/dist/subscriberbot/components/Modal.js +51 -0
- package/dist/subscriberbot/components/Modal.js.map +1 -0
- package/dist/subscriberbot/components/index.js +7 -0
- package/dist/subscriberbot/components/viz.js +196 -0
- package/dist/subscriberbot/components/viz.js.map +1 -0
- package/dist/subscriberbot/hooks/useSubscriberBotApi.js +364 -161
- package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
- package/dist/subscriberbot/pages/AssetVaultPage.js +182 -0
- package/dist/subscriberbot/pages/AssetVaultPage.js.map +1 -0
- package/dist/subscriberbot/pages/DashboardPage.js +272 -199
- package/dist/subscriberbot/pages/DashboardPage.js.map +1 -1
- package/dist/subscriberbot/pages/InboxPage.js +682 -126
- package/dist/subscriberbot/pages/InboxPage.js.map +1 -1
- package/dist/subscriberbot/pages/InsightsPage.js +220 -0
- package/dist/subscriberbot/pages/InsightsPage.js.map +1 -0
- package/dist/subscriberbot/pages/InvoicesPage.js +592 -81
- package/dist/subscriberbot/pages/InvoicesPage.js.map +1 -1
- package/dist/subscriberbot/pages/WorkflowsPage.js +344 -169
- package/dist/subscriberbot/pages/WorkflowsPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentDetailPage.js +401 -116
- package/dist/subscriberbot/pages/agents/AgentDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentsListPage.js +185 -93
- package/dist/subscriberbot/pages/agents/AgentsListPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.js +735 -135
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderNewPage.js +217 -153
- package/dist/subscriberbot/pages/providers/ProviderNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProvidersListPage.js +216 -111
- package/dist/subscriberbot/pages/providers/ProvidersListPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +902 -250
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js +242 -192
- package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js +174 -131
- package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,13 +5,15 @@ import { formatCurrency as r, formatDate as i, formatEnumLabel as a, formatInter
|
|
|
5
5
|
import { StatusBadge as s } from "../../components/StatusBadge.js";
|
|
6
6
|
import { EmptyState as c } from "../../components/EmptyState.js";
|
|
7
7
|
import { ErrorView as l, LoadingView as u } from "../../components/StateViews.js";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
8
|
+
import { MiniProgress as d } from "../../components/viz.js";
|
|
9
|
+
import "../../components/index.js";
|
|
10
|
+
import { useRelationships as f } from "../../hooks/useSubscriberBotApi.js";
|
|
11
|
+
import { useMemo as p, useState as m } from "react";
|
|
12
|
+
import { ChevronRight as h, Plus as g, Repeat2 as _, Search as v } from "lucide-react";
|
|
13
|
+
import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
14
|
+
import { GlassCard as S, PagePurpose as C } from "@burdenoff/fe-libs/ui";
|
|
13
15
|
//#region src/subscriberbot/pages/relationships/RelationshipsListPage.tsx
|
|
14
|
-
var
|
|
16
|
+
var w = 20, T = [
|
|
15
17
|
{
|
|
16
18
|
value: "",
|
|
17
19
|
label: "All statuses"
|
|
@@ -44,7 +46,7 @@ var S = 20, C = [
|
|
|
44
46
|
value: "CANCELLED",
|
|
45
47
|
label: "Cancelled"
|
|
46
48
|
}
|
|
47
|
-
],
|
|
49
|
+
], E = [
|
|
48
50
|
{
|
|
49
51
|
value: "",
|
|
50
52
|
label: "All kinds"
|
|
@@ -81,160 +83,201 @@ var S = 20, C = [
|
|
|
81
83
|
value: "TRIAL",
|
|
82
84
|
label: "Trial"
|
|
83
85
|
}
|
|
84
|
-
];
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
], D = "rounded-lg border border-border-subtle bg-bg-surface px-3 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]";
|
|
87
|
+
function O(e) {
|
|
88
|
+
return e == null ? "accent" : e >= 66 ? "positive" : e >= 33 ? "warning" : "negative";
|
|
89
|
+
}
|
|
90
|
+
function k() {
|
|
91
|
+
let { navigateTo: k } = e(), [A, j] = m(0), [M, N] = m(""), [P, F] = m(""), [I, L] = m(""), [R, z] = m(""), { data: B, isLoading: V, isError: H, refetch: U, isFetching: W } = f({
|
|
92
|
+
status: M || void 0,
|
|
93
|
+
kind: P || void 0,
|
|
94
|
+
search: I || void 0,
|
|
89
95
|
pagination: {
|
|
90
|
-
limit:
|
|
91
|
-
offset:
|
|
96
|
+
limit: w,
|
|
97
|
+
offset: A * w
|
|
92
98
|
}
|
|
93
|
-
}),
|
|
94
|
-
|
|
95
|
-
let e = M.toLowerCase();
|
|
96
|
-
return z.filter((t) => t.label.toLowerCase().includes(e) || (t.provider?.name?.toLowerCase().includes(e) ?? !1));
|
|
97
|
-
}, [z, M]);
|
|
98
|
-
return /* @__PURE__ */ y(n, {
|
|
99
|
+
}), G = p(() => B?.items ?? [], [B]), K = B?.pageInfo.hasMore ?? !1;
|
|
100
|
+
return /* @__PURE__ */ x(n, {
|
|
99
101
|
title: "Relationships",
|
|
100
102
|
description: "Every subscription, membership, license and contract you track",
|
|
101
|
-
actions: /* @__PURE__ */
|
|
103
|
+
actions: /* @__PURE__ */ x("button", {
|
|
102
104
|
type: "button",
|
|
103
|
-
onClick: () =>
|
|
105
|
+
onClick: () => k("/relationships/new"),
|
|
104
106
|
className: "inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90",
|
|
105
|
-
children: [/* @__PURE__ */
|
|
107
|
+
children: [/* @__PURE__ */ b(g, { className: "h-4 w-4" }), " Add relationship"]
|
|
106
108
|
}),
|
|
107
109
|
children: [
|
|
108
|
-
/* @__PURE__ */
|
|
110
|
+
/* @__PURE__ */ b(C, {
|
|
109
111
|
className: "mb-4",
|
|
110
112
|
children: "This is the fundamental object of Subscriber Bot. Filter by lifecycle and kind, open any row to manage its plan, payment, entitlements, assets and renewals, or advance its lifecycle."
|
|
111
113
|
}),
|
|
112
|
-
/* @__PURE__ */
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
children:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
114
|
+
/* @__PURE__ */ b(S, {
|
|
115
|
+
treatment: "plain",
|
|
116
|
+
className: "p-3 sm:p-4",
|
|
117
|
+
children: /* @__PURE__ */ x("div", {
|
|
118
|
+
className: "flex flex-col gap-3 md:flex-row md:items-center",
|
|
119
|
+
children: [/* @__PURE__ */ x("form", {
|
|
120
|
+
onSubmit: (e) => {
|
|
121
|
+
e.preventDefault(), L(R.trim()), j(0);
|
|
122
|
+
},
|
|
123
|
+
className: "relative flex-1 min-w-0",
|
|
124
|
+
children: [/* @__PURE__ */ b(v, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ b("input", {
|
|
125
|
+
type: "text",
|
|
126
|
+
"aria-label": "Search relationships by label",
|
|
127
|
+
placeholder: "Search relationships by label…",
|
|
128
|
+
value: R,
|
|
129
|
+
onChange: (e) => z(e.target.value),
|
|
130
|
+
className: "w-full rounded-lg border border-border-subtle bg-bg-surface py-2.5 pl-10 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
|
|
131
|
+
})]
|
|
132
|
+
}), /* @__PURE__ */ x("div", {
|
|
133
|
+
className: "flex flex-wrap gap-2 sm:flex-nowrap",
|
|
134
|
+
children: [/* @__PURE__ */ b("select", {
|
|
135
|
+
"aria-label": "Filter by kind",
|
|
136
|
+
value: P,
|
|
137
|
+
onChange: (e) => {
|
|
138
|
+
F(e.target.value), j(0);
|
|
139
|
+
},
|
|
140
|
+
className: t(D, "flex-1 sm:flex-none"),
|
|
141
|
+
children: E.map((e) => /* @__PURE__ */ b("option", {
|
|
142
|
+
value: e.value,
|
|
143
|
+
children: e.label
|
|
144
|
+
}, e.value))
|
|
145
|
+
}), /* @__PURE__ */ b("select", {
|
|
146
|
+
"aria-label": "Filter by status",
|
|
147
|
+
value: M,
|
|
148
|
+
onChange: (e) => {
|
|
149
|
+
N(e.target.value), j(0);
|
|
150
|
+
},
|
|
151
|
+
className: t(D, "flex-1 sm:flex-none"),
|
|
152
|
+
children: T.map((e) => /* @__PURE__ */ b("option", {
|
|
153
|
+
value: e.value,
|
|
154
|
+
children: e.label
|
|
155
|
+
}, e.value))
|
|
156
|
+
})]
|
|
157
|
+
})]
|
|
158
|
+
})
|
|
145
159
|
}),
|
|
146
|
-
|
|
160
|
+
V ? /* @__PURE__ */ b(u, { label: "Loading relationships…" }) : H ? /* @__PURE__ */ b(l, {
|
|
147
161
|
message: "Could not load relationships.",
|
|
148
|
-
onRetry: () => void
|
|
149
|
-
}) :
|
|
162
|
+
onRetry: () => void U()
|
|
163
|
+
}) : G.length === 0 ? /* @__PURE__ */ b("div", {
|
|
150
164
|
className: "mt-6",
|
|
151
|
-
children: /* @__PURE__ */
|
|
165
|
+
children: /* @__PURE__ */ b(c, {
|
|
152
166
|
illustration: "empty-data",
|
|
153
|
-
icon: /* @__PURE__ */
|
|
167
|
+
icon: /* @__PURE__ */ b(_, { className: "h-12 w-12" }),
|
|
154
168
|
title: "No relationships found",
|
|
155
|
-
description:
|
|
156
|
-
action: /* @__PURE__ */
|
|
169
|
+
description: I ? `No relationships match "${I}".` : "Add your first recurring relationship to start tracking it.",
|
|
170
|
+
action: /* @__PURE__ */ b("button", {
|
|
157
171
|
type: "button",
|
|
158
|
-
onClick: () =>
|
|
172
|
+
onClick: () => k("/relationships/new"),
|
|
159
173
|
className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90",
|
|
160
174
|
children: "Add relationship"
|
|
161
175
|
})
|
|
162
176
|
})
|
|
163
|
-
}) : /* @__PURE__ */ y
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
/* @__PURE__ */
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
className: "
|
|
194
|
-
children:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
177
|
+
}) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(S, {
|
|
178
|
+
treatment: "plain",
|
|
179
|
+
className: "mt-4 overflow-hidden p-0",
|
|
180
|
+
children: /* @__PURE__ */ b("div", {
|
|
181
|
+
className: "overflow-x-auto",
|
|
182
|
+
children: /* @__PURE__ */ x("div", {
|
|
183
|
+
className: "min-w-[720px]",
|
|
184
|
+
children: [/* @__PURE__ */ x("div", {
|
|
185
|
+
className: "grid grid-cols-[minmax(0,2.4fr)_1fr_1fr_1fr_1fr_1.3fr] gap-4 border-b border-border-seam bg-bg-sunken/60 px-4 py-2.5 text-xs font-medium uppercase tracking-wide text-text-secondary",
|
|
186
|
+
children: [
|
|
187
|
+
/* @__PURE__ */ b("span", { children: "Subscription" }),
|
|
188
|
+
/* @__PURE__ */ b("span", { children: "Kind" }),
|
|
189
|
+
/* @__PURE__ */ b("span", { children: "Status" }),
|
|
190
|
+
/* @__PURE__ */ b("span", {
|
|
191
|
+
className: "text-right",
|
|
192
|
+
children: "Amount"
|
|
193
|
+
}),
|
|
194
|
+
/* @__PURE__ */ b("span", {
|
|
195
|
+
className: "text-right",
|
|
196
|
+
children: "Renews on"
|
|
197
|
+
}),
|
|
198
|
+
/* @__PURE__ */ b("span", { children: "Health" })
|
|
199
|
+
]
|
|
200
|
+
}), /* @__PURE__ */ b("div", {
|
|
201
|
+
className: "divide-y divide-border-subtle",
|
|
202
|
+
children: G.map((e) => {
|
|
203
|
+
let t = e.healthScore;
|
|
204
|
+
return /* @__PURE__ */ x("button", {
|
|
205
|
+
type: "button",
|
|
206
|
+
onClick: () => k(`/relationships/${e.id}`),
|
|
207
|
+
className: "group grid w-full grid-cols-[minmax(0,2.4fr)_1fr_1fr_1fr_1fr_1.3fr] items-center gap-4 px-4 py-3.5 text-left transition-colors hover:bg-bg-sunken",
|
|
208
|
+
children: [
|
|
209
|
+
/* @__PURE__ */ b("div", {
|
|
210
|
+
className: "flex min-w-0 items-center gap-2",
|
|
211
|
+
children: /* @__PURE__ */ x("div", {
|
|
212
|
+
className: "min-w-0",
|
|
213
|
+
children: [/* @__PURE__ */ b("p", {
|
|
214
|
+
className: "truncate text-sm font-semibold text-text-primary",
|
|
215
|
+
children: e.label
|
|
216
|
+
}), e.provider?.name && /* @__PURE__ */ b("p", {
|
|
217
|
+
className: "truncate text-xs text-text-secondary",
|
|
218
|
+
children: e.provider.name
|
|
219
|
+
})]
|
|
220
|
+
})
|
|
221
|
+
}),
|
|
222
|
+
/* @__PURE__ */ b("span", { children: /* @__PURE__ */ b("span", {
|
|
223
|
+
className: "inline-flex items-center rounded-md bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary",
|
|
224
|
+
children: a(e.kind)
|
|
225
|
+
}) }),
|
|
226
|
+
/* @__PURE__ */ b("span", { children: /* @__PURE__ */ b(s, { status: e.status }) }),
|
|
227
|
+
/* @__PURE__ */ x("span", {
|
|
228
|
+
className: "text-right text-sm font-semibold tabular-nums text-text-primary",
|
|
229
|
+
children: [r(e.amount, e.currency), /* @__PURE__ */ b("span", {
|
|
230
|
+
className: "text-xs font-normal text-text-secondary",
|
|
231
|
+
children: o(e.interval)
|
|
232
|
+
})]
|
|
233
|
+
}),
|
|
234
|
+
/* @__PURE__ */ b("span", {
|
|
235
|
+
className: "text-right text-xs text-text-secondary",
|
|
236
|
+
children: e.renewsAt ? i(e.renewsAt) : "—"
|
|
237
|
+
}),
|
|
238
|
+
/* @__PURE__ */ x("span", {
|
|
239
|
+
className: "flex items-center gap-2",
|
|
240
|
+
children: [t == null ? /* @__PURE__ */ b("span", {
|
|
241
|
+
className: "text-xs text-text-secondary",
|
|
242
|
+
children: "—"
|
|
243
|
+
}) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(d, {
|
|
244
|
+
value: t,
|
|
245
|
+
max: 100,
|
|
246
|
+
tone: O(t),
|
|
247
|
+
className: "w-16 shrink-0 sm:w-20"
|
|
248
|
+
}), /* @__PURE__ */ b("span", {
|
|
249
|
+
className: "text-xs tabular-nums text-text-secondary",
|
|
250
|
+
children: t
|
|
251
|
+
})] }), /* @__PURE__ */ b(h, { className: "ml-auto h-4 w-4 shrink-0 text-text-secondary opacity-0 transition-opacity group-hover:opacity-100" })]
|
|
252
|
+
})
|
|
253
|
+
]
|
|
254
|
+
}, e.id);
|
|
212
255
|
})
|
|
213
|
-
]
|
|
214
|
-
}
|
|
215
|
-
})
|
|
216
|
-
}), /* @__PURE__ */
|
|
256
|
+
})]
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
}), /* @__PURE__ */ x("div", {
|
|
217
260
|
className: "mt-4 flex items-center justify-between",
|
|
218
|
-
children: [/* @__PURE__ */
|
|
261
|
+
children: [/* @__PURE__ */ x("p", {
|
|
219
262
|
className: "text-xs text-text-secondary",
|
|
220
263
|
children: [
|
|
221
264
|
"Page ",
|
|
222
|
-
|
|
223
|
-
|
|
265
|
+
A + 1,
|
|
266
|
+
W ? " · refreshing…" : ""
|
|
224
267
|
]
|
|
225
|
-
}), /* @__PURE__ */
|
|
268
|
+
}), /* @__PURE__ */ x("div", {
|
|
226
269
|
className: "flex gap-2",
|
|
227
|
-
children: [/* @__PURE__ */
|
|
270
|
+
children: [/* @__PURE__ */ b("button", {
|
|
228
271
|
type: "button",
|
|
229
|
-
disabled:
|
|
230
|
-
onClick: () =>
|
|
231
|
-
className: t("rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium",
|
|
272
|
+
disabled: A === 0,
|
|
273
|
+
onClick: () => j((e) => Math.max(0, e - 1)),
|
|
274
|
+
className: t("rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium", A === 0 ? "cursor-not-allowed text-text-secondary opacity-50" : "text-text-primary hover:bg-bg-sunken"),
|
|
232
275
|
children: "Previous"
|
|
233
|
-
}), /* @__PURE__ */
|
|
276
|
+
}), /* @__PURE__ */ b("button", {
|
|
234
277
|
type: "button",
|
|
235
|
-
disabled: !
|
|
236
|
-
onClick: () =>
|
|
237
|
-
className: t("rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium",
|
|
278
|
+
disabled: !K,
|
|
279
|
+
onClick: () => j((e) => e + 1),
|
|
280
|
+
className: t("rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium", K ? "text-text-primary hover:bg-bg-sunken" : "cursor-not-allowed text-text-secondary opacity-50"),
|
|
238
281
|
children: "Next"
|
|
239
282
|
})]
|
|
240
283
|
})]
|
|
@@ -243,6 +286,6 @@ function T() {
|
|
|
243
286
|
});
|
|
244
287
|
}
|
|
245
288
|
//#endregion
|
|
246
|
-
export {
|
|
289
|
+
export { k as RelationshipsListPage };
|
|
247
290
|
|
|
248
291
|
//# sourceMappingURL=RelationshipsListPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelationshipsListPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/relationships/RelationshipsListPage.tsx"],"sourcesContent":["/**\n * Relationships list\n * @module subscriberbot/pages/relationships\n *\n * The core CRUDL list — every recurring relationship (subscription /\n * membership / license / contract) in one paginated, filterable view.\n * Data comes from `subListRelationships` (a SubRelationshipConnection).\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Search, Repeat2 } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useRelationships } from '../../hooks/useSubscriberBotApi';\nimport { cn } from '../../utils/cn';\nimport {\n formatCurrency,\n formatDate,\n formatEnumLabel,\n formatIntervalSuffix,\n} from '../../utils/formatters';\nimport type { SubRelationshipKind, SubRelationshipStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\nconst STATUS_OPTIONS: Array<{ value: '' | SubRelationshipStatus; label: string }> = [\n { value: '', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'TRIAL', label: 'Trial' },\n { value: 'PAUSED', label: 'Paused' },\n { value: 'PAST_DUE', label: 'Past due' },\n { value: 'LAPSED', label: 'Lapsed' },\n { value: 'EXPIRED', label: 'Expired' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nconst KIND_OPTIONS: Array<{ value: '' | SubRelationshipKind; label: string }> = [\n { value: '', label: 'All kinds' },\n { value: 'SUBSCRIPTION', label: 'Subscription' },\n { value: 'MEMBERSHIP', label: 'Membership' },\n { value: 'LICENSE', label: 'License' },\n { value: 'CONTRACT', label: 'Contract' },\n { value: 'NEWSLETTER', label: 'Newsletter' },\n { value: 'RECURRING_PAYMENT', label: 'Recurring payment' },\n { value: 'ENROLLMENT', label: 'Enrollment' },\n { value: 'TRIAL', label: 'Trial' },\n];\n\nexport function RelationshipsListPage() {\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [statusFilter, setStatusFilter] = useState<'' | SubRelationshipStatus>('');\n const [kindFilter, setKindFilter] = useState<'' | SubRelationshipKind>('');\n const [search, setSearch] = useState('');\n\n const { data, isLoading, isError, refetch, isFetching } = useRelationships({\n status: statusFilter || undefined,\n kind: kindFilter || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = useMemo(() => data?.items ?? [], [data]);\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n // Client-side label search over the current page (server has no search arg).\n const visible = useMemo(() => {\n if (!search.trim()) return items;\n const q = search.toLowerCase();\n return items.filter(\n (r) =>\n r.label.toLowerCase().includes(q) || (r.provider?.name?.toLowerCase().includes(q) ?? false)\n );\n }, [items, search]);\n\n return (\n <PageLayout\n title=\"Relationships\"\n description=\"Every subscription, membership, license and contract you track\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> Add relationship\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n This is the fundamental object of Subscriber Bot. Filter by lifecycle and kind, open any row\n to manage its plan, payment, entitlements, assets and renewals, or advance its lifecycle.\n </PagePurpose>\n\n <EmphasisPanel>\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n placeholder=\"Search relationships on this page…\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2.5 pl-10 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </EmphasisPanel>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-3\">\n <select\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as '' | SubRelationshipStatus);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {STATUS_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <select\n value={kindFilter}\n onChange={(e) => {\n setKindFilter(e.target.value as '' | SubRelationshipKind);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {KIND_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading relationships…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load relationships.\" onRetry={() => void refetch()} />\n ) : visible.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Repeat2 className=\"h-12 w-12\" />}\n title=\"No relationships found\"\n description={\n search\n ? `No matches for \"${search}\" on this page.`\n : 'Add your first recurring relationship to start tracking it.'\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n Add relationship\n </button>\n }\n />\n </div>\n ) : (\n <>\n <div className=\"mt-6 overflow-hidden rounded-lg border border-border-subtle bg-bg-surface\">\n <div className=\"hidden grid-cols-[2fr_1fr_1fr_1fr_1fr] gap-4 border-b border-border-subtle bg-bg-sunken px-4 py-2.5 text-xs font-medium text-text-secondary sm:grid\">\n <span>Relationship</span>\n <span>Kind</span>\n <span>Status</span>\n <span className=\"text-right\">Amount</span>\n <span className=\"text-right\">Renews</span>\n </div>\n <div className=\"divide-y divide-border-subtle\">\n {visible.map((r) => (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => navigateTo(`/relationships/${r.id}`)}\n className=\"grid w-full grid-cols-1 gap-2 px-4 py-3 text-left hover:bg-bg-sunken sm:grid-cols-[2fr_1fr_1fr_1fr_1fr] sm:items-center sm:gap-4\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{r.label}</p>\n {r.provider?.name && (\n <p className=\"truncate text-xs text-text-secondary\">{r.provider.name}</p>\n )}\n </div>\n <span className=\"text-xs text-text-secondary\">{formatEnumLabel(r.kind)}</span>\n <span>\n <StatusBadge status={r.status} />\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary sm:text-right\">\n {formatCurrency(r.amount, r.currency)}\n <span className=\"text-xs font-normal text-text-secondary\">\n {formatIntervalSuffix(r.interval)}\n </span>\n </span>\n <span className=\"text-xs text-text-secondary sm:text-right\">\n {r.renewsAt ? formatDate(r.renewsAt) : '—'}\n </span>\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n Page {page + 1}\n {isFetching ? ' · refreshing…' : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => Math.max(0, p - 1))}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n page === 0\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n Previous\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setPage((p) => p + 1)}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n !hasMore\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n Next\n </button>\n </div>\n </div>\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RelationshipsListPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/relationships/RelationshipsListPage.tsx"],"sourcesContent":["/**\n * Relationships list\n * @module subscriberbot/pages/relationships\n *\n * The core CRUDL list — every recurring relationship (subscription /\n * membership / license / contract) in one paginated, filterable view.\n * Data comes from `subListRelationships` (a SubRelationshipConnection).\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Search, Repeat2, ChevronRight } from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { MiniProgress } from '../../components';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useRelationships } from '../../hooks/useSubscriberBotApi';\nimport { cn } from '../../utils/cn';\nimport {\n formatCurrency,\n formatDate,\n formatEnumLabel,\n formatIntervalSuffix,\n} from '../../utils/formatters';\nimport type { SubRelationshipKind, SubRelationshipStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\nconst STATUS_OPTIONS: Array<{ value: '' | SubRelationshipStatus; label: string }> = [\n { value: '', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'TRIAL', label: 'Trial' },\n { value: 'PAUSED', label: 'Paused' },\n { value: 'PAST_DUE', label: 'Past due' },\n { value: 'LAPSED', label: 'Lapsed' },\n { value: 'EXPIRED', label: 'Expired' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nconst KIND_OPTIONS: Array<{ value: '' | SubRelationshipKind; label: string }> = [\n { value: '', label: 'All kinds' },\n { value: 'SUBSCRIPTION', label: 'Subscription' },\n { value: 'MEMBERSHIP', label: 'Membership' },\n { value: 'LICENSE', label: 'License' },\n { value: 'CONTRACT', label: 'Contract' },\n { value: 'NEWSLETTER', label: 'Newsletter' },\n { value: 'RECURRING_PAYMENT', label: 'Recurring payment' },\n { value: 'ENROLLMENT', label: 'Enrollment' },\n { value: 'TRIAL', label: 'Trial' },\n];\n\nconst selectCls =\n 'rounded-lg border border-border-subtle bg-bg-surface px-3 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n\n/** Map a 0–100 health score onto a MiniProgress tone. */\nfunction healthTone(score: number | null | undefined): 'positive' | 'accent' | 'warning' | 'negative' {\n if (score == null) return 'accent';\n if (score >= 66) return 'positive';\n if (score >= 33) return 'warning';\n return 'negative';\n}\n\nexport function RelationshipsListPage() {\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [statusFilter, setStatusFilter] = useState<'' | SubRelationshipStatus>('');\n const [kindFilter, setKindFilter] = useState<'' | SubRelationshipKind>('');\n const [search, setSearch] = useState('');\n const [searchInput, setSearchInput] = useState('');\n\n const { data, isLoading, isError, refetch, isFetching } = useRelationships({\n status: statusFilter || undefined,\n kind: kindFilter || undefined,\n search: search || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = useMemo(() => data?.items ?? [], [data]);\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n const submitSearch = (e: React.FormEvent) => {\n e.preventDefault();\n setSearch(searchInput.trim());\n setPage(0);\n };\n\n return (\n <PageLayout\n title=\"Relationships\"\n description=\"Every subscription, membership, license and contract you track\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> Add relationship\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n This is the fundamental object of Subscriber Bot. Filter by lifecycle and kind, open any row\n to manage its plan, payment, entitlements, assets and renewals, or advance its lifecycle.\n </PagePurpose>\n\n {/* Toolbar — search + filters, responsive */}\n <GlassCard treatment=\"plain\" className=\"p-3 sm:p-4\">\n <div className=\"flex flex-col gap-3 md:flex-row md:items-center\">\n <form onSubmit={submitSearch} className=\"relative flex-1 min-w-0\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n aria-label=\"Search relationships by label\"\n placeholder=\"Search relationships by label…\"\n value={searchInput}\n onChange={(e) => setSearchInput(e.target.value)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2.5 pl-10 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </form>\n <div className=\"flex flex-wrap gap-2 sm:flex-nowrap\">\n <select\n aria-label=\"Filter by kind\"\n value={kindFilter}\n onChange={(e) => {\n setKindFilter(e.target.value as '' | SubRelationshipKind);\n setPage(0);\n }}\n className={cn(selectCls, 'flex-1 sm:flex-none')}\n >\n {KIND_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <select\n aria-label=\"Filter by status\"\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as '' | SubRelationshipStatus);\n setPage(0);\n }}\n className={cn(selectCls, 'flex-1 sm:flex-none')}\n >\n {STATUS_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n </GlassCard>\n\n {isLoading ? (\n <LoadingView label=\"Loading relationships…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load relationships.\" onRetry={() => void refetch()} />\n ) : items.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Repeat2 className=\"h-12 w-12\" />}\n title=\"No relationships found\"\n description={\n search\n ? `No relationships match \"${search}\".`\n : 'Add your first recurring relationship to start tracking it.'\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n Add relationship\n </button>\n }\n />\n </div>\n ) : (\n <>\n {/* Portfolio table — scrolls horizontally on small screens, never the page */}\n <GlassCard treatment=\"plain\" className=\"mt-4 overflow-hidden p-0\">\n <div className=\"overflow-x-auto\">\n <div className=\"min-w-[720px]\">\n <div className=\"grid grid-cols-[minmax(0,2.4fr)_1fr_1fr_1fr_1fr_1.3fr] gap-4 border-b border-border-seam bg-bg-sunken/60 px-4 py-2.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n <span>Subscription</span>\n <span>Kind</span>\n <span>Status</span>\n <span className=\"text-right\">Amount</span>\n <span className=\"text-right\">Renews on</span>\n <span>Health</span>\n </div>\n <div className=\"divide-y divide-border-subtle\">\n {items.map((r) => {\n const score = r.healthScore;\n return (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => navigateTo(`/relationships/${r.id}`)}\n className=\"group grid w-full grid-cols-[minmax(0,2.4fr)_1fr_1fr_1fr_1fr_1.3fr] items-center gap-4 px-4 py-3.5 text-left transition-colors hover:bg-bg-sunken\"\n >\n <div className=\"flex min-w-0 items-center gap-2\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {r.label}\n </p>\n {r.provider?.name && (\n <p className=\"truncate text-xs text-text-secondary\">\n {r.provider.name}\n </p>\n )}\n </div>\n </div>\n <span>\n <span className=\"inline-flex items-center rounded-md bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n {formatEnumLabel(r.kind)}\n </span>\n </span>\n <span>\n <StatusBadge status={r.status} />\n </span>\n <span className=\"text-right text-sm font-semibold tabular-nums text-text-primary\">\n {formatCurrency(r.amount, r.currency)}\n <span className=\"text-xs font-normal text-text-secondary\">\n {formatIntervalSuffix(r.interval)}\n </span>\n </span>\n <span className=\"text-right text-xs text-text-secondary\">\n {r.renewsAt ? formatDate(r.renewsAt) : '—'}\n </span>\n <span className=\"flex items-center gap-2\">\n {score != null ? (\n <>\n <MiniProgress\n value={score}\n max={100}\n tone={healthTone(score)}\n className=\"w-16 shrink-0 sm:w-20\"\n />\n <span className=\"text-xs tabular-nums text-text-secondary\">{score}</span>\n </>\n ) : (\n <span className=\"text-xs text-text-secondary\">—</span>\n )}\n <ChevronRight className=\"ml-auto h-4 w-4 shrink-0 text-text-secondary opacity-0 transition-opacity group-hover:opacity-100\" />\n </span>\n </button>\n );\n })}\n </div>\n </div>\n </div>\n </GlassCard>\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n Page {page + 1}\n {isFetching ? ' · refreshing…' : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => Math.max(0, p - 1))}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n page === 0\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n Previous\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setPage((p) => p + 1)}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n !hasMore\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n Next\n </button>\n </div>\n </div>\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|