@burdenoff/microfe-subscriberbot 2026.827.1 → 2026.1014.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 (56) 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/hooks/index.d.ts +1 -0
  5. package/dist/subscriberbot/hooks/index.d.ts.map +1 -1
  6. package/dist/subscriberbot/hooks/index.js +1 -0
  7. package/dist/subscriberbot/hooks/useTr.d.ts +17 -0
  8. package/dist/subscriberbot/hooks/useTr.d.ts.map +1 -0
  9. package/dist/subscriberbot/hooks/useTr.js +14 -0
  10. package/dist/subscriberbot/hooks/useTr.js.map +1 -0
  11. package/dist/subscriberbot/pages/AssetVaultPage.d.ts.map +1 -1
  12. package/dist/subscriberbot/pages/AssetVaultPage.js +111 -109
  13. package/dist/subscriberbot/pages/AssetVaultPage.js.map +1 -1
  14. package/dist/subscriberbot/pages/DashboardPage.d.ts.map +1 -1
  15. package/dist/subscriberbot/pages/DashboardPage.js +113 -105
  16. package/dist/subscriberbot/pages/DashboardPage.js.map +1 -1
  17. package/dist/subscriberbot/pages/InboxPage.d.ts.map +1 -1
  18. package/dist/subscriberbot/pages/InboxPage.js +404 -369
  19. package/dist/subscriberbot/pages/InboxPage.js.map +1 -1
  20. package/dist/subscriberbot/pages/InsightsPage.d.ts.map +1 -1
  21. package/dist/subscriberbot/pages/InsightsPage.js +94 -93
  22. package/dist/subscriberbot/pages/InsightsPage.js.map +1 -1
  23. package/dist/subscriberbot/pages/InvoicesPage.d.ts.map +1 -1
  24. package/dist/subscriberbot/pages/InvoicesPage.js +360 -366
  25. package/dist/subscriberbot/pages/InvoicesPage.js.map +1 -1
  26. package/dist/subscriberbot/pages/WorkflowsPage.d.ts.map +1 -1
  27. package/dist/subscriberbot/pages/WorkflowsPage.js +209 -193
  28. package/dist/subscriberbot/pages/WorkflowsPage.js.map +1 -1
  29. package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts.map +1 -1
  30. package/dist/subscriberbot/pages/agents/AgentDetailPage.js +268 -236
  31. package/dist/subscriberbot/pages/agents/AgentDetailPage.js.map +1 -1
  32. package/dist/subscriberbot/pages/agents/AgentNewPage.d.ts.map +1 -1
  33. package/dist/subscriberbot/pages/agents/AgentNewPage.js +96 -76
  34. package/dist/subscriberbot/pages/agents/AgentNewPage.js.map +1 -1
  35. package/dist/subscriberbot/pages/agents/AgentsListPage.d.ts.map +1 -1
  36. package/dist/subscriberbot/pages/agents/AgentsListPage.js +145 -161
  37. package/dist/subscriberbot/pages/agents/AgentsListPage.js.map +1 -1
  38. package/dist/subscriberbot/pages/providers/ProviderDetailPage.d.ts.map +1 -1
  39. package/dist/subscriberbot/pages/providers/ProviderDetailPage.js +514 -441
  40. package/dist/subscriberbot/pages/providers/ProviderDetailPage.js.map +1 -1
  41. package/dist/subscriberbot/pages/providers/ProviderNewPage.d.ts.map +1 -1
  42. package/dist/subscriberbot/pages/providers/ProviderNewPage.js +167 -129
  43. package/dist/subscriberbot/pages/providers/ProviderNewPage.js.map +1 -1
  44. package/dist/subscriberbot/pages/providers/ProvidersListPage.d.ts.map +1 -1
  45. package/dist/subscriberbot/pages/providers/ProvidersListPage.js +171 -218
  46. package/dist/subscriberbot/pages/providers/ProvidersListPage.js.map +1 -1
  47. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
  48. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +340 -279
  49. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
  50. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.d.ts.map +1 -1
  51. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js +185 -166
  52. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js.map +1 -1
  53. package/dist/subscriberbot/pages/relationships/RelationshipsListPage.d.ts.map +1 -1
  54. package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js +153 -182
  55. package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js.map +1 -1
  56. package/package.json +1 -1
