@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.
- package/dist/subscriberbot/admin/AdminDashboardPage.d.ts.map +1 -1
- package/dist/subscriberbot/admin/AdminDashboardPage.js +80 -83
- package/dist/subscriberbot/admin/AdminDashboardPage.js.map +1 -1
- package/dist/subscriberbot/components/viz.js +51 -27
- package/dist/subscriberbot/components/viz.js.map +1 -1
- package/dist/subscriberbot/hooks/index.d.ts +2 -1
- package/dist/subscriberbot/hooks/index.d.ts.map +1 -1
- package/dist/subscriberbot/hooks/index.js +1 -0
- package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +12 -1
- package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
- package/dist/subscriberbot/hooks/useSubscriberBotApi.js +82 -56
- package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
- package/dist/subscriberbot/hooks/useTr.d.ts +17 -0
- package/dist/subscriberbot/hooks/useTr.d.ts.map +1 -0
- package/dist/subscriberbot/hooks/useTr.js +14 -0
- package/dist/subscriberbot/hooks/useTr.js.map +1 -0
- package/dist/subscriberbot/pages/AssetVaultPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/AssetVaultPage.js +155 -97
- package/dist/subscriberbot/pages/AssetVaultPage.js.map +1 -1
- package/dist/subscriberbot/pages/DashboardPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/DashboardPage.js +113 -105
- package/dist/subscriberbot/pages/DashboardPage.js.map +1 -1
- package/dist/subscriberbot/pages/InboxPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/InboxPage.js +404 -369
- package/dist/subscriberbot/pages/InboxPage.js.map +1 -1
- package/dist/subscriberbot/pages/InsightsPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/InsightsPage.js +94 -93
- package/dist/subscriberbot/pages/InsightsPage.js.map +1 -1
- package/dist/subscriberbot/pages/InvoicesPage.d.ts +3 -2
- package/dist/subscriberbot/pages/InvoicesPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/InvoicesPage.js +368 -362
- package/dist/subscriberbot/pages/InvoicesPage.js.map +1 -1
- package/dist/subscriberbot/pages/WorkflowsPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/WorkflowsPage.js +209 -193
- package/dist/subscriberbot/pages/WorkflowsPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts +3 -1
- package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentDetailPage.js +296 -228
- package/dist/subscriberbot/pages/agents/AgentDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentNewPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentNewPage.js +96 -76
- package/dist/subscriberbot/pages/agents/AgentNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentsListPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentsListPage.js +145 -161
- package/dist/subscriberbot/pages/agents/AgentsListPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.d.ts +4 -2
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.js +577 -402
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderNewPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderNewPage.js +167 -129
- package/dist/subscriberbot/pages/providers/ProviderNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProvidersListPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/providers/ProvidersListPage.js +171 -218
- package/dist/subscriberbot/pages/providers/ProvidersListPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +340 -279
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipNewPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js +185 -166
- package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipsListPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js +153 -182
- package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js.map +1 -1
- package/dist/subscriberbot/types/index.d.ts +37 -0
- package/dist/subscriberbot/types/index.d.ts.map +1 -1
- 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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
|
15
|
-
CONTRACT:
|
|
16
|
-
LICENSE_KEY:
|
|
17
|
-
CREDENTIAL:
|
|
18
|
-
CERTIFICATE:
|
|
19
|
-
INVOICE:
|
|
20
|
-
RECEIPT:
|
|
21
|
-
WARRANTY:
|
|
22
|
-
DOMAIN:
|
|
23
|
-
},
|
|
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
|
-
],
|
|
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
|
|
40
|
-
let a = u(), [
|
|
41
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
52
|
+
children: /* @__PURE__ */ E(l, { className: "h-5 w-5" })
|
|
48
53
|
}),
|
|
49
|
-
/* @__PURE__ */
|
|
54
|
+
/* @__PURE__ */ D("div", {
|
|
50
55
|
className: "min-w-0 flex-1",
|
|
51
56
|
children: [
|
|
52
|
-
/* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ E("p", {
|
|
53
58
|
className: "truncate font-medium text-text-primary",
|
|
54
59
|
children: t.name
|
|
55
60
|
}),
|
|
56
|
-
/* @__PURE__ */
|
|
61
|
+
/* @__PURE__ */ E("p", {
|
|
57
62
|
className: "truncate text-xs text-text-secondary",
|
|
58
63
|
children: n
|
|
59
64
|
}),
|
|
60
|
-
/* @__PURE__ */
|
|
65
|
+
/* @__PURE__ */ D("div", {
|
|
61
66
|
className: "mt-1 flex flex-wrap items-center gap-2 text-xs",
|
|
62
|
-
children: [/* @__PURE__ */
|
|
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__ */
|
|
66
|
-
className: e(
|
|
67
|
-
children:
|
|
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__ */
|
|
77
|
+
t.file?.id && /* @__PURE__ */ E("button", {
|
|
73
78
|
type: "button",
|
|
74
79
|
onClick: async () => {
|
|
75
|
-
if (!(!t.file?.id ||
|
|
76
|
-
|
|
80
|
+
if (!(!t.file?.id || s)) {
|
|
81
|
+
c(!0);
|
|
77
82
|
try {
|
|
78
|
-
let e = await
|
|
83
|
+
let e = await o(t.file.id);
|
|
79
84
|
window.open(e, "_blank", "noopener,noreferrer");
|
|
80
85
|
} finally {
|
|
81
|
-
|
|
86
|
+
c(!1);
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
},
|
|
85
|
-
disabled:
|
|
86
|
-
"aria-label":
|
|
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__ */
|
|
93
|
+
children: /* @__PURE__ */ E(_, { className: "h-4 w-4" })
|
|
89
94
|
})
|
|
90
95
|
]
|
|
91
96
|
});
|
|
92
97
|
}
|
|
93
|
-
var
|
|
94
|
-
let
|
|
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
|
|
101
|
+
for (let t of g.data?.items ?? []) e.set(t.id, t.label);
|
|
97
102
|
return e;
|
|
98
|
-
}, [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
]),
|
|
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
|
|
105
|
-
return
|
|
106
|
-
}, [
|
|
107
|
-
return
|
|
108
|
-
message: "Could not load the asset vault.",
|
|
109
|
-
onRetry: () => void
|
|
110
|
-
}) : /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
123
|
+
/* @__PURE__ */ D("div", {
|
|
119
124
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-3",
|
|
120
125
|
children: [
|
|
121
|
-
/* @__PURE__ */
|
|
122
|
-
icon: /* @__PURE__ */
|
|
123
|
-
value:
|
|
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__ */
|
|
127
|
-
icon: /* @__PURE__ */
|
|
128
|
-
value:
|
|
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__ */
|
|
132
|
-
icon: /* @__PURE__ */ w
|
|
133
|
-
value:
|
|
134
|
-
label: "Expiring within 30 days",
|
|
135
|
-
trend:
|
|
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__ */
|
|
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__ */
|
|
147
|
+
children: [/* @__PURE__ */ D("div", {
|
|
143
148
|
className: "relative flex-1",
|
|
144
|
-
children: [/* @__PURE__ */
|
|
145
|
-
value:
|
|
146
|
-
onChange: (e) =>
|
|
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__ */
|
|
156
|
+
}), /* @__PURE__ */ E("div", {
|
|
152
157
|
className: "flex flex-wrap gap-1.5",
|
|
153
|
-
children: ["ALL", ...
|
|
158
|
+
children: ["ALL", ...j].map((t) => /* @__PURE__ */ E("button", {
|
|
154
159
|
type: "button",
|
|
155
|
-
onClick: () =>
|
|
156
|
-
"aria-pressed":
|
|
157
|
-
className: e("rounded-full px-3 py-1 text-xs font-medium transition-colors",
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 {
|
|
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;
|
|
1
|
+
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/subscriberbot/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoEH,wBAAgB,aAAa,gCAwc5B"}
|