@burdenoff/microfe-subscriberbot 2026.801.1 → 2026.829.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 (67) hide show
  1. package/dist/subscriberbot/admin/AdminDashboardPage.d.ts.map +1 -1
  2. package/dist/subscriberbot/admin/AdminDashboardPage.js +80 -83
  3. package/dist/subscriberbot/admin/AdminDashboardPage.js.map +1 -1
  4. package/dist/subscriberbot/components/viz.js +51 -27
  5. package/dist/subscriberbot/components/viz.js.map +1 -1
  6. package/dist/subscriberbot/hooks/index.d.ts +2 -1
  7. package/dist/subscriberbot/hooks/index.d.ts.map +1 -1
  8. package/dist/subscriberbot/hooks/index.js +1 -0
  9. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +12 -1
  10. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
  11. package/dist/subscriberbot/hooks/useSubscriberBotApi.js +82 -56
  12. package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
  13. package/dist/subscriberbot/hooks/useTr.d.ts +17 -0
  14. package/dist/subscriberbot/hooks/useTr.d.ts.map +1 -0
  15. package/dist/subscriberbot/hooks/useTr.js +14 -0
  16. package/dist/subscriberbot/hooks/useTr.js.map +1 -0
  17. package/dist/subscriberbot/pages/AssetVaultPage.d.ts.map +1 -1
  18. package/dist/subscriberbot/pages/AssetVaultPage.js +155 -97
  19. package/dist/subscriberbot/pages/AssetVaultPage.js.map +1 -1
  20. package/dist/subscriberbot/pages/DashboardPage.d.ts.map +1 -1
  21. package/dist/subscriberbot/pages/DashboardPage.js +113 -105
  22. package/dist/subscriberbot/pages/DashboardPage.js.map +1 -1
  23. package/dist/subscriberbot/pages/InboxPage.d.ts.map +1 -1
  24. package/dist/subscriberbot/pages/InboxPage.js +404 -369
  25. package/dist/subscriberbot/pages/InboxPage.js.map +1 -1
  26. package/dist/subscriberbot/pages/InsightsPage.d.ts.map +1 -1
  27. package/dist/subscriberbot/pages/InsightsPage.js +94 -93
  28. package/dist/subscriberbot/pages/InsightsPage.js.map +1 -1
  29. package/dist/subscriberbot/pages/InvoicesPage.d.ts +3 -2
  30. package/dist/subscriberbot/pages/InvoicesPage.d.ts.map +1 -1
  31. package/dist/subscriberbot/pages/InvoicesPage.js +368 -362
  32. package/dist/subscriberbot/pages/InvoicesPage.js.map +1 -1
  33. package/dist/subscriberbot/pages/WorkflowsPage.d.ts.map +1 -1
  34. package/dist/subscriberbot/pages/WorkflowsPage.js +209 -193
  35. package/dist/subscriberbot/pages/WorkflowsPage.js.map +1 -1
  36. package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts +3 -1
  37. package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts.map +1 -1
  38. package/dist/subscriberbot/pages/agents/AgentDetailPage.js +296 -228
  39. package/dist/subscriberbot/pages/agents/AgentDetailPage.js.map +1 -1
  40. package/dist/subscriberbot/pages/agents/AgentNewPage.d.ts.map +1 -1
  41. package/dist/subscriberbot/pages/agents/AgentNewPage.js +96 -76
  42. package/dist/subscriberbot/pages/agents/AgentNewPage.js.map +1 -1
  43. package/dist/subscriberbot/pages/agents/AgentsListPage.d.ts.map +1 -1
  44. package/dist/subscriberbot/pages/agents/AgentsListPage.js +145 -161
  45. package/dist/subscriberbot/pages/agents/AgentsListPage.js.map +1 -1
  46. package/dist/subscriberbot/pages/providers/ProviderDetailPage.d.ts +4 -2
  47. package/dist/subscriberbot/pages/providers/ProviderDetailPage.d.ts.map +1 -1
  48. package/dist/subscriberbot/pages/providers/ProviderDetailPage.js +577 -402
  49. package/dist/subscriberbot/pages/providers/ProviderDetailPage.js.map +1 -1
  50. package/dist/subscriberbot/pages/providers/ProviderNewPage.d.ts.map +1 -1
  51. package/dist/subscriberbot/pages/providers/ProviderNewPage.js +167 -129
  52. package/dist/subscriberbot/pages/providers/ProviderNewPage.js.map +1 -1
  53. package/dist/subscriberbot/pages/providers/ProvidersListPage.d.ts.map +1 -1
  54. package/dist/subscriberbot/pages/providers/ProvidersListPage.js +171 -218
  55. package/dist/subscriberbot/pages/providers/ProvidersListPage.js.map +1 -1
  56. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
  57. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +340 -279
  58. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
  59. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.d.ts.map +1 -1
  60. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js +185 -166
  61. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js.map +1 -1
  62. package/dist/subscriberbot/pages/relationships/RelationshipsListPage.d.ts.map +1 -1
  63. package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js +153 -182
  64. package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js.map +1 -1
  65. package/dist/subscriberbot/types/index.d.ts +37 -0
  66. package/dist/subscriberbot/types/index.d.ts.map +1 -1
  67. package/package.json +2 -2