@@ -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 { AlarmClock as m, Award as h, Download as g, File as _, FileKey as v, FileText as y, Globe as b, Receipt as x, Search as S, ShieldCheck as C, Vault as w } from "lucide-react";
11
- import { jsx as T, jsxs as E } from "react/jsx-runtime";
12
- import { GlassCard as D, PagePurpose as O } 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 k = {
15
- CONTRACT: y,
16
- LICENSE_KEY: v,
17
- CREDENTIAL: v,
18
- CERTIFICATE: h,
19
- INVOICE: x,
20
- RECEIPT: x,
21
- WARRANTY: C,
22
- DOMAIN: b
23
- }, A = [
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,197 +30,198 @@ var k = {
29
30
  "RECEIPT",
30
31
  "WARRANTY",
31
32
  "DOMAIN"
32
- ], j = (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;
38
- }, M = (e) => {
39
+ }, N = (e) => {
39
40
  if (!e) return Infinity;
40
41
  let t = new Date(e).getTime();
41
42
  return Number.isNaN(t) ? Infinity : t;
42
43
  };
43
- function N({ asset: t, subscription: n }) {
44
- let a = u(), [o, s] = p(!1), c = k[t.type] ?? _, l = j(t.expiresAt);
45
- return /* @__PURE__ */ E(D, {
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, {
46
47
  treatment: "metric",
47
48
  className: "flex items-start gap-3 p-4",
48
49
  children: [
49
- /* @__PURE__ */ T("span", {
50
+ /* @__PURE__ */ E("span", {
50
51
  className: "grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-accent-wash text-text-link",
51
- children: /* @__PURE__ */ T(c, { className: "h-5 w-5" })
52
+ children: /* @__PURE__ */ E(l, { className: "h-5 w-5" })
52
53
  }),
53
- /* @__PURE__ */ E("div", {
54
+ /* @__PURE__ */ D("div", {
54
55
  className: "min-w-0 flex-1",
55
56
  children: [
56
- /* @__PURE__ */ T("p", {
57
+ /* @__PURE__ */ E("p", {
57
58
  className: "truncate font-medium text-text-primary",
58
59
  children: t.name
59
60
  }),
60
- /* @__PURE__ */ T("p", {
61
+ /* @__PURE__ */ E("p", {
61
62
  className: "truncate text-xs text-text-secondary",
62
63
  children: n
63
64
  }),
64
- /* @__PURE__ */ E("div", {
65
+ /* @__PURE__ */ D("div", {
65
66
  className: "mt-1 flex flex-wrap items-center gap-2 text-xs",
66
- children: [/* @__PURE__ */ T("span", {
67
+ children: [/* @__PURE__ */ E("span", {
67
68
  className: "rounded bg-bg-sunken px-1.5 py-0.5 text-text-secondary",
68
69
  children: i(t.type)
69
70
  }), t.expiresAt && /* @__PURE__ */ E("span", {
70
- className: e(l ? "text-status-warning-text" : "text-text-secondary"),
71
- children: ["Expires ", r(t.expiresAt)]
71
+ className: e(d ? "text-status-warning-text" : "text-text-secondary"),
72
+ children: a("assetVault.expires", "Expires {{date}}", { date: r(t.expiresAt) })
72
73
  })]
73
74
  })
74
75
  ]
75
76
  }),
76
- t.file?.id && /* @__PURE__ */ T("button", {
77
+ t.file?.id && /* @__PURE__ */ E("button", {
77
78
  type: "button",
78
79
  onClick: async () => {
79
- if (!(!t.file?.id || o)) {
80
- s(!0);
80
+ if (!(!t.file?.id || s)) {
81
+ c(!0);
81
82
  try {
82
- let e = await a(t.file.id);
83
+ let e = await o(t.file.id);
83
84
  window.open(e, "_blank", "noopener,noreferrer");
84
85
  } finally {
85
- s(!1);
86
+ c(!1);
86
87
  }
87
88
  }
88
89
  },
89
- disabled: o,
90
- "aria-label": `Download ${t.name}`,
90
+ disabled: s,
91
+ "aria-label": a("assetVault.downloadAriaLabel", "Download {{name}}", { name: t.name }),
91
92
  className: "shrink-0 rounded-lg border border-border-subtle p-2 text-text-secondary hover:text-text-primary disabled:opacity-50",
92
- children: /* @__PURE__ */ T(g, { className: "h-4 w-4" })
93
+ children: /* @__PURE__ */ E(_, { className: "h-4 w-4" })
93
94
  })
94
95
  ]
95
96
  });
96
97
  }
97
- var P = () => {
98
- let u = l(), h = d({ pagination: { limit: 100 } }), [g, v] = p(""), [b, x] = p("ALL"), k = f(() => {
98
+ var F = () => {
99
+ let u = l(), g = d({ pagination: { limit: 100 } }), _ = f(), [y, x] = m(""), [S, A] = m("ALL"), F = p(() => {
99
100
  let e = /* @__PURE__ */ new Map();
100
- for (let t of h.data?.items ?? []) e.set(t.id, t.label);
101
+ for (let t of g.data?.items ?? []) e.set(t.id, t.label);
101
102
  return e;
102
- }, [h.data]), P = f(() => u.data ?? [], [u.data]), F = f(() => P.filter((e) => (b === "ALL" || e.type === b) && (!g || e.name.toLowerCase().includes(g.toLowerCase()))), [
103
- P,
104
- b,
105
- g
106
- ]), I = 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(() => {
107
108
  let e = /* @__PURE__ */ new Map();
108
- for (let t of F) e.set(t.type, [...e.get(t.type) ?? [], t]);
109
- return A.filter((t) => e.has(t)).map((t) => [t, e.get(t)]);
110
- }, [F]), L = f(() => P.filter((e) => j(e.expiresAt)).sort((e, t) => M(e.expiresAt) - M(t.expiresAt)), [P]);
111
- return u.isLoading ? /* @__PURE__ */ T(s, { label: "Loading your asset vault…" }) : u.isError ? /* @__PURE__ */ T(o, {
112
- message: "Could not load the asset vault.",
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."),
113
114
  onRetry: () => void u.refetch()
114
- }) : /* @__PURE__ */ E(t, {
115
- title: "Asset vault",
116
- description: "Every contract, receipt, license key and document attached to your subscriptions — in one secure place.",
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."),
117
118
  children: [
118
- /* @__PURE__ */ T(O, {
119
+ /* @__PURE__ */ E(k, {
119
120
  className: "mb-2",
120
- 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.")
121
122
  }),
122
- /* @__PURE__ */ E("div", {
123
+ /* @__PURE__ */ D("div", {
123
124
  className: "grid grid-cols-1 gap-4 sm:grid-cols-3",
124
125
  children: [
125
- /* @__PURE__ */ T(n, {
126
- icon: /* @__PURE__ */ T(_, { className: "h-5 w-5" }),
127
- value: P.length,
128
- label: "Documents stored"
129
- }),
130
- /* @__PURE__ */ T(n, {
131
- icon: /* @__PURE__ */ T(y, { className: "h-5 w-5" }),
126
+ /* @__PURE__ */ E(n, {
127
+ icon: /* @__PURE__ */ E(v, { className: "h-5 w-5" }),
132
128
  value: I.length,
133
- label: "Document types"
129
+ label: _("assetVault.metrics.documentsStored", "Documents stored")
130
+ }),
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")
134
135
  }),
135
- /* @__PURE__ */ T(n, {
136
- icon: /* @__PURE__ */ T(C, { className: "h-5 w-5" }),
137
- value: L.length,
138
- label: "Expiring within 30 days",
139
- trend: L.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
140
141
  })
141
142
  ]
142
143
  }),
143
- /* @__PURE__ */ E(D, {
144
+ /* @__PURE__ */ D(O, {
144
145
  treatment: "plain",
145
146
  className: "flex flex-col gap-3 p-4 sm:flex-row sm:items-center",
146
- children: [/* @__PURE__ */ E("div", {
147
+ children: [/* @__PURE__ */ D("div", {
147
148
  className: "relative flex-1",
148
- children: [/* @__PURE__ */ T(S, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ T("input", {
149
- value: g,
150
- onChange: (e) => v(e.target.value),
151
- placeholder: "Search documents…",
152
- "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"),
153
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"
154
155
  })]
155
- }), /* @__PURE__ */ T("div", {
156
+ }), /* @__PURE__ */ E("div", {
156
157
  className: "flex flex-wrap gap-1.5",
157
- children: ["ALL", ...A].map((t) => /* @__PURE__ */ T("button", {
158
+ children: ["ALL", ...j].map((t) => /* @__PURE__ */ E("button", {
158
159
  type: "button",
159
- onClick: () => x(t),
160
- "aria-pressed": b === t,
161
- className: e("rounded-full px-3 py-1 text-xs font-medium transition-colors", b === t ? "bg-accent-primary text-accent-foreground" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
162
- 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)
163
164
  }, t))
164
165
  })]
165
166
  }),
166
- /* @__PURE__ */ E("div", {
167
+ /* @__PURE__ */ D("div", {
167
168
  className: "grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]",
168
- children: [/* @__PURE__ */ T("div", {
169
+ children: [/* @__PURE__ */ E("div", {
169
170
  className: "min-w-0",
170
- children: F.length === 0 ? /* @__PURE__ */ T(a, {
171
- icon: /* @__PURE__ */ T(w, { className: "h-8 w-8" }),
172
- title: P.length === 0 ? "No documents yet" : "No matching documents",
173
- description: P.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."
174
- }) : I.map(([e, t]) => /* @__PURE__ */ T(c, {
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, {
175
176
  title: `${i(e)} · ${t.length}`,
176
- children: /* @__PURE__ */ T("div", {
177
+ children: /* @__PURE__ */ E("div", {
177
178
  className: "grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3",
178
- children: t.map((e) => /* @__PURE__ */ T(N, {
179
+ children: t.map((e) => /* @__PURE__ */ E(P, {
179
180
  asset: e,
180
- subscription: k.get(e.relationshipId) ?? "—"
181
+ subscription: F.get(e.relationshipId) ?? "—"
181
182
  }, e.id))
182
183
  })
183
184
  }, e))
184
- }), P.length > 0 && /* @__PURE__ */ T("aside", {
185
+ }), I.length > 0 && /* @__PURE__ */ E("aside", {
185
186
  className: "lg:sticky lg:top-4 lg:self-start",
186
- children: /* @__PURE__ */ E(D, {
187
+ children: /* @__PURE__ */ D(O, {
187
188
  treatment: "insight",
188
189
  className: "p-5",
189
190
  children: [
190
- /* @__PURE__ */ E("h3", {
191
+ /* @__PURE__ */ D("h3", {
191
192
  className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
192
- children: [/* @__PURE__ */ T(m, {
193
+ children: [/* @__PURE__ */ E(h, {
193
194
  className: "h-4 w-4 text-status-warning-text",
194
195
  "aria-hidden": !0
195
- }), "Expiring soon"]
196
+ }), _("assetVault.expiringSoon.title", "Expiring soon")]
196
197
  }),
197
- /* @__PURE__ */ T("p", {
198
+ /* @__PURE__ */ E("p", {
198
199
  className: "mt-0.5 text-xs text-text-secondary",
199
- children: "Documents lapsing in the next 30 days"
200
+ children: _("assetVault.expiringSoon.description", "Documents lapsing in the next 30 days")
200
201
  }),
201
- L.length === 0 ? /* @__PURE__ */ T("p", {
202
+ z.length === 0 ? /* @__PURE__ */ E("p", {
202
203
  className: "mt-4 text-sm text-text-secondary",
203
- children: "Nothing in the vault expires in the next 30 days."
204
- }) : /* @__PURE__ */ T("ul", {
204
+ children: _("assetVault.expiringSoon.nothing", "Nothing in the vault expires in the next 30 days.")
205
+ }) : /* @__PURE__ */ E("ul", {
205
206
  className: "mt-4 space-y-2.5",
206
- children: L.map((e) => /* @__PURE__ */ E("li", {
207
+ children: z.map((e) => /* @__PURE__ */ D("li", {
207
208
  className: "rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-3",
208
209
  children: [
209
- /* @__PURE__ */ T("p", {
210
+ /* @__PURE__ */ E("p", {
210
211
  className: "truncate text-sm font-medium text-status-warning-text",
211
212
  children: e.name
212
213
  }),
213
- /* @__PURE__ */ T("p", {
214
+ /* @__PURE__ */ E("p", {
214
215
  className: "truncate text-xs text-text-secondary",
215
- children: k.get(e.relationshipId) ?? "—"
216
+ children: F.get(e.relationshipId) ?? "—"
216
217
  }),
217
- /* @__PURE__ */ E("p", {
218
+ /* @__PURE__ */ D("p", {
218
219
  className: "mt-1 text-xs text-text-secondary",
219
220
  children: [
220
221
  i(e.type),
221
- " · Expires ",
222
- r(e.expiresAt)
222
+ " ·",
223
+ " ",
224
+ _("assetVault.expires", "Expires {{date}}", { date: r(e.expiresAt) })
223
225
  ]
224
226
  })
225
227
  ]
@@ -233,6 +235,6 @@ var P = () => {
233
235
  });
234
236
  };
235
237
  //#endregion
236
- export { P as AssetVaultPage };
238
+ export { F as AssetVaultPage };
237
239
 
238
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. 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 { 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 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(\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) 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 <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={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 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 Expiring soon\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Documents lapsing in the next 30 days\n </p>\n {expiringSoon.length === 0 ? (\n <p className=\"mt-4 text-sm text-text-secondary\">\n Nothing in the vault expires in the next 30 days.\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)} · Expires {formatDate(a.expiresAt)}\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
+ {"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"}