@burdenoff/microfe-subscriberbot 2026.1030.5 → 2026.1030.7

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 (32) hide show
  1. package/dist/subscriberbot/SubscriberBotRoutes.d.ts.map +1 -1
  2. package/dist/subscriberbot/SubscriberBotRoutes.js +10 -6
  3. package/dist/subscriberbot/SubscriberBotRoutes.js.map +1 -1
  4. package/dist/subscriberbot/components/savings/SavingsCards.d.ts +2 -2
  5. package/dist/subscriberbot/components/savings/SavingsCards.d.ts.map +1 -1
  6. package/dist/subscriberbot/components/savings/SavingsCards.js +128 -104
  7. package/dist/subscriberbot/components/savings/SavingsCards.js.map +1 -1
  8. package/dist/subscriberbot/components/savings/savingsModel.d.ts +14 -9
  9. package/dist/subscriberbot/components/savings/savingsModel.d.ts.map +1 -1
  10. package/dist/subscriberbot/components/savings/savingsModel.js +9 -7
  11. package/dist/subscriberbot/components/savings/savingsModel.js.map +1 -1
  12. package/dist/subscriberbot/hooks/index.d.ts +1 -1
  13. package/dist/subscriberbot/hooks/index.d.ts.map +1 -1
  14. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +4 -1
  15. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
  16. package/dist/subscriberbot/hooks/useSubscriberBotApi.js +137 -72
  17. package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
  18. package/dist/subscriberbot/pages/SavingsPage.js +1 -1
  19. package/dist/subscriberbot/pages/SavingsPage.js.map +1 -1
  20. package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.d.ts +13 -0
  21. package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.d.ts.map +1 -0
  22. package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.js +703 -0
  23. package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.js.map +1 -0
  24. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
  25. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +276 -262
  26. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
  27. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.d.ts.map +1 -1
  28. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js +65 -45
  29. package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js.map +1 -1
  30. package/dist/subscriberbot/types/index.d.ts +171 -15
  31. package/dist/subscriberbot/types/index.d.ts.map +1 -1
  32. package/package.json +1 -1