@@ -6,21 +6,22 @@ import { EmptyState as a } from "../components/EmptyState.js";
6
6
  import { ErrorView as o, LoadingView as s, Section as c } from "../components/StateViews.js";
7
7
  import "../components/index.js";
8
8
  import { useAllAssets as l, useFileDownloadUrl as u, useRelationships as d } from "../hooks/useSubscriberBotApi.js";
9
- import { useMemo as f, useState as p } from "react";
10
- import { Award as m, Download as h, File as g, FileKey as _, FileText as v, Globe as y, Receipt as b, Search as x, ShieldCheck as S, Vault as C } from "lucide-react";
11
- import { jsx as w, jsxs as T } from "react/jsx-runtime";
12
- import { GlassCard as E, PagePurpose as D } from "@burdenoff/fe-libs/ui";
9
+ import { useTr as f } from "../hooks/useTr.js";
10
+ import { useMemo as p, useState as m } from "react";
11
+ import { AlarmClock as h, Award as g, Download as _, File as v, FileKey as y, FileText as b, Globe as x, Receipt as S, Search as C, ShieldCheck as w, Vault as T } from "lucide-react";
12
+ import { jsx as E, jsxs as D } from "react/jsx-runtime";
13
+ import { GlassCard as O, PagePurpose as k } from "@burdenoff/fe-libs/ui";
13
14
  //#region src/subscriberbot/pages/AssetVaultPage.tsx
