@burdenoff/microfe-assethandler 2026.817.1 → 2026.817.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"PortfolioPortfolioOverviewPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/PortfolioPortfolioOverviewPage.tsx"],"names":[],"mappings":"AAiHA,wBAAgB,8BAA8B,gCA8Q7C"}
1
+ {"version":3,"file":"PortfolioPortfolioOverviewPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/PortfolioPortfolioOverviewPage.tsx"],"names":[],"mappings":"AA0HA,wBAAgB,8BAA8B,gCAwR7C"}
@@ -9,13 +9,13 @@ import { StatTile as s } from "../components/StatTile.js";
9
9
  import { ChartCard as c } from "../components/ChartCard.js";
10
10
  import "../components/index.js";
11
11
  import { useMemo as l } from "react";
12
- import { Activity as u, Building2 as d, CheckCircle2 as f, DollarSign as p, Layers as m, MapPin as h, Package as g, PieChart as _, TrendingDown as v, Wallet as y, Wrench as b } from "lucide-react";
13
- import { gql as x } from "@apollo/client";
14
- import { useQuery as ee } from "@apollo/client/react";
12
+ import { Activity as u, Building2 as d, CheckCircle2 as f, DollarSign as ee, Layers as p, MapPin as m, Package as h, PieChart as g, TrendingDown as _, Wallet as v, Wrench as y } from "lucide-react";
13
+ import { gql as b } from "@apollo/client";
14
+ import { useQuery as x } from "@apollo/client/react";
15
15
  import { Fragment as S, jsx as C, jsxs as w } from "react/jsx-runtime";
16
16
  import { GradientText as T, IllustratedEmptyState as E, PagePurpose as D, Table as O, TableBody as k, TableCell as A, TableHead as j, TableHeader as M, TableRow as N } from "@burdenoff/fe-libs/ui";
17
17
  //#region src/assethandler/pages/PortfolioPortfolioOverviewPage.tsx