@@ -0,0 +1,703 @@
1
+ import { useSubscriberBot as e } from "../../context/SubscriberBotContext.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { PageLayout as n } from "../../components/PageLayout.js";
4
+ import { MetricsCard as r } from "../../components/MetricsCard.js";
5
+ import { formatCurrency as i, formatDate as a } from "../../utils/formatters.js";
6
+ import { EmptyState as o } from "../../components/EmptyState.js";
7
+ import { Modal as s } from "../../components/Modal.js";
8
+ import { useTr as c } from "../../hooks/useTr.js";
9
+ import { ErrorView as l, LoadingView as u, Section as d } from "../../components/StateViews.js";
10
+ import { BarList as f, MiniProgress as p, Sparkline as m } from "../../components/viz.js";
11
+ import "../../components/index.js";
12
+ import { useEnterpriseSpendDashboard as h, useSetSpendBudget as g, useSpendBudgets as _ } from "../../hooks/useSubscriberBotApi.js";
13
+ import { useMemo as v, useState as y } from "react";
14
+ import { AlertTriangle as b, Building2 as x, CalendarClock as S, Copy as C, EyeOff as w, Layers as T, PiggyBank as E, ShieldAlert as D, Target as O, Users as k, Wallet as A } from "lucide-react";
15
+ import { jsx as j, jsxs as M } from "react/jsx-runtime";
16
+ import { GlassCard as N, PagePurpose as P } from "@burdenoff/fe-libs/ui";
17
+ //#region src/subscriberbot/pages/enterprise/EnterpriseSpendPage.tsx
18
+ var F = {
19
+ UNCATALOGUED: {
20
+ key: "enterprise.shadow.reasons.uncatalogued",
21
+ fallback: "Not in the catalogue"
22
+ },
23
+ UNVERIFIED_PROVIDER: {
24
+ key: "enterprise.shadow.reasons.unverified",
25
+ fallback: "Unverified provider"
26
+ },
27
+ DEPRECATED_PROVIDER: {
28
+ key: "enterprise.shadow.reasons.deprecated",
29
+ fallback: "Deprecated provider"
30
+ }
31
+ }, I = {
32
+ EXACT_DUPLICATE: {
33
+ key: "enterprise.duplicates.types.exact",
34
+ fallback: "Same vendor"
35
+ },
36
+ CAPABILITY: {
37
+ key: "enterprise.duplicates.types.capability",
38
+ fallback: "Same capability"
39
+ },
40
+ ENTITLEMENT: {
41
+ key: "enterprise.duplicates.types.entitlement",
42
+ fallback: "Covered by another plan"
43
+ }
44
+ }, L = {
45
+ PAST_DUE: {
46
+ key: "enterprise.risks.kinds.pastDue",
47
+ fallback: "Payment failed"
48
+ },
49
+ MISSING_INVOICE: {
50
+ key: "enterprise.risks.kinds.missingInvoice",
51
+ fallback: "Missing invoice"
52
+ },
53
+ UNTRACKED_TRIAL: {
54
+ key: "enterprise.risks.kinds.untrackedTrial",
55
+ fallback: "Untracked trial"
56
+ },
57
+ HIGH_COST_AI: {
58
+ key: "enterprise.risks.kinds.highCostAi",
59
+ fallback: "High-cost AI service"
60
+ }
61
+ }, R = {
62
+ HIGH: "bg-status-error-bg-subtle text-status-error-text",
63
+ MEDIUM: "bg-status-warning-bg-subtle text-status-warning-text",
64
+ LOW: "bg-bg-sunken text-text-secondary"
65
+ }, z = "w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]", B = "mb-1.5 block text-xs font-medium text-text-secondary", V = "truncate text-left text-sm font-medium text-text-primary hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] rounded";
66
+ function H(e, t) {
67
+ return t ?? e("enterprise.departments.unassigned", "Unassigned");
68
+ }
69
+ var U = ({ status: e, fmt: n, tr: r }) => /* @__PURE__ */ M("div", {
70
+ className: "space-y-1.5",
71
+ children: [/* @__PURE__ */ j(p, {
72
+ value: Math.min(e.utilizationPct, 100),
73
+ tone: e.overBudget ? "negative" : e.utilizationPct >= 85 ? "warning" : "positive"
74
+ }), /* @__PURE__ */ j("p", {
75
+ className: t("text-xs tabular-nums", e.overBudget ? "text-status-error-text" : "text-text-secondary"),
76
+ children: e.overBudget ? r("enterprise.budget.over", "{{amount}} over a {{budget}} budget", {
77
+ amount: n(-e.variance),
78
+ budget: n(e.monthlyBudget)
79
+ }) : r("enterprise.budget.under", "{{amount}} left of {{budget}} ({{pct}}% used)", {
80
+ amount: n(e.variance),
81
+ budget: n(e.monthlyBudget),
82
+ pct: e.utilizationPct
83
+ })
84
+ })]
85
+ }), W = ({ title: e, icon: t, children: n, testId: r }) => /* @__PURE__ */ M(N, {
86
+ treatment: "plain",
87
+ className: "p-6",
88
+ "data-testid": r,
89
+ children: [/* @__PURE__ */ M("div", {
90
+ className: "mb-4 flex items-center gap-2",
91
+ children: [/* @__PURE__ */ j("span", {
92
+ className: "text-text-secondary",
93
+ children: t
94
+ }), /* @__PURE__ */ j("h3", {
95
+ className: "text-sm font-semibold text-text-primary",
96
+ children: e
97
+ })]
98
+ }), n]
99
+ }), G = ({ currency: e, departments: t, onClose: n }) => {
100
+ let r = c(), a = _(), o = g(), [l, d] = y(""), [f, p] = y(""), [m, h] = y(null), v = (a.data ?? []).filter((t) => t.currency === e), b = (t, n) => {
101
+ h(null), o.mutate({
102
+ department: t,
103
+ currency: e,
104
+ monthlyAmount: n
105
+ }, {
106
+ onSuccess: () => {
107
+ p(""), d("");
108
+ },
109
+ onError: (e) => h(e instanceof Error ? e.message : r("enterprise.budgets.error", "Could not save the budget."))
110
+ });
111
+ };
112
+ return /* @__PURE__ */ M(s, {
113
+ title: r("enterprise.budgets.title", "Monthly budgets"),
114
+ description: r("enterprise.budgets.description", "Budgets apply to spend billed in {{currency}}. Leave the department empty for the company-wide budget.", { currency: e }),
115
+ onClose: n,
116
+ maxWidthClassName: "max-w-xl",
117
+ children: [/* @__PURE__ */ M("form", {
118
+ onSubmit: (e) => {
119
+ e.preventDefault();
120
+ let t = Number(f);
121
+ if (!f.trim() || !Number.isFinite(t) || t <= 0) {
122
+ h(r("enterprise.budgets.amountRequired", "Enter a monthly amount above zero."));
123
+ return;
124
+ }
125
+ b(l.trim() || null, t);
126
+ },
127
+ className: "space-y-4",
128
+ children: [m && /* @__PURE__ */ j("div", {
129
+ className: "rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text",
130
+ children: m
131
+ }), /* @__PURE__ */ M("div", {
132
+ className: "grid grid-cols-1 gap-3 sm:grid-cols-[1fr_10rem_auto] sm:items-end",
133
+ children: [
134
+ /* @__PURE__ */ M("div", { children: [
135
+ /* @__PURE__ */ j("label", {
136
+ htmlFor: "budget-department",
137
+ className: B,
138
+ children: r("enterprise.budgets.department", "Department (optional)")
139
+ }),
140
+ /* @__PURE__ */ j("input", {
141
+ id: "budget-department",
142
+ list: "budget-department-options",
143
+ value: l,
144
+ maxLength: 120,
145
+ onChange: (e) => d(e.target.value),
146
+ placeholder: r("enterprise.budgets.companyWide", "Company-wide"),
147
+ className: z
148
+ }),
149
+ /* @__PURE__ */ j("datalist", {
150
+ id: "budget-department-options",
151
+ children: t.map((e) => /* @__PURE__ */ j("option", { value: e }, e))
152
+ })
153
+ ] }),
154
+ /* @__PURE__ */ M("div", { children: [/* @__PURE__ */ j("label", {
155
+ htmlFor: "budget-amount",
156
+ className: B,
157
+ children: r("enterprise.budgets.amount", "Per month ({{currency}})", { currency: e })
158
+ }), /* @__PURE__ */ j("input", {
159
+ id: "budget-amount",
160
+ type: "number",
161
+ min: "0",
162
+ step: "0.01",
163
+ inputMode: "decimal",
164
+ value: f,
165
+ onChange: (e) => p(e.target.value),
166
+ className: z
167
+ })] }),
168
+ /* @__PURE__ */ j("button", {
169
+ type: "submit",
170
+ disabled: o.isPending,
171
+ className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-60",
172
+ children: r("enterprise.budgets.save", "Save budget")
173
+ })
174
+ ]
175
+ })]
176
+ }), /* @__PURE__ */ j("div", {
177
+ className: "mt-6",
178
+ children: a.isLoading ? /* @__PURE__ */ j(u, { label: r("enterprise.budgets.loading", "Loading budgets…") }) : v.length === 0 ? /* @__PURE__ */ j("p", {
179
+ className: "text-sm text-text-secondary",
180
+ children: r("enterprise.budgets.none", "No budgets set for {{currency}} yet.", { currency: e })
181
+ }) : /* @__PURE__ */ j("ul", {
182
+ className: "divide-y divide-border-subtle rounded-lg border border-border-subtle",
183
+ children: v.map((e) => /* @__PURE__ */ M("li", {
184
+ className: "flex items-center justify-between gap-3 px-4 py-3",
185
+ children: [/* @__PURE__ */ j("span", {
186
+ className: "truncate text-sm text-text-primary",
187
+ children: e.department ?? r("enterprise.budgets.companyWide", "Company-wide")
188
+ }), /* @__PURE__ */ M("span", {
189
+ className: "flex items-center gap-3",
190
+ children: [/* @__PURE__ */ j("span", {
191
+ className: "text-sm font-semibold tabular-nums text-text-primary",
192
+ children: i(e.monthlyAmount, e.currency)
193
+ }), /* @__PURE__ */ j("button", {
194
+ type: "button",
195
+ onClick: () => b(e.department, null),
196
+ disabled: o.isPending,
197
+ className: "text-xs font-medium text-status-error-text hover:underline disabled:opacity-60",
198
+ children: r("enterprise.budgets.remove", "Remove")
199
+ })]
200
+ })]
201
+ }, e.id))
202
+ })
203
+ })]
204
+ });
205
+ };
206
+ function K() {
207
+ let s = c(), { navigateTo: g } = e(), [_, z] = y(null), [B, K] = y(!1), q = h(_), J = q.data, Y = v(() => {
208
+ let e = J?.currency ?? "USD";
209
+ return (t) => i(t, e);
210
+ }, [J?.currency]), X = (e) => g(`/relationships/${e}`);
211
+ if (q.isLoading) return /* @__PURE__ */ j(u, { label: s("enterprise.loading", "Aggregating company-wide spend…") });
212
+ if (q.isError || !J) return /* @__PURE__ */ j(l, {
213
+ message: s("enterprise.error", "Could not load the enterprise spend dashboard."),
214
+ onRetry: () => void q.refetch()
215
+ });
216
+ let Z = J.renewalPipeline[0], Q = J.monthlySpendChangePct, $ = [J.currency, ...J.otherCurrencies.map((e) => e.currency)], ee = J.departments.map((e) => e.department).filter((e) => !!e), te = J.activeRelationshipCount === 0 && J.shadowItCount === 0 && J.riskFlagCount === 0 && J.departments.length === 0;
217
+ return /* @__PURE__ */ M(n, {
218
+ title: s("enterprise.title", "SaaS spend management"),
219
+ description: s("enterprise.description", "Company-wide recurring spend by department, with duplicate tools, shadow IT, renewals and risks."),
220
+ actions: /* @__PURE__ */ M("div", {
221
+ className: "flex items-center gap-2",
222
+ children: [$.length > 1 && /* @__PURE__ */ M("label", {
223
+ className: "flex items-center gap-2 text-xs text-text-secondary",
224
+ children: [s("enterprise.currency", "Currency"), /* @__PURE__ */ j("select", {
225
+ "aria-label": s("enterprise.currency", "Currency"),
226
+ value: J.currency,
227
+ onChange: (e) => z(e.target.value),
228
+ className: "rounded-lg border border-border-subtle bg-bg-surface px-2 py-1.5 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]",
229
+ children: $.map((e) => /* @__PURE__ */ j("option", {
230
+ value: e,
231
+ children: e
232
+ }, e))
233
+ })]
234
+ }), /* @__PURE__ */ M("button", {
235
+ type: "button",
236
+ onClick: () => K(!0),
237
+ className: "inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken",
238
+ children: [
239
+ /* @__PURE__ */ j(O, {
240
+ className: "h-4 w-4",
241
+ "aria-hidden": !0
242
+ }),
243
+ " ",
244
+ s("enterprise.manageBudgets", "Budgets")
245
+ ]
246
+ })]
247
+ }),
248
+ children: [
249
+ /* @__PURE__ */ j(P, {
250
+ className: "mb-6",
251
+ children: s("enterprise.purpose", "For finance, IT and procurement. Spend is the monthly run-rate of every subscription billing right now, computed live from your portfolio in one currency at a time. Allocate subscriptions to a department on the subscription itself.")
252
+ }),
253
+ J.truncated && /* @__PURE__ */ j("p", {
254
+ className: "mb-4 rounded-lg bg-status-warning-bg-subtle px-4 py-2 text-sm text-status-warning-text",
255
+ children: s("enterprise.truncated", "This workspace has more subscriptions than one dashboard aggregates; figures cover the most recent ones.")
256
+ }),
257
+ te ? /* @__PURE__ */ j(o, {
258
+ icon: /* @__PURE__ */ j(x, { className: "h-8 w-8" }),
259
+ title: s("enterprise.emptyTitle", "No company spend to analyse yet"),
260
+ description: s("enterprise.emptyDescription", "Add your SaaS subscriptions and allocate them to departments to see spend, duplicates, shadow IT and renewals here."),
261
+ action: /* @__PURE__ */ j("button", {
262
+ type: "button",
263
+ onClick: () => g("/relationships/new"),
264
+ className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90",
265
+ children: s("enterprise.emptyAction", "Add a subscription")
266
+ })
267
+ }) : /* @__PURE__ */ M("div", {
268
+ className: "space-y-6",
269
+ children: [
270
+ /* @__PURE__ */ M("div", {
271
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4",
272
+ "data-testid": "enterprise-kpis",
273
+ children: [
274
+ /* @__PURE__ */ j(r, {
275
+ featured: !0,
276
+ icon: /* @__PURE__ */ j(A, { className: "h-5 w-5" }),
277
+ value: Y(J.annualSpend),
278
+ label: s("enterprise.kpis.annualSpend", "Total SaaS spend / year ({{monthly}} / month)", { monthly: Y(J.monthlySpend) }),
279
+ trend: Q == null ? void 0 : Q > 0 ? "up" : Q < 0 ? "down" : "neutral",
280
+ trendValue: Q == null ? void 0 : `${Q > 0 ? "+" : ""}${Q}%`
281
+ }),
282
+ /* @__PURE__ */ j(r, {
283
+ icon: /* @__PURE__ */ j(T, { className: "h-5 w-5" }),
284
+ value: J.activeRelationshipCount,
285
+ label: s("enterprise.kpis.activeLicenses", "Active licences")
286
+ }),
287
+ /* @__PURE__ */ j(r, {
288
+ icon: /* @__PURE__ */ j(k, { className: "h-5 w-5" }),
289
+ value: J.seatUtilizationPct == null ? "—" : `${J.seatUtilizationPct}%`,
290
+ label: J.seatUtilizationPct == null ? s("enterprise.kpis.seatsUntracked", "Seats utilised (no seat data yet)") : s("enterprise.kpis.seats", "Seats utilised ({{used}} of {{assigned}})", {
291
+ used: J.seatsUsed,
292
+ assigned: J.seatsAssigned
293
+ })
294
+ }),
295
+ /* @__PURE__ */ j(r, {
296
+ icon: /* @__PURE__ */ j(S, { className: "h-5 w-5" }),
297
+ value: Z ? Z.relationshipCount : 0,
298
+ label: s("enterprise.kpis.renewals", "Renewals this quarter ({{amount}})", { amount: Y(Z?.amount ?? 0) })
299
+ })
300
+ ]
301
+ }),
302
+ /* @__PURE__ */ M("div", {
303
+ className: "grid grid-cols-1 gap-4 lg:grid-cols-3",
304
+ children: [
305
+ /* @__PURE__ */ M(W, {
306
+ title: s("enterprise.trend.title", "Monthly run-rate, last 150 days"),
307
+ icon: /* @__PURE__ */ j(A, { className: "h-4 w-4" }),
308
+ children: [/* @__PURE__ */ j(m, {
309
+ points: J.trend.map((e) => e.monthlySpend),
310
+ positive: (Q ?? 0) <= 0,
311
+ className: "w-full"
312
+ }), /* @__PURE__ */ j("p", {
313
+ className: "mt-2 text-xs text-text-secondary",
314
+ children: s("enterprise.trend.vs", "{{now}} now vs {{before}} 30 days ago", {
315
+ now: Y(J.monthlySpend),
316
+ before: Y(J.previousMonthlySpend)
317
+ })
318
+ })]
319
+ }),
320
+ /* @__PURE__ */ j(W, {
321
+ title: s("enterprise.budget.title", "Company budget"),
322
+ icon: /* @__PURE__ */ j(O, { className: "h-4 w-4" }),
323
+ testId: "enterprise-budget",
324
+ children: J.budget ? /* @__PURE__ */ j(U, {
325
+ status: J.budget,
326
+ fmt: Y,
327
+ tr: s
328
+ }) : /* @__PURE__ */ M("div", {
329
+ className: "space-y-3",
330
+ children: [/* @__PURE__ */ j("p", {
331
+ className: "text-sm text-text-secondary",
332
+ children: s("enterprise.budget.none", "No company-wide budget set for {{currency}}.", { currency: J.currency })
333
+ }), /* @__PURE__ */ j("button", {
334
+ type: "button",
335
+ onClick: () => K(!0),
336
+ className: "text-sm font-medium text-text-primary underline",
337
+ children: s("enterprise.budget.set", "Set a budget")
338
+ })]
339
+ })
340
+ }),
341
+ /* @__PURE__ */ M(W, {
342
+ title: s("enterprise.savings.title", "Consolidation opportunity"),
343
+ icon: /* @__PURE__ */ j(E, { className: "h-4 w-4" }),
344
+ children: [/* @__PURE__ */ M("p", {
345
+ className: "text-2xl font-bold tabular-nums text-text-primary",
346
+ children: [Y(J.duplicatePotentialMonthlySavings), /* @__PURE__ */ j("span", {
347
+ className: "text-sm font-normal text-text-secondary",
348
+ children: s("enterprise.perMonth", "/mo")
349
+ })]
350
+ }), /* @__PURE__ */ j("p", {
351
+ className: "mt-1 text-xs text-text-secondary",
352
+ children: s("enterprise.savings.detail", "Upper bound if each duplicated tool is consolidated into one subscription; shadow IT costs {{shadow}} / month.", { shadow: Y(J.shadowItMonthlySpend) })
353
+ })]
354
+ })
355
+ ]
356
+ }),
357
+ /* @__PURE__ */ j(d, {
358
+ title: s("enterprise.departments.title", "Spend by department"),
359
+ description: s("enterprise.departments.description", "Monthly run-rate per department, with each department budget."),
360
+ children: /* @__PURE__ */ j(N, {
361
+ treatment: "plain",
362
+ className: "p-6",
363
+ "data-testid": "enterprise-departments",
364
+ children: J.departments.length ? /* @__PURE__ */ M("div", {
365
+ className: "grid grid-cols-1 gap-6 lg:grid-cols-2",
366
+ children: [/* @__PURE__ */ j(f, {
367
+ data: J.departments.map((e) => ({
368
+ label: H(s, e.department),
369
+ value: e.monthlySpend
370
+ })),
371
+ valueFormat: Y
372
+ }), /* @__PURE__ */ j("ul", {
373
+ className: "divide-y divide-border-subtle",
374
+ children: J.departments.map((e) => /* @__PURE__ */ M("li", {
375
+ className: "py-3 first:pt-0",
376
+ children: [/* @__PURE__ */ M("div", {
377
+ className: "flex items-center justify-between gap-3 text-sm",
378
+ children: [/* @__PURE__ */ j("span", {
379
+ className: "truncate font-medium text-text-primary",
380
+ children: H(s, e.department)
381
+ }), /* @__PURE__ */ j("span", {
382
+ className: "shrink-0 tabular-nums text-text-secondary",
383
+ children: s("enterprise.departments.row", "{{count}} subs · {{share}}%", {
384
+ count: e.relationshipCount,
385
+ share: e.sharePct
386
+ })
387
+ })]
388
+ }), e.budget && /* @__PURE__ */ j("div", {
389
+ className: "mt-2",
390
+ children: /* @__PURE__ */ j(U, {
391
+ status: e.budget,
392
+ fmt: Y,
393
+ tr: s
394
+ })
395
+ })]
396
+ }, e.department ?? "__unassigned"))
397
+ })]
398
+ }) : /* @__PURE__ */ j("p", {
399
+ className: "text-sm text-text-secondary",
400
+ children: s("enterprise.departments.none", "No spend is billing right now.")
401
+ })
402
+ })
403
+ }),
404
+ /* @__PURE__ */ M("div", {
405
+ className: "grid grid-cols-1 gap-4 lg:grid-cols-2",
406
+ children: [/* @__PURE__ */ j(W, {
407
+ title: s("enterprise.duplicates.title", "Duplicate tools ({{count}})", { count: J.duplicateTools.length }),
408
+ icon: /* @__PURE__ */ j(C, { className: "h-4 w-4" }),
409
+ testId: "enterprise-duplicates",
410
+ children: J.duplicateTools.length ? /* @__PURE__ */ j("ul", {
411
+ className: "space-y-3",
412
+ children: J.duplicateTools.map((e) => /* @__PURE__ */ M("li", {
413
+ className: "rounded-lg border border-status-warning-text/30 bg-status-warning-bg-subtle p-3",
414
+ children: [
415
+ /* @__PURE__ */ M("div", {
416
+ className: "flex items-start justify-between gap-3",
417
+ children: [/* @__PURE__ */ M("div", {
418
+ className: "min-w-0",
419
+ children: [e.providerId ? /* @__PURE__ */ j("button", {
420
+ type: "button",
421
+ className: V,
422
+ onClick: () => g(`/providers/${e.providerId}`),
423
+ children: e.name
424
+ }) : /* @__PURE__ */ j("p", {
425
+ className: "truncate text-sm font-medium text-text-primary",
426
+ children: e.name
427
+ }), /* @__PURE__ */ j("p", {
428
+ className: "text-xs text-text-secondary",
429
+ children: s("enterprise.duplicates.summary", "{{count}} subscriptions · {{teams}}", {
430
+ count: e.relationshipCount,
431
+ teams: e.departments.length ? e.departments.join(", ") : s("enterprise.departments.unassigned", "Unassigned")
432
+ })
433
+ })]
434
+ }), /* @__PURE__ */ j("span", {
435
+ className: "shrink-0 text-right text-xs text-status-warning-text",
436
+ children: s("enterprise.duplicates.save", "Save up to {{amount}}/mo", { amount: Y(e.potentialMonthlySavings) })
437
+ })]
438
+ }),
439
+ /* @__PURE__ */ j("ul", {
440
+ className: "mt-2 flex flex-wrap gap-1.5",
441
+ children: e.relationships.map((e) => /* @__PURE__ */ j("li", { children: /* @__PURE__ */ M("button", {
442
+ type: "button",
443
+ onClick: () => X(e.relationshipId),
444
+ className: t("rounded-md px-2 py-1 text-xs hover:bg-bg-sunken", e.keep ? "border border-status-success-text/40 bg-status-success-bg-subtle text-status-success-text" : "bg-bg-surface text-text-primary"),
445
+ children: [
446
+ e.keep ? `${s("enterprise.duplicates.keep", "Keep")}: ` : "",
447
+ e.label,
448
+ " · ",
449
+ Y(e.monthlySpend)
450
+ ]
451
+ }) }, e.relationshipId))
452
+ }),
453
+ /* @__PURE__ */ M("p", {
454
+ className: "mt-2 text-xs text-text-secondary",
455
+ children: [
456
+ s(I[e.type].key, I[e.type].fallback),
457
+ " ·",
458
+ " ",
459
+ e.rationale
460
+ ]
461
+ })
462
+ ]
463
+ }, e.key))
464
+ }) : /* @__PURE__ */ j("p", {
465
+ className: "text-sm text-text-secondary",
466
+ children: s("enterprise.duplicates.none", "No duplicate or overlapping tools found.")
467
+ })
468
+ }), /* @__PURE__ */ j(W, {
469
+ title: s("enterprise.shadow.title", "Shadow IT ({{count}})", { count: J.shadowItCount }),
470
+ icon: /* @__PURE__ */ j(w, { className: "h-4 w-4" }),
471
+ testId: "enterprise-shadow-it",
472
+ children: J.shadowIt.length ? /* @__PURE__ */ j("ul", {
473
+ className: "divide-y divide-border-subtle",
474
+ children: J.shadowIt.map((e) => /* @__PURE__ */ M("li", {
475
+ className: "flex items-center justify-between gap-3 py-2.5",
476
+ children: [/* @__PURE__ */ M("div", {
477
+ className: "min-w-0",
478
+ children: [/* @__PURE__ */ j("button", {
479
+ type: "button",
480
+ className: V,
481
+ onClick: () => X(e.relationshipId),
482
+ children: e.label
483
+ }), /* @__PURE__ */ M("p", {
484
+ className: "text-xs text-text-secondary",
485
+ children: [
486
+ s(F[e.reason].key, F[e.reason].fallback),
487
+ " · ",
488
+ H(s, e.department),
489
+ " · ",
490
+ s("enterprise.shadow.found", "found {{date}}", { date: a(e.detectedAt) })
491
+ ]
492
+ })]
493
+ }), /* @__PURE__ */ j("span", {
494
+ className: "shrink-0 text-sm font-semibold tabular-nums text-text-primary",
495
+ children: Y(e.monthlySpend)
496
+ })]
497
+ }, e.relationshipId))
498
+ }) : /* @__PURE__ */ j("p", {
499
+ className: "text-sm text-text-secondary",
500
+ children: s("enterprise.shadow.none", "Every discovered tool is linked to an approved provider.")
501
+ })
502
+ })]
503
+ }),
504
+ /* @__PURE__ */ j(d, {
505
+ title: s("enterprise.renewals.title", "Renewal pipeline"),
506
+ description: s("enterprise.renewals.description", "Renewal charges due by quarter, for planning renegotiations."),
507
+ children: /* @__PURE__ */ j("div", {
508
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4",
509
+ "data-testid": "enterprise-renewals",
510
+ children: J.renewalPipeline.map((e) => /* @__PURE__ */ M(N, {
511
+ treatment: "plain",
512
+ className: "p-5",
513
+ children: [
514
+ /* @__PURE__ */ j("p", {
515
+ className: "text-xs font-medium uppercase tracking-wide text-text-secondary",
516
+ children: e.label
517
+ }),
518
+ /* @__PURE__ */ j("p", {
519
+ className: "mt-1 text-xl font-bold tabular-nums text-text-primary",
520
+ children: Y(e.amount)
521
+ }),
522
+ /* @__PURE__ */ j("p", {
523
+ className: "text-xs text-text-secondary",
524
+ children: s("enterprise.renewals.counts", "{{renewals}} renewals · {{subs}} subscriptions", {
525
+ renewals: e.renewalCount,
526
+ subs: e.relationshipCount
527
+ })
528
+ }),
529
+ e.topRenewals.length > 0 && /* @__PURE__ */ j("ul", {
530
+ className: "mt-3 space-y-1.5",
531
+ children: e.topRenewals.map((e) => /* @__PURE__ */ M("li", {
532
+ className: "flex items-center justify-between gap-2",
533
+ children: [/* @__PURE__ */ j("button", {
534
+ type: "button",
535
+ className: t(V, "text-xs"),
536
+ onClick: () => X(e.relationshipId),
537
+ children: e.label
538
+ }), /* @__PURE__ */ M("span", {
539
+ className: "shrink-0 text-xs tabular-nums text-text-secondary",
540
+ children: [
541
+ a(e.renewsAt),
542
+ " · ",
543
+ Y(e.amount)
544
+ ]
545
+ })]
546
+ }, e.relationshipId))
547
+ })
548
+ ]
549
+ }, e.label))
550
+ })
551
+ }),
552
+ /* @__PURE__ */ j(d, {
553
+ title: s("enterprise.risks.title", "Compliance and risk flags ({{count}})", { count: J.riskFlagCount }),
554
+ children: /* @__PURE__ */ j(N, {
555
+ treatment: "plain",
556
+ className: "p-2",
557
+ "data-testid": "enterprise-risks",
558
+ children: J.riskFlags.length ? /* @__PURE__ */ j("ul", {
559
+ className: "divide-y divide-border-subtle",
560
+ children: J.riskFlags.map((e) => /* @__PURE__ */ M("li", {
561
+ className: "flex flex-col gap-2 p-4 sm:flex-row sm:items-center sm:justify-between",
562
+ children: [/* @__PURE__ */ M("div", {
563
+ className: "flex min-w-0 items-start gap-3",
564
+ children: [/* @__PURE__ */ j("span", {
565
+ className: t("shrink-0 rounded-md px-2 py-0.5 text-xs font-medium", R[e.severity]),
566
+ children: s(L[e.kind].key, L[e.kind].fallback)
567
+ }), /* @__PURE__ */ M("div", {
568
+ className: "min-w-0",
569
+ children: [/* @__PURE__ */ j("button", {
570
+ type: "button",
571
+ className: V,
572
+ onClick: () => X(e.relationshipId),
573
+ children: e.label
574
+ }), /* @__PURE__ */ j("p", {
575
+ className: "text-xs text-text-secondary",
576
+ children: e.detail
577
+ })]
578
+ })]
579
+ }), /* @__PURE__ */ M("span", {
580
+ className: "shrink-0 text-xs text-text-secondary",
581
+ children: [
582
+ H(s, e.department),
583
+ " · ",
584
+ Y(e.monthlySpend),
585
+ s("enterprise.perMonth", "/mo")
586
+ ]
587
+ })]
588
+ }, `${e.kind}-${e.relationshipId}`))
589
+ }) : /* @__PURE__ */ M("p", {
590
+ className: "flex items-center gap-2 p-4 text-sm text-text-secondary",
591
+ children: [/* @__PURE__ */ j(D, {
592
+ className: "h-4 w-4",
593
+ "aria-hidden": !0
594
+ }), s("enterprise.risks.none", "No compliance or risk issues found.")]
595
+ })
596
+ })
597
+ }),
598
+ /* @__PURE__ */ j(d, {
599
+ title: s("enterprise.licenses.title", "Licences"),
600
+ children: /* @__PURE__ */ M(N, {
601
+ treatment: "plain",
602
+ className: "overflow-x-auto",
603
+ "data-testid": "enterprise-licenses",
604
+ children: [/* @__PURE__ */ M("table", {
605
+ className: "w-full min-w-[640px] text-sm",
606
+ children: [/* @__PURE__ */ j("thead", { children: /* @__PURE__ */ M("tr", {
607
+ className: "border-b border-border-subtle text-left text-xs text-text-secondary",
608
+ children: [
609
+ /* @__PURE__ */ j("th", {
610
+ className: "px-4 py-3 font-medium",
611
+ children: s("enterprise.licenses.app", "App")
612
+ }),
613
+ /* @__PURE__ */ j("th", {
614
+ className: "px-4 py-3 font-medium",
615
+ children: s("enterprise.licenses.department", "Department")
616
+ }),
617
+ /* @__PURE__ */ j("th", {
618
+ className: "px-4 py-3 font-medium",
619
+ children: s("enterprise.licenses.seats", "Seats used / assigned")
620
+ }),
621
+ /* @__PURE__ */ j("th", {
622
+ className: "px-4 py-3 font-medium",
623
+ children: s("enterprise.licenses.utilization", "Utilisation")
624
+ }),
625
+ /* @__PURE__ */ j("th", {
626
+ className: "px-4 py-3 text-right font-medium",
627
+ children: s("enterprise.licenses.annualCost", "Annual cost")
628
+ }),
629
+ /* @__PURE__ */ j("th", {
630
+ className: "px-4 py-3 font-medium",
631
+ children: s("enterprise.licenses.renews", "Renews")
632
+ })
633
+ ]
634
+ }) }), /* @__PURE__ */ j("tbody", {
635
+ className: "divide-y divide-border-subtle",
636
+ children: J.licenses.map((e) => /* @__PURE__ */ M("tr", { children: [
637
+ /* @__PURE__ */ M("td", {
638
+ className: "max-w-[16rem] px-4 py-3",
639
+ children: [/* @__PURE__ */ j("button", {
640
+ type: "button",
641
+ className: V,
642
+ onClick: () => X(e.relationshipId),
643
+ children: e.label
644
+ }), e.providerName && /* @__PURE__ */ j("p", {
645
+ className: "truncate text-xs text-text-secondary",
646
+ children: e.providerName
647
+ })]
648
+ }),
649
+ /* @__PURE__ */ j("td", {
650
+ className: "px-4 py-3 text-text-secondary",
651
+ children: H(s, e.department)
652
+ }),
653
+ /* @__PURE__ */ j("td", {
654
+ className: "px-4 py-3 tabular-nums text-text-primary",
655
+ children: e.seatsAssigned == null ? "—" : `${e.seatsUsed ?? 0} / ${e.seatsAssigned}`
656
+ }),
657
+ /* @__PURE__ */ j("td", {
658
+ className: "w-40 px-4 py-3",
659
+ children: e.utilizationPct == null ? /* @__PURE__ */ j("span", {
660
+ className: "text-text-secondary",
661
+ children: "—"
662
+ }) : /* @__PURE__ */ j(p, {
663
+ value: e.utilizationPct,
664
+ tone: e.utilizationPct >= 70 ? "positive" : "warning"
665
+ })
666
+ }),
667
+ /* @__PURE__ */ j("td", {
668
+ className: "px-4 py-3 text-right font-semibold tabular-nums text-text-primary",
669
+ children: Y(e.annualCost)
670
+ }),
671
+ /* @__PURE__ */ j("td", {
672
+ className: "px-4 py-3 text-text-secondary",
673
+ children: e.renewsAt ? a(e.renewsAt) : "—"
674
+ })
675
+ ] }, e.relationshipId))
676
+ })]
677
+ }), J.licenses.length === 0 && /* @__PURE__ */ j("p", {
678
+ className: "p-4 text-sm text-text-secondary",
679
+ children: s("enterprise.licenses.none", "No subscription is billing right now.")
680
+ })]
681
+ })
682
+ }),
683
+ J.otherCurrencies.length > 0 && /* @__PURE__ */ M("p", {
684
+ className: "flex items-start gap-2 text-xs text-text-secondary",
685
+ children: [/* @__PURE__ */ j(b, {
686
+ className: "mt-0.5 h-3.5 w-3.5 shrink-0",
687
+ "aria-hidden": !0
688
+ }), s("enterprise.otherCurrencies", "Not included above (amounts are never converted): {{list}}. Switch currency to see them.", { list: J.otherCurrencies.map((e) => `${i(e.monthlySpend, e.currency)}/mo`).join(", ") })]
689
+ })
690
+ ]
691
+ }),
692
+ B && /* @__PURE__ */ j(G, {
693
+ currency: J.currency,
694
+ departments: ee,
695
+ onClose: () => K(!1)
696
+ })
697
+ ]
698
+ });
699
+ }
700
+ //#endregion
701
+ export { K as EnterpriseSpendPage };
702
+
703
+ //# sourceMappingURL=EnterpriseSpendPage.js.map