14
- var O = {
15
- CONTRACT: v,
16
- LICENSE_KEY: _,
17
- CREDENTIAL: _,
18
- CERTIFICATE: m,
19
- INVOICE: b,
20
- RECEIPT: b,
21
- WARRANTY: S,
22
- DOMAIN: y
23
- }, k = [
15
+ var A = {
16
+ CONTRACT: b,
17
+ LICENSE_KEY: y,
18
+ CREDENTIAL: y,
19
+ CERTIFICATE: g,
20
+ INVOICE: S,
21
+ RECEIPT: S,
22
+ WARRANTY: w,
23
+ DOMAIN: x
24
+ }, j = [
24
25
  "CONTRACT",
25
26
  "LICENSE_KEY",
26
27
  "CREDENTIAL",
@@ -29,154 +30,211 @@ var O = {
29
30
  "RECEIPT",
30
31
  "WARRANTY",
31
32
  "DOMAIN"
32
- ], A = (e) => {
33
+ ], M = (e) => {
33
34
  if (!e) return !1;
34
35
  let t = new Date(e).getTime();
35
36
  if (Number.isNaN(t)) return !1;
36
37
  let n = (t - Date.now()) / 864e5;
37
38
  return n >= 0 && n <= 30;
39
+ }, N = (e) => {
40
+ if (!e) return Infinity;
41
+ let t = new Date(e).getTime();
42
+ return Number.isNaN(t) ? Infinity : t;
38
43
  };
39
- function j({ asset: t, subscription: n }) {
40
- let a = u(), [o, s] = p(!1), c = O[t.type] ?? g, l = A(t.expiresAt);
41
- return /* @__PURE__ */ T(E, {
44
+ function P({ asset: t, subscription: n }) {
45
+ let a = f(), o = u(), [s, c] = m(!1), l = A[t.type] ?? v, d = M(t.expiresAt);
46
+ return /* @__PURE__ */ D(O, {
42
47
  treatment: "metric",
43
48
  className: "flex items-start gap-3 p-4",
44
49
  children: [
45
- /* @__PURE__ */ w("span", {
50
+ /* @__PURE__ */ E("span", {
46
51
  className: "grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-accent-wash text-text-link",
47
- children: /* @__PURE__ */ w(c, { className: "h-5 w-5" })
52
+ children: /* @__PURE__ */ E(l, { className: "h-5 w-5" })
48
53
  }),
49
- /* @__PURE__ */ T("div", {
54
+ /* @__PURE__ */ D("div", {
50
55
  className: "min-w-0 flex-1",
51
56
  children: [
52
- /* @__PURE__ */ w("p", {
57
+ /* @__PURE__ */ E("p", {
53
58
  className: "truncate font-medium text-text-primary",
54
59
  children: t.name
55
60
  }),
56
- /* @__PURE__ */ w("p", {
61
+ /* @__PURE__ */ E("p", {
57
62
  className: "truncate text-xs text-text-secondary",
58
63
  children: n
59
64
  }),
60
- /* @__PURE__ */ T("div", {
65
+ /* @__PURE__ */ D("div", {
61
66
  className: "mt-1 flex flex-wrap items-center gap-2 text-xs",
62
- children: [/* @__PURE__ */ w("span", {
67
+ children: [/* @__PURE__ */ E("span", {
63
68
  className: "rounded bg-bg-sunken px-1.5 py-0.5 text-text-secondary",
64
69
  children: i(t.type)
65
- }), t.expiresAt && /* @__PURE__ */ T("span", {
66
- className: e(l ? "text-status-warning-text" : "text-text-secondary"),
67
- children: ["Expires ", r(t.expiresAt)]
70
+ }), t.expiresAt && /* @__PURE__ */ E("span", {
71
+ className: e(d ? "text-status-warning-text" : "text-text-secondary"),
72
+ children: a("assetVault.expires", "Expires {{date}}", { date: r(t.expiresAt) })
68
73
  })]
69
74
  })
70
75
  ]
71
76
  }),
72
- t.file?.id && /* @__PURE__ */ w("button", {
77
+ t.file?.id && /* @__PURE__ */ E("button", {
73
78
  type: "button",
74
79
  onClick: async () => {
75
- if (!(!t.file?.id || o)) {
76
- s(!0);
80
+ if (!(!t.file?.id || s)) {
81
+ c(!0);
77
82
  try {
78
- let e = await a(t.file.id);
83
+ let e = await o(t.file.id);
79
84
  window.open(e, "_blank", "noopener,noreferrer");
80
85
  } finally {
81
- s(!1);
86
+ c(!1);
82
87
  }
83
88
  }
84
89
  },
85
- disabled: o,
86
- "aria-label": `Download ${t.name}`,
90
+ disabled: s,
91
+ "aria-label": a("assetVault.downloadAriaLabel", "Download {{name}}", { name: t.name }),
87
92
  className: "shrink-0 rounded-lg border border-border-subtle p-2 text-text-secondary hover:text-text-primary disabled:opacity-50",
88
- children: /* @__PURE__ */ w(h, { className: "h-4 w-4" })
93
+ children: /* @__PURE__ */ E(_, { className: "h-4 w-4" })
89
94
  })
90
95
  ]
91
96
  });
92
97
  }
93
- var M = () => {
94
- let r = l(), u = d({ pagination: { limit: 100 } }), [m, h] = p(""), [_, y] = p("ALL"), b = f(() => {
98
+ var F = () => {
99
+ let u = l(), g = d({ pagination: { limit: 100 } }), _ = f(), [y, x] = m(""), [S, A] = m("ALL"), F = p(() => {
95
100
  let e = /* @__PURE__ */ new Map();
96
- for (let t of u.data?.items ?? []) e.set(t.id, t.label);
101
+ for (let t of g.data?.items ?? []) e.set(t.id, t.label);
97
102
  return e;
98
- }, [u.data]), O = f(() => r.data ?? [], [r.data]), M = f(() => O.filter((e) => (_ === "ALL" || e.type === _) && (!m || e.name.toLowerCase().includes(m.toLowerCase()))), [
99
- O,
100
- _,
101
- m
102
- ]), N = f(() => {
103
+ }, [g.data]), I = p(() => u.data ?? [], [u.data]), L = p(() => I.filter((e) => (S === "ALL" || e.type === S) && (!y || e.name.toLowerCase().includes(y.toLowerCase()))), [
104
+ I,
105
+ S,
106
+ y
107
+ ]), R = p(() => {
103
108
  let e = /* @__PURE__ */ new Map();
104
- for (let t of M) e.set(t.type, [...e.get(t.type) ?? [], t]);
105
- return k.filter((t) => e.has(t)).map((t) => [t, e.get(t)]);
106
- }, [M]), P = f(() => O.filter((e) => A(e.expiresAt)), [O]);
107
- return r.isLoading ? /* @__PURE__ */ w(s, { label: "Loading your asset vault…" }) : r.isError ? /* @__PURE__ */ w(o, {
108
- message: "Could not load the asset vault.",
109
- onRetry: () => void r.refetch()
110
- }) : /* @__PURE__ */ T(t, {
111
- title: "Asset vault",
112
- description: "Every contract, receipt, license key and document attached to your subscriptions — in one secure place.",
109
+ for (let t of L) e.set(t.type, [...e.get(t.type) ?? [], t]);
110
+ return j.filter((t) => e.has(t)).map((t) => [t, e.get(t)]);
111
+ }, [L]), z = p(() => I.filter((e) => M(e.expiresAt)).sort((e, t) => N(e.expiresAt) - N(t.expiresAt)), [I]);
112
+ return u.isLoading ? /* @__PURE__ */ E(s, { label: _("assetVault.loading", "Loading your asset vault…") }) : u.isError ? /* @__PURE__ */ E(o, {
113
+ message: _("assetVault.error", "Could not load the asset vault."),
114
+ onRetry: () => void u.refetch()
115
+ }) : /* @__PURE__ */ D(t, {
116
+ title: _("assetVault.title", "Asset vault"),
117
+ description: _("assetVault.description", "Every contract, receipt, license key and document attached to your subscriptions — in one secure place."),
113
118
  children: [
114
- /* @__PURE__ */ w(D, {
119
+ /* @__PURE__ */ E(k, {
115
120
  className: "mb-2",
116
- children: "Your subscription documents, together — contracts, receipts, license keys and more. Assets are stored in the shared files service and linked to the subscription they belong to. Attach new documents from any relationship’s detail page."
121
+ children: _("assetVault.purpose", "Your subscription documents, together — contracts, receipts, license keys and more. Assets are stored in the shared files service and linked to the subscription they belong to. Attach new documents from any relationship’s detail page.")
117
122
  }),
118
- /* @__PURE__ */ T("div", {
123
+ /* @__PURE__ */ D("div", {
119
124
  className: "grid grid-cols-1 gap-4 sm:grid-cols-3",
120
125
  children: [
121
- /* @__PURE__ */ w(n, {
122
- icon: /* @__PURE__ */ w(g, { className: "h-5 w-5" }),
123
- value: O.length,
124
- label: "Documents stored"
126
+ /* @__PURE__ */ E(n, {
127
+ icon: /* @__PURE__ */ E(v, { className: "h-5 w-5" }),
128
+ value: I.length,
129
+ label: _("assetVault.metrics.documentsStored", "Documents stored")
125
130
  }),
126
- /* @__PURE__ */ w(n, {
127
- icon: /* @__PURE__ */ w(v, { className: "h-5 w-5" }),
128
- value: N.length,
129
- label: "Document types"
131
+ /* @__PURE__ */ E(n, {
132
+ icon: /* @__PURE__ */ E(b, { className: "h-5 w-5" }),
133
+ value: R.length,
134
+ label: _("assetVault.metrics.documentTypes", "Document types")
130
135
  }),
131
- /* @__PURE__ */ w(n, {
132
- icon: /* @__PURE__ */ w(S, { className: "h-5 w-5" }),
133
- value: P.length,
134
- label: "Expiring within 30 days",
135
- trend: P.length > 0 ? "down" : void 0
136
+ /* @__PURE__ */ E(n, {
137
+ icon: /* @__PURE__ */ E(w, { className: "h-5 w-5" }),
138
+ value: z.length,
139
+ label: _("assetVault.metrics.expiring30Days", "Expiring within 30 days"),
140
+ trend: z.length > 0 ? "down" : void 0
136
141
  })
137
142
  ]
138
143
  }),
139
- /* @__PURE__ */ T(E, {
144
+ /* @__PURE__ */ D(O, {
140
145
  treatment: "plain",
141
146
  className: "flex flex-col gap-3 p-4 sm:flex-row sm:items-center",
142
- children: [/* @__PURE__ */ T("div", {
147
+ children: [/* @__PURE__ */ D("div", {
143
148
  className: "relative flex-1",
144
- children: [/* @__PURE__ */ w(x, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ w("input", {
145
- value: m,
146
- onChange: (e) => h(e.target.value),
147
- placeholder: "Search documents…",
148
- "aria-label": "Search documents",
149
+ children: [/* @__PURE__ */ E(C, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ E("input", {
150
+ value: y,
151
+ onChange: (e) => x(e.target.value),
152
+ placeholder: _("assetVault.searchPlaceholder", "Search documents…"),
153
+ "aria-label": _("assetVault.searchAriaLabel", "Search documents"),
149
154
  className: "w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary"
150
155
  })]
151
- }), /* @__PURE__ */ w("div", {
156
+ }), /* @__PURE__ */ E("div", {
152
157
  className: "flex flex-wrap gap-1.5",
153
- children: ["ALL", ...k].map((t) => /* @__PURE__ */ w("button", {
158
+ children: ["ALL", ...j].map((t) => /* @__PURE__ */ E("button", {
154
159
  type: "button",
155
- onClick: () => y(t),
156
- "aria-pressed": _ === t,
157
- className: e("rounded-full px-3 py-1 text-xs font-medium transition-colors", _ === t ? "bg-accent-primary text-accent-foreground" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
158
- children: t === "ALL" ? "All" : i(t)
160
+ onClick: () => A(t),
161
+ "aria-pressed": S === t,
162
+ className: e("rounded-full px-3 py-1 text-xs font-medium transition-colors", S === t ? "bg-accent-primary text-accent-foreground" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
163
+ children: t === "ALL" ? _("assetVault.filterAll", "All") : i(t)
159
164
  }, t))
160
165
  })]
161
166
  }),
162
- M.length === 0 ? /* @__PURE__ */ w(a, {
163
- icon: /* @__PURE__ */ w(C, { className: "h-8 w-8" }),
164
- title: O.length === 0 ? "No documents yet" : "No matching documents",
165
- description: O.length === 0 ? "Attach a contract, receipt or license key from any subscription’s detail page and it will appear here." : "Try a different search or type filter."
166
- }) : N.map(([e, t]) => /* @__PURE__ */ w(c, {
167
- title: `${i(e)} · ${t.length}`,
168
- children: /* @__PURE__ */ w("div", {
169
- className: "grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3",
170
- children: t.map((e) => /* @__PURE__ */ w(j, {
171
- asset: e,
172
- subscription: b.get(e.relationshipId) ?? "Subscription"
173
- }, e.id))
174
- })
175
- }, e))
167
+ /* @__PURE__ */ D("div", {
168
+ className: "grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]",
169
+ children: [/* @__PURE__ */ E("div", {
170
+ className: "min-w-0",
171
+ children: L.length === 0 ? /* @__PURE__ */ E(a, {
172
+ icon: /* @__PURE__ */ E(T, { className: "h-8 w-8" }),
173
+ title: I.length === 0 ? _("assetVault.empty.noDocumentsTitle", "No documents yet") : _("assetVault.empty.noMatchesTitle", "No matching documents"),
174
+ description: I.length === 0 ? _("assetVault.empty.noDocumentsDescription", "Attach a contract, receipt or license key from any subscription’s detail page and it will appear here.") : _("assetVault.empty.noMatchesDescription", "Try a different search or type filter.")
175
+ }) : R.map(([e, t]) => /* @__PURE__ */ E(c, {
176
+ title: `${i(e)} · ${t.length}`,
177
+ children: /* @__PURE__ */ E("div", {
178
+ className: "grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3",
179
+ children: t.map((e) => /* @__PURE__ */ E(P, {
180
+ asset: e,
181
+ subscription: F.get(e.relationshipId) ?? "—"
182
+ }, e.id))
183
+ })
184
+ }, e))
185
+ }), I.length > 0 && /* @__PURE__ */ E("aside", {
186
+ className: "lg:sticky lg:top-4 lg:self-start",
187
+ children: /* @__PURE__ */ D(O, {
188
+ treatment: "insight",
189
+ className: "p-5",
190
+ children: [
191
+ /* @__PURE__ */ D("h3", {
192
+ className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
193
+ children: [/* @__PURE__ */ E(h, {
194
+ className: "h-4 w-4 text-status-warning-text",
195
+ "aria-hidden": !0
196
+ }), _("assetVault.expiringSoon.title", "Expiring soon")]
197
+ }),
198
+ /* @__PURE__ */ E("p", {
199
+ className: "mt-0.5 text-xs text-text-secondary",
200
+ children: _("assetVault.expiringSoon.description", "Documents lapsing in the next 30 days")
201
+ }),
202
+ z.length === 0 ? /* @__PURE__ */ E("p", {
203
+ className: "mt-4 text-sm text-text-secondary",
204
+ children: _("assetVault.expiringSoon.nothing", "Nothing in the vault expires in the next 30 days.")
205
+ }) : /* @__PURE__ */ E("ul", {
206
+ className: "mt-4 space-y-2.5",
207
+ children: z.map((e) => /* @__PURE__ */ D("li", {
208
+ className: "rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-3",
209
+ children: [
210
+ /* @__PURE__ */ E("p", {
211
+ className: "truncate text-sm font-medium text-status-warning-text",
212
+ children: e.name
213
+ }),
214
+ /* @__PURE__ */ E("p", {
215
+ className: "truncate text-xs text-text-secondary",
216
+ children: F.get(e.relationshipId) ?? "—"
217
+ }),
218
+ /* @__PURE__ */ D("p", {
219
+ className: "mt-1 text-xs text-text-secondary",
220
+ children: [
221
+ i(e.type),
222
+ " ·",
223
+ " ",
224
+ _("assetVault.expires", "Expires {{date}}", { date: r(e.expiresAt) })
225
+ ]
226
+ })
227
+ ]
228
+ }, e.id))
229
+ })
230
+ ]
231
+ })
232
+ })]
233
+ })
176
234
  ]
177
235
  });
178
236
  };
179
237
  //#endregion
180
- export { M as AssetVaultPage };
238
+ export { F as AssetVaultPage };
181
239
 
182
240
  //# sourceMappingURL=AssetVaultPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AssetVaultPage.js","names":[],"sources":["../../../src/subscriberbot/pages/AssetVaultPage.tsx"],"sourcesContent":["/**\n * Digital Asset Vault — every document attached to any subscription in the\n * workspace (contracts, receipts, license keys, warranties). Real data only:\n * `useAllAssets` (workspace-wide) joined to `useRelationships` for the owning\n * subscription label; downloads resolve a presigned URL via files-svc.\n * @module subscriberbot/pages\n */\nimport { useMemo, useState, type FC } from 'react';\nimport {\n FileText,\n FileKey,\n Receipt,\n ShieldCheck,\n File as FileIcon,\n Globe,\n Award,\n Search,\n Download,\n Vault,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n} from '../components';\nimport { useAllAssets, useRelationships, useFileDownloadUrl } from '../hooks/useSubscriberBotApi';\nimport { formatDate, formatEnumLabel } from '../utils/formatters';\nimport type { SubAsset, SubAssetType } from '../types';\nimport { cn } from '../utils/cn';\n\nconst TYPE_ICON: Record<SubAssetType, typeof FileText> = {\n CONTRACT: FileText,\n LICENSE_KEY: FileKey,\n CREDENTIAL: FileKey,\n CERTIFICATE: Award,\n INVOICE: Receipt,\n RECEIPT: Receipt,\n WARRANTY: ShieldCheck,\n DOMAIN: Globe,\n};\n\nconst TYPE_ORDER: SubAssetType[] = [\n 'CONTRACT',\n 'LICENSE_KEY',\n 'CREDENTIAL',\n 'CERTIFICATE',\n 'INVOICE',\n 'RECEIPT',\n 'WARRANTY',\n 'DOMAIN',\n];\n\nconst isExpiringSoon = (expiresAt: string | null): boolean => {\n if (!expiresAt) return false;\n const t = new Date(expiresAt).getTime();\n if (Number.isNaN(t)) return false;\n const days = (t - Date.now()) / 86_400_000;\n return days >= 0 && days <= 30;\n};\n\nfunction AssetCard({ asset, subscription }: { asset: SubAsset; subscription: string }) {\n const getDownloadUrl = useFileDownloadUrl();\n const [busy, setBusy] = useState(false);\n const Icon = TYPE_ICON[asset.type] ?? FileIcon;\n const soon = isExpiringSoon(asset.expiresAt);\n\n const download = async () => {\n if (!asset.file?.id || busy) return;\n setBusy(true);\n try {\n const url = await getDownloadUrl(asset.file.id);\n window.open(url, '_blank', 'noopener,noreferrer');\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex items-start gap-3 p-4\">\n <span className=\"grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-accent-wash text-text-link\">\n <Icon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium text-text-primary\">{asset.name}</p>\n <p className=\"truncate text-xs text-text-secondary\">{subscription}</p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs\">\n <span className=\"rounded bg-bg-sunken px-1.5 py-0.5 text-text-secondary\">\n {formatEnumLabel(asset.type)}\n </span>\n {asset.expiresAt && (\n <span className={cn(soon ? 'text-status-warning-text' : 'text-text-secondary')}>\n Expires {formatDate(asset.expiresAt)}\n </span>\n )}\n </div>\n </div>\n {asset.file?.id && (\n <button\n type=\"button\"\n onClick={download}\n disabled={busy}\n aria-label={`Download ${asset.name}`}\n className=\"shrink-0 rounded-lg border border-border-subtle p-2 text-text-secondary hover:text-text-primary disabled:opacity-50\"\n >\n <Download className=\"h-4 w-4\" />\n </button>\n )}\n </GlassCard>\n );\n}\n\nexport const AssetVaultPage: FC = () => {\n const assetsQuery = useAllAssets();\n const relQuery = useRelationships({ pagination: { limit: 100 } });\n const [search, setSearch] = useState('');\n const [typeFilter, setTypeFilter] = useState<SubAssetType | 'ALL'>('ALL');\n\n const relLabels = useMemo(() => {\n const map = new Map<string, string>();\n for (const r of relQuery.data?.items ?? []) map.set(r.id, r.label);\n return map;\n }, [relQuery.data]);\n\n const assets = useMemo(() => assetsQuery.data ?? [], [assetsQuery.data]);\n const filtered = useMemo(\n () =>\n assets.filter(\n (a) =>\n (typeFilter === 'ALL' || a.type === typeFilter) &&\n (!search || a.name.toLowerCase().includes(search.toLowerCase()))\n ),\n [assets, typeFilter, search]\n );\n\n const grouped = useMemo(() => {\n const g = new Map<SubAssetType, SubAsset[]>();\n for (const a of filtered) g.set(a.type, [...(g.get(a.type) ?? []), a]);\n return TYPE_ORDER.filter((t) => g.has(t)).map((t) => [t, g.get(t)!] as const);\n }, [filtered]);\n\n const expiringSoon = useMemo(() => assets.filter((a) => isExpiringSoon(a.expiresAt)), [assets]);\n\n if (assetsQuery.isLoading) return <LoadingView label=\"Loading your asset vault…\" />;\n if (assetsQuery.isError)\n return (\n <ErrorView\n message=\"Could not load the asset vault.\"\n onRetry={() => void assetsQuery.refetch()}\n />\n );\n\n return (\n <PageLayout\n title=\"Asset vault\"\n description=\"Every contract, receipt, license key and document attached to your subscriptions — in one secure place.\"\n >\n <PagePurpose className=\"mb-2\">\n Your subscription documents, together — contracts, receipts, license keys and more. Assets\n are stored in the shared files service and linked to the subscription they belong to. Attach\n new documents from any relationship’s detail page.\n </PagePurpose>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<FileIcon className=\"h-5 w-5\" />}\n value={assets.length}\n label=\"Documents stored\"\n />\n <MetricsCard\n icon={<FileText className=\"h-5 w-5\" />}\n value={grouped.length}\n label=\"Document types\"\n />\n <MetricsCard\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n value={expiringSoon.length}\n label=\"Expiring within 30 days\"\n trend={expiringSoon.length > 0 ? 'down' : undefined}\n />\n </div>\n\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-3 p-4 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\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 value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search documents…\"\n aria-label=\"Search documents\"\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {(['ALL', ...TYPE_ORDER] as const).map((t) => (\n <button\n key={t}\n type=\"button\"\n onClick={() => setTypeFilter(t)}\n aria-pressed={typeFilter === t}\n className={cn(\n 'rounded-full px-3 py-1 text-xs font-medium transition-colors',\n typeFilter === t\n ? 'bg-accent-primary text-accent-foreground'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {t === 'ALL' ? 'All' : formatEnumLabel(t)}\n </button>\n ))}\n </div>\n </GlassCard>\n\n {filtered.length === 0 ? (\n <EmptyState\n icon={<Vault className=\"h-8 w-8\" />}\n title={assets.length === 0 ? 'No documents yet' : 'No matching documents'}\n description={\n assets.length === 0\n ? 'Attach a contract, receipt or license key from any subscription’s detail page and it will appear here.'\n : 'Try a different search or type filter.'\n }\n />\n ) : (\n grouped.map(([type, items]) => (\n <Section key={type} title={`${formatEnumLabel(type)} · ${items.length}`}>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3\">\n {items.map((a) => (\n <AssetCard\n key={a.id}\n asset={a}\n subscription={relLabels.get(a.relationshipId) ?? 'Subscription'}\n />\n ))}\n </div>\n </Section>\n ))\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AssetVaultPage.js","names":[],"sources":["../../../src/subscriberbot/pages/AssetVaultPage.tsx"],"sourcesContent":["/**\n * Digital Asset Vault — every document attached to any subscription in the\n * workspace (contracts, receipts, license keys, warranties). Real data only:\n * `useAllAssets` (workspace-wide) joined to `useRelationships` for the owning\n * subscription label; downloads resolve a presigned URL via files-svc. The\n * \"Expiring soon\" rail lists the real assets whose `expiresAt` falls inside the\n * next 30 days — the same set the headline metric counts.\n * @module subscriberbot/pages\n */\nimport { useMemo, useState, type FC } from 'react';\nimport {\n FileText,\n FileKey,\n Receipt,\n ShieldCheck,\n File as FileIcon,\n Globe,\n Award,\n Search,\n Download,\n Vault,\n AlarmClock,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n} from '../components';\nimport { useAllAssets, useRelationships, useFileDownloadUrl } from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { formatDate, formatEnumLabel } from '../utils/formatters';\nimport type { SubAsset, SubAssetType } from '../types';\nimport { cn } from '../utils/cn';\n\nconst TYPE_ICON: Record<SubAssetType, typeof FileText> = {\n CONTRACT: FileText,\n LICENSE_KEY: FileKey,\n CREDENTIAL: FileKey,\n CERTIFICATE: Award,\n INVOICE: Receipt,\n RECEIPT: Receipt,\n WARRANTY: ShieldCheck,\n DOMAIN: Globe,\n};\n\nconst TYPE_ORDER: SubAssetType[] = [\n 'CONTRACT',\n 'LICENSE_KEY',\n 'CREDENTIAL',\n 'CERTIFICATE',\n 'INVOICE',\n 'RECEIPT',\n 'WARRANTY',\n 'DOMAIN',\n];\n\nconst isExpiringSoon = (expiresAt: string | null): boolean => {\n if (!expiresAt) return false;\n const t = new Date(expiresAt).getTime();\n if (Number.isNaN(t)) return false;\n const days = (t - Date.now()) / 86_400_000;\n return days >= 0 && days <= 30;\n};\n\n/** Sort key for expiry — undated/unparseable assets sink to the bottom. */\nconst expiryTime = (expiresAt: string | null): number => {\n if (!expiresAt) return Number.POSITIVE_INFINITY;\n const t = new Date(expiresAt).getTime();\n return Number.isNaN(t) ? Number.POSITIVE_INFINITY : t;\n};\n\nfunction AssetCard({ asset, subscription }: { asset: SubAsset; subscription: string }) {\n const tr = useTr();\n const getDownloadUrl = useFileDownloadUrl();\n const [busy, setBusy] = useState(false);\n const Icon = TYPE_ICON[asset.type] ?? FileIcon;\n const soon = isExpiringSoon(asset.expiresAt);\n\n const download = async () => {\n if (!asset.file?.id || busy) return;\n setBusy(true);\n try {\n const url = await getDownloadUrl(asset.file.id);\n window.open(url, '_blank', 'noopener,noreferrer');\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex items-start gap-3 p-4\">\n <span className=\"grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-accent-wash text-text-link\">\n <Icon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium text-text-primary\">{asset.name}</p>\n <p className=\"truncate text-xs text-text-secondary\">{subscription}</p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs\">\n <span className=\"rounded bg-bg-sunken px-1.5 py-0.5 text-text-secondary\">\n {formatEnumLabel(asset.type)}\n </span>\n {asset.expiresAt && (\n <span className={cn(soon ? 'text-status-warning-text' : 'text-text-secondary')}>\n {tr('assetVault.expires', 'Expires {{date}}', { date: formatDate(asset.expiresAt) })}\n </span>\n )}\n </div>\n </div>\n {asset.file?.id && (\n <button\n type=\"button\"\n onClick={download}\n disabled={busy}\n aria-label={tr('assetVault.downloadAriaLabel', 'Download {{name}}', { name: asset.name })}\n className=\"shrink-0 rounded-lg border border-border-subtle p-2 text-text-secondary hover:text-text-primary disabled:opacity-50\"\n >\n <Download className=\"h-4 w-4\" />\n </button>\n )}\n </GlassCard>\n );\n}\n\nexport const AssetVaultPage: FC = () => {\n const assetsQuery = useAllAssets();\n const relQuery = useRelationships({ pagination: { limit: 100 } });\n const tr = useTr();\n const [search, setSearch] = useState('');\n const [typeFilter, setTypeFilter] = useState<SubAssetType | 'ALL'>('ALL');\n\n const relLabels = useMemo(() => {\n const map = new Map<string, string>();\n for (const r of relQuery.data?.items ?? []) map.set(r.id, r.label);\n return map;\n }, [relQuery.data]);\n\n const assets = useMemo(() => assetsQuery.data ?? [], [assetsQuery.data]);\n const filtered = useMemo(\n () =>\n assets.filter(\n (a) =>\n (typeFilter === 'ALL' || a.type === typeFilter) &&\n (!search || a.name.toLowerCase().includes(search.toLowerCase()))\n ),\n [assets, typeFilter, search]\n );\n\n const grouped = useMemo(() => {\n const g = new Map<SubAssetType, SubAsset[]>();\n for (const a of filtered) g.set(a.type, [...(g.get(a.type) ?? []), a]);\n return TYPE_ORDER.filter((t) => g.has(t)).map((t) => [t, g.get(t)!] as const);\n }, [filtered]);\n\n const expiringSoon = useMemo(\n () =>\n assets\n .filter((a) => isExpiringSoon(a.expiresAt))\n .sort((a, b) => expiryTime(a.expiresAt) - expiryTime(b.expiresAt)),\n [assets]\n );\n\n if (assetsQuery.isLoading)\n return <LoadingView label={tr('assetVault.loading', 'Loading your asset vault…')} />;\n if (assetsQuery.isError)\n return (\n <ErrorView\n message={tr('assetVault.error', 'Could not load the asset vault.')}\n onRetry={() => void assetsQuery.refetch()}\n />\n );\n\n return (\n <PageLayout\n title={tr('assetVault.title', 'Asset vault')}\n description={tr(\n 'assetVault.description',\n 'Every contract, receipt, license key and document attached to your subscriptions — in one secure place.'\n )}\n >\n <PagePurpose className=\"mb-2\">\n {tr(\n 'assetVault.purpose',\n 'Your subscription documents, together — contracts, receipts, license keys and more. Assets are stored in the shared files service and linked to the subscription they belong to. Attach new documents from any relationship’s detail page.'\n )}\n </PagePurpose>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<FileIcon className=\"h-5 w-5\" />}\n value={assets.length}\n label={tr('assetVault.metrics.documentsStored', 'Documents stored')}\n />\n <MetricsCard\n icon={<FileText className=\"h-5 w-5\" />}\n value={grouped.length}\n label={tr('assetVault.metrics.documentTypes', 'Document types')}\n />\n <MetricsCard\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n value={expiringSoon.length}\n label={tr('assetVault.metrics.expiring30Days', 'Expiring within 30 days')}\n trend={expiringSoon.length > 0 ? 'down' : undefined}\n />\n </div>\n\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-3 p-4 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\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 value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder={tr('assetVault.searchPlaceholder', 'Search documents…')}\n aria-label={tr('assetVault.searchAriaLabel', 'Search documents')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {(['ALL', ...TYPE_ORDER] as const).map((t) => (\n <button\n key={t}\n type=\"button\"\n onClick={() => setTypeFilter(t)}\n aria-pressed={typeFilter === t}\n className={cn(\n 'rounded-full px-3 py-1 text-xs font-medium transition-colors',\n typeFilter === t\n ? 'bg-accent-primary text-accent-foreground'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {t === 'ALL' ? tr('assetVault.filterAll', 'All') : formatEnumLabel(t)}\n </button>\n ))}\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n <div className=\"min-w-0\">\n {filtered.length === 0 ? (\n <EmptyState\n icon={<Vault className=\"h-8 w-8\" />}\n title={\n assets.length === 0\n ? tr('assetVault.empty.noDocumentsTitle', 'No documents yet')\n : tr('assetVault.empty.noMatchesTitle', 'No matching documents')\n }\n description={\n assets.length === 0\n ? tr(\n 'assetVault.empty.noDocumentsDescription',\n 'Attach a contract, receipt or license key from any subscription’s detail page and it will appear here.'\n )\n : tr(\n 'assetVault.empty.noMatchesDescription',\n 'Try a different search or type filter.'\n )\n }\n />\n ) : (\n grouped.map(([type, items]) => (\n <Section key={type} title={`${formatEnumLabel(type)} · ${items.length}`}>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3\">\n {items.map((a) => (\n <AssetCard\n key={a.id}\n asset={a}\n subscription={relLabels.get(a.relationshipId) ?? '—'}\n />\n ))}\n </div>\n </Section>\n ))\n )}\n </div>\n\n {assets.length > 0 && (\n <aside className=\"lg:sticky lg:top-4 lg:self-start\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <AlarmClock className=\"h-4 w-4 text-status-warning-text\" aria-hidden />\n {tr('assetVault.expiringSoon.title', 'Expiring soon')}\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {tr('assetVault.expiringSoon.description', 'Documents lapsing in the next 30 days')}\n </p>\n {expiringSoon.length === 0 ? (\n <p className=\"mt-4 text-sm text-text-secondary\">\n {tr(\n 'assetVault.expiringSoon.nothing',\n 'Nothing in the vault expires in the next 30 days.'\n )}\n </p>\n ) : (\n <ul className=\"mt-4 space-y-2.5\">\n {expiringSoon.map((a) => (\n <li\n key={a.id}\n className=\"rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-3\"\n >\n <p className=\"truncate text-sm font-medium text-status-warning-text\">\n {a.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {relLabels.get(a.relationshipId) ?? '—'}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {formatEnumLabel(a.type)} ·{' '}\n {tr('assetVault.expires', 'Expires {{date}}', {\n date: formatDate(a.expiresAt),\n })}\n </p>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </aside>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAmEH,wBAAgB,aAAa,gCA4X5B"}
1
+ {"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoEH,wBAAgB,aAAa,gCAwc5B"}