18
- var P = x`
18
+ var P = b`
19
19
  query AssetHandlerFinancialSummaryOverview {
20
20
  financialSummary {
21
21
  totalAssetValue
@@ -28,36 +28,48 @@ var P = x`
28
28
  transactionExpense
29
29
  }
30
30
  }
31
- `, F = (e) => e === void 0 ? "—" : e;
32
- function I(e) {
31
+ `, F = b`
32
+ query AssetHandlerPortfolioAssetTco {
33
+ assets {
34
+ id
35
+ totalCostOfOwnership
36
+ }
37
+ }
38
+ `, I = (e) => e === void 0 ? "—" : e;
39
+ function L(e) {
33
40
  return e.replace(/_/g, " ").replace(/\b\w/g, (e) => e.toUpperCase());
34
41
  }
35
- function L(e, t) {
42
+ function R(e, t) {
36
43
  let n = e.reduce((e, n) => {
37
44
  let r = t(n);
38
45
  return e[r] = (e[r] || 0) + 1, e;
39
46
  }, {});
40
47
  return Object.entries(n).sort((e, t) => t[1] - e[1]);
41
48
  }
42
- var R = {
49
+ var z = {
43
50
  success: "bg-status-success-bg-subtle text-status-success-text",
44
51
  warning: "bg-status-warning-bg-subtle text-status-warning-text",
45
52
  error: "bg-status-error-bg-subtle text-status-error-text",
46
53
  info: "bg-status-info-bg-subtle text-status-info-text",
47
54
  neutral: "bg-bg-sunken text-text-secondary"
48
- }, z = (e) => e === "active" ? "success" : e === "maintenance" ? "warning" : e === "disposed" ? "error" : "neutral";
49
- function B({ tone: e, children: t }) {
55
+ }, B = (e) => e === "active" ? "success" : e === "maintenance" ? "warning" : e === "disposed" ? "error" : "neutral";
56
+ function V({ tone: e, children: t }) {
50
57
  return /* @__PURE__ */ C("span", {
51
- className: i("inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium capitalize whitespace-nowrap", R[e]),
58
+ className: i("inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium capitalize whitespace-nowrap", z[e]),
52
59
  children: t
53
60
  });
54
61
  }
55
- function V() {
56
- let { format: i } = n(), x = (e) => i(Math.round(e)), R = r(), { assets: V } = e(), { data: H } = ee(P, {
62
+ function H() {
63
+ let { format: i } = n(), b = (e) => i(Math.round(e)), z = r(), { assets: H } = e(), { data: U } = x(P, {
57
64
  fetchPolicy: "cache-and-network",
58
65
  errorPolicy: "all"
59
- }), U = H?.financialSummary, W = l(() => V.reduce((e, t) => e + (t.totalCostOfOwnership ?? 0), 0), [V]), G = l(() => {
60
- let e = V.reduce((e, t) => {
66
+ }), W = U?.financialSummary, { data: G } = x(F, {
67
+ fetchPolicy: "cache-and-network",
68
+ errorPolicy: "all"
69
+ }), K = l(() => {
70
+ if (G?.assets?.length) return G.assets.reduce((e, t) => e + (t.totalCostOfOwnership ?? 0), 0);
71
+ }, [G]), q = l(() => {
72
+ let e = H.reduce((e, t) => {
61
73
  let n = t.department?.trim() || "Unassigned", r = e[n] ?? {
62
74
  count: 0,
63
75
  value: 0
@@ -65,16 +77,16 @@ function V() {
65
77
  return r.count += 1, r.value += t.currentValue ?? 0, e[n] = r, e;
66
78
  }, {});
67
79
  return Object.entries(e).sort((e, t) => t[1].value - e[1].value);
68
- }, [V]), K = l(() => G.filter(([, { value: e }]) => e > 0).slice(0, 8).map(([e, { value: t }]) => ({
80
+ }, [H]), J = l(() => q.filter(([, { value: e }]) => e > 0).slice(0, 8).map(([e, { value: t }]) => ({
69
81
  department: e,
70
82
  value: Math.round(t)
71
- })), [G]), q = l(() => L(V, (e) => I(e.category)), [V]), J = l(() => L(V, (e) => e.status), [V]), Y = l(() => L(V, (e) => e.condition), [V]), X = l(() => L(V, (e) => e.location.trim() || "Unlocated"), [V]), Z = l(() => q.map(([e, t]) => ({
83
+ })), [q]), Y = l(() => R(H, (e) => L(e.category)), [H]), X = l(() => R(H, (e) => e.status), [H]), Z = l(() => R(H, (e) => e.condition), [H]), Q = l(() => R(H, (e) => e.location.trim() || "Unlocated"), [H]), te = l(() => Y.map(([e, t]) => ({
72
84
  name: e,
73
85
  value: t
74
- })), [q]), Q = l(() => J.map(([e, t]) => ({
75
- status: I(e),
86
+ })), [Y]), ne = l(() => X.map(([e, t]) => ({
87
+ status: L(e),
76
88
  count: t
77
- })), [J]), te = l(() => {
89
+ })), [X]), re = l(() => {
78
90
  let e = [
79
91
  "excellent",
80
92
  "good",
@@ -82,11 +94,11 @@ function V() {
82
94
  "poor",
83
95
  "damaged"
84
96
  ];
85
- return [...Y].sort((t, n) => e.indexOf(t[0]) - e.indexOf(n[0])).map(([e, t]) => ({
86
- condition: I(e),
97
+ return [...Z].sort((t, n) => e.indexOf(t[0]) - e.indexOf(n[0])).map(([e, t]) => ({
98
+ condition: L(e),
87
99
  count: t
88
100
  }));
89
- }, [Y]), $ = J.find(([e]) => e === "active")?.[1] ?? 0, ne = J.find(([e]) => e === "maintenance")?.[1] ?? 0, re = X.filter(([e]) => e !== "Unlocated").length;
101
+ }, [Z]), $ = X.find(([e]) => e === "active")?.[1] ?? 0, ie = X.find(([e]) => e === "maintenance")?.[1] ?? 0, ae = Q.filter(([e]) => e !== "Unlocated").length;
90
102
  return /* @__PURE__ */ w("div", {
91
103
  className: "space-y-6 p-4 sm:p-6",
92
104
  children: [
@@ -105,7 +117,7 @@ function V() {
105
117
  })
106
118
  ] }),
107
119
  /* @__PURE__ */ C(D, { children: "The big-picture view of everything you own: how many assets you have and how they break down by category, status, condition and location. Use it to inform replacement planning and audit." }),
108
- V.length === 0 ? /* @__PURE__ */ C(E, {
120
+ H.length === 0 ? /* @__PURE__ */ C(E, {
109
121
  illustration: "empty-data",
110
122
  title: "No assets in your portfolio yet",
111
123
  description: "Once you add assets, this overview will summarise them by category, status, condition and location."
@@ -116,27 +128,27 @@ function V() {
116
128
  /* @__PURE__ */ C(s, {
117
129
  hero: !0,
118
130
  label: "Total Asset Value",
119
- value: F(U?.totalAssetValue),
120
- format: x,
121
- icon: p
131
+ value: I(W?.totalAssetValue),
132
+ format: b,
133
+ icon: ee
122
134
  }),
123
135
  /* @__PURE__ */ C(s, {
124
136
  label: "Total Depreciation",
125
- value: F(U?.totalDepreciation),
126
- format: x,
127
- icon: v
137
+ value: I(W?.totalDepreciation),
138
+ format: b,
139
+ icon: _
128
140
  }),
129
141
  /* @__PURE__ */ C(s, {
130
142
  label: "Total Cost of Ownership",
131
- value: W,
132
- format: x,
133
- icon: y
143
+ value: I(K),
144
+ format: b,
145
+ icon: v
134
146
  }),
135
147
  /* @__PURE__ */ C(s, {
136
- label: R("assethandler.portfolio.metrics.totalAssets", "Total {{assets}}"),
137
- value: V.length,
148
+ label: z("assethandler.portfolio.metrics.totalAssets", "Total {{assets}}"),
149
+ value: H.length,
138
150
  format: t,
139
- icon: g
151
+ icon: h
140
152
  })
141
153
  ]
142
154
  }),
@@ -147,23 +159,23 @@ function V() {
147
159
  label: "Active",
148
160
  value: $,
149
161
  icon: f,
150
- delta: `${V.length > 0 ? Math.round($ / V.length * 100) : 0}% of estate`,
162
+ delta: `${H.length > 0 ? Math.round($ / H.length * 100) : 0}% of estate`,
151
163
  deltaTone: "neutral"
152
164
  }),
153
165
  /* @__PURE__ */ C(s, {
154
166
  label: "In Maintenance",
155
- value: ne,
156
- icon: b
167
+ value: ie,
168
+ icon: y
157
169
  }),
158
170
  /* @__PURE__ */ C(s, {
159
171
  label: "Locations",
160
- value: re,
161
- icon: h
172
+ value: ae,
173
+ icon: m
162
174
  }),
163
175
  /* @__PURE__ */ C(s, {
164
176
  label: "Categories",
165
- value: q.length,
166
- icon: m
177
+ value: Y.length,
178
+ icon: p
167
179
  })
168
180
  ]
169
181
  }),
@@ -171,11 +183,11 @@ function V() {
171
183
  className: "grid gap-6 lg:grid-cols-2",
172
184
  children: [/* @__PURE__ */ C(c, {
173
185
  title: "Assets by Category",
174
- icon: _,
186
+ icon: g,
175
187
  children: /* @__PURE__ */ C(o, {
176
- data: Z,
177
- centerLabel: R("assethandler.portfolio.donut.centerLabel", "{{assets}}"),
178
- centerValue: V.length,
188
+ data: te,
189
+ centerLabel: z("assethandler.portfolio.donut.centerLabel", "{{assets}}"),
190
+ centerValue: H.length,
179
191
  height: 280,
180
192
  ariaLabel: "Assets by category"
181
193
  })
@@ -183,12 +195,12 @@ function V() {
183
195
  title: "Value by Department",
184
196
  subtitle: "Summed current book value",
185
197
  icon: d,
186
- children: K.length === 0 ? /* @__PURE__ */ C(E, {
198
+ children: J.length === 0 ? /* @__PURE__ */ C(E, {
187
199
  illustration: "empty-data",
188
200
  title: "No department values yet",
189
201
  description: "Assign departments and record current values to see this breakdown."
190
202
  }) : /* @__PURE__ */ C(a, {
191
- data: K,
203
+ data: J,
192
204
  xKey: "department",
193
205
  series: [{
194
206
  key: "value",
@@ -196,8 +208,8 @@ function V() {
196
208
  }],
197
209
  orientation: "horizontal",
198
210
  categoryWidth: 120,
199
- height: Math.max(220, K.length * 44),
200
- valueFormat: x,
211
+ height: Math.max(220, J.length * 44),
212
+ valueFormat: b,
201
213
  ariaLabel: "Current value by department"
202
214
  })
203
215
  })]
@@ -208,7 +220,7 @@ function V() {
208
220
  title: "Assets by Status",
209
221
  icon: u,
210
222
  children: /* @__PURE__ */ C(a, {
211
- data: Q,
223
+ data: ne,
212
224
  xKey: "status",
213
225
  series: [{
214
226
  key: "count",
@@ -219,13 +231,13 @@ function V() {
219
231
  })
220
232
  }), /* @__PURE__ */ C(c, {
221
233
  title: "Assets by Location",
222
- icon: h,
234
+ icon: m,
223
235
  children: /* @__PURE__ */ C("div", {
224
236
  className: "overflow-x-auto",
225
237
  children: /* @__PURE__ */ w(O, { children: [/* @__PURE__ */ C(M, { children: /* @__PURE__ */ w(N, { children: [/* @__PURE__ */ C(j, { children: "Location" }), /* @__PURE__ */ C(j, {
226
238
  className: "text-right",
227
239
  children: "Assets"
228
- })] }) }), /* @__PURE__ */ C(k, { children: X.map(([e, t]) => /* @__PURE__ */ w(N, { children: [/* @__PURE__ */ C(A, {
240
+ })] }) }), /* @__PURE__ */ C(k, { children: Q.map(([e, t]) => /* @__PURE__ */ w(N, { children: [/* @__PURE__ */ C(A, {
229
241
  className: "font-medium",
230
242
  children: e
231
243
  }), /* @__PURE__ */ C(A, {
@@ -239,9 +251,9 @@ function V() {
239
251
  className: "grid gap-6 lg:grid-cols-2",
240
252
  children: [/* @__PURE__ */ C(c, {
241
253
  title: "Asset Condition Mix",
242
- icon: b,
254
+ icon: y,
243
255
  children: /* @__PURE__ */ C(a, {
244
- data: te,
256
+ data: re,
245
257
  xKey: "condition",
246
258
  series: [{
247
259
  key: "count",
@@ -253,7 +265,7 @@ function V() {
253
265
  }), /* @__PURE__ */ C(c, {
254
266
  title: "Department Breakdown",
255
267
  subtitle: "Assets and current value",
256
- icon: y,
268
+ icon: v,
257
269
  children: /* @__PURE__ */ C("div", {
258
270
  className: "overflow-x-auto",
259
271
  children: /* @__PURE__ */ w(O, { children: [/* @__PURE__ */ C(M, { children: /* @__PURE__ */ w(N, { children: [
@@ -266,7 +278,7 @@ function V() {
266
278
  className: "text-right",
267
279
  children: "Current Value"
268
280
  })
269
- ] }) }), /* @__PURE__ */ C(k, { children: G.map(([e, { count: t, value: n }]) => /* @__PURE__ */ w(N, { children: [
281
+ ] }) }), /* @__PURE__ */ C(k, { children: q.map(([e, { count: t, value: n }]) => /* @__PURE__ */ w(N, { children: [
270
282
  /* @__PURE__ */ C(A, {
271
283
  className: "font-medium",
272
284
  children: e
@@ -277,7 +289,7 @@ function V() {
277
289
  }),
278
290
  /* @__PURE__ */ C(A, {
279
291
  className: "text-right tabular-nums",
280
- children: x(n)
292
+ children: b(n)
281
293
  })
282
294
  ] }, e)) })] })
283
295
  })
@@ -288,10 +300,10 @@ function V() {
288
300
  icon: u,
289
301
  children: /* @__PURE__ */ C("div", {
290
302
  className: "flex flex-wrap gap-3",
291
- children: J.map(([e, t]) => /* @__PURE__ */ w("div", {
303
+ children: X.map(([e, t]) => /* @__PURE__ */ w("div", {
292
304
  className: "flex items-center gap-2 rounded-lg border border-border-seam bg-gloss-card p-3",
293
- children: [/* @__PURE__ */ C(B, {
294
- tone: z(e),
305
+ children: [/* @__PURE__ */ C(V, {
306
+ tone: B(e),
295
307
  children: e.replace(/_/g, " ")
296
308
  }), /* @__PURE__ */ C("span", {
297
309
  className: "text-lg font-bold tabular-nums",
@@ -305,6 +317,6 @@ function V() {
305
317
  });
306
318
  }
307
319
  //#endregion
308
- export { V as PortfolioPortfolioOverviewPage };
320
+ export { H as PortfolioPortfolioOverviewPage };
309
321
 
310
322
  //# sourceMappingURL=PortfolioPortfolioOverviewPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PortfolioPortfolioOverviewPage.js","names":[],"sources":["../../../src/assethandler/pages/PortfolioPortfolioOverviewPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { useAssetMoney } from '../providers/AssetHandlerProvider';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n GradientText,\n PagePurpose,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Package,\n CheckCircle2,\n Wrench,\n MapPin,\n DollarSign,\n TrendingDown,\n Wallet,\n PieChart,\n Building2,\n Activity,\n Layers,\n} from 'lucide-react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useAssetHandlerStore, type Asset } from '../store/assethandlerStore';\nimport { useCopy } from '../hooks/useCopy';\nimport { cn } from '../utils/cn';\nimport {\n StatTile,\n ChartCard,\n DonutChart,\n BarsChart,\n formatCompact,\n type ChartDatum,\n type DonutSlice,\n} from '../components';\n\n// ─── Backend financial rollup (wspace-assethandler-svc) ──────────────────────\ninterface AssetHandlerFinancialSummary {\n totalAssetValue: number;\n totalDepreciation: number;\n totalMaintenanceSpend: number;\n budgetAllocated: number;\n budgetSpent: number;\n invoicesOutstanding: number;\n invoicesOutstandingCount: number;\n transactionExpense: number;\n}\n\nconst FINANCIAL_SUMMARY_QUERY = gql`\n query AssetHandlerFinancialSummaryOverview {\n financialSummary {\n totalAssetValue\n totalDepreciation\n totalMaintenanceSpend\n budgetAllocated\n budgetSpent\n invoicesOutstanding\n invoicesOutstandingCount\n transactionExpense\n }\n }\n`;\n\nconst moneyOrDash = (value: number | undefined): number | string =>\n value === undefined ? '—' : value;\n\nfunction titleCase(value: string): string {\n return value.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction countBy(assets: Asset[], key: (a: Asset) => string): Array<[string, number]> {\n const map = assets.reduce<Record<string, number>>((acc, a) => {\n const k = key(a);\n acc[k] = (acc[k] || 0) + 1;\n return acc;\n }, {});\n return Object.entries(map).sort((a, b) => b[1] - a[1]);\n}\n\ntype PillTone = 'success' | 'warning' | 'error' | 'info' | 'neutral';\nconst PILL_CLASS: Record<PillTone, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n info: 'bg-status-info-bg-subtle text-status-info-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\nconst statusTone = (status: string): PillTone =>\n status === 'active'\n ? 'success'\n : status === 'maintenance'\n ? 'warning'\n : status === 'disposed'\n ? 'error'\n : 'neutral';\nfunction StatusPill({ tone, children }: { tone: PillTone; children: React.ReactNode }) {\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium capitalize whitespace-nowrap',\n PILL_CLASS[tone]\n )}\n >\n {children}\n </span>\n );\n}\n\nexport function PortfolioPortfolioOverviewPage() {\n const { format: money } = useAssetMoney();\n const moneyFull = (value: number) => money(Math.round(value));\n const tc = useCopy();\n const { assets } = useAssetHandlerStore();\n\n const { data: financialData } = useQuery<{ financialSummary: AssetHandlerFinancialSummary }>(\n FINANCIAL_SUMMARY_QUERY,\n { fetchPolicy: 'cache-and-network', errorPolicy: 'all' }\n );\n const financialSummary = financialData?.financialSummary;\n\n // TCO summed from store assets (undefined per-asset figures treated as 0).\n const totalCostOfOwnership = useMemo(\n () => assets.reduce((sum, a) => sum + (a.totalCostOfOwnership ?? 0), 0),\n [assets]\n );\n\n // Per-department count + summed current value, sorted by value desc.\n const byDepartment = useMemo(() => {\n const map = assets.reduce<Record<string, { count: number; value: number }>>((acc, a) => {\n const dept = a.department?.trim() || 'Unassigned';\n const entry = acc[dept] ?? { count: 0, value: 0 };\n entry.count += 1;\n entry.value += a.currentValue ?? 0;\n acc[dept] = entry;\n return acc;\n }, {});\n return Object.entries(map).sort((a, b) => b[1].value - a[1].value);\n }, [assets]);\n\n const departmentBars = useMemo<ChartDatum[]>(\n () =>\n byDepartment\n .filter(([, { value }]) => value > 0)\n .slice(0, 8)\n .map(([dept, { value }]) => ({ department: dept, value: Math.round(value) })),\n [byDepartment]\n );\n\n const byCategory = useMemo(() => countBy(assets, (a) => titleCase(a.category)), [assets]);\n const byStatus = useMemo(() => countBy(assets, (a) => a.status), [assets]);\n const byCondition = useMemo(() => countBy(assets, (a) => a.condition), [assets]);\n const byLocation = useMemo(\n () => countBy(assets, (a) => a.location.trim() || 'Unlocated'),\n [assets]\n );\n\n const categoryDonut = useMemo<DonutSlice[]>(\n () => byCategory.map(([name, value]) => ({ name, value })),\n [byCategory]\n );\n const statusBars = useMemo<ChartDatum[]>(\n () => byStatus.map(([status, count]) => ({ status: titleCase(status), count })),\n [byStatus]\n );\n const conditionBars = useMemo<ChartDatum[]>(() => {\n const order = ['excellent', 'good', 'fair', 'poor', 'damaged'];\n return [...byCondition]\n .sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0]))\n .map(([condition, count]) => ({ condition: titleCase(condition), count }));\n }, [byCondition]);\n\n const activeCount = byStatus.find(([s]) => s === 'active')?.[1] ?? 0;\n const maintenanceCount = byStatus.find(([s]) => s === 'maintenance')?.[1] ?? 0;\n const locationCount = byLocation.filter(([l]) => l !== 'Unlocated').length;\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n <div>\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Portfolio Overview\n </p>\n <h1 className=\"mt-1 text-2xl font-bold sm:text-3xl\">\n <GradientText>Portfolio Overview</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Management-level rollup of the entire estate — how it is distributed and what it is worth.\n </p>\n </div>\n\n <PagePurpose>\n The big-picture view of everything you own: how many assets you have and how they break down\n by category, status, condition and location. Use it to inform replacement planning and\n audit.\n </PagePurpose>\n\n {assets.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No assets in your portfolio yet\"\n description=\"Once you add assets, this overview will summarise them by category, status, condition and location.\"\n />\n ) : (\n <>\n {/* Primary value KPIs */}\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n hero\n label=\"Total Asset Value\"\n value={moneyOrDash(financialSummary?.totalAssetValue)}\n format={moneyFull}\n icon={DollarSign}\n />\n <StatTile\n label=\"Total Depreciation\"\n value={moneyOrDash(financialSummary?.totalDepreciation)}\n format={moneyFull}\n icon={TrendingDown}\n />\n <StatTile\n label=\"Total Cost of Ownership\"\n value={totalCostOfOwnership}\n format={moneyFull}\n icon={Wallet}\n />\n <StatTile\n label={tc('assethandler.portfolio.metrics.totalAssets', 'Total {{assets}}')}\n value={assets.length}\n format={formatCompact}\n icon={Package}\n />\n </div>\n\n {/* Secondary count KPIs */}\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n label=\"Active\"\n value={activeCount}\n icon={CheckCircle2}\n delta={`${assets.length > 0 ? Math.round((activeCount / assets.length) * 100) : 0}% of estate`}\n deltaTone=\"neutral\"\n />\n <StatTile label=\"In Maintenance\" value={maintenanceCount} icon={Wrench} />\n <StatTile label=\"Locations\" value={locationCount} icon={MapPin} />\n <StatTile label=\"Categories\" value={byCategory.length} icon={Layers} />\n </div>\n\n {/* Category donut + Value by department */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard title=\"Assets by Category\" icon={PieChart}>\n <DonutChart\n data={categoryDonut}\n centerLabel={tc('assethandler.portfolio.donut.centerLabel', '{{assets}}')}\n centerValue={assets.length}\n height={280}\n ariaLabel=\"Assets by category\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Value by Department\"\n subtitle=\"Summed current book value\"\n icon={Building2}\n >\n {departmentBars.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No department values yet\"\n description=\"Assign departments and record current values to see this breakdown.\"\n />\n ) : (\n <BarsChart\n data={departmentBars}\n xKey=\"department\"\n series={[{ key: 'value', label: 'Current value' }]}\n orientation=\"horizontal\"\n categoryWidth={120}\n height={Math.max(220, departmentBars.length * 44)}\n valueFormat={moneyFull}\n ariaLabel=\"Current value by department\"\n />\n )}\n </ChartCard>\n </div>\n\n {/* Status bars + Location table */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard title=\"Assets by Status\" icon={Activity}>\n <BarsChart\n data={statusBars}\n xKey=\"status\"\n series={[{ key: 'count', label: 'Assets' }]}\n height={260}\n ariaLabel=\"Assets by status\"\n />\n </ChartCard>\n\n <ChartCard title=\"Assets by Location\" icon={MapPin}>\n <div className=\"overflow-x-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Location</TableHead>\n <TableHead className=\"text-right\">Assets</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {byLocation.map(([location, count]) => (\n <TableRow key={location}>\n <TableCell className=\"font-medium\">{location}</TableCell>\n <TableCell className=\"text-right tabular-nums\">{count}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </ChartCard>\n </div>\n\n {/* Condition mix + Department value table */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard title=\"Asset Condition Mix\" icon={Wrench}>\n <BarsChart\n data={conditionBars}\n xKey=\"condition\"\n series={[{ key: 'count', label: 'Assets' }]}\n height={260}\n ariaLabel=\"Assets by condition\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Department Breakdown\"\n subtitle=\"Assets and current value\"\n icon={Wallet}\n >\n <div className=\"overflow-x-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Department</TableHead>\n <TableHead className=\"text-right\">Assets</TableHead>\n <TableHead className=\"text-right\">Current Value</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {byDepartment.map(([dept, { count, value }]) => (\n <TableRow key={dept}>\n <TableCell className=\"font-medium\">{dept}</TableCell>\n <TableCell className=\"text-right tabular-nums\">{count}</TableCell>\n <TableCell className=\"text-right tabular-nums\">\n {moneyFull(value)}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </ChartCard>\n </div>\n\n {/* Status legend chips */}\n <ChartCard title=\"Status Breakdown\" icon={Activity}>\n <div className=\"flex flex-wrap gap-3\">\n {byStatus.map(([status, count]) => (\n <div\n key={status}\n className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-gloss-card p-3\"\n >\n <StatusPill tone={statusTone(status)}>{status.replace(/_/g, ' ')}</StatusPill>\n <span className=\"text-lg font-bold tabular-nums\">{count}</span>\n </div>\n ))}\n </div>\n </ChartCard>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PortfolioPortfolioOverviewPage.js","names":[],"sources":["../../../src/assethandler/pages/PortfolioPortfolioOverviewPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { useAssetMoney } from '../providers/AssetHandlerProvider';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n GradientText,\n PagePurpose,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Package,\n CheckCircle2,\n Wrench,\n MapPin,\n DollarSign,\n TrendingDown,\n Wallet,\n PieChart,\n Building2,\n Activity,\n Layers,\n} from 'lucide-react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useAssetHandlerStore, type Asset } from '../store/assethandlerStore';\nimport { useCopy } from '../hooks/useCopy';\nimport { cn } from '../utils/cn';\nimport {\n StatTile,\n ChartCard,\n DonutChart,\n BarsChart,\n formatCompact,\n type ChartDatum,\n type DonutSlice,\n} from '../components';\n\n// ─── Backend financial rollup (wspace-assethandler-svc) ──────────────────────\ninterface AssetHandlerFinancialSummary {\n totalAssetValue: number;\n totalDepreciation: number;\n totalMaintenanceSpend: number;\n budgetAllocated: number;\n budgetSpent: number;\n invoicesOutstanding: number;\n invoicesOutstandingCount: number;\n transactionExpense: number;\n}\n\nconst FINANCIAL_SUMMARY_QUERY = gql`\n query AssetHandlerFinancialSummaryOverview {\n financialSummary {\n totalAssetValue\n totalDepreciation\n totalMaintenanceSpend\n budgetAllocated\n budgetSpent\n invoicesOutstanding\n invoicesOutstandingCount\n transactionExpense\n }\n }\n`;\n\nconst ASSET_TCO_QUERY = gql`\n query AssetHandlerPortfolioAssetTco {\n assets {\n id\n totalCostOfOwnership\n }\n }\n`;\n\nconst moneyOrDash = (value: number | undefined): number | string =>\n value === undefined ? '—' : value;\n\nfunction titleCase(value: string): string {\n return value.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction countBy(assets: Asset[], key: (a: Asset) => string): Array<[string, number]> {\n const map = assets.reduce<Record<string, number>>((acc, a) => {\n const k = key(a);\n acc[k] = (acc[k] || 0) + 1;\n return acc;\n }, {});\n return Object.entries(map).sort((a, b) => b[1] - a[1]);\n}\n\ntype PillTone = 'success' | 'warning' | 'error' | 'info' | 'neutral';\nconst PILL_CLASS: Record<PillTone, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n info: 'bg-status-info-bg-subtle text-status-info-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\nconst statusTone = (status: string): PillTone =>\n status === 'active'\n ? 'success'\n : status === 'maintenance'\n ? 'warning'\n : status === 'disposed'\n ? 'error'\n : 'neutral';\nfunction StatusPill({ tone, children }: { tone: PillTone; children: React.ReactNode }) {\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium capitalize whitespace-nowrap',\n PILL_CLASS[tone]\n )}\n >\n {children}\n </span>\n );\n}\n\nexport function PortfolioPortfolioOverviewPage() {\n const { format: money } = useAssetMoney();\n const moneyFull = (value: number) => money(Math.round(value));\n const tc = useCopy();\n const { assets } = useAssetHandlerStore();\n\n const { data: financialData } = useQuery<{ financialSummary: AssetHandlerFinancialSummary }>(\n FINANCIAL_SUMMARY_QUERY,\n { fetchPolicy: 'cache-and-network', errorPolicy: 'all' }\n );\n const financialSummary = financialData?.financialSummary;\n\n // TCO is fetched here rather than in the module bootstrap. Each\n // Asset.totalCostOfOwnership costs two aggregate queries server-side, so\n // selecting it for every asset is 2N database operations and blew past this\n // service's concurrency ceiling, taking the whole bootstrap — and therefore\n // every AssetHandler page — down with it (BOFF-6381, BOFF-6395). Scoping it\n // to the one page that shows the figure keeps that cost off every other\n // page, and lets this tile degrade to a dash on its own if the query fails\n // rather than reporting a silently-wrong total.\n const { data: tcoData } = useQuery<{\n assets: Array<{ id: string; totalCostOfOwnership?: number | null }>;\n }>(ASSET_TCO_QUERY, { fetchPolicy: 'cache-and-network', errorPolicy: 'all' });\n const totalCostOfOwnership = useMemo(() => {\n if (!tcoData?.assets?.length) return undefined;\n return tcoData.assets.reduce((sum, a) => sum + (a.totalCostOfOwnership ?? 0), 0);\n }, [tcoData]);\n\n // Per-department count + summed current value, sorted by value desc.\n const byDepartment = useMemo(() => {\n const map = assets.reduce<Record<string, { count: number; value: number }>>((acc, a) => {\n const dept = a.department?.trim() || 'Unassigned';\n const entry = acc[dept] ?? { count: 0, value: 0 };\n entry.count += 1;\n entry.value += a.currentValue ?? 0;\n acc[dept] = entry;\n return acc;\n }, {});\n return Object.entries(map).sort((a, b) => b[1].value - a[1].value);\n }, [assets]);\n\n const departmentBars = useMemo<ChartDatum[]>(\n () =>\n byDepartment\n .filter(([, { value }]) => value > 0)\n .slice(0, 8)\n .map(([dept, { value }]) => ({ department: dept, value: Math.round(value) })),\n [byDepartment]\n );\n\n const byCategory = useMemo(() => countBy(assets, (a) => titleCase(a.category)), [assets]);\n const byStatus = useMemo(() => countBy(assets, (a) => a.status), [assets]);\n const byCondition = useMemo(() => countBy(assets, (a) => a.condition), [assets]);\n const byLocation = useMemo(\n () => countBy(assets, (a) => a.location.trim() || 'Unlocated'),\n [assets]\n );\n\n const categoryDonut = useMemo<DonutSlice[]>(\n () => byCategory.map(([name, value]) => ({ name, value })),\n [byCategory]\n );\n const statusBars = useMemo<ChartDatum[]>(\n () => byStatus.map(([status, count]) => ({ status: titleCase(status), count })),\n [byStatus]\n );\n const conditionBars = useMemo<ChartDatum[]>(() => {\n const order = ['excellent', 'good', 'fair', 'poor', 'damaged'];\n return [...byCondition]\n .sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0]))\n .map(([condition, count]) => ({ condition: titleCase(condition), count }));\n }, [byCondition]);\n\n const activeCount = byStatus.find(([s]) => s === 'active')?.[1] ?? 0;\n const maintenanceCount = byStatus.find(([s]) => s === 'maintenance')?.[1] ?? 0;\n const locationCount = byLocation.filter(([l]) => l !== 'Unlocated').length;\n\n return (\n <div className=\"space-y-6 p-4 sm:p-6\">\n <div>\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Portfolio Overview\n </p>\n <h1 className=\"mt-1 text-2xl font-bold sm:text-3xl\">\n <GradientText>Portfolio Overview</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Management-level rollup of the entire estate — how it is distributed and what it is worth.\n </p>\n </div>\n\n <PagePurpose>\n The big-picture view of everything you own: how many assets you have and how they break down\n by category, status, condition and location. Use it to inform replacement planning and\n audit.\n </PagePurpose>\n\n {assets.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No assets in your portfolio yet\"\n description=\"Once you add assets, this overview will summarise them by category, status, condition and location.\"\n />\n ) : (\n <>\n {/* Primary value KPIs */}\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n hero\n label=\"Total Asset Value\"\n value={moneyOrDash(financialSummary?.totalAssetValue)}\n format={moneyFull}\n icon={DollarSign}\n />\n <StatTile\n label=\"Total Depreciation\"\n value={moneyOrDash(financialSummary?.totalDepreciation)}\n format={moneyFull}\n icon={TrendingDown}\n />\n <StatTile\n label=\"Total Cost of Ownership\"\n value={moneyOrDash(totalCostOfOwnership)}\n format={moneyFull}\n icon={Wallet}\n />\n <StatTile\n label={tc('assethandler.portfolio.metrics.totalAssets', 'Total {{assets}}')}\n value={assets.length}\n format={formatCompact}\n icon={Package}\n />\n </div>\n\n {/* Secondary count KPIs */}\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatTile\n label=\"Active\"\n value={activeCount}\n icon={CheckCircle2}\n delta={`${assets.length > 0 ? Math.round((activeCount / assets.length) * 100) : 0}% of estate`}\n deltaTone=\"neutral\"\n />\n <StatTile label=\"In Maintenance\" value={maintenanceCount} icon={Wrench} />\n <StatTile label=\"Locations\" value={locationCount} icon={MapPin} />\n <StatTile label=\"Categories\" value={byCategory.length} icon={Layers} />\n </div>\n\n {/* Category donut + Value by department */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard title=\"Assets by Category\" icon={PieChart}>\n <DonutChart\n data={categoryDonut}\n centerLabel={tc('assethandler.portfolio.donut.centerLabel', '{{assets}}')}\n centerValue={assets.length}\n height={280}\n ariaLabel=\"Assets by category\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Value by Department\"\n subtitle=\"Summed current book value\"\n icon={Building2}\n >\n {departmentBars.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No department values yet\"\n description=\"Assign departments and record current values to see this breakdown.\"\n />\n ) : (\n <BarsChart\n data={departmentBars}\n xKey=\"department\"\n series={[{ key: 'value', label: 'Current value' }]}\n orientation=\"horizontal\"\n categoryWidth={120}\n height={Math.max(220, departmentBars.length * 44)}\n valueFormat={moneyFull}\n ariaLabel=\"Current value by department\"\n />\n )}\n </ChartCard>\n </div>\n\n {/* Status bars + Location table */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard title=\"Assets by Status\" icon={Activity}>\n <BarsChart\n data={statusBars}\n xKey=\"status\"\n series={[{ key: 'count', label: 'Assets' }]}\n height={260}\n ariaLabel=\"Assets by status\"\n />\n </ChartCard>\n\n <ChartCard title=\"Assets by Location\" icon={MapPin}>\n <div className=\"overflow-x-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Location</TableHead>\n <TableHead className=\"text-right\">Assets</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {byLocation.map(([location, count]) => (\n <TableRow key={location}>\n <TableCell className=\"font-medium\">{location}</TableCell>\n <TableCell className=\"text-right tabular-nums\">{count}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </ChartCard>\n </div>\n\n {/* Condition mix + Department value table */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <ChartCard title=\"Asset Condition Mix\" icon={Wrench}>\n <BarsChart\n data={conditionBars}\n xKey=\"condition\"\n series={[{ key: 'count', label: 'Assets' }]}\n height={260}\n ariaLabel=\"Assets by condition\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Department Breakdown\"\n subtitle=\"Assets and current value\"\n icon={Wallet}\n >\n <div className=\"overflow-x-auto\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Department</TableHead>\n <TableHead className=\"text-right\">Assets</TableHead>\n <TableHead className=\"text-right\">Current Value</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {byDepartment.map(([dept, { count, value }]) => (\n <TableRow key={dept}>\n <TableCell className=\"font-medium\">{dept}</TableCell>\n <TableCell className=\"text-right tabular-nums\">{count}</TableCell>\n <TableCell className=\"text-right tabular-nums\">\n {moneyFull(value)}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n </ChartCard>\n </div>\n\n {/* Status legend chips */}\n <ChartCard title=\"Status Breakdown\" icon={Activity}>\n <div className=\"flex flex-wrap gap-3\">\n {byStatus.map(([status, count]) => (\n <div\n key={status}\n className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-gloss-card p-3\"\n >\n <StatusPill tone={statusTone(status)}>{status.replace(/_/g, ' ')}</StatusPill>\n <span className=\"text-lg font-bold tabular-nums\">{count}</span>\n </div>\n ))}\n </div>\n </ChartCard>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AssetHandlerProvider.d.ts","sourceRoot":"","sources":["../../../src/assethandler/providers/AssetHandlerProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAKnD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAItD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAiUpE,eAAO,MAAM,eAAe,6BAI3B,CAAC;AAEF,eAAO,MAAM,mBAAmB;;;CAAgD,CAAC;AAEjF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAO;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAMrE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,QAAO;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,MAAM,CAAC;IACrD,kEAAkE;IAClE,OAAO,EAAE,cAAc,CAAC;CAazB,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,QAAO,GAAG,CAAC,MAAM,CAGjD,CAAC;AAEF,eAAO,MAAM,4BAA4B,QAAO;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAC;CAIpD,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,EAAE,CAAC,iBAAiB,CAAC,qBAAqB,CAAC,CAwC7E,CAAC"}
1
+ {"version":3,"file":"AssetHandlerProvider.d.ts","sourceRoot":"","sources":["../../../src/assethandler/providers/AssetHandlerProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAKnD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAItD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AA6UpE,eAAO,MAAM,eAAe,6BAI3B,CAAC;AAEF,eAAO,MAAM,mBAAmB;;;CAAgD,CAAC;AAEjF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAO;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAMrE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,QAAO;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,MAAM,CAAC;IACrD,kEAAkE;IAClE,OAAO,EAAE,cAAc,CAAC;CAazB,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,QAAO,GAAG,CAAC,MAAM,CAGjD,CAAC;AAEF,eAAO,MAAM,4BAA4B,QAAO;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAC;CAIpD,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,EAAE,CAAC,iBAAiB,CAAC,qBAAqB,CAAC,CAwC7E,CAAC"}
@@ -38,7 +38,19 @@ var v = i(null), y = f`
38
38
  disposalNotes
39
39
  currentValue
40
40
  accumulatedDepreciation
41
- totalCostOfOwnership
41
+ # totalCostOfOwnership is deliberately NOT selected here. Unlike
42
+ # currentValue and accumulatedDepreciation, which are computed in-process
43
+ # from fields already on the row, Asset.totalCostOfOwnership calls
44
+ # AssetHandlerService.computeAssetTco, which issues TWO aggregate queries
45
+ # per asset (maintenanceRecord + workOrder) under Promise.all. Selecting
46
+ # it turns this one-root query into 1 + 2N database operations — 21 for
47
+ # ten assets — against a service that caps out around three (BOFF-6395),
48
+ # so it errored the batch and left the whole module empty again even
49
+ # after the bootstrap was split. It scales with the number of assets, so
50
+ # it can never be made to fit here.
51
+ #
52
+ # The only consumer that read it from the store is
53
+ # PortfolioPortfolioOverviewPage, which now fetches it itself.
42
54
  }
43
55
  }
44
56
  `, b = f`
@@ -1 +1 @@
1
- {"version":3,"file":"AssetHandlerProvider.js","names":[],"sources":["../../../src/assethandler/providers/AssetHandlerProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useEffect } from 'react';\nimport { MemoryRouter } from 'react-router';\nimport { ApolloClient, HttpLink, InMemoryCache, gql } from '@apollo/client';\nimport { ApolloProvider, useApolloClient, useQuery } from '@apollo/client/react';\nimport type { AssetHandlerRootProps } from '../types';\nimport type { AssetHandlerBootstrapPayload } from '../store/assethandlerStore';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport { DEFAULT_CURRENCY, currencySymbol, formatMoney } from '../utils/money';\nimport type { ValueFormatter } from '../components/charts/chartTheme';\nimport { formatCurrencyCompact } from '../components/charts/chartTheme';\nexport { formatAssetCategory } from '../utils/categoryPresentation';\n\ntype AssetHandlerContextValue = AssetHandlerRootProps;\n\nconst AssetHandlerContext = createContext<AssetHandlerContextValue | null>(null);\n\n/**\n * The module's data is fetched as several small queries rather than one query\n * with all thirteen root fields.\n *\n * BOFF-6381: `wspace-assethandler-svc` sets `enableRlsGucTagging: true`, so\n * be-sdk routes every Prisma call through `runOperationWithRlsGuc`, which wraps\n * it in its own interactive transaction and pins a pooled connection for the\n * duration. GraphQL resolves root fields in parallel, so the old single query\n * asked for thirteen simultaneously-pinned transactions from a service that\n * measured a hard ceiling of about three. It failed 100% of the time in\n * production — `data: null`, so `hydrateFromBootstrap` never ran and every\n * AssetHandler page rendered empty, including the machinery register.\n *\n * Measured against prod (success rate by concurrent operations):\n * 1 -> 100% 2 -> 100% 3 -> 100% 4 -> 90% 6 -> 57% 8 -> 45%\n * A sibling service without the extension was 100% at all of those, so the\n * ceiling is this service's, not the database's.\n *\n * The batches below are therefore sized to stay at or under that ceiling, and\n * the hydrator awaits them one at a time — issuing them in parallel would\n * recreate exactly the concurrency the split exists to avoid.\n * `dashboardOverview` is alone because it internally fans out to four parallel\n * counts in `AssetHandlerService.getDashboardOverview`.\n *\n * The first batch keeps the name `AssetHandlerBootstrap` so the ~36 existing\n * `refetchQueries: ['AssetHandlerBootstrap']` call sites keep working. When it\n * refetches, its new `data` identity re-runs the sequential loop, so those\n * sites still refresh the whole module, as before.\n */\n\nconst Q_ASSETS = gql`\n query AssetHandlerBootstrap {\n assets {\n id\n assetNumber\n name\n description\n category\n status\n condition\n serialNumber\n locationName\n assignedTo\n tags\n createdAt\n updatedAt\n purchaseValue\n purchaseDate\n usefulLifeMonths\n salvageValue\n depreciationMethod\n meterUnit\n department\n warrantyExpiry\n supplier\n disposalDate\n disposalMethod\n disposalNotes\n currentValue\n accumulatedDepreciation\n totalCostOfOwnership\n }\n }\n`;\n\nconst Q_OVERVIEW = gql`\n query AssetHandlerBootstrapOverview {\n dashboardOverview {\n totalAssets\n openIncidents\n openWorkOrders\n totalTransactions\n }\n }\n`;\n\nconst Q_OPS = gql`\n query AssetHandlerBootstrapOps {\n incidents {\n id\n title\n description\n category\n priority\n status\n assignedTo\n reportedBy\n reportedAt\n resolvedAt\n resolution\n slaBreached\n }\n workOrders {\n id\n title\n description\n type\n status\n priority\n assetId\n assignedTo\n createdAt\n updatedAt\n completedAt\n }\n }\n`;\n\nconst Q_MAINTENANCE = gql`\n query AssetHandlerBootstrapMaintenance {\n maintenanceRecords {\n id\n assetId\n title\n description\n type\n status\n priority\n scheduledAt\n completedAt\n estimatedCost\n actualCost\n checklist\n createdAt\n updatedAt\n }\n transactions {\n id\n type\n status\n amount\n currency\n category\n description\n vendor\n department\n occurredAt\n }\n }\n`;\n\nconst Q_ITIL = gql`\n query AssetHandlerBootstrapItil {\n changeRequests {\n id\n title\n description\n type\n status\n risk\n requestedBy\n assignedTo\n affectedAssets\n scheduledStart\n scheduledEnd\n approvals\n createdAt\n updatedAt\n }\n problems {\n id\n title\n description\n status\n priority\n category\n assignedTo\n relatedIncidents\n rootCause\n workaround\n resolution\n createdAt\n updatedAt\n }\n }\n`;\n\nconst Q_FINANCE = gql`\n query AssetHandlerBootstrapFinance {\n budgets {\n id\n name\n category\n allocated\n spent\n remaining\n period\n department\n status\n }\n invoices {\n id\n number\n vendor\n amount\n currency\n issueDate\n dueDate\n status\n items\n relatedPO\n paidAt\n }\n }\n`;\n\nconst Q_SECURITY = gql`\n query AssetHandlerBootstrapSecurity {\n securityEvents: getVaultSecurityEvents {\n items {\n id\n eventType\n severity\n title: message\n description\n source: resourceType\n userId\n ipAddress\n occurredAt: timestamp\n resolved: reviewed\n }\n }\n complianceFrameworks: securityComplianceFrameworks(filter: { sourceProduct: \"assethandler\" }) {\n items {\n id\n name\n version\n status\n score\n totalControls\n passedControls\n failedControls\n lastAssessment\n nextAssessment\n lastAssessedAt\n }\n }\n securityPolicies: securityGovernancePolicies(filter: { sourceProduct: \"assethandler\" }) {\n items {\n id\n name\n description\n category\n status\n version\n effectiveDate\n reviewDate\n owner\n applicableTo\n }\n }\n }\n`;\n\nfunction AssetHandlerBootstrapHydrator() {\n const hydrateFromBootstrap = useAssetHandlerStore((s) => s.hydrateFromBootstrap);\n const setBootstrapRefetch = useAssetHandlerStore((s) => s.setBootstrapRefetch);\n\n const client = useApolloClient();\n\n // Batch 1 stays a `useQuery` named `AssetHandlerBootstrap` so Apollo keeps it\n // as an active, named, cached query — that is what makes the existing\n // `refetchQueries: ['AssetHandlerBootstrap']` call sites work.\n const assets = useQuery<AssetHandlerBootstrapPayload>(Q_ASSETS, {\n fetchPolicy: 'cache-and-network',\n errorPolicy: 'all',\n });\n\n useEffect(() => {\n if (assets.data) hydrateFromBootstrap(assets.data);\n }, [assets.data, hydrateFromBootstrap]);\n\n // The remaining batches run strictly one after another, awaited in a loop\n // rather than as parallel hooks — running them concurrently would recreate\n // exactly the load the split exists to avoid. Hydration is additive, so a\n // batch that fails leaves its slice at the previous value instead of\n // blanking the whole module the way the single combined query did.\n useEffect(() => {\n if (!assets.data) return;\n let cancelled = false;\n void (async () => {\n for (const query of [Q_OVERVIEW, Q_OPS, Q_MAINTENANCE, Q_ITIL, Q_FINANCE, Q_SECURITY]) {\n if (cancelled) return;\n try {\n const result = await client.query<AssetHandlerBootstrapPayload>({\n query,\n fetchPolicy: 'network-only',\n errorPolicy: 'all',\n });\n if (!cancelled && result.data) hydrateFromBootstrap(result.data);\n } catch {\n // One failed section must not stop the others, and must not clear\n // what is already on screen.\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [assets.data, client, hydrateFromBootstrap]);\n\n // Refetching batch 1 is enough to refresh the module: a new `assets.data`\n // identity re-runs the sequential loop above, so the existing\n // `refetchQueries: ['AssetHandlerBootstrap']` sites still refresh everything.\n const refetchAssets = assets.refetch;\n useEffect(() => {\n setBootstrapRefetch(() => {\n void refetchAssets();\n });\n return () => setBootstrapRefetch(null);\n }, [refetchAssets, setBootstrapRefetch]);\n\n return null;\n}\n\nexport const useAssetHandler = () => {\n const context = useContext(AssetHandlerContext);\n if (!context) throw new Error('useAssetHandler must be used within AssetHandlerProvider');\n return context;\n};\n\nexport const DEFAULT_TERMINOLOGY = { asset: 'Asset', assets: 'Assets' } as const;\n\n/**\n * Resolved singular/plural noun for the module, applying consumer overrides on\n * top of the module defaults (\"Asset\"/\"Assets\").\n */\nexport const useAssetTerminology = (): { asset: string; assets: string } => {\n const { terminology } = useAssetHandler();\n return {\n asset: terminology?.asset || DEFAULT_TERMINOLOGY.asset,\n assets: terminology?.assets || DEFAULT_TERMINOLOGY.assets,\n };\n};\n\n/**\n * Resolved money presentation for the module: the consumer's `currency` prop\n * with the module default (\"USD\") applied, plus the three formatters every\n * surface needs. Amounts are never converted — see {@link formatMoney}.\n */\nexport const useAssetMoney = (): {\n currency: string;\n symbol: string;\n /** Full amount; renders an em dash for null/undefined/non-finite. */\n format: (value: number | null | undefined) => string;\n /** Compact amount for axis ticks and dense KPIs, e.g. `₹8.5L`. */\n compact: ValueFormatter;\n} => {\n const { currency } = useAssetHandler();\n const resolved = currency || DEFAULT_CURRENCY;\n return useMemo(\n () => ({\n currency: resolved,\n symbol: currencySymbol(resolved),\n format: (value: number | null | undefined) => formatMoney(value, resolved),\n compact: formatCurrencyCompact(resolved),\n }),\n [resolved]\n );\n};\n\n/** Set of category ids the consumer asked to hide (empty by default). */\nexport const useHiddenCategoryIds = (): Set<string> => {\n const { hiddenCategoryIds } = useAssetHandler();\n return useMemo(() => new Set(hiddenCategoryIds ?? []), [hiddenCategoryIds]);\n};\n\nexport const useAssetCategoryPresentation = (): {\n labels?: Record<string, string>;\n options?: AssetHandlerRootProps['categoryOptions'];\n} => {\n const { categoryLabels, categoryOptions } = useAssetHandler();\n return { labels: categoryLabels, options: categoryOptions };\n};\n\nexport const AssetHandlerProvider: FC<PropsWithChildren<AssetHandlerRootProps>> = ({\n children,\n ...props\n}) => {\n const apolloClient = useMemo(\n () =>\n new ApolloClient({\n link: new HttpLink({\n uri: props.apiGatewayUrl\n ? `${props.apiGatewayUrl}/workspaces/graphql`\n : '/workspaces/graphql',\n headers: props.authToken\n ? {\n Authorization: `Bearer ${props.authToken}`,\n ...(props.workspaceToken\n ? { 'X-Workspace-Authorization': `Bearer ${props.workspaceToken}` }\n : {}),\n }\n : undefined,\n }),\n cache: new InMemoryCache(),\n }),\n [props.apiGatewayUrl, props.authToken, props.workspaceToken]\n );\n\n const isIntegrated = !!props.basePath;\n const content = (\n <AssetHandlerContext.Provider value={props}>\n <AssetHandlerBootstrapHydrator />\n {children}\n </AssetHandlerContext.Provider>\n );\n\n return isIntegrated ? (\n content\n ) : (\n <ApolloProvider client={apolloClient}>\n <MemoryRouter>{content}</MemoryRouter>\n </ApolloProvider>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AssetHandlerProvider.js","names":[],"sources":["../../../src/assethandler/providers/AssetHandlerProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useEffect } from 'react';\nimport { MemoryRouter } from 'react-router';\nimport { ApolloClient, HttpLink, InMemoryCache, gql } from '@apollo/client';\nimport { ApolloProvider, useApolloClient, useQuery } from '@apollo/client/react';\nimport type { AssetHandlerRootProps } from '../types';\nimport type { AssetHandlerBootstrapPayload } from '../store/assethandlerStore';\nimport { useAssetHandlerStore } from '../store/assethandlerStore';\nimport { DEFAULT_CURRENCY, currencySymbol, formatMoney } from '../utils/money';\nimport type { ValueFormatter } from '../components/charts/chartTheme';\nimport { formatCurrencyCompact } from '../components/charts/chartTheme';\nexport { formatAssetCategory } from '../utils/categoryPresentation';\n\ntype AssetHandlerContextValue = AssetHandlerRootProps;\n\nconst AssetHandlerContext = createContext<AssetHandlerContextValue | null>(null);\n\n/**\n * The module's data is fetched as several small queries rather than one query\n * with all thirteen root fields.\n *\n * BOFF-6381: `wspace-assethandler-svc` sets `enableRlsGucTagging: true`, so\n * be-sdk routes every Prisma call through `runOperationWithRlsGuc`, which wraps\n * it in its own interactive transaction and pins a pooled connection for the\n * duration. GraphQL resolves root fields in parallel, so the old single query\n * asked for thirteen simultaneously-pinned transactions from a service that\n * measured a hard ceiling of about three. It failed 100% of the time in\n * production — `data: null`, so `hydrateFromBootstrap` never ran and every\n * AssetHandler page rendered empty, including the machinery register.\n *\n * Measured against prod (success rate by concurrent operations):\n * 1 -> 100% 2 -> 100% 3 -> 100% 4 -> 90% 6 -> 57% 8 -> 45%\n * A sibling service without the extension was 100% at all of those, so the\n * ceiling is this service's, not the database's.\n *\n * The batches below are therefore sized to stay at or under that ceiling, and\n * the hydrator awaits them one at a time — issuing them in parallel would\n * recreate exactly the concurrency the split exists to avoid.\n * `dashboardOverview` is alone because it internally fans out to four parallel\n * counts in `AssetHandlerService.getDashboardOverview`.\n *\n * The first batch keeps the name `AssetHandlerBootstrap` so the ~36 existing\n * `refetchQueries: ['AssetHandlerBootstrap']` call sites keep working. When it\n * refetches, its new `data` identity re-runs the sequential loop, so those\n * sites still refresh the whole module, as before.\n */\n\nconst Q_ASSETS = gql`\n query AssetHandlerBootstrap {\n assets {\n id\n assetNumber\n name\n description\n category\n status\n condition\n serialNumber\n locationName\n assignedTo\n tags\n createdAt\n updatedAt\n purchaseValue\n purchaseDate\n usefulLifeMonths\n salvageValue\n depreciationMethod\n meterUnit\n department\n warrantyExpiry\n supplier\n disposalDate\n disposalMethod\n disposalNotes\n currentValue\n accumulatedDepreciation\n # totalCostOfOwnership is deliberately NOT selected here. Unlike\n # currentValue and accumulatedDepreciation, which are computed in-process\n # from fields already on the row, Asset.totalCostOfOwnership calls\n # AssetHandlerService.computeAssetTco, which issues TWO aggregate queries\n # per asset (maintenanceRecord + workOrder) under Promise.all. Selecting\n # it turns this one-root query into 1 + 2N database operations — 21 for\n # ten assets — against a service that caps out around three (BOFF-6395),\n # so it errored the batch and left the whole module empty again even\n # after the bootstrap was split. It scales with the number of assets, so\n # it can never be made to fit here.\n #\n # The only consumer that read it from the store is\n # PortfolioPortfolioOverviewPage, which now fetches it itself.\n }\n }\n`;\n\nconst Q_OVERVIEW = gql`\n query AssetHandlerBootstrapOverview {\n dashboardOverview {\n totalAssets\n openIncidents\n openWorkOrders\n totalTransactions\n }\n }\n`;\n\nconst Q_OPS = gql`\n query AssetHandlerBootstrapOps {\n incidents {\n id\n title\n description\n category\n priority\n status\n assignedTo\n reportedBy\n reportedAt\n resolvedAt\n resolution\n slaBreached\n }\n workOrders {\n id\n title\n description\n type\n status\n priority\n assetId\n assignedTo\n createdAt\n updatedAt\n completedAt\n }\n }\n`;\n\nconst Q_MAINTENANCE = gql`\n query AssetHandlerBootstrapMaintenance {\n maintenanceRecords {\n id\n assetId\n title\n description\n type\n status\n priority\n scheduledAt\n completedAt\n estimatedCost\n actualCost\n checklist\n createdAt\n updatedAt\n }\n transactions {\n id\n type\n status\n amount\n currency\n category\n description\n vendor\n department\n occurredAt\n }\n }\n`;\n\nconst Q_ITIL = gql`\n query AssetHandlerBootstrapItil {\n changeRequests {\n id\n title\n description\n type\n status\n risk\n requestedBy\n assignedTo\n affectedAssets\n scheduledStart\n scheduledEnd\n approvals\n createdAt\n updatedAt\n }\n problems {\n id\n title\n description\n status\n priority\n category\n assignedTo\n relatedIncidents\n rootCause\n workaround\n resolution\n createdAt\n updatedAt\n }\n }\n`;\n\nconst Q_FINANCE = gql`\n query AssetHandlerBootstrapFinance {\n budgets {\n id\n name\n category\n allocated\n spent\n remaining\n period\n department\n status\n }\n invoices {\n id\n number\n vendor\n amount\n currency\n issueDate\n dueDate\n status\n items\n relatedPO\n paidAt\n }\n }\n`;\n\nconst Q_SECURITY = gql`\n query AssetHandlerBootstrapSecurity {\n securityEvents: getVaultSecurityEvents {\n items {\n id\n eventType\n severity\n title: message\n description\n source: resourceType\n userId\n ipAddress\n occurredAt: timestamp\n resolved: reviewed\n }\n }\n complianceFrameworks: securityComplianceFrameworks(filter: { sourceProduct: \"assethandler\" }) {\n items {\n id\n name\n version\n status\n score\n totalControls\n passedControls\n failedControls\n lastAssessment\n nextAssessment\n lastAssessedAt\n }\n }\n securityPolicies: securityGovernancePolicies(filter: { sourceProduct: \"assethandler\" }) {\n items {\n id\n name\n description\n category\n status\n version\n effectiveDate\n reviewDate\n owner\n applicableTo\n }\n }\n }\n`;\n\nfunction AssetHandlerBootstrapHydrator() {\n const hydrateFromBootstrap = useAssetHandlerStore((s) => s.hydrateFromBootstrap);\n const setBootstrapRefetch = useAssetHandlerStore((s) => s.setBootstrapRefetch);\n\n const client = useApolloClient();\n\n // Batch 1 stays a `useQuery` named `AssetHandlerBootstrap` so Apollo keeps it\n // as an active, named, cached query — that is what makes the existing\n // `refetchQueries: ['AssetHandlerBootstrap']` call sites work.\n const assets = useQuery<AssetHandlerBootstrapPayload>(Q_ASSETS, {\n fetchPolicy: 'cache-and-network',\n errorPolicy: 'all',\n });\n\n useEffect(() => {\n if (assets.data) hydrateFromBootstrap(assets.data);\n }, [assets.data, hydrateFromBootstrap]);\n\n // The remaining batches run strictly one after another, awaited in a loop\n // rather than as parallel hooks — running them concurrently would recreate\n // exactly the load the split exists to avoid. Hydration is additive, so a\n // batch that fails leaves its slice at the previous value instead of\n // blanking the whole module the way the single combined query did.\n useEffect(() => {\n if (!assets.data) return;\n let cancelled = false;\n void (async () => {\n for (const query of [Q_OVERVIEW, Q_OPS, Q_MAINTENANCE, Q_ITIL, Q_FINANCE, Q_SECURITY]) {\n if (cancelled) return;\n try {\n const result = await client.query<AssetHandlerBootstrapPayload>({\n query,\n fetchPolicy: 'network-only',\n errorPolicy: 'all',\n });\n if (!cancelled && result.data) hydrateFromBootstrap(result.data);\n } catch {\n // One failed section must not stop the others, and must not clear\n // what is already on screen.\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [assets.data, client, hydrateFromBootstrap]);\n\n // Refetching batch 1 is enough to refresh the module: a new `assets.data`\n // identity re-runs the sequential loop above, so the existing\n // `refetchQueries: ['AssetHandlerBootstrap']` sites still refresh everything.\n const refetchAssets = assets.refetch;\n useEffect(() => {\n setBootstrapRefetch(() => {\n void refetchAssets();\n });\n return () => setBootstrapRefetch(null);\n }, [refetchAssets, setBootstrapRefetch]);\n\n return null;\n}\n\nexport const useAssetHandler = () => {\n const context = useContext(AssetHandlerContext);\n if (!context) throw new Error('useAssetHandler must be used within AssetHandlerProvider');\n return context;\n};\n\nexport const DEFAULT_TERMINOLOGY = { asset: 'Asset', assets: 'Assets' } as const;\n\n/**\n * Resolved singular/plural noun for the module, applying consumer overrides on\n * top of the module defaults (\"Asset\"/\"Assets\").\n */\nexport const useAssetTerminology = (): { asset: string; assets: string } => {\n const { terminology } = useAssetHandler();\n return {\n asset: terminology?.asset || DEFAULT_TERMINOLOGY.asset,\n assets: terminology?.assets || DEFAULT_TERMINOLOGY.assets,\n };\n};\n\n/**\n * Resolved money presentation for the module: the consumer's `currency` prop\n * with the module default (\"USD\") applied, plus the three formatters every\n * surface needs. Amounts are never converted — see {@link formatMoney}.\n */\nexport const useAssetMoney = (): {\n currency: string;\n symbol: string;\n /** Full amount; renders an em dash for null/undefined/non-finite. */\n format: (value: number | null | undefined) => string;\n /** Compact amount for axis ticks and dense KPIs, e.g. `₹8.5L`. */\n compact: ValueFormatter;\n} => {\n const { currency } = useAssetHandler();\n const resolved = currency || DEFAULT_CURRENCY;\n return useMemo(\n () => ({\n currency: resolved,\n symbol: currencySymbol(resolved),\n format: (value: number | null | undefined) => formatMoney(value, resolved),\n compact: formatCurrencyCompact(resolved),\n }),\n [resolved]\n );\n};\n\n/** Set of category ids the consumer asked to hide (empty by default). */\nexport const useHiddenCategoryIds = (): Set<string> => {\n const { hiddenCategoryIds } = useAssetHandler();\n return useMemo(() => new Set(hiddenCategoryIds ?? []), [hiddenCategoryIds]);\n};\n\nexport const useAssetCategoryPresentation = (): {\n labels?: Record<string, string>;\n options?: AssetHandlerRootProps['categoryOptions'];\n} => {\n const { categoryLabels, categoryOptions } = useAssetHandler();\n return { labels: categoryLabels, options: categoryOptions };\n};\n\nexport const AssetHandlerProvider: FC<PropsWithChildren<AssetHandlerRootProps>> = ({\n children,\n ...props\n}) => {\n const apolloClient = useMemo(\n () =>\n new ApolloClient({\n link: new HttpLink({\n uri: props.apiGatewayUrl\n ? `${props.apiGatewayUrl}/workspaces/graphql`\n : '/workspaces/graphql',\n headers: props.authToken\n ? {\n Authorization: `Bearer ${props.authToken}`,\n ...(props.workspaceToken\n ? { 'X-Workspace-Authorization': `Bearer ${props.workspaceToken}` }\n : {}),\n }\n : undefined,\n }),\n cache: new InMemoryCache(),\n }),\n [props.apiGatewayUrl, props.authToken, props.workspaceToken]\n );\n\n const isIntegrated = !!props.basePath;\n const content = (\n <AssetHandlerContext.Provider value={props}>\n <AssetHandlerBootstrapHydrator />\n {children}\n </AssetHandlerContext.Provider>\n );\n\n return isIntegrated ? (\n content\n ) : (\n <ApolloProvider client={apolloClient}>\n <MemoryRouter>{content}</MemoryRouter>\n </ApolloProvider>\n );\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-assethandler",
3
- "version": "2026.817.1",
3
+ "version": "2026.817.2",
4
4
  "description": "Burdenoff AssetHandler Microfrontend",
5
5
  "type": "module",
6
6
  "files